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)