tor-browser

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

unify.py (9984B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import os
      6 import re
      7 import struct
      8 import subprocess
      9 from collections import OrderedDict
     10 from tempfile import mkstemp
     11 
     12 import buildconfig
     13 
     14 import mozpack.path as mozpath
     15 from mozbuild.util import hexdump
     16 from mozpack.errors import errors
     17 from mozpack.executables import MACHO_SIGNATURES
     18 from mozpack.files import BaseFile, BaseFinder, ExecutableFile, GeneratedFile
     19 
     20 # Regular expressions for unifying install.rdf
     21 FIND_TARGET_PLATFORM = re.compile(
     22    r"""
     23    <(?P<ns>[-._0-9A-Za-z]+:)?targetPlatform>  # The targetPlatform tag, with any namespace
     24    (?P<platform>[^<]*)                        # The actual platform value
     25    </(?P=ns)?targetPlatform>                  # The closing tag
     26    """,
     27    re.X,
     28 )
     29 FIND_TARGET_PLATFORM_ATTR = re.compile(
     30    r"""
     31    (?P<tag><(?:[-._0-9A-Za-z]+:)?Description) # The opening part of the <Description> tag
     32    (?P<attrs>[^>]*?)\s+                       # The initial attributes
     33    (?P<ns>[-._0-9A-Za-z]+:)?targetPlatform=   # The targetPlatform attribute, with any namespace
     34    [\'"](?P<platform>[^\'"]+)[\'"]            # The actual platform value
     35    (?P<otherattrs>[^>]*?>)                    # The remaining attributes and closing angle bracket
     36    """,
     37    re.X,
     38 )
     39 
     40 
     41 def may_unify_binary(file):
     42    """
     43    Return whether the given BaseFile instance is an ExecutableFile that
     44    may be unified. Only non-fat Mach-O binaries are to be unified.
     45    """
     46    if isinstance(file, ExecutableFile):
     47        signature = file.open().read(4)
     48        if len(signature) < 4:
     49            return False
     50        signature = struct.unpack(">L", signature)[0]
     51        if signature in MACHO_SIGNATURES:
     52            return True
     53    return False
     54 
     55 
     56 class UnifiedExecutableFile(BaseFile):
     57    """
     58    File class for executable and library files that to be unified with 'lipo'.
     59    """
     60 
     61    def __init__(self, executable1, executable2):
     62        """
     63        Initialize a UnifiedExecutableFile with a pair of ExecutableFiles to
     64        be unified. They are expected to be non-fat Mach-O executables.
     65        """
     66        assert isinstance(executable1, ExecutableFile)
     67        assert isinstance(executable2, ExecutableFile)
     68        self._executables = (executable1, executable2)
     69 
     70    def copy(self, dest, skip_if_older=True):
     71        """
     72        Create a fat executable from the two Mach-O executable given when
     73        creating the instance.
     74        skip_if_older is ignored.
     75        """
     76        assert isinstance(dest, str)
     77        tmpfiles = []
     78        try:
     79            for e in self._executables:
     80                fd, f = mkstemp()
     81                os.close(fd)
     82                tmpfiles.append(f)
     83                e.copy(f, skip_if_older=False)
     84            lipo = buildconfig.substs.get("LIPO") or "lipo"
     85            subprocess.check_call([lipo, "-create"] + tmpfiles + ["-output", dest])
     86        except Exception as e:
     87            errors.error(
     88                "Failed to unify %s and %s: %s"
     89                % (self._executables[0].path, self._executables[1].path, str(e))
     90            )
     91        finally:
     92            for f in tmpfiles:
     93                os.unlink(f)
     94 
     95 
     96 class UnifiedFinder(BaseFinder):
     97    """
     98    Helper to get unified BaseFile instances from two distinct trees on the
     99    file system.
    100    """
    101 
    102    def __init__(self, finder1, finder2, sorted=[], **kargs):
    103        """
    104        Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder
    105        instances from which files are picked. UnifiedFinder.find() will act as
    106        FileFinder.find() but will error out when matches can only be found in
    107        one of the two trees and not the other. It will also error out if
    108        matches can be found on both ends but their contents are not identical.
    109 
    110        The sorted argument gives a list of mozpath.match patterns. File
    111        paths matching one of these patterns will have their contents compared
    112        with their lines sorted.
    113        """
    114        assert isinstance(finder1, BaseFinder)
    115        assert isinstance(finder2, BaseFinder)
    116        self._finder1 = finder1
    117        self._finder2 = finder2
    118        self._sorted = sorted
    119        BaseFinder.__init__(self, finder1.base, **kargs)
    120 
    121    def _find(self, path):
    122        """
    123        UnifiedFinder.find() implementation.
    124        """
    125        # There is no `OrderedSet`.  Operator `|` was added only in
    126        # Python 3.9, so we merge by hand.
    127        all_paths = OrderedDict()
    128 
    129        files1 = OrderedDict()
    130        for p, f in self._finder1.find(path):
    131            files1[p] = f
    132            all_paths[p] = True
    133        files2 = OrderedDict()
    134        for p, f in self._finder2.find(path):
    135            files2[p] = f
    136            all_paths[p] = True
    137 
    138        for p in all_paths:
    139            err = errors.count
    140            unified = self.unify_file(p, files1.get(p), files2.get(p))
    141            if unified:
    142                yield p, unified
    143            elif err == errors.count:  # No errors have already been reported.
    144                self._report_difference(p, files1.get(p), files2.get(p))
    145 
    146    def _report_difference(self, path, file1, file2):
    147        """
    148        Report differences between files in both trees.
    149        """
    150        if not file1:
    151            errors.error("File missing in %s: %s" % (self._finder1.base, path))
    152            return
    153        if not file2:
    154            errors.error("File missing in %s: %s" % (self._finder2.base, path))
    155            return
    156 
    157        errors.error(
    158            "Can't unify %s: file differs between %s and %s"
    159            % (path, self._finder1.base, self._finder2.base)
    160        )
    161        if not isinstance(file1, ExecutableFile) and not isinstance(
    162            file2, ExecutableFile
    163        ):
    164            from difflib import unified_diff
    165 
    166            try:
    167                lines1 = [l.decode("utf-8") for l in file1.open().readlines()]
    168                lines2 = [l.decode("utf-8") for l in file2.open().readlines()]
    169            except UnicodeDecodeError:
    170                lines1 = hexdump(file1.open().read())
    171                lines2 = hexdump(file2.open().read())
    172 
    173            for line in unified_diff(
    174                lines1,
    175                lines2,
    176                os.path.join(self._finder1.base, path),
    177                os.path.join(self._finder2.base, path),
    178            ):
    179                errors.out.write(line)
    180 
    181    def unify_file(self, path, file1, file2):
    182        """
    183        Given two BaseFiles and the path they were found at, return a
    184        unified version of the files. If the files match, the first BaseFile
    185        may be returned.
    186        If the files don't match or one of them is `None`, the method returns
    187        `None`.
    188        Subclasses may decide to unify by using one of the files in that case.
    189        """
    190        if not file1 or not file2:
    191            return None
    192 
    193        if may_unify_binary(file1) and may_unify_binary(file2):
    194            return UnifiedExecutableFile(file1, file2)
    195 
    196        content1 = file1.open().readlines()
    197        content2 = file2.open().readlines()
    198        if content1 == content2:
    199            return file1
    200        for pattern in self._sorted:
    201            if mozpath.match(path, pattern):
    202                if sorted(content1) == sorted(content2):
    203                    return file1
    204                break
    205        return None
    206 
    207 
    208 class UnifiedBuildFinder(UnifiedFinder):
    209    """
    210    Specialized UnifiedFinder for Mozilla applications packaging. It allows
    211    ``*.manifest`` files to differ in their order, and unifies ``buildconfig.html``
    212    files by merging their content.
    213    """
    214 
    215    def __init__(self, finder1, finder2, **kargs):
    216        UnifiedFinder.__init__(
    217            self, finder1, finder2, sorted=["**/*.manifest"], **kargs
    218        )
    219 
    220    def unify_file(self, path, file1, file2):
    221        """
    222        Unify files taking Mozilla application special cases into account.
    223        Otherwise defer to UnifiedFinder.unify_file.
    224        """
    225        basename = mozpath.basename(path)
    226        if file1 and file2 and basename == "buildconfig.html":
    227            content1 = file1.open().readlines()
    228            content2 = file2.open().readlines()
    229            # Copy everything from the first file up to the end of its <div>,
    230            # insert a <hr> between the two files and copy the second file's
    231            # content beginning after its leading <h1>.
    232            return GeneratedFile(
    233                b"".join(
    234                    content1[: content1.index(b"    </div>\n")]
    235                    + [b"      <hr> </hr>\n"]
    236                    + content2[
    237                        content2.index(b"      <h1>Build Configuration</h1>\n") + 1 :
    238                    ]
    239                )
    240            )
    241        elif file1 and file2 and basename == "install.rdf":
    242            # install.rdf files often have em:targetPlatform (either as
    243            # attribute or as tag) that will differ between platforms. The
    244            # unified install.rdf should contain both em:targetPlatforms if
    245            # they exist, or strip them if only one file has a target platform.
    246            content1, content2 = (
    247                FIND_TARGET_PLATFORM_ATTR.sub(
    248                    lambda m: m.group("tag")
    249                    + m.group("attrs")
    250                    + m.group("otherattrs")
    251                    + "<%stargetPlatform>%s</%stargetPlatform>"
    252                    % (m.group("ns") or "", m.group("platform"), m.group("ns") or ""),
    253                    f.open().read().decode("utf-8"),
    254                )
    255                for f in (file1, file2)
    256            )
    257 
    258            platform2 = FIND_TARGET_PLATFORM.search(content2)
    259            return GeneratedFile(
    260                FIND_TARGET_PLATFORM.sub(
    261                    lambda m: m.group(0) + platform2.group(0) if platform2 else "",
    262                    content1,
    263                )
    264            )
    265        return UnifiedFinder.unify_file(self, path, file1, file2)