tor-browser

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

plist_util.py (7830B)


      1 # Copyright 2016 The Chromium Authors
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import argparse
      6 import codecs
      7 import plistlib
      8 import os
      9 import re
     10 import subprocess
     11 import sys
     12 import tempfile
     13 import shlex
     14 
     15 # Xcode substitutes variables like ${PRODUCT_NAME} or $(PRODUCT_NAME) when
     16 # compiling Info.plist. It also supports supports modifiers like :identifier
     17 # or :rfc1034identifier. SUBSTITUTION_REGEXP_LIST is a list of regular
     18 # expressions matching a variable substitution pattern with an optional
     19 # modifier, while INVALID_CHARACTER_REGEXP matches all characters that are
     20 # not valid in an "identifier" value (used when applying the modifier).
     21 INVALID_CHARACTER_REGEXP = re.compile(r'[_/\s]')
     22 SUBSTITUTION_REGEXP_LIST = (
     23    re.compile(r'\$\{(?P<id>[^}]*?)(?P<modifier>:[^}]*)?\}'),
     24    re.compile(r'\$\((?P<id>[^}]*?)(?P<modifier>:[^}]*)?\)'),
     25 )
     26 
     27 
     28 class SubstitutionError(Exception):
     29  def __init__(self, key):
     30    super(SubstitutionError, self).__init__()
     31    self.key = key
     32 
     33  def __str__(self):
     34    return "SubstitutionError: {}".format(self.key)
     35 
     36 
     37 def InterpolateString(value, substitutions):
     38  """Interpolates variable references into |value| using |substitutions|.
     39 
     40  Inputs:
     41    value: a string
     42    substitutions: a mapping of variable names to values
     43 
     44  Returns:
     45    A new string with all variables references ${VARIABLES} replaced by their
     46    value in |substitutions|. Raises SubstitutionError if a variable has no
     47    substitution.
     48  """
     49 
     50  def repl(match):
     51    variable = match.group('id')
     52    if variable not in substitutions:
     53      raise SubstitutionError(variable)
     54    # Some values need to be identifier and thus the variables references may
     55    # contains :modifier attributes to indicate how they should be converted
     56    # to identifiers ("identifier" replaces all invalid characters by '_' and
     57    # "rfc1034identifier" replaces them by "-" to make valid URI too).
     58    modifier = match.group('modifier')
     59    if modifier == ':identifier':
     60      return INVALID_CHARACTER_REGEXP.sub('_', substitutions[variable])
     61    elif modifier == ':rfc1034identifier':
     62      return INVALID_CHARACTER_REGEXP.sub('-', substitutions[variable])
     63    else:
     64      return substitutions[variable]
     65 
     66  for substitution_regexp in SUBSTITUTION_REGEXP_LIST:
     67    value = substitution_regexp.sub(repl, value)
     68  return value
     69 
     70 
     71 def Interpolate(value, substitutions):
     72  """Interpolates variable references into |value| using |substitutions|.
     73 
     74  Inputs:
     75    value: a value, can be a dictionary, list, string or other
     76    substitutions: a mapping of variable names to values
     77 
     78  Returns:
     79    A new value with all variables references ${VARIABLES} replaced by their
     80    value in |substitutions|. Raises SubstitutionError if a variable has no
     81    substitution.
     82  """
     83  if isinstance(value, dict):
     84    return {k: Interpolate(v, substitutions) for k, v in value.items()}
     85  if isinstance(value, list):
     86    return [Interpolate(v, substitutions) for v in value]
     87  if isinstance(value, str):
     88    return InterpolateString(value, substitutions)
     89  return value
     90 
     91 
     92 def LoadPList(path):
     93  """Loads Plist at |path| and returns it as a dictionary."""
     94  with open(path, 'rb') as f:
     95    return plistlib.load(f)
     96 
     97 
     98 def SavePList(path, format, data):
     99  """Saves |data| as a Plist to |path| in the specified |format|."""
    100  # The open() call does not replace the destination file but updates it
    101  # in place, so if more than one hardlink points to destination all of them
    102  # will be modified. This is not what is expected, so delete destination file
    103  # if it does exist.
    104  try:
    105    os.unlink(path)
    106  except FileNotFoundError:
    107    pass
    108  with open(path, 'wb') as f:
    109    plist_format = {'binary1': plistlib.FMT_BINARY, 'xml1': plistlib.FMT_XML}
    110    plistlib.dump(data, f, fmt=plist_format[format])
    111 
    112 
    113 def MergePList(plist1, plist2):
    114  """Merges |plist1| with |plist2| recursively.
    115 
    116  Creates a new dictionary representing a Property List (.plist) files by
    117  merging the two dictionary |plist1| and |plist2| recursively (only for
    118  dictionary values). List value will be concatenated.
    119 
    120  Args:
    121    plist1: a dictionary representing a Property List (.plist) file
    122    plist2: a dictionary representing a Property List (.plist) file
    123 
    124  Returns:
    125    A new dictionary representing a Property List (.plist) file by merging
    126    |plist1| with |plist2|. If any value is a dictionary, they are merged
    127    recursively, otherwise |plist2| value is used. If values are list, they
    128    are concatenated.
    129  """
    130  result = plist1.copy()
    131  for key, value in plist2.items():
    132    if isinstance(value, dict):
    133      old_value = result.get(key)
    134      if isinstance(old_value, dict):
    135        value = MergePList(old_value, value)
    136    if isinstance(value, list):
    137      value = plist1.get(key, []) + plist2.get(key, [])
    138    result[key] = value
    139  return result
    140 
    141 
    142 class Action(object):
    143  """Class implementing one action supported by the script."""
    144 
    145  @classmethod
    146  def Register(cls, subparsers):
    147    parser = subparsers.add_parser(cls.name, help=cls.help)
    148    parser.set_defaults(func=cls._Execute)
    149    cls._Register(parser)
    150 
    151 
    152 class MergeAction(Action):
    153  """Class to merge multiple plist files."""
    154 
    155  name = 'merge'
    156  help = 'merge multiple plist files'
    157 
    158  @staticmethod
    159  def _Register(parser):
    160    parser.add_argument('-o',
    161                        '--output',
    162                        required=True,
    163                        help='path to the output plist file')
    164    parser.add_argument('-f',
    165                        '--format',
    166                        required=True,
    167                        choices=('xml1', 'binary1'),
    168                        help='format of the plist file to generate')
    169    parser.add_argument(
    170        '-x',
    171        '--xcode-version',
    172        help='version of Xcode, ignored (can be used to force rebuild)')
    173    parser.add_argument('path', nargs="+", help='path to plist files to merge')
    174 
    175  @staticmethod
    176  def _Execute(args):
    177    data = {}
    178    for filename in args.path:
    179      data = MergePList(data, LoadPList(filename))
    180    SavePList(args.output, args.format, data)
    181 
    182 
    183 class SubstituteAction(Action):
    184  """Class implementing the variable substitution in a plist file."""
    185 
    186  name = 'substitute'
    187  help = 'perform pattern substitution in a plist file'
    188 
    189  @staticmethod
    190  def _Register(parser):
    191    parser.add_argument('-o',
    192                        '--output',
    193                        required=True,
    194                        help='path to the output plist file')
    195    parser.add_argument('-t',
    196                        '--template',
    197                        required=True,
    198                        help='path to the template file')
    199    parser.add_argument('-s',
    200                        '--substitution',
    201                        action='append',
    202                        default=[],
    203                        help='substitution rule in the format key=value')
    204    parser.add_argument('-f',
    205                        '--format',
    206                        required=True,
    207                        choices=('xml1', 'binary1'),
    208                        help='format of the plist file to generate')
    209    parser.add_argument(
    210        '-x',
    211        '--xcode-version',
    212        help='version of Xcode, ignored (can be used to force rebuild)')
    213 
    214  @staticmethod
    215  def _Execute(args):
    216    substitutions = {}
    217    for substitution in args.substitution:
    218      key, value = substitution.split('=', 1)
    219      substitutions[key] = value
    220    data = Interpolate(LoadPList(args.template), substitutions)
    221    SavePList(args.output, args.format, data)
    222 
    223 
    224 def Main():
    225  parser = argparse.ArgumentParser(description='manipulate plist files')
    226  subparsers = parser.add_subparsers()
    227 
    228  for action in [MergeAction, SubstituteAction]:
    229    action.Register(subparsers)
    230 
    231  args = parser.parse_args()
    232  args.func(args)
    233 
    234 
    235 if __name__ == '__main__':
    236  sys.exit(Main())