tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

profile (9116B)


      1 #!/bin/sh
      2 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
      3 # vim: set filetype=python:
      4 
      5 # This Source Code Form is subject to the terms of the Mozilla Public
      6 # License, v. 2.0. If a copy of the MPL was not distributed with this
      7 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      8 
      9 # The beginning of this script is both valid shell and valid python,
     10 # such that the script starts with the shell and is reexecuted python
     11 '''which' mach > /dev/null 2>&1 && exec mach python "$0" "$@" ||
     12 echo "mach not found, either add it to your \$PATH or run this script via ./mach python testing/profiles/profile"; exit  # noqa
     13 '''
     14 
     15 """This script can be used to:
     16 
     17     1) Show all preferences for a given suite
     18     2) Diff preferences between two suites or profiles
     19     3) Sort preference files alphabetically for a given profile
     20 
     21 To use, either make sure that `mach` is on your $PATH, or run:
     22     $ ./mach python testing/profiles/profile <args>
     23 
     24 For more details run:
     25     $ ./profile -- --help
     26 """
     27 
     28 import json
     29 import os
     30 import sys
     31 from argparse import ArgumentParser
     32 from itertools import chain
     33 
     34 from mozprofile import Profile
     35 from mozprofile.prefs import Preferences
     36 
     37 here = os.path.abspath(os.path.dirname(__file__))
     38 
     39 try:
     40     import jsondiff
     41 except ImportError:
     42     from mozbuild.base import MozbuildObject
     43     build = MozbuildObject.from_environment(cwd=here)
     44     build.virtualenv_manager.install_pip_package("jsondiff")
     45     import jsondiff
     46 
     47 
     48 FORMAT_STRINGS = {
     49     'names': (
     50         '{pref}',
     51         '{pref}',
     52     ),
     53     'pretty': (
     54         '{pref}: {value}',
     55         '{pref}: {value_a} => {value_b}'
     56     ),
     57 }
     58 
     59 
     60 def read_prefs(profile, pref_files=None):
     61     """Read and return all preferences set in the given profile.
     62 
     63     :param profile: Profile name relative to this `here`.
     64     :returns: A dictionary of preferences set in the profile.
     65     """
     66     pref_files = pref_files or Profile.preference_file_names
     67     prefs = {}
     68     for name in pref_files:
     69         path = os.path.join(here, profile, name)
     70         if not os.path.isfile(path):
     71             continue
     72 
     73         try:
     74             prefs.update(Preferences.read_json(path))
     75         except ValueError:
     76             prefs.update(Preferences.read_prefs(path))
     77     return prefs
     78 
     79 
     80 def get_profiles(key):
     81     """Return a list of profile names for key."""
     82     with open(os.path.join(here, 'profiles.json'), 'r') as fh:
     83         profiles = json.load(fh)
     84 
     85     if '+' in key:
     86         keys = key.split('+')
     87     else:
     88         keys = [key]
     89 
     90     names = set()
     91     for key in keys:
     92         if key in profiles:
     93             names.update(profiles[key])
     94         elif os.path.isdir(os.path.join(here, key)):
     95             names.add(key)
     96 
     97     if not names:
     98         raise ValueError('{} is not a recognized suite or profile'.format(key))
     99     return names
    100 
    101 
    102 def read(key):
    103     """Read preferences relevant to either a profile or suite.
    104 
    105     :param key: Can either be the name of a profile, or the name of
    106                 a suite as defined in suites.json.
    107     """
    108     prefs = {}
    109     for profile in get_profiles(key):
    110         prefs.update(read_prefs(profile))
    111     return prefs
    112 
    113 
    114 def format_diff(diff, fmt, limit_key):
    115     """Format a diff."""
    116     indent = '  '
    117     if limit_key:
    118         diff = {limit_key: diff[limit_key]}
    119         indent = ''
    120 
    121     if fmt == 'json':
    122         print(json.dumps(diff, sort_keys=True, indent=2))
    123         return 0
    124 
    125     lines = []
    126     for key, prefs in sorted(diff.items()):
    127         if not limit_key:
    128             lines.append("{}:".format(key))
    129 
    130         for pref, value in sorted(prefs.items()):
    131             context = {'pref': pref, 'value': repr(value)}
    132 
    133             if isinstance(value, list):
    134                 context['value_a'] = repr(value[0])
    135                 context['value_b'] = repr(value[1])
    136                 text = FORMAT_STRINGS[fmt][1].format(**context)
    137             else:
    138                 text = FORMAT_STRINGS[fmt][0].format(**context)
    139 
    140             lines.append('{}{}'.format(indent, text))
    141         lines.append('')
    142     print('\n'.join(lines).strip())
    143 
    144 
    145 def diff(a, b, fmt, limit_key):
    146     """Diff two profiles or suites.
    147 
    148     :param a: The first profile or suite name.
    149     :param b: The second profile or suite name.
    150     """
    151     prefs_a = read(a)
    152     prefs_b = read(b)
    153     res = jsondiff.diff(prefs_a, prefs_b, syntax='symmetric')
    154     if not res:
    155         return 0
    156 
    157     if isinstance(res, list) and len(res) == 2:
    158         res = {
    159             jsondiff.Symbol('delete'): res[0],
    160             jsondiff.Symbol('insert'): res[1],
    161         }
    162 
    163     # Post process results to make them JSON compatible and a
    164     # bit more clear. Also calculate identical prefs.
    165     results = {}
    166     results['change'] = {k: v for k, v in res.items() if not isinstance(k, jsondiff.Symbol)}
    167 
    168     symbols = [(k, v) for k, v in res.items() if isinstance(k, jsondiff.Symbol)]
    169     results['insert'] = {k: v for sym, pref in symbols for k, v in pref.items()
    170                          if sym.label == 'insert'}
    171     results['delete'] = {k: v for sym, pref in symbols for k, v in pref.items()
    172                          if sym.label == 'delete'}
    173 
    174     same = set(prefs_a.keys()) - set(chain(*results.values()))
    175     results['same'] = {k: v for k, v in prefs_a.items() if k in same}
    176     return format_diff(results, fmt, limit_key)
    177 
    178 
    179 def read_with_comments(path):
    180     with open(path, 'r') as fh:
    181         lines = fh.readlines()
    182 
    183     result = []
    184     buf = []
    185     for line in lines:
    186         line = line.strip()
    187         if not line:
    188             continue
    189 
    190         if line.startswith('//'):
    191             buf.append(line)
    192             continue
    193 
    194         if buf:
    195             result.append(buf + [line])
    196             buf = []
    197             continue
    198 
    199         result.append([line])
    200     return result
    201 
    202 
    203 def sort_file(path):
    204     """Sort the given pref file alphabetically, preserving preceding comments
    205     that start with '//'.
    206 
    207     :param path: Path to the preference file to sort.
    208     """
    209     result = read_with_comments(path)
    210     result = sorted(result, key=lambda x: x[-1])
    211     result = chain(*result)
    212 
    213     with open(path, 'w') as fh:
    214         fh.write('\n'.join(result) + '\n')
    215 
    216 
    217 def sort(profile):
    218     """Sort all prefs in the given profile alphabetically. This will preserve
    219     comments on preceding lines.
    220 
    221     :param profile: The name of the profile to sort.
    222     """
    223     pref_files = Profile.preference_file_names
    224 
    225     for name in pref_files:
    226         path = os.path.join(here, profile, name)
    227         if os.path.isfile(path):
    228             sort_file(path)
    229 
    230 
    231 def show(suite):
    232     """Display all prefs set in profiles used by the given suite.
    233 
    234     :param suite: The name of the suite to show preferences for. This must
    235                   be a key in suites.json.
    236     """
    237     for k, v in sorted(read(suite).items()):
    238         print("{}: {}".format(k, repr(v)))
    239 
    240 
    241 def rm(profile, pref_file):
    242     if pref_file == '-':
    243         lines = sys.stdin.readlines()
    244     else:
    245         with open(pref_file, 'r') as fh:
    246             lines = fh.readlines()
    247 
    248     lines = [l.strip() for l in lines if l.strip()]
    249     if not lines:
    250         return
    251 
    252     def filter_line(content):
    253         return not any(line in content[-1] for line in lines)
    254 
    255     path = os.path.join(here, profile, 'user.js')
    256     contents = read_with_comments(path)
    257     contents = filter(filter_line, contents)
    258     contents = chain(*contents)
    259     with open(path, 'w') as fh:
    260         fh.write('\n'.join(contents))
    261 
    262 
    263 def cli(args=sys.argv[1:]):
    264     parser = ArgumentParser()
    265     subparsers = parser.add_subparsers(dest='func')
    266     subparsers.required = True
    267 
    268     diff_parser = subparsers.add_parser('diff')
    269     diff_parser.add_argument('a', metavar='A',
    270                              help="Path to the first profile or suite name to diff.")
    271     diff_parser.add_argument('b', metavar='B',
    272                              help="Path to the second profile or suite name to diff.")
    273     diff_parser.add_argument('-f', '--format', dest='fmt', default='pretty',
    274                              choices=['pretty', 'json', 'names'],
    275                              help="Format to dump diff in (default: pretty)")
    276     diff_parser.add_argument('-k', '--limit-key', default=None,
    277                              choices=['change', 'delete', 'insert', 'same'],
    278                              help="Restrict diff to the specified key.")
    279     diff_parser.set_defaults(func=diff)
    280 
    281     sort_parser = subparsers.add_parser('sort')
    282     sort_parser.add_argument('profile', help="Path to profile to sort preferences.")
    283     sort_parser.set_defaults(func=sort)
    284 
    285     show_parser = subparsers.add_parser('show')
    286     show_parser.add_argument('suite', help="Name of suite to show arguments for.")
    287     show_parser.set_defaults(func=show)
    288 
    289     rm_parser = subparsers.add_parser('rm')
    290     rm_parser.add_argument('profile', help="Name of the profile to remove prefs from.")
    291     rm_parser.add_argument('--pref-file', default='-', help="File containing a list of pref "
    292                            "substrings to delete (default: stdin)")
    293     rm_parser.set_defaults(func=rm)
    294 
    295     args = vars(parser.parse_args(args))
    296     func = args.pop('func')
    297     func(**args)
    298 
    299 
    300 if __name__ == '__main__':
    301     sys.exit(cli())