tor-browser

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

mozjar.py (29180B)


      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 functools
      6 import os
      7 import struct
      8 import zlib
      9 from collections import OrderedDict
     10 from io import BytesIO, UnsupportedOperation
     11 from zipfile import ZIP_DEFLATED, ZIP_STORED
     12 
     13 import mozpack.path as mozpath
     14 from mozbuild.util import ensure_bytes
     15 
     16 JAR_STORED = ZIP_STORED
     17 JAR_DEFLATED = ZIP_DEFLATED
     18 MAX_WBITS = 15
     19 
     20 
     21 class JarReaderError(Exception):
     22    """Error type for Jar reader errors."""
     23 
     24 
     25 class JarWriterError(Exception):
     26    """Error type for Jar writer errors."""
     27 
     28 
     29 class JarStruct:
     30    """
     31    Helper used to define ZIP archive raw data structures. Data structures
     32    handled by this helper all start with a magic number, defined in
     33    subclasses MAGIC field as a 32-bits unsigned integer, followed by data
     34    structured as described in subclasses STRUCT field.
     35 
     36    The STRUCT field contains a list of (name, type) pairs where name is a
     37    field name, and the type can be one of 'uint32', 'uint16' or one of the
     38    field names. In the latter case, the field is considered to be a string
     39    buffer with a length given in that field.
     40    For example,
     41 
     42    .. code-block:: python
     43 
     44        STRUCT = [
     45            ('version', 'uint32'),
     46            ('filename_size', 'uint16'),
     47            ('filename', 'filename_size')
     48        ]
     49 
     50    describes a structure with a 'version' 32-bits unsigned integer field,
     51    followed by a 'filename_size' 16-bits unsigned integer field, followed by a
     52    filename_size-long string buffer 'filename'.
     53 
     54    Fields that are used as other fields size are not stored in objects. In the
     55    above example, an instance of such subclass would only have two attributes:
     56      - obj['version']
     57      - obj['filename']
     58 
     59    filename_size would be obtained with len(obj['filename']).
     60 
     61    JarStruct subclasses instances can be either initialized from existing data
     62    (deserialized), or with empty fields.
     63    """
     64 
     65    TYPE_MAPPING = {"uint32": (b"I", 4), "uint16": (b"H", 2)}
     66 
     67    def __init__(self, data=None):
     68        """
     69        Create an instance from the given data. Data may be omitted to create
     70        an instance with empty fields.
     71        """
     72        assert self.MAGIC and isinstance(self.STRUCT, OrderedDict)
     73        self.size_fields = set(
     74            t for t in self.STRUCT.values() if t not in JarStruct.TYPE_MAPPING
     75        )
     76        self._values = {}
     77        if data:
     78            self._init_data(data)
     79        else:
     80            self._init_empty()
     81 
     82    def _init_data(self, data):
     83        """
     84        Initialize an instance from data, following the data structure
     85        described in self.STRUCT. The self.MAGIC signature is expected at
     86        data[:4].
     87        """
     88        assert data is not None
     89        self.signature, size = JarStruct.get_data("uint32", data)
     90        if self.signature != self.MAGIC:
     91            raise JarReaderError("Bad magic")
     92        offset = size
     93        # For all fields used as other fields sizes, keep track of their value
     94        # separately.
     95        sizes = dict((t, 0) for t in self.size_fields)
     96        for name, t in self.STRUCT.items():
     97            if t in JarStruct.TYPE_MAPPING:
     98                value, size = JarStruct.get_data(t, data[offset:])
     99            else:
    100                size = sizes[t]
    101                value = data[offset : offset + size]
    102                if isinstance(value, memoryview):
    103                    value = value.tobytes()
    104            if name not in sizes:
    105                self._values[name] = value
    106            else:
    107                sizes[name] = value
    108            offset += size
    109 
    110    def _init_empty(self):
    111        """
    112        Initialize an instance with empty fields.
    113        """
    114        self.signature = self.MAGIC
    115        for name, t in self.STRUCT.items():
    116            if name in self.size_fields:
    117                continue
    118            self._values[name] = 0 if t in JarStruct.TYPE_MAPPING else ""
    119 
    120    @staticmethod
    121    def get_data(type, data):
    122        """
    123        Deserialize a single field of given type (must be one of
    124        JarStruct.TYPE_MAPPING) at the given offset in the given data.
    125        """
    126        assert type in JarStruct.TYPE_MAPPING
    127        assert data is not None
    128        format, size = JarStruct.TYPE_MAPPING[type]
    129        data = data[:size]
    130        if isinstance(data, memoryview):
    131            data = data.tobytes()
    132        return struct.unpack(b"<" + format, data)[0], size
    133 
    134    def serialize(self):
    135        """
    136        Serialize the data structure according to the data structure definition
    137        from self.STRUCT.
    138        """
    139        serialized = struct.pack(b"<I", self.signature)
    140        sizes = dict(
    141            (t, name)
    142            for name, t in self.STRUCT.items()
    143            if t not in JarStruct.TYPE_MAPPING
    144        )
    145        for name, t in self.STRUCT.items():
    146            if t in JarStruct.TYPE_MAPPING:
    147                format, size = JarStruct.TYPE_MAPPING[t]
    148                if name in sizes:
    149                    value = len(self[sizes[name]])
    150                else:
    151                    value = self[name]
    152                serialized += struct.pack(b"<" + format, value)
    153            else:
    154                serialized += ensure_bytes(self[name])
    155        return serialized
    156 
    157    @property
    158    def size(self):
    159        """
    160        Return the size of the data structure, given the current values of all
    161        variable length fields.
    162        """
    163        size = JarStruct.TYPE_MAPPING["uint32"][1]
    164        for name, type in self.STRUCT.items():
    165            if type in JarStruct.TYPE_MAPPING:
    166                size += JarStruct.TYPE_MAPPING[type][1]
    167            else:
    168                size += len(self[name])
    169        return size
    170 
    171    def __getitem__(self, key):
    172        return self._values[key]
    173 
    174    def __setitem__(self, key, value):
    175        if key not in self.STRUCT:
    176            raise KeyError(key)
    177        if key in self.size_fields:
    178            raise AttributeError("can't set attribute")
    179        self._values[key] = value
    180 
    181    def __contains__(self, key):
    182        return key in self._values
    183 
    184    def __iter__(self):
    185        return iter(self._values.items())
    186 
    187    def __repr__(self):
    188        return "<%s %s>" % (
    189            self.__class__.__name__,
    190            " ".join("%s=%s" % (n, v) for n, v in self),
    191        )
    192 
    193 
    194 class JarCdirEnd(JarStruct):
    195    """
    196    End of central directory record.
    197    """
    198 
    199    MAGIC = 0x06054B50
    200    STRUCT = OrderedDict([
    201        ("disk_num", "uint16"),
    202        ("cdir_disk", "uint16"),
    203        ("disk_entries", "uint16"),
    204        ("cdir_entries", "uint16"),
    205        ("cdir_size", "uint32"),
    206        ("cdir_offset", "uint32"),
    207        ("comment_size", "uint16"),
    208        ("comment", "comment_size"),
    209    ])
    210 
    211 
    212 CDIR_END_SIZE = JarCdirEnd().size
    213 
    214 
    215 class JarCdirEntry(JarStruct):
    216    """
    217    Central directory file header
    218    """
    219 
    220    MAGIC = 0x02014B50
    221    STRUCT = OrderedDict([
    222        ("creator_version", "uint16"),
    223        ("min_version", "uint16"),
    224        ("general_flag", "uint16"),
    225        ("compression", "uint16"),
    226        ("lastmod_time", "uint16"),
    227        ("lastmod_date", "uint16"),
    228        ("crc32", "uint32"),
    229        ("compressed_size", "uint32"),
    230        ("uncompressed_size", "uint32"),
    231        ("filename_size", "uint16"),
    232        ("extrafield_size", "uint16"),
    233        ("filecomment_size", "uint16"),
    234        ("disknum", "uint16"),
    235        ("internal_attr", "uint16"),
    236        ("external_attr", "uint32"),
    237        ("offset", "uint32"),
    238        ("filename", "filename_size"),
    239        ("extrafield", "extrafield_size"),
    240        ("filecomment", "filecomment_size"),
    241    ])
    242 
    243 
    244 class JarLocalFileHeader(JarStruct):
    245    """
    246    Local file header
    247    """
    248 
    249    MAGIC = 0x04034B50
    250    STRUCT = OrderedDict([
    251        ("min_version", "uint16"),
    252        ("general_flag", "uint16"),
    253        ("compression", "uint16"),
    254        ("lastmod_time", "uint16"),
    255        ("lastmod_date", "uint16"),
    256        ("crc32", "uint32"),
    257        ("compressed_size", "uint32"),
    258        ("uncompressed_size", "uint32"),
    259        ("filename_size", "uint16"),
    260        ("extra_field_size", "uint16"),
    261        ("filename", "filename_size"),
    262        ("extra_field", "extra_field_size"),
    263    ])
    264 
    265 
    266 class JarFileReader:
    267    """
    268    File-like class for use by JarReader to give access to individual files
    269    within a Jar archive.
    270    """
    271 
    272    def __init__(self, header, data):
    273        """
    274        Initialize a JarFileReader. header is the local file header
    275        corresponding to the file in the jar archive, data a buffer containing
    276        the file data.
    277        """
    278        assert header["compression"] in [JAR_DEFLATED, JAR_STORED]
    279        self._data = data
    280        # Copy some local file header fields.
    281        for name in ["compressed_size", "uncompressed_size", "crc32"]:
    282            setattr(self, name, header[name])
    283        self.filename = header["filename"].decode()
    284        self.compressed = header["compression"] != JAR_STORED
    285        self.compress = header["compression"]
    286 
    287    def readable(self):
    288        return True
    289 
    290    def read(self, length=-1):
    291        """
    292        Read some amount of uncompressed data.
    293        """
    294        return self.uncompressed_data.read(length)
    295 
    296    def readinto(self, b):
    297        """
    298        Read bytes into a pre-allocated, writable bytes-like object `b` and return
    299        the number of bytes read.
    300        """
    301        return self.uncompressed_data.readinto(b)
    302 
    303    def readlines(self):
    304        """
    305        Return a list containing all the lines of data in the uncompressed
    306        data.
    307        """
    308        return self.read().splitlines(True)
    309 
    310    def __iter__(self):
    311        """
    312        Iterator, to support the "for line in fileobj" constructs.
    313        """
    314        return iter(self.readlines())
    315 
    316    def seek(self, pos, whence=os.SEEK_SET):
    317        """
    318        Change the current position in the uncompressed data. Subsequent reads
    319        will start from there.
    320        """
    321        return self.uncompressed_data.seek(pos, whence)
    322 
    323    def close(self):
    324        """
    325        Free the uncompressed data buffer.
    326        """
    327        self.uncompressed_data.close()
    328 
    329    @property
    330    def closed(self):
    331        return self.uncompressed_data.closed
    332 
    333    @property
    334    def compressed_data(self):
    335        """
    336        Return the raw compressed data.
    337        """
    338        return self._data[: self.compressed_size]
    339 
    340    @property
    341    def uncompressed_data(self):
    342        """
    343        Return the uncompressed data.
    344        """
    345        if hasattr(self, "_uncompressed_data"):
    346            return self._uncompressed_data
    347        data = self.compressed_data
    348        if self.compress == JAR_STORED:
    349            data = data.tobytes()
    350        elif self.compress == JAR_DEFLATED:
    351            data = zlib.decompress(data.tobytes(), -MAX_WBITS)
    352        else:
    353            assert False  # Can't be another value per __init__
    354        if len(data) != self.uncompressed_size:
    355            raise JarReaderError("Corrupted file? %s" % self.filename)
    356        self._uncompressed_data = BytesIO(data)
    357        return self._uncompressed_data
    358 
    359 
    360 class JarReader:
    361    """
    362    Class with methods to read Jar files. Can open standard jar files as well
    363    as Mozilla jar files (see further details in the JarWriter documentation).
    364    """
    365 
    366    def __init__(self, file=None, fileobj=None, data=None):
    367        """
    368        Opens the given file as a Jar archive. Use the given file-like object
    369        if one is given instead of opening the given file name.
    370        """
    371        if fileobj:
    372            data = fileobj.read()
    373        elif file:
    374            data = open(file, "rb").read()
    375        self._data = memoryview(data)
    376        # The End of Central Directory Record has a variable size because of
    377        # comments it may contain, so scan for it from the end of the file.
    378        offset = -CDIR_END_SIZE
    379        while True:
    380            signature = JarStruct.get_data("uint32", self._data[offset:])[0]
    381            if signature == JarCdirEnd.MAGIC:
    382                break
    383            if offset == -len(self._data):
    384                raise JarReaderError("Not a jar?")
    385            offset -= 1
    386        self._cdir_end = JarCdirEnd(self._data[offset:])
    387 
    388    def close(self):
    389        """
    390        Free some resources associated with the Jar.
    391        """
    392        del self._data
    393 
    394    @property
    395    def compression(self):
    396        entries = self.entries
    397        if not entries:
    398            return JAR_STORED
    399        return max(f["compression"] for f in entries.values())
    400 
    401    @property
    402    def entries(self):
    403        """
    404        Return an ordered dict of central directory entries, indexed by
    405        filename, in the order they appear in the Jar archive central
    406        directory. Directory entries are skipped.
    407        """
    408        if hasattr(self, "_entries"):
    409            return self._entries
    410        preload = 0
    411        if self.is_optimized:
    412            preload = JarStruct.get_data("uint32", self._data)[0]
    413        entries = OrderedDict()
    414        offset = self._cdir_end["cdir_offset"]
    415        for e in range(self._cdir_end["cdir_entries"]):
    416            entry = JarCdirEntry(self._data[offset:])
    417            offset += entry.size
    418            # Creator host system. 0 is MSDOS, 3 is Unix
    419            host = entry["creator_version"] >> 8
    420            # External attributes values depend on host above. On Unix the
    421            # higher bits are the stat.st_mode value. On MSDOS, the lower bits
    422            # are the FAT attributes.
    423            xattr = entry["external_attr"]
    424            # Skip directories
    425            if (host == 0 and xattr & 0x10) or (host == 3 and xattr & (0o040000 << 16)):
    426                continue
    427            entries[entry["filename"].decode()] = entry
    428            if entry["offset"] < preload:
    429                self._last_preloaded = entry["filename"].decode()
    430        self._entries = entries
    431        return entries
    432 
    433    @property
    434    def is_optimized(self):
    435        """
    436        Return whether the jar archive is optimized.
    437        """
    438        # In optimized jars, the central directory is at the beginning of the
    439        # file, after a single 32-bits value, which is the length of data
    440        # preloaded.
    441        return self._cdir_end["cdir_offset"] == JarStruct.TYPE_MAPPING["uint32"][1]
    442 
    443    @property
    444    def last_preloaded(self):
    445        """
    446        Return the name of the last file that is set to be preloaded.
    447        See JarWriter documentation for more details on preloading.
    448        """
    449        if hasattr(self, "_last_preloaded"):
    450            return self._last_preloaded
    451        self._last_preloaded = None
    452        self.entries
    453        return self._last_preloaded
    454 
    455    def _getreader(self, entry):
    456        """
    457        Helper to create a JarFileReader corresponding to the given central
    458        directory entry.
    459        """
    460        header = JarLocalFileHeader(self._data[entry["offset"] :])
    461        for key, value in entry:
    462            if key in header and header[key] != value:
    463                raise JarReaderError(
    464                    "Central directory and file header "
    465                    + "mismatch. Corrupted archive?"
    466                )
    467        return JarFileReader(header, self._data[entry["offset"] + header.size :])
    468 
    469    def __iter__(self):
    470        """
    471        Iterate over all files in the Jar archive, in the form of
    472        JarFileReaders.
    473            for file in jarReader:
    474                ...
    475        """
    476        for entry in self.entries.values():
    477            yield self._getreader(entry)
    478 
    479    def __getitem__(self, name):
    480        """
    481        Get a JarFileReader for the given file name.
    482        """
    483        return self._getreader(self.entries[name])
    484 
    485    def __contains__(self, name):
    486        """
    487        Return whether the given file name appears in the Jar archive.
    488        """
    489        return name in self.entries
    490 
    491 
    492 class JarWriter:
    493    """
    494    Class with methods to write Jar files. Can write more-or-less standard jar
    495    archives as well as jar archives optimized for Gecko. See the documentation
    496    for the close() member function for a description of both layouts.
    497    """
    498 
    499    def __init__(self, file=None, fileobj=None, compress=True, compress_level=9):
    500        """
    501        Initialize a Jar archive in the given file. Use the given file-like
    502        object if one is given instead of opening the given file name.
    503        The compress option determines the default behavior for storing data
    504        in the jar archive. The optimize options determines whether the jar
    505        archive should be optimized for Gecko or not. ``compress_level``
    506        defines the zlib compression level. It must be a value between 0 and 9
    507        and defaults to 9, the highest and slowest level of compression.
    508        """
    509        if fileobj:
    510            self._data = fileobj
    511        else:
    512            self._data = open(file, "wb")
    513        if compress is True:
    514            compress = JAR_DEFLATED
    515        self._compress = compress
    516        self._compress_level = compress_level
    517        self._contents = OrderedDict()
    518        self._last_preloaded = None
    519 
    520    def __enter__(self):
    521        """
    522        Context manager __enter__ method for JarWriter.
    523        """
    524        return self
    525 
    526    def __exit__(self, type, value, tb):
    527        """
    528        Context manager __exit__ method for JarWriter.
    529        """
    530        self.finish()
    531 
    532    def finish(self):
    533        """
    534        Flush and close the Jar archive.
    535 
    536        Standard jar archives are laid out like the following:
    537            - Local file header 1
    538            - File data 1
    539            - Local file header 2
    540            - File data 2
    541            - (...)
    542            - Central directory entry pointing at Local file header 1
    543            - Central directory entry pointing at Local file header 2
    544            - (...)
    545            - End of central directory, pointing at first central directory
    546              entry.
    547 
    548        Jar archives optimized for Gecko are laid out like the following:
    549            - 32-bits unsigned integer giving the amount of data to preload.
    550            - Central directory entry pointing at Local file header 1
    551            - Central directory entry pointing at Local file header 2
    552            - (...)
    553            - End of central directory, pointing at first central directory
    554              entry.
    555            - Local file header 1
    556            - File data 1
    557            - Local file header 2
    558            - File data 2
    559            - (...)
    560            - End of central directory, pointing at first central directory
    561              entry.
    562 
    563        The duplication of the End of central directory is to accomodate some
    564        Zip reading tools that want an end of central directory structure to
    565        follow the central directory entries.
    566        """
    567        offset = 0
    568        headers = {}
    569        preload_size = 0
    570        # Prepare central directory entries
    571        for entry, content in self._contents.values():
    572            header = JarLocalFileHeader()
    573            for name in entry.STRUCT:
    574                if name in header:
    575                    header[name] = entry[name]
    576            entry["offset"] = offset
    577            offset += len(content) + header.size
    578            if entry["filename"].decode() == self._last_preloaded:
    579                preload_size = offset
    580            headers[entry] = header
    581        # Prepare end of central directory
    582        end = JarCdirEnd()
    583        end["disk_entries"] = len(self._contents)
    584        end["cdir_entries"] = end["disk_entries"]
    585        end["cdir_size"] = functools.reduce(
    586            lambda x, y: x + y[0].size, self._contents.values(), 0
    587        )
    588        # On optimized archives, store the preloaded size and the central
    589        # directory entries, followed by the first end of central directory.
    590        if preload_size:
    591            end["cdir_offset"] = 4
    592            offset = end["cdir_size"] + end["cdir_offset"] + end.size
    593            preload_size += offset
    594            self._data.write(struct.pack("<I", preload_size))
    595            for entry, _ in self._contents.values():
    596                entry["offset"] += offset
    597                self._data.write(entry.serialize())
    598            self._data.write(end.serialize())
    599        # Store local file entries followed by compressed data
    600        for entry, content in self._contents.values():
    601            self._data.write(headers[entry].serialize())
    602            if isinstance(content, memoryview):
    603                self._data.write(content.tobytes())
    604            else:
    605                self._data.write(content)
    606        # On non optimized archives, store the central directory entries.
    607        if not preload_size:
    608            end["cdir_offset"] = offset
    609            for entry, _ in self._contents.values():
    610                self._data.write(entry.serialize())
    611        # Store the end of central directory.
    612        self._data.write(end.serialize())
    613        self._data.close()
    614 
    615    def add(self, name, data, compress=None, mode=None, skip_duplicates=False):
    616        """
    617        Add a new member to the jar archive, with the given name and the given
    618        data.
    619        The compress option indicates how the given data should be compressed
    620        (one of JAR_STORED or JAR_DEFLATE), or compressed according
    621        to the default defined when creating the JarWriter (None). True and
    622        False are allowed values for backwards compatibility, mapping,
    623        respectively, to JAR_DEFLATE and JAR_STORED.
    624        When the data should be compressed, it is only really compressed if
    625        the compressed size is smaller than the uncompressed size.
    626        The mode option gives the unix permissions that should be stored for the
    627        jar entry, which defaults to 0o100644 (regular file, u+rw, g+r, o+r) if
    628        not specified.
    629        If a duplicated member is found skip_duplicates will prevent raising
    630        an exception if set to True.
    631        The given data may be a buffer, a file-like instance, a Deflater or a
    632        JarFileReader instance. The latter two allow to avoid uncompressing
    633        data to recompress it.
    634        """
    635        if isinstance(name, bytes):
    636            name = name.decode()
    637        name = mozpath.normsep(name)
    638 
    639        if name in self._contents and not skip_duplicates:
    640            raise JarWriterError("File %s already in JarWriter" % name)
    641        if compress is None:
    642            compress = self._compress
    643        if compress is True:
    644            compress = JAR_DEFLATED
    645        if compress is False:
    646            compress = JAR_STORED
    647        if isinstance(data, (JarFileReader, Deflater)) and data.compress == compress:
    648            deflater = data
    649        else:
    650            deflater = Deflater(compress, compress_level=self._compress_level)
    651            if isinstance(data, (bytes, str)):
    652                deflater.write(data)
    653            elif hasattr(data, "read"):
    654                try:
    655                    data.seek(0)
    656                except (UnsupportedOperation, AttributeError):
    657                    pass
    658                deflater.write(data.read())
    659            else:
    660                raise JarWriterError("Don't know how to handle %s" % type(data))
    661        # Fill a central directory entry for this new member.
    662        entry = JarCdirEntry()
    663        entry["creator_version"] = 20
    664        if mode is None:
    665            # If no mode is given, default to u+rw, g+r, o+r.
    666            mode = 0o000644
    667        if not mode & 0o777000:
    668            # If no file type is given, default to regular file.
    669            mode |= 0o100000
    670        # Set creator host system (upper byte of creator_version) to 3 (Unix) so
    671        # mode is honored when there is one.
    672        entry["creator_version"] |= 3 << 8
    673        entry["external_attr"] = (mode & 0xFFFF) << 16
    674        if deflater.compressed:
    675            entry["min_version"] = 20  # Version 2.0 supports deflated streams
    676            entry["general_flag"] = 2  # Max compression
    677            entry["compression"] = deflater.compress
    678        else:
    679            entry["min_version"] = 10  # Version 1.0 for stored streams
    680            entry["general_flag"] = 0
    681            entry["compression"] = JAR_STORED
    682        # January 1st, 2010. See bug 592369.
    683        entry["lastmod_date"] = ((2010 - 1980) << 9) | (1 << 5) | 1
    684        entry["lastmod_time"] = 0
    685        entry["crc32"] = deflater.crc32
    686        entry["compressed_size"] = deflater.compressed_size
    687        entry["uncompressed_size"] = deflater.uncompressed_size
    688        entry["filename"] = name.encode()
    689        self._contents[name] = entry, deflater.compressed_data
    690 
    691    def preload(self, files):
    692        """
    693        Set which members of the jar archive should be preloaded when opening
    694        the archive in Gecko. This reorders the members according to the order
    695        of given list.
    696        """
    697        new_contents = OrderedDict()
    698        for f in files:
    699            if f not in self._contents:
    700                continue
    701            new_contents[f] = self._contents[f]
    702            self._last_preloaded = f
    703        for f in self._contents:
    704            if f not in new_contents:
    705                new_contents[f] = self._contents[f]
    706        self._contents = new_contents
    707 
    708 
    709 class Deflater:
    710    """
    711    File-like interface to zlib compression. The data is actually not
    712    compressed unless the compressed form is smaller than the uncompressed
    713    data.
    714    """
    715 
    716    def __init__(self, compress=True, compress_level=9):
    717        """
    718        Initialize a Deflater. The compress argument determines how to
    719        compress.
    720        """
    721        self._data = BytesIO()
    722        if compress is True:
    723            compress = JAR_DEFLATED
    724        elif compress is False:
    725            compress = JAR_STORED
    726        self.compress = compress
    727        if compress == JAR_DEFLATED:
    728            self._deflater = zlib.compressobj(compress_level, zlib.DEFLATED, -MAX_WBITS)
    729            self._deflated = BytesIO()
    730        else:
    731            assert compress == JAR_STORED
    732            self._deflater = None
    733        self.crc32 = 0
    734 
    735    def write(self, data):
    736        """
    737        Append a buffer to the Deflater.
    738        """
    739        if isinstance(data, memoryview):
    740            data = data.tobytes()
    741        if isinstance(data, str):
    742            data = data.encode()
    743        self._data.write(data)
    744 
    745        if self.compress:
    746            if self._deflater:
    747                self._deflated.write(self._deflater.compress(data))
    748            else:
    749                raise JarWriterError("Can't write after flush")
    750 
    751        self.crc32 = zlib.crc32(data, self.crc32) & 0xFFFFFFFF
    752 
    753    def close(self):
    754        """
    755        Close the Deflater.
    756        """
    757        self._data.close()
    758        if self.compress:
    759            self._deflated.close()
    760 
    761    def _flush(self):
    762        """
    763        Flush the underlying zlib compression object.
    764        """
    765        if self.compress and self._deflater:
    766            self._deflated.write(self._deflater.flush())
    767            self._deflater = None
    768 
    769    @property
    770    def compressed(self):
    771        """
    772        Return whether the data should be compressed.
    773        """
    774        return self._compressed_size < self.uncompressed_size
    775 
    776    @property
    777    def _compressed_size(self):
    778        """
    779        Return the real compressed size of the data written to the Deflater. If
    780        the Deflater is set not to compress, the uncompressed size is returned.
    781        Otherwise, the actual compressed size is returned, whether or not it is
    782        a win over the uncompressed size.
    783        """
    784        if self.compress:
    785            self._flush()
    786            return self._deflated.tell()
    787        return self.uncompressed_size
    788 
    789    @property
    790    def compressed_size(self):
    791        """
    792        Return the compressed size of the data written to the Deflater. If the
    793        Deflater is set not to compress, the uncompressed size is returned.
    794        Otherwise, if the data should not be compressed (the real compressed
    795        size is bigger than the uncompressed size), return the uncompressed
    796        size.
    797        """
    798        if self.compressed:
    799            return self._compressed_size
    800        return self.uncompressed_size
    801 
    802    @property
    803    def uncompressed_size(self):
    804        """
    805        Return the size of the data written to the Deflater.
    806        """
    807        return self._data.tell()
    808 
    809    @property
    810    def compressed_data(self):
    811        """
    812        Return the compressed data, if the data should be compressed (real
    813        compressed size smaller than the uncompressed size), or the
    814        uncompressed data otherwise.
    815        """
    816        if self.compressed:
    817            return self._deflated.getvalue()
    818        return self._data.getvalue()
    819 
    820 
    821 class JarLog(dict):
    822    """
    823    Helper to read the file Gecko generates when setting MOZ_JAR_LOG_FILE.
    824    The jar log is then available as a dict with the jar path as key, and
    825    the corresponding access log as a list value. Only the first access to
    826    a given member of a jar is stored.
    827    """
    828 
    829    def __init__(self, file=None, fileobj=None):
    830        if not fileobj:
    831            fileobj = open(file)
    832        for line in fileobj:
    833            jar, path = line.strip().split(None, 1)
    834            if not jar or not path:
    835                continue
    836            entry = self.setdefault(jar, [])
    837            if path not in entry:
    838                entry.append(path)