| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| from io import BytesIO |
| import struct |
| import zlib |
| import os |
| from zipfile import ( |
| ZIP_STORED, |
| ZIP_DEFLATED, |
| ) |
| from collections import OrderedDict |
| from urlparse import urlparse, ParseResult |
| import mozpack.path |
| |
| JAR_STORED = ZIP_STORED |
| JAR_DEFLATED = ZIP_DEFLATED |
| MAX_WBITS = 15 |
| |
| |
| class JarReaderError(Exception): |
| '''Error type for Jar reader errors.''' |
| |
| |
| class JarWriterError(Exception): |
| '''Error type for Jar writer errors.''' |
| |
| |
| class JarStruct(object): |
| ''' |
| Helper used to define ZIP archive raw data structures. Data structures |
| handled by this helper all start with a magic number, defined in |
| subclasses MAGIC field as a 32-bits unsigned integer, followed by data |
| structured as described in subclasses STRUCT field. |
| |
| The STRUCT field contains a list of (name, type) pairs where name is a |
| field name, and the type can be one of 'uint32', 'uint16' or one of the |
| field names. In the latter case, the field is considered to be a string |
| buffer with a length given in that field. |
| For example, |
| STRUCT = [ |
| ('version', 'uint32'), |
| ('filename_size', 'uint16'), |
| ('filename', 'filename_size') |
| ] |
| describes a structure with a 'version' 32-bits unsigned integer field, |
| followed by a 'filename_size' 16-bits unsigned integer field, followed by a |
| filename_size-long string buffer 'filename'. |
| |
| Fields that are used as other fields size are not stored in objects. In the |
| above example, an instance of such subclass would only have two attributes: |
| obj['version'] |
| obj['filename'] |
| filename_size would be obtained with len(obj['filename']). |
| |
| JarStruct subclasses instances can be either initialized from existing data |
| (deserialized), or with empty fields. |
| ''' |
| |
| TYPE_MAPPING = {'uint32': ('I', 4), 'uint16': ('H', 2)} |
| |
| def __init__(self, data=None): |
| ''' |
| Create an instance from the given data. Data may be omitted to create |
| an instance with empty fields. |
| ''' |
| assert self.MAGIC and isinstance(self.STRUCT, OrderedDict) |
| self.size_fields = set(t for t in self.STRUCT.itervalues() |
| if not t in JarStruct.TYPE_MAPPING) |
| self._values = {} |
| if data: |
| self._init_data(data) |
| else: |
| self._init_empty() |
| |
| def _init_data(self, data): |
| ''' |
| Initialize an instance from data, following the data structure |
| described in self.STRUCT. The self.MAGIC signature is expected at |
| data[:4]. |
| ''' |
| assert data is not None |
| self.signature, size = JarStruct.get_data('uint32', data) |
| if self.signature != self.MAGIC: |
| raise JarReaderError('Bad magic') |
| offset = size |
| # For all fields used as other fields sizes, keep track of their value |
| # separately. |
| sizes = dict((t, 0) for t in self.size_fields) |
| for name, t in self.STRUCT.iteritems(): |
| if t in JarStruct.TYPE_MAPPING: |
| value, size = JarStruct.get_data(t, data[offset:]) |
| else: |
| size = sizes[t] |
| value = data[offset:offset + size] |
| if isinstance(value, memoryview): |
| value = value.tobytes() |
| if not name in sizes: |
| self._values[name] = value |
| else: |
| sizes[name] = value |
| offset += size |
| |
| def _init_empty(self): |
| ''' |
| Initialize an instance with empty fields. |
| ''' |
| self.signature = self.MAGIC |
| for name, t in self.STRUCT.iteritems(): |
| if name in self.size_fields: |
| continue |
| self._values[name] = 0 if t in JarStruct.TYPE_MAPPING else '' |
| |
| @staticmethod |
| def get_data(type, data): |
| ''' |
| Deserialize a single field of given type (must be one of |
| JarStruct.TYPE_MAPPING) at the given offset in the given data. |
| ''' |
| assert type in JarStruct.TYPE_MAPPING |
| assert data is not None |
| format, size = JarStruct.TYPE_MAPPING[type] |
| data = data[:size] |
| if isinstance(data, memoryview): |
| data = data.tobytes() |
| return struct.unpack('<' + format, data)[0], size |
| |
| def serialize(self): |
| ''' |
| Serialize the data structure according to the data structure definition |
| from self.STRUCT. |
| ''' |
| serialized = struct.pack('<I', self.signature) |
| sizes = dict((t, name) for name, t in self.STRUCT.iteritems() |
| if not t in JarStruct.TYPE_MAPPING) |
| for name, t in self.STRUCT.iteritems(): |
| if t in JarStruct.TYPE_MAPPING: |
| format, size = JarStruct.TYPE_MAPPING[t] |
| if name in sizes: |
| value = len(self[sizes[name]]) |
| else: |
| value = self[name] |
| serialized += struct.pack('<' + format, value) |
| else: |
| serialized += self[name] |
| return serialized |
| |
| @property |
| def size(self): |
| ''' |
| Return the size of the data structure, given the current values of all |
| variable length fields. |
| ''' |
| size = JarStruct.TYPE_MAPPING['uint32'][1] |
| for name, type in self.STRUCT.iteritems(): |
| if type in JarStruct.TYPE_MAPPING: |
| size += JarStruct.TYPE_MAPPING[type][1] |
| else: |
| size += len(self[name]) |
| return size |
| |
| def __getitem__(self, key): |
| return self._values[key] |
| |
| def __setitem__(self, key, value): |
| if not key in self.STRUCT: |
| raise KeyError(key) |
| if key in self.size_fields: |
| raise AttributeError("can't set attribute") |
| self._values[key] = value |
| |
| def __contains__(self, key): |
| return key in self._values |
| |
| def __iter__(self): |
| return self._values.iteritems() |
| |
| def __repr__(self): |
| return "<%s %s>" % (self.__class__.__name__, |
| ' '.join('%s=%s' % (n, v) for n, v in self)) |
| |
| |
| class JarCdirEnd(JarStruct): |
| ''' |
| End of central directory record. |
| ''' |
| MAGIC = 0x06054b50 |
| STRUCT = OrderedDict([ |
| ('disk_num', 'uint16'), |
| ('cdir_disk', 'uint16'), |
| ('disk_entries', 'uint16'), |
| ('cdir_entries', 'uint16'), |
| ('cdir_size', 'uint32'), |
| ('cdir_offset', 'uint32'), |
| ('comment_size', 'uint16'), |
| ('comment', 'comment_size'), |
| ]) |
| |
| CDIR_END_SIZE = JarCdirEnd().size |
| |
| |
| class JarCdirEntry(JarStruct): |
| ''' |
| Central directory file header |
| ''' |
| MAGIC = 0x02014b50 |
| STRUCT = OrderedDict([ |
| ('creator_version', 'uint16'), |
| ('min_version', 'uint16'), |
| ('general_flag', 'uint16'), |
| ('compression', 'uint16'), |
| ('lastmod_time', 'uint16'), |
| ('lastmod_date', 'uint16'), |
| ('crc32', 'uint32'), |
| ('compressed_size', 'uint32'), |
| ('uncompressed_size', 'uint32'), |
| ('filename_size', 'uint16'), |
| ('extrafield_size', 'uint16'), |
| ('filecomment_size', 'uint16'), |
| ('disknum', 'uint16'), |
| ('internal_attr', 'uint16'), |
| ('external_attr', 'uint32'), |
| ('offset', 'uint32'), |
| ('filename', 'filename_size'), |
| ('extrafield', 'extrafield_size'), |
| ('filecomment', 'filecomment_size'), |
| ]) |
| |
| |
| class JarLocalFileHeader(JarStruct): |
| ''' |
| Local file header |
| ''' |
| MAGIC = 0x04034b50 |
| STRUCT = OrderedDict([ |
| ('min_version', 'uint16'), |
| ('general_flag', 'uint16'), |
| ('compression', 'uint16'), |
| ('lastmod_time', 'uint16'), |
| ('lastmod_date', 'uint16'), |
| ('crc32', 'uint32'), |
| ('compressed_size', 'uint32'), |
| ('uncompressed_size', 'uint32'), |
| ('filename_size', 'uint16'), |
| ('extra_field_size', 'uint16'), |
| ('filename', 'filename_size'), |
| ('extra_field', 'extra_field_size'), |
| ]) |
| |
| |
| class JarFileReader(object): |
| ''' |
| File-like class for use by JarReader to give access to individual files |
| within a Jar archive. |
| ''' |
| def __init__(self, header, data): |
| ''' |
| Initialize a JarFileReader. header is the local file header |
| corresponding to the file in the jar archive, data a buffer containing |
| the file data. |
| ''' |
| assert header['compression'] in [JAR_DEFLATED, JAR_STORED] |
| self._data = data |
| # Copy some local file header fields. |
| for name in ['filename', 'compressed_size', |
| 'uncompressed_size', 'crc32']: |
| setattr(self, name, header[name]) |
| self.compressed = header['compression'] == JAR_DEFLATED |
| |
| def read(self, length=-1): |
| ''' |
| Read some amount of uncompressed data. |
| ''' |
| return self.uncompressed_data.read(length) |
| |
| def readlines(self): |
| ''' |
| Return a list containing all the lines of data in the uncompressed |
| data. |
| ''' |
| return self.read().splitlines(True) |
| |
| def __iter__(self): |
| ''' |
| Iterator, to support the "for line in fileobj" constructs. |
| ''' |
| return iter(self.readlines()) |
| |
| def seek(self, pos, whence=os.SEEK_SET): |
| ''' |
| Change the current position in the uncompressed data. Subsequent reads |
| will start from there. |
| ''' |
| return self.uncompressed_data.seek(pos, whence) |
| |
| def close(self): |
| ''' |
| Free the uncompressed data buffer. |
| ''' |
| self.uncompressed_data.close() |
| |
| @property |
| def compressed_data(self): |
| ''' |
| Return the raw compressed data. |
| ''' |
| return self._data[:self.compressed_size] |
| |
| @property |
| def uncompressed_data(self): |
| ''' |
| Return the uncompressed data. |
| ''' |
| if hasattr(self, '_uncompressed_data'): |
| return self._uncompressed_data |
| data = self.compressed_data |
| if self.compressed: |
| data = zlib.decompress(data.tobytes(), -MAX_WBITS) |
| else: |
| data = data.tobytes() |
| if len(data) != self.uncompressed_size: |
| raise JarReaderError('Corrupted file? %s' % self.filename) |
| self._uncompressed_data = BytesIO(data) |
| return self._uncompressed_data |
| |
| |
| class JarReader(object): |
| ''' |
| Class with methods to read Jar files. Can open standard jar files as well |
| as Mozilla jar files (see further details in the JarWriter documentation). |
| ''' |
| def __init__(self, file=None, fileobj=None): |
| ''' |
| Opens the given file as a Jar archive. Use the given file-like object |
| if one is given instead of opening the given file name. |
| ''' |
| if fileobj: |
| data = fileobj.read() |
| else: |
| data = open(file, 'rb').read() |
| self._data = memoryview(data) |
| # The End of Central Directory Record has a variable size because of |
| # comments it may contain, so scan for it from the end of the file. |
| offset = -CDIR_END_SIZE |
| while True: |
| signature = JarStruct.get_data('uint32', self._data[offset:])[0] |
| if signature == JarCdirEnd.MAGIC: |
| break |
| if offset == -len(self._data): |
| raise JarReaderError('Not a jar?') |
| offset -= 1 |
| self._cdir_end = JarCdirEnd(self._data[offset:]) |
| |
| def close(self): |
| ''' |
| Free some resources associated with the Jar. |
| ''' |
| del self._data |
| |
| @property |
| def entries(self): |
| ''' |
| Return an ordered dict of central directory entries, indexed by |
| filename, in the order they appear in the Jar archive central |
| directory. Directory entries are skipped. |
| ''' |
| if hasattr(self, '_entries'): |
| return self._entries |
| preload = 0 |
| if self.is_optimized: |
| preload = JarStruct.get_data('uint32', self._data)[0] |
| entries = OrderedDict() |
| offset = self._cdir_end['cdir_offset'] |
| for e in xrange(self._cdir_end['cdir_entries']): |
| entry = JarCdirEntry(self._data[offset:]) |
| offset += entry.size |
| # Creator host system. 0 is MSDOS, 3 is Unix |
| host = entry['creator_version'] >> 16 |
| # External attributes values depend on host above. On Unix the |
| # higher bits are the stat.st_mode value. On MSDOS, the lower bits |
| # are the FAT attributes. |
| xattr = entry['external_attr'] |
| # Skip directories |
| if (host == 0 and xattr & 0x10) or (host == 3 and |
| xattr & (040000 << 16)): |
| continue |
| entries[entry['filename']] = entry |
| if entry['offset'] < preload: |
| self._last_preloaded = entry['filename'] |
| self._entries = entries |
| return entries |
| |
| @property |
| def is_optimized(self): |
| ''' |
| Return whether the jar archive is optimized. |
| ''' |
| # In optimized jars, the central directory is at the beginning of the |
| # file, after a single 32-bits value, which is the length of data |
| # preloaded. |
| return self._cdir_end['cdir_offset'] == \ |
| JarStruct.TYPE_MAPPING['uint32'][1] |
| |
| @property |
| def last_preloaded(self): |
| ''' |
| Return the name of the last file that is set to be preloaded. |
| See JarWriter documentation for more details on preloading. |
| ''' |
| if hasattr(self, '_last_preloaded'): |
| return self._last_preloaded |
| self._last_preloaded = None |
| self.entries |
| return self._last_preloaded |
| |
| def _getreader(self, entry): |
| ''' |
| Helper to create a JarFileReader corresponding to the given central |
| directory entry. |
| ''' |
| header = JarLocalFileHeader(self._data[entry['offset']:]) |
| for key, value in entry: |
| if key in header and header[key] != value: |
| raise JarReaderError('Central directory and file header ' + |
| 'mismatch. Corrupted archive?') |
| return JarFileReader(header, |
| self._data[entry['offset'] + header.size:]) |
| |
| def __iter__(self): |
| ''' |
| Iterate over all files in the Jar archive, in the form of |
| JarFileReaders. |
| for file in jarReader: |
| ... |
| ''' |
| for entry in self.entries.itervalues(): |
| yield self._getreader(entry) |
| |
| def __getitem__(self, name): |
| ''' |
| Get a JarFileReader for the given file name. |
| ''' |
| return self._getreader(self.entries[name]) |
| |
| def __contains__(self, name): |
| ''' |
| Return whether the given file name appears in the Jar archive. |
| ''' |
| return name in self.entries |
| |
| |
| class JarWriter(object): |
| ''' |
| Class with methods to write Jar files. Can write more-or-less standard jar |
| archives as well as jar archives optimized for Gecko. See the documentation |
| for the close() member function for a description of both layouts. |
| ''' |
| def __init__(self, file=None, fileobj=None, compress=True, optimize=True): |
| ''' |
| Initialize a Jar archive in the given file. Use the given file-like |
| object if one is given instead of opening the given file name. |
| The compress option determines the default behavior for storing data |
| in the jar archive. The optimize options determines whether the jar |
| archive should be optimized for Gecko or not. |
| ''' |
| if fileobj: |
| self._data = fileobj |
| else: |
| self._data = open(file, 'wb') |
| self._compress = compress |
| self._contents = OrderedDict() |
| self._last_preloaded = None |
| self._optimize = optimize |
| |
| def __enter__(self): |
| ''' |
| Context manager __enter__ method for JarWriter. |
| ''' |
| return self |
| |
| def __exit__(self, type, value, tb): |
| ''' |
| Context manager __exit__ method for JarWriter. |
| ''' |
| self.finish() |
| |
| def finish(self): |
| ''' |
| Flush and close the Jar archive. |
| |
| Standard jar archives are laid out like the following: |
| - Local file header 1 |
| - File data 1 |
| - Local file header 2 |
| - File data 2 |
| - (...) |
| - Central directory entry pointing at Local file header 1 |
| - Central directory entry pointing at Local file header 2 |
| - (...) |
| - End of central directory, pointing at first central directory |
| entry. |
| |
| Jar archives optimized for Gecko are laid out like the following: |
| - 32-bits unsigned integer giving the amount of data to preload. |
| - Central directory entry pointing at Local file header 1 |
| - Central directory entry pointing at Local file header 2 |
| - (...) |
| - End of central directory, pointing at first central directory |
| entry. |
| - Local file header 1 |
| - File data 1 |
| - Local file header 2 |
| - File data 2 |
| - (...) |
| - End of central directory, pointing at first central directory |
| entry. |
| The duplication of the End of central directory is to accomodate some |
| Zip reading tools that want an end of central directory structure to |
| follow the central directory entries. |
| ''' |
| offset = 0 |
| headers = {} |
| preload_size = 0 |
| # Prepare central directory entries |
| for entry, content in self._contents.itervalues(): |
| header = JarLocalFileHeader() |
| for name in entry.STRUCT: |
| if name in header: |
| header[name] = entry[name] |
| entry['offset'] = offset |
| offset += len(content) + header.size |
| if entry['filename'] == self._last_preloaded: |
| preload_size = offset |
| headers[entry] = header |
| # Prepare end of central directory |
| end = JarCdirEnd() |
| end['disk_entries'] = len(self._contents) |
| end['cdir_entries'] = end['disk_entries'] |
| end['cdir_size'] = reduce(lambda x, y: x + y[0].size, |
| self._contents.values(), 0) |
| # On optimized archives, store the preloaded size and the central |
| # directory entries, followed by the first end of central directory. |
| if self._optimize: |
| end['cdir_offset'] = 4 |
| offset = end['cdir_size'] + end['cdir_offset'] + end.size |
| if preload_size: |
| preload_size += offset |
| self._data.write(struct.pack('<I', preload_size)) |
| for entry, _ in self._contents.itervalues(): |
| entry['offset'] += offset |
| self._data.write(entry.serialize()) |
| self._data.write(end.serialize()) |
| # Store local file entries followed by compressed data |
| for entry, content in self._contents.itervalues(): |
| self._data.write(headers[entry].serialize()) |
| self._data.write(content) |
| # On non optimized archives, store the central directory entries. |
| if not self._optimize: |
| end['cdir_offset'] = offset |
| for entry, _ in self._contents.itervalues(): |
| self._data.write(entry.serialize()) |
| # Store the end of central directory. |
| self._data.write(end.serialize()) |
| self._data.close() |
| |
| def add(self, name, data, compress=None): |
| ''' |
| Add a new member to the jar archive, with the given name and the given |
| data. |
| The compress option indicates if the given data should be compressed |
| (True), not compressed (False), or compressed according to the default |
| defined when creating the JarWriter (None). |
| When the data should be compressed (True or None with self.compress == |
| True), it is only really compressed if the compressed size is smaller |
| than the uncompressed size. |
| The given data may be a buffer, a file-like instance, a Deflater or a |
| JarFileReader instance. The latter two allow to avoid uncompressing |
| data to recompress it. |
| ''' |
| if name in self._contents: |
| raise JarWriterError("File %s already in JarWriter" % name) |
| if compress is None: |
| compress = self._compress |
| if (isinstance(data, JarFileReader) and data.compressed == compress) \ |
| or (isinstance(data, Deflater) and data.compress == compress): |
| deflater = data |
| else: |
| deflater = Deflater(compress) |
| if isinstance(data, basestring): |
| deflater.write(data) |
| elif hasattr(data, 'read'): |
| data.seek(0) |
| deflater.write(data.read()) |
| else: |
| raise JarWriterError("Don't know how to handle %s" % |
| type(data)) |
| # Fill a central directory entry for this new member. |
| entry = JarCdirEntry() |
| # Not storing as created on unix, which avoids having to deal with |
| # st_mode. |
| entry['creator_version'] = 20 |
| if deflater.compressed: |
| entry['min_version'] = 20 # Version 2.0 supports deflated streams |
| entry['general_flag'] = 2 # Max compression |
| entry['compression'] = JAR_DEFLATED |
| else: |
| entry['min_version'] = 10 # Version 1.0 for stored streams |
| entry['general_flag'] = 0 |
| entry['compression'] = JAR_STORED |
| # January 1st, 2010. See bug 592369. |
| entry['lastmod_date'] = ((2010 - 1980) << 9) | (1 << 5) | 1 |
| entry['lastmod_time'] = 0 |
| entry['crc32'] = deflater.crc32 |
| entry['compressed_size'] = deflater.compressed_size |
| entry['uncompressed_size'] = deflater.uncompressed_size |
| entry['filename'] = name |
| self._contents[name] = entry, deflater.compressed_data |
| |
| def preload(self, files): |
| ''' |
| Set which members of the jar archive should be preloaded when opening |
| the archive in Gecko. This reorders the members according to the order |
| of given list. |
| ''' |
| new_contents = OrderedDict() |
| for f in files: |
| if not f in self._contents: |
| continue |
| new_contents[f] = self._contents[f] |
| self._last_preloaded = f |
| for f in self._contents: |
| if not f in new_contents: |
| new_contents[f] = self._contents[f] |
| self._contents = new_contents |
| |
| |
| class Deflater(object): |
| ''' |
| File-like interface to zlib compression. The data is actually not |
| compressed unless the compressed form is smaller than the uncompressed |
| data. |
| ''' |
| def __init__(self, compress=True): |
| ''' |
| Initialize a Deflater. The compress argument determines whether to |
| try to compress at all. |
| ''' |
| self._data = BytesIO() |
| self.compress = compress |
| if compress: |
| self._deflater = zlib.compressobj(9, zlib.DEFLATED, -MAX_WBITS) |
| self._deflated = BytesIO() |
| else: |
| self._deflater = None |
| |
| def write(self, data): |
| ''' |
| Append a buffer to the Deflater. |
| ''' |
| self._data.write(data) |
| if self.compress: |
| if self._deflater: |
| if isinstance(data, memoryview): |
| data = data.tobytes() |
| self._deflated.write(self._deflater.compress(data)) |
| else: |
| raise JarWriterError("Can't write after flush") |
| |
| def close(self): |
| ''' |
| Close the Deflater. |
| ''' |
| self._data.close() |
| if self.compress: |
| self._deflated.close() |
| |
| def _flush(self): |
| ''' |
| Flush the underlying zlib compression object. |
| ''' |
| if self.compress and self._deflater: |
| self._deflated.write(self._deflater.flush()) |
| self._deflater = None |
| |
| @property |
| def compressed(self): |
| ''' |
| Return whether the data should be compressed. |
| ''' |
| return self._compressed_size < self.uncompressed_size |
| |
| @property |
| def _compressed_size(self): |
| ''' |
| Return the real compressed size of the data written to the Deflater. If |
| the Deflater is set not to compress, the uncompressed size is returned. |
| Otherwise, the actual compressed size is returned, whether or not it is |
| a win over the uncompressed size. |
| ''' |
| if self.compress: |
| self._flush() |
| return self._deflated.tell() |
| return self.uncompressed_size |
| |
| @property |
| def compressed_size(self): |
| ''' |
| Return the compressed size of the data written to the Deflater. If the |
| Deflater is set not to compress, the uncompressed size is returned. |
| Otherwise, if the data should not be compressed (the real compressed |
| size is bigger than the uncompressed size), return the uncompressed |
| size. |
| ''' |
| if self.compressed: |
| return self._compressed_size |
| return self.uncompressed_size |
| |
| @property |
| def uncompressed_size(self): |
| ''' |
| Return the size of the data written to the Deflater. |
| ''' |
| return self._data.tell() |
| |
| @property |
| def crc32(self): |
| ''' |
| Return the crc32 of the data written to the Deflater. |
| ''' |
| return zlib.crc32(self._data.getvalue()) & 0xffffffff |
| |
| @property |
| def compressed_data(self): |
| ''' |
| Return the compressed data, if the data should be compressed (real |
| compressed size smaller than the uncompressed size), or the |
| uncompressed data otherwise. |
| ''' |
| if self.compressed: |
| return self._deflated.getvalue() |
| return self._data.getvalue() |
| |
| |
| class JarLog(dict): |
| ''' |
| Helper to read the file Gecko generates when setting MOZ_JAR_LOG_FILE. |
| The jar log is then available as a dict with the jar path as key (see |
| canonicalize for more details on the key value), and the corresponding |
| access log as a list value. Only the first access to a given member of |
| a jar is stored. |
| ''' |
| def __init__(self, file=None, fileobj=None): |
| if not fileobj: |
| fileobj = open(file, 'r') |
| urlmap = {} |
| for line in fileobj: |
| url, path = line.strip().split(None, 1) |
| if not url or not path: |
| continue |
| if url not in urlmap: |
| urlmap[url] = JarLog.canonicalize(url) |
| jar = urlmap[url] |
| entry = self.setdefault(jar, []) |
| if path not in entry: |
| entry.append(path) |
| |
| @staticmethod |
| def canonicalize(url): |
| ''' |
| The jar path is stored in a MOZ_JAR_LOG_FILE log as a url. This method |
| returns a unique value corresponding to such urls. |
| - file:///{path} becomes {path} |
| - jar:file:///{path}!/{subpath} becomes ({path}, {subpath}) |
| - jar:jar:file:///{path}!/{subpath}!/{subpath2} becomes |
| ({path}, {subpath}, {subpath2}) |
| ''' |
| if not isinstance(url, ParseResult): |
| # Assume that if it doesn't start with jar: or file:, it's a path. |
| if not url.startswith(('jar:', 'file:')): |
| url = 'file:///' + os.path.abspath(url) |
| url = urlparse(url) |
| assert url.scheme |
| assert url.scheme in ('jar', 'file') |
| if url.scheme == 'jar': |
| path = JarLog.canonicalize(url.path) |
| if isinstance(path, tuple): |
| return path[:-1] + tuple(path[-1].split('!/', 1)) |
| return tuple(path.split('!/', 1)) |
| if url.scheme == 'file': |
| assert os.path.isabs(url.path) |
| path = url.path |
| # On Windows, url.path will be /drive:/path ; on Unix systems, |
| # /path. As we want drive:/path instead of /drive:/path on Windows, |
| # remove the leading /. |
| if os.path.isabs(path[1:]): |
| path = path[1:] |
| path = os.path.realpath(path) |
| return mozpack.path.normsep(os.path.normcase(path)) |