diff options
Diffstat (limited to 'modules/libjar')
114 files changed, 12626 insertions, 0 deletions
diff --git a/modules/libjar/appnote.txt b/modules/libjar/appnote.txt new file mode 100644 index 0000000000..7b96643cad --- /dev/null +++ b/modules/libjar/appnote.txt @@ -0,0 +1,1192 @@ +Revised: 03/01/1999 + +Disclaimer +---------- + +Although PKWARE will attempt to supply current and accurate +information relating to its file formats, algorithms, and the +subject programs, the possibility of error can not be eliminated. +PKWARE therefore expressly disclaims any warranty that the +information contained in the associated materials relating to the +subject programs and/or the format of the files created or +accessed by the subject programs and/or the algorithms used by +the subject programs, or any other matter, is current, correct or +accurate as delivered. Any risk of damage due to any possible +inaccurate information is assumed by the user of the information. +Furthermore, the information relating to the subject programs +and/or the file formats created or accessed by the subject +programs and/or the algorithms used by the subject programs is +subject to change without notice. + +General Format of a ZIP file +---------------------------- + + Files stored in arbitrary order. Large zipfiles can span multiple + diskette media. + + Overall zipfile format: + + [local file header + file data + data_descriptor] . . . + [central directory] end of central directory record + + + A. Local file header: + + local file header signature 4 bytes (0x04034b50) + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + filename length 2 bytes + extra field length 2 bytes + + filename (variable size) + extra field (variable size) + + B. Data descriptor: + + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + + This descriptor exists only if bit 3 of the general + purpose bit flag is set (see below). It is byte aligned + and immediately follows the last byte of compressed data. + This descriptor is used only when it was not possible to + seek in the output zip file, e.g., when the output zip file + was standard output or a non seekable device. + + C. Central directory structure: + + [file header] . . . end of central dir record + + File header: + + central file header signature 4 bytes (0x02014b50) + version made by 2 bytes + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + filename length 2 bytes + extra field length 2 bytes + file comment length 2 bytes + disk number start 2 bytes + internal file attributes 2 bytes + external file attributes 4 bytes + relative offset of local header 4 bytes + + filename (variable size) + extra field (variable size) + file comment (variable size) + + End of central dir record: + + end of central dir signature 4 bytes (0x06054b50) + number of this disk 2 bytes + number of the disk with the + start of the central directory 2 bytes + total number of entries in + the central dir on this disk 2 bytes + total number of entries in + the central dir 2 bytes + size of the central directory 4 bytes + offset of start of central + directory with respect to + the starting disk number 4 bytes + zipfile comment length 2 bytes + zipfile comment (variable size) + + D. Explanation of fields: + + version made by (2 bytes) + + The upper byte indicates the compatibility of the file + attribute information. If the external file attributes + are compatible with MS-DOS and can be read by PKZIP for + DOS version 2.04g then this value will be zero. If these + attributes are not compatible, then this value will + identify the host system on which the attributes are + compatible. Software can use this information to determine + the line record format for text files etc. The current + mappings are: + + 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + 1 - Amiga 2 - VAX/VMS + 3 - Unix 4 - VM/CMS + 5 - Atari ST 6 - OS/2 H.P.F.S. + 7 - Macintosh 8 - Z-System + 9 - CP/M 10 - Windows NTFS + 11 thru 255 - unused + + The lower byte indicates the version number of the + software used to encode the file. The value/10 + indicates the major version number, and the value + mod 10 is the minor version number. + + version needed to extract (2 bytes) + + The minimum software version needed to extract the + file, mapped as above. + + general purpose bit flag: (2 bytes) + + Bit 0: If set, indicates that the file is encrypted. + + (For Method 6 - Imploding) + Bit 1: If the compression method used was type 6, + Imploding, then this bit, if set, indicates + an 8K sliding dictionary was used. If clear, + then a 4K sliding dictionary was used. + Bit 2: If the compression method used was type 6, + Imploding, then this bit, if set, indicates + 3 Shannon-Fano trees were used to encode the + sliding dictionary output. If clear, then 2 + Shannon-Fano trees were used. + + (For Method 8 - Deflating) + Bit 2 Bit 1 + 0 0 Normal (-en) compression option was used. + 0 1 Maximum (-ex) compression option was used. + 1 0 Fast (-ef) compression option was used. + 1 1 Super Fast (-es) compression option was used. + + Note: Bits 1 and 2 are undefined if the compression + method is any other. + + Bit 3: If this bit is set, the fields crc-32, compressed + size and uncompressed size are set to zero in the + local header. The correct values are put in the + data descriptor immediately following the compressed + data. (Note: PKZIP version 2.04g for DOS only + recognizes this bit for method 8 compression, newer + versions of PKZIP recognize this bit for any + compression method.) + + Bit 4: Reserved for use with method 8, for enhanced + deflating. + + Bit 5: If this bit is set, this indicates that the file is + compressed patched data. (Note: Requires PKZIP + version 2.70 or greater) + + Bit 6: Currently unused. + + Bit 7: Currently unused. + + Bit 8: Currently unused. + + Bit 9: Currently unused. + + Bit 10: Currently unused. + + Bit 11: Currently unused. + + Bit 12: Reserved by PKWARE for enhanced compression. + + Bit 13: Reserved by PKWARE. + + Bit 14: Reserved by PKWARE. + + Bit 15: Reserved by PKWARE. + + compression method: (2 bytes) + + (see accompanying documentation for algorithm + descriptions) + + 0 - The file is stored (no compression) + 1 - The file is Shrunk + 2 - The file is Reduced with compression factor 1 + 3 - The file is Reduced with compression factor 2 + 4 - The file is Reduced with compression factor 3 + 5 - The file is Reduced with compression factor 4 + 6 - The file is Imploded + 7 - Reserved for Tokenizing compression algorithm + 8 - The file is Deflated + 9 - Reserved for enhanced Deflating + 10 - PKWARE Date Compression Library Imploding + + date and time fields: (2 bytes each) + + The date and time are encoded in standard MS-DOS format. + If input came from standard input, the date and time are + those at which compression was started for this data. + + CRC-32: (4 bytes) + + The CRC-32 algorithm was generously contributed by + David Schwaderer and can be found in his excellent + book "C Programmers Guide to NetBIOS" published by + Howard W. Sams & Co. Inc. The 'magic number' for + the CRC is 0xdebb20e3. The proper CRC pre and post + conditioning is used, meaning that the CRC register + is pre-conditioned with all ones (a starting value + of 0xffffffff) and the value is post-conditioned by + taking the one's complement of the CRC residual. + If bit 3 of the general purpose flag is set, this + field is set to zero in the local header and the correct + value is put in the data descriptor and in the central + directory. + + compressed size: (4 bytes) + uncompressed size: (4 bytes) + + The size of the file compressed and uncompressed, + respectively. If bit 3 of the general purpose bit flag + is set, these fields are set to zero in the local header + and the correct values are put in the data descriptor and + in the central directory. + + filename length: (2 bytes) + extra field length: (2 bytes) + file comment length: (2 bytes) + + The length of the filename, extra field, and comment + fields respectively. The combined length of any + directory record and these three fields should not + generally exceed 65,535 bytes. If input came from standard + input, the filename length is set to zero. + + disk number start: (2 bytes) + + The number of the disk on which this file begins. + + internal file attributes: (2 bytes) + + The lowest bit of this field indicates, if set, that + the file is apparently an ASCII or text file. If not + set, that the file apparently contains binary data. + The remaining bits are unused in version 1.0. + + Bits 1 and 2 are reserved for use by PKWARE. + + external file attributes: (4 bytes) + + The mapping of the external attributes is + host-system dependent (see 'version made by'). For + MS-DOS, the low order byte is the MS-DOS directory + attribute byte. If input came from standard input, this + field is set to zero. + + relative offset of local header: (4 bytes) + + This is the offset from the start of the first disk on + which this file appears, to where the local header should + be found. + + filename: (Variable) + + The name of the file, with optional relative path. + The path stored should not contain a drive or + device letter, or a leading slash. All slashes + should be forward slashes '/' as opposed to + backwards slashes '\' for compatibility with Amiga + and Unix file systems etc. If input came from standard + input, there is no filename field. + + extra field: (Variable) + + This is for future expansion. If additional information + needs to be stored in the future, it should be stored + here. Earlier versions of the software can then safely + skip this file, and find the next file or header. This + field will be 0 length in version 1.0. + + In order to allow different programs and different types + of information to be stored in the 'extra' field in .ZIP + files, the following structure should be used for all + programs storing data in this field: + + header1+data1 + header2+data2 . . . + + Each header should consist of: + + Header ID - 2 bytes + Data Size - 2 bytes + + Note: all fields stored in Intel low-byte/high-byte order. + + The Header ID field indicates the type of data that is in + the following data block. + + Header ID's of 0 thru 31 are reserved for use by PKWARE. + The remaining ID's can be used by third party vendors for + proprietary usage. + + The current Header ID mappings defined by PKWARE are: + + 0x0007 AV Info + 0x0009 OS/2 + 0x000a NTFS + 0x000c VAX/VMS + 0x000d Unix + 0x000f Patch Descriptor + + Several third party mappings commonly used are: + + 0x4b46 FWKCS MD5 (see below) + 0x07c8 Macintosh + 0x4341 Acorn/SparkFS + 0x4453 Windows NT security descriptor (binary ACL) + 0x4704 VM/CMS + 0x470f MVS + 0x4c41 OS/2 access control list (text ACL) + 0x4d49 Info-ZIP VMS (VAX or Alpha) + 0x5455 extended timestamp + 0x5855 Info-ZIP Unix (original, also OS/2, NT, etc) + 0x6542 BeOS/BeBox + 0x756e ASi Unix + 0x7855 Info-ZIP Unix (new) + 0xfd4a SMS/QDOS + + The Data Size field indicates the size of the following + data block. Programs can use this value to skip to the + next header block, passing over any data blocks that are + not of interest. + + Note: As stated above, the size of the entire .ZIP file + header, including the filename, comment, and extra + field should not exceed 64K in size. + + In case two different programs should appropriate the same + Header ID value, it is strongly recommended that each + program place a unique signature of at least two bytes in + size (and preferably 4 bytes or bigger) at the start of + each data area. Every program should verify that its + unique signature is present, in addition to the Header ID + value being correct, before assuming that it is a block of + known type. + + -OS/2 Extra Field: + + The following is the layout of the OS/2 attributes "extra" + block. (Last Revision 09/05/95) + + Note: all fields stored in Intel low-byte/high-byte order. + + Value Size Description + ----- ---- ----------- + (OS/2) 0x0009 2 bytes Tag for this "extra" block type + TSize 2 bytes Size for the following data block + BSize 4 bytes Uncompressed Block Size + CType 2 bytes Compression type + EACRC 4 bytes CRC value for uncompress block + (var) variable Compressed block + + The OS/2 extended attribute structure (FEA2LIST) is + compressed and then stored in it's entirety within this + structure. There will only ever be one "block" of data in + VarFields[]. + + -UNIX Extra Field: + + The following is the layout of the Unix "extra" block. + Note: all fields are stored in Intel low-byte/high-byte + order. + + Value Size Description + ----- ---- ----------- + (UNIX) 0x000d 2 bytes Tag for this "extra" block type + TSize 2 bytes Size for the following data block + Atime 4 bytes File last access time + Mtime 4 bytes File last modification time + Uid 2 bytes File user ID + Gid 2 bytes File group ID + (var) variable Variable length data field + + The variable length data field will contain file type + specific data. Currently the only values allowed are + the original "linked to" file names for hard or symbolic + links. + + -VAX/VMS Extra Field: + + The following is the layout of the VAX/VMS attributes + "extra" block. + + Note: all fields stored in Intel low-byte/high-byte order. + + Value Size Description + ----- ---- ----------- + (VMS) 0x000c 2 bytes Tag for this "extra" block type + TSize 2 bytes Size of the total "extra" block + CRC 4 bytes 32-bit CRC for remainder of the block + Tag1 2 bytes VMS attribute tag value #1 + Size1 2 bytes Size of attribute #1, in bytes + (var.) Size1 Attribute #1 data + . + . + . + TagN 2 bytes VMS attribute tage value #N + SizeN 2 bytes Size of attribute #N, in bytes + (var.) SizeN Attribute #N data + + Rules: + + 1. There will be one or more of attributes present, which + will each be preceded by the above TagX & SizeX values. + These values are identical to the ATR$C_XXXX and + ATR$S_XXXX constants which are defined in ATR.H under + VMS C. Neither of these values will ever be zero. + + 2. No word alignment or padding is performed. + + 3. A well-behaved PKZIP/VMS program should never produce + more than one sub-block with the same TagX value. Also, + there will never be more than one "extra" block of type + 0x000c in a particular directory record. + + -NTFS Extra Field: + + The following is the layout of the NTFS attributes + "extra" block. + + Note: all fields stored in Intel low-byte/high-byte order. + + Value Size Description + ----- ---- ----------- + (NTFS) 0x000a 2 bytes Tag for this "extra" block type + TSize 2 bytes Size of the total "extra" block + Reserved 4 bytes Reserved for future use + Tag1 2 bytes NTFS attribute tag value #1 + Size1 2 bytes Size of attribute #1, in bytes + (var.) Size1 Attribute #1 data + . + . + . + TagN 2 bytes NTFS attribute tage value #N + SizeN 2 bytes Size of attribute #N, in bytes + (var.) SizeN Attribute #N data + + For NTFS, values for Tag1 through TagN are as follows: + (currently only one set of attributes is defined for NTFS) + + Tag Size Description + ----- ---- ----------- + 0x0001 2 bytes Tag for attribute #1 + Size1 2 bytes Size of attribute #1, in bytes + Mtime 8 bytes File last modification time + Atime 8 bytes File last access time + Ctime 8 bytes File creation time + + -PATCH Descriptor Extra Field: + + The following is the layout of the Patch Descriptor "extra" + block. + + Note: all fields stored in Intel low-byte/high-byte order. + + Value Size Description + ----- ---- ----------- + (Patch) 0x000f 2 bytes Tag for this "extra" block type + TSize 2 bytes Size of the total "extra" block + Version 2 bytes Version of the descriptor + Flags 4 bytes Actions and reactions (see below) + OldSize 4 bytes Size of the file about to be patched + OldCRC 4 bytes 32-bit CRC of the file to be patched + NewSize 4 bytes Size of the resulting file + NewCRC 4 bytes 32-bit CRC of the resulting file + + Actions and reactions + + Bits Description + ---- ---------------- + 0 Use for autodetection + 1 Treat as selfpatch + 2-3 RESERVED + 4-5 Action (see below) + 6-7 RESERVED + 8-9 Reaction (see below) to absent file + 10-11 Reaction (see below) to newer file + 12-13 Reaction (see below) to unknown file + 14-15 RESERVED + 16-31 RESERVED + + Actions + + Action Value + ------ ----- + none 0 + add 1 + delete 2 + patch 3 + + Reactions + + Reaction Value + -------- ----- + ask 0 + skip 1 + ignore 2 + fail 3 + + - FWKCS MD5 Extra Field: + + The FWKCS Contents_Signature System, used in + automatically identifying files independent of filename, + optionally adds and uses an extra field to support the + rapid creation of an enhanced contents_signature: + + Header ID = 0x4b46 + Data Size = 0x0013 + Preface = 'M','D','5' + followed by 16 bytes containing the uncompressed file's + 128_bit MD5 hash(1), low byte first. + + When FWKCS revises a zipfile central directory to add + this extra field for a file, it also replaces the + central directory entry for that file's uncompressed + filelength with a measured value. + + FWKCS provides an option to strip this extra field, if + present, from a zipfile central directory. In adding + this extra field, FWKCS preserves Zipfile Authenticity + Verification; if stripping this extra field, FWKCS + preserves all versions of AV through PKZIP version 2.04g. + + FWKCS, and FWKCS Contents_Signature System, are + trademarks of Frederick W. Kantor. + + (1) R. Rivest, RFC1321.TXT, MIT Laboratory for Computer + Science and RSA Data Security, Inc., April 1992. + ll.76-77: "The MD5 algorithm is being placed in the + public domain for review and possible adoption as a + standard." + + file comment: (Variable) + + The comment for this file. + + number of this disk: (2 bytes) + + The number of this disk, which contains central + directory end record. + + number of the disk with the start of the central + directory: (2 bytes) + + The number of the disk on which the central + directory starts. + + total number of entries in the central dir on + this disk: (2 bytes) + + The number of central directory entries on this disk. + + total number of entries in the central dir: (2 bytes) + + The total number of files in the zipfile. + + size of the central directory: (4 bytes) + + The size (in bytes) of the entire central directory. + + offset of start of central directory with respect to + the starting disk number: (4 bytes) + + Offset of the start of the central directory on the + disk on which the central directory starts. + + zipfile comment length: (2 bytes) + + The length of the comment for this zipfile. + + zipfile comment: (Variable) + + The comment for this zipfile. + + D. General notes: + + 1) All fields unless otherwise noted are unsigned and stored + in Intel low-byte:high-byte, low-word:high-word order. + + 2) String fields are not null terminated, since the + length is given explicitly. + + 3) Local headers should not span disk boundaries. Also, even + though the central directory can span disk boundaries, no + single record in the central directory should be split + across disks. + + 4) The entries in the central directory may not necessarily + be in the same order that files appear in the zipfile. + +UnShrinking - Method 1 +---------------------- + +Shrinking is a Dynamic Ziv-Lempel-Welch compression algorithm +with partial clearing. The initial code size is 9 bits, and +the maximum code size is 13 bits. Shrinking differs from +conventional Dynamic Ziv-Lempel-Welch implementations in several +respects: + +1) The code size is controlled by the compressor, and is not + automatically increased when codes larger than the current + code size are created (but not necessarily used). When + the decompressor encounters the code sequence 256 + (decimal) followed by 1, it should increase the code size + read from the input stream to the next bit size. No + blocking of the codes is performed, so the next code at + the increased size should be read from the input stream + immediately after where the previous code at the smaller + bit size was read. Again, the decompressor should not + increase the code size used until the sequence 256,1 is + encountered. + +2) When the table becomes full, total clearing is not + performed. Rather, when the compressor emits the code + sequence 256,2 (decimal), the decompressor should clear + all leaf nodes from the Ziv-Lempel tree, and continue to + use the current code size. The nodes that are cleared + from the Ziv-Lempel tree are then re-used, with the lowest + code value re-used first, and the highest code value + re-used last. The compressor can emit the sequence 256,2 + at any time. + +Expanding - Methods 2-5 +----------------------- + +The Reducing algorithm is actually a combination of two +distinct algorithms. The first algorithm compresses repeated +byte sequences, and the second algorithm takes the compressed +stream from the first algorithm and applies a probabilistic +compression method. + +The probabilistic compression stores an array of 'follower +sets' S(j), for j=0 to 255, corresponding to each possible +ASCII character. Each set contains between 0 and 32 +characters, to be denoted as S(j)[0],...,S(j)[m], where m<32. +The sets are stored at the beginning of the data area for a +Reduced file, in reverse order, with S(255) first, and S(0) +last. + +The sets are encoded as { N(j), S(j)[0],...,S(j)[N(j)-1] }, +where N(j) is the size of set S(j). N(j) can be 0, in which +case the follower set for S(j) is empty. Each N(j) value is +encoded in 6 bits, followed by N(j) eight bit character values +corresponding to S(j)[0] to S(j)[N(j)-1] respectively. If +N(j) is 0, then no values for S(j) are stored, and the value +for N(j-1) immediately follows. + +Immediately after the follower sets, is the compressed data +stream. The compressed data stream can be interpreted for the +probabilistic decompression as follows: + +let Last-Character <- 0. +loop until done + if the follower set S(Last-Character) is empty then + read 8 bits from the input stream, and copy this + value to the output stream. + otherwise if the follower set S(Last-Character) is non-empty then + read 1 bit from the input stream. + if this bit is not zero then + read 8 bits from the input stream, and copy this + value to the output stream. + otherwise if this bit is zero then + read B(N(Last-Character)) bits from the input + stream, and assign this value to I. + Copy the value of S(Last-Character)[I] to the + output stream. + + assign the last value placed on the output stream to + Last-Character. +end loop + +B(N(j)) is defined as the minimal number of bits required to +encode the value N(j)-1. + +The decompressed stream from above can then be expanded to +re-create the original file as follows: + +let State <- 0. + +loop until done + read 8 bits from the input stream into C. + case State of + 0: if C is not equal to DLE (144 decimal) then + copy C to the output stream. + otherwise if C is equal to DLE then + let State <- 1. + + 1: if C is non-zero then + let V <- C. + let Len <- L(V) + let State <- F(Len). + otherwise if C is zero then + copy the value 144 (decimal) to the output stream. + let State <- 0 + + 2: let Len <- Len + C + let State <- 3. + + 3: move backwards D(V,C) bytes in the output stream + (if this position is before the start of the output + stream, then assume that all the data before the + start of the output stream is filled with zeros). + copy Len+3 bytes from this position to the output stream. + let State <- 0. + end case +end loop + +The functions F,L, and D are dependent on the 'compression +factor', 1 through 4, and are defined as follows: + +For compression factor 1: + L(X) equals the lower 7 bits of X. + F(X) equals 2 if X equals 127 otherwise F(X) equals 3. + D(X,Y) equals the (upper 1 bit of X) * 256 + Y + 1. +For compression factor 2: + L(X) equals the lower 6 bits of X. + F(X) equals 2 if X equals 63 otherwise F(X) equals 3. + D(X,Y) equals the (upper 2 bits of X) * 256 + Y + 1. +For compression factor 3: + L(X) equals the lower 5 bits of X. + F(X) equals 2 if X equals 31 otherwise F(X) equals 3. + D(X,Y) equals the (upper 3 bits of X) * 256 + Y + 1. +For compression factor 4: + L(X) equals the lower 4 bits of X. + F(X) equals 2 if X equals 15 otherwise F(X) equals 3. + D(X,Y) equals the (upper 4 bits of X) * 256 + Y + 1. + +Imploding - Method 6 +-------------------- + +The Imploding algorithm is actually a combination of two distinct +algorithms. The first algorithm compresses repeated byte +sequences using a sliding dictionary. The second algorithm is +used to compress the encoding of the sliding dictionary output, +using multiple Shannon-Fano trees. + +The Imploding algorithm can use a 4K or 8K sliding dictionary +size. The dictionary size used can be determined by bit 1 in the +general purpose flag word; a 0 bit indicates a 4K dictionary +while a 1 bit indicates an 8K dictionary. + +The Shannon-Fano trees are stored at the start of the compressed +file. The number of trees stored is defined by bit 2 in the +general purpose flag word; a 0 bit indicates two trees stored, a +1 bit indicates three trees are stored. If 3 trees are stored, +the first Shannon-Fano tree represents the encoding of the +Literal characters, the second tree represents the encoding of +the Length information, the third represents the encoding of the +Distance information. When 2 Shannon-Fano trees are stored, the +Length tree is stored first, followed by the Distance tree. + +The Literal Shannon-Fano tree, if present is used to represent +the entire ASCII character set, and contains 256 values. This +tree is used to compress any data not compressed by the sliding +dictionary algorithm. When this tree is present, the Minimum +Match Length for the sliding dictionary is 3. If this tree is +not present, the Minimum Match Length is 2. + +The Length Shannon-Fano tree is used to compress the Length part +of the (length,distance) pairs from the sliding dictionary +output. The Length tree contains 64 values, ranging from the +Minimum Match Length, to 63 plus the Minimum Match Length. + +The Distance Shannon-Fano tree is used to compress the Distance +part of the (length,distance) pairs from the sliding dictionary +output. The Distance tree contains 64 values, ranging from 0 to +63, representing the upper 6 bits of the distance value. The +distance values themselves will be between 0 and the sliding +dictionary size, either 4K or 8K. + +The Shannon-Fano trees themselves are stored in a compressed +format. The first byte of the tree data represents the number of +bytes of data representing the (compressed) Shannon-Fano tree +minus 1. The remaining bytes represent the Shannon-Fano tree +data encoded as: + + High 4 bits: Number of values at this bit length + 1. (1 - 16) + Low 4 bits: Bit Length needed to represent value + 1. (1 - 16) + +The Shannon-Fano codes can be constructed from the bit lengths +using the following algorithm: + +1) Sort the Bit Lengths in ascending order, while retaining the + order of the original lengths stored in the file. + +2) Generate the Shannon-Fano trees: + + Code <- 0 + CodeIncrement <- 0 + LastBitLength <- 0 + i <- number of Shannon-Fano codes - 1 (either 255 or 63) + + loop while i >= 0 + Code = Code + CodeIncrement + if BitLength(i) <> LastBitLength then + LastBitLength=BitLength(i) + CodeIncrement = 1 shifted left (16 - LastBitLength) + ShannonCode(i) = Code + i <- i - 1 + end loop + +3) Reverse the order of all the bits in the above ShannonCode() + vector, so that the most significant bit becomes the least + significant bit. For example, the value 0x1234 (hex) would + become 0x2C48 (hex). + +4) Restore the order of Shannon-Fano codes as originally stored + within the file. + +Example: + + This example will show the encoding of a Shannon-Fano tree + of size 8. Notice that the actual Shannon-Fano trees used + for Imploding are either 64 or 256 entries in size. + +Example: 0x02, 0x42, 0x01, 0x13 + + The first byte indicates 3 values in this table. Decoding the + bytes: + 0x42 = 5 codes of 3 bits long + 0x01 = 1 code of 2 bits long + 0x13 = 2 codes of 4 bits long + + This would generate the original bit length array of: + (3, 3, 3, 3, 3, 2, 4, 4) + + There are 8 codes in this table for the values 0 thru 7. Using + the algorithm to obtain the Shannon-Fano codes produces: + + Reversed Order Original +Val Sorted Constructed Code Value Restored Length +--- ------ ----------------- -------- -------- ------ +0: 2 1100000000000000 11 101 3 +1: 3 1010000000000000 101 001 3 +2: 3 1000000000000000 001 110 3 +3: 3 0110000000000000 110 010 3 +4: 3 0100000000000000 010 100 3 +5: 3 0010000000000000 100 11 2 +6: 4 0001000000000000 1000 1000 4 +7: 4 0000000000000000 0000 0000 4 + +The values in the Val, Order Restored and Original Length columns +now represent the Shannon-Fano encoding tree that can be used for +decoding the Shannon-Fano encoded data. How to parse the +variable length Shannon-Fano values from the data stream is beyond +the scope of this document. (See the references listed at the end of +this document for more information.) However, traditional decoding +schemes used for Huffman variable length decoding, such as the +Greenlaw algorithm, can be successfully applied. + +The compressed data stream begins immediately after the +compressed Shannon-Fano data. The compressed data stream can be +interpreted as follows: + +loop until done + read 1 bit from input stream. + + if this bit is non-zero then (encoded data is literal data) + if Literal Shannon-Fano tree is present + read and decode character using Literal Shannon-Fano tree. + otherwise + read 8 bits from input stream. + copy character to the output stream. + otherwise (encoded data is sliding dictionary match) + if 8K dictionary size + read 7 bits for offset Distance (lower 7 bits of offset). + otherwise + read 6 bits for offset Distance (lower 6 bits of offset). + + using the Distance Shannon-Fano tree, read and decode the + upper 6 bits of the Distance value. + + using the Length Shannon-Fano tree, read and decode + the Length value. + + Length <- Length + Minimum Match Length + + if Length = 63 + Minimum Match Length + read 8 bits from the input stream, + add this value to Length. + + move backwards Distance+1 bytes in the output stream, and + copy Length characters from this position to the output + stream. (if this position is before the start of the output + stream, then assume that all the data before the start of + the output stream is filled with zeros). +end loop + +Tokenizing - Method 7 +-------------------- + +This method is not used by PKZIP. + +Deflating - Method 8 +----------------- + +The Deflate algorithm is similar to the Implode algorithm using +a sliding dictionary of up to 32K with secondary compression +from Huffman/Shannon-Fano codes. + +The compressed data is stored in blocks with a header describing +the block and the Huffman codes used in the data block. The header +format is as follows: + + Bit 0: Last Block bit This bit is set to 1 if this is the last + compressed block in the data. + Bits 1-2: Block type + 00 (0) - Block is stored - All stored data is byte aligned. + Skip bits until next byte, then next word = block + length, followed by the ones compliment of the block + length word. Remaining data in block is the stored + data. + + 01 (1) - Use fixed Huffman codes for literal and distance codes. + Lit Code Bits Dist Code Bits + --------- ---- --------- ---- + 0 - 143 8 0 - 31 5 + 144 - 255 9 + 256 - 279 7 + 280 - 287 8 + + Literal codes 286-287 and distance codes 30-31 are + never used but participate in the huffman construction. + + 10 (2) - Dynamic Huffman codes. (See expanding Huffman codes) + + 11 (3) - Reserved - Flag a "Error in compressed data" if seen. + +Expanding Huffman Codes +----------------------- +If the data block is stored with dynamic Huffman codes, the Huffman +codes are sent in the following compressed format: + + 5 Bits: # of Literal codes sent - 256 (256 - 286) + All other codes are never sent. + 5 Bits: # of Dist codes - 1 (1 - 32) + 4 Bits: # of Bit Length codes - 3 (3 - 19) + +The Huffman codes are sent as bit lengths and the codes are built as +described in the implode algorithm. The bit lengths themselves are +compressed with Huffman codes. There are 19 bit length codes: + + 0 - 15: Represent bit lengths of 0 - 15 + 16: Copy the previous bit length 3 - 6 times. + The next 2 bits indicate repeat length (0 = 3, ... ,3 = 6) + Example: Codes 8, 16 (+2 bits 11), 16 (+2 bits 10) will + expand to 12 bit lengths of 8 (1 + 6 + 5) + 17: Repeat a bit length of 0 for 3 - 10 times. (3 bits of length) + 18: Repeat a bit length of 0 for 11 - 138 times (7 bits of length) + +The lengths of the bit length codes are sent packed 3 bits per value +(0 - 7) in the following order: + + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + +The Huffman codes should be built as described in the Implode algorithm +except codes are assigned starting at the shortest bit length, i.e. the +shortest code should be all 0's rather than all 1's. Also, codes with +a bit length of zero do not participate in the tree construction. The +codes are then used to decode the bit lengths for the literal and +distance tables. + +The bit lengths for the literal tables are sent first with the number +of entries sent described by the 5 bits sent earlier. There are up +to 286 literal characters; the first 256 represent the respective 8 +bit character, code 256 represents the End-Of-Block code, the remaining +29 codes represent copy lengths of 3 thru 258. There are up to 30 +distance codes representing distances from 1 thru 32k as described +below. + + Length Codes + ------------ + Extra Extra Extra Extra + Code Bits Length Code Bits Lengths Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- ---- ---- --------- + 257 0 3 265 1 11,12 273 3 35-42 281 5 131-162 + 258 0 4 266 1 13,14 274 3 43-50 282 5 163-194 + 259 0 5 267 1 15,16 275 3 51-58 283 5 195-226 + 260 0 6 268 1 17,18 276 3 59-66 284 5 227-257 + 261 0 7 269 2 19-22 277 4 67-82 285 0 258 + 262 0 8 270 2 23-26 278 4 83-98 + 263 0 9 271 2 27-30 279 4 99-114 + 264 0 10 272 2 31-34 280 4 115-130 + + Distance Codes + -------------- + Extra Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- ---- ---- -------- + 0 0 1 8 3 17-24 16 7 257-384 24 11 4097-6144 + 1 0 2 9 3 25-32 17 7 385-512 25 11 6145-8192 + 2 0 3 10 4 33-48 18 8 513-768 26 12 8193-12288 + 3 0 4 11 4 49-64 19 8 769-1024 27 12 12289-16384 + 4 1 5,6 12 5 65-96 20 9 1025-1536 28 13 16385-24576 + 5 1 7,8 13 5 97-128 21 9 1537-2048 29 13 24577-32768 + 6 2 9-12 14 6 129-192 22 10 2049-3072 + 7 2 13-16 15 6 193-256 23 10 3073-4096 + +The compressed data stream begins immediately after the +compressed header data. The compressed data stream can be +interpreted as follows: + +do + read header from input stream. + + if stored block + skip bits until byte aligned + read count and 1's compliment of count + copy count bytes data block + otherwise + loop until end of block code sent + decode literal character from input stream + if literal < 256 + copy character to the output stream + otherwise + if literal = end of block + break from loop + otherwise + decode distance from input stream + + move backwards distance bytes in the output stream, and + copy length characters from this position to the output + stream. + end loop +while not last block + +if data descriptor exists + skip bits until byte aligned + read crc and sizes +endif + +Decryption +---------- + +The encryption used in PKZIP was generously supplied by Roger +Schlafly. PKWARE is grateful to Mr. Schlafly for his expert +help and advice in the field of data encryption. + +PKZIP encrypts the compressed data stream. Encrypted files must +be decrypted before they can be extracted. + +Each encrypted file has an extra 12 bytes stored at the start of +the data area defining the encryption header for that file. The +encryption header is originally set to random values, and then +itself encrypted, using three, 32-bit keys. The key values are +initialized using the supplied encryption password. After each byte +is encrypted, the keys are then updated using pseudo-random number +generation techniques in combination with the same CRC-32 algorithm +used in PKZIP and described elsewhere in this document. + +The following is the basic steps required to decrypt a file: + +1) Initialize the three 32-bit keys with the password. +2) Read and decrypt the 12-byte encryption header, further + initializing the encryption keys. +3) Read and decrypt the compressed data stream using the + encryption keys. + +Step 1 - Initializing the encryption keys +----------------------------------------- + +Key(0) <- 305419896 +Key(1) <- 591751049 +Key(2) <- 878082192 + +loop for i <- 0 to length(password)-1 + update_keys(password(i)) +end loop + +Where update_keys() is defined as: + +update_keys(char): + Key(0) <- crc32(key(0),char) + Key(1) <- Key(1) + (Key(0) & 000000ffH) + Key(1) <- Key(1) * 134775813 + 1 + Key(2) <- crc32(key(2),key(1) >> 24) +end update_keys + +Where crc32(old_crc,char) is a routine that given a CRC value and a +character, returns an updated CRC value after applying the CRC-32 +algorithm described elsewhere in this document. + +Step 2 - Decrypting the encryption header +----------------------------------------- + +The purpose of this step is to further initialize the encryption +keys, based on random data, to render a plaintext attack on the +data ineffective. + +Read the 12-byte encryption header into Buffer, in locations +Buffer(0) thru Buffer(11). + +loop for i <- 0 to 11 + C <- buffer(i) ^ decrypt_byte() + update_keys(C) + buffer(i) <- C +end loop + +Where decrypt_byte() is defined as: + +unsigned char decrypt_byte() + local unsigned short temp + temp <- Key(2) | 2 + decrypt_byte <- (temp * (temp ^ 1)) >> 8 +end decrypt_byte + +After the header is decrypted, the last 1 or 2 bytes in Buffer +should be the high-order word/byte of the CRC for the file being +decrypted, stored in Intel low-byte/high-byte order. Versions of +PKZIP prior to 2.0 used a 2 byte CRC check; a 1 byte CRC check is +used on versions after 2.0. This can be used to test if the password +supplied is correct or not. + +Step 3 - Decrypting the compressed data stream +---------------------------------------------- + +The compressed data stream can be decrypted as follows: + +loop until done + read a character into C + Temp <- C ^ decrypt_byte() + update_keys(temp) + output Temp +end loop + +In addition to the above mentioned contributors to PKZIP and PKUNZIP, +I would like to extend special thanks to Robert Mahoney for suggesting +the extension .ZIP for this software. + +References: + + Fiala, Edward R., and Greene, Daniel H., "Data compression with + finite windows", Communications of the ACM, Volume 32, Number 4, + April 1989, pages 490-505. + + Held, Gilbert, "Data Compression, Techniques and Applications, + Hardware and Software Considerations", John Wiley & Sons, 1987. + + Huffman, D.A., "A method for the construction of minimum-redundancy + codes", Proceedings of the IRE, Volume 40, Number 9, September 1952, + pages 1098-1101. + + Nelson, Mark, "LZW Data Compression", Dr. Dobbs Journal, Volume 14, + Number 10, October 1989, pages 29-37. + + Nelson, Mark, "The Data Compression Book", M&T Books, 1991. + + Storer, James A., "Data Compression, Methods and Theory", + Computer Science Press, 1988 + + Welch, Terry, "A Technique for High-Performance Data Compression", + IEEE Computer, Volume 17, Number 6, June 1984, pages 8-19. + + Ziv, J. and Lempel, A., "A universal algorithm for sequential data + compression", Communications of the ACM, Volume 30, Number 6, + June 1987, pages 520-540. + + Ziv, J. and Lempel, A., "Compression of individual sequences via + variable-rate coding", IEEE Transactions on Information Theory, + Volume 24, Number 5, September 1978, pages 530-536. diff --git a/modules/libjar/components.conf b/modules/libjar/components.conf new file mode 100644 index 0000000000..7005454d9d --- /dev/null +++ b/modules/libjar/components.conf @@ -0,0 +1,48 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{88e2fd0b-f7f4-480c-9483-7846b00e8dad}', + 'contract_ids': ['@mozilla.org/libjar/zip-reader;1'], + 'type': 'nsJAR', + 'headers': ['/modules/libjar/nsJAR.h'], + }, + { + 'cid': '{c7e410d4-85f2-11d3-9f63-006008a6efe9}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=jar'], + 'singleton': True, + 'type': 'nsJARProtocolHandler', + 'headers': ['/modules/libjar/nsJARProtocolHandler.h'], + 'constructor': 'nsJARProtocolHandler::GetSingleton', + 'protocol_config': { + 'scheme': 'jar', + 'flags': [ + 'URI_NORELATIVE', + 'URI_NOAUTH', + # URI_LOADABLE_BY_ANYONE, since it's our inner URI that will + # matter anyway. + 'URI_LOADABLE_BY_ANYONE', + ], + }, + }, + { + 'cid': '{245abae2-b947-4ded-a46d-9829d3cca462}', + 'type': 'nsJARURI::Mutator', + 'headers': ['nsJARURI.h'], + }, + { + 'cid': '{19d9161b-a2a9-4518-b2c9-fcb8296d6dcd}', + 'type': 'nsJARURI::Mutator', + 'headers': ['nsJARURI.h'], + }, + { + 'cid': '{608b7f6f-4b60-40d6-87ed-d933bf53d8c1}', + 'contract_ids': ['@mozilla.org/libjar/zip-reader-cache;1'], + 'type': 'nsZipReaderCache', + 'headers': ['/modules/libjar/nsJAR.h'], + }, +] diff --git a/modules/libjar/moz.build b/modules/libjar/moz.build new file mode 100644 index 0000000000..ccd2e2f422 --- /dev/null +++ b/modules/libjar/moz.build @@ -0,0 +1,47 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: JAR") + +if CONFIG["MOZ_ZIPWRITER"]: + DIRS += ["zipwriter"] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +XPIDL_SOURCES += [ + "nsIJARChannel.idl", + "nsIJARURI.idl", + "nsIZipReader.idl", +] + +XPIDL_MODULE = "jar" + +EXPORTS += [ + "nsJARProtocolHandler.h", + "nsJARURI.h", + "nsZipArchive.h", + "zipstruct.h", +] + +UNIFIED_SOURCES += [ + "nsJAR.cpp", + "nsJARChannel.cpp", + "nsJARInputStream.cpp", + "nsJARProtocolHandler.cpp", + "nsJARURI.cpp", + "nsZipArchive.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/modules/libjar/nsIJARChannel.idl b/modules/libjar/nsIJARChannel.idl new file mode 100644 index 0000000000..765752e933 --- /dev/null +++ b/modules/libjar/nsIJARChannel.idl @@ -0,0 +1,38 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#include "nsIChannel.idl" + +interface nsIFile; +interface nsIZipEntry; + +[scriptable, builtinclass, uuid(e72b179b-d5df-4d87-b5de-fd73a65c60f6)] +interface nsIJARChannel : nsIChannel +{ + /** + * Returns the JAR file. May be null if the jar is remote. + * Setting the JAR file is optional and overrides the JAR + * file used for local file JARs. Setting the JAR file after + * the channel has been opened is not permitted. + */ + attribute nsIFile jarFile; + + /** + * Returns the zip entry if the file is synchronously accessible. + * This will work even without opening the channel. + */ + readonly attribute nsIZipEntry zipEntry; + + /** + * If the JAR file is cached in the JAR cache, returns true and + * holds a reference to the cached zip reader to be used when + * the channel is read from, ensuring the cached reader will be used. + * For a successful read from the cached reader, close() should not + * be called on the reader--per nsIZipReader::getZip() documentation. + * Returns false if the JAR file is not cached. Calling this method + * after the channel has been opened is not permitted. + */ + boolean ensureCached(); +}; diff --git a/modules/libjar/nsIJARURI.idl b/modules/libjar/nsIJARURI.idl new file mode 100644 index 0000000000..702e587159 --- /dev/null +++ b/modules/libjar/nsIJARURI.idl @@ -0,0 +1,41 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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/. */ + +#include "nsIURL.idl" + +/** + * JAR URLs have the following syntax + * + * jar:<jar-file-uri>!/<jar-entry> + * + * EXAMPLE: jar:http://www.big.com/blue.jar!/ocean.html + * + * The nsIURL methods operate on the <jar-entry> part of the spec. + */ +[scriptable, builtinclass, uuid(646a508c-f786-4e14-be6d-8dda2a633c60)] +interface nsIJARURI : nsIURL { + + /** + * Returns the root URI (the one for the actual JAR file) for this JAR + * (e.g., http://www.big.com/blue.jar). + */ + readonly attribute nsIURI JARFile; + + /** + * Returns the entry specified for this JAR URI (e.g., "ocean.html"). This + * value may contain %-escaped byte sequences. + */ + readonly attribute AUTF8String JAREntry; +}; + +[uuid(d66df117-eda7-4324-b4e4-1f670ff6718e)] +interface nsIJARURIMutator : nsISupports +{ + /** + * Will initialize a URI using the passed spec, baseURI and charset. + */ + void setSpecBaseCharset(in AUTF8String aSpec, in nsIURI aBase, in string aCharset); +}; diff --git a/modules/libjar/nsIZipReader.idl b/modules/libjar/nsIZipReader.idl new file mode 100644 index 0000000000..7c00a7074e --- /dev/null +++ b/modules/libjar/nsIZipReader.idl @@ -0,0 +1,260 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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/. */ + +#include "nsISupports.idl" + +%{C++ +struct PRFileDesc; +%} + +[ptr] native PRFileDescStar(PRFileDesc); + +interface nsIUTF8StringEnumerator; +interface nsIInputStream; +interface nsIFile; + +[scriptable, builtinclass, uuid(fad6f72f-13d8-4e26-9173-53007a4afe71)] +interface nsIZipEntry : nsISupports +{ + /** + * The type of compression used for the item. The possible values and + * their meanings are defined in the zip file specification at + * http://www.pkware.com/business_and_developers/developer/appnote/ + */ + [infallible] readonly attribute unsigned short compression; + /** + * The compressed size of the data in the item. + */ + [infallible] readonly attribute unsigned long size; + /** + * The uncompressed size of the data in the item. + */ + [infallible] readonly attribute unsigned long realSize; + /** + * The CRC-32 hash of the file in the entry. + */ + [infallible] readonly attribute unsigned long CRC32; + /** + * True if the name of the entry ends with '/' and false otherwise. + */ + [infallible] readonly attribute boolean isDirectory; + /** + * The time at which this item was last modified. + */ + readonly attribute PRTime lastModifiedTime; + /** + * Use this attribute to determine whether this item is an actual zip entry + * or is one synthesized for part of a real entry's path. A synthesized + * entry represents a directory within the zip file which has no + * corresponding entry within the zip file. For example, the entry for the + * directory foo/ in a zip containing exactly one entry for foo/bar.txt + * is synthetic. If the zip file contains an actual entry for a directory, + * this attribute will be false for the nsIZipEntry for that directory. + * It is impossible for a file to be synthetic. + */ + [infallible] readonly attribute boolean isSynthetic; + /** + * The UNIX style file permissions of this item. + */ + [infallible] readonly attribute unsigned long permissions; +}; + +[scriptable, uuid(9ba4ef54-e0a0-4f65-9d23-128482448885)] +interface nsIZipReader : nsISupports +{ + /** + * Opens a zip file for reading. + * It is allowed to open with another file, + * but it needs to be closed first with close(). + */ + void open(in nsIFile zipFile); + + /** + * Opens a zip file inside a zip file for reading. + */ + void openInner(in nsIZipReader zipReader, in AUTF8String zipEntry); + + /** + * The file that represents the zip with which this zip reader was + * initialized. This will be null if there is no underlying file. + */ + readonly attribute nsIFile file; + + /** + * Closes a zip reader. Subsequent attempts to extract files or read from + * its input stream will result in an error. + * + * Subsequent attempts to access a nsIZipEntry obtained from this zip + * reader will cause unspecified behavior. + */ + void close(); + + /** + * Tests the integrity of the archive by performing a CRC check + * on each item expanded into memory. If an entry is specified + * the integrity of only that item is tested. If null (javascript) + * or ""_ns (c++) is passed in the integrity of all items + * in the archive are tested. + */ + void test(in AUTF8String aEntryName); + + /** + * Extracts a zip entry into a local file specified by outFile. + * The entry must be stored in the zip in either uncompressed or + * DEFLATE-compressed format for the extraction to be successful. + * If the entry is a directory, the directory will be extracted + * non-recursively. + */ + void extract(in AUTF8String zipEntry, in nsIFile outFile); + + /** + * Returns a nsIZipEntry describing a specified zip entry. + */ + nsIZipEntry getEntry(in AUTF8String zipEntry); + + /** + * Checks whether the zipfile contains an entry specified by entryName. + */ + boolean hasEntry(in AUTF8String zipEntry); + + /** + * Returns a string enumerator containing the matching entry names. + * + * @param aPattern + * A regular expression used to find matching entries in the zip file. + * Set this parameter to null (javascript) or ""_ns (c++) or "*" + * to get all entries; otherwise, use the + * following syntax: + * + * o * matches anything + * o ? matches one character + * o $ matches the end of the string + * o [abc] matches one occurrence of a, b, or c. The only character that + * must be escaped inside the brackets is ]. ^ and - must never + * appear in the first and second positions within the brackets, + * respectively. (In the former case, the behavior specified for + * '[^az]' will happen.) + * o [a-z] matches any character between a and z. The characters a and z + * must either both be letters or both be numbers, with the + * character represented by 'a' having a lower ASCII value than + * the character represented by 'z'. + * o [^az] matches any character except a or z. If ] is to appear inside + * the brackets as a character to not match, it must be escaped. + * o pat~pat2 returns matches to the pattern 'pat' which do not also match + * the pattern 'pat2'. This may be used to perform filtering + * upon the results of one pattern to remove all matches which + * also match another pattern. For example, because '*' + * matches any string and '*z*' matches any string containing a + * 'z', '*~*z*' will match all strings except those containing + * a 'z'. Note that a pattern may not use '~' multiple times, + * so a string such as '*~*z*~*y*' is not a valid pattern. + * o (foo|bar) will match either the pattern foo or the pattern bar. + * Neither of the patterns foo or bar may use the 'pat~pat2' + * syntax described immediately above. + * o \ will escape a special character. Escaping is required for all + * special characters unless otherwise specified. + * o All other characters match case-sensitively. + * + * An aPattern not conforming to this syntax has undefined behavior. + * + * @throws NS_ERROR_ILLEGAL_VALUE on many but not all invalid aPattern + * values. + */ + nsIUTF8StringEnumerator findEntries(in AUTF8String aPattern); + + /** + * Returns an input stream containing the contents of the specified zip + * entry. + * @param zipEntry the name of the entry to open the stream from + */ + nsIInputStream getInputStream(in AUTF8String zipEntry); + + /** + * Returns an input stream containing the contents of the specified zip + * entry. If the entry refers to a directory (ends with '/'), a directory stream + * is opened, otherwise the contents of the file entry is returned. + * @param aJarSpec the Spec of the URI for the JAR (only used for directory streams) + * @param zipEntry the name of the entry to open the stream from + */ + nsIInputStream getInputStreamWithSpec(in AUTF8String aJarSpec, in AUTF8String zipEntry); +}; + +//////////////////////////////////////////////////////////////////////////////// +// nsIZipReaderCache + +[scriptable, uuid(31179807-9fcd-46c4-befa-2ade209a394b)] +interface nsIZipReaderCache : nsISupports +{ + /** + * Initializes a new zip reader cache. + * @param cacheSize - the number of released entries to maintain before + * beginning to throw some out (note that the number of outstanding + * entries can be much greater than this number -- this is the count + * for those otherwise unused entries) + */ + void init(in unsigned long cacheSize); + + /** + * Returns a (possibly shared) nsIZipReader for an nsIFile. + * + * If the zip reader for given file is not in the cache, a new zip reader + * is created, initialized, and opened (see nsIZipReader::init and + * nsIZipReader::open). Otherwise the previously created zip reader is + * returned. + * + * @note If someone called close() on the shared nsIZipReader, this method + * will return the closed zip reader. + */ + nsIZipReader getZip(in nsIFile zipFile); + + /** + * Like getZip(), returns a (possibly shared) nsIZipReader for an nsIFile, + * but if a zip reader for the given file is not in the cache, returns + * error NS_ERROR_CACHE_KEY_NOT_FOUND rather than creating a new reader. + * + * @note If someone called close() on the shared nsIZipReader, this method + * will return the closed zip reader. + */ + nsIZipReader getZipIfCached(in nsIFile zipFile); + + /** + * returns true if this zipreader already has this file cached + */ + bool isCached(in nsIFile zipFile); + + /** + * Returns a (possibly shared) nsIZipReader for a zip inside another zip + * + * See getZip + */ + nsIZipReader getInnerZip(in nsIFile zipFile, in AUTF8String zipEntry); + + /** + * Returns the cached NSPR file descriptor of the file. + * Note: currently not supported on Windows platform. + */ + PRFileDescStar getFd(in nsIFile zipFile); +}; + +//////////////////////////////////////////////////////////////////////////////// + +%{C++ + +#define NS_ZIPREADER_CID \ +{ /* 88e2fd0b-f7f4-480c-9483-7846b00e8dad */ \ + 0x88e2fd0b, 0xf7f4, 0x480c, \ + { 0x94, 0x83, 0x78, 0x46, 0xb0, 0x0e, 0x8d, 0xad } \ +} + +#define NS_ZIPREADERCACHE_CID \ +{ /* 608b7f6f-4b60-40d6-87ed-d933bf53d8c1 */ \ + 0x608b7f6f, 0x4b60, 0x40d6, \ + { 0x87, 0xed, 0xd9, 0x33, 0xbf, 0x53, 0xd8, 0xc1 } \ +} + +%} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/modules/libjar/nsJAR.cpp b/modules/libjar/nsJAR.cpp new file mode 100644 index 0000000000..e61066c69d --- /dev/null +++ b/modules/libjar/nsJAR.cpp @@ -0,0 +1,885 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#include <string.h> +#include "nsJARInputStream.h" +#include "nsJAR.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Logging.h" +#include "mozilla/Omnijar.h" +#include "mozilla/Unused.h" + +#ifdef XP_UNIX +# include <sys/stat.h> +#elif defined(XP_WIN) +# include <io.h> +#endif + +using namespace mozilla; + +static LazyLogModule gJarLog("nsJAR"); + +#ifdef LOG +# undef LOG +#endif +#ifdef LOG_ENABLED +# undef LOG_ENABLED +#endif + +#define LOG(args) MOZ_LOG(gJarLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(gJarLog, mozilla::LogLevel::Debug) + +//---------------------------------------------- +// nsJAR constructor/destructor +//---------------------------------------------- + +// The following initialization makes a guess of 10 entries per jarfile. +nsJAR::nsJAR() + : mReleaseTime(PR_INTERVAL_NO_TIMEOUT), + mLock("nsJAR::mLock"), + mCache(nullptr) {} + +nsJAR::~nsJAR() { Close(); } + +NS_IMPL_QUERY_INTERFACE(nsJAR, nsIZipReader) +NS_IMPL_ADDREF(nsJAR) + +// Custom Release method works with nsZipReaderCache... +// Release might be called from multi-thread, we have to +// take this function carefully to avoid delete-after-use. +MozExternalRefCountType nsJAR::Release(void) { + nsrefcnt count; + MOZ_ASSERT(0 != mRefCnt, "dup release"); + + RefPtr<nsZipReaderCache> cache; + if (mRefCnt == 2) { // don't use a lock too frequently + // Use a mutex here to guarantee mCache is not racing and the target + // instance is still valid to increase ref-count. + RecursiveMutexAutoLock lock(mLock); + cache = mCache; + mCache = nullptr; + } + if (cache) { + DebugOnly<nsresult> rv = cache->ReleaseZip(this); + MOZ_ASSERT(NS_SUCCEEDED(rv), "failed to release zip file"); + } + + count = --mRefCnt; // don't access any member variable after this line + NS_LOG_RELEASE(this, count, "nsJAR"); + if (0 == count) { + mRefCnt = 1; /* stabilize */ + /* enable this to find non-threadsafe destructors: */ + /* NS_ASSERT_OWNINGTHREAD(nsJAR); */ + delete this; + return 0; + } + + return count; +} + +//---------------------------------------------- +// nsIZipReader implementation +//---------------------------------------------- + +NS_IMETHODIMP +nsJAR::Open(nsIFile* zipFile) { + NS_ENSURE_ARG_POINTER(zipFile); + RecursiveMutexAutoLock lock(mLock); + LOG(("Open[%p] %s", this, zipFile->HumanReadablePath().get())); + if (mZip) return NS_ERROR_FAILURE; // Already open! + + mZipFile = zipFile; + mOuterZipEntry.Truncate(); + + // The omnijar is special, it is opened early on and closed late + RefPtr<nsZipArchive> zip = mozilla::Omnijar::GetReader(zipFile); + if (!zip) { + zip = nsZipArchive::OpenArchive(zipFile); + } + mZip = zip; + return mZip ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJAR::OpenInner(nsIZipReader* aZipReader, const nsACString& aZipEntry) { + nsresult rv; + + LOG(("OpenInner[%p] %s", this, PromiseFlatCString(aZipEntry).get())); + NS_ENSURE_ARG_POINTER(aZipReader); + + nsCOMPtr<nsIFile> zipFile; + rv = aZipReader->GetFile(getter_AddRefs(zipFile)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsZipArchive> innerZip = + mozilla::Omnijar::GetInnerReader(zipFile, aZipEntry); + if (innerZip) { + RecursiveMutexAutoLock lock(mLock); + if (mZip) { + return NS_ERROR_FAILURE; + } + mZip = innerZip; + return NS_OK; + } + + bool exist; + rv = aZipReader->HasEntry(aZipEntry, &exist); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(exist, NS_ERROR_FILE_NOT_FOUND); + + RefPtr<nsZipHandle> handle; + { + nsJAR* outerJAR = static_cast<nsJAR*>(aZipReader); + RecursiveMutexAutoLock outerLock(outerJAR->mLock); + rv = nsZipHandle::Init(outerJAR->mZip.get(), + PromiseFlatCString(aZipEntry).get(), + getter_AddRefs(handle)); + NS_ENSURE_SUCCESS(rv, rv); + } + + RecursiveMutexAutoLock lock(mLock); + MOZ_ASSERT(!mZip, "Another thread tried to open this nsJAR racily!"); + mZipFile = zipFile.forget(); + mOuterZipEntry.Assign(aZipEntry); + mZip = nsZipArchive::OpenArchive(handle); + return mZip ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJAR::GetFile(nsIFile** result) { + RecursiveMutexAutoLock lock(mLock); + LOG(("GetFile[%p]", this)); + *result = mZipFile; + NS_IF_ADDREF(*result); + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::Close() { + RecursiveMutexAutoLock lock(mLock); + LOG(("Close[%p]", this)); + if (!mZip) { + return NS_ERROR_FAILURE; // Never opened or already closed. + } + + mZip = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::Test(const nsACString& aEntryName) { + RecursiveMutexAutoLock lock(mLock); + if (!mZip) { + return NS_ERROR_FAILURE; + } + return mZip->Test( + aEntryName.IsEmpty() ? nullptr : PromiseFlatCString(aEntryName).get()); +} + +NS_IMETHODIMP +nsJAR::Extract(const nsACString& aEntryName, nsIFile* outFile) { + // nsZipArchive and zlib are not thread safe + // we need to use a lock to prevent bug #51267 + RecursiveMutexAutoLock lock(mLock); + if (!mZip) { + return NS_ERROR_FAILURE; + } + + LOG(("Extract[%p] %s", this, PromiseFlatCString(aEntryName).get())); + nsZipItem* item = mZip->GetItem(PromiseFlatCString(aEntryName).get()); + NS_ENSURE_TRUE(item, NS_ERROR_FILE_NOT_FOUND); + + // Remove existing file or directory so we set permissions correctly. + // If it's a directory that already exists and contains files, throw + // an exception and return. + + nsresult rv = outFile->Remove(false); + if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY || rv == NS_ERROR_FAILURE) return rv; + + if (item->IsDirectory()) { + rv = outFile->Create(nsIFile::DIRECTORY_TYPE, item->Mode()); + // XXX Do this in nsZipArchive? It would be nice to keep extraction + // XXX code completely there, but that would require a way to get a + // XXX PRDir from outFile. + } else { + PRFileDesc* fd; + rv = outFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, item->Mode(), + &fd); + if (NS_FAILED(rv)) return rv; + + // ExtractFile also closes the fd handle and resolves the symlink if needed + rv = mZip->ExtractFile(item, outFile, fd); + } + if (NS_FAILED(rv)) return rv; + + // nsIFile needs milliseconds, while prtime is in microseconds. + // non-fatal if this fails, ignore errors + outFile->SetLastModifiedTime(item->LastModTime() / PR_USEC_PER_MSEC); + + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::GetEntry(const nsACString& aEntryName, nsIZipEntry** result) { + RecursiveMutexAutoLock lock(mLock); + LOG(("GetEntry[%p] %s", this, PromiseFlatCString(aEntryName).get())); + if (!mZip) { + return NS_ERROR_FAILURE; + } + nsZipItem* zipItem = mZip->GetItem(PromiseFlatCString(aEntryName).get()); + NS_ENSURE_TRUE(zipItem, NS_ERROR_FILE_NOT_FOUND); + + nsJARItem* jarItem = new nsJARItem(zipItem); + + NS_ADDREF(*result = jarItem); + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::HasEntry(const nsACString& aEntryName, bool* result) { + RecursiveMutexAutoLock lock(mLock); + LOG(("HasEntry[%p] %s", this, PromiseFlatCString(aEntryName).get())); + if (!mZip) { + return NS_ERROR_FAILURE; + } + *result = mZip->GetItem(PromiseFlatCString(aEntryName).get()) != nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::FindEntries(const nsACString& aPattern, + nsIUTF8StringEnumerator** result) { + NS_ENSURE_ARG_POINTER(result); + RecursiveMutexAutoLock lock(mLock); + LOG(("FindEntries[%p] %s", this, PromiseFlatCString(aPattern).get())); + if (!mZip) { + return NS_ERROR_FAILURE; + } + + nsZipFind* find; + nsresult rv = mZip->FindInit( + aPattern.IsEmpty() ? nullptr : PromiseFlatCString(aPattern).get(), &find); + NS_ENSURE_SUCCESS(rv, rv); + + nsIUTF8StringEnumerator* zipEnum = new nsJAREnumerator(find); + + NS_ADDREF(*result = zipEnum); + return NS_OK; +} + +NS_IMETHODIMP +nsJAR::GetInputStream(const nsACString& aFilename, nsIInputStream** result) { + return GetInputStreamWithSpec(""_ns, aFilename, result); +} + +NS_IMETHODIMP +nsJAR::GetInputStreamWithSpec(const nsACString& aJarDirSpec, + const nsACString& aEntryName, + nsIInputStream** result) { + NS_ENSURE_ARG_POINTER(result); + RecursiveMutexAutoLock lock(mLock); + if (!mZip) { + return NS_ERROR_FAILURE; + } + + LOG(("GetInputStreamWithSpec[%p] %s %s", this, + PromiseFlatCString(aJarDirSpec).get(), + PromiseFlatCString(aEntryName).get())); + // Watch out for the jar:foo.zip!/ (aDir is empty) top-level special case! + nsZipItem* item = nullptr; + const nsCString& entry = PromiseFlatCString(aEntryName); + if (*entry.get()) { + // First check if item exists in jar + item = mZip->GetItem(entry.get()); + if (!item) return NS_ERROR_FILE_NOT_FOUND; + } + nsJARInputStream* jis = new nsJARInputStream(); + // addref now so we can call InitFile/InitDirectory() + NS_ADDREF(*result = jis); + + nsresult rv = NS_OK; + if (!item || item->IsDirectory()) { + rv = jis->InitDirectory(this, aJarDirSpec, entry.get()); + } else { + RefPtr<nsZipHandle> fd = mZip->GetFD(); + rv = jis->InitFile(fd, mZip->GetData(item), item); + } + if (NS_FAILED(rv)) { + NS_RELEASE(*result); + } + return rv; +} + +nsresult nsJAR::GetFullJarPath(nsACString& aResult) { + RecursiveMutexAutoLock lock(mLock); + NS_ENSURE_ARG_POINTER(mZipFile); + + nsresult rv = mZipFile->GetPersistentDescriptor(aResult); + if (NS_FAILED(rv)) { + return rv; + } + + if (mOuterZipEntry.IsEmpty()) { + aResult.InsertLiteral("file:", 0); + } else { + aResult.InsertLiteral("jar:", 0); + aResult.AppendLiteral("!/"); + aResult.Append(mOuterZipEntry); + } + return NS_OK; +} + +nsresult nsJAR::GetNSPRFileDesc(PRFileDesc** aNSPRFileDesc) { + RecursiveMutexAutoLock lock(mLock); + if (!aNSPRFileDesc) { + return NS_ERROR_ILLEGAL_VALUE; + } + *aNSPRFileDesc = nullptr; + + if (!mZip) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsZipHandle> handle = mZip->GetFD(); + if (!handle) { + return NS_ERROR_FAILURE; + } + + return handle->GetNSPRFileDesc(aNSPRFileDesc); +} + +//---------------------------------------------- +// nsJAR private implementation +//---------------------------------------------- +nsresult nsJAR::LoadEntry(const nsACString& aFilename, nsCString& aBuf) { + //-- Get a stream for reading the file + nsresult rv; + nsCOMPtr<nsIInputStream> manifestStream; + rv = GetInputStream(aFilename, getter_AddRefs(manifestStream)); + if (NS_FAILED(rv)) return NS_ERROR_FILE_NOT_FOUND; + + //-- Read the manifest file into memory + char* buf; + uint64_t len64; + rv = manifestStream->Available(&len64); + if (NS_FAILED(rv)) return rv; + NS_ENSURE_TRUE(len64 < UINT32_MAX, NS_ERROR_FILE_CORRUPTED); // bug 164695 + uint32_t len = (uint32_t)len64; + buf = (char*)malloc(len + 1); + if (!buf) return NS_ERROR_OUT_OF_MEMORY; + uint32_t bytesRead; + rv = manifestStream->Read(buf, len, &bytesRead); + if (bytesRead != len) { + rv = NS_ERROR_FILE_CORRUPTED; + } + if (NS_FAILED(rv)) { + free(buf); + return rv; + } + buf[len] = '\0'; // Null-terminate the buffer + aBuf.Adopt(buf, len); + return NS_OK; +} + +int32_t nsJAR::ReadLine(const char** src) { + if (!*src) { + return 0; + } + + //--Moves pointer to beginning of next line and returns line length + // not including CR/LF. + int32_t length; + const char* eol = strpbrk(*src, "\r\n"); + + if (eol == nullptr) // Probably reached end of file before newline + { + length = strlen(*src); + if (length == 0) // immediate end-of-file + *src = nullptr; + else // some data left on this line + *src += length; + } else { + length = eol - *src; + if (eol[0] == '\r' && eol[1] == '\n') // CR LF, so skip 2 + *src = eol + 2; + else // Either CR or LF, so skip 1 + *src = eol + 1; + } + return length; +} + +NS_IMPL_ISUPPORTS(nsJAREnumerator, nsIUTF8StringEnumerator, nsIStringEnumerator) + +//---------------------------------------------- +// nsJAREnumerator::HasMore +//---------------------------------------------- +NS_IMETHODIMP +nsJAREnumerator::HasMore(bool* aResult) { + // try to get the next element + if (!mName) { + NS_ASSERTION(mFind, "nsJAREnumerator: Missing zipFind."); + nsresult rv = mFind->FindNext(&mName, &mNameLen); + if (rv == NS_ERROR_FILE_NOT_FOUND) { + *aResult = false; // No more matches available + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); // no error translation + } + + *aResult = true; + return NS_OK; +} + +//---------------------------------------------- +// nsJAREnumerator::GetNext +//---------------------------------------------- +NS_IMETHODIMP +nsJAREnumerator::GetNext(nsACString& aResult) { + // check if the current item is "stale" + if (!mName) { + bool bMore; + nsresult rv = HasMore(&bMore); + if (NS_FAILED(rv) || !bMore) + return NS_ERROR_FAILURE; // no error translation + } + aResult.Assign(mName, mNameLen); + mName = 0; // we just gave this one away + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsJARItem, nsIZipEntry) + +nsJARItem::nsJARItem(nsZipItem* aZipItem) + : mSize(aZipItem->Size()), + mRealsize(aZipItem->RealSize()), + mCrc32(aZipItem->CRC32()), + mLastModTime(aZipItem->LastModTime()), + mCompression(aZipItem->Compression()), + mPermissions(aZipItem->Mode()), + mIsDirectory(aZipItem->IsDirectory()), + mIsSynthetic(aZipItem->isSynthetic) {} + +//------------------------------------------ +// nsJARItem::GetCompression +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetCompression(uint16_t* aCompression) { + NS_ENSURE_ARG_POINTER(aCompression); + + *aCompression = mCompression; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetSize +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetSize(uint32_t* aSize) { + NS_ENSURE_ARG_POINTER(aSize); + + *aSize = mSize; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetRealSize +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetRealSize(uint32_t* aRealsize) { + NS_ENSURE_ARG_POINTER(aRealsize); + + *aRealsize = mRealsize; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetCrc32 +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetCRC32(uint32_t* aCrc32) { + NS_ENSURE_ARG_POINTER(aCrc32); + + *aCrc32 = mCrc32; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetIsDirectory +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetIsDirectory(bool* aIsDirectory) { + NS_ENSURE_ARG_POINTER(aIsDirectory); + + *aIsDirectory = mIsDirectory; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetIsSynthetic +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetIsSynthetic(bool* aIsSynthetic) { + NS_ENSURE_ARG_POINTER(aIsSynthetic); + + *aIsSynthetic = mIsSynthetic; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetLastModifiedTime +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetLastModifiedTime(PRTime* aLastModTime) { + NS_ENSURE_ARG_POINTER(aLastModTime); + + *aLastModTime = mLastModTime; + return NS_OK; +} + +//------------------------------------------ +// nsJARItem::GetPermissions +//------------------------------------------ +NS_IMETHODIMP +nsJARItem::GetPermissions(uint32_t* aPermissions) { + NS_ENSURE_ARG_POINTER(aPermissions); + + *aPermissions = mPermissions; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIZipReaderCache + +NS_IMPL_ISUPPORTS(nsZipReaderCache, nsIZipReaderCache, nsIObserver, + nsISupportsWeakReference) + +nsZipReaderCache::nsZipReaderCache() + : mLock("nsZipReaderCache.mLock"), + mCacheSize(0), + mZips() +#ifdef ZIP_CACHE_HIT_RATE + , + mZipCacheLookups(0), + mZipCacheHits(0), + mZipCacheFlushes(0), + mZipSyncMisses(0) +#endif +{ +} + +NS_IMETHODIMP +nsZipReaderCache::Init(uint32_t cacheSize) { + MutexAutoLock lock(mLock); + mCacheSize = cacheSize; + + // Register as a memory pressure observer + nsCOMPtr<nsIObserverService> os = + do_GetService("@mozilla.org/observer-service;1"); + if (os) { + os->AddObserver(this, "memory-pressure", true); + os->AddObserver(this, "chrome-flush-caches", true); + os->AddObserver(this, "flush-cache-entry", true); + } + // ignore failure of the observer registration. + + return NS_OK; +} + +nsZipReaderCache::~nsZipReaderCache() { + for (const auto& zip : mZips.Values()) { + zip->SetZipReaderCache(nullptr); + } + +#ifdef ZIP_CACHE_HIT_RATE + printf( + "nsZipReaderCache size=%d hits=%d lookups=%d rate=%f%% flushes=%d missed " + "%d\n", + mCacheSize, mZipCacheHits, mZipCacheLookups, + (float)mZipCacheHits / mZipCacheLookups, mZipCacheFlushes, + mZipSyncMisses); +#endif +} + +NS_IMETHODIMP +nsZipReaderCache::IsCached(nsIFile* zipFile, bool* aResult) { + NS_ENSURE_ARG_POINTER(zipFile); + nsresult rv; + MutexAutoLock lock(mLock); + + nsAutoCString uri; + rv = zipFile->GetPersistentDescriptor(uri); + if (NS_FAILED(rv)) return rv; + + uri.InsertLiteral("file:", 0); + + *aResult = mZips.Contains(uri); + return NS_OK; +} + +nsresult nsZipReaderCache::GetZip(nsIFile* zipFile, nsIZipReader** result, + bool failOnMiss) { + NS_ENSURE_ARG_POINTER(zipFile); + nsresult rv; + MutexAutoLock lock(mLock); + +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheLookups++; +#endif + + nsAutoCString uri; + rv = zipFile->GetPersistentDescriptor(uri); + if (NS_FAILED(rv)) return rv; + + uri.InsertLiteral("file:", 0); + + RefPtr<nsJAR> zip; + mZips.Get(uri, getter_AddRefs(zip)); + if (zip) { +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheHits++; +#endif + zip->ClearReleaseTime(); + } else { + if (failOnMiss) { + return NS_ERROR_CACHE_KEY_NOT_FOUND; + } + + zip = new nsJAR(); + zip->SetZipReaderCache(this); + rv = zip->Open(zipFile); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(!mZips.Contains(uri)); + mZips.InsertOrUpdate(uri, RefPtr{zip}); + } + zip.forget(result); + return rv; +} + +NS_IMETHODIMP +nsZipReaderCache::GetZipIfCached(nsIFile* zipFile, nsIZipReader** result) { + return GetZip(zipFile, result, true); +} + +NS_IMETHODIMP +nsZipReaderCache::GetZip(nsIFile* zipFile, nsIZipReader** result) { + return GetZip(zipFile, result, false); +} + +NS_IMETHODIMP +nsZipReaderCache::GetInnerZip(nsIFile* zipFile, const nsACString& entry, + nsIZipReader** result) { + NS_ENSURE_ARG_POINTER(zipFile); + + nsCOMPtr<nsIZipReader> outerZipReader; + nsresult rv = GetZip(zipFile, getter_AddRefs(outerZipReader)); + NS_ENSURE_SUCCESS(rv, rv); + + MutexAutoLock lock(mLock); + +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheLookups++; +#endif + + nsAutoCString uri; + rv = zipFile->GetPersistentDescriptor(uri); + if (NS_FAILED(rv)) return rv; + + uri.InsertLiteral("jar:", 0); + uri.AppendLiteral("!/"); + uri.Append(entry); + + RefPtr<nsJAR> zip; + mZips.Get(uri, getter_AddRefs(zip)); + if (zip) { +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheHits++; +#endif + zip->ClearReleaseTime(); + } else { + zip = new nsJAR(); + zip->SetZipReaderCache(this); + + rv = zip->OpenInner(outerZipReader, entry); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(!mZips.Contains(uri)); + mZips.InsertOrUpdate(uri, RefPtr{zip}); + } + zip.forget(result); + return rv; +} + +NS_IMETHODIMP +nsZipReaderCache::GetFd(nsIFile* zipFile, PRFileDesc** aRetVal) { +#if defined(XP_WIN) + MOZ_CRASH("Not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; +#else + if (!zipFile) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + nsAutoCString uri; + rv = zipFile->GetPersistentDescriptor(uri); + if (NS_FAILED(rv)) { + return rv; + } + uri.InsertLiteral("file:", 0); + + MutexAutoLock lock(mLock); + RefPtr<nsJAR> zip; + mZips.Get(uri, getter_AddRefs(zip)); + if (!zip) { + return NS_ERROR_FAILURE; + } + + zip->ClearReleaseTime(); + rv = zip->GetNSPRFileDesc(aRetVal); + // Do this to avoid possible deadlock on mLock with ReleaseZip(). + { + MutexAutoUnlock unlock(mLock); + zip = nullptr; + } + return rv; +#endif /* XP_WIN */ +} + +nsresult nsZipReaderCache::ReleaseZip(nsJAR* zip) { + nsresult rv; + MutexAutoLock lock(mLock); + + // It is possible that two thread compete for this zip. The dangerous + // case is where one thread Releases the zip and discovers that the ref + // count has gone to one. Before it can call this ReleaseZip method + // another thread calls our GetZip method. The ref count goes to two. That + // second thread then Releases the zip and the ref count goes to one. It + // then tries to enter this ReleaseZip method and blocks while the first + // thread is still here. The first thread continues and remove the zip from + // the cache and calls its Release method sending the ref count to 0 and + // deleting the zip. However, the second thread is still blocked at the + // start of ReleaseZip, but the 'zip' param now hold a reference to a + // deleted zip! + // + // So, we are going to try safeguarding here by searching our hashtable while + // locked here for the zip. We return fast if it is not found. + + bool found = false; + for (const auto& current : mZips.Values()) { + if (zip == current) { + found = true; + break; + } + } + + if (!found) { +#ifdef ZIP_CACHE_HIT_RATE + mZipSyncMisses++; +#endif + return NS_OK; + } + + zip->SetReleaseTime(); + + if (mZips.Count() <= mCacheSize) return NS_OK; + + // Find the oldest zip. + nsJAR* oldest = nullptr; + for (const auto& current : mZips.Values()) { + PRIntervalTime currentReleaseTime = current->GetReleaseTime(); + if (currentReleaseTime != PR_INTERVAL_NO_TIMEOUT) { + if (oldest == nullptr || currentReleaseTime < oldest->GetReleaseTime()) { + oldest = current; + } + } + } + + // Because of the craziness above it is possible that there is no zip that + // needs removing. + if (!oldest) return NS_OK; + +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheFlushes++; +#endif + + // remove from hashtable + nsAutoCString uri; + rv = oldest->GetFullJarPath(uri); + if (NS_FAILED(rv)) { + return rv; + } + + // Retrieving and removing the JAR should be done without an extra AddRef + // and Release, or we'll trigger nsJAR::Release's magic refcount 1 case + // an extra time. + RefPtr<nsJAR> removed; + mZips.Remove(uri, getter_AddRefs(removed)); + NS_ASSERTION(removed, "botched"); + NS_ASSERTION(oldest == removed, "removed wrong entry"); + + if (removed) removed->SetZipReaderCache(nullptr); + + return NS_OK; +} + +NS_IMETHODIMP +nsZipReaderCache::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aSomeData) { + if (strcmp(aTopic, "memory-pressure") == 0) { + MutexAutoLock lock(mLock); + for (auto iter = mZips.Iter(); !iter.Done(); iter.Next()) { + RefPtr<nsJAR>& current = iter.Data(); + if (current->GetReleaseTime() != PR_INTERVAL_NO_TIMEOUT) { + current->SetZipReaderCache(nullptr); + iter.Remove(); + } + } + } else if (strcmp(aTopic, "chrome-flush-caches") == 0) { + MutexAutoLock lock(mLock); + for (const auto& current : mZips.Values()) { + current->SetZipReaderCache(nullptr); + } + mZips.Clear(); + } else if (strcmp(aTopic, "flush-cache-entry") == 0) { + nsCOMPtr<nsIFile> file; + if (aSubject) { + file = do_QueryInterface(aSubject); + } else if (aSomeData) { + nsDependentString fileName(aSomeData); + Unused << NS_NewLocalFile(fileName, false, getter_AddRefs(file)); + } + + if (!file) return NS_OK; + + nsAutoCString uri; + if (NS_FAILED(file->GetPersistentDescriptor(uri))) return NS_OK; + + uri.InsertLiteral("file:", 0); + + MutexAutoLock lock(mLock); + + RefPtr<nsJAR> zip; + mZips.Get(uri, getter_AddRefs(zip)); + if (!zip) return NS_OK; + +#ifdef ZIP_CACHE_HIT_RATE + mZipCacheFlushes++; +#endif + + zip->SetZipReaderCache(nullptr); + + mZips.Remove(uri); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/modules/libjar/nsJAR.h b/modules/libjar/nsJAR.h new file mode 100644 index 0000000000..01332b8616 --- /dev/null +++ b/modules/libjar/nsJAR.h @@ -0,0 +1,190 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsJAR_h_ +#define nsJAR_h_ + +#include "nscore.h" +#include "prio.h" +#include "mozilla/Logging.h" +#include "prinrval.h" + +#include "mozilla/Atomics.h" +#include "mozilla/RecursiveMutex.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsString.h" +#include "nsIFile.h" +#include "nsStringEnumerator.h" +#include "nsHashKeys.h" +#include "nsRefPtrHashtable.h" +#include "nsTHashtable.h" +#include "nsIZipReader.h" +#include "nsZipArchive.h" +#include "nsWeakReference.h" +#include "nsIObserver.h" +#include "mozilla/Attributes.h" + +class nsZipReaderCache; + +/*------------------------------------------------------------------------- + * Class nsJAR declaration. + * nsJAR serves as an XPCOM wrapper for nsZipArchive with the addition of + * JAR manifest file parsing. + *------------------------------------------------------------------------*/ +class nsJAR final : public nsIZipReader { + // Allows nsJARInputStream to call the verification functions + friend class nsJARInputStream; + // Allows nsZipReaderCache to access mOuterZipEntry + friend class nsZipReaderCache; + + private: + virtual ~nsJAR(); + + public: + nsJAR(); + + NS_DEFINE_STATIC_CID_ACCESSOR(NS_ZIPREADER_CID) + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIZIPREADER + + nsresult GetFullJarPath(nsACString& aResult); + + // These access to mReleaseTime, which is locked by nsZipReaderCache's + // mLock, not nsJAR's mLock + PRIntervalTime GetReleaseTime() { return mReleaseTime; } + + bool IsReleased() { return mReleaseTime != PR_INTERVAL_NO_TIMEOUT; } + + void SetReleaseTime() { mReleaseTime = PR_IntervalNow(); } + + void ClearReleaseTime() { mReleaseTime = PR_INTERVAL_NO_TIMEOUT; } + + void SetZipReaderCache(nsZipReaderCache* aCache) { + mozilla::RecursiveMutexAutoLock lock(mLock); + mCache = aCache; + } + + nsresult GetNSPRFileDesc(PRFileDesc** aNSPRFileDesc); + + protected: + nsresult LoadEntry(const nsACString& aFilename, nsCString& aBuf); + int32_t ReadLine(const char** src); + + // used by nsZipReaderCache for flushing entries; access is locked by + // nsZipReaderCache's mLock + PRIntervalTime mReleaseTime; + + //-- Private data members, protected by mLock + mozilla::RecursiveMutex mLock; + // The entry in the zip this zip is reading from + nsCString mOuterZipEntry MOZ_GUARDED_BY(mLock); + // The zip/jar file on disk + nsCOMPtr<nsIFile> mZipFile MOZ_GUARDED_BY(mLock); + // The underlying zip archive + RefPtr<nsZipArchive> mZip MOZ_GUARDED_BY(mLock); + // if cached, this points to the cache it's contained in + nsZipReaderCache* mCache MOZ_GUARDED_BY(mLock); +}; + +/** + * nsJARItem + * + * An individual JAR entry. A set of nsJARItems matching a + * supplied pattern are returned in a nsJAREnumerator. + */ +class nsJARItem : public nsIZipEntry { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIZIPENTRY + + explicit nsJARItem(nsZipItem* aZipItem); + + private: + virtual ~nsJARItem() {} + + const uint32_t mSize; /* size in original file */ + const uint32_t mRealsize; /* inflated size */ + const uint32_t mCrc32; + const PRTime mLastModTime; + const uint16_t mCompression; + const uint32_t mPermissions; + const bool mIsDirectory; + const bool mIsSynthetic; +}; + +/** + * nsJAREnumerator + * + * Enumerates a list of files in a zip archive + * (based on a pattern match in its member nsZipFind). + */ +class nsJAREnumerator final : public nsStringEnumeratorBase { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUTF8STRINGENUMERATOR + + using nsStringEnumeratorBase::GetNext; + + explicit nsJAREnumerator(nsZipFind* aFind) + : mFind(aFind), mName(nullptr), mNameLen(0) { + NS_ASSERTION(mFind, "nsJAREnumerator: Missing zipFind."); + } + + private: + nsZipFind* mFind; + const char* mName; // pointer to an name owned by mArchive -- DON'T delete + uint16_t mNameLen; + + ~nsJAREnumerator() { delete mFind; } +}; + +//////////////////////////////////////////////////////////////////////////////// + +#if defined(DEBUG_warren) || defined(DEBUG_jband) +# define ZIP_CACHE_HIT_RATE +#endif + +class nsZipReaderCache : public nsIZipReaderCache, + public nsIObserver, + public nsSupportsWeakReference { + friend class nsJAR; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIZIPREADERCACHE + NS_DECL_NSIOBSERVER + + nsZipReaderCache(); + + nsresult ReleaseZip(nsJAR* reader); + + typedef nsRefPtrHashtable<nsCStringHashKey, nsJAR> ZipsHashtable; + + protected: + void AssertLockOwned() { mLock.AssertCurrentThreadOwns(); } + + virtual ~nsZipReaderCache(); + + mozilla::Mutex mLock; + uint32_t mCacheSize MOZ_GUARDED_BY(mLock); + ZipsHashtable mZips MOZ_GUARDED_BY(mLock); + +#ifdef ZIP_CACHE_HIT_RATE + uint32_t mZipCacheLookups MOZ_GUARDED_BY(mLock); + uint32_t mZipCacheHits MOZ_GUARDED_BY(mLock); + uint32_t mZipCacheFlushes MOZ_GUARDED_BY(mLock); + uint32_t mZipSyncMisses MOZ_GUARDED_BY(mLock); +#endif + + private: + nsresult GetZip(nsIFile* zipFile, nsIZipReader** result, bool failOnMiss); +}; + +//////////////////////////////////////////////////////////////////////////////// + +#endif /* nsJAR_h_ */ diff --git a/modules/libjar/nsJARChannel.cpp b/modules/libjar/nsJARChannel.cpp new file mode 100644 index 0000000000..9e1a3cff7c --- /dev/null +++ b/modules/libjar/nsJARChannel.cpp @@ -0,0 +1,1365 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* 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/. */ + +#include "nsJAR.h" +#include "nsJARChannel.h" +#include "nsJARProtocolHandler.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsEscape.h" +#include "nsContentUtils.h" +#include "nsProxyRelease.h" +#include "nsContentSecurityManager.h" +#include "nsComponentManagerUtils.h" + +#include "nsIFileURL.h" +#include "nsIURIMutator.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryComms.h" +#include "private/pprio.h" +#include "nsInputStreamPump.h" +#include "nsThreadUtils.h" +#include "nsJARProtocolHandler.h" + +using namespace mozilla; +using namespace mozilla::net; + +static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); + +// the entry for a directory will either be empty (in the case of the +// top-level directory) or will end with a slash +#define ENTRY_IS_DIRECTORY(_entry) \ + ((_entry).IsEmpty() || '/' == (_entry).Last()) + +//----------------------------------------------------------------------------- + +// +// set MOZ_LOG=nsJarProtocol:5 +// +static LazyLogModule gJarProtocolLog("nsJarProtocol"); + +// Ignore any LOG macro that we inherit from arbitrary headers. (We define our +// own LOG macro below.) +#ifdef LOG +# undef LOG +#endif +#ifdef LOG_ENABLED +# undef LOG_ENABLED +#endif + +#define LOG(args) MOZ_LOG(gJarProtocolLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(gJarProtocolLog, mozilla::LogLevel::Debug) + +//----------------------------------------------------------------------------- +// nsJARInputThunk +// +// this class allows us to do some extra work on the stream transport thread. +//----------------------------------------------------------------------------- + +class nsJARInputThunk : public nsIInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + + nsJARInputThunk(nsIZipReader* zipReader, nsIURI* fullJarURI, + const nsACString& jarEntry, bool usingJarCache) + : mUsingJarCache(usingJarCache), + mJarReader(zipReader), + mJarEntry(jarEntry), + mContentLength(-1) { + MOZ_DIAGNOSTIC_ASSERT(zipReader, "zipReader must not be null"); + if (ENTRY_IS_DIRECTORY(mJarEntry) && fullJarURI) { + nsCOMPtr<nsIURI> urlWithoutQueryRef; + nsresult rv = NS_MutateURI(fullJarURI) + .SetQuery(""_ns) + .SetRef(""_ns) + .Finalize(urlWithoutQueryRef); + if (NS_SUCCEEDED(rv) && urlWithoutQueryRef) { + rv = urlWithoutQueryRef->GetAsciiSpec(mJarDirSpec); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Finding a jar dir spec shouldn't fail."); + } else { + MOZ_CRASH("Shouldn't fail to strip query and ref off jar URI."); + } + } + } + + int64_t GetContentLength() { return mContentLength; } + + nsresult Init(); + + private: + virtual ~nsJARInputThunk() { Close(); } + + bool mUsingJarCache; + nsCOMPtr<nsIZipReader> mJarReader; + nsCString mJarDirSpec; + nsCOMPtr<nsIInputStream> mJarStream; + nsCString mJarEntry; + int64_t mContentLength; +}; + +NS_IMPL_ISUPPORTS(nsJARInputThunk, nsIInputStream) + +nsresult nsJARInputThunk::Init() { + if (!mJarReader) { + return NS_ERROR_INVALID_ARG; + } + nsresult rv; + if (ENTRY_IS_DIRECTORY(mJarEntry)) { + // A directory stream also needs the Spec of the FullJarURI + // because is included in the stream data itself. + + NS_ENSURE_STATE(!mJarDirSpec.IsEmpty()); + + rv = mJarReader->GetInputStreamWithSpec(mJarDirSpec, mJarEntry, + getter_AddRefs(mJarStream)); + } else { + rv = mJarReader->GetInputStream(mJarEntry, getter_AddRefs(mJarStream)); + } + if (NS_FAILED(rv)) { + return rv; + } + + // ask the JarStream for the content length + uint64_t avail; + rv = mJarStream->Available((uint64_t*)&avail); + if (NS_FAILED(rv)) return rv; + + mContentLength = avail < INT64_MAX ? (int64_t)avail : -1; + + return NS_OK; +} + +NS_IMETHODIMP +nsJARInputThunk::Close() { + nsresult rv = NS_OK; + + if (mJarStream) rv = mJarStream->Close(); + + if (!mUsingJarCache && mJarReader) mJarReader->Close(); + + mJarReader = nullptr; + + return rv; +} + +NS_IMETHODIMP +nsJARInputThunk::Available(uint64_t* avail) { + return mJarStream->Available(avail); +} + +NS_IMETHODIMP +nsJARInputThunk::StreamStatus() { return mJarStream->StreamStatus(); } + +NS_IMETHODIMP +nsJARInputThunk::Read(char* buf, uint32_t count, uint32_t* countRead) { + return mJarStream->Read(buf, count, countRead); +} + +NS_IMETHODIMP +nsJARInputThunk::ReadSegments(nsWriteSegmentFun writer, void* closure, + uint32_t count, uint32_t* countRead) { + // stream transport does only calls Read() + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsJARInputThunk::IsNonBlocking(bool* nonBlocking) { + *nonBlocking = false; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsJARChannel +//----------------------------------------------------------------------------- + +nsJARChannel::nsJARChannel() + : mOpened(false), + mCanceled(false), + mContentLength(-1), + mLoadFlags(LOAD_NORMAL), + mStatus(NS_OK), + mIsPending(false), + mEnableOMT(true), + mPendingEvent() { + LOG(("nsJARChannel::nsJARChannel [this=%p]\n", this)); + // hold an owning reference to the jar handler + mJarHandler = gJarHandler; +} + +nsJARChannel::~nsJARChannel() { + LOG(("nsJARChannel::~nsJARChannel [this=%p]\n", this)); + if (NS_IsMainThread()) { + return; + } + + // Proxy release the following members to main thread. + NS_ReleaseOnMainThread("nsJARChannel::mLoadInfo", mLoadInfo.forget()); + NS_ReleaseOnMainThread("nsJARChannel::mCallbacks", mCallbacks.forget()); + NS_ReleaseOnMainThread("nsJARChannel::mProgressSink", mProgressSink.forget()); + NS_ReleaseOnMainThread("nsJARChannel::mLoadGroup", mLoadGroup.forget()); + NS_ReleaseOnMainThread("nsJARChannel::mListener", mListener.forget()); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsJARChannel, nsHashPropertyBag, nsIRequest, + nsIChannel, nsIStreamListener, nsIRequestObserver, + nsIThreadRetargetableRequest, + nsIThreadRetargetableStreamListener, nsIJARChannel) + +nsresult nsJARChannel::Init(nsIURI* uri) { + LOG(("nsJARChannel::Init [this=%p]\n", this)); + nsresult rv; + + mWorker = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + mJarURI = do_QueryInterface(uri, &rv); + if (NS_FAILED(rv)) return rv; + + mOriginalURI = mJarURI; + + // Prevent loading jar:javascript URIs (see bug 290982). + nsCOMPtr<nsIURI> innerURI; + rv = mJarURI->GetJARFile(getter_AddRefs(innerURI)); + if (NS_FAILED(rv)) { + return rv; + } + + if (innerURI->SchemeIs("javascript")) { + NS_WARNING("blocking jar:javascript:"); + return NS_ERROR_INVALID_ARG; + } + + mJarURI->GetSpec(mSpec); + return rv; +} + +nsresult nsJARChannel::CreateJarInput(nsIZipReaderCache* jarCache, + nsJARInputThunk** resultInput) { + LOG(("nsJARChannel::CreateJarInput [this=%p]\n", this)); + MOZ_ASSERT(resultInput); + MOZ_ASSERT(mJarFile); + + // important to pass a clone of the file since the nsIFile impl is not + // necessarily MT-safe + nsCOMPtr<nsIFile> clonedFile; + nsresult rv = NS_OK; + if (mJarFile) { + rv = mJarFile->Clone(getter_AddRefs(clonedFile)); + if (NS_FAILED(rv)) return rv; + } + + nsCOMPtr<nsIZipReader> reader; + if (mPreCachedJarReader) { + reader = mPreCachedJarReader; + } else if (jarCache) { + if (mInnerJarEntry.IsEmpty()) + rv = jarCache->GetZip(clonedFile, getter_AddRefs(reader)); + else + rv = jarCache->GetInnerZip(clonedFile, mInnerJarEntry, + getter_AddRefs(reader)); + } else { + // create an uncached jar reader + nsCOMPtr<nsIZipReader> outerReader = do_CreateInstance(kZipReaderCID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = outerReader->Open(clonedFile); + if (NS_FAILED(rv)) return rv; + + if (mInnerJarEntry.IsEmpty()) + reader = outerReader; + else { + reader = do_CreateInstance(kZipReaderCID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = reader->OpenInner(outerReader, mInnerJarEntry); + } + } + if (NS_FAILED(rv)) return rv; + + RefPtr<nsJARInputThunk> input = + new nsJARInputThunk(reader, mJarURI, mJarEntry, jarCache != nullptr); + rv = input->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + // Make GetContentLength meaningful + mContentLength = input->GetContentLength(); + + input.forget(resultInput); + return NS_OK; +} + +nsresult nsJARChannel::LookupFile() { + LOG(("nsJARChannel::LookupFile [this=%p %s]\n", this, mSpec.get())); + + if (mJarFile) return NS_OK; + + nsresult rv; + + rv = mJarURI->GetJARFile(getter_AddRefs(mJarBaseURI)); + if (NS_FAILED(rv)) return rv; + + rv = mJarURI->GetJAREntry(mJarEntry); + if (NS_FAILED(rv)) return rv; + + // The name of the JAR entry must not contain URL-escaped characters: + // we're moving from URL domain to a filename domain here. nsStandardURL + // does basic escaping by default, which breaks reading zipped files which + // have e.g. spaces in their filenames. + NS_UnescapeURL(mJarEntry); + + if (mJarFileOverride) { + mJarFile = mJarFileOverride; + LOG(("nsJARChannel::LookupFile [this=%p] Overriding mJarFile\n", this)); + return NS_OK; + } + + // try to get a nsIFile directly from the url, which will often succeed. + { + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mJarBaseURI); + if (fileURL) fileURL->GetFile(getter_AddRefs(mJarFile)); + } + + // try to handle a nested jar + if (!mJarFile) { + nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(mJarBaseURI); + if (jarURI) { + nsCOMPtr<nsIFileURL> fileURL; + nsCOMPtr<nsIURI> innerJarURI; + rv = jarURI->GetJARFile(getter_AddRefs(innerJarURI)); + if (NS_SUCCEEDED(rv)) fileURL = do_QueryInterface(innerJarURI); + if (fileURL) { + fileURL->GetFile(getter_AddRefs(mJarFile)); + jarURI->GetJAREntry(mInnerJarEntry); + } + } + } + + return rv; +} + +nsresult CreateLocalJarInput(nsIZipReaderCache* aJarCache, nsIFile* aFile, + const nsACString& aInnerJarEntry, + nsIJARURI* aJarURI, const nsACString& aJarEntry, + nsJARInputThunk** aResultInput) { + LOG(("nsJARChannel::CreateLocalJarInput [aJarCache=%p, %s, %s]\n", aJarCache, + PromiseFlatCString(aInnerJarEntry).get(), + PromiseFlatCString(aJarEntry).get())); + + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aJarCache); + MOZ_ASSERT(aResultInput); + + nsresult rv; + + nsCOMPtr<nsIZipReader> reader; + if (aInnerJarEntry.IsEmpty()) { + rv = aJarCache->GetZip(aFile, getter_AddRefs(reader)); + } else { + rv = aJarCache->GetInnerZip(aFile, aInnerJarEntry, getter_AddRefs(reader)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<nsJARInputThunk> input = + new nsJARInputThunk(reader, aJarURI, aJarEntry, aJarCache != nullptr); + rv = input->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + input.forget(aResultInput); + return NS_OK; +} + +nsresult nsJARChannel::OpenLocalFile() { + LOG(("nsJARChannel::OpenLocalFile [this=%p]\n", this)); + + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(mWorker); + MOZ_ASSERT(mIsPending); + MOZ_ASSERT(mJarFile); + + nsresult rv; + + // Set mLoadGroup and mOpened before AsyncOpen return, and set back if + // if failed when callback. + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + SetOpened(); + + if (mPreCachedJarReader || !mEnableOMT) { + RefPtr<nsJARInputThunk> input; + rv = CreateJarInput(gJarHandler->JarCache(), getter_AddRefs(input)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return OnOpenLocalFileComplete(rv, true); + } + return ContinueOpenLocalFile(input, true); + } + + nsCOMPtr<nsIZipReaderCache> jarCache = gJarHandler->JarCache(); + if (NS_WARN_IF(!jarCache)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIFile> clonedFile; + rv = mJarFile->Clone(getter_AddRefs(clonedFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIJARURI> localJARURI = mJarURI; + + nsAutoCString jarEntry(mJarEntry); + nsAutoCString innerJarEntry(mInnerJarEntry); + + RefPtr<nsJARChannel> self = this; + return mWorker->Dispatch(NS_NewRunnableFunction( + "nsJARChannel::OpenLocalFile", [self, jarCache, clonedFile, localJARURI, + jarEntry, innerJarEntry]() mutable { + RefPtr<nsJARInputThunk> input; + nsresult rv = + CreateLocalJarInput(jarCache, clonedFile, innerJarEntry, + localJARURI, jarEntry, getter_AddRefs(input)); + + nsCOMPtr<nsIRunnable> target; + if (NS_SUCCEEDED(rv)) { + target = NewRunnableMethod<RefPtr<nsJARInputThunk>, bool>( + "nsJARChannel::ContinueOpenLocalFile", self, + &nsJARChannel::ContinueOpenLocalFile, input, false); + } else { + target = NewRunnableMethod<nsresult, bool>( + "nsJARChannel::OnOpenLocalFileComplete", self, + &nsJARChannel::OnOpenLocalFileComplete, rv, false); + } + + // nsJARChannel must be release on main thread, and sometimes + // this still hold nsJARChannel after dispatched. + self = nullptr; + + NS_DispatchToMainThread(target.forget()); + })); +} + +nsresult nsJARChannel::ContinueOpenLocalFile(nsJARInputThunk* aInput, + bool aIsSyncCall) { + LOG(("nsJARChannel::ContinueOpenLocalFile [this=%p %p]\n", this, aInput)); + + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsPending); + + // Make GetContentLength meaningful + mContentLength = aInput->GetContentLength(); + + nsresult rv; + RefPtr<nsJARInputThunk> input = aInput; + // Create input stream pump and call AsyncRead as a block. + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input.forget()); + if (NS_SUCCEEDED(rv)) { + rv = mPump->AsyncRead(this); + } + + if (NS_SUCCEEDED(rv)) { + rv = CheckPendingEvents(); + } + + return OnOpenLocalFileComplete(rv, aIsSyncCall); +} + +nsresult nsJARChannel::OnOpenLocalFileComplete(nsresult aResult, + bool aIsSyncCall) { + LOG(("nsJARChannel::OnOpenLocalFileComplete [this=%p %08x]\n", this, + static_cast<uint32_t>(aResult))); + + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsPending); + + if (NS_FAILED(aResult)) { + if (aResult == NS_ERROR_FILE_NOT_FOUND) { + CheckForBrokenChromeURL(mLoadInfo, mOriginalURI); + } + if (!aIsSyncCall) { + NotifyError(aResult); + } + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aResult); + } + + mOpened = false; + mIsPending = false; + mListener = nullptr; + mCallbacks = nullptr; + mProgressSink = nullptr; + + return aResult; + } + + return NS_OK; +} + +nsresult nsJARChannel::CheckPendingEvents() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsPending); + MOZ_ASSERT(mPump); + + nsresult rv; + + uint32_t suspendCount = mPendingEvent.suspendCount; + while (suspendCount--) { + if (NS_WARN_IF(NS_FAILED(rv = mPump->Suspend()))) { + return rv; + } + } + + if (mPendingEvent.isCanceled) { + if (NS_WARN_IF(NS_FAILED(rv = mPump->Cancel(mStatus)))) { + return rv; + } + mPendingEvent.isCanceled = false; + } + + return NS_OK; +} + +void nsJARChannel::NotifyError(nsresult aError) { + MOZ_ASSERT(NS_FAILED(aError)); + + mStatus = aError; + + OnStartRequest(nullptr); + OnStopRequest(nullptr, aError); +} + +void nsJARChannel::FireOnProgress(uint64_t aProgress) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mProgressSink); + + mProgressSink->OnProgress(this, aProgress, mContentLength); +} + +//----------------------------------------------------------------------------- +// nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsJARChannel::GetName(nsACString& result) { return mJarURI->GetSpec(result); } + +NS_IMETHODIMP +nsJARChannel::IsPending(bool* result) { + *result = mIsPending; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetStatus(nsresult* status) { + if (mPump && NS_SUCCEEDED(mStatus)) + mPump->GetStatus(status); + else + *status = mStatus; + return NS_OK; +} + +NS_IMETHODIMP nsJARChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsJARChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsJARChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +nsJARChannel::Cancel(nsresult status) { + mCanceled = true; + mStatus = status; + if (mPump) { + return mPump->Cancel(status); + } + + if (mIsPending) { + mPendingEvent.isCanceled = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::Suspend() { + ++mPendingEvent.suspendCount; + + if (mPump) { + return mPump->Suspend(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::Resume() { + if (NS_WARN_IF(mPendingEvent.suspendCount == 0)) { + return NS_ERROR_UNEXPECTED; + } + --mPendingEvent.suspendCount; + + if (mPump) { + return mPump->Resume(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsJARChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsJARChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +nsJARChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_IF_ADDREF(*aLoadGroup = mLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsJARChannel::GetOriginalURI(nsIURI** aURI) { + *aURI = mOriginalURI; + NS_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mJarURI); + + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetOwner(nsISupports** aOwner) { + // JAR signatures are not processed to avoid main-thread network I/O (bug + // 726125) + *aOwner = mOwner; + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetOwner(nsISupports* aOwner) { + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + NS_IF_ADDREF(*aCallbacks = mCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + mCallbacks = aCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + MOZ_ASSERT(aSecurityInfo, "Null out param"); + *aSecurityInfo = nullptr; + return NS_OK; +} + +bool nsJARChannel::GetContentTypeGuess(nsACString& aResult) const { + const char *ext = nullptr, *fileName = mJarEntry.get(); + int32_t len = mJarEntry.Length(); + + // check if we're displaying a directory + // mJarEntry will be empty if we're trying to display + // the topmost directory in a zip, e.g. jar:foo.zip!/ + if (ENTRY_IS_DIRECTORY(mJarEntry)) { + aResult.AssignLiteral(APPLICATION_HTTP_INDEX_FORMAT); + return true; + } + + // Not a directory, take a guess by its extension + for (int32_t i = len - 1; i >= 0; i--) { + if (fileName[i] == '.') { + ext = &fileName[i + 1]; + break; + } + } + if (!ext) { + return false; + } + nsIMIMEService* mimeServ = gJarHandler->MimeService(); + if (!mimeServ) { + return false; + } + mimeServ->GetTypeFromExtension(nsDependentCString(ext), aResult); + return !aResult.IsEmpty(); +} + +NS_IMETHODIMP +nsJARChannel::GetContentType(nsACString& aResult) { + // If the Jar file has not been open yet, + // We return application/x-unknown-content-type + if (!mOpened) { + aResult.AssignLiteral(UNKNOWN_CONTENT_TYPE); + return NS_OK; + } + + aResult = mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetContentType(const nsACString& aContentType) { + // We behave like HTTP channels (treat this as a hint if called before open, + // and override the charset if called after open). + // mContentCharset is unchanged if not parsed + NS_ParseResponseContentType(aContentType, mContentType, mContentCharset); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetContentCharset(nsACString& aContentCharset) { + // If someone gives us a charset hint we should just use that charset. + // So we don't care when this is being called. + aContentCharset = mContentCharset; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetContentCharset(const nsACString& aContentCharset) { + mContentCharset = aContentCharset; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsJARChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsJARChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsJARChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsJARChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsJARChannel::GetContentLength(int64_t* result) { + *result = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetContentLength(int64_t aContentLength) { + // XXX does this really make any sense at all? + mContentLength = aContentLength; + return NS_OK; +} + +static void RecordZeroLengthEvent(bool aIsSync, const nsCString& aSpec, + nsresult aStatus, bool aCanceled, + nsILoadInfo* aLoadInfo) { + if (!StaticPrefs::network_jar_record_failure_reason()) { + return; + } + + if (aLoadInfo) { + bool shouldSkipCheckForBrokenURLOrZeroSized; + MOZ_ALWAYS_SUCCEEDS(aLoadInfo->GetShouldSkipCheckForBrokenURLOrZeroSized( + &shouldSkipCheckForBrokenURLOrZeroSized)); + if (shouldSkipCheckForBrokenURLOrZeroSized) { + return; + } + } + + // The event can only hold 80 characters. + // We only save the file name and path inside the jar. + auto findFilenameStart = [](const nsCString& aSpec) -> uint32_t { + int32_t pos = aSpec.Find("!/"); + if (pos == kNotFound) { + MOZ_ASSERT(false, "This should not happen"); + return 0; + } + int32_t from = aSpec.RFindChar('/', pos); + if (from == kNotFound) { + MOZ_ASSERT(false, "This should not happen"); + return 0; + } + // Skip over the slash + from++; + return from; + }; + + // If for some reason we are unable to extract the filename we report the + // entire string, or 80 characters of it, to make sure we don't miss any + // events. + uint32_t from = findFilenameStart(aSpec); + nsAutoCString fileName(Substring(aSpec, from)); + + // Bug 1702937: Filter aboutNetError.xhtml. + // Note that aboutNetError.xhtml is causing ~90% of the XHTML error events. We + // should investigate this in the future. + if (StringEndsWith(fileName, "aboutNetError.xhtml"_ns)) { + return; + } + + nsAutoCString errorCString; + mozilla::GetErrorName(aStatus, errorCString); + + // To test this telemetry we use a zip file and we want to make + // sure don't filter it out. + bool isTest = fileName.Find("test_empty_file.zip!") != -1; + bool isOmniJa = StringBeginsWith(fileName, "omni.ja!"_ns); + + Telemetry::SetEventRecordingEnabled("zero_byte_load"_ns, true); + Telemetry::EventID eventType = Telemetry::EventID::Zero_byte_load_Load_Others; + if (StringEndsWith(fileName, ".ftl"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Ftl; + } else if (StringEndsWith(fileName, ".dtd"_ns)) { + // We're going to skip reporting telemetry on res DTDs. + // See Bug 1693711 for investigation into those empty loads. + if (!isTest && StringBeginsWith(fileName, "omni.ja!/res/dtd"_ns)) { + return; + } + + eventType = Telemetry::EventID::Zero_byte_load_Load_Dtd; + } else if (StringEndsWith(fileName, ".properties"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Properties; + } else if (StringEndsWith(fileName, ".js"_ns) || + StringEndsWith(fileName, ".jsm"_ns)) { + // We're going to skip reporting telemetry on JS loads + // coming not from omni.ja. + // See Bug 1693711 for investigation into those empty loads. + if (!isTest && !isOmniJa) { + return; + } + eventType = Telemetry::EventID::Zero_byte_load_Load_Js; + } else if (StringEndsWith(fileName, ".xml"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Xml; + } else if (StringEndsWith(fileName, ".xhtml"_ns)) { + // This error seems to be very common and is not strongly + // correlated to YSOD. + if (aStatus == NS_ERROR_PARSED_DATA_CACHED) { + return; + } + + // We're not investigating YSODs from extensions for now. + if (!isOmniJa) { + return; + } + + eventType = Telemetry::EventID::Zero_byte_load_Load_Xhtml; + } else if (StringEndsWith(fileName, ".css"_ns)) { + // Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo. + if (aStatus == NS_BINDING_ABORTED) { + return; + } + + // Bug 1702937: Filter css/NS_ERROR_CORRUPTED_CONTENT that is coming from + // outside of omni.ja. + if (!isOmniJa && aStatus == NS_ERROR_CORRUPTED_CONTENT) { + return; + } + eventType = Telemetry::EventID::Zero_byte_load_Load_Css; + } else if (StringEndsWith(fileName, ".json"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Json; + } else if (StringEndsWith(fileName, ".html"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Html; + // See bug 1695560. Filter out non-omni.ja HTML. + if (!isOmniJa) { + return; + } + + // See bug 1695560. "activity-stream-noscripts.html" with NS_ERROR_FAILURE + // is filtered out. + if (fileName.EqualsLiteral("omni.ja!/chrome/browser/res/activity-stream/" + "prerendered/activity-stream-noscripts.html") && + aStatus == NS_ERROR_FAILURE) { + return; + } + } else if (StringEndsWith(fileName, ".png"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Png; + // See bug 1695560. + // Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo. + if (!isOmniJa || aStatus == NS_BINDING_ABORTED) { + return; + } + } else if (StringEndsWith(fileName, ".svg"_ns)) { + eventType = Telemetry::EventID::Zero_byte_load_Load_Svg; + // See bug 1695560. + // Bug 1702937: Filter out svg+'css'+'png'/NS_BINDING_ABORTED combo. + if (!isOmniJa || aStatus == NS_BINDING_ABORTED) { + return; + } + } + + // We're going to, for now, filter out `other` category. + // See Bug 1693711 for investigation into those empty loads. + // Bug 1702937: Filter other/*.ico/NS_BINDING_ABORTED. + if (!isTest && eventType == Telemetry::EventID::Zero_byte_load_Load_Others && + (!isOmniJa || (aStatus == NS_BINDING_ABORTED && + StringEndsWith(fileName, ".ico"_ns)))) { + return; + } + + // FTL uses I/O to test for file presence, so we get + // a high volume of events from it, but it is not erronous. + // Also, Fluent is resilient to empty loads, so even if any + // of the errors are real errors, they don't cause YSOD. + // We can investigate them separately. + if (!isTest && + (eventType == Telemetry::EventID::Zero_byte_load_Load_Ftl || + eventType == Telemetry::EventID::Zero_byte_load_Load_Json) && + aStatus == NS_ERROR_FILE_NOT_FOUND) { + return; + } + + // See bug 1695560. "search-extensions/google/favicon.ico" with + // NS_BINDING_ABORTED is filtered out. + if (fileName.EqualsLiteral( + "omni.ja!/chrome/browser/search-extensions/google/favicon.ico") && + aStatus == NS_BINDING_ABORTED) { + return; + } + + // See bug 1695560. "update.locale" with + // NS_ERROR_FILE_NOT_FOUND is filtered out. + if (fileName.EqualsLiteral("omni.ja!/update.locale") && + aStatus == NS_ERROR_FILE_NOT_FOUND) { + return; + } + + auto res = CopyableTArray<Telemetry::EventExtraEntry>{}; + res.SetCapacity(4); + res.AppendElement( + Telemetry::EventExtraEntry{"sync"_ns, aIsSync ? "true"_ns : "false"_ns}); + res.AppendElement(Telemetry::EventExtraEntry{"file_name"_ns, fileName}); + res.AppendElement(Telemetry::EventExtraEntry{"status"_ns, errorCString}); + res.AppendElement(Telemetry::EventExtraEntry{ + "cancelled"_ns, aCanceled ? "true"_ns : "false"_ns}); + Telemetry::RecordEvent(eventType, Nothing{}, Some(res)); +} + +NS_IMETHODIMP +nsJARChannel::Open(nsIInputStream** aStream) { + LOG(("nsJARChannel::Open [this=%p]\n", this)); + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + auto recordEvent = MakeScopeExit([&] { + if (mContentLength <= 0 || NS_FAILED(rv)) { + RecordZeroLengthEvent(true, mSpec, rv, mCanceled, mLoadInfo); + } + }); + + LOG(("nsJARChannel::Open [this=%p]\n", this)); + + NS_ENSURE_TRUE(!mOpened, NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + + mJarFile = nullptr; + + rv = LookupFile(); + if (NS_FAILED(rv)) return rv; + + // If mJarFile was not set by LookupFile, we can't open a channel. + if (!mJarFile) { + MOZ_ASSERT_UNREACHABLE("only file-backed jars are supported"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + RefPtr<nsJARInputThunk> input; + rv = CreateJarInput(gJarHandler->JarCache(), getter_AddRefs(input)); + if (NS_FAILED(rv)) return rv; + + input.forget(aStream); + SetOpened(); + + return NS_OK; +} + +void nsJARChannel::SetOpened() { + MOZ_ASSERT(!mOpened, "Opening channel twice?"); + mOpened = true; + // Compute the content type now. + if (!GetContentTypeGuess(mContentType)) { + mContentType.Assign(UNKNOWN_CONTENT_TYPE); + } +} + +NS_IMETHODIMP +nsJARChannel::AsyncOpen(nsIStreamListener* aListener) { + LOG(("nsJARChannel::AsyncOpen [this=%p]\n", this)); + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_FAILED(rv)) { + mIsPending = false; + mListener = nullptr; + mCallbacks = nullptr; + mProgressSink = nullptr; + return rv; + } + + LOG(("nsJARChannel::AsyncOpen [this=%p]\n", this)); + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mOpened, NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + + mJarFile = nullptr; + + // Initialize mProgressSink + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, mProgressSink); + + mListener = listener; + mIsPending = true; + + rv = LookupFile(); + if (NS_FAILED(rv) || !mJarFile) { + // Not a local file... + mIsPending = false; + mListener = nullptr; + mCallbacks = nullptr; + mProgressSink = nullptr; + return mJarFile ? rv : NS_ERROR_UNSAFE_CONTENT_TYPE; + } + + rv = OpenLocalFile(); + if (NS_FAILED(rv)) { + mIsPending = false; + mListener = nullptr; + mCallbacks = nullptr; + mProgressSink = nullptr; + return rv; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIJARChannel +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsJARChannel::GetJarFile(nsIFile** aFile) { + NS_IF_ADDREF(*aFile = mJarFile); + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::SetJarFile(nsIFile* aFile) { + if (mOpened) { + return NS_ERROR_IN_PROGRESS; + } + mJarFileOverride = aFile; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::EnsureCached(bool* aIsCached) { + nsresult rv; + *aIsCached = false; + + if (mOpened) { + return NS_ERROR_ALREADY_OPENED; + } + + if (mPreCachedJarReader) { + // We've already been called and found the JAR is cached + *aIsCached = true; + return NS_OK; + } + + nsCOMPtr<nsIURI> innerFileURI; + rv = mJarURI->GetJARFile(getter_AddRefs(innerFileURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFileURL> innerFileURL = do_QueryInterface(innerFileURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> jarFile; + rv = innerFileURL->GetFile(getter_AddRefs(jarFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIProtocolHandler> handler; + rv = ioService->GetProtocolHandler("jar", getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + auto jarHandler = static_cast<nsJARProtocolHandler*>(handler.get()); + MOZ_ASSERT(jarHandler); + + nsIZipReaderCache* jarCache = jarHandler->JarCache(); + + rv = jarCache->GetZipIfCached(jarFile, getter_AddRefs(mPreCachedJarReader)); + if (rv == NS_ERROR_CACHE_KEY_NOT_FOUND) { + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + + *aIsCached = true; + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::GetZipEntry(nsIZipEntry** aZipEntry) { + nsresult rv = LookupFile(); + if (NS_FAILED(rv)) return rv; + + if (!mJarFile) return NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr<nsIZipReader> reader; + rv = gJarHandler->JarCache()->GetZip(mJarFile, getter_AddRefs(reader)); + if (NS_FAILED(rv)) return rv; + + return reader->GetEntry(mJarEntry, aZipEntry); +} + +//----------------------------------------------------------------------------- +// nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsJARChannel::OnStartRequest(nsIRequest* req) { + LOG(("nsJARChannel::OnStartRequest [this=%p %s]\n", this, mSpec.get())); + + mRequest = req; + nsresult rv = mListener->OnStartRequest(this); + if (NS_FAILED(rv)) { + return rv; + } + + // Restrict loadable content types. + nsAutoCString contentType; + GetContentType(contentType); + auto contentPolicyType = mLoadInfo->GetExternalContentPolicyType(); + if (contentType.Equals(APPLICATION_HTTP_INDEX_FORMAT) && + contentPolicyType != ExtContentPolicy::TYPE_DOCUMENT && + contentPolicyType != ExtContentPolicy::TYPE_FETCH) { + return NS_ERROR_CORRUPTED_CONTENT; + } + if (contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET && + !contentType.EqualsLiteral(TEXT_CSS)) { + return NS_ERROR_CORRUPTED_CONTENT; + } + if (contentPolicyType == ExtContentPolicy::TYPE_SCRIPT && + !nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentType))) { + return NS_ERROR_CORRUPTED_CONTENT; + } + + return rv; +} + +NS_IMETHODIMP +nsJARChannel::OnStopRequest(nsIRequest* req, nsresult status) { + LOG(("nsJARChannel::OnStopRequest [this=%p %s status=%" PRIx32 "]\n", this, + mSpec.get(), static_cast<uint32_t>(status))); + + if (NS_SUCCEEDED(mStatus)) mStatus = status; + + if (mListener) { + if (!mOnDataCalled || NS_FAILED(status)) { + RecordZeroLengthEvent(false, mSpec, status, mCanceled, mLoadInfo); + } + + mListener->OnStopRequest(this, status); + mListener = nullptr; + } + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, status); + + mRequest = nullptr; + mPump = nullptr; + mIsPending = false; + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + mProgressSink = nullptr; + +#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA) +#else + // To deallocate file descriptor by RemoteOpenFileChild destructor. + mJarFile = nullptr; +#endif + + return NS_OK; +} + +NS_IMETHODIMP +nsJARChannel::OnDataAvailable(nsIRequest* req, nsIInputStream* stream, + uint64_t offset, uint32_t count) { + LOG(("nsJARChannel::OnDataAvailable [this=%p %s]\n", this, mSpec.get())); + + nsresult rv; + + // don't send out OnDataAvailable notifications if we've been canceled. + if (mCanceled) { + return mStatus; + } + + mOnDataCalled = true; + rv = mListener->OnDataAvailable(this, stream, offset, count); + + // simply report progress here instead of hooking ourselves up as a + // nsITransportEventSink implementation. + // XXX do the 64-bit stuff for real + if (mProgressSink && NS_SUCCEEDED(rv)) { + if (NS_IsMainThread()) { + FireOnProgress(offset + count); + } else { + NS_DispatchToMainThread(NewRunnableMethod<uint64_t>( + "nsJARChannel::FireOnProgress", this, &nsJARChannel::FireOnProgress, + offset + count)); + } + } + + return rv; // let the pump cancel on failure +} + +NS_IMETHODIMP +nsJARChannel::RetargetDeliveryTo(nsISerialEventTarget* aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIThreadRetargetableRequest> request = do_QueryInterface(mRequest); + if (!request) { + return NS_ERROR_NO_INTERFACE; + } + + return request->RetargetDeliveryTo(aEventTarget); +} + +NS_IMETHODIMP +nsJARChannel::GetDeliveryTarget(nsISerialEventTarget** aEventTarget) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIThreadRetargetableRequest> request = do_QueryInterface(mRequest); + if (!request) { + return NS_ERROR_NO_INTERFACE; + } + + return request->GetDeliveryTarget(aEventTarget); +} + +NS_IMETHODIMP +nsJARChannel::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIThreadRetargetableStreamListener> listener = + do_QueryInterface(mListener); + if (!listener) { + return NS_ERROR_NO_INTERFACE; + } + + return listener->CheckListenerChain(); +} diff --git a/modules/libjar/nsJARChannel.h b/modules/libjar/nsJARChannel.h new file mode 100644 index 0000000000..25c88060d8 --- /dev/null +++ b/modules/libjar/nsJARChannel.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsJARChannel_h__ +#define nsJARChannel_h__ + +#include "mozilla/net/MemoryDownloader.h" +#include "nsIJARChannel.h" +#include "nsIJARURI.h" +#include "nsIEventTarget.h" +#include "nsIInputStreamPump.h" +#include "nsIInterfaceRequestor.h" +#include "nsIProgressEventSink.h" +#include "nsIStreamListener.h" +#include "nsIZipReader.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsHashPropertyBag.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozilla/Atomics.h" +#include "mozilla/Logging.h" + +class nsJARInputThunk; +class nsJARProtocolHandler; +class nsInputStreamPump; + +//----------------------------------------------------------------------------- + +class nsJARChannel final : public nsIJARChannel, + public nsIStreamListener, + public nsIThreadRetargetableRequest, + public nsIThreadRetargetableStreamListener, + public nsHashPropertyBag { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIJARCHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + nsJARChannel(); + + nsresult Init(nsIURI* uri); + + void SetFile(nsIFile* file); + + private: + virtual ~nsJARChannel(); + + nsresult CreateJarInput(nsIZipReaderCache*, nsJARInputThunk**); + nsresult LookupFile(); + nsresult OpenLocalFile(); + nsresult ContinueOpenLocalFile(nsJARInputThunk* aInput, bool aIsSyncCall); + nsresult OnOpenLocalFileComplete(nsresult aResult, bool aIsSyncCall); + nsresult CheckPendingEvents(); + void NotifyError(nsresult aError); + void FireOnProgress(uint64_t aProgress); + + // Returns false if we don't know the content type of this channel, in which + // case we should use the content-type hint. + bool GetContentTypeGuess(nsACString&) const; + void SetOpened(); + + nsCString mSpec; + + bool mOpened; + mozilla::Atomic<bool, mozilla::ReleaseAcquire> mCanceled; + bool mOnDataCalled = false; + + RefPtr<nsJARProtocolHandler> mJarHandler; + nsCOMPtr<nsIJARURI> mJarURI; + nsCOMPtr<nsIURI> mOriginalURI; + nsCOMPtr<nsISupports> mOwner; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsIProgressEventSink> mProgressSink; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsIStreamListener> mListener; + nsCString mContentType; + nsCString mContentCharset; + int64_t mContentLength; + uint32_t mLoadFlags; + mozilla::Atomic<nsresult, mozilla::ReleaseAcquire> mStatus; + bool mIsPending; // the AsyncOpen is in progress. + + bool mEnableOMT; + // |Cancel()|, |Suspend()|, and |Resume()| might be called during AsyncOpen. + struct { + bool isCanceled; + mozilla::Atomic<uint32_t> suspendCount; + } mPendingEvent; + + nsCOMPtr<nsIInputStreamPump> mPump; + // mRequest is only non-null during OnStartRequest, so we'll have a pointer + // to the request if we get called back via RetargetDeliveryTo. + nsCOMPtr<nsIRequest> mRequest; + nsCOMPtr<nsIFile> mJarFile; + nsCOMPtr<nsIFile> mJarFileOverride; + nsCOMPtr<nsIZipReader> mPreCachedJarReader; + nsCOMPtr<nsIURI> mJarBaseURI; + nsCString mJarEntry; + nsCString mInnerJarEntry; + + // use StreamTransportService as background thread + nsCOMPtr<nsIEventTarget> mWorker; +}; + +#endif // nsJARChannel_h__ diff --git a/modules/libjar/nsJARInputStream.cpp b/modules/libjar/nsJARInputStream.cpp new file mode 100644 index 0000000000..fd00a3a419 --- /dev/null +++ b/modules/libjar/nsJARInputStream.cpp @@ -0,0 +1,399 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* nsJARInputStream.cpp + * + * 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/. */ + +#include "nsJARInputStream.h" +#include "zipstruct.h" // defines ZIP compression codes +#include "nsZipArchive.h" +#include "mozilla/MmapFaultHandler.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" + +#include "nsEscape.h" +#include "nsDebug.h" +#include <algorithm> +#include <limits> +#if defined(XP_WIN) +# include <windows.h> +#endif + +/*--------------------------------------------- + * nsISupports implementation + *--------------------------------------------*/ + +NS_IMPL_ISUPPORTS(nsJARInputStream, nsIInputStream) + +/*---------------------------------------------------------- + * nsJARInputStream implementation + * Takes ownership of |fd|, even on failure + *--------------------------------------------------------*/ + +nsresult nsJARInputStream::InitFile(nsZipHandle* aFd, const uint8_t* aData, + nsZipItem* aItem) { + nsresult rv = NS_OK; + MOZ_DIAGNOSTIC_ASSERT(aFd, "Argument may not be null"); + if (!aFd) { + return NS_ERROR_INVALID_ARG; + } + MOZ_ASSERT(aItem, "Argument may not be null"); + + // Mark it as closed, in case something fails in initialisation + mMode = MODE_CLOSED; + //-- prepare for the compression type + switch (aItem->Compression()) { + case STORED: + mMode = MODE_COPY; + break; + + case DEFLATED: + rv = gZlibInit(&mZs); + NS_ENSURE_SUCCESS(rv, rv); + + mMode = MODE_INFLATE; + mInCrc = aItem->CRC32(); + mOutCrc = crc32(0L, Z_NULL, 0); + break; + + default: + mFd = aFd; + return NS_ERROR_NOT_IMPLEMENTED; + } + + // Must keep handle to filepointer and mmap structure as long as we need + // access to the mmapped data + mFd = aFd; + mZs.next_in = (Bytef*)aData; + if (!mZs.next_in) { + return NS_ERROR_FILE_CORRUPTED; + } + mZs.avail_in = aItem->Size(); + mOutSize = aItem->RealSize(); + mZs.total_out = 0; + return NS_OK; +} + +nsresult nsJARInputStream::InitDirectory(nsJAR* aJar, + const nsACString& aJarDirSpec, + const char* aDir) { + MOZ_ASSERT(aJar, "Argument may not be null"); + MOZ_ASSERT(aDir, "Argument may not be null"); + + // Mark it as closed, in case something fails in initialisation + mMode = MODE_CLOSED; + + // Keep the zipReader for getting the actual zipItems + mJar = aJar; + mJar->mLock.AssertCurrentThreadIn(); + mozilla::UniquePtr<nsZipFind> find; + nsresult rv; + // We can get aDir's contents as strings via FindEntries + // with the following pattern (see nsIZipReader.findEntries docs) + // assuming dirName is properly escaped: + // + // dirName + "?*~" + dirName + "?*/?*" + nsDependentCString dirName(aDir); + mNameLen = dirName.Length(); + + // iterate through dirName and copy it to escDirName, escaping chars + // which are special at the "top" level of the regexp so FindEntries + // works correctly + nsAutoCString escDirName; + const char* curr = dirName.BeginReading(); + const char* end = dirName.EndReading(); + while (curr != end) { + switch (*curr) { + case '*': + case '?': + case '$': + case '[': + case ']': + case '^': + case '~': + case '(': + case ')': + case '\\': + escDirName.Append('\\'); + [[fallthrough]]; + default: + escDirName.Append(*curr); + } + ++curr; + } + nsAutoCString pattern = escDirName + "?*~"_ns + escDirName + "?*/?*"_ns; + rv = mJar->mZip->FindInit(pattern.get(), getter_Transfers(find)); + if (NS_FAILED(rv)) return rv; + + const char* name; + uint16_t nameLen; + while ((rv = find->FindNext(&name, &nameLen)) == NS_OK) { + // Must copy, to make it zero-terminated + mArray.AppendElement(nsCString(name, nameLen)); + } + + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + return NS_ERROR_FAILURE; // no error translation + } + + // Sort it + mArray.Sort(); + + mBuffer.AssignLiteral("300: "); + mBuffer.Append(aJarDirSpec); + mBuffer.AppendLiteral( + "\n200: filename content-length last-modified file-type\n"); + + // Open for reading + mMode = MODE_DIRECTORY; + mZs.total_out = 0; + mArrPos = 0; + return NS_OK; +} + +NS_IMETHODIMP +nsJARInputStream::Available(uint64_t* _retval) { + // A lot of callers don't check the error code. + // They just use the _retval value. + *_retval = 0; + + uint64_t maxAvailableSize = 0; + + switch (mMode) { + case MODE_NOTINITED: + break; + + case MODE_CLOSED: + return NS_BASE_STREAM_CLOSED; + + case MODE_DIRECTORY: + *_retval = mBuffer.Length(); + break; + + case MODE_INFLATE: + case MODE_COPY: + maxAvailableSize = mozilla::StaticPrefs::network_jar_max_available_size(); + if (!maxAvailableSize) { + maxAvailableSize = std::numeric_limits<uint64_t>::max(); + } + *_retval = std::min<uint64_t>(mOutSize - mZs.total_out, maxAvailableSize); + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsJARInputStream::StreamStatus() { + return mMode == MODE_CLOSED ? NS_BASE_STREAM_CLOSED : NS_OK; +} + +NS_IMETHODIMP +nsJARInputStream::Read(char* aBuffer, uint32_t aCount, uint32_t* aBytesRead) { + NS_ENSURE_ARG_POINTER(aBuffer); + NS_ENSURE_ARG_POINTER(aBytesRead); + + *aBytesRead = 0; + + nsresult rv = NS_OK; + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + switch (mMode) { + case MODE_NOTINITED: + return NS_OK; + + case MODE_CLOSED: + return NS_BASE_STREAM_CLOSED; + + case MODE_DIRECTORY: + return ReadDirectory(aBuffer, aCount, aBytesRead); + + case MODE_INFLATE: + if (mZs.total_out < mOutSize) { + rv = ContinueInflate(aBuffer, aCount, aBytesRead); + } + // be aggressive about releasing the file! + // note that sometimes, we will release mFd before we've finished + // deflating - this is because zlib buffers the input + if (mZs.avail_in == 0) { + mFd = nullptr; + } + break; + + case MODE_COPY: + if (mFd) { + MOZ_DIAGNOSTIC_ASSERT(mOutSize >= mZs.total_out, + "Did we read more than expected?"); + uint32_t count = std::min(aCount, mOutSize - uint32_t(mZs.total_out)); + if (count) { + std::copy(mZs.next_in + mZs.total_out, + mZs.next_in + mZs.total_out + count, aBuffer); + mZs.total_out += count; + } + *aBytesRead = count; + } + // be aggressive about releasing the file! + // note that sometimes, we will release mFd before we've finished copying. + if (mZs.total_out >= mOutSize) { + mFd = nullptr; + } + break; + } + MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE) + return rv; +} + +NS_IMETHODIMP +nsJARInputStream::ReadSegments(nsWriteSegmentFun writer, void* closure, + uint32_t count, uint32_t* _retval) { + // don't have a buffer to read from, so this better not be called! + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsJARInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP +nsJARInputStream::Close() { + if (mMode == MODE_INFLATE) { + inflateEnd(&mZs); + } + mMode = MODE_CLOSED; + mFd = nullptr; + return NS_OK; +} + +nsresult nsJARInputStream::ContinueInflate(char* aBuffer, uint32_t aCount, + uint32_t* aBytesRead) { + bool finished = false; + + // No need to check the args, ::Read did that, but assert them at least + NS_ASSERTION(aBuffer, "aBuffer parameter must not be null"); + NS_ASSERTION(aBytesRead, "aBytesRead parameter must not be null"); + + // Keep old total_out count + const uint32_t oldTotalOut = mZs.total_out; + + // make sure we aren't reading too much + mZs.avail_out = std::min(aCount, (mOutSize - oldTotalOut)); + mZs.next_out = (unsigned char*)aBuffer; + + if (mMode == MODE_INFLATE) { + // now inflate + int zerr = inflate(&mZs, Z_SYNC_FLUSH); + if ((zerr != Z_OK) && (zerr != Z_STREAM_END)) { + return NS_ERROR_FILE_CORRUPTED; + } + finished = (zerr == Z_STREAM_END); + } + + *aBytesRead = (mZs.total_out - oldTotalOut); + + // Calculate the CRC on the output + mOutCrc = crc32(mOutCrc, (unsigned char*)aBuffer, *aBytesRead); + + // be aggressive about ending the inflation + // for some reason we don't always get Z_STREAM_END + if (finished || mZs.total_out >= mOutSize) { + if (mMode == MODE_INFLATE) { + int zerr = inflateEnd(&mZs); + if (zerr != Z_OK) { + return NS_ERROR_FILE_CORRUPTED; + } + + // Stream is finished but has a different size from what + // we expected. + if (mozilla::StaticPrefs::network_jar_require_size_match() && + mZs.total_out != mOutSize) { + return NS_ERROR_FILE_CORRUPTED; + } + } + + // stop returning valid data as soon as we know we have a bad CRC + if (mOutCrc != mInCrc) { + return NS_ERROR_FILE_CORRUPTED; + } + } + + return NS_OK; +} + +nsresult nsJARInputStream::ReadDirectory(char* aBuffer, uint32_t aCount, + uint32_t* aBytesRead) { + // No need to check the args, ::Read did that, but assert them at least + NS_ASSERTION(aBuffer, "aBuffer parameter must not be null"); + NS_ASSERTION(aBytesRead, "aBytesRead parameter must not be null"); + + // If the buffer contains data, copy what's there up to the desired amount + uint32_t numRead = CopyDataToBuffer(aBuffer, aCount); + + if (aCount > 0) { + mozilla::RecursiveMutexAutoLock lock(mJar->mLock); + // empty the buffer and start writing directory entry lines to it + mBuffer.Truncate(); + mCurPos = 0; + const uint32_t arrayLen = mArray.Length(); + + for (; aCount > mBuffer.Length(); mArrPos++) { + // have we consumed all the directory contents? + if (arrayLen <= mArrPos) break; + + const char* entryName = mArray[mArrPos].get(); + uint32_t entryNameLen = mArray[mArrPos].Length(); + nsZipItem* ze = mJar->mZip->GetItem(entryName); + NS_ENSURE_TRUE(ze, NS_ERROR_FILE_NOT_FOUND); + + // Last Modified Time + PRExplodedTime tm; + PR_ExplodeTime(ze->LastModTime(), PR_GMTParameters, &tm); + char itemLastModTime[65]; + PR_FormatTimeUSEnglish(itemLastModTime, sizeof(itemLastModTime), + " %a,%%20%d%%20%b%%20%Y%%20%H:%M:%S%%20GMT ", &tm); + + // write a 201: line to the buffer for this item + // 200: filename content-length last-modified file-type + mBuffer.AppendLiteral("201: "); + + // Names must be escaped and relative, so use the pre-calculated length + // of the directory name as the offset into the string + // NS_EscapeURL adds the escaped URL to the give string buffer + NS_EscapeURL(entryName + mNameLen, entryNameLen - mNameLen, + esc_Minimal | esc_AlwaysCopy, mBuffer); + + mBuffer.Append(' '); + mBuffer.AppendInt(ze->RealSize(), 10); + mBuffer.Append(itemLastModTime); // starts/ends with ' ' + if (ze->IsDirectory()) + mBuffer.AppendLiteral("DIRECTORY\n"); + else + mBuffer.AppendLiteral("FILE\n"); + } + + // Copy up to the desired amount of data to buffer + numRead += CopyDataToBuffer(aBuffer, aCount); + } + + *aBytesRead = numRead; + return NS_OK; +} + +uint32_t nsJARInputStream::CopyDataToBuffer(char*& aBuffer, uint32_t& aCount) { + const uint32_t writeLength = + std::min<uint32_t>(aCount, mBuffer.Length() - mCurPos); + + if (writeLength > 0) { + std::copy(mBuffer.get() + mCurPos, mBuffer.get() + mCurPos + writeLength, + aBuffer); + mCurPos += writeLength; + aCount -= writeLength; + aBuffer += writeLength; + } + + // return number of bytes copied to the buffer so the + // Read method can return the number of bytes copied + return writeLength; +} diff --git a/modules/libjar/nsJARInputStream.h b/modules/libjar/nsJARInputStream.h new file mode 100644 index 0000000000..69cbf605b6 --- /dev/null +++ b/modules/libjar/nsJARInputStream.h @@ -0,0 +1,74 @@ +/* nsJARInputStream.h + * + * 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/. */ + +#ifndef nsJARINPUTSTREAM_h__ +#define nsJARINPUTSTREAM_h__ + +#include "nsIInputStream.h" +#include "nsJAR.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" + +/*------------------------------------------------------------------------- + * Class nsJARInputStream declaration. This class defines the type of the + * object returned by calls to nsJAR::GetInputStream(filename) for the + * purpose of reading a file item out of a JAR file. + *------------------------------------------------------------------------*/ +class nsJARInputStream final : public nsIInputStream { + public: + nsJARInputStream() + : mOutSize(0), + mInCrc(0), + mOutCrc(0), + mNameLen(0), + mCurPos(0), + mArrPos(0), + mMode(MODE_NOTINITED) { + memset(&mZs, 0, sizeof(z_stream)); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + + // takes ownership of |fd|, even on failure + nsresult InitFile(nsZipHandle* aFd, const uint8_t* aData, nsZipItem* item); + + nsresult InitDirectory(nsJAR* aJar, const nsACString& aJarDirSpec, + const char* aDir); + + private: + ~nsJARInputStream() { Close(); } + + RefPtr<nsZipHandle> mFd; // handle for reading + uint32_t mOutSize; // inflated size + uint32_t mInCrc; // CRC as provided by the zipentry + uint32_t mOutCrc; // CRC as calculated by me + z_stream mZs; // zip data structure + + /* For directory reading */ + RefPtr<nsJAR> mJar; // string reference to zipreader + uint32_t mNameLen; // length of dirname + nsCString mBuffer; // storage for generated text of stream + uint32_t mCurPos; // Current position in buffer + uint32_t mArrPos; // current position within mArray + nsTArray<nsCString> mArray; // array of names in (zip) directory + + typedef enum { + MODE_NOTINITED, + MODE_CLOSED, + MODE_DIRECTORY, + MODE_INFLATE, + MODE_COPY + } JISMode; + + JISMode mMode; // Modus of the stream + + nsresult ContinueInflate(char* aBuf, uint32_t aCount, uint32_t* aBytesRead); + nsresult ReadDirectory(char* aBuf, uint32_t aCount, uint32_t* aBytesRead); + uint32_t CopyDataToBuffer(char*& aBuffer, uint32_t& aCount); +}; + +#endif /* nsJARINPUTSTREAM_h__ */ diff --git a/modules/libjar/nsJARProtocolHandler.cpp b/modules/libjar/nsJARProtocolHandler.cpp new file mode 100644 index 0000000000..5fd3954d4f --- /dev/null +++ b/modules/libjar/nsJARProtocolHandler.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozilla/ClearOnShutdown.h" +#include "nsJARProtocolHandler.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "nsJARURI.h" +#include "nsJARChannel.h" +#include "nsString.h" +#include "nsNetCID.h" +#include "nsIMIMEService.h" +#include "nsMimeTypes.h" +#include "nsThreadUtils.h" + +static NS_DEFINE_CID(kZipReaderCacheCID, NS_ZIPREADERCACHE_CID); + +#define NS_JAR_CACHE_SIZE 32 + +//----------------------------------------------------------------------------- + +mozilla::StaticRefPtr<nsJARProtocolHandler> gJarHandler; + +nsJARProtocolHandler::nsJARProtocolHandler() { MOZ_ASSERT(NS_IsMainThread()); } + +nsJARProtocolHandler::~nsJARProtocolHandler() {} + +nsresult nsJARProtocolHandler::Init() { + nsresult rv; + + mJARCache = do_CreateInstance(kZipReaderCacheCID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = mJARCache->Init(NS_JAR_CACHE_SIZE); + return rv; +} + +nsIMIMEService* nsJARProtocolHandler::MimeService() { + if (!mMimeService) mMimeService = do_GetService("@mozilla.org/mime;1"); + + return mMimeService.get(); +} + +NS_IMPL_ISUPPORTS(nsJARProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +already_AddRefed<nsJARProtocolHandler> nsJARProtocolHandler::GetSingleton() { + if (!gJarHandler) { + gJarHandler = new nsJARProtocolHandler(); + if (NS_SUCCEEDED(gJarHandler->Init())) { + ClearOnShutdown(&gJarHandler); + } else { + gJarHandler = nullptr; + } + } + return do_AddRef(gJarHandler); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsJARProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("jar"); + return NS_OK; +} + +NS_IMETHODIMP +nsJARProtocolHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + nsJARChannel* chan = new nsJARChannel(); + if (!chan) return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(chan); + + nsresult rv = chan->Init(uri); + if (NS_FAILED(rv)) { + NS_RELEASE(chan); + return rv; + } + + // set the loadInfo on the new channel + rv = chan->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + NS_RELEASE(chan); + return rv; + } + + *result = chan; + return NS_OK; +} + +NS_IMETHODIMP +nsJARProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/modules/libjar/nsJARProtocolHandler.h b/modules/libjar/nsJARProtocolHandler.h new file mode 100644 index 0000000000..6e77d91ba3 --- /dev/null +++ b/modules/libjar/nsJARProtocolHandler.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsJARProtocolHandler_h__ +#define nsJARProtocolHandler_h__ + +#include "mozilla/StaticPtr.h" +#include "nsIProtocolHandler.h" +#include "nsIZipReader.h" +#include "nsIMIMEService.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" + +class nsJARProtocolHandler final : public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + // nsJARProtocolHandler methods: + nsJARProtocolHandler(); + + static already_AddRefed<nsJARProtocolHandler> GetSingleton(); + + nsresult Init(); + + // returns non addref'ed pointer. + nsIMIMEService* MimeService(); + nsIZipReaderCache* JarCache() const { return mJARCache; } + + protected: + virtual ~nsJARProtocolHandler(); + + nsCOMPtr<nsIZipReaderCache> mJARCache; + nsCOMPtr<nsIMIMEService> mMimeService; +}; + +extern mozilla::StaticRefPtr<nsJARProtocolHandler> gJarHandler; + +#define NS_JARPROTOCOLHANDLER_CID \ + { /* 0xc7e410d4-0x85f2-11d3-9f63-006008a6efe9 */ \ + 0xc7e410d4, 0x85f2, 0x11d3, { \ + 0x9f, 0x63, 0x00, 0x60, 0x08, 0xa6, 0xef, 0xe9 \ + } \ + } + +#endif // !nsJARProtocolHandler_h__ diff --git a/modules/libjar/nsJARURI.cpp b/modules/libjar/nsJARURI.cpp new file mode 100644 index 0000000000..e6837e998f --- /dev/null +++ b/modules/libjar/nsJARURI.cpp @@ -0,0 +1,714 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#include "base/basictypes.h" + +#include "nsJARURI.h" +#include "nsNetUtil.h" +#include "nsIClassInfoImpl.h" +#include "nsIIOService.h" +#include "nsIStandardURL.h" +#include "nsCRT.h" +#include "nsReadableUtils.h" +#include "nsNetCID.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsQueryObject.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla::ipc; + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_CLASSINFO(nsJARURI, nullptr, nsIClassInfo::THREADSAFE, NS_JARURI_CID) +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(nsJARURI) + +nsJARURI::nsJARURI() {} + +nsJARURI::~nsJARURI() {} + +// XXX Why is this threadsafe? +NS_IMPL_ADDREF(nsJARURI) +NS_IMPL_RELEASE(nsJARURI) +NS_INTERFACE_MAP_BEGIN(nsJARURI) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIJARURI) + NS_INTERFACE_MAP_ENTRY(nsIURI) + NS_INTERFACE_MAP_ENTRY(nsIURL) + NS_INTERFACE_MAP_ENTRY(nsIJARURI) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + NS_IMPL_QUERY_CLASSINFO(nsJARURI) + NS_INTERFACE_MAP_ENTRY(nsINestedURI) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsJARURI) +NS_INTERFACE_MAP_END + +nsresult nsJARURI::Init(const char* charsetHint) { + mCharsetHint = charsetHint; + return NS_OK; +} + +#define NS_JAR_SCHEME "jar:"_ns +#define NS_JAR_DELIMITER "!/"_ns +#define NS_BOGUS_ENTRY_SCHEME "x:///"_ns + +// FormatSpec takes the entry spec (including the "x:///" at the +// beginning) and gives us a full JAR spec. +nsresult nsJARURI::FormatSpec(const nsACString& entrySpec, nsACString& result, + bool aIncludeScheme) { + // The entrySpec MUST start with "x:///" + NS_ASSERTION(StringBeginsWith(entrySpec, NS_BOGUS_ENTRY_SCHEME), + "bogus entry spec"); + + nsAutoCString fileSpec; + nsresult rv = mJARFile->GetSpec(fileSpec); + if (NS_FAILED(rv)) return rv; + + if (aIncludeScheme) + result = NS_JAR_SCHEME; + else + result.Truncate(); + + result.Append(fileSpec + NS_JAR_DELIMITER + + Substring(entrySpec, 5, entrySpec.Length() - 5)); + return NS_OK; +} + +nsresult nsJARURI::CreateEntryURL(const nsACString& entryFilename, + const char* charset, nsIURL** url) { + *url = nullptr; + // Flatten the concatenation, just in case. See bug 128288 + nsAutoCString spec(NS_BOGUS_ENTRY_SCHEME + entryFilename); + return NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .Apply(&nsIStandardURLMutator::Init, nsIStandardURL::URLTYPE_NO_AUTHORITY, + -1, spec, charset, nullptr, nullptr) + .Finalize(url); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISerializable methods: + +NS_IMETHODIMP +nsJARURI::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT_UNREACHABLE("Use nsIURIMutator.read() instead"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsJARURI::ReadPrivate(nsIObjectInputStream* aInputStream) { + nsresult rv; + + nsCOMPtr<nsISupports> supports; + rv = aInputStream->ReadObject(true, getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(rv, rv); + + mJARFile = do_QueryInterface(supports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aInputStream->ReadObject(true, getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(rv, rv); + + mJAREntry = do_QueryInterface(supports); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aInputStream->ReadCString(mCharsetHint); + return rv; +} + +NS_IMETHODIMP +nsJARURI::Write(nsIObjectOutputStream* aOutputStream) { + nsresult rv; + + rv = aOutputStream->WriteCompoundObject(mJARFile, NS_GET_IID(nsIURI), true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aOutputStream->WriteCompoundObject(mJAREntry, NS_GET_IID(nsIURL), true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aOutputStream->WriteStringZ(mCharsetHint.get()); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIURI methods: + +NS_IMETHODIMP +nsJARURI::GetSpec(nsACString& aSpec) { + nsAutoCString entrySpec; + mJAREntry->GetSpec(entrySpec); + return FormatSpec(entrySpec, aSpec); +} + +NS_IMETHODIMP +nsJARURI::GetSpecIgnoringRef(nsACString& aSpec) { + nsAutoCString entrySpec; + mJAREntry->GetSpecIgnoringRef(entrySpec); + return FormatSpec(entrySpec, aSpec); +} + +NS_IMETHODIMP +nsJARURI::GetDisplaySpec(nsACString& aUnicodeSpec) { + return GetSpec(aUnicodeSpec); +} + +NS_IMETHODIMP +nsJARURI::GetDisplayHostPort(nsACString& aUnicodeHostPort) { + return GetHostPort(aUnicodeHostPort); +} + +NS_IMETHODIMP +nsJARURI::GetDisplayPrePath(nsACString& aPrePath) { + return GetPrePath(aPrePath); +} + +NS_IMETHODIMP +nsJARURI::GetDisplayHost(nsACString& aUnicodeHost) { + return GetHost(aUnicodeHost); +} + +NS_IMETHODIMP +nsJARURI::GetHasRef(bool* result) { return mJAREntry->GetHasRef(result); } + +nsresult nsJARURI::SetSpecInternal(const nsACString& aSpec) { + return SetSpecWithBase(aSpec, nullptr); +} + +// Queries this list of interfaces. If none match, it queries mURI. +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsJARURI::Mutator, nsIURISetters, nsIURIMutator, + nsIURLMutator, nsISerializable, + nsIJARURIMutator) + +NS_IMETHODIMP +nsJARURI::Mutator::SetFileName(const nsACString& aFileName, + nsIURIMutator** aMutator) { + if (!mURI) { + return NS_ERROR_NULL_POINTER; + } + if (aMutator) { + nsCOMPtr<nsIURIMutator> mutator = this; + mutator.forget(aMutator); + } + return mURI->SetFileNameInternal(aFileName); +} + +NS_IMETHODIMP +nsJARURI::Mutator::SetFileBaseName(const nsACString& aFileBaseName, + nsIURIMutator** aMutator) { + if (!mURI) { + return NS_ERROR_NULL_POINTER; + } + if (aMutator) { + nsCOMPtr<nsIURIMutator> mutator = this; + mutator.forget(aMutator); + } + return mURI->SetFileBaseNameInternal(aFileBaseName); +} + +NS_IMETHODIMP +nsJARURI::Mutator::SetFileExtension(const nsACString& aFileExtension, + nsIURIMutator** aMutator) { + if (!mURI) { + return NS_ERROR_NULL_POINTER; + } + if (aMutator) { + nsCOMPtr<nsIURIMutator> mutator = this; + mutator.forget(aMutator); + } + return mURI->SetFileExtensionInternal(aFileExtension); +} + +NS_IMETHODIMP +nsJARURI::Mutate(nsIURIMutator** aMutator) { + RefPtr<nsJARURI::Mutator> mutator = new nsJARURI::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} + +nsresult nsJARURI::SetSpecWithBase(const nsACString& aSpec, nsIURI* aBaseURL) { + nsresult rv; + + nsCOMPtr<nsIIOService> ioServ(do_GetIOService(&rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + rv = ioServ->ExtractScheme(aSpec, scheme); + if (NS_FAILED(rv)) { + // not an absolute URI + if (!aBaseURL) return NS_ERROR_MALFORMED_URI; + + RefPtr<nsJARURI> otherJAR = do_QueryObject(aBaseURL); + NS_ENSURE_TRUE(otherJAR, NS_NOINTERFACE); + + mJARFile = otherJAR->mJARFile; + + nsCOMPtr<nsIURI> entry; + + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .Apply(&nsIStandardURLMutator::Init, + nsIStandardURL::URLTYPE_NO_AUTHORITY, -1, aSpec, + mCharsetHint.get(), otherJAR->mJAREntry, nullptr) + .Finalize(entry); + if (NS_FAILED(rv)) { + return rv; + } + + mJAREntry = do_QueryInterface(entry); + if (!mJAREntry) return NS_NOINTERFACE; + + return NS_OK; + } + + NS_ENSURE_TRUE(scheme.EqualsLiteral("jar"), NS_ERROR_MALFORMED_URI); + + nsACString::const_iterator begin, end; + aSpec.BeginReading(begin); + aSpec.EndReading(end); + + while (begin != end && *begin != ':') ++begin; + + ++begin; // now we're past the "jar:" + + nsACString::const_iterator delim_begin = begin; + nsACString::const_iterator delim_end = end; + nsACString::const_iterator frag = begin; + + if (FindInReadable(NS_JAR_DELIMITER, delim_begin, delim_end)) { + frag = delim_end; + } + while (frag != end && (*frag != '#' && *frag != '?')) { + ++frag; + } + if (frag != end) { + // there was a fragment or query, mark that as the end of the URL to scan + end = frag; + } + + // Search backward from the end for the "!/" delimiter. Remember, jar URLs + // can nest, e.g.: + // jar:jar:http://www.foo.com/bar.jar!/a.jar!/b.html + // This gets the b.html document from out of the a.jar file, that's + // contained within the bar.jar file. + // Also, the outermost "inner" URI may be a relative URI: + // jar:../relative.jar!/a.html + + delim_begin = begin; + delim_end = end; + + if (!RFindInReadable(NS_JAR_DELIMITER, delim_begin, delim_end)) { + return NS_ERROR_MALFORMED_URI; + } + + rv = ioServ->NewURI(Substring(begin, delim_begin), mCharsetHint.get(), + aBaseURL, getter_AddRefs(mJARFile)); + if (NS_FAILED(rv)) return rv; + + // skip over any extra '/' chars + while (*delim_end == '/') ++delim_end; + + aSpec.EndReading(end); // set to the original 'end' + return SetJAREntry(Substring(delim_end, end)); +} + +NS_IMETHODIMP +nsJARURI::GetPrePath(nsACString& prePath) { + prePath = NS_JAR_SCHEME; + return NS_OK; +} + +NS_IMETHODIMP +nsJARURI::GetScheme(nsACString& aScheme) { + aScheme = "jar"; + return NS_OK; +} + +nsresult nsJARURI::SetScheme(const nsACString& aScheme) { + // doesn't make sense to set the scheme of a jar: URL + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetUserPass(nsACString& aUserPass) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetUserPass(const nsACString& aUserPass) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetUsername(nsACString& aUsername) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetUsername(const nsACString& aUsername) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetPassword(nsACString& aPassword) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetPassword(const nsACString& aPassword) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetHostPort(nsACString& aHostPort) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetHostPort(const nsACString& aHostPort) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetHost(nsACString& aHost) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetHost(const nsACString& aHost) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +nsJARURI::GetPort(int32_t* aPort) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::SetPort(int32_t aPort) { return NS_ERROR_FAILURE; } + +nsresult nsJARURI::GetPathQueryRef(nsACString& aPath) { + nsAutoCString entrySpec; + mJAREntry->GetSpec(entrySpec); + return FormatSpec(entrySpec, aPath, false); +} + +nsresult nsJARURI::SetPathQueryRef(const nsACString& aPath) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsJARURI::GetAsciiSpec(nsACString& aSpec) { + // XXX Shouldn't this like... make sure it returns ASCII or something? + return GetSpec(aSpec); +} + +NS_IMETHODIMP +nsJARURI::GetAsciiHostPort(nsACString& aHostPort) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +nsJARURI::GetAsciiHost(nsACString& aHost) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +nsJARURI::Equals(nsIURI* other, bool* result) { + return EqualsInternal(other, eHonorRef, result); +} + +NS_IMETHODIMP +nsJARURI::EqualsExceptRef(nsIURI* other, bool* result) { + return EqualsInternal(other, eIgnoreRef, result); +} + +// Helper method: +/* virtual */ +nsresult nsJARURI::EqualsInternal(nsIURI* other, + nsJARURI::RefHandlingEnum refHandlingMode, + bool* result) { + *result = false; + + if (!other) return NS_OK; // not equal + + RefPtr<nsJARURI> otherJAR = do_QueryObject(other); + if (!otherJAR) return NS_OK; // not equal + + bool equal; + nsresult rv = mJARFile->Equals(otherJAR->mJARFile, &equal); + if (NS_FAILED(rv) || !equal) { + return rv; // not equal + } + + return refHandlingMode == eHonorRef + ? mJAREntry->Equals(otherJAR->mJAREntry, result) + : mJAREntry->EqualsExceptRef(otherJAR->mJAREntry, result); +} + +NS_IMETHODIMP +nsJARURI::SchemeIs(const char* i_Scheme, bool* o_Equals) { + MOZ_ASSERT(o_Equals); + if (!i_Scheme) { + *o_Equals = false; + return NS_OK; + } + + *o_Equals = nsCRT::strcasecmp("jar", i_Scheme) == 0; + return NS_OK; +} + +nsresult nsJARURI::Clone(nsIURI** result) { + RefPtr<nsJARURI> uri = new nsJARURI(); + uri->mJARFile = mJARFile; + uri->mJAREntry = mJAREntry; + uri.forget(result); + + return NS_OK; +} + +NS_IMETHODIMP +nsJARURI::Resolve(const nsACString& relativePath, nsACString& result) { + nsresult rv; + + nsCOMPtr<nsIIOService> ioServ(do_GetIOService(&rv)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString scheme; + rv = ioServ->ExtractScheme(relativePath, scheme); + if (NS_SUCCEEDED(rv)) { + // then aSpec is absolute + result = relativePath; + return NS_OK; + } + + nsAutoCString resolvedPath; + mJAREntry->Resolve(relativePath, resolvedPath); + + return FormatSpec(resolvedPath, result); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIURL methods: + +NS_IMETHODIMP +nsJARURI::GetFilePath(nsACString& filePath) { + return mJAREntry->GetFilePath(filePath); +} + +nsresult nsJARURI::SetFilePath(const nsACString& filePath) { + return NS_MutateURI(mJAREntry).SetFilePath(filePath).Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetQuery(nsACString& query) { return mJAREntry->GetQuery(query); } + +nsresult nsJARURI::SetQuery(const nsACString& query) { + return NS_MutateURI(mJAREntry).SetQuery(query).Finalize(mJAREntry); +} + +nsresult nsJARURI::SetQueryWithEncoding(const nsACString& query, + const mozilla::Encoding* encoding) { + return NS_MutateURI(mJAREntry) + .SetQueryWithEncoding(query, encoding) + .Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetRef(nsACString& ref) { return mJAREntry->GetRef(ref); } + +nsresult nsJARURI::SetRef(const nsACString& ref) { + return NS_MutateURI(mJAREntry).SetRef(ref).Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetDirectory(nsACString& directory) { + return mJAREntry->GetDirectory(directory); +} + +NS_IMETHODIMP +nsJARURI::GetFileName(nsACString& fileName) { + return mJAREntry->GetFileName(fileName); +} + +nsresult nsJARURI::SetFileNameInternal(const nsACString& fileName) { + return NS_MutateURI(mJAREntry) + .Apply(&nsIURLMutator::SetFileName, fileName, nullptr) + .Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetFileBaseName(nsACString& fileBaseName) { + return mJAREntry->GetFileBaseName(fileBaseName); +} + +nsresult nsJARURI::SetFileBaseNameInternal(const nsACString& fileBaseName) { + return NS_MutateURI(mJAREntry) + .Apply(&nsIURLMutator::SetFileBaseName, fileBaseName, nullptr) + .Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetFileExtension(nsACString& fileExtension) { + return mJAREntry->GetFileExtension(fileExtension); +} + +nsresult nsJARURI::SetFileExtensionInternal(const nsACString& fileExtension) { + return NS_MutateURI(mJAREntry) + .Apply(&nsIURLMutator::SetFileExtension, fileExtension, nullptr) + .Finalize(mJAREntry); +} + +NS_IMETHODIMP +nsJARURI::GetCommonBaseSpec(nsIURI* uriToCompare, nsACString& commonSpec) { + commonSpec.Truncate(); + + NS_ENSURE_ARG_POINTER(uriToCompare); + + commonSpec.Truncate(); + nsCOMPtr<nsIJARURI> otherJARURI(do_QueryInterface(uriToCompare)); + if (!otherJARURI) { + // Nothing in common + return NS_OK; + } + + nsCOMPtr<nsIURI> otherJARFile; + nsresult rv = otherJARURI->GetJARFile(getter_AddRefs(otherJARFile)); + if (NS_FAILED(rv)) return rv; + + bool equal; + rv = mJARFile->Equals(otherJARFile, &equal); + if (NS_FAILED(rv)) return rv; + + if (!equal) { + // See what the JAR file URIs have in common + nsCOMPtr<nsIURL> ourJARFileURL(do_QueryInterface(mJARFile)); + if (!ourJARFileURL) { + // Not a URL, so nothing in common + return NS_OK; + } + nsAutoCString common; + rv = ourJARFileURL->GetCommonBaseSpec(otherJARFile, common); + if (NS_FAILED(rv)) return rv; + + commonSpec = NS_JAR_SCHEME + common; + return NS_OK; + } + + // At this point we have the same JAR file. Compare the JAREntrys + nsAutoCString otherEntry; + rv = otherJARURI->GetJAREntry(otherEntry); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURL> url; + rv = CreateEntryURL(otherEntry, nullptr, getter_AddRefs(url)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString common; + rv = mJAREntry->GetCommonBaseSpec(url, common); + if (NS_FAILED(rv)) return rv; + + rv = FormatSpec(common, commonSpec); + return rv; +} + +NS_IMETHODIMP +nsJARURI::GetRelativeSpec(nsIURI* uriToCompare, nsACString& relativeSpec) { + GetSpec(relativeSpec); + + NS_ENSURE_ARG_POINTER(uriToCompare); + + nsCOMPtr<nsIJARURI> otherJARURI(do_QueryInterface(uriToCompare)); + if (!otherJARURI) { + // Nothing in common + return NS_OK; + } + + nsCOMPtr<nsIURI> otherJARFile; + nsresult rv = otherJARURI->GetJARFile(getter_AddRefs(otherJARFile)); + if (NS_FAILED(rv)) return rv; + + bool equal; + rv = mJARFile->Equals(otherJARFile, &equal); + if (NS_FAILED(rv)) return rv; + + if (!equal) { + // We live in different JAR files. Nothing in common. + return rv; + } + + // Same JAR file. Compare the JAREntrys + nsAutoCString otherEntry; + rv = otherJARURI->GetJAREntry(otherEntry); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURL> url; + rv = CreateEntryURL(otherEntry, nullptr, getter_AddRefs(url)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString relativeEntrySpec; + rv = mJAREntry->GetRelativeSpec(url, relativeEntrySpec); + if (NS_FAILED(rv)) return rv; + + if (!StringBeginsWith(relativeEntrySpec, NS_BOGUS_ENTRY_SCHEME)) { + // An actual relative spec! + relativeSpec = relativeEntrySpec; + } + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIJARURI methods: + +NS_IMETHODIMP +nsJARURI::GetJARFile(nsIURI** jarFile) { return GetInnerURI(jarFile); } + +NS_IMETHODIMP +nsJARURI::GetJAREntry(nsACString& entryPath) { + nsAutoCString filePath; + mJAREntry->GetFilePath(filePath); + NS_ASSERTION(filePath.Length() > 0, "path should never be empty!"); + // Trim off the leading '/' + entryPath = Substring(filePath, 1, filePath.Length() - 1); + return NS_OK; +} + +nsresult nsJARURI::SetJAREntry(const nsACString& entryPath) { + return CreateEntryURL(entryPath, mCharsetHint.get(), + getter_AddRefs(mJAREntry)); +} + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsJARURI::GetInnerURI(nsIURI** aURI) { + nsCOMPtr<nsIURI> uri = mJARFile; + uri.forget(aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsJARURI::GetInnermostURI(nsIURI** uri) { + return NS_ImplGetInnermostURI(this, uri); +} + +void nsJARURI::Serialize(URIParams& aParams) { + JARURIParams params; + + SerializeURI(mJARFile, params.jarFile()); + SerializeURI(mJAREntry, params.jarEntry()); + params.charset() = mCharsetHint; + + aParams = params; +} + +bool nsJARURI::Deserialize(const URIParams& aParams) { + if (aParams.type() != URIParams::TJARURIParams) { + NS_ERROR("Received unknown parameters from the other process!"); + return false; + } + + const JARURIParams& params = aParams.get_JARURIParams(); + + nsCOMPtr<nsIURI> file = DeserializeURI(params.jarFile()); + if (!file) { + NS_ERROR("Couldn't deserialize jar file URI!"); + return false; + } + + nsCOMPtr<nsIURI> entry = DeserializeURI(params.jarEntry()); + if (!entry) { + NS_ERROR("Couldn't deserialize jar entry URI!"); + return false; + } + + nsCOMPtr<nsIURL> entryURL = do_QueryInterface(entry); + if (!entryURL) { + NS_ERROR("Couldn't QI jar entry URI to nsIURL!"); + return false; + } + + mJARFile.swap(file); + mJAREntry.swap(entryURL); + mCharsetHint = params.charset(); + + return true; +} diff --git a/modules/libjar/nsJARURI.h b/modules/libjar/nsJARURI.h new file mode 100644 index 0000000000..7be600f021 --- /dev/null +++ b/modules/libjar/nsJARURI.h @@ -0,0 +1,156 @@ +/* -*- Mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef nsJARURI_h__ +#define nsJARURI_h__ + +#include "nsIJARURI.h" +#include "nsISerializable.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsINestedURI.h" +#include "nsIURIMutator.h" + +#define NS_THIS_JARURI_IMPL_CID \ + { /* 9a55f629-730b-4d08-b75b-fa7d9570a691 */ \ + 0x9a55f629, 0x730b, 0x4d08, { \ + 0xb7, 0x5b, 0xfa, 0x7d, 0x95, 0x70, 0xa6, 0x91 \ + } \ + } + +#define NS_JARURI_CID \ + { /* 245abae2-b947-4ded-a46d-9829d3cca462 */ \ + 0x245abae2, 0xb947, 0x4ded, { \ + 0xa4, 0x6d, 0x98, 0x29, 0xd3, 0xcc, 0xa4, 0x62 \ + } \ + } + +#define NS_JARURIMUTATOR_CID \ + { /* 19d9161b-a2a9-4518-b2c9-fcb8296d6dcd */ \ + 0x19d9161b, 0xa2a9, 0x4518, { \ + 0xb2, 0xc9, 0xfc, 0xb8, 0x29, 0x6d, 0x6d, 0xcd \ + } \ + } + +class nsJARURI final : public nsIJARURI, + public nsISerializable, + public nsINestedURI { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURI + NS_DECL_NSIURL + NS_DECL_NSIJARURI + NS_DECL_NSISERIALIZABLE + NS_DECL_NSINESTEDURI + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_JARURI_IMPL_CID) + + // nsJARURI + nsresult FormatSpec(const nsACString& entryPath, nsACString& result, + bool aIncludeScheme = true); + nsresult CreateEntryURL(const nsACString& entryFilename, const char* charset, + nsIURL** url); + + protected: + nsJARURI(); + virtual ~nsJARURI(); + nsresult SetJAREntry(const nsACString& entryPath); + nsresult Init(const char* charsetHint); + nsresult SetSpecWithBase(const nsACString& aSpec, nsIURI* aBaseURL); + + // enum used in a few places to specify how .ref attribute should be handled + enum RefHandlingEnum { eIgnoreRef, eHonorRef, eReplaceRef }; + + // Helper to share code between Equals methods. + virtual nsresult EqualsInternal(nsIURI* other, + RefHandlingEnum refHandlingMode, + bool* result); + + nsCOMPtr<nsIURI> mJARFile; + // mJarEntry stored as a URL so that we can easily access things + // like extensions, refs, etc. + nsCOMPtr<nsIURL> mJAREntry; + nsCString mCharsetHint; + + private: + nsresult Clone(nsIURI** aURI); + nsresult SetSpecInternal(const nsACString& input); + nsresult SetScheme(const nsACString& input); + nsresult SetUserPass(const nsACString& input); + nsresult SetUsername(const nsACString& input); + nsresult SetPassword(const nsACString& input); + nsresult SetHostPort(const nsACString& aValue); + nsresult SetHost(const nsACString& input); + nsresult SetPort(int32_t port); + nsresult SetPathQueryRef(const nsACString& input); + nsresult SetRef(const nsACString& input); + nsresult SetFilePath(const nsACString& input); + nsresult SetQuery(const nsACString& input); + nsresult SetQueryWithEncoding(const nsACString& input, + const mozilla::Encoding* encoding); + bool Deserialize(const mozilla::ipc::URIParams&); + nsresult ReadPrivate(nsIObjectInputStream* aStream); + + nsresult SetFileNameInternal(const nsACString& fileName); + nsresult SetFileBaseNameInternal(const nsACString& fileBaseName); + nsresult SetFileExtensionInternal(const nsACString& fileExtension); + + public: + class Mutator final : public nsIURIMutator, + public BaseURIMutator<nsJARURI>, + public nsIURLMutator, + public nsISerializable, + public nsIJARURIMutator { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + NS_DEFINE_NSIMUTATOR_COMMON + NS_DECL_NSIURLMUTATOR + + NS_IMETHOD + Write(nsIObjectOutputStream* aOutputStream) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + [[nodiscard]] NS_IMETHOD Read(nsIObjectInputStream* aStream) override { + return InitFromInputStream(aStream); + } + + NS_IMETHOD + SetSpecBaseCharset(const nsACString& aSpec, nsIURI* aBaseURI, + const char* aCharset) override { + RefPtr<nsJARURI> uri; + if (mURI) { + mURI.swap(uri); + } else { + uri = new nsJARURI(); + } + + nsresult rv = uri->Init(aCharset); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uri->SetSpecWithBase(aSpec, aBaseURI); + if (NS_FAILED(rv)) { + return rv; + } + + mURI.swap(uri); + return NS_OK; + } + + explicit Mutator() {} + + private: + virtual ~Mutator() {} + + friend class nsJARURI; + }; + + friend BaseURIMutator<nsJARURI>; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsJARURI, NS_THIS_JARURI_IMPL_CID) + +#endif // nsJARURI_h__ diff --git a/modules/libjar/nsZipArchive.cpp b/modules/libjar/nsZipArchive.cpp new file mode 100644 index 0000000000..1f43a0d625 --- /dev/null +++ b/modules/libjar/nsZipArchive.cpp @@ -0,0 +1,1189 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + * This module implements a simple archive extractor for the PKZIP format. + */ + +#define READTYPE int32_t +#include "zlib.h" +#include "nsISupportsUtils.h" +#include "mozilla/MmapFaultHandler.h" +#include "prio.h" +#include "mozilla/Attributes.h" +#include "mozilla/Logging.h" +#include "mozilla/MemUtils.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/StaticMutex.h" +#include "stdlib.h" +#include "nsDirectoryService.h" +#include "nsWildCard.h" +#include "nsXULAppAPI.h" +#include "nsZipArchive.h" +#include "nsString.h" +#include "prenv.h" +#if defined(XP_WIN) +# include <windows.h> +#endif + +// For placement new used for arena allocations of zip file list +#include <new> +#define ZIP_ARENABLOCKSIZE (1 * 1024) + +#ifdef XP_UNIX +# include <sys/mman.h> +# include <sys/types.h> +# include <sys/stat.h> +# include <limits.h> +# include <unistd.h> +#elif defined(XP_WIN) +# include <io.h> +#endif + +#ifdef __SYMBIAN32__ +# include <sys/syslimits.h> +#endif /*__SYMBIAN32__*/ + +#ifndef XP_UNIX /* we need some constants defined in limits.h and unistd.h */ +# ifndef S_IFMT +# define S_IFMT 0170000 +# endif +# ifndef S_IFLNK +# define S_IFLNK 0120000 +# endif +# ifndef PATH_MAX +# define PATH_MAX 1024 +# endif +#endif /* XP_UNIX */ + +#ifdef XP_WIN +# include "private/pprio.h" // To get PR_ImportFile +#endif + +using namespace mozilla; + +static LazyLogModule gZipLog("nsZipArchive"); + +#ifdef LOG +# undef LOG +#endif +#ifdef LOG_ENABLED +# undef LOG_ENABLED +#endif + +#define LOG(args) MOZ_LOG(gZipLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(gZipLog, mozilla::LogLevel::Debug) + +static const uint32_t kMaxNameLength = PATH_MAX; /* Maximum name length */ +// For synthetic zip entries. Date/time corresponds to 1980-01-01 00:00. +static const uint16_t kSyntheticTime = 0; +static const uint16_t kSyntheticDate = (1 + (1 << 5) + (0 << 9)); + +static uint16_t xtoint(const uint8_t* ii); +static uint32_t xtolong(const uint8_t* ll); +static uint32_t HashName(const char* aName, uint16_t nameLen); + +class ZipArchiveLogger { + public: + void Init(const char* env) { + StaticMutexAutoLock lock(sLock); + + // AddRef + MOZ_ASSERT(mRefCnt >= 0); + ++mRefCnt; + + if (!mFd) { + nsCOMPtr<nsIFile> logFile; + nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false, + getter_AddRefs(logFile)); + if (NS_FAILED(rv)) return; + + // Create the log file and its parent directory (in case it doesn't exist) + rv = logFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + if (NS_FAILED(rv)) return; + + PRFileDesc* file; +#ifdef XP_WIN + // PR_APPEND is racy on Windows, so open a handle ourselves with flags + // that will work, and use PR_ImportFile to make it a PRFileDesc. This can + // go away when bug 840435 is fixed. + nsAutoString path; + logFile->GetPath(path); + if (path.IsEmpty()) return; + HANDLE handle = + CreateFileW(path.get(), FILE_APPEND_DATA, FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) return; + file = PR_ImportFile((PROsfd)handle); + if (!file) return; +#else + rv = logFile->OpenNSPRFileDesc( + PR_WRONLY | PR_CREATE_FILE | PR_APPEND | PR_SYNC, 0644, &file); + if (NS_FAILED(rv)) return; +#endif + mFd = file; + } + } + + void Write(const nsACString& zip, const char* entry) { + StaticMutexAutoLock lock(sLock); + + if (mFd) { + nsCString buf(zip); + buf.Append(' '); + buf.Append(entry); + buf.Append('\n'); + PR_Write(mFd, buf.get(), buf.Length()); + } + } + + void Release() { + StaticMutexAutoLock lock(sLock); + + MOZ_ASSERT(mRefCnt > 0); + if ((0 == --mRefCnt) && mFd) { + PR_Close(mFd); + mFd = nullptr; + } + } + + private: + static StaticMutex sLock; + int mRefCnt MOZ_GUARDED_BY(sLock); + PRFileDesc* mFd MOZ_GUARDED_BY(sLock); +}; + +StaticMutex ZipArchiveLogger::sLock; +static ZipArchiveLogger zipLog; + +//*********************************************************** +// For every inflation the following allocations are done: +// malloc(1 * 9520) +// malloc(32768 * 1) +//*********************************************************** + +nsresult gZlibInit(z_stream* zs) { + memset(zs, 0, sizeof(z_stream)); + int zerr = inflateInit2(zs, -MAX_WBITS); + if (zerr != Z_OK) return NS_ERROR_OUT_OF_MEMORY; + + return NS_OK; +} + +nsZipHandle::nsZipHandle() + : mFileData(nullptr), + mLen(0), + mMap(nullptr), + mRefCnt(0), + mFileStart(nullptr), + mTotalLen(0) {} + +NS_IMPL_ADDREF(nsZipHandle) +NS_IMPL_RELEASE(nsZipHandle) + +nsresult nsZipHandle::Init(nsIFile* file, nsZipHandle** ret, PRFileDesc** aFd) { + mozilla::AutoFDClose fd; + int32_t flags = PR_RDONLY; +#if defined(XP_WIN) + flags |= nsIFile::OS_READAHEAD; +#endif + LOG(("ZipHandle::Init %s", file->HumanReadablePath().get())); + nsresult rv = file->OpenNSPRFileDesc(flags, 0000, &fd.rwget()); + if (NS_FAILED(rv)) return rv; + + int64_t size = PR_Available64(fd); + if (size >= INT32_MAX) return NS_ERROR_FILE_TOO_BIG; + + PRFileMap* map = PR_CreateFileMap(fd, size, PR_PROT_READONLY); + if (!map) return NS_ERROR_FAILURE; + + uint8_t* buf = (uint8_t*)PR_MemMap(map, 0, (uint32_t)size); + // Bug 525755: PR_MemMap fails when fd points at something other than a normal + // file. + if (!buf) { + PR_CloseFileMap(map); + return NS_ERROR_FAILURE; + } + + RefPtr<nsZipHandle> handle = new nsZipHandle(); + if (!handle) { + PR_MemUnmap(buf, (uint32_t)size); + PR_CloseFileMap(map); + return NS_ERROR_OUT_OF_MEMORY; + } + +#if defined(XP_WIN) + if (aFd) { + *aFd = fd.forget(); + } +#else + handle->mNSPRFileDesc = fd.forget(); +#endif + handle->mFile.Init(file); + handle->mTotalLen = (uint32_t)size; + handle->mFileStart = buf; + rv = handle->findDataStart(); + if (NS_FAILED(rv)) { + PR_MemUnmap(buf, (uint32_t)size); + handle->mFileStart = nullptr; + PR_CloseFileMap(map); + return rv; + } + handle->mMap = map; + handle.forget(ret); + return NS_OK; +} + +nsresult nsZipHandle::Init(nsZipArchive* zip, const char* entry, + nsZipHandle** ret) { + RefPtr<nsZipHandle> handle = new nsZipHandle(); + if (!handle) return NS_ERROR_OUT_OF_MEMORY; + + LOG(("ZipHandle::Init entry %s", entry)); + handle->mBuf = MakeUnique<nsZipItemPtr<uint8_t>>(zip, entry); + if (!handle->mBuf) return NS_ERROR_OUT_OF_MEMORY; + + if (!handle->mBuf->Buffer()) return NS_ERROR_UNEXPECTED; + + handle->mMap = nullptr; + handle->mFile.Init(zip, entry); + handle->mTotalLen = handle->mBuf->Length(); + handle->mFileStart = handle->mBuf->Buffer(); + nsresult rv = handle->findDataStart(); + if (NS_FAILED(rv)) { + return rv; + } + handle.forget(ret); + return NS_OK; +} + +// This function finds the start of the ZIP data. If the file is a regular ZIP, +// this is just the start of the file. If the file is a CRX file, the start of +// the data is after the CRX header. +// CRX header reference: (CRX version 2) +// Header requires little-endian byte ordering with 4-byte alignment. +// 32 bits : magicNumber - Defined as a |char m[] = "Cr24"|. +// Equivilant to |uint32_t m = 0x34327243|. +// 32 bits : version - Unsigned integer representing the CRX file +// format version. Currently equal to 2. +// 32 bits : pubKeyLength - Unsigned integer representing the length +// of the public key in bytes. +// 32 bits : sigLength - Unsigned integer representing the length +// of the signature in bytes. +// pubKeyLength : publicKey - Contents of the author's public key. +// sigLength : signature - Signature of the ZIP content. +// Signature is created using the RSA +// algorithm with the SHA-1 hash function. +nsresult nsZipHandle::findDataStart() { + // In the CRX header, integers are 32 bits. Our pointer to the file is of + // type |uint8_t|, which is guaranteed to be 8 bits. + const uint32_t CRXIntSize = 4; + + MMAP_FAULT_HANDLER_BEGIN_HANDLE(this) + if (mTotalLen > CRXIntSize * 4 && xtolong(mFileStart) == kCRXMagic) { + const uint8_t* headerData = mFileStart; + headerData += CRXIntSize * 2; // Skip magic number and version number + uint32_t pubKeyLength = xtolong(headerData); + headerData += CRXIntSize; + uint32_t sigLength = xtolong(headerData); + uint32_t headerSize = CRXIntSize * 4 + pubKeyLength + sigLength; + if (mTotalLen > headerSize) { + mLen = mTotalLen - headerSize; + mFileData = mFileStart + headerSize; + return NS_OK; + } + return NS_ERROR_FILE_CORRUPTED; + } + mLen = mTotalLen; + mFileData = mFileStart; + MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE) + return NS_OK; +} + +int64_t nsZipHandle::SizeOfMapping() { return mTotalLen; } + +nsresult nsZipHandle::GetNSPRFileDesc(PRFileDesc** aNSPRFileDesc) { + if (!aNSPRFileDesc) { + return NS_ERROR_ILLEGAL_VALUE; + } + + *aNSPRFileDesc = mNSPRFileDesc; + if (!mNSPRFileDesc) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +nsZipHandle::~nsZipHandle() { + if (mMap) { + PR_MemUnmap((void*)mFileStart, mTotalLen); + PR_CloseFileMap(mMap); + } + mFileStart = nullptr; + mFileData = nullptr; + mMap = nullptr; + mBuf = nullptr; +} + +//*********************************************************** +// nsZipArchive -- public methods +//*********************************************************** + +//--------------------------------------------- +// nsZipArchive::OpenArchive +//--------------------------------------------- +/* static */ +already_AddRefed<nsZipArchive> nsZipArchive::OpenArchive( + nsZipHandle* aZipHandle, PRFileDesc* aFd) { + nsresult rv; + RefPtr<nsZipArchive> self(new nsZipArchive(aZipHandle, aFd, rv)); + LOG(("ZipHandle::OpenArchive[%p]", self.get())); + if (NS_FAILED(rv)) { + self = nullptr; + } + return self.forget(); +} + +/* static */ +already_AddRefed<nsZipArchive> nsZipArchive::OpenArchive(nsIFile* aFile) { + RefPtr<nsZipHandle> handle; +#if defined(XP_WIN) + mozilla::AutoFDClose fd; + nsresult rv = nsZipHandle::Init(aFile, getter_AddRefs(handle), &fd.rwget()); +#else + nsresult rv = nsZipHandle::Init(aFile, getter_AddRefs(handle)); +#endif + if (NS_FAILED(rv)) return nullptr; + +#if defined(XP_WIN) + return OpenArchive(handle, fd.get()); +#else + return OpenArchive(handle); +#endif +} + +//--------------------------------------------- +// nsZipArchive::Test +//--------------------------------------------- +nsresult nsZipArchive::Test(const char* aEntryName) { + nsZipItem* currItem; + + if (aEntryName) // only test specified item + { + currItem = GetItem(aEntryName); + if (!currItem) return NS_ERROR_FILE_NOT_FOUND; + //-- don't test (synthetic) directory items + if (currItem->IsDirectory()) return NS_OK; + return ExtractFile(currItem, 0, 0); + } + + // test all items in archive + for (auto* item : mFiles) { + for (currItem = item; currItem; currItem = currItem->next) { + //-- don't test (synthetic) directory items + if (currItem->IsDirectory()) continue; + nsresult rv = ExtractFile(currItem, 0, 0); + if (rv != NS_OK) return rv; + } + } + + return NS_OK; +} + +//--------------------------------------------- +// nsZipArchive::GetItem +//--------------------------------------------- +nsZipItem* nsZipArchive::GetItem(const char* aEntryName) { + MutexAutoLock lock(mLock); + + LOG(("ZipHandle::GetItem[%p] %s", this, aEntryName)); + if (aEntryName) { + uint32_t len = strlen(aEntryName); + //-- If the request is for a directory, make sure that synthetic entries + //-- are created for the directories without their own entry. + if (!mBuiltSynthetics) { + if ((len > 0) && (aEntryName[len - 1] == '/')) { + if (BuildSynthetics() != NS_OK) return 0; + } + } + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + nsZipItem* item = mFiles[HashName(aEntryName, len)]; + while (item) { + if ((len == item->nameLength) && + (!memcmp(aEntryName, item->Name(), len))) { + // Successful GetItem() is a good indicator that the file is about to be + // read + if (mUseZipLog && mURI.Length()) { + zipLog.Write(mURI, aEntryName); + } + return item; //-- found it + } + item = item->next; + } + MMAP_FAULT_HANDLER_CATCH(nullptr) + } + return nullptr; +} + +//--------------------------------------------- +// nsZipArchive::ExtractFile +// This extracts the item to the filehandle provided. +// If 'aFd' is null, it only tests the extraction. +// On extraction error(s) it removes the file. +//--------------------------------------------- +nsresult nsZipArchive::ExtractFile(nsZipItem* item, nsIFile* outFile, + PRFileDesc* aFd) { + MutexAutoLock lock(mLock); + LOG(("ZipHandle::ExtractFile[%p]", this)); + if (!item) return NS_ERROR_ILLEGAL_VALUE; + if (!mFd) return NS_ERROR_FAILURE; + + // Directory extraction is handled in nsJAR::Extract, + // so the item to be extracted should never be a directory + MOZ_ASSERT(!item->IsDirectory()); + + Bytef outbuf[ZIP_BUFLEN]; + + nsZipCursor cursor(item, this, outbuf, ZIP_BUFLEN, true); + + nsresult rv = NS_OK; + + while (true) { + uint32_t count = 0; + uint8_t* buf = cursor.Read(&count); + if (!buf) { + rv = NS_ERROR_FILE_CORRUPTED; + break; + } + if (count == 0) { + break; + } + + if (aFd && PR_Write(aFd, buf, count) < (READTYPE)count) { + rv = NS_ERROR_FILE_NO_DEVICE_SPACE; + break; + } + } + + //-- delete the file on errors + if (aFd) { + PR_Close(aFd); + if (NS_FAILED(rv) && outFile) { + outFile->Remove(false); + } + } + + return rv; +} + +//--------------------------------------------- +// nsZipArchive::FindInit +//--------------------------------------------- +nsresult nsZipArchive::FindInit(const char* aPattern, nsZipFind** aFind) { + if (!aFind) return NS_ERROR_ILLEGAL_VALUE; + + MutexAutoLock lock(mLock); + + LOG(("ZipHandle::FindInit[%p]", this)); + // null out param in case an error happens + *aFind = nullptr; + + bool regExp = false; + char* pattern = 0; + + // Create synthetic directory entries on demand + nsresult rv = BuildSynthetics(); + if (rv != NS_OK) return rv; + + // validate the pattern + if (aPattern) { + switch (NS_WildCardValid((char*)aPattern)) { + case INVALID_SXP: + return NS_ERROR_ILLEGAL_VALUE; + + case NON_SXP: + regExp = false; + break; + + case VALID_SXP: + regExp = true; + break; + + default: + // undocumented return value from RegExpValid! + MOZ_ASSERT(false); + return NS_ERROR_ILLEGAL_VALUE; + } + + pattern = strdup(aPattern); + if (!pattern) return NS_ERROR_OUT_OF_MEMORY; + } + + *aFind = new nsZipFind(this, pattern, regExp); + if (!*aFind) { + free(pattern); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +//--------------------------------------------- +// nsZipFind::FindNext +//--------------------------------------------- +nsresult nsZipFind::FindNext(const char** aResult, uint16_t* aNameLen) { + if (!mArchive || !aResult || !aNameLen) return NS_ERROR_ILLEGAL_VALUE; + + MutexAutoLock lock(mArchive->mLock); + *aResult = 0; + *aNameLen = 0; + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mArchive->GetFD()) + // we start from last match, look for next + while (mSlot < ZIP_TABSIZE) { + // move to next in current chain, or move to new slot + mItem = mItem ? mItem->next : mArchive->mFiles[mSlot]; + + bool found = false; + if (!mItem) + ++mSlot; // no more in this chain, move to next slot + else if (!mPattern) + found = true; // always match + else if (mRegExp) { + char buf[kMaxNameLength + 1]; + memcpy(buf, mItem->Name(), mItem->nameLength); + buf[mItem->nameLength] = '\0'; + found = (NS_WildCardMatch(buf, mPattern, false) == MATCH); + } else + found = ((mItem->nameLength == strlen(mPattern)) && + (memcmp(mItem->Name(), mPattern, mItem->nameLength) == 0)); + if (found) { + // Need also to return the name length, as it is NOT zero-terminatdd... + *aResult = mItem->Name(); + *aNameLen = mItem->nameLength; + LOG(("ZipHandle::FindNext[%p] %s", this, *aResult)); + return NS_OK; + } + } + MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE) + LOG(("ZipHandle::FindNext[%p] not found %s", this, mPattern)); + return NS_ERROR_FILE_NOT_FOUND; +} + +//*********************************************************** +// nsZipArchive -- private implementation +//*********************************************************** + +//--------------------------------------------- +// nsZipArchive::CreateZipItem +//--------------------------------------------- +nsZipItem* nsZipArchive::CreateZipItem() { + // Arena allocate the nsZipItem + return (nsZipItem*)mArena.Allocate(sizeof(nsZipItem), mozilla::fallible); +} + +//--------------------------------------------- +// nsZipArchive::BuildFileList +//--------------------------------------------- +nsresult nsZipArchive::BuildFileList(PRFileDesc* aFd) + MOZ_NO_THREAD_SAFETY_ANALYSIS { + // We're only called from the constructor, but need to call + // CreateZipItem(), which touches locked data, and modify mFiles. Turn + // off thread-safety, which normally doesn't apply for constructors + // anyways + + // Get archive size using end pos + const uint8_t* buf; + const uint8_t* startp = mFd->mFileData; + const uint8_t* endp = startp + mFd->mLen; + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + uint32_t centralOffset = 4; + LOG(("ZipHandle::BuildFileList[%p]", this)); + // Only perform readahead in the parent process. Children processes + // don't need readahead when the file has already been readahead by + // the parent process, and readahead only really happens for omni.ja, + // which is used in the parent process. + if (XRE_IsParentProcess() && mFd->mLen > ZIPCENTRAL_SIZE && + xtolong(startp + centralOffset) == CENTRALSIG) { + // Success means optimized jar layout from bug 559961 is in effect + uint32_t readaheadLength = xtolong(startp); + mozilla::PrefetchMemory(const_cast<uint8_t*>(startp), readaheadLength); + } else { + for (buf = endp - ZIPEND_SIZE; buf > startp; buf--) { + if (xtolong(buf) == ENDSIG) { + centralOffset = xtolong(((ZipEnd*)buf)->offset_central_dir); + break; + } + } + } + + if (!centralOffset) { + return NS_ERROR_FILE_CORRUPTED; + } + + buf = startp + centralOffset; + + // avoid overflow of startp + centralOffset. + if (buf < startp) { + return NS_ERROR_FILE_CORRUPTED; + } + + //-- Read the central directory headers + uint32_t sig = 0; + while ((buf + int32_t(sizeof(uint32_t)) > buf) && + (buf + int32_t(sizeof(uint32_t)) <= endp) && + ((sig = xtolong(buf)) == CENTRALSIG)) { + // Make sure there is enough data available. + if ((buf > endp) || (endp - buf < ZIPCENTRAL_SIZE)) { + return NS_ERROR_FILE_CORRUPTED; + } + + // Read the fixed-size data. + ZipCentral* central = (ZipCentral*)buf; + + uint16_t namelen = xtoint(central->filename_len); + uint16_t extralen = xtoint(central->extrafield_len); + uint16_t commentlen = xtoint(central->commentfield_len); + uint32_t diff = ZIPCENTRAL_SIZE + namelen + extralen + commentlen; + + // Sanity check variable sizes and refuse to deal with + // anything too big: it's likely a corrupt archive. + if (namelen < 1 || namelen > kMaxNameLength) { + return NS_ERROR_FILE_CORRUPTED; + } + if (buf >= buf + diff || // No overflow + buf >= endp - diff) { + return NS_ERROR_FILE_CORRUPTED; + } + + // Point to the next item at the top of loop + buf += diff; + + nsZipItem* item = CreateZipItem(); + if (!item) return NS_ERROR_OUT_OF_MEMORY; + + item->central = central; + item->nameLength = namelen; + item->isSynthetic = false; + + // Add item to file table +#ifdef DEBUG + nsDependentCSubstring name(item->Name(), namelen); + LOG((" %s", PromiseFlatCString(name).get())); +#endif + uint32_t hash = HashName(item->Name(), namelen); + item->next = mFiles[hash]; + mFiles[hash] = item; + + sig = 0; + } /* while reading central directory records */ + + if (sig != ENDSIG) { + return NS_ERROR_FILE_CORRUPTED; + } + + MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE) + return NS_OK; +} + +//--------------------------------------------- +// nsZipArchive::BuildSynthetics +//--------------------------------------------- +nsresult nsZipArchive::BuildSynthetics() { + mLock.AssertCurrentThreadOwns(); + + if (mBuiltSynthetics) return NS_OK; + mBuiltSynthetics = true; + + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + // Create synthetic entries for any missing directories. + // Do this when all ziptable has scanned to prevent double entries. + for (auto* item : mFiles) { + for (; item != nullptr; item = item->next) { + if (item->isSynthetic) continue; + + //-- add entries for directories in the current item's path + //-- go from end to beginning, because then we can stop trying + //-- to create diritems if we find that the diritem we want to + //-- create already exists + //-- start just before the last char so as to not add the item + //-- twice if it's a directory + uint16_t namelen = item->nameLength; + MOZ_ASSERT(namelen > 0, + "Attempt to build synthetic for zero-length entry name!"); + const char* name = item->Name(); + for (uint16_t dirlen = namelen - 1; dirlen > 0; dirlen--) { + if (name[dirlen - 1] != '/') continue; + + // The character before this is '/', so if this is also '/' then we + // have an empty path component. Skip it. + if (name[dirlen] == '/') continue; + + // Is the directory already in the file table? + uint32_t hash = HashName(item->Name(), dirlen); + bool found = false; + for (nsZipItem* zi = mFiles[hash]; zi != nullptr; zi = zi->next) { + if ((dirlen == zi->nameLength) && + (0 == memcmp(item->Name(), zi->Name(), dirlen))) { + // we've already added this dir and all its parents + found = true; + break; + } + } + // if the directory was found, break out of the directory + // creation loop now that we know all implicit directories + // are there -- otherwise, start creating the zip item + if (found) break; + + nsZipItem* diritem = CreateZipItem(); + if (!diritem) return NS_ERROR_OUT_OF_MEMORY; + + // Point to the central record of the original item for the name part. + diritem->central = item->central; + diritem->nameLength = dirlen; + diritem->isSynthetic = true; + + // add diritem to the file table + diritem->next = mFiles[hash]; + mFiles[hash] = diritem; + } /* end processing of dirs in item's name */ + } + } + MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE) + return NS_OK; +} + +//--------------------------------------------- +// nsZipArchive::GetFD +//--------------------------------------------- +nsZipHandle* nsZipArchive::GetFD() const { return mFd.get(); } + +//--------------------------------------------- +// nsZipArchive::GetDataOffset +// Returns 0 on an error; 0 is not a valid result for any success case +//--------------------------------------------- +uint32_t nsZipArchive::GetDataOffset(nsZipItem* aItem) { + MOZ_ASSERT(aItem); + MOZ_DIAGNOSTIC_ASSERT(mFd); + + uint32_t offset; + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + //-- read local header to get variable length values and calculate + //-- the real data offset + uint32_t len = mFd->mLen; + MOZ_DIAGNOSTIC_ASSERT(len <= UINT32_MAX, "mLen > 2GB"); + const uint8_t* data = mFd->mFileData; + offset = aItem->LocalOffset(); + if (len < ZIPLOCAL_SIZE || offset > len - ZIPLOCAL_SIZE) { + return 0; + } + // Check there's enough space for the signature + if (offset > mFd->mLen) { + NS_WARNING("Corrupt local offset in JAR file"); + return 0; + } + + // -- check signature before using the structure, in case the zip file is + // corrupt + ZipLocal* Local = (ZipLocal*)(data + offset); + if ((xtolong(Local->signature) != LOCALSIG)) return 0; + + //-- NOTE: extralen is different in central header and local header + //-- for archives created using the Unix "zip" utility. To set + //-- the offset accurately we need the _local_ extralen. + offset += ZIPLOCAL_SIZE + xtoint(Local->filename_len) + + xtoint(Local->extrafield_len); + // Check data points inside the file. + if (offset > mFd->mLen) { + NS_WARNING("Corrupt data offset in JAR file"); + return 0; + } + + MMAP_FAULT_HANDLER_CATCH(0) + // can't be 0 + return offset; +} + +//--------------------------------------------- +// nsZipArchive::GetData +//--------------------------------------------- +const uint8_t* nsZipArchive::GetData(nsZipItem* aItem) { + MOZ_DIAGNOSTIC_ASSERT(aItem); + if (!aItem) { + return nullptr; + } + uint32_t offset = GetDataOffset(aItem); + + MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd) + // -- check if there is enough source data in the file + if (!offset || mFd->mLen < aItem->Size() || + offset > mFd->mLen - aItem->Size() || + (aItem->Compression() == STORED && aItem->Size() != aItem->RealSize())) { + return nullptr; + } + MMAP_FAULT_HANDLER_CATCH(nullptr) + + return mFd->mFileData + offset; +} + +//--------------------------------------------- +// nsZipArchive::SizeOfMapping +//--------------------------------------------- +int64_t nsZipArchive::SizeOfMapping() { return mFd ? mFd->SizeOfMapping() : 0; } + +//------------------------------------------ +// nsZipArchive constructor and destructor +//------------------------------------------ + +nsZipArchive::nsZipArchive(nsZipHandle* aZipHandle, PRFileDesc* aFd, + nsresult& aRv) + : mRefCnt(0), mFd(aZipHandle), mUseZipLog(false), mBuiltSynthetics(false) { + // initialize the table to nullptr + memset(mFiles, 0, sizeof(mFiles)); + MOZ_DIAGNOSTIC_ASSERT(aZipHandle); + + //-- get table of contents for archive + aRv = BuildFileList(aFd); + if (NS_FAILED(aRv)) { + return; // whomever created us must destroy us in this case + } + if (aZipHandle->mFile && XRE_IsParentProcess()) { + static char* env = PR_GetEnv("MOZ_JAR_LOG_FILE"); + if (env) { + mUseZipLog = true; + + zipLog.Init(env); + // We only log accesses in jar/zip archives within the NS_GRE_DIR + // and/or the APK on Android. For the former, we log the archive path + // relative to NS_GRE_DIR, and for the latter, the nested-archive + // path within the APK. This makes the path match the path of the + // archives relative to the packaged dist/$APP_NAME directory in a + // build. + if (aZipHandle->mFile.IsZip()) { + // Nested archive, likely omni.ja in APK. + aZipHandle->mFile.GetPath(mURI); + } else if (nsDirectoryService::gService) { + // We can reach here through the initialization of Omnijar from + // XRE_InitCommandLine, which happens before the directory service + // is initialized. When that happens, it means the opened archive is + // the APK, and we don't care to log that one, so we just skip + // when the directory service is not initialized. + nsCOMPtr<nsIFile> dir = aZipHandle->mFile.GetBaseFile(); + nsCOMPtr<nsIFile> gre_dir; + nsAutoCString path; + if (NS_SUCCEEDED(nsDirectoryService::gService->Get( + NS_GRE_DIR, NS_GET_IID(nsIFile), getter_AddRefs(gre_dir)))) { + nsAutoCString leaf; + nsCOMPtr<nsIFile> parent; + while (NS_SUCCEEDED(dir->GetNativeLeafName(leaf)) && + NS_SUCCEEDED(dir->GetParent(getter_AddRefs(parent)))) { + if (!parent) { + break; + } + dir = parent; + if (path.Length()) { + path.Insert('/', 0); + } + path.Insert(leaf, 0); + bool equals; + if (NS_SUCCEEDED(dir->Equals(gre_dir, &equals)) && equals) { + mURI.Assign(path); + break; + } + } + } + } + } + } +} + +NS_IMPL_ADDREF(nsZipArchive) +NS_IMPL_RELEASE(nsZipArchive) + +nsZipArchive::~nsZipArchive() { + LOG(("Closing nsZipArchive[%p]", this)); + if (mUseZipLog) { + zipLog.Release(); + } +} + +//------------------------------------------ +// nsZipFind constructor and destructor +//------------------------------------------ + +nsZipFind::nsZipFind(nsZipArchive* aZip, char* aPattern, bool aRegExp) + : mArchive(aZip), + mPattern(aPattern), + mItem(nullptr), + mSlot(0), + mRegExp(aRegExp) { + MOZ_COUNT_CTOR(nsZipFind); +} + +nsZipFind::~nsZipFind() { + free(mPattern); + + MOZ_COUNT_DTOR(nsZipFind); +} + +//------------------------------------------ +// helper functions +//------------------------------------------ + +/* + * HashName + * + * returns a hash key for the entry name + */ +MOZ_NO_SANITIZE_UNSIGNED_OVERFLOW +static uint32_t HashName(const char* aName, uint16_t len) { + MOZ_ASSERT(aName != 0); + + const uint8_t* p = (const uint8_t*)aName; + const uint8_t* endp = p + len; + uint32_t val = 0; + while (p != endp) { + val = val * 37 + *p++; + } + + return (val % ZIP_TABSIZE); +} + +/* + * x t o i n t + * + * Converts a two byte ugly endianed integer + * to our platform's integer. + */ +static uint16_t xtoint(const uint8_t* ii) { + return (uint16_t)((ii[0]) | (ii[1] << 8)); +} + +/* + * x t o l o n g + * + * Converts a four byte ugly endianed integer + * to our platform's integer. + */ +static uint32_t xtolong(const uint8_t* ll) { + return (uint32_t)((ll[0] << 0) | (ll[1] << 8) | (ll[2] << 16) | + (ll[3] << 24)); +} + +/* + * GetModTime + * + * returns last modification time in microseconds + */ +static PRTime GetModTime(uint16_t aDate, uint16_t aTime) { + // Note that on DST shift we can't handle correctly the hour that is valid + // in both DST zones + PRExplodedTime time; + + time.tm_usec = 0; + + time.tm_hour = (aTime >> 11) & 0x1F; + time.tm_min = (aTime >> 5) & 0x3F; + time.tm_sec = (aTime & 0x1F) * 2; + + time.tm_year = (aDate >> 9) + 1980; + time.tm_month = ((aDate >> 5) & 0x0F) - 1; + time.tm_mday = aDate & 0x1F; + + time.tm_params.tp_gmt_offset = 0; + time.tm_params.tp_dst_offset = 0; + + PR_NormalizeTime(&time, PR_GMTParameters); + time.tm_params.tp_gmt_offset = PR_LocalTimeParameters(&time).tp_gmt_offset; + PR_NormalizeTime(&time, PR_GMTParameters); + time.tm_params.tp_dst_offset = PR_LocalTimeParameters(&time).tp_dst_offset; + + return PR_ImplodeTime(&time); +} + +nsZipItem::nsZipItem() + : next(nullptr), central(nullptr), nameLength(0), isSynthetic(false) {} + +uint32_t nsZipItem::LocalOffset() { return xtolong(central->localhdr_offset); } + +uint32_t nsZipItem::Size() { return isSynthetic ? 0 : xtolong(central->size); } + +uint32_t nsZipItem::RealSize() { + return isSynthetic ? 0 : xtolong(central->orglen); +} + +uint32_t nsZipItem::CRC32() { + return isSynthetic ? 0 : xtolong(central->crc32); +} + +uint16_t nsZipItem::Date() { + return isSynthetic ? kSyntheticDate : xtoint(central->date); +} + +uint16_t nsZipItem::Time() { + return isSynthetic ? kSyntheticTime : xtoint(central->time); +} + +uint16_t nsZipItem::Compression() { + return isSynthetic ? STORED : xtoint(central->method); +} + +bool nsZipItem::IsDirectory() { + return isSynthetic || ((nameLength > 0) && ('/' == Name()[nameLength - 1])); +} + +uint16_t nsZipItem::Mode() { + if (isSynthetic) return 0755; + return ((uint16_t)(central->external_attributes[2]) | 0x100); +} + +const uint8_t* nsZipItem::GetExtraField(uint16_t aTag, uint16_t* aBlockSize) { + if (isSynthetic) return nullptr; + + const unsigned char* buf = + ((const unsigned char*)central) + ZIPCENTRAL_SIZE + nameLength; + uint32_t buflen; + + MMAP_FAULT_HANDLER_BEGIN_BUFFER(central, ZIPCENTRAL_SIZE + nameLength) + buflen = (uint32_t)xtoint(central->extrafield_len); + MMAP_FAULT_HANDLER_CATCH(nullptr) + + uint32_t pos = 0; + uint16_t tag, blocksize; + + MMAP_FAULT_HANDLER_BEGIN_BUFFER(buf, buflen) + while (buf && (pos + 4) <= buflen) { + tag = xtoint(buf + pos); + blocksize = xtoint(buf + pos + 2); + + if (aTag == tag && (pos + 4 + blocksize) <= buflen) { + *aBlockSize = blocksize; + return buf + pos; + } + + pos += blocksize + 4; + } + MMAP_FAULT_HANDLER_CATCH(nullptr) + + return nullptr; +} + +PRTime nsZipItem::LastModTime() { + if (isSynthetic) return GetModTime(kSyntheticDate, kSyntheticTime); + + // Try to read timestamp from extra field + uint16_t blocksize; + const uint8_t* tsField = GetExtraField(EXTENDED_TIMESTAMP_FIELD, &blocksize); + if (tsField && blocksize >= 5 && tsField[4] & EXTENDED_TIMESTAMP_MODTIME) { + return (PRTime)(xtolong(tsField + 5)) * PR_USEC_PER_SEC; + } + + return GetModTime(Date(), Time()); +} + +nsZipCursor::nsZipCursor(nsZipItem* item, nsZipArchive* aZip, uint8_t* aBuf, + uint32_t aBufSize, bool doCRC) + : mItem(item), + mBuf(aBuf), + mBufSize(aBufSize), + mZs(), + mCRC(0), + mDoCRC(doCRC) { + if (mItem->Compression() == DEFLATED) { +#ifdef DEBUG + nsresult status = +#endif + gZlibInit(&mZs); + NS_ASSERTION(status == NS_OK, "Zlib failed to initialize"); + NS_ASSERTION(aBuf, "Must pass in a buffer for DEFLATED nsZipItem"); + } + + mZs.avail_in = item->Size(); + mZs.next_in = (Bytef*)aZip->GetData(item); + + if (doCRC) mCRC = crc32(0L, Z_NULL, 0); +} + +nsZipCursor::~nsZipCursor() { + if (mItem->Compression() == DEFLATED) { + inflateEnd(&mZs); + } +} + +uint8_t* nsZipCursor::ReadOrCopy(uint32_t* aBytesRead, bool aCopy) { + int zerr; + uint8_t* buf = nullptr; + bool verifyCRC = true; + + if (!mZs.next_in) return nullptr; + MMAP_FAULT_HANDLER_BEGIN_BUFFER(mZs.next_in, mZs.avail_in) + switch (mItem->Compression()) { + case STORED: + if (!aCopy) { + *aBytesRead = mZs.avail_in; + buf = mZs.next_in; + mZs.next_in += mZs.avail_in; + mZs.avail_in = 0; + } else { + *aBytesRead = mZs.avail_in > mBufSize ? mBufSize : mZs.avail_in; + memcpy(mBuf, mZs.next_in, *aBytesRead); + mZs.avail_in -= *aBytesRead; + mZs.next_in += *aBytesRead; + } + break; + case DEFLATED: + buf = mBuf; + mZs.next_out = buf; + mZs.avail_out = mBufSize; + + zerr = inflate(&mZs, Z_PARTIAL_FLUSH); + if (zerr != Z_OK && zerr != Z_STREAM_END) return nullptr; + + *aBytesRead = mZs.next_out - buf; + verifyCRC = (zerr == Z_STREAM_END); + break; + default: + return nullptr; + } + + if (mDoCRC) { + mCRC = crc32(mCRC, (const unsigned char*)buf, *aBytesRead); + if (verifyCRC && mCRC != mItem->CRC32()) return nullptr; + } + MMAP_FAULT_HANDLER_CATCH(nullptr) + return buf; +} + +nsZipItemPtr_base::nsZipItemPtr_base(nsZipArchive* aZip, const char* aEntryName, + bool doCRC) + : mReturnBuf(nullptr), mReadlen(0) { + // make sure the ziparchive hangs around + mZipHandle = aZip->GetFD(); + + nsZipItem* item = aZip->GetItem(aEntryName); + if (!item) return; + + uint32_t size = 0; + bool compressed = (item->Compression() == DEFLATED); + if (compressed) { + size = item->RealSize(); + mAutoBuf = MakeUniqueFallible<uint8_t[]>(size); + if (!mAutoBuf) { + return; + } + } + + nsZipCursor cursor(item, aZip, mAutoBuf.get(), size, doCRC); + mReturnBuf = cursor.Read(&mReadlen); + if (!mReturnBuf) { + return; + } + + if (mReadlen != item->RealSize()) { + NS_ASSERTION(mReadlen == item->RealSize(), "nsZipCursor underflow"); + mReturnBuf = nullptr; + return; + } +} diff --git a/modules/libjar/nsZipArchive.h b/modules/libjar/nsZipArchive.h new file mode 100644 index 0000000000..e38445ea87 --- /dev/null +++ b/modules/libjar/nsZipArchive.h @@ -0,0 +1,401 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsZipArchive_h_ +#define nsZipArchive_h_ + +#include "mozilla/Attributes.h" + +#define ZIP_TABSIZE 256 +#define ZIP_BUFLEN \ + (4 * 1024) /* Used as output buffer when deflating items to a file */ + +#include "zlib.h" +#include "zipstruct.h" +#include "nsIFile.h" +#include "nsISupportsImpl.h" // For mozilla::ThreadSafeAutoRefCnt +#include "mozilla/ArenaAllocator.h" +#include "mozilla/FileUtils.h" +#include "mozilla/FileLocation.h" +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" + +class nsZipFind; +struct PRFileDesc; + +/** + * This file defines some of the basic structures used by libjar to + * read Zip files. It makes use of zlib in order to do the decompression. + * + * A few notes on the classes/structs: + * nsZipArchive represents a single Zip file, and maintains an index + * of all the items in the file. + * nsZipItem represents a single item (file) in the Zip archive. + * nsZipFind represents the metadata involved in doing a search, + * and current state of the iteration of found objects. + * 'MT''safe' reading from the zipfile is performed through JARInputStream, + * which maintains its own file descriptor, allowing for multiple reads + * concurrently from the same zip file. + * + * nsZipArchives are accessed from multiple threads. + */ + +/** + * nsZipItem -- a helper struct for nsZipArchive + * + * each nsZipItem represents one file in the archive and all the + * information needed to manipulate it. + */ +class nsZipItem final { + public: + nsZipItem(); + + const char* Name() { return ((const char*)central) + ZIPCENTRAL_SIZE; } + + uint32_t LocalOffset(); + uint32_t Size(); + uint32_t RealSize(); + uint32_t CRC32(); + uint16_t Date(); + uint16_t Time(); + uint16_t Compression(); + bool IsDirectory(); + uint16_t Mode(); + const uint8_t* GetExtraField(uint16_t aTag, uint16_t* aBlockSize); + PRTime LastModTime(); + + nsZipItem* next; + const ZipCentral* central; + uint16_t nameLength; + bool isSynthetic; +}; + +class nsZipHandle; + +/** + * nsZipArchive -- a class for reading the PKZIP file format. + * + */ +class nsZipArchive final { + friend class nsZipFind; + + /** destructing the object closes the archive */ + ~nsZipArchive(); + + public: + static const char* sFileCorruptedReason; + + /** + * OpenArchive + * + * @param aZipHandle The nsZipHandle used to access the zip + * @param aFd Optional PRFileDesc for Windows readahead optimization + * @return status code + */ + static already_AddRefed<nsZipArchive> OpenArchive(nsZipHandle* aZipHandle, + PRFileDesc* aFd = nullptr); + + /** + * OpenArchive + * + * Convenience function that generates nsZipHandle + * + * @param aFile The file used to access the zip + * @return status code + */ + static already_AddRefed<nsZipArchive> OpenArchive(nsIFile* aFile); + + /** + * Test the integrity of items in this archive by running + * a CRC check after extracting each item into a memory + * buffer. If an entry name is supplied only the + * specified item is tested. Else, if null is supplied + * then all the items in the archive are tested. + * + * @return status code + */ + nsresult Test(const char* aEntryName); + + /** + * GetItem + * @param aEntryName Name of file in the archive + * @return pointer to nsZipItem + */ + nsZipItem* GetItem(const char* aEntryName); + + /** + * ExtractFile + * + * @param zipEntry Name of file in archive to extract + * @param outFD Filedescriptor to write contents to + * @param outname Name of file to write to + * @return status code + */ + nsresult ExtractFile(nsZipItem* zipEntry, nsIFile* outFile, + PRFileDesc* outFD); + + /** + * FindInit + * + * Initializes a search for files in the archive. FindNext() returns + * the actual matches. The nsZipFind must be deleted when you're done + * + * @param aPattern a string or RegExp pattern to search for + * (may be nullptr to find all files in archive) + * @param aFind a pointer to a pointer to a structure used + * in FindNext. In the case of an error this + * will be set to nullptr. + * @return status code + */ + nsresult FindInit(const char* aPattern, nsZipFind** aFind); + + /* + * Gets an undependent handle to the mapped file. + */ + nsZipHandle* GetFD() const; + + /** + * Gets the data offset. + * @param aItem Pointer to nsZipItem + * returns 0 on failure. + */ + uint32_t GetDataOffset(nsZipItem* aItem); + + /** + * Get pointer to the data of the item. + * @param aItem Pointer to nsZipItem + * reutrns null when zip file is corrupt. + */ + const uint8_t* GetData(nsZipItem* aItem); + + /** + * Gets the amount of memory taken up by the archive's mapping. + * @return the size + */ + int64_t SizeOfMapping(); + + /* + * Refcounting + */ + NS_METHOD_(MozExternalRefCountType) AddRef(void); + NS_METHOD_(MozExternalRefCountType) Release(void); + + private: + nsZipArchive(nsZipHandle* aZipHandle, PRFileDesc* aFd, nsresult& aRv); + + //--- private members --- + mozilla::ThreadSafeAutoRefCnt mRefCnt; /* ref count */ + NS_DECL_OWNINGTHREAD + + // These fields are all effectively const after the constructor + // file handle + const RefPtr<nsZipHandle> mFd; + // file URI, for logging + nsCString mURI; + // Is true if we use zipLog to log accesses in jar/zip archives. This helper + // variable avoids grabbing zipLog's lock when not necessary. + // Effectively const after constructor + bool mUseZipLog; + + mozilla::Mutex mLock{"nsZipArchive"}; + // all of the following members are guarded by mLock: + nsZipItem* mFiles[ZIP_TABSIZE] MOZ_GUARDED_BY(mLock); + mozilla::ArenaAllocator<1024, sizeof(void*)> mArena MOZ_GUARDED_BY(mLock); + // Whether we synthesized the directory entries + bool mBuiltSynthetics MOZ_GUARDED_BY(mLock); + + private: + //--- private methods --- + nsZipItem* CreateZipItem() MOZ_REQUIRES(mLock); + nsresult BuildFileList(PRFileDesc* aFd = nullptr); + nsresult BuildSynthetics(); + + nsZipArchive& operator=(const nsZipArchive& rhs) = delete; + nsZipArchive(const nsZipArchive& rhs) = delete; +}; + +/** + * nsZipFind + * + * a helper class for nsZipArchive, representing a search + */ +class nsZipFind final { + public: + nsZipFind(nsZipArchive* aZip, char* aPattern, bool regExp); + ~nsZipFind(); + + nsresult FindNext(const char** aResult, uint16_t* aNameLen); + + private: + RefPtr<nsZipArchive> mArchive; + char* mPattern; + nsZipItem* mItem; + uint16_t mSlot; + bool mRegExp; + + nsZipFind& operator=(const nsZipFind& rhs) = delete; + nsZipFind(const nsZipFind& rhs) = delete; +}; + +/** + * nsZipCursor -- a low-level class for reading the individual items in a zip. + */ +class nsZipCursor final { + public: + /** + * Initializes the cursor + * + * @param aItem Item of interest + * @param aZip Archive + * @param aBuf Buffer used for decompression. + * This determines the maximum Read() size in the + * compressed case. + * @param aBufSize Buffer size + * @param doCRC When set to true Read() will check crc + */ + nsZipCursor(nsZipItem* aItem, nsZipArchive* aZip, uint8_t* aBuf = nullptr, + uint32_t aBufSize = 0, bool doCRC = false); + + ~nsZipCursor(); + + /** + * Performs reads. In the compressed case it uses aBuf(passed in constructor), + * for stored files it returns a zero-copy buffer. + * + * @param aBytesRead Outparam for number of bytes read. + * @return data read or nullptr if item is corrupted. + */ + uint8_t* Read(uint32_t* aBytesRead) { return ReadOrCopy(aBytesRead, false); } + + /** + * Performs a copy. It always uses aBuf(passed in constructor). + * + * @param aBytesRead Outparam for number of bytes read. + * @return data read or nullptr if item is corrupted. + */ + uint8_t* Copy(uint32_t* aBytesRead) { return ReadOrCopy(aBytesRead, true); } + + private: + /* Actual implementation for both Read and Copy above */ + uint8_t* ReadOrCopy(uint32_t* aBytesRead, bool aCopy); + + nsZipItem* mItem; + uint8_t* mBuf; + uint32_t mBufSize; + z_stream mZs; + uint32_t mCRC; + bool mDoCRC; +}; + +/** + * nsZipItemPtr - a RAII convenience class for reading the individual items in a + * zip. It reads whole files and does zero-copy IO for stored files. A buffer is + * allocated for decompression. Do not use when the file may be very large. + */ +class nsZipItemPtr_base { + public: + /** + * Initializes the reader + * + * @param aZip Archive + * @param aEntryName Archive membername + * @param doCRC When set to true Read() will check crc + */ + nsZipItemPtr_base(nsZipArchive* aZip, const char* aEntryName, bool doCRC); + + uint32_t Length() const { return mReadlen; } + + protected: + RefPtr<nsZipHandle> mZipHandle; + mozilla::UniquePtr<uint8_t[]> mAutoBuf; + uint8_t* mReturnBuf; + uint32_t mReadlen; +}; + +template <class T> +class nsZipItemPtr final : public nsZipItemPtr_base { + static_assert(sizeof(T) == sizeof(char), + "This class cannot be used with larger T without re-examining" + " a number of assumptions."); + + public: + nsZipItemPtr(nsZipArchive* aZip, const char* aEntryName, bool doCRC = false) + : nsZipItemPtr_base(aZip, aEntryName, doCRC) {} + /** + * @return buffer containing the whole zip member or nullptr on error. + * The returned buffer is owned by nsZipItemReader. + */ + const T* Buffer() const { return (const T*)mReturnBuf; } + + operator const T*() const { return Buffer(); } + + /** + * Relinquish ownership of zip member if compressed. + * Copy member into a new buffer if uncompressed. + * @return a buffer with whole zip member. It is caller's responsibility to + * free() it. + */ + mozilla::UniquePtr<T[]> Forget() { + if (!mReturnBuf) return nullptr; + // In uncompressed mmap case, give up buffer + if (mAutoBuf.get() == mReturnBuf) { + mReturnBuf = nullptr; + return mozilla::UniquePtr<T[]>(reinterpret_cast<T*>(mAutoBuf.release())); + } + auto ret = mozilla::MakeUnique<T[]>(Length()); + memcpy(ret.get(), mReturnBuf, Length()); + mReturnBuf = nullptr; + return ret; + } +}; + +class nsZipHandle final { + friend class nsZipArchive; + friend class nsZipFind; + friend class mozilla::FileLocation; + friend class nsJARInputStream; +#if defined(XP_UNIX) && !defined(XP_DARWIN) + friend class MmapAccessScope; +#endif + + public: + static nsresult Init(nsIFile* file, nsZipHandle** ret, + PRFileDesc** aFd = nullptr); + static nsresult Init(nsZipArchive* zip, const char* entry, nsZipHandle** ret); + + NS_METHOD_(MozExternalRefCountType) AddRef(void); + NS_METHOD_(MozExternalRefCountType) Release(void); + + int64_t SizeOfMapping(); + + nsresult GetNSPRFileDesc(PRFileDesc** aNSPRFileDesc); + + protected: + const uint8_t* mFileData; /* pointer to zip data */ + uint32_t mLen; /* length of zip data */ + mozilla::FileLocation mFile; /* source file if any, for logging */ + + private: + nsZipHandle(); + ~nsZipHandle(); + + nsresult findDataStart(); + + PRFileMap* mMap; /* nspr datastructure for mmap */ + mozilla::AutoFDClose mNSPRFileDesc; + mozilla::UniquePtr<nsZipItemPtr<uint8_t> > mBuf; + mozilla::ThreadSafeAutoRefCnt mRefCnt; /* ref count */ + NS_DECL_OWNINGTHREAD + + const uint8_t* mFileStart; /* pointer to mmaped file */ + uint32_t mTotalLen; /* total length of the mmaped file */ + + /* Magic number for CRX type expressed in Big Endian since it is a literal */ + static const uint32_t kCRXMagic = 0x34327243; +}; + +nsresult gZlibInit(z_stream* zs); + +#endif /* nsZipArchive_h_ */ diff --git a/modules/libjar/test/mochitest/bug1173171.zip b/modules/libjar/test/mochitest/bug1173171.zip Binary files differnew file mode 100644 index 0000000000..48ba268ddf --- /dev/null +++ b/modules/libjar/test/mochitest/bug1173171.zip diff --git a/modules/libjar/test/mochitest/bug1173171.zip^headers^ b/modules/libjar/test/mochitest/bug1173171.zip^headers^ new file mode 100644 index 0000000000..28b8aa0a57 --- /dev/null +++ b/modules/libjar/test/mochitest/bug1173171.zip^headers^ @@ -0,0 +1 @@ +Content-Type: application/java-archive diff --git a/modules/libjar/test/mochitest/mochitest.ini b/modules/libjar/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..71c315b91b --- /dev/null +++ b/modules/libjar/test/mochitest/mochitest.ini @@ -0,0 +1,5 @@ + +[test_bug1173171.html] +support-files = + bug1173171.zip + bug1173171.zip^headers^
\ No newline at end of file diff --git a/modules/libjar/test/mochitest/test_bug1173171.html b/modules/libjar/test/mochitest/test_bug1173171.html new file mode 100644 index 0000000000..6f18513f86 --- /dev/null +++ b/modules/libjar/test/mochitest/test_bug1173171.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1173171 +--> +<head> + <title>Test for Bug 1173171</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe id="testFrame"></iframe> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1173171 **/ + +// __xhr(method, url, responseType__. +// A simple async XMLHttpRequest call. +// Returns a promise with the response. +let xhr = function(method, url, responseType) { + return new Promise(function(resolve, reject) { + let xhrInstance = new XMLHttpRequest(); + xhrInstance.open(method, url, true); + xhrInstance.onload = function() { + resolve(xhrInstance.response); + }; + xhrInstance.onerror = function() { + resolve(null); + }; + xhrInstance.responseType = responseType; + xhrInstance.send(); + }); +}; + +let jarURL = "jar:http://mochi.test:8888/tests/modules/libjar/test/mochitest/bug403331.zip!/test.html"; + +// Test behavior when blocking is deactivated and activated. +add_task(async function() { + let response = await xhr("GET", jarURL, "document"); + is(response, null, "Remote jars should be blocked."); +}); + +</script> +</pre> + +</body> +</html> diff --git a/modules/libjar/test/unit/data/empty b/modules/libjar/test/unit/data/empty new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/modules/libjar/test/unit/data/empty diff --git a/modules/libjar/test/unit/data/test_1801102.jar b/modules/libjar/test/unit/data/test_1801102.jar Binary files differnew file mode 100644 index 0000000000..c3cd11112d --- /dev/null +++ b/modules/libjar/test/unit/data/test_1801102.jar diff --git a/modules/libjar/test/unit/data/test_bug333423.zip b/modules/libjar/test/unit/data/test_bug333423.zip Binary files differnew file mode 100644 index 0000000000..42662a4085 --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug333423.zip diff --git a/modules/libjar/test/unit/data/test_bug336691.zip b/modules/libjar/test/unit/data/test_bug336691.zip Binary files differnew file mode 100644 index 0000000000..16bd2d6f2c --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug336691.zip diff --git a/modules/libjar/test/unit/data/test_bug370103.jar b/modules/libjar/test/unit/data/test_bug370103.jar Binary files differnew file mode 100644 index 0000000000..7723568bad --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug370103.jar diff --git a/modules/libjar/test/unit/data/test_bug379841.zip b/modules/libjar/test/unit/data/test_bug379841.zip Binary files differnew file mode 100644 index 0000000000..f3c5205197 --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug379841.zip diff --git a/modules/libjar/test/unit/data/test_bug589292.zip b/modules/libjar/test/unit/data/test_bug589292.zip Binary files differnew file mode 100644 index 0000000000..b7b9e65b47 --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug589292.zip diff --git a/modules/libjar/test/unit/data/test_bug597702.zip b/modules/libjar/test/unit/data/test_bug597702.zip Binary files differnew file mode 100644 index 0000000000..55ce8fff42 --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug597702.zip diff --git a/modules/libjar/test/unit/data/test_bug637286.zip b/modules/libjar/test/unit/data/test_bug637286.zip Binary files differnew file mode 100644 index 0000000000..5dc8e747ea --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug637286.zip diff --git a/modules/libjar/test/unit/data/test_bug658093.zip b/modules/libjar/test/unit/data/test_bug658093.zip Binary files differnew file mode 100644 index 0000000000..d32180101a --- /dev/null +++ b/modules/libjar/test/unit/data/test_bug658093.zip diff --git a/modules/libjar/test/unit/data/test_corrupt.zip b/modules/libjar/test/unit/data/test_corrupt.zip Binary files differnew file mode 100644 index 0000000000..d7f5f42f93 --- /dev/null +++ b/modules/libjar/test/unit/data/test_corrupt.zip diff --git a/modules/libjar/test/unit/data/test_corrupt2.zip b/modules/libjar/test/unit/data/test_corrupt2.zip new file mode 100644 index 0000000000..0cfbf08886 --- /dev/null +++ b/modules/libjar/test/unit/data/test_corrupt2.zip @@ -0,0 +1 @@ +2 diff --git a/modules/libjar/test/unit/data/test_corrupt3.zip b/modules/libjar/test/unit/data/test_corrupt3.zip Binary files differnew file mode 100644 index 0000000000..86f8d76c9e --- /dev/null +++ b/modules/libjar/test/unit/data/test_corrupt3.zip diff --git a/modules/libjar/test/unit/data/test_crx_dummy.crx b/modules/libjar/test/unit/data/test_crx_dummy.crx Binary files differnew file mode 100644 index 0000000000..516e4ff807 --- /dev/null +++ b/modules/libjar/test/unit/data/test_crx_dummy.crx diff --git a/modules/libjar/test/unit/data/test_empty_file.zip b/modules/libjar/test/unit/data/test_empty_file.zip Binary files differnew file mode 100644 index 0000000000..b613b60c02 --- /dev/null +++ b/modules/libjar/test/unit/data/test_empty_file.zip diff --git a/modules/libjar/test/unit/data/test_umlaute.zip b/modules/libjar/test/unit/data/test_umlaute.zip Binary files differnew file mode 100644 index 0000000000..d147138e18 --- /dev/null +++ b/modules/libjar/test/unit/data/test_umlaute.zip diff --git a/modules/libjar/test/unit/data/uncompressed.zip b/modules/libjar/test/unit/data/uncompressed.zip Binary files differnew file mode 100644 index 0000000000..192bf15616 --- /dev/null +++ b/modules/libjar/test/unit/data/uncompressed.zip diff --git a/modules/libjar/test/unit/test_bug1328865.js b/modules/libjar/test/unit/test_bug1328865.js new file mode 100644 index 0000000000..3c973cb77c --- /dev/null +++ b/modules/libjar/test/unit/test_bug1328865.js @@ -0,0 +1,50 @@ +/* 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/. */ + +"use strict"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Check that reading non existant inner jars results in the right error + +add_task(async function () { + var file = do_get_file("data/test_bug597702.zip"); + var outerJarBase = "jar:" + Services.io.newFileURI(file).spec + "!/"; + var goodSpec = + "jar:" + outerJarBase + "inner.jar!/hello#!/ignore%20this%20part"; + var goodChannel = NetUtil.newChannel({ + uri: goodSpec, + loadUsingSystemPrincipal: true, + }); + var instr = goodChannel.open(); + + ok(!!instr, "Should be able to open channel"); +}); + +add_task(async function () { + var file = do_get_file("data/test_bug597702.zip"); + var outerJarBase = "jar:" + Services.io.newFileURI(file).spec + "!/"; + var goodSpec = + "jar:" + outerJarBase + "inner.jar!/hello?ignore%20this%20part!/"; + var goodChannel = NetUtil.newChannel({ + uri: goodSpec, + loadUsingSystemPrincipal: true, + }); + var instr = goodChannel.open(); + + ok(!!instr, "Should be able to open channel"); +}); + +add_task(async function () { + var file = do_get_file("data/test_bug597702.zip"); + var outerJarBase = "jar:" + Services.io.newFileURI(file).spec + "!/"; + var goodSpec = "jar:" + outerJarBase + "inner.jar!/hello?ignore#this!/part"; + var goodChannel = NetUtil.newChannel({ + uri: goodSpec, + loadUsingSystemPrincipal: true, + }); + var instr = goodChannel.open(); + + ok(!!instr, "Should be able to open channel"); +}); diff --git a/modules/libjar/test/unit/test_bug1550815.js b/modules/libjar/test/unit/test_bug1550815.js new file mode 100644 index 0000000000..cb6264de7a --- /dev/null +++ b/modules/libjar/test/unit/test_bug1550815.js @@ -0,0 +1,32 @@ +// Test checks SIGBUS handling on Linux. The test cannot be used to check page +// error exception on Windows because the file cannot be truncated while it's +// being used by zipreader. +function run_test() { + var file = do_get_file("data/test_bug333423.zip"); + var tmpFile = do_get_tempdir(); + + file.copyTo(tmpFile, "bug1550815.zip"); + tmpFile.append("bug1550815.zip"); + + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(tmpFile); + + // Truncate the file + var ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(tmpFile, -1, -1, 0); + ostream.close(); + + try { + zipReader.test("modules/libjar/test/Makefile.in"); + Assert.ok(false, "Should not reach here."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_NOT_FOUND); + } + + zipReader.close(); + tmpFile.remove(false); +} diff --git a/modules/libjar/test/unit/test_bug278262.js b/modules/libjar/test/unit/test_bug278262.js new file mode 100644 index 0000000000..3a5e7e951c --- /dev/null +++ b/modules/libjar/test/unit/test_bug278262.js @@ -0,0 +1,32 @@ +// Regression test for bug 278262 - JAR URIs should resolve relative URIs in the base section. + +const path = "data/test_bug333423.zip"; + +function test_relative_sub() { + var ios = Services.io; + var spec = "jar:" + ios.newFileURI(do_get_file(path)).spec + "!/"; + var base = ios.newURI(spec); + var uri = ios.newURI("../modules/libjar", null, base); + + // This is the URI we expect to see. + var expected = + "jar:" + ios.newFileURI(do_get_file(path)).spec + "!/modules/libjar"; + + Assert.equal(uri.spec, expected); +} + +function test_relative_base() { + var ios = Services.io; + var base = ios.newFileURI(do_get_file("data/empty")); + var uri = ios.newURI("jar:../" + path + "!/", null, base); + + // This is the URI we expect to see. + var expected = "jar:" + ios.newFileURI(do_get_file(path)).spec + "!/"; + + Assert.equal(uri.spec, expected); +} + +function run_test() { + test_relative_sub(); + test_relative_base(); +} diff --git a/modules/libjar/test/unit/test_bug333423.js b/modules/libjar/test/unit/test_bug333423.js new file mode 100644 index 0000000000..e2fc535020 --- /dev/null +++ b/modules/libjar/test/unit/test_bug333423.js @@ -0,0 +1,24 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Regression test for bug 333423 - crash enumerating entries of a +// closed nsIZipReader +function run_test() { + // the build script have created the zip we can test on in the current dir. + var file = do_get_file("data/test_bug333423.zip"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + zipreader.close(); + // this should error out and not crash + Assert.throws( + () => zipreader.findEntries("*.*"), + /NS_ERROR_FAILURE/, + "Should error out on a closed zipreader" + ); +} diff --git a/modules/libjar/test/unit/test_bug336691.js b/modules/libjar/test/unit/test_bug336691.js new file mode 100644 index 0000000000..3740335acc --- /dev/null +++ b/modules/libjar/test/unit/test_bug336691.js @@ -0,0 +1,10 @@ +// Regression test for bug 336691 - nsZipArchive::Test shouldn't try to ExtractFile on directories. +function run_test() { + var file = do_get_file("data/test_bug336691.zip"); + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(file); + zipReader.test(null); // We shouldn't crash here. + zipReader.close(); +} diff --git a/modules/libjar/test/unit/test_bug370103.js b/modules/libjar/test/unit/test_bug370103.js new file mode 100644 index 0000000000..d9d0e3e6a2 --- /dev/null +++ b/modules/libjar/test/unit/test_bug370103.js @@ -0,0 +1,25 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Regression test for bug 370103 - crash when passing a null listener to +// nsIChannel.asyncOpen +function run_test() { + // Compose the jar: url + var file = do_get_file("data/test_bug370103.jar"); + var url = Services.io.newFileURI(file).spec; + url = "jar:" + url + "!/test_bug370103"; + + // Try opening channel with null listener + var channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + + var exception = false; + try { + channel.asyncOpen(null); + } catch (e) { + exception = true; + } + + Assert.ok(exception); // should throw exception instead of crashing +} diff --git a/modules/libjar/test/unit/test_bug379841.js b/modules/libjar/test/unit/test_bug379841.js new file mode 100644 index 0000000000..c8bd026bb3 --- /dev/null +++ b/modules/libjar/test/unit/test_bug379841.js @@ -0,0 +1,23 @@ +// Regression test for bug 379841 - nsIZipReader's last modified time ignores seconds + +const path = "data/test_bug379841.zip"; +// Retrieved time should be within 2 seconds of original file's time. +const MAX_TIME_DIFF = 2000000; + +var ENTRY_NAME = "test"; +// Actual time of file was 07 May 2007 13:35:49 UTC +var ENTRY_TIME = new Date(Date.UTC(2007, 4, 7, 13, 35, 49, 0)); + +function run_test() { + var file = do_get_file(path); + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(file); + var entry = zipReader.getEntry(ENTRY_NAME); + var diff = Math.abs(entry.lastModifiedTime - ENTRY_TIME.getTime() * 1000); + zipReader.close(); + if (diff >= MAX_TIME_DIFF) { + do_throw(diff); + } +} diff --git a/modules/libjar/test/unit/test_bug453254.js b/modules/libjar/test/unit/test_bug453254.js new file mode 100644 index 0000000000..150a15daa3 --- /dev/null +++ b/modules/libjar/test/unit/test_bug453254.js @@ -0,0 +1,17 @@ +function run_test() { + const zipCache = Cc["@mozilla.org/libjar/zip-reader-cache;1"].createInstance( + Ci.nsIZipReaderCache + ); + zipCache.init(1024); + try { + zipCache.getZip(null); + do_throw("Shouldn't get here!"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException && e.result == Cr.NS_ERROR_INVALID_POINTER) + ) { + throw e; + } + // do nothing, this test passes + } +} diff --git a/modules/libjar/test/unit/test_bug458158.js b/modules/libjar/test/unit/test_bug458158.js new file mode 100644 index 0000000000..15e327fba5 --- /dev/null +++ b/modules/libjar/test/unit/test_bug458158.js @@ -0,0 +1,16 @@ +function run_test() { + var zReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + try { + zReader.open(null); + do_throw("Shouldn't get here!"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException && e.result == Cr.NS_ERROR_NULL_POINTER) + ) { + throw e; + } + // do nothing, this test passes + } +} diff --git a/modules/libjar/test/unit/test_bug589292.js b/modules/libjar/test/unit/test_bug589292.js new file mode 100644 index 0000000000..07b43d2973 --- /dev/null +++ b/modules/libjar/test/unit/test_bug589292.js @@ -0,0 +1,23 @@ +// Make sure we behave appropriately when asking for content-disposition + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const path = "data/test_bug589292.zip"; + +function run_test() { + var spec = + "jar:" + Services.io.newFileURI(do_get_file(path)).spec + "!/foo.txt"; + var channel = NetUtil.newChannel({ + uri: spec, + loadUsingSystemPrincipal: true, + }); + channel.open(); + try { + channel.contentDisposition; + Assert.ok(false, "The channel has content disposition?!"); + } catch (e) { + // This is what we want to happen - there's no underlying channel, so no + // content-disposition header is available + Assert.ok(true, "How are you reading this?!"); + } +} diff --git a/modules/libjar/test/unit/test_bug597702.js b/modules/libjar/test/unit/test_bug597702.js new file mode 100644 index 0000000000..8514c593f1 --- /dev/null +++ b/modules/libjar/test/unit/test_bug597702.js @@ -0,0 +1,35 @@ +/* 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/. */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Check that reading non existant inner jars results in the right error + +function run_test() { + var file = do_get_file("data/test_bug597702.zip"); + var outerJarBase = "jar:" + Services.io.newFileURI(file).spec + "!/"; + var goodSpec = "jar:" + outerJarBase + "inner.jar!/hello"; + var badSpec = "jar:" + outerJarBase + "jar_that_isnt_in_the.jar!/hello"; + var goodChannel = NetUtil.newChannel({ + uri: goodSpec, + loadUsingSystemPrincipal: true, + }); + var badChannel = NetUtil.newChannel({ + uri: badSpec, + loadUsingSystemPrincipal: true, + }); + + try { + goodChannel.open(); + } catch (e) { + do_throw("Failed to open file in inner jar"); + } + + try { + badChannel.open(); + do_throw("Failed to report that file doesn't exist"); + } catch (e) { + Assert.ok(e.name == "NS_ERROR_FILE_NOT_FOUND"); + } +} diff --git a/modules/libjar/test/unit/test_bug637286.js b/modules/libjar/test/unit/test_bug637286.js new file mode 100644 index 0000000000..114e6b19f2 --- /dev/null +++ b/modules/libjar/test/unit/test_bug637286.js @@ -0,0 +1,26 @@ +/* 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/. */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Check that the zip cache can expire entries from nested jars + +function open_inner_zip(base, idx) { + var spec = "jar:" + base + "inner" + idx + ".zip!/foo"; + var channel = NetUtil.newChannel({ + uri: spec, + loadUsingSystemPrincipal: true, + }); + channel.open(); +} + +function run_test() { + var file = do_get_file("data/test_bug637286.zip"); + var outerJarBase = "jar:" + Services.io.newFileURI(file).spec + "!/"; + + for (var i = 0; i < 40; i++) { + open_inner_zip(outerJarBase, i); + gc(); + } +} diff --git a/modules/libjar/test/unit/test_bug658093.js b/modules/libjar/test/unit/test_bug658093.js new file mode 100644 index 0000000000..92b7654775 --- /dev/null +++ b/modules/libjar/test/unit/test_bug658093.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Check that we don't crash on reading a directory entry signature + +function run_test() { + var file = do_get_file("data/test_bug658093.zip"); + var spec = "jar:" + Services.io.newFileURI(file).spec + "!/0000"; + var channel = NetUtil.newChannel({ + uri: spec, + loadUsingSystemPrincipal: true, + }); + var failed = false; + try { + channel.open(); + } catch (e) { + failed = true; + } + Assert.ok(failed); +} diff --git a/modules/libjar/test/unit/test_corrupt_1211262.js b/modules/libjar/test/unit/test_corrupt_1211262.js new file mode 100644 index 0000000000..c6fc6b2761 --- /dev/null +++ b/modules/libjar/test/unit/test_corrupt_1211262.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Regression test ensuring that that a STORED entry with differing compressed +// and uncompressed sizes is considered to be corrupt. +function run_test() { + var file = do_get_file("data/test_corrupt3.zip"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + + var failed = false; + for (let entryPath of zipreader.findEntries("*")) { + let entry = zipreader.getEntry(entryPath); + if (!entry.isDirectory) { + try { + zipreader.getInputStream(entryPath); + } catch (e) { + failed = true; + } + } + } + + Assert.ok(failed); +} diff --git a/modules/libjar/test/unit/test_corrupt_1801102.js b/modules/libjar/test/unit/test_corrupt_1801102.js new file mode 100644 index 0000000000..395cf36c7f --- /dev/null +++ b/modules/libjar/test/unit/test_corrupt_1801102.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Regression test ensuring that that a STORED entry with differing compressed +// and uncompressed sizes is considered to be corrupt. + +add_task(async function test1801102() { + let file = do_get_file("data/test_1801102.jar"); + + let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + Assert.throws( + () => zipreader.test(""), + /NS_ERROR_FILE_CORRUPTED/, + "must throw" + ); +}); diff --git a/modules/libjar/test/unit/test_corrupt_536911.js b/modules/libjar/test/unit/test_corrupt_536911.js new file mode 100644 index 0000000000..1b8bb9d7e4 --- /dev/null +++ b/modules/libjar/test/unit/test_corrupt_536911.js @@ -0,0 +1,32 @@ +/* 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/. */ + +function wrapInputStream(input) { + var nsIScriptableInputStream = Ci.nsIScriptableInputStream; + var factory = Cc["@mozilla.org/scriptableinputstream;1"]; + var wrapper = factory.createInstance(nsIScriptableInputStream); + wrapper.init(input); + return wrapper; +} + +// Check that files can be read from after closing zipreader +function run_test() { + // the build script have created the zip we can test on in the current dir. + var file = do_get_file("data/test_corrupt.zip"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + // var entries = zipreader.findEntries(null); + // the signature for file is corrupt, should not segfault + var failed = false; + try { + var stream = wrapInputStream(zipreader.getInputStream("file")); + stream.read(1024); + } catch (ex) { + failed = true; + } + Assert.ok(failed); +} diff --git a/modules/libjar/test/unit/test_corrupt_541828.js b/modules/libjar/test/unit/test_corrupt_541828.js new file mode 100644 index 0000000000..dfd0ace015 --- /dev/null +++ b/modules/libjar/test/unit/test_corrupt_541828.js @@ -0,0 +1,21 @@ +/* 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/. */ + +// Check that files can be read from after closing zipreader +function run_test() { + // the build script have created the zip we can test on in the current dir. + var file = do_get_file("data/test_corrupt2.zip"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + var failed = false; + try { + zipreader.open(file); + // corrupt files should trigger an exception + } catch (ex) { + failed = true; + } + Assert.ok(failed); +} diff --git a/modules/libjar/test/unit/test_crx.js b/modules/libjar/test/unit/test_crx.js new file mode 100644 index 0000000000..7e48c3c893 --- /dev/null +++ b/modules/libjar/test/unit/test_crx.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +function wrapInputStream(input) { + let nsIScriptableInputStream = Ci.nsIScriptableInputStream; + let factory = Cc["@mozilla.org/scriptableinputstream;1"]; + let wrapper = factory.createInstance(nsIScriptableInputStream); + wrapper.init(input); + return wrapper; +} + +// Make sure that we can read from CRX files as if they were ZIP files. +function run_test() { + // Note: test_crx_dummy.crx is a dummy crx file created for this test. The + // public key and signature fields in the header are both empty. + let file = do_get_file("data/test_crx_dummy.crx"); + + let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + // do crc stuff + function check_archive_crc() { + zipreader.test(null); + return true; + } + Assert.ok(check_archive_crc()); + zipreader.findEntries(null); + let stream = wrapInputStream( + zipreader.getInputStream("modules/libjar/test/Makefile.in") + ); + let dirstream = wrapInputStream( + zipreader.getInputStream("modules/libjar/test/") + ); + zipreader.close(); + zipreader = null; + Cu.forceGC(); + Assert.ok(!!stream.read(1024).length); + Assert.ok(!!dirstream.read(100).length); +} diff --git a/modules/libjar/test/unit/test_dirjar_bug525755.js b/modules/libjar/test/unit/test_dirjar_bug525755.js new file mode 100644 index 0000000000..4e718e927d --- /dev/null +++ b/modules/libjar/test/unit/test_dirjar_bug525755.js @@ -0,0 +1,23 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Check that we refuse to open weird files +function run_test() { + // open a bogus file + var file = do_get_file("/"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + var failed = false; + try { + zipreader.open(file); + } catch (e) { + failed = true; + } + Assert.ok(failed); + zipreader = null; +} diff --git a/modules/libjar/test/unit/test_empty_jar_telemetry.js b/modules/libjar/test/unit/test_empty_jar_telemetry.js new file mode 100644 index 0000000000..f63b4028d7 --- /dev/null +++ b/modules/libjar/test/unit/test_empty_jar_telemetry.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const nsIBinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +// Enable the collection (during test) for all products so even products +// that don't collect the data will be able to run the test without failure. +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +Services.prefs.setBoolPref("network.jar.record_failure_reason", true); + +const fileBase = "test_empty_file.zip"; +const file = do_get_file("data/" + fileBase); +const tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); +var copy; + +function setup() { + copy = tmpDir.clone(); + copy.append("zzdxphd909dbc6r2bxtqss2m000017"); + copy.append("zzdxphd909dbc6r2bxtqss2m000017"); + copy.append(fileBase); + file.copyTo(copy.parent, copy.leafName); +} + +setup(); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.jar.record_failure_reason"); + try { + copy.remove(false); + } catch (e) {} +}); + +function Listener(callback) { + this._callback = callback; +} +Listener.prototype = { + gotStartRequest: false, + available: -1, + gotStopRequest: false, + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), + onDataAvailable(request, stream, offset, count) { + try { + this.available = stream.available(); + Assert.equal(this.available, count); + // Need to consume stream to avoid assertion + new nsIBinaryInputStream(stream).readBytes(count); + } catch (ex) { + do_throw(ex); + } + }, + onStartRequest(request) { + this.gotStartRequest = true; + }, + onStopRequest(request, status) { + this.gotStopRequest = true; + Assert.equal(status, 0); + if (this._callback) { + this._callback.call(null, this); + } + }, +}; + +const TELEMETRY_EVENTS_FILTERS = { + category: "zero_byte_load", + method: "load", +}; + +function makeChan() { + var uri = "jar:" + Services.io.newFileURI(copy).spec + "!/test.txt"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + return chan; +} + +add_task(async function test_empty_jar_file_async() { + var chan = makeChan(); + + Services.telemetry.setEventRecordingEnabled("zero_byte_load", true); + Services.telemetry.clearEvents(); + + await new Promise(resolve => { + chan.asyncOpen( + new Listener(function (l) { + Assert.ok(chan.contentLength == 0); + resolve(); + }) + ); + }); + + TelemetryTestUtils.assertEvents( + [ + { + category: "zero_byte_load", + method: "load", + object: "others", + value: null, + extra: { + sync: "false", + file_name: `${fileBase}!/test.txt`, + status: "NS_OK", + cancelled: "false", + }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); +}); + +add_task(async function test_empty_jar_file_sync() { + var chan = makeChan(); + + Services.telemetry.setEventRecordingEnabled("zero_byte_load", true); + Services.telemetry.clearEvents(); + + await new Promise(resolve => { + let stream = chan.open(); + Assert.equal(stream.available(), 0); + resolve(); + }); + + TelemetryTestUtils.assertEvents( + [ + { + category: "zero_byte_load", + method: "load", + object: "others", + value: null, + extra: { + sync: "true", + file_name: `${fileBase}!/test.txt`, + status: "NS_OK", + cancelled: "false", + }, + }, + ], + TELEMETRY_EVENTS_FILTERS + ); +}); diff --git a/modules/libjar/test/unit/test_fault_handler.js b/modules/libjar/test/unit/test_fault_handler.js new file mode 100644 index 0000000000..4c2dfd6353 --- /dev/null +++ b/modules/libjar/test/unit/test_fault_handler.js @@ -0,0 +1,138 @@ +/* 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/. */ + +"use strict"; + +// This test checks that we properly handle exceptions occuring when a +// network mapped drive is disconnected while we're reading from mmaped +// file on that drive. +// Because sharing folders requires network priviledges, this test cannot +// be completely automated. It will create the necessary files and wait +// while you run a `net share sharedfolder=...path...` in a CMD terminal +// with Administator priviledges. +// See bug 1551562 and bug 1707853 + +/* import-globals-from ../../zipwriter/test/unit/head_zipwriter.js */ + +function wrapInputStream(input) { + let wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + wrapper.init(input); + return wrapper; +} + +const { Subprocess } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" +); + +const XPI_NAME = "testjar.xpi"; +const SHARE_NAME = "sharedfolder"; + +async function net(args, silent = false) { + if (!silent) { + info(`Executing "net ${args.join(" ")}"`); + } + let proc = await Subprocess.call({ + command: "C:\\Windows\\System32\\net.exe", + arguments: args, + environmentAppend: true, + stderr: "stdout", + }); + let { exitCode } = await proc.wait(); + let stdout = await proc.stdout.readString(); + if (!silent) { + info(`stdout: ${stdout}`); + equal(exitCode, 0); + } + + // Introduce a small delay so we're sure the command effects are visible + await new Promise(resolve => do_timeout(500, resolve)); + return stdout; +} + +const time = 1199145600000; // Jan 1st 2008 + +add_task(async function test() { + let tmpFile = do_get_profile().clone(); + tmpFile.append("sharedfolder"); + tmpFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + let tmpDir = tmpFile.clone(); + tmpFile.append(XPI_NAME); + + let DATA = "a".repeat(1024 * 1024 * 10); // 10Mb + + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(DATA, DATA.length); + zipW.addEntryStream( + "data", + time * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + stream, + false + ); + zipW.close(); + + // find the computer name + let lines = await net(["config", "workstation"], true); + let COMPUTER_NAME; + for (let l of lines.split("\n")) { + if (l.startsWith("Computer name")) { + COMPUTER_NAME = l.split("\\\\")[1].trim(); + } + } + ok(COMPUTER_NAME); + + dump( + `\n\n-------\nNow in a CMD with Administrator priviledges please run:\nnet share ${SHARE_NAME}=${tmpDir.path.replaceAll( + "\\\\", + "\\" + )} /grant:everyone,READ\n\n-------\n` + ); + + info("waiting while you run command"); + while (true) { + let output = await net(["share"], true); + if (output.includes(SHARE_NAME)) { + break; + } + } + + // Map the network share to a X: drive + await net(["use", "x:", `\\\\${COMPUTER_NAME}\\${SHARE_NAME}`]); + + // the build script have created the zip we can test on in the current dir. + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(`X:\\${XPI_NAME}`); + info(file.path); + ok(file.exists()); + + let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + stream = wrapInputStream(zipreader.getInputStream("data")); + + // Delete the X: drive + await net(["use", "x:", "/delete", "/y"]); + + // Checks that we get the expected failure + Assert.throws( + () => { + while (true) { + Assert.ok(!!stream.read(1024).length); + } + }, + /NS_ERROR_FAILURE/, + "Got fault handler exception" + ); + + // This part is optional, but it's good to clean up. + dump( + `\n\n-------\nNow in a CMD with Administrator priviledges please run:\nnet share ${SHARE_NAME} /delete\n\n-------\n` + ); +}); diff --git a/modules/libjar/test/unit/test_jarchannel.js b/modules/libjar/test/unit/test_jarchannel.js new file mode 100644 index 0000000000..1af6d18854 --- /dev/null +++ b/modules/libjar/test/unit/test_jarchannel.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Tests some basic jar channel functionality + */ + +const { Constructor: ctor } = Components; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const ios = Services.io; +const dirSvc = Services.dirsvc; +const obs = Services.obs; + +const nsIBinaryInputStream = ctor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const fileBase = "test_bug637286.zip"; +const file = do_get_file("data/" + fileBase); +const jarBase = "jar:" + ios.newFileURI(file).spec + "!"; +const tmpDir = dirSvc.get("TmpD", Ci.nsIFile); + +function Listener(callback) { + this._callback = callback; +} +Listener.prototype = { + gotStartRequest: false, + available: -1, + gotStopRequest: false, + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), + onDataAvailable(request, stream, offset, count) { + try { + this.available = stream.available(); + Assert.equal(this.available, count); + // Need to consume stream to avoid assertion + new nsIBinaryInputStream(stream).readBytes(count); + } catch (ex) { + do_throw(ex); + } + }, + onStartRequest(request) { + this.gotStartRequest = true; + }, + onStopRequest(request, status) { + this.gotStopRequest = true; + Assert.equal(status, 0); + if (this._callback) { + this._callback.call(null, this); + } + }, +}; + +/** + * Basic reading test for asynchronously opened jar channel + */ +function testAsync() { + var uri = jarBase + "/inner40.zip"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + Assert.ok(chan.contentLength < 0); + chan.asyncOpen( + new Listener(function (l) { + Assert.ok(chan.contentLength > 0); + Assert.ok(l.gotStartRequest); + Assert.ok(l.gotStopRequest); + Assert.equal(l.available, chan.contentLength); + + run_next_test(); + }) + ); +} + +add_test(testAsync); +// Run same test again so we test the codepath for a zipcache hit +add_test(testAsync); + +/** + * Basic test for nsIZipReader. + */ +function testZipEntry() { + var uri = jarBase + "/inner40.zip"; + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIJARChannel); + var entry = chan.zipEntry; + Assert.ok(entry.CRC32 == 0x8b635486); + Assert.ok(entry.realSize == 184); + run_next_test(); +} + +add_test(testZipEntry); + +/** + * Basic reading test for synchronously opened jar channels + */ +add_test(function testSync() { + var uri = jarBase + "/inner40.zip"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var stream = chan.open(); + Assert.ok(chan.contentLength > 0); + Assert.equal(stream.available(), chan.contentLength); + stream.close(); + stream.close(); // should still not throw + + run_next_test(); +}); + +/** + * Basic reading test for synchronously opened, nested jar channels + */ +add_test(function testSyncNested() { + var uri = "jar:" + jarBase + "/inner40.zip!/foo"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var stream = chan.open(); + Assert.ok(chan.contentLength > 0); + Assert.equal(stream.available(), chan.contentLength); + stream.close(); + stream.close(); // should still not throw + + run_next_test(); +}); + +/** + * Basic reading test for asynchronously opened, nested jar channels + */ +add_test(function testAsyncNested(next) { + var uri = "jar:" + jarBase + "/inner40.zip!/foo"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + chan.asyncOpen( + new Listener(function (l) { + Assert.ok(chan.contentLength > 0); + Assert.ok(l.gotStartRequest); + Assert.ok(l.gotStopRequest); + Assert.equal(l.available, chan.contentLength); + + run_next_test(); + }) + ); +}); + +/** + * Verify that file locks are released when closing a synchronously + * opened jar channel stream + */ +add_test(function testSyncCloseUnlocks() { + var copy = tmpDir.clone(); + copy.append(fileBase); + file.copyTo(copy.parent, copy.leafName); + var uri = "jar:" + ios.newFileURI(copy).spec + "!/inner40.zip"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var stream = chan.open(); + Assert.ok(chan.contentLength > 0); + stream.close(); + + // Drop any jar caches + obs.notifyObservers(null, "chrome-flush-caches"); + + try { + copy.remove(false); + } catch (ex) { + do_throw(ex); + } + + run_next_test(); +}); + +/** + * Verify that file locks are released when closing an asynchronously + * opened jar channel stream + */ +add_test(function testAsyncCloseUnlocks() { + var copy = tmpDir.clone(); + copy.append(fileBase); + file.copyTo(copy.parent, copy.leafName); + + var uri = "jar:" + ios.newFileURI(copy).spec + "!/inner40.zip"; + var chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + + chan.asyncOpen( + new Listener(function (l) { + Assert.ok(chan.contentLength > 0); + + // Drop any jar caches + obs.notifyObservers(null, "chrome-flush-caches"); + + try { + copy.remove(false); + } catch (ex) { + do_throw(ex); + } + + run_next_test(); + }) + ); +}); + +function run_test() { + return run_next_test(); +} diff --git a/modules/libjar/test/unit/test_jarinput_stream_zipreader_reference.js b/modules/libjar/test/unit/test_jarinput_stream_zipreader_reference.js new file mode 100644 index 0000000000..66d9bb873c --- /dev/null +++ b/modules/libjar/test/unit/test_jarinput_stream_zipreader_reference.js @@ -0,0 +1,42 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +function wrapInputStream(input) { + var nsIScriptableInputStream = Ci.nsIScriptableInputStream; + var factory = Cc["@mozilla.org/scriptableinputstream;1"]; + var wrapper = factory.createInstance(nsIScriptableInputStream); + wrapper.init(input); + return wrapper; +} + +// Check that files can be read from after closing zipreader +function run_test() { + // the build script have created the zip we can test on in the current dir. + var file = do_get_file("data/test_bug333423.zip"); + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(file); + // do crc stuff + function check_archive_crc() { + zipreader.test(null); + return true; + } + Assert.ok(check_archive_crc()); + zipreader.findEntries(null); + var stream = wrapInputStream( + zipreader.getInputStream("modules/libjar/test/Makefile.in") + ); + var dirstream = wrapInputStream( + zipreader.getInputStream("modules/libjar/test/") + ); + zipreader.close(); + zipreader = null; + Cu.forceGC(); + Assert.ok(!!stream.read(1024).length); + Assert.ok(!!dirstream.read(100).length); +} diff --git a/modules/libjar/test/unit/test_not_found.js b/modules/libjar/test/unit/test_not_found.js new file mode 100644 index 0000000000..90c298e64a --- /dev/null +++ b/modules/libjar/test/unit/test_not_found.js @@ -0,0 +1,18 @@ +// Should report file not found on non-existent files + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const path = "data/test_bug333423.zip"; + +function run_test() { + var spec = "jar:" + Services.io.newFileURI(do_get_file(path)).spec + "!/"; + var channel = NetUtil.newChannel({ + uri: spec + "file_that_isnt_in.archive", + loadUsingSystemPrincipal: true, + }); + try { + channel.open(); + do_throw("Failed to report that file doesn't exist"); + } catch (e) { + Assert.ok(e.name == "NS_ERROR_FILE_NOT_FOUND"); + } +} diff --git a/modules/libjar/test/unit/test_umlaute.js b/modules/libjar/test/unit/test_umlaute.js new file mode 100644 index 0000000000..93e43529cd --- /dev/null +++ b/modules/libjar/test/unit/test_umlaute.js @@ -0,0 +1,37 @@ +function run_test() { + var tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + + var zipfile = do_get_file("data/test_umlaute.zip"); + + var testFile = tmpDir.clone(); + testFile.append("test_\u00FC.txt"); + if (testFile.exists()) { + testFile.remove(false); + } + + var zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipreader.open(zipfile); + + var entries = zipreader.findEntries(null); + Assert.ok(entries.hasMore()); + + var entryName = entries.getNext(); + Assert.equal(entryName, "test_\u00FC.txt"); + + Assert.ok(zipreader.hasEntry(entryName)); + + var target = tmpDir.clone(); + target.append(entryName); + target.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o640); // 0640 + + zipreader.extract(entryName, target); + + var entry = zipreader.getEntry(entryName); + Assert.ok(entry != null); + + zipreader.test(entryName); + + zipreader.close(); +} diff --git a/modules/libjar/test/unit/test_uncompressed.js b/modules/libjar/test/unit/test_uncompressed.js new file mode 100644 index 0000000000..2b0e4820e9 --- /dev/null +++ b/modules/libjar/test/unit/test_uncompressed.js @@ -0,0 +1,10 @@ +// Make sure uncompressed files pass crc +function run_test() { + var file = do_get_file("data/uncompressed.zip"); + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance( + Ci.nsIZipReader + ); + zipReader.open(file); + zipReader.test("hello"); + zipReader.close(); +} diff --git a/modules/libjar/test/unit/xpcshell.ini b/modules/libjar/test/unit/xpcshell.ini new file mode 100644 index 0000000000..31285a4c00 --- /dev/null +++ b/modules/libjar/test/unit/xpcshell.ini @@ -0,0 +1,55 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' +support-files = + data/empty + data/test_bug333423.zip + data/test_bug336691.zip + data/test_bug370103.jar + data/test_bug379841.zip + data/test_bug589292.zip + data/test_bug597702.zip + data/test_bug637286.zip + data/test_bug658093.zip + data/test_1801102.jar + data/test_corrupt.zip + data/test_corrupt2.zip + data/test_corrupt3.zip + data/test_crx_dummy.crx + data/test_umlaute.zip + data/uncompressed.zip + data/test_empty_file.zip + +[test_jarchannel.js] +skip-if = os == "mac" +[test_bug278262.js] +[test_bug333423.js] +[test_bug336691.js] +[test_bug370103.js] +[test_bug379841.js] +[test_bug453254.js] +[test_bug458158.js] +[test_bug589292.js] +[test_bug597702.js] +[test_bug637286.js] +[test_bug658093.js] +[test_corrupt_536911.js] +[test_corrupt_541828.js] +[test_corrupt_1211262.js] +[test_corrupt_1801102.js] +[test_crx.js] +[test_dirjar_bug525755.js] +[test_jarinput_stream_zipreader_reference.js] +[test_not_found.js] +[test_uncompressed.js] +[test_umlaute.js] +[test_bug1328865.js] +[test_bug1550815.js] +# recovering from SIGBUS is temporarily disabled by bug 1583735 +skip-if = true +[test_empty_jar_telemetry.js] +[test_fault_handler.js] +head = ../../zipwriter/test/unit/head_zipwriter.js +skip-if = + os != "win" # tests windows specific exception handling + true # Requires elevated priviledges. See bug 1707853 and comments in test diff --git a/modules/libjar/zipstruct.h b/modules/libjar/zipstruct.h new file mode 100644 index 0000000000..f4739cc108 --- /dev/null +++ b/modules/libjar/zipstruct.h @@ -0,0 +1,103 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef _zipstruct_h +#define _zipstruct_h + +/* + * Certain constants and structures for + * the Phil Katz ZIP archive format. + * + */ + +typedef struct ZipLocal_ { + unsigned char signature[4]; + unsigned char word[2]; + unsigned char bitflag[2]; + unsigned char method[2]; + unsigned char time[2]; + unsigned char date[2]; + unsigned char crc32[4]; + unsigned char size[4]; + unsigned char orglen[4]; + unsigned char filename_len[2]; + unsigned char extrafield_len[2]; +} ZipLocal; + +/* + * 'sizeof(struct XXX)' includes padding on ARM (see bug 87965) + * As the internals of a jar/zip file must not depend on the target + * architecture (i386, ppc, ARM, ...), use a fixed value instead. + */ +#define ZIPLOCAL_SIZE (4 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2) + +typedef struct ZipCentral_ { + unsigned char signature[4]; + unsigned char version_made_by[2]; + unsigned char version[2]; + unsigned char bitflag[2]; + unsigned char method[2]; + unsigned char time[2]; + unsigned char date[2]; + unsigned char crc32[4]; + unsigned char size[4]; + unsigned char orglen[4]; + unsigned char filename_len[2]; + unsigned char extrafield_len[2]; + unsigned char commentfield_len[2]; + unsigned char diskstart_number[2]; + unsigned char internal_attributes[2]; + unsigned char external_attributes[4]; + unsigned char localhdr_offset[4]; +} ZipCentral; + +/* + * 'sizeof(struct XXX)' includes padding on ARM (see bug 87965) + * As the internals of a jar/zip file must not depend on the target + * architecture (i386, ppc, ARM, ...), use a fixed value instead. + */ +#define ZIPCENTRAL_SIZE \ + (4 + 2 + 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 + 4) + +typedef struct ZipEnd_ { + unsigned char signature[4]; + unsigned char disk_nr[2]; + unsigned char start_central_dir[2]; + unsigned char total_entries_disk[2]; + unsigned char total_entries_archive[2]; + unsigned char central_dir_size[4]; + unsigned char offset_central_dir[4]; + unsigned char commentfield_len[2]; +} ZipEnd; + +/* + * 'sizeof(struct XXX)' includes padding on ARM (see bug 87965) + * As the internals of a jar/zip file must not depend on the target + * architecture (i386, ppc, ARM, ...), use a fixed value instead. + */ +#define ZIPEND_SIZE (4 + 2 + 2 + 2 + 2 + 4 + 4 + 2) + +/* signatures */ +#define LOCALSIG 0x04034B50l +#define CENTRALSIG 0x02014B50l +#define ENDSIG 0x06054B50l + +/* extra fields */ +#define EXTENDED_TIMESTAMP_FIELD 0x5455 +#define EXTENDED_TIMESTAMP_MODTIME 0x01 + +/* compression methods */ +#define STORED 0 +#define SHRUNK 1 +#define REDUCED1 2 +#define REDUCED2 3 +#define REDUCED3 4 +#define REDUCED4 5 +#define IMPLODED 6 +#define TOKENIZED 7 +#define DEFLATED 8 +#define UNSUPPORTED 0xFF + +#endif /* _zipstruct_h */ diff --git a/modules/libjar/zipwriter/StreamFunctions.cpp b/modules/libjar/zipwriter/StreamFunctions.cpp new file mode 100644 index 0000000000..a16db28844 --- /dev/null +++ b/modules/libjar/zipwriter/StreamFunctions.cpp @@ -0,0 +1,44 @@ +/* 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/. + */ + +#include "nscore.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" + +/* + * Fully reads the required amount of data. Keeps reading until all the + * data is retrieved or an error is hit. + */ +nsresult ZW_ReadData(nsIInputStream* aStream, char* aBuffer, uint32_t aCount) { + while (aCount > 0) { + uint32_t read; + nsresult rv = aStream->Read(aBuffer, aCount, &read); + NS_ENSURE_SUCCESS(rv, rv); + aCount -= read; + aBuffer += read; + // If we hit EOF before reading the data we need then throw. + if (read == 0 && aCount > 0) return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +/* + * Fully writes the required amount of data. Keeps writing until all the + * data is written or an error is hit. + */ +nsresult ZW_WriteData(nsIOutputStream* aStream, const char* aBuffer, + uint32_t aCount) { + while (aCount > 0) { + uint32_t written; + nsresult rv = aStream->Write(aBuffer, aCount, &written); + NS_ENSURE_SUCCESS(rv, rv); + if (written <= 0) return NS_ERROR_FAILURE; + aCount -= written; + aBuffer += written; + } + + return NS_OK; +} diff --git a/modules/libjar/zipwriter/StreamFunctions.h b/modules/libjar/zipwriter/StreamFunctions.h new file mode 100644 index 0000000000..b3fc20d04a --- /dev/null +++ b/modules/libjar/zipwriter/StreamFunctions.h @@ -0,0 +1,58 @@ +/* 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/. + */ + +#ifndef _nsStreamFunctions_h_ +#define _nsStreamFunctions_h_ + +#include "nscore.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" + +/* + * ZIP file data is stored little-endian. These are helper functions to read and + * write little endian data to/from a char buffer. + * The off argument, where present, is incremented according to the number of + * bytes consumed from the buffer. + */ +inline void WRITE8(uint8_t* buf, uint32_t* off, uint8_t val) { + buf[(*off)++] = val; +} + +inline void WRITE16(uint8_t* buf, uint32_t* off, uint16_t val) { + WRITE8(buf, off, val & 0xff); + WRITE8(buf, off, (val >> 8) & 0xff); +} + +inline void WRITE32(uint8_t* buf, uint32_t* off, uint32_t val) { + WRITE16(buf, off, val & 0xffff); + WRITE16(buf, off, (val >> 16) & 0xffff); +} + +inline uint8_t READ8(const uint8_t* buf, uint32_t* off) { + return buf[(*off)++]; +} + +inline uint16_t READ16(const uint8_t* buf, uint32_t* off) { + uint16_t val = READ8(buf, off); + val |= READ8(buf, off) << 8; + return val; +} + +inline uint32_t READ32(const uint8_t* buf, uint32_t* off) { + uint32_t val = READ16(buf, off); + val |= READ16(buf, off) << 16; + return val; +} + +inline uint32_t PEEK32(const uint8_t* buf) { + return (uint32_t)((buf[0]) | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24)); +} + +nsresult ZW_ReadData(nsIInputStream* aStream, char* aBuffer, uint32_t aCount); + +nsresult ZW_WriteData(nsIOutputStream* aStream, const char* aBuffer, + uint32_t aCount); + +#endif diff --git a/modules/libjar/zipwriter/components.conf b/modules/libjar/zipwriter/components.conf new file mode 100644 index 0000000000..14ea6a7e4a --- /dev/null +++ b/modules/libjar/zipwriter/components.conf @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{461cd5dd-73c6-47a4-8cc3-603b37d84a61}', + 'contract_ids': [ + '@mozilla.org/streamconv;1?from=uncompressed&to=deflate', + '@mozilla.org/streamconv;1?from=uncompressed&to=gzip', + '@mozilla.org/streamconv;1?from=uncompressed&to=rawdeflate', + '@mozilla.org/streamconv;1?from=uncompressed&to=x-gzip', + ], + 'type': 'nsDeflateConverter', + 'headers': ['/modules/libjar/zipwriter/nsDeflateConverter.h'], + }, + { + 'cid': '{430d416c-a722-4ad1-be98-d9a445f85e3f}', + 'contract_ids': ['@mozilla.org/zipwriter;1'], + 'type': 'nsZipWriter', + 'headers': ['/modules/libjar/zipwriter/nsZipWriter.h'], + }, +] diff --git a/modules/libjar/zipwriter/moz.build b/modules/libjar/zipwriter/moz.build new file mode 100644 index 0000000000..4d94522536 --- /dev/null +++ b/modules/libjar/zipwriter/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +XPIDL_SOURCES += [ + "nsIZipWriter.idl", +] + +XPIDL_MODULE = "zipwriter" + +UNIFIED_SOURCES += [ + "nsDeflateConverter.cpp", + "nsZipDataStream.cpp", + "nsZipHeader.cpp", + "nsZipWriter.cpp", + "StreamFunctions.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" diff --git a/modules/libjar/zipwriter/nsDeflateConverter.cpp b/modules/libjar/zipwriter/nsDeflateConverter.cpp new file mode 100644 index 0000000000..1fead9aa7b --- /dev/null +++ b/modules/libjar/zipwriter/nsDeflateConverter.cpp @@ -0,0 +1,178 @@ +/* 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/. + */ + +#include "StreamFunctions.h" +#include "nsDeflateConverter.h" +#include "nsStringStream.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "plstr.h" +#include "mozilla/UniquePtr.h" + +#define ZLIB_TYPE "deflate" +#define GZIP_TYPE "gzip" +#define X_GZIP_TYPE "x-gzip" + +using namespace mozilla; + +/** + * nsDeflateConverter is a stream converter applies the deflate compression + * method to the data. + */ +NS_IMPL_ISUPPORTS(nsDeflateConverter, nsIStreamConverter, nsIStreamListener, + nsIRequestObserver) + +nsresult nsDeflateConverter::Init() { + int zerr; + + mOffset = 0; + + mZstream.zalloc = Z_NULL; + mZstream.zfree = Z_NULL; + mZstream.opaque = Z_NULL; + + int32_t window = MAX_WBITS; + switch (mWrapMode) { + case WRAP_NONE: + window = -window; + break; + case WRAP_GZIP: + window += 16; + break; + default: + break; + } + + zerr = deflateInit2(&mZstream, mLevel, Z_DEFLATED, window, 8, + Z_DEFAULT_STRATEGY); + if (zerr != Z_OK) return NS_ERROR_OUT_OF_MEMORY; + + mZstream.next_out = mWriteBuffer; + mZstream.avail_out = sizeof(mWriteBuffer); + + // mark the input buffer as empty. + mZstream.avail_in = 0; + mZstream.next_in = Z_NULL; + + return NS_OK; +} + +NS_IMETHODIMP nsDeflateConverter::Convert(nsIInputStream* aFromStream, + const char* aFromType, + const char* aToType, + nsISupports* aCtxt, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsDeflateConverter::AsyncConvertData(const char* aFromType, + const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + if (mListener) return NS_ERROR_ALREADY_INITIALIZED; + + NS_ENSURE_ARG_POINTER(aListener); + + if (!PL_strncasecmp(aToType, ZLIB_TYPE, sizeof(ZLIB_TYPE) - 1)) { + mWrapMode = WRAP_ZLIB; + } else if (!nsCRT::strcasecmp(aToType, GZIP_TYPE) || + !nsCRT::strcasecmp(aToType, X_GZIP_TYPE)) { + mWrapMode = WRAP_GZIP; + } else { + mWrapMode = WRAP_NONE; + } + + nsresult rv = Init(); + NS_ENSURE_SUCCESS(rv, rv); + + mListener = aListener; + mContext = aCtxt; + return rv; +} + +NS_IMETHODIMP +nsDeflateConverter::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, + nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsDeflateConverter::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) { + if (!mListener) return NS_ERROR_NOT_INITIALIZED; + + auto buffer = MakeUnique<char[]>(aCount); + NS_ENSURE_TRUE(buffer, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = ZW_ReadData(aInputStream, buffer.get(), aCount); + NS_ENSURE_SUCCESS(rv, rv); + + // make sure we aren't reading too much + mZstream.avail_in = aCount; + mZstream.next_in = (unsigned char*)buffer.get(); + + int zerr = Z_OK; + // deflate loop + while (mZstream.avail_in > 0 && zerr == Z_OK) { + zerr = deflate(&mZstream, Z_NO_FLUSH); + + while (mZstream.avail_out == 0) { + // buffer is full, push the data out to the listener + rv = PushAvailableData(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + zerr = deflate(&mZstream, Z_NO_FLUSH); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsDeflateConverter::OnStartRequest(nsIRequest* aRequest) { + if (!mListener) return NS_ERROR_NOT_INITIALIZED; + + return mListener->OnStartRequest(aRequest); +} + +NS_IMETHODIMP nsDeflateConverter::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (!mListener) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + int zerr; + do { + zerr = deflate(&mZstream, Z_FINISH); + rv = PushAvailableData(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + } while (zerr == Z_OK); + + deflateEnd(&mZstream); + + return mListener->OnStopRequest(aRequest, aStatusCode); +} + +nsresult nsDeflateConverter::PushAvailableData(nsIRequest* aRequest) { + uint32_t bytesToWrite = sizeof(mWriteBuffer) - mZstream.avail_out; + // We don't need to do anything if there isn't any data + if (bytesToWrite == 0) return NS_OK; + + MOZ_ASSERT(bytesToWrite <= INT32_MAX); + nsCOMPtr<nsIInputStream> stream; + nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), + Span((char*)mWriteBuffer, bytesToWrite), + NS_ASSIGNMENT_DEPEND); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mListener->OnDataAvailable(aRequest, stream, mOffset, bytesToWrite); + + // now set the state for 'deflate' + mZstream.next_out = mWriteBuffer; + mZstream.avail_out = sizeof(mWriteBuffer); + + mOffset += bytesToWrite; + return rv; +} diff --git a/modules/libjar/zipwriter/nsDeflateConverter.h b/modules/libjar/zipwriter/nsDeflateConverter.h new file mode 100644 index 0000000000..4a82299c1c --- /dev/null +++ b/modules/libjar/zipwriter/nsDeflateConverter.h @@ -0,0 +1,57 @@ +/* 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/. + */ + +#ifndef _nsDeflateConverter_h_ +#define _nsDeflateConverter_h_ + +#include "nsIStreamConverter.h" +#include "nsCOMPtr.h" +#include "zlib.h" +#include "mozilla/Attributes.h" + +#define DEFLATECONVERTER_CID \ + { \ + 0x461cd5dd, 0x73c6, 0x47a4, { \ + 0x8c, 0xc3, 0x60, 0x3b, 0x37, 0xd8, 0x4a, 0x61 \ + } \ + } + +class nsDeflateConverter final : public nsIStreamConverter { + public: + static constexpr size_t kZipBufLen = (4 * 1024 - 1); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSISTREAMCONVERTER + + nsDeflateConverter() : mWrapMode(WRAP_NONE), mOffset(0), mZstream() { + // 6 is Z_DEFAULT_COMPRESSION but we need the actual value + mLevel = 6; + } + + explicit nsDeflateConverter(int32_t level) + : mWrapMode(WRAP_NONE), mOffset(0), mZstream() { + mLevel = level; + } + + private: + ~nsDeflateConverter() {} + + enum WrapMode { WRAP_ZLIB, WRAP_GZIP, WRAP_NONE }; + + WrapMode mWrapMode; + uint64_t mOffset; + int32_t mLevel; + nsCOMPtr<nsIStreamListener> mListener; + nsCOMPtr<nsISupports> mContext; + z_stream mZstream; + unsigned char mWriteBuffer[kZipBufLen]; + + nsresult Init(); + nsresult PushAvailableData(nsIRequest* aRequest); +}; + +#endif diff --git a/modules/libjar/zipwriter/nsIZipWriter.idl b/modules/libjar/zipwriter/nsIZipWriter.idl new file mode 100644 index 0000000000..9cf32fd1e7 --- /dev/null +++ b/modules/libjar/zipwriter/nsIZipWriter.idl @@ -0,0 +1,223 @@ +/* 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/. + */ + +#include "nsISupports.idl" +interface nsIChannel; +interface nsIInputStream; +interface nsIRequestObserver; +interface nsIFile; +interface nsIZipEntry; + +/** + * nsIZipWriter + * + * An interface for a zip archiver that can be used from script. + * + * The interface supports both a synchronous method of archiving data and a + * queueing system to allow operations to be prepared then run in sequence + * with notification after completion. + * + * Operations added to the queue do not get performed until performQueue is + * called at which point they will be performed in the order that they were + * added to the queue. + * + * Operations performed on the queue will throw any errors out to the + * observer. + * + * An attempt to perform a synchronous operation while the background queue + * is in progress will throw NS_ERROR_IN_PROGRESS. + * + * Entry names should use /'s as path separators and should not start with + * a /. + * + * It is not generally necessary to add directory entries in order to add file + * entries within them, however it is possible that some zip programs may + * experience problems what that. + */ +[scriptable, uuid(3ca10750-797e-4a22-bcfe-66170b5e96dd)] +interface nsIZipWriter : nsISupports +{ + /** + * Some predefined compression levels + */ + const uint32_t COMPRESSION_NONE = 0; + const uint32_t COMPRESSION_FASTEST = 1; + const uint32_t COMPRESSION_DEFAULT = 6; + const uint32_t COMPRESSION_BEST = 9; + + /** + * Gets or sets the comment associated with the open zip file. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + */ + attribute ACString comment; + + /** + * Indicates that operations on the background queue are being performed. + */ + readonly attribute boolean inQueue; + + /** + * The file that the zipwriter is writing to. + */ + readonly attribute nsIFile file; + + /** + * Opens a zip file. + * + * @param aFile the zip file to open + * @param aIoFlags the open flags for the zip file from prio.h + * + * @throws NS_ERROR_ALREADY_INITIALIZED if a zip file is already open + * @throws NS_ERROR_INVALID_ARG if aFile is null + * @throws NS_ERROR_FILE_NOT_FOUND if aFile does not exist and flags did + * not allow for creation + * @throws NS_ERROR_FILE_CORRUPTED if the file does not contain zip markers + * @throws <other-error> on failure to open zip file (most likely corrupt + * or unsupported form) + */ + void open(in nsIFile aFile, in int32_t aIoFlags); + + /** + * Returns a nsIZipEntry describing a specified zip entry or null if there + * is no such entry in the zip file + * + * @param aZipEntry the path of the entry + */ + nsIZipEntry getEntry(in AUTF8String aZipEntry); + + /** + * Checks whether the zipfile contains an entry specified by zipEntry. + * + * @param aZipEntry the path of the entry + */ + boolean hasEntry(in AUTF8String aZipEntry); + + /** + * Adds a new directory entry to the zip file. If aZipEntry does not end with + * "/" then it will be added. + * + * @param aZipEntry the path of the directory entry + * @param aModTime the modification time of the entry in microseconds + * @param aQueue adds the operation to the background queue. Will be + * performed when processQueue is called. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_FILE_ALREADY_EXISTS if the path already exists in the + * file + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws NS_ERROR_INVALID_ARG if aModTime is older than 1980-1-1 + */ + void addEntryDirectory(in AUTF8String aZipEntry, in PRTime aModTime, + in boolean aQueue); + + /** + * Adds a new file or directory to the zip file. If the specified file is + * a directory then this will be equivalent to a call to + * addEntryDirectory(aZipEntry, aFile.lastModifiedTime, aQueue) + * + * @param aZipEntry the path of the file entry + * @param aCompression the compression level, 0 is no compression, 9 is best + * @param aFile the file to get the data and modification time from + * @param aQueue adds the operation to the background queue. Will be + * performed when processQueue is called. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_FILE_ALREADY_EXISTS if the path already exists in the zip + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws NS_ERROR_FILE_NOT_FOUND if file does not exist + */ + void addEntryFile(in AUTF8String aZipEntry, + in int32_t aCompression, in nsIFile aFile, + in boolean aQueue); + + /** + * Adds data from a channel to the zip file. If the operation is performed + * on the queue then the channel will be opened asynchronously, otherwise + * the channel must support being opened synchronously. + * + * @param aZipEntry the path of the file entry + * @param aModTime the modification time of the entry in microseconds + * @param aCompression the compression level, 0 is no compression, 9 is best + * @param aChannel the channel to get the data from + * @param aQueue adds the operation to the background queue. Will be + * performed when processQueue is called. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_FILE_ALREADY_EXISTS if the path already exists in the zip + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws NS_ERROR_INVALID_ARG if aModTime is older than 1980-1-1 + */ + void addEntryChannel(in AUTF8String aZipEntry, in PRTime aModTime, + in int32_t aCompression, in nsIChannel aChannel, + in boolean aQueue); + + /** + * Adds data from an input stream to the zip file. + * + * @param aZipEntry the path of the file entry + * @param aModTime the modification time of the entry in microseconds + * @param aCompression the compression level, 0 is no compression, 9 is best + * @param aStream the input stream to get the data from + * @param aQueue adds the operation to the background queue. Will be + * performed when processQueue is called. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_FILE_ALREADY_EXISTS if the path already exists in the zip + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws NS_ERROR_INVALID_ARG if aModTime is older than 1980-1-1 + */ + void addEntryStream(in AUTF8String aZipEntry, in PRTime aModTime, + in int32_t aCompression, in nsIInputStream aStream, + in boolean aQueue); + + /** + * Removes an existing entry from the zip file. + * + * @param aZipEntry the path of the entry to be removed + * @param aQueue adds the operation to the background queue. Will be + * performed when processQueue is called. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws NS_ERROR_FILE_NOT_FOUND if no entry with the given path exists + * @throws <other-error> on failure to update the zip file + */ + void removeEntry(in AUTF8String aZipEntry, in boolean aQueue); + + /** + * Processes all queued items until complete or some error occurs. The + * observer will be notified when the first operation starts and when the + * last operation completes. Any failures will be passed to the observer. + * The zip writer will be busy until the queue is complete or some error + * halted processing of the queue early. In the event of an early failure, + * remaining items will stay in the queue and calling processQueue will + * continue. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_IN_PROGRESS if the queue is already in progress + */ + void processQueue(in nsIRequestObserver aObserver, in nsISupports aContext); + + /** + * Closes the zip file. + * + * @throws NS_ERROR_NOT_INITIALIZED if no zip file has been opened + * @throws NS_ERROR_IN_PROGRESS if another operation is currently in progress + * @throws <other-error> on failure to complete the zip file + */ + void close(); + + /** + * Make all stored(uncompressed) files align to given alignment size. + * + * @param aAlignSize is the alignment size, valid values from 2 to 32768, and + must be power of 2. + * + * @throws NS_ERROR_INVALID_ARG if aAlignSize is invalid + * @throws <other-error> on failure to update the zip file + */ + void alignStoredFiles(in uint16_t aAlignSize); +}; diff --git a/modules/libjar/zipwriter/nsZipDataStream.cpp b/modules/libjar/zipwriter/nsZipDataStream.cpp new file mode 100644 index 0000000000..97bfda5a63 --- /dev/null +++ b/modules/libjar/zipwriter/nsZipDataStream.cpp @@ -0,0 +1,158 @@ +/* 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/. + */ + +#include "StreamFunctions.h" +#include "nsZipDataStream.h" +#include "nsStringStream.h" +#include "nsISeekableStream.h" +#include "nsDeflateConverter.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" + +#define ZIP_METHOD_STORE 0 +#define ZIP_METHOD_DEFLATE 8 + +using namespace mozilla; + +/** + * nsZipDataStream handles the writing an entry's into the zip file. + * It is set up to wither write the data as is, or in the event that compression + * has been requested to pass it through a stream converter. + * Currently only the deflate compression method is supported. + * The CRC checksum for the entry's data is also generated here. + */ +NS_IMPL_ISUPPORTS(nsZipDataStream, nsIStreamListener, nsIRequestObserver) + +nsresult nsZipDataStream::Init(nsZipWriter* aWriter, nsIOutputStream* aStream, + nsZipHeader* aHeader, int32_t aCompression) { + mWriter = aWriter; + mHeader = aHeader; + mStream = aStream; + mHeader->mCRC = crc32(0L, Z_NULL, 0); + + nsresult rv = + NS_NewSimpleStreamListener(getter_AddRefs(mOutput), aStream, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + if (aCompression > 0) { + mHeader->mMethod = ZIP_METHOD_DEFLATE; + nsCOMPtr<nsIStreamConverter> converter = + new nsDeflateConverter(aCompression); + NS_ENSURE_TRUE(converter, NS_ERROR_OUT_OF_MEMORY); + + rv = converter->AsyncConvertData("uncompressed", "rawdeflate", mOutput, + nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + mOutput = converter; + } else { + mHeader->mMethod = ZIP_METHOD_STORE; + } + + return NS_OK; +} + +NS_IMETHODIMP nsZipDataStream::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) { + if (!mOutput) return NS_ERROR_NOT_INITIALIZED; + + auto buffer = MakeUnique<char[]>(aCount); + NS_ENSURE_TRUE(buffer, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = ZW_ReadData(aInputStream, buffer.get(), aCount); + NS_ENSURE_SUCCESS(rv, rv); + + return ProcessData(aRequest, nullptr, buffer.get(), aOffset, aCount); +} + +NS_IMETHODIMP nsZipDataStream::OnStartRequest(nsIRequest* aRequest) { + if (!mOutput) return NS_ERROR_NOT_INITIALIZED; + + return mOutput->OnStartRequest(aRequest); +} + +NS_IMETHODIMP nsZipDataStream::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (!mOutput) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = mOutput->OnStopRequest(aRequest, aStatusCode); + mOutput = nullptr; + if (NS_FAILED(rv)) { + mWriter->EntryCompleteCallback(mHeader, rv); + } else { + rv = CompleteEntry(); + rv = mWriter->EntryCompleteCallback(mHeader, rv); + } + + mStream = nullptr; + mWriter = nullptr; + mHeader = nullptr; + + return rv; +} + +inline nsresult nsZipDataStream::CompleteEntry() { + nsresult rv; + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + int64_t pos; + rv = seekable->Tell(&pos); + NS_ENSURE_SUCCESS(rv, rv); + + mHeader->mCSize = pos - mHeader->mOffset - mHeader->GetFileHeaderLength(); + mHeader->mWriteOnClose = true; + return NS_OK; +} + +nsresult nsZipDataStream::ProcessData(nsIRequest* aRequest, + nsISupports* aContext, char* aBuffer, + uint64_t aOffset, uint32_t aCount) { + mHeader->mCRC = crc32( + mHeader->mCRC, reinterpret_cast<const unsigned char*>(aBuffer), aCount); + + MOZ_ASSERT(aCount <= INT32_MAX); + nsCOMPtr<nsIInputStream> stream; + nsresult rv = NS_NewByteInputStream( + getter_AddRefs(stream), Span(aBuffer, aCount), NS_ASSIGNMENT_DEPEND); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mOutput->OnDataAvailable(aRequest, stream, aOffset, aCount); + mHeader->mUSize += aCount; + + return rv; +} + +nsresult nsZipDataStream::ReadStream(nsIInputStream* aStream) { + if (!mOutput) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = OnStartRequest(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + auto buffer = MakeUnique<char[]>(4096); + NS_ENSURE_TRUE(buffer, NS_ERROR_OUT_OF_MEMORY); + + uint32_t read = 0; + uint32_t offset = 0; + do { + rv = aStream->Read(buffer.get(), 4096, &read); + if (NS_FAILED(rv)) { + OnStopRequest(nullptr, rv); + return rv; + } + + if (read > 0) { + rv = ProcessData(nullptr, nullptr, buffer.get(), offset, read); + if (NS_FAILED(rv)) { + OnStopRequest(nullptr, rv); + return rv; + } + offset += read; + } + } while (read > 0); + + return OnStopRequest(nullptr, NS_OK); +} diff --git a/modules/libjar/zipwriter/nsZipDataStream.h b/modules/libjar/zipwriter/nsZipDataStream.h new file mode 100644 index 0000000000..5f0d27de45 --- /dev/null +++ b/modules/libjar/zipwriter/nsZipDataStream.h @@ -0,0 +1,40 @@ +/* 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/. + */ + +#ifndef _nsZipDataStream_h_ +#define _nsZipDataStream_h_ + +#include "nsZipWriter.h" +#include "nsIOutputStream.h" +#include "nsIStreamListener.h" +#include "mozilla/Attributes.h" + +class nsZipDataStream final : public nsIStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsZipDataStream() {} + + nsresult Init(nsZipWriter* aWriter, nsIOutputStream* aStream, + nsZipHeader* aHeader, int32_t aCompression); + + nsresult ReadStream(nsIInputStream* aStream); + + private: + ~nsZipDataStream() {} + + nsCOMPtr<nsIStreamListener> mOutput; + nsCOMPtr<nsIOutputStream> mStream; + RefPtr<nsZipWriter> mWriter; + RefPtr<nsZipHeader> mHeader; + + nsresult CompleteEntry(); + nsresult ProcessData(nsIRequest* aRequest, nsISupports* aContext, + char* aBuffer, uint64_t aOffset, uint32_t aCount); +}; + +#endif diff --git a/modules/libjar/zipwriter/nsZipHeader.cpp b/modules/libjar/zipwriter/nsZipHeader.cpp new file mode 100644 index 0000000000..7506670f1a --- /dev/null +++ b/modules/libjar/zipwriter/nsZipHeader.cpp @@ -0,0 +1,376 @@ +/* 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/. + */ + +#include "StreamFunctions.h" +#include "nsZipHeader.h" +#include "prtime.h" + +#define ZIP_FILE_HEADER_SIGNATURE 0x04034b50 +#define ZIP_FILE_HEADER_SIZE 30 +#define ZIP_CDS_HEADER_SIGNATURE 0x02014b50 +#define ZIP_CDS_HEADER_SIZE 46 + +#define FLAGS_IS_UTF8 0x800 + +#define ZIP_EXTENDED_TIMESTAMP_FIELD 0x5455 +#define ZIP_EXTENDED_TIMESTAMP_MODTIME 0x01 + +using namespace mozilla; + +/** + * nsZipHeader represents an entry from a zip file. + */ +NS_IMPL_ISUPPORTS(nsZipHeader, nsIZipEntry) + +NS_IMETHODIMP nsZipHeader::GetCompression(uint16_t* aCompression) { + NS_ASSERTION(mInited, "Not initalised"); + + *aCompression = mMethod; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetSize(uint32_t* aSize) { + NS_ASSERTION(mInited, "Not initalised"); + + *aSize = mCSize; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetRealSize(uint32_t* aRealSize) { + NS_ASSERTION(mInited, "Not initalised"); + + *aRealSize = mUSize; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetCRC32(uint32_t* aCRC32) { + NS_ASSERTION(mInited, "Not initalised"); + + *aCRC32 = mCRC; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetIsDirectory(bool* aIsDirectory) { + NS_ASSERTION(mInited, "Not initalised"); + + if (mName.Last() == '/') + *aIsDirectory = true; + else + *aIsDirectory = false; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetLastModifiedTime(PRTime* aLastModifiedTime) { + NS_ASSERTION(mInited, "Not initalised"); + + // Try to read timestamp from extra field + uint16_t blocksize; + const uint8_t* tsField = + GetExtraField(ZIP_EXTENDED_TIMESTAMP_FIELD, false, &blocksize); + if (tsField && blocksize >= 5) { + uint32_t pos = 4; + uint8_t flags; + flags = READ8(tsField, &pos); + if (flags & ZIP_EXTENDED_TIMESTAMP_MODTIME) { + *aLastModifiedTime = (PRTime)(READ32(tsField, &pos)) * PR_USEC_PER_SEC; + return NS_OK; + } + } + + // Use DOS date/time fields + // Note that on DST shift we can't handle correctly the hour that is valid + // in both DST zones + PRExplodedTime time; + + time.tm_usec = 0; + + time.tm_hour = (mTime >> 11) & 0x1F; + time.tm_min = (mTime >> 5) & 0x3F; + time.tm_sec = (mTime & 0x1F) * 2; + + time.tm_year = (mDate >> 9) + 1980; + time.tm_month = ((mDate >> 5) & 0x0F) - 1; + time.tm_mday = mDate & 0x1F; + + time.tm_params.tp_gmt_offset = 0; + time.tm_params.tp_dst_offset = 0; + + PR_NormalizeTime(&time, PR_GMTParameters); + time.tm_params.tp_gmt_offset = PR_LocalTimeParameters(&time).tp_gmt_offset; + PR_NormalizeTime(&time, PR_GMTParameters); + time.tm_params.tp_dst_offset = PR_LocalTimeParameters(&time).tp_dst_offset; + + *aLastModifiedTime = PR_ImplodeTime(&time); + + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetIsSynthetic(bool* aIsSynthetic) { + NS_ASSERTION(mInited, "Not initalised"); + + *aIsSynthetic = false; + return NS_OK; +} + +NS_IMETHODIMP nsZipHeader::GetPermissions(uint32_t* aPermissions) { + NS_ASSERTION(mInited, "Not initalised"); + + // Always give user read access at least, this matches nsIZipReader's + // behaviour + *aPermissions = ((mEAttr >> 16) & 0xfff) | 0x100; + return NS_OK; +} + +nsresult nsZipHeader::Init(const nsACString& aPath, PRTime aDate, + uint32_t aAttr, uint32_t aOffset) { + NS_ASSERTION(!mInited, "Already initalised"); + + PRExplodedTime time; + PR_ExplodeTime(aDate, PR_LocalTimeParameters, &time); + if (time.tm_year < 1980) { + return NS_ERROR_INVALID_ARG; + } + + mTime = time.tm_sec / 2 + (time.tm_min << 5) + (time.tm_hour << 11); + mDate = + time.tm_mday + ((time.tm_month + 1) << 5) + ((time.tm_year - 1980) << 9); + + // Store modification timestamp as extra field + // First fill CDS extra field + mFieldLength = 9; + mExtraField = MakeUnique<uint8_t[]>(mFieldLength); + if (!mExtraField) { + mFieldLength = 0; + } else { + uint32_t pos = 0; + WRITE16(mExtraField.get(), &pos, ZIP_EXTENDED_TIMESTAMP_FIELD); + WRITE16(mExtraField.get(), &pos, 5); + WRITE8(mExtraField.get(), &pos, ZIP_EXTENDED_TIMESTAMP_MODTIME); + WRITE32(mExtraField.get(), &pos, aDate / PR_USEC_PER_SEC); + + // Fill local extra field + mLocalExtraField = MakeUnique<uint8_t[]>(mFieldLength); + if (mLocalExtraField) { + mLocalFieldLength = mFieldLength; + memcpy(mLocalExtraField.get(), mExtraField.get(), mLocalFieldLength); + } + } + + mEAttr = aAttr; + mOffset = aOffset; + mName = aPath; + mComment.Truncate(); + // Claim a UTF-8 path in case it needs it. + mFlags |= FLAGS_IS_UTF8; + mInited = true; + + return NS_OK; +} + +uint32_t nsZipHeader::GetFileHeaderLength() { + return ZIP_FILE_HEADER_SIZE + mName.Length() + mLocalFieldLength; +} + +nsresult nsZipHeader::WriteFileHeader(nsIOutputStream* aStream) { + NS_ASSERTION(mInited, "Not initalised"); + + uint8_t buf[ZIP_FILE_HEADER_SIZE]; + uint32_t pos = 0; + WRITE32(buf, &pos, ZIP_FILE_HEADER_SIGNATURE); + WRITE16(buf, &pos, mVersionNeeded); + WRITE16(buf, &pos, mFlags); + WRITE16(buf, &pos, mMethod); + WRITE16(buf, &pos, mTime); + WRITE16(buf, &pos, mDate); + WRITE32(buf, &pos, mCRC); + WRITE32(buf, &pos, mCSize); + WRITE32(buf, &pos, mUSize); + WRITE16(buf, &pos, mName.Length()); + WRITE16(buf, &pos, mLocalFieldLength); + + nsresult rv = ZW_WriteData(aStream, (const char*)buf, pos); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ZW_WriteData(aStream, mName.get(), mName.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + if (mLocalFieldLength) { + rv = ZW_WriteData(aStream, (const char*)mLocalExtraField.get(), + mLocalFieldLength); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +uint32_t nsZipHeader::GetCDSHeaderLength() { + return ZIP_CDS_HEADER_SIZE + mName.Length() + mComment.Length() + + mFieldLength; +} + +nsresult nsZipHeader::WriteCDSHeader(nsIOutputStream* aStream) { + NS_ASSERTION(mInited, "Not initalised"); + + uint8_t buf[ZIP_CDS_HEADER_SIZE]; + uint32_t pos = 0; + WRITE32(buf, &pos, ZIP_CDS_HEADER_SIGNATURE); + WRITE16(buf, &pos, mVersionMade); + WRITE16(buf, &pos, mVersionNeeded); + WRITE16(buf, &pos, mFlags); + WRITE16(buf, &pos, mMethod); + WRITE16(buf, &pos, mTime); + WRITE16(buf, &pos, mDate); + WRITE32(buf, &pos, mCRC); + WRITE32(buf, &pos, mCSize); + WRITE32(buf, &pos, mUSize); + WRITE16(buf, &pos, mName.Length()); + WRITE16(buf, &pos, mFieldLength); + WRITE16(buf, &pos, mComment.Length()); + WRITE16(buf, &pos, mDisk); + WRITE16(buf, &pos, mIAttr); + WRITE32(buf, &pos, mEAttr); + WRITE32(buf, &pos, mOffset); + + nsresult rv = ZW_WriteData(aStream, (const char*)buf, pos); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ZW_WriteData(aStream, mName.get(), mName.Length()); + NS_ENSURE_SUCCESS(rv, rv); + if (mExtraField) { + rv = ZW_WriteData(aStream, (const char*)mExtraField.get(), mFieldLength); + NS_ENSURE_SUCCESS(rv, rv); + } + return ZW_WriteData(aStream, mComment.get(), mComment.Length()); +} + +nsresult nsZipHeader::ReadCDSHeader(nsIInputStream* stream) { + NS_ASSERTION(!mInited, "Already initalised"); + + uint8_t buf[ZIP_CDS_HEADER_SIZE]; + + nsresult rv = ZW_ReadData(stream, (char*)buf, ZIP_CDS_HEADER_SIZE); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t pos = 0; + uint32_t signature = READ32(buf, &pos); + if (signature != ZIP_CDS_HEADER_SIGNATURE) return NS_ERROR_FILE_CORRUPTED; + + mVersionMade = READ16(buf, &pos); + mVersionNeeded = READ16(buf, &pos); + mFlags = READ16(buf, &pos); + mMethod = READ16(buf, &pos); + mTime = READ16(buf, &pos); + mDate = READ16(buf, &pos); + mCRC = READ32(buf, &pos); + mCSize = READ32(buf, &pos); + mUSize = READ32(buf, &pos); + uint16_t namelength = READ16(buf, &pos); + mFieldLength = READ16(buf, &pos); + uint16_t commentlength = READ16(buf, &pos); + mDisk = READ16(buf, &pos); + mIAttr = READ16(buf, &pos); + mEAttr = READ32(buf, &pos); + mOffset = READ32(buf, &pos); + + if (namelength > 0) { + auto field = MakeUnique<char[]>(namelength); + NS_ENSURE_TRUE(field, NS_ERROR_OUT_OF_MEMORY); + rv = ZW_ReadData(stream, field.get(), namelength); + NS_ENSURE_SUCCESS(rv, rv); + mName.Assign(field.get(), namelength); + } else + mName.Truncate(); + + if (mFieldLength > 0) { + mExtraField = MakeUnique<uint8_t[]>(mFieldLength); + NS_ENSURE_TRUE(mExtraField, NS_ERROR_OUT_OF_MEMORY); + rv = ZW_ReadData(stream, (char*)mExtraField.get(), mFieldLength); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (commentlength > 0) { + auto field = MakeUnique<char[]>(commentlength); + NS_ENSURE_TRUE(field, NS_ERROR_OUT_OF_MEMORY); + rv = ZW_ReadData(stream, field.get(), commentlength); + NS_ENSURE_SUCCESS(rv, rv); + mComment.Assign(field.get(), commentlength); + } else + mComment.Truncate(); + + mInited = true; + return NS_OK; +} + +const uint8_t* nsZipHeader::GetExtraField(uint16_t aTag, bool aLocal, + uint16_t* aBlockSize) { + const uint8_t* buf = aLocal ? mLocalExtraField.get() : mExtraField.get(); + uint32_t buflen = aLocal ? mLocalFieldLength : mFieldLength; + uint32_t pos = 0; + uint16_t tag, blocksize; + + while (buf && (pos + 4) <= buflen) { + tag = READ16(buf, &pos); + blocksize = READ16(buf, &pos); + + if (aTag == tag && (pos + blocksize) <= buflen) { + *aBlockSize = blocksize; + return buf + pos - 4; + } + + pos += blocksize; + } + + return nullptr; +} + +/* + * Pad extra field to align data starting position to specified size. + */ +nsresult nsZipHeader::PadExtraField(uint32_t aOffset, uint16_t aAlignSize) { + uint32_t pad_size; + uint32_t pa_offset; + uint32_t pa_end; + + // Check for range and power of 2. + if (aAlignSize < 2 || aAlignSize > 32768 || + (aAlignSize & (aAlignSize - 1)) != 0) { + return NS_ERROR_INVALID_ARG; + } + + // Point to current starting data position. + aOffset += ZIP_FILE_HEADER_SIZE + mName.Length() + mLocalFieldLength; + + // Calculate aligned offset. + pa_offset = aOffset & ~(aAlignSize - 1); + pa_end = pa_offset + aAlignSize; + pad_size = pa_end - aOffset; + if (pad_size == 0) { + return NS_OK; + } + + // Leave enough room(at least 4 bytes) for valid values in extra field. + while (pad_size < 4) { + pad_size += aAlignSize; + } + // Extra field length is 2 bytes. + if (mLocalFieldLength + pad_size > 65535) { + return NS_ERROR_FAILURE; + } + + UniquePtr<uint8_t[]> field = std::move(mLocalExtraField); + uint32_t pos = mLocalFieldLength; + + mLocalExtraField = MakeUnique<uint8_t[]>(mLocalFieldLength + pad_size); + memcpy(mLocalExtraField.get(), field.get(), mLocalFieldLength); + // Use 0xFFFF as tag ID to avoid conflict with other IDs. + // For more information, please read "Extensible data fields" section in: + // http://www.pkware.com/documents/casestudies/APPNOTE.TXT + WRITE16(mLocalExtraField.get(), &pos, 0xFFFF); + WRITE16(mLocalExtraField.get(), &pos, pad_size - 4); + memset(mLocalExtraField.get() + pos, 0, pad_size - 4); + mLocalFieldLength += pad_size; + + return NS_OK; +} diff --git a/modules/libjar/zipwriter/nsZipHeader.h b/modules/libjar/zipwriter/nsZipHeader.h new file mode 100644 index 0000000000..528730b000 --- /dev/null +++ b/modules/libjar/zipwriter/nsZipHeader.h @@ -0,0 +1,92 @@ +/* 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/. + */ + +#ifndef _nsZipHeader_h_ +#define _nsZipHeader_h_ + +#include "nsString.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsIZipReader.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" + +// High word is S_IFREG, low word is DOS file attribute +#define ZIP_ATTRS_FILE 0x80000000 +// High word is S_IFDIR, low word is DOS dir attribute +#define ZIP_ATTRS_DIRECTORY 0x40000010 +#define PERMISSIONS_FILE 0644 +#define PERMISSIONS_DIR 0755 + +// Combine file type attributes with unix style permissions +#define ZIP_ATTRS(p, a) ((p & 0xfff) << 16) | a + +class nsZipHeader final : public nsIZipEntry { + ~nsZipHeader() { + mExtraField = nullptr; + mLocalExtraField = nullptr; + } + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIZIPENTRY + + nsZipHeader() + : mCRC(0), + mCSize(0), + mUSize(0), + mEAttr(0), + mOffset(0), + mFieldLength(0), + mLocalFieldLength(0), + mVersionMade(0x0300 + + 23), // Generated on Unix by v2.3 (matches infozip) + mVersionNeeded(20), // Requires v2.0 to extract + mFlags(0), + mMethod(0), + mTime(0), + mDate(0), + mDisk(0), + mIAttr(0), + mInited(false), + mWriteOnClose(false), + mExtraField(nullptr), + mLocalExtraField(nullptr) {} + + uint32_t mCRC; + uint32_t mCSize; + uint32_t mUSize; + uint32_t mEAttr; + uint32_t mOffset; + uint32_t mFieldLength; + uint32_t mLocalFieldLength; + uint16_t mVersionMade; + uint16_t mVersionNeeded; + uint16_t mFlags; + uint16_t mMethod; + uint16_t mTime; + uint16_t mDate; + uint16_t mDisk; + uint16_t mIAttr; + bool mInited; + bool mWriteOnClose; + nsCString mName; + nsCString mComment; + mozilla::UniquePtr<uint8_t[]> mExtraField; + mozilla::UniquePtr<uint8_t[]> mLocalExtraField; + + nsresult Init(const nsACString& aPath, PRTime aDate, uint32_t aAttr, + uint32_t aOffset); + uint32_t GetFileHeaderLength(); + nsresult WriteFileHeader(nsIOutputStream* aStream); + uint32_t GetCDSHeaderLength(); + nsresult WriteCDSHeader(nsIOutputStream* aStream); + nsresult ReadCDSHeader(nsIInputStream* aStream); + const uint8_t* GetExtraField(uint16_t aTag, bool aLocal, + uint16_t* aBlockSize); + nsresult PadExtraField(uint32_t aOffset, uint16_t aAlignSize); +}; + +#endif diff --git a/modules/libjar/zipwriter/nsZipWriter.cpp b/modules/libjar/zipwriter/nsZipWriter.cpp new file mode 100644 index 0000000000..43c890a7ea --- /dev/null +++ b/modules/libjar/zipwriter/nsZipWriter.cpp @@ -0,0 +1,1059 @@ +/* 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/. + */ + +#include "nsZipWriter.h" + +#include <algorithm> + +#include "StreamFunctions.h" +#include "nsZipDataStream.h" +#include "nsISeekableStream.h" +#include "nsIStreamListener.h" +#include "nsIInputStreamPump.h" +#include "nsComponentManagerUtils.h" +#include "nsError.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIFile.h" +#include "prio.h" + +#define ZIP_EOCDR_HEADER_SIZE 22 +#define ZIP_EOCDR_HEADER_SIGNATURE 0x06054b50 + +using namespace mozilla; + +/** + * nsZipWriter is used to create and add to zip files. + * It is based on the spec available at + * http://www.pkware.com/documents/casestudies/APPNOTE.TXT. + * + * The basic structure of a zip file created is slightly simpler than that + * illustrated in the spec because certain features of the zip format are + * unsupported: + * + * [local file header 1] + * [file data 1] + * . + * . + * . + * [local file header n] + * [file data n] + * [central directory] + * [end of central directory record] + */ +NS_IMPL_ISUPPORTS(nsZipWriter, nsIZipWriter, nsIRequestObserver) + +nsZipWriter::nsZipWriter() : mCDSOffset(0), mCDSDirty(false), mInQueue(false) {} + +nsZipWriter::~nsZipWriter() { + if (mStream && !mInQueue) Close(); +} + +NS_IMETHODIMP nsZipWriter::GetComment(nsACString& aComment) { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + aComment = mComment; + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::SetComment(const nsACString& aComment) { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + mComment = aComment; + mCDSDirty = true; + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::GetInQueue(bool* aInQueue) { + *aInQueue = mInQueue; + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::GetFile(nsIFile** aFile) { + if (!mFile) return NS_ERROR_NOT_INITIALIZED; + + nsCOMPtr<nsIFile> file; + nsresult rv = mFile->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aFile = file); + return NS_OK; +} + +/* + * Reads file entries out of an existing zip file. + */ +nsresult nsZipWriter::ReadFile(nsIFile* aFile) { + int64_t size; + nsresult rv = aFile->GetFileSize(&size); + NS_ENSURE_SUCCESS(rv, rv); + + // If the file is too short, it cannot be a valid archive, thus we fail + // without even attempting to open it + NS_ENSURE_TRUE(size > ZIP_EOCDR_HEADER_SIZE, NS_ERROR_FILE_CORRUPTED); + + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t buf[1024]; + int64_t seek = size - 1024; + uint32_t length = 1024; + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(inputStream); + + while (true) { + if (seek < 0) { + length += (int32_t)seek; + seek = 0; + } + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, seek); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + rv = ZW_ReadData(inputStream, (char*)buf, length); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + /* + * We have to backtrack from the end of the file until we find the + * CDS signature + */ + // We know it's at least this far from the end + for (uint32_t pos = length - ZIP_EOCDR_HEADER_SIZE; (int32_t)pos >= 0; + pos--) { + uint32_t sig = PEEK32(buf + pos); + if (sig == ZIP_EOCDR_HEADER_SIGNATURE) { + // Skip down to entry count + pos += 10; + uint32_t entries = READ16(buf, &pos); + // Skip past CDS size + pos += 4; + mCDSOffset = READ32(buf, &pos); + uint32_t commentlen = READ16(buf, &pos); + + if (commentlen == 0) + mComment.Truncate(); + else if (pos + commentlen <= length) + mComment.Assign((const char*)buf + pos, commentlen); + else { + if ((seek + pos + commentlen) > size) { + inputStream->Close(); + return NS_ERROR_FILE_CORRUPTED; + } + auto field = MakeUnique<char[]>(commentlen); + NS_ENSURE_TRUE(field, NS_ERROR_OUT_OF_MEMORY); + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, seek + pos); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + rv = ZW_ReadData(inputStream, field.get(), length); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + mComment.Assign(field.get(), commentlen); + } + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, mCDSOffset); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + for (uint32_t entry = 0; entry < entries; entry++) { + nsZipHeader* header = new nsZipHeader(); + if (!header) { + inputStream->Close(); + mEntryHash.Clear(); + mHeaders.Clear(); + return NS_ERROR_OUT_OF_MEMORY; + } + rv = header->ReadCDSHeader(inputStream); + if (NS_FAILED(rv)) { + inputStream->Close(); + mEntryHash.Clear(); + mHeaders.Clear(); + return rv; + } + mEntryHash.InsertOrUpdate(header->mName, mHeaders.Count()); + if (!mHeaders.AppendObject(header)) return NS_ERROR_OUT_OF_MEMORY; + } + + return inputStream->Close(); + } + } + + if (seek == 0) { + // We've reached the start with no signature found. Corrupt. + inputStream->Close(); + return NS_ERROR_FILE_CORRUPTED; + } + + // Overlap by the size of the end of cdr + seek -= (1024 - ZIP_EOCDR_HEADER_SIZE); + } + // Will never reach here in reality + MOZ_ASSERT_UNREACHABLE("Loop should never complete"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsZipWriter::Open(nsIFile* aFile, int32_t aIoFlags) { + if (mStream) return NS_ERROR_ALREADY_INITIALIZED; + + NS_ENSURE_ARG_POINTER(aFile); + + // Need to be able to write to the file + if (aIoFlags & PR_RDONLY) return NS_ERROR_FAILURE; + + nsresult rv = aFile->Clone(getter_AddRefs(mFile)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = mFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists && !(aIoFlags & PR_CREATE_FILE)) return NS_ERROR_FILE_NOT_FOUND; + + if (exists && !(aIoFlags & (PR_TRUNCATE | PR_WRONLY))) { + rv = ReadFile(mFile); + NS_ENSURE_SUCCESS(rv, rv); + mCDSDirty = false; + } else { + mCDSOffset = 0; + mCDSDirty = true; + mComment.Truncate(); + } + + // Silently drop PR_APPEND + aIoFlags &= 0xef; + + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), mFile, aIoFlags); + if (NS_FAILED(rv)) { + mHeaders.Clear(); + mEntryHash.Clear(); + return rv; + } + + rv = NS_NewBufferedOutputStream(getter_AddRefs(mStream), stream.forget(), + 64 * 1024); + if (NS_FAILED(rv)) { + mHeaders.Clear(); + mEntryHash.Clear(); + return rv; + } + + if (mCDSOffset > 0) { + rv = SeekCDS(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::GetEntry(const nsACString& aZipEntry, + nsIZipEntry** _retval) { + int32_t pos; + if (mEntryHash.Get(aZipEntry, &pos)) + NS_ADDREF(*_retval = mHeaders[pos]); + else + *_retval = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::HasEntry(const nsACString& aZipEntry, + bool* _retval) { + *_retval = mEntryHash.Get(aZipEntry, nullptr); + + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::AddEntryDirectory(const nsACString& aZipEntry, + PRTime aModTime, bool aQueue) { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + if (aQueue) { + nsZipQueueItem item; + item.mOperation = OPERATION_ADD; + item.mZipEntry = aZipEntry; + item.mModTime = aModTime; + item.mPermissions = PERMISSIONS_DIR; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mQueue.AppendElement(item); + return NS_OK; + } + + if (mInQueue) return NS_ERROR_IN_PROGRESS; + return InternalAddEntryDirectory(aZipEntry, aModTime, PERMISSIONS_DIR); +} + +NS_IMETHODIMP nsZipWriter::AddEntryFile(const nsACString& aZipEntry, + int32_t aCompression, nsIFile* aFile, + bool aQueue) { + NS_ENSURE_ARG_POINTER(aFile); + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + if (aQueue) { + nsZipQueueItem item; + item.mOperation = OPERATION_ADD; + item.mZipEntry = aZipEntry; + item.mCompression = aCompression; + rv = aFile->Clone(getter_AddRefs(item.mFile)); + NS_ENSURE_SUCCESS(rv, rv); + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mQueue.AppendElement(item); + return NS_OK; + } + + if (mInQueue) return NS_ERROR_IN_PROGRESS; + + bool exists; + rv = aFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) return NS_ERROR_FILE_NOT_FOUND; + + bool isdir; + rv = aFile->IsDirectory(&isdir); + NS_ENSURE_SUCCESS(rv, rv); + + PRTime modtime; + rv = aFile->GetLastModifiedTime(&modtime); + NS_ENSURE_SUCCESS(rv, rv); + modtime *= PR_USEC_PER_MSEC; + + uint32_t permissions; + rv = aFile->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (isdir) return InternalAddEntryDirectory(aZipEntry, modtime, permissions); + + if (mEntryHash.Get(aZipEntry, nullptr)) return NS_ERROR_FILE_ALREADY_EXISTS; + + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddEntryStream(aZipEntry, modtime, aCompression, inputStream, false, + permissions); + NS_ENSURE_SUCCESS(rv, rv); + + return inputStream->Close(); +} + +NS_IMETHODIMP nsZipWriter::AddEntryChannel(const nsACString& aZipEntry, + PRTime aModTime, + int32_t aCompression, + nsIChannel* aChannel, bool aQueue) { + NS_ENSURE_ARG_POINTER(aChannel); + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + if (aQueue) { + nsZipQueueItem item; + item.mOperation = OPERATION_ADD; + item.mZipEntry = aZipEntry; + item.mModTime = aModTime; + item.mCompression = aCompression; + item.mPermissions = PERMISSIONS_FILE; + item.mChannel = aChannel; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mQueue.AppendElement(item); + return NS_OK; + } + + if (mInQueue) return NS_ERROR_IN_PROGRESS; + if (mEntryHash.Get(aZipEntry, nullptr)) return NS_ERROR_FILE_ALREADY_EXISTS; + + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = aChannel->Open(getter_AddRefs(inputStream)); + + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddEntryStream(aZipEntry, aModTime, aCompression, inputStream, false, + PERMISSIONS_FILE); + NS_ENSURE_SUCCESS(rv, rv); + + return inputStream->Close(); +} + +NS_IMETHODIMP nsZipWriter::AddEntryStream(const nsACString& aZipEntry, + PRTime aModTime, int32_t aCompression, + nsIInputStream* aStream, + bool aQueue) { + return AddEntryStream(aZipEntry, aModTime, aCompression, aStream, aQueue, + PERMISSIONS_FILE); +} + +nsresult nsZipWriter::AddEntryStream(const nsACString& aZipEntry, + PRTime aModTime, int32_t aCompression, + nsIInputStream* aStream, bool aQueue, + uint32_t aPermissions) { + NS_ENSURE_ARG_POINTER(aStream); + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + if (aQueue) { + nsZipQueueItem item; + item.mOperation = OPERATION_ADD; + item.mZipEntry = aZipEntry; + item.mModTime = aModTime; + item.mCompression = aCompression; + item.mPermissions = aPermissions; + item.mStream = aStream; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mQueue.AppendElement(item); + return NS_OK; + } + + if (mInQueue) return NS_ERROR_IN_PROGRESS; + if (mEntryHash.Get(aZipEntry, nullptr)) return NS_ERROR_FILE_ALREADY_EXISTS; + + RefPtr<nsZipHeader> header = new nsZipHeader(); + NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = header->Init( + aZipEntry, aModTime, ZIP_ATTRS(aPermissions, ZIP_ATTRS_FILE), mCDSOffset); + if (NS_FAILED(rv)) { + SeekCDS(); + return rv; + } + + rv = header->WriteFileHeader(mStream); + if (NS_FAILED(rv)) { + SeekCDS(); + return rv; + } + + RefPtr<nsZipDataStream> stream = new nsZipDataStream(); + if (!stream) { + SeekCDS(); + return NS_ERROR_OUT_OF_MEMORY; + } + rv = stream->Init(this, mStream, header, aCompression); + if (NS_FAILED(rv)) { + SeekCDS(); + return rv; + } + + rv = stream->ReadStream(aStream); + if (NS_FAILED(rv)) SeekCDS(); + return rv; +} + +NS_IMETHODIMP nsZipWriter::RemoveEntry(const nsACString& aZipEntry, + bool aQueue) { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + + if (aQueue) { + nsZipQueueItem item; + item.mOperation = OPERATION_REMOVE; + item.mZipEntry = aZipEntry; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mQueue.AppendElement(item); + return NS_OK; + } + + if (mInQueue) return NS_ERROR_IN_PROGRESS; + + int32_t pos; + if (mEntryHash.Get(aZipEntry, &pos)) { + // Flush any remaining data before we seek. + nsresult rv = mStream->Flush(); + NS_ENSURE_SUCCESS(rv, rv); + if (pos < mHeaders.Count() - 1) { + // This is not the last entry, pull back the data. + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream); + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, + mHeaders[pos]->mOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), mFile); + NS_ENSURE_SUCCESS(rv, rv); + seekable = do_QueryInterface(inputStream); + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, + mHeaders[pos + 1]->mOffset); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + uint32_t count = mCDSOffset - mHeaders[pos + 1]->mOffset; + uint32_t read = 0; + char buf[4096]; + while (count > 0) { + read = std::min(count, (uint32_t)sizeof(buf)); + + rv = inputStream->Read(buf, read, &read); + if (NS_FAILED(rv)) { + inputStream->Close(); + Cleanup(); + return rv; + } + + rv = ZW_WriteData(mStream, buf, read); + if (NS_FAILED(rv)) { + inputStream->Close(); + Cleanup(); + return rv; + } + + count -= read; + } + inputStream->Close(); + + // Rewrite header offsets and update hash + uint32_t shift = (mHeaders[pos + 1]->mOffset - mHeaders[pos]->mOffset); + mCDSOffset -= shift; + int32_t pos2 = pos + 1; + while (pos2 < mHeaders.Count()) { + mEntryHash.InsertOrUpdate(mHeaders[pos2]->mName, pos2 - 1); + mHeaders[pos2]->mOffset -= shift; + pos2++; + } + } else { + // Remove the last entry is just a case of moving the CDS + mCDSOffset = mHeaders[pos]->mOffset; + rv = SeekCDS(); + NS_ENSURE_SUCCESS(rv, rv); + } + + mEntryHash.Remove(mHeaders[pos]->mName); + mHeaders.RemoveObjectAt(pos); + mCDSDirty = true; + + return NS_OK; + } + + return NS_ERROR_FILE_NOT_FOUND; +} + +NS_IMETHODIMP nsZipWriter::ProcessQueue(nsIRequestObserver* aObserver, + nsISupports* aContext) { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + if (mInQueue) return NS_ERROR_IN_PROGRESS; + + mProcessObserver = aObserver; + mProcessContext = aContext; + mInQueue = true; + + if (mProcessObserver) mProcessObserver->OnStartRequest(nullptr); + + BeginProcessingNextItem(); + + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::Close() { + if (!mStream) return NS_ERROR_NOT_INITIALIZED; + if (mInQueue) return NS_ERROR_IN_PROGRESS; + + if (mCDSDirty) { + uint32_t size = 0; + for (int32_t i = 0; i < mHeaders.Count(); i++) { + nsresult rv = mHeaders[i]->WriteCDSHeader(mStream); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + size += mHeaders[i]->GetCDSHeaderLength(); + } + + uint8_t buf[ZIP_EOCDR_HEADER_SIZE]; + uint32_t pos = 0; + WRITE32(buf, &pos, ZIP_EOCDR_HEADER_SIGNATURE); + WRITE16(buf, &pos, 0); + WRITE16(buf, &pos, 0); + WRITE16(buf, &pos, mHeaders.Count()); + WRITE16(buf, &pos, mHeaders.Count()); + WRITE32(buf, &pos, size); + WRITE32(buf, &pos, mCDSOffset); + WRITE16(buf, &pos, mComment.Length()); + + nsresult rv = ZW_WriteData(mStream, (const char*)buf, pos); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + rv = ZW_WriteData(mStream, mComment.get(), mComment.Length()); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream); + rv = seekable->SetEOF(); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + // Go back and rewrite the file headers + for (int32_t i = 0; i < mHeaders.Count(); i++) { + nsZipHeader* header = mHeaders[i]; + if (!header->mWriteOnClose) continue; + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, header->mOffset); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + rv = header->WriteFileHeader(mStream); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + } + } + + nsresult rv = mStream->Close(); + mStream = nullptr; + mHeaders.Clear(); + mEntryHash.Clear(); + mQueue.Clear(); + + return rv; +} + +// Our nsIRequestObserver monitors removal operations performed on the queue +NS_IMETHODIMP nsZipWriter::OnStartRequest(nsIRequest* aRequest) { + return NS_OK; +} + +NS_IMETHODIMP nsZipWriter::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (NS_FAILED(aStatusCode)) { + FinishQueue(aStatusCode); + Cleanup(); + } + + nsresult rv = mStream->Flush(); + if (NS_FAILED(rv)) { + FinishQueue(rv); + Cleanup(); + return rv; + } + rv = SeekCDS(); + if (NS_FAILED(rv)) { + FinishQueue(rv); + return rv; + } + + BeginProcessingNextItem(); + + return NS_OK; +} + +/* + * Make all stored(uncompressed) files align to given alignment size. + */ +NS_IMETHODIMP nsZipWriter::AlignStoredFiles(uint16_t aAlignSize) { + nsresult rv; + + // Check for range and power of 2. + if (aAlignSize < 2 || aAlignSize > 32768 || + (aAlignSize & (aAlignSize - 1)) != 0) { + return NS_ERROR_INVALID_ARG; + } + + for (int i = 0; i < mHeaders.Count(); i++) { + nsZipHeader* header = mHeaders[i]; + + // Check whether this entry is file and compression method is stored. + bool isdir; + rv = header->GetIsDirectory(&isdir); + if (NS_FAILED(rv)) { + return rv; + } + if (isdir || header->mMethod != 0) { + continue; + } + // Pad extra field to align data starting position to specified size. + uint32_t old_len = header->mLocalFieldLength; + rv = header->PadExtraField(header->mOffset, aAlignSize); + if (NS_FAILED(rv)) { + continue; + } + // No padding means data already aligned. + uint32_t shift = header->mLocalFieldLength - old_len; + if (shift == 0) { + continue; + } + + // Flush any remaining data before we start. + rv = mStream->Flush(); + if (NS_FAILED(rv)) { + return rv; + } + + // Open zip file for reading. + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), mFile); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsISeekableStream> in_seekable = do_QueryInterface(inputStream); + nsCOMPtr<nsISeekableStream> out_seekable = do_QueryInterface(mStream); + + uint32_t data_offset = + header->mOffset + header->GetFileHeaderLength() - shift; + uint32_t count = mCDSOffset - data_offset; + uint32_t read; + char buf[4096]; + + // Shift data to aligned postion. + while (count > 0) { + read = std::min(count, (uint32_t)sizeof(buf)); + + rv = in_seekable->Seek(nsISeekableStream::NS_SEEK_SET, + data_offset + count - read); + if (NS_FAILED(rv)) { + break; + } + + rv = inputStream->Read(buf, read, &read); + if (NS_FAILED(rv)) { + break; + } + + rv = out_seekable->Seek(nsISeekableStream::NS_SEEK_SET, + data_offset + count - read + shift); + if (NS_FAILED(rv)) { + break; + } + + rv = ZW_WriteData(mStream, buf, read); + if (NS_FAILED(rv)) { + break; + } + + count -= read; + } + inputStream->Close(); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + // Update current header + rv = out_seekable->Seek(nsISeekableStream::NS_SEEK_SET, header->mOffset); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + rv = header->WriteFileHeader(mStream); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + // Update offset of all other headers + int pos = i + 1; + while (pos < mHeaders.Count()) { + mHeaders[pos]->mOffset += shift; + pos++; + } + mCDSOffset += shift; + rv = SeekCDS(); + if (NS_FAILED(rv)) { + return rv; + } + mCDSDirty = true; + } + + return NS_OK; +} + +nsresult nsZipWriter::InternalAddEntryDirectory(const nsACString& aZipEntry, + PRTime aModTime, + uint32_t aPermissions) { + RefPtr<nsZipHeader> header = new nsZipHeader(); + NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY); + + uint32_t zipAttributes = ZIP_ATTRS(aPermissions, ZIP_ATTRS_DIRECTORY); + + nsresult rv = NS_OK; + if (aZipEntry.Last() != '/') { + nsCString dirPath; + dirPath.Assign(aZipEntry + "/"_ns); + rv = header->Init(dirPath, aModTime, zipAttributes, mCDSOffset); + } else { + rv = header->Init(aZipEntry, aModTime, zipAttributes, mCDSOffset); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + Cleanup(); + return rv; + } + + if (mEntryHash.Get(header->mName, nullptr)) + return NS_ERROR_FILE_ALREADY_EXISTS; + + rv = header->WriteFileHeader(mStream); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + + mCDSDirty = true; + mCDSOffset += header->GetFileHeaderLength(); + mEntryHash.InsertOrUpdate(header->mName, mHeaders.Count()); + + if (!mHeaders.AppendObject(header)) { + Cleanup(); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +/* + * Recovering from an error while adding a new entry is simply a case of + * seeking back to the CDS. If we fail trying to do that though then cleanup + * and bail out. + */ +nsresult nsZipWriter::SeekCDS() { + nsresult rv; + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream, &rv); + if (NS_FAILED(rv)) { + Cleanup(); + return rv; + } + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, mCDSOffset); + if (NS_FAILED(rv)) Cleanup(); + return rv; +} + +/* + * In a bad error condition this essentially closes down the component as best + * it can. + */ +void nsZipWriter::Cleanup() { + mHeaders.Clear(); + mEntryHash.Clear(); + if (mStream) mStream->Close(); + mStream = nullptr; + mFile = nullptr; +} + +/* + * Called when writing a file to the zip is complete. + */ +nsresult nsZipWriter::EntryCompleteCallback(nsZipHeader* aHeader, + nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + mEntryHash.InsertOrUpdate(aHeader->mName, mHeaders.Count()); + if (!mHeaders.AppendObject(aHeader)) { + mEntryHash.Remove(aHeader->mName); + SeekCDS(); + return NS_ERROR_OUT_OF_MEMORY; + } + mCDSDirty = true; + mCDSOffset += aHeader->mCSize + aHeader->GetFileHeaderLength(); + + if (mInQueue) BeginProcessingNextItem(); + + return NS_OK; + } + + nsresult rv = SeekCDS(); + if (mInQueue) FinishQueue(aStatus); + return rv; +} + +inline nsresult nsZipWriter::BeginProcessingAddition(nsZipQueueItem* aItem, + bool* complete) { + if (aItem->mFile) { + bool exists; + nsresult rv = aItem->mFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) return NS_ERROR_FILE_NOT_FOUND; + + bool isdir; + rv = aItem->mFile->IsDirectory(&isdir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aItem->mFile->GetLastModifiedTime(&aItem->mModTime); + NS_ENSURE_SUCCESS(rv, rv); + aItem->mModTime *= PR_USEC_PER_MSEC; + + rv = aItem->mFile->GetPermissions(&aItem->mPermissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isdir) { + // Set up for fall through to stream reader + rv = NS_NewLocalFileInputStream(getter_AddRefs(aItem->mStream), + aItem->mFile); + NS_ENSURE_SUCCESS(rv, rv); + } + // If a dir then this will fall through to the plain dir addition + } + + uint32_t zipAttributes = ZIP_ATTRS(aItem->mPermissions, ZIP_ATTRS_FILE); + + if (aItem->mStream || aItem->mChannel) { + RefPtr<nsZipHeader> header = new nsZipHeader(); + NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = header->Init(aItem->mZipEntry, aItem->mModTime, zipAttributes, + mCDSOffset); + NS_ENSURE_SUCCESS(rv, rv); + + rv = header->WriteFileHeader(mStream); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsZipDataStream> stream = new nsZipDataStream(); + NS_ENSURE_TRUE(stream, NS_ERROR_OUT_OF_MEMORY); + rv = stream->Init(this, mStream, header, aItem->mCompression); + NS_ENSURE_SUCCESS(rv, rv); + + if (aItem->mStream) { + nsCOMPtr<nsIInputStreamPump> pump; + nsCOMPtr<nsIInputStream> tmpStream = aItem->mStream; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), tmpStream.forget(), 0, 0, + true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = pump->AsyncRead(stream); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = aItem->mChannel->AsyncOpen(stream); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + // Must be plain directory addition + *complete = true; + return InternalAddEntryDirectory(aItem->mZipEntry, aItem->mModTime, + aItem->mPermissions); +} + +inline nsresult nsZipWriter::BeginProcessingRemoval(int32_t aPos) { + // Open the zip file for reading + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), mFile); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIInputStreamPump> pump; + nsCOMPtr<nsIInputStream> tmpStream = inputStream; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), tmpStream.forget(), 0, 0, + true); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + nsCOMPtr<nsIStreamListener> listener; + rv = NS_NewSimpleStreamListener(getter_AddRefs(listener), mStream, this); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream); + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, mHeaders[aPos]->mOffset); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + uint32_t shift = (mHeaders[aPos + 1]->mOffset - mHeaders[aPos]->mOffset); + mCDSOffset -= shift; + int32_t pos2 = aPos + 1; + while (pos2 < mHeaders.Count()) { + mEntryHash.InsertOrUpdate(mHeaders[pos2]->mName, pos2 - 1); + mHeaders[pos2]->mOffset -= shift; + pos2++; + } + + mEntryHash.Remove(mHeaders[aPos]->mName); + mHeaders.RemoveObjectAt(aPos); + mCDSDirty = true; + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + inputStream->Close(); + Cleanup(); + return rv; + } + return NS_OK; +} + +/* + * Starts processing on the next item in the queue. + */ +void nsZipWriter::BeginProcessingNextItem() { + while (!mQueue.IsEmpty()) { + nsZipQueueItem next = mQueue[0]; + mQueue.RemoveElementAt(0); + + if (next.mOperation == OPERATION_REMOVE) { + int32_t pos = -1; + if (mEntryHash.Get(next.mZipEntry, &pos)) { + if (pos < mHeaders.Count() - 1) { + nsresult rv = BeginProcessingRemoval(pos); + if (NS_FAILED(rv)) FinishQueue(rv); + return; + } + + mCDSOffset = mHeaders[pos]->mOffset; + nsresult rv = SeekCDS(); + if (NS_FAILED(rv)) { + FinishQueue(rv); + return; + } + mEntryHash.Remove(mHeaders[pos]->mName); + mHeaders.RemoveObjectAt(pos); + } else { + FinishQueue(NS_ERROR_FILE_NOT_FOUND); + return; + } + } else if (next.mOperation == OPERATION_ADD) { + if (mEntryHash.Get(next.mZipEntry, nullptr)) { + FinishQueue(NS_ERROR_FILE_ALREADY_EXISTS); + return; + } + + bool complete = false; + nsresult rv = BeginProcessingAddition(&next, &complete); + if (NS_FAILED(rv)) { + SeekCDS(); + FinishQueue(rv); + return; + } + if (!complete) return; + } + } + + FinishQueue(NS_OK); +} + +/* + * Ends processing with the given status. + */ +void nsZipWriter::FinishQueue(nsresult aStatus) { + nsCOMPtr<nsIRequestObserver> observer = mProcessObserver; + // Clean up everything first in case the observer decides to queue more + // things + mProcessObserver = nullptr; + mProcessContext = nullptr; + mInQueue = false; + + if (observer) observer->OnStopRequest(nullptr, aStatus); +} diff --git a/modules/libjar/zipwriter/nsZipWriter.h b/modules/libjar/zipwriter/nsZipWriter.h new file mode 100644 index 0000000000..8ba04c72c7 --- /dev/null +++ b/modules/libjar/zipwriter/nsZipWriter.h @@ -0,0 +1,79 @@ +/* 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/. + */ + +#ifndef _nsZipWriter_h_ +#define _nsZipWriter_h_ + +#include "nsIZipWriter.h" +#include "nsIRequestObserver.h" +#include "nsZipHeader.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "nsTHashMap.h" +#include "mozilla/Attributes.h" + +#define ZIPWRITER_CONTRACTID "@mozilla.org/zipwriter;1" +#define ZIPWRITER_CID \ + { \ + 0x430d416c, 0xa722, 0x4ad1, { \ + 0xbe, 0x98, 0xd9, 0xa4, 0x45, 0xf8, 0x5e, 0x3f \ + } \ + } + +#define OPERATION_ADD 0 +#define OPERATION_REMOVE 1 +struct nsZipQueueItem { + public: + uint32_t mOperation; + nsCString mZipEntry; + nsCOMPtr<nsIFile> mFile; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsIInputStream> mStream; + PRTime mModTime; + int32_t mCompression; + uint32_t mPermissions; +}; + +class nsZipWriter final : public nsIZipWriter, public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIZIPWRITER + NS_DECL_NSIREQUESTOBSERVER + + nsZipWriter(); + nsresult EntryCompleteCallback(nsZipHeader* aHeader, nsresult aStatus); + + private: + ~nsZipWriter(); + + uint32_t mCDSOffset; + bool mCDSDirty; + bool mInQueue; + + nsCOMPtr<nsIFile> mFile; + nsCOMPtr<nsIRequestObserver> mProcessObserver; + nsCOMPtr<nsISupports> mProcessContext; + nsCOMPtr<nsIOutputStream> mStream; + nsCOMArray<nsZipHeader> mHeaders; + nsTArray<nsZipQueueItem> mQueue; + nsTHashMap<nsCStringHashKey, int32_t> mEntryHash; + nsCString mComment; + + nsresult SeekCDS(); + void Cleanup(); + nsresult ReadFile(nsIFile* aFile); + nsresult InternalAddEntryDirectory(const nsACString& aZipEntry, + PRTime aModTime, uint32_t aPermissions); + nsresult BeginProcessingAddition(nsZipQueueItem* aItem, bool* complete); + nsresult BeginProcessingRemoval(int32_t aPos); + nsresult AddEntryStream(const nsACString& aZipEntry, PRTime aModTime, + int32_t aCompression, nsIInputStream* aStream, + bool aQueue, uint32_t aPermissions); + void BeginProcessingNextItem(); + void FinishQueue(nsresult aStatus); +}; + +#endif diff --git a/modules/libjar/zipwriter/test/unit/data/emptyfile.txt b/modules/libjar/zipwriter/test/unit/data/emptyfile.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/emptyfile.txt diff --git a/modules/libjar/zipwriter/test/unit/data/smallfile.txt b/modules/libjar/zipwriter/test/unit/data/smallfile.txt new file mode 100644 index 0000000000..e068fcf81d --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/smallfile.txt @@ -0,0 +1 @@ +Small (16 bytes)
\ No newline at end of file diff --git a/modules/libjar/zipwriter/test/unit/data/test.png b/modules/libjar/zipwriter/test/unit/data/test.png Binary files differnew file mode 100644 index 0000000000..c648f7299d --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test.png diff --git a/modules/libjar/zipwriter/test/unit/data/test.txt b/modules/libjar/zipwriter/test/unit/data/test.txt new file mode 100644 index 0000000000..1040981a30 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test.txt @@ -0,0 +1,5 @@ +This is a test text file for the zipwriter component. +It will be made available in the unit test directory. +It will also be compressed into a testcase zip file +made by a 3rd party zip tool to test the opening of +existing zip files. diff --git a/modules/libjar/zipwriter/test/unit/data/test.zip b/modules/libjar/zipwriter/test/unit/data/test.zip Binary files differnew file mode 100644 index 0000000000..96581fe8b5 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test.zip diff --git a/modules/libjar/zipwriter/test/unit/data/test_bug399727.html b/modules/libjar/zipwriter/test/unit/data/test_bug399727.html new file mode 100644 index 0000000000..a6556274f1 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test_bug399727.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Content-Language" content="en-gb"><meta http-equiv="Content-Type" content="text/html; charset=windows-1252"><meta name="generator" content="Fog Creek CityDesk 2.0.19"><meta name="citydesk" content="BB40F561/251"><title>TK's Sleater-Kinney pages</title><style type="text/css">
+<!--
+body {
+ font-family: verdana, arial;
+ font-size: 10pt;
+ color: black;
+ background-color: #D0E8E8;
+}
+td {
+ font-family: verdana, arial;
+ font-size: 10pt;
+}
+hr {
+ color: #808080;
+ height: 1px;
+}
+caption {
+ font-family: verdana, arial;
+ font-size: 8pt;
+ color: #808080;
+ background-color: white;
+}
+.bigText {
+ font-family: verdana, arial;
+ font-size: 18pt;
+ font-weight: bold;
+ color: black;
+ padding-bottom: 10px;
+}
+.smallText {
+ font-family: verdana, arial;
+ font-size: 7pt;
+ color: #808080;
+}
+.tabText {
+ font-family: Courier, "Courier New", monospace;
+ font-size: 7pt;
+ color: #808080;
+}
+.emphasis
+{
+ font-weight: bold;
+ color: #000000;
+}
+.articleHeader {
+ padding-bottom: 4px;
+ border-bottom: #808080 dotted 1px;
+}
+.headerCell {
+ background-color: #689090;
+ color: white;
+ padding: 4px;
+}
+.navbarText {
+ color: white;
+ font-weight: bold;
+}
+.articleTable {
+ border: #808080 solid 1px;
+ background-color: white;
+ padding: 15px;
+}
+
+
+
+-->
+</style></head><body>
+
+
+
+
+
+
+
+
+
+
+<div align="center">
+ <center>
+ <table border="0" cellpadding="0" cellspacing="0" width="700">
+ <tbody><tr>
+ <td><p class="bigText">TK's Sleater-Kinney pages</p>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top">
+ <table class="articleTable" border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tbody><tr>
+ <td class="headerCell" align="right"><p class="navbarText">
+ : <a class="navbarText" href="http://tk-jk.net/S-K/index.html"> <span class="1home">Home</span></a>
+ : <a class="navbarText" href="http://tk-jk.net/S-K/blog/general/Archive.html"> <span class="2archive">Archive</span></a>
+ : <a class="navbarText" href="http://tk-jk.net/S-K/blog/general/Tabs.html"> <span class="3tabs">Tabs</span></a>
+ : <a class="navbarText" href="http://tk-jk.net/S-K/blog/general/Drumtabs.html"> <span class="4drumtabs">Drum tabs</span></a>
+ : <a class="navbarText" href="http://tk-jk.net/S-K/blog/general/SetLists.html"> <span class="5setlists">Set Lists</span></a>
+ : <a class="navbarText" href="http://tk-jk.net/S-K/blog/general/About.html"> <span class="6about">About</span></a> :
+</p>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top">
+ <div align="left">
+ <table border="0" cellpadding="0" cellspacing="0" width="100%">
+ <tbody><tr>
+ <td width="100%">
+<p class="bigText"><strong>All Hands on the Bad One</strong>
+</p>
+<p></p><p><strong>All Hands on the Bad One, <em>"All Hands on the Bad One"</em></strong></p>
+<p dir="ltr">This, followed by <a href="http://tk-jk.net/S-K/Tabs/YouthDecay.html">Youth Decay</a> - wow - most bands would be happy just to have just one of these songs. S-K has a boatload.</p>
+<p dir="ltr">Tabbing this was very frustrating. Riff 3 is a
+masterpiece of guitar and vocal harmony, rhythm and guitar
+interplay. It has a bass intro and stacks guitars and vocals on
+top. This is the definition of Sleater-Kinney magic. You can't get
+it all done on two guitars. You can get close. I
+haven't done it justice.</p>
+<p dir="ltr">As usual the guitar 2's rhythms are vital to the S-K sound. It's still good if you don't get it just right.</p>
+<p dir="ltr">I sure hope I've done this well enough for you to figure out.</p>
+<ul dir="ltr">
+<li>
+<div style="margin-right: 0px;">Quarter note = 153 or so.</div>
+</li><li>
+<div style="margin-right: 0px;">Standard tuning. But tuning down 3 half steps makes Riff 3 easier and more authentic.</div>
+</li><li>
+<div style="margin-right: 0px;">Tabbed where it felt good on the fretboard but there are alternatives.</div>
+</li><li>
+<div style="margin-right: 0px;">Tuning down is required for authenticity but it sounds great in standard tuning</div></li></ul><strong>
+</strong><p dir="ltr"><strong><br>1:</strong></p>
+<p></p><span><img alt="All Hands on the Bad One Riff 1" src="AllHandsontheBadOne_files/AHOTBO1.jpg" cd:pos="2" align="left" border="0" height="500" hspace="4" width="580"><br clear="all"></span>
+<p><strong></strong> </p>
+<p><strong>2:</strong></p><span><img alt="All Hands on the Bad One Riff 2" src="AllHandsontheBadOne_files/AHOTBO2.jpg" cd:pos="2" align="left" border="0" height="257" hspace="4" width="580"><br clear="all"></span>
+<p><strong></strong> </p>
+<p><strong>Riff 3 (3a and 3b) is a little masterpeice:</strong></p><span><img alt="All Hands on the Bad One Riff 3a" src="AllHandsontheBadOne_files/AHOTBO3a.jpg" cd:pos="2" align="left" border="0" height="446" hspace="4" width="580"><br clear="all"></span>
+<p><span><img alt="All Hands on the Bad One Riff 3b" src="AllHandsontheBadOne_files/AHOTBO3b.jpg" cd:pos="2" align="left" border="0" height="179" hspace="4" width="580"><br clear="all"></span></p>
+<p> </p>
+<p>The Outro</p>
+<p><span><img alt="All Hands on the Bad One Riff 4" src="AllHandsontheBadOne_files/AHOTBO4.jpg" cd:pos="2" align="left" border="0" height="216" hspace="4" width="580"><br clear="all"></span></p><p></p>
+ </td>
+ </tr>
+ </tbody></table>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top">
+ <hr>
+ <center>
+ <div class="smallText">
+ The views expressed within this site pretty much represent those of the author.<br>
+ Copyright (c) 2002 <a href="http://tk-jk.net/">Terry Kearns</a>. All rights reserved.
+ </div>
+ </center>
+ </td>
+ </tr>
+ </tbody></table>
+ </td>
+ </tr>
+ </tbody></table>
+ </center>
+</div>
+</body></html>
\ No newline at end of file diff --git a/modules/libjar/zipwriter/test/unit/data/test_bug399727.zlib b/modules/libjar/zipwriter/test/unit/data/test_bug399727.zlib Binary files differnew file mode 100644 index 0000000000..080b63c520 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test_bug399727.zlib diff --git a/modules/libjar/zipwriter/test/unit/data/test_bug446708/thumbs/st14-1.tiff b/modules/libjar/zipwriter/test/unit/data/test_bug446708/thumbs/st14-1.tiff Binary files differnew file mode 100644 index 0000000000..6b04657580 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test_bug446708/thumbs/st14-1.tiff diff --git a/modules/libjar/zipwriter/test/unit/data/test_bug717061.gz b/modules/libjar/zipwriter/test/unit/data/test_bug717061.gz Binary files differnew file mode 100644 index 0000000000..f990c6e519 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test_bug717061.gz diff --git a/modules/libjar/zipwriter/test/unit/data/test_bug717061.html b/modules/libjar/zipwriter/test/unit/data/test_bug717061.html new file mode 100644 index 0000000000..80ce0319b4 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/data/test_bug717061.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Content-Language" content="en-us"><title>Lorem Ipsum</title><style type="text/css"> +body { + font-family: verdana, arial; + font-size: 10pt; + color: black; + background-color: #D0E8E8; +} +</style></head><body> +<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> + +<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p> + +<p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.</p> + +</body></html> diff --git a/modules/libjar/zipwriter/test/unit/head_zipwriter.js b/modules/libjar/zipwriter/test/unit/head_zipwriter.js new file mode 100644 index 0000000000..43136d6cec --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/head_zipwriter.js @@ -0,0 +1,60 @@ +/* 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/. + */ + +const NS_ERROR_IN_PROGRESS = 2152398863; + +const PR_RDONLY = 0x01; +const PR_WRONLY = 0x02; +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_APPEND = 0x10; +const PR_TRUNCATE = 0x20; +const PR_SYNC = 0x40; +const PR_EXCL = 0x80; + +const ZIP_EOCDR_HEADER_SIZE = 22; +const ZIP_FILE_HEADER_SIZE = 30; +const ZIP_CDS_HEADER_SIZE = 46; +const ZIP_METHOD_STORE = 0; +const ZIP_METHOD_DEFLATE = 8; +const ZIP_EXTENDED_TIMESTAMP_SIZE = 9; + +const PR_USEC_PER_MSEC = 1000; +const PR_USEC_PER_SEC = 1000000; +const PR_MSEC_PER_SEC = 1000; + +const DATA_DIR = "data/"; + +var ioSvc = Services.io; + +var ZipWriter = Components.Constructor( + "@mozilla.org/zipwriter;1", + "nsIZipWriter" +); +var ZipReader = Components.Constructor( + "@mozilla.org/libjar/zip-reader;1", + "nsIZipReader", + "open" +); + +var tmpDir = do_get_profile(); +var tmpFile = tmpDir.clone(); +tmpFile.append("zipwriter-test.zip"); +if (tmpFile.exists()) { + tmpFile.remove(true); +} + +var zipW = new ZipWriter(); + +registerCleanupFunction(function () { + try { + zipW.close(); + } catch (e) { + // Just ignore a failure here and attempt to delete the file anyway. + } + if (tmpFile.exists()) { + tmpFile.remove(true); + } +}); diff --git a/modules/libjar/zipwriter/test/unit/test_alignment.js b/modules/libjar/zipwriter/test/unit/test_alignment.js new file mode 100644 index 0000000000..afc5cf2b30 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_alignment.js @@ -0,0 +1,117 @@ +/* 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/. + */ + +const DATA = "ZIP WRITER TEST DATA"; +const FILENAME = "test_data.txt"; +const time = 1199145600000; // Jan 1st 2008 + +var TESTS = [ + { + name: "test.txt", + compression: Ci.nsIZipWriter.COMPRESSION_DEFAULT, + }, + { + name: "test.png", + compression: Ci.nsIZipWriter.COMPRESSION_NONE, + }, +]; + +function swap16(n) { + return (((n >> 8) & 0xff) << 0) | (((n >> 0) & 0xff) << 8); +} + +function swap32(n) { + return ( + (((n >> 24) & 0xff) << 0) | + (((n >> 16) & 0xff) << 8) | + (((n >> 8) & 0xff) << 16) | + (((n >> 0) & 0xff) << 24) + ); +} + +function move_to_data(bis, offset) { + bis.readBytes(18); // Move to compressed size + var size = swap32(bis.read32()); + bis.readBytes(4); + var file_len = swap16(bis.read16()); + var extra_len = swap16(bis.read16()); + bis.readBytes(file_len); + bis.readBytes(extra_len); + offset += ZIP_FILE_HEADER_SIZE + file_len + extra_len; + + return { offset, size }; +} + +function test_alignment(align_size) { + // Create zip for testing. + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + for (var i = 0; i < TESTS.length; i++) { + var source = do_get_file(DATA_DIR + TESTS[i].name); + zipW.addEntryFile(TESTS[i].name, TESTS[i].compression, source, false); + } + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(DATA, DATA.length); + zipW.addEntryStream( + FILENAME, + time * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + stream, + false + ); + zipW.alignStoredFiles(align_size); + zipW.close(); + + // Check data can be decompressed. + var zipR = new ZipReader(tmpFile); + stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(zipR.getInputStream(FILENAME)); + var result = stream.read(DATA.length); + Assert.equal(result, DATA); + stream.close(); + zipR.close(); + + // Check data is correct and aligned. + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(tmpFile, -1, -1, null); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(fis); + var offset = 0; + + var ret = move_to_data(bis, offset); // "test.txt" + offset = ret.offset; + bis.readBytes(ret.size); + offset += ret.size; + + ret = move_to_data(bis, offset); // "test.png" + offset = ret.offset; + Assert.equal(offset % align_size, 0); + bis.readBytes(ret.size); + offset += ret.size; + + ret = move_to_data(bis, offset); // "test_data.txt" + offset = ret.offset; + result = bis.readBytes(DATA.length); + Assert.equal(result, DATA); + Assert.equal(offset % align_size, 0); + + fis.close(); + bis.close(); +} + +function run_test() { + test_alignment(2); + test_alignment(4); + test_alignment(16); + test_alignment(4096); + test_alignment(32768); +} diff --git a/modules/libjar/zipwriter/test/unit/test_asyncadd.js b/modules/libjar/zipwriter/test/unit/test_asyncadd.js new file mode 100644 index 0000000000..ef70d292d7 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_asyncadd.js @@ -0,0 +1,111 @@ +/* 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/. + */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +// Values taken from using zipinfo to list the test.zip contents +var TESTS = [ + { + name: "test.txt", + size: 232, + crc: 0x0373ac26, + }, + { + name: "test.png", + size: 3402, + crc: 0x504a5c30, + }, +]; + +var size = 0; + +var observer = { + onStartRequest(request) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + + zipW.close(); + size += ZIP_EOCDR_HEADER_SIZE; + + Assert.equal(size, tmpFile.fileSize); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + + for (var i = 0; i < TESTS.length; i++) { + var source = do_get_file(DATA_DIR + TESTS[i].name); + for (let method in methods) { + var entryName = method + "/" + TESTS[i].name; + Assert.ok(zipR.hasEntry(entryName)); + + var entry = zipR.getEntry(entryName); + Assert.equal(entry.realSize, TESTS[i].size); + Assert.equal(entry.size, TESTS[i].size); + Assert.equal(entry.CRC32, TESTS[i].crc); + Assert.equal( + Math.floor(entry.lastModifiedTime / PR_USEC_PER_SEC), + Math.floor(source.lastModifiedTime / PR_MSEC_PER_SEC) + ); + + zipR.test(entryName); + } + } + + zipR.close(); + do_test_finished(); + }, +}; + +var methods = { + file: function method_file(entry, source) { + zipW.addEntryFile(entry, Ci.nsIZipWriter.COMPRESSION_NONE, source, true); + }, + channel: function method_channel(entry, source) { + zipW.addEntryChannel( + entry, + source.lastModifiedTime * PR_MSEC_PER_SEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + NetUtil.newChannel({ + uri: ioSvc.newFileURI(source), + loadUsingSystemPrincipal: true, + }), + true + ); + }, + stream: function method_stream(entry, source) { + zipW.addEntryStream( + entry, + source.lastModifiedTime * PR_MSEC_PER_SEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + NetUtil.newChannel({ + uri: ioSvc.newFileURI(source), + loadUsingSystemPrincipal: true, + }).open(), + true + ); + }, +}; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + for (var i = 0; i < TESTS.length; i++) { + var source = do_get_file(DATA_DIR + TESTS[i].name); + for (let method in methods) { + var entry = method + "/" + TESTS[i].name; + methods[method](entry, source); + size += + ZIP_FILE_HEADER_SIZE + + ZIP_CDS_HEADER_SIZE + + ZIP_EXTENDED_TIMESTAMP_SIZE * 2 + + entry.length * 2 + + TESTS[i].size; + } + } + do_test_pending(); + zipW.processQueue(observer, null); + Assert.ok(zipW.inQueue); +} diff --git a/modules/libjar/zipwriter/test/unit/test_asyncbadadd.js b/modules/libjar/zipwriter/test/unit/test_asyncbadadd.js new file mode 100644 index 0000000000..7d1c4ec610 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_asyncbadadd.js @@ -0,0 +1,31 @@ +/* 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/. + */ + +const FILENAME = "missing.txt"; + +var observer = { + onStartRequest(request) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_ERROR_FILE_NOT_FOUND); + zipW.close(); + Assert.equal(ZIP_EOCDR_HEADER_SIZE, tmpFile.fileSize); + do_test_finished(); + }, +}; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + var source = tmpDir.clone(); + source.append(FILENAME); + zipW.addEntryFile(FILENAME, Ci.nsIZipWriter.COMPRESSION_NONE, source, true); + + do_test_pending(); + zipW.processQueue(observer, null); + + // With nothing to actually do the queue would have completed immediately + Assert.ok(!zipW.inQueue); +} diff --git a/modules/libjar/zipwriter/test/unit/test_asyncbadremove.js b/modules/libjar/zipwriter/test/unit/test_asyncbadremove.js new file mode 100644 index 0000000000..623bc46c59 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_asyncbadremove.js @@ -0,0 +1,27 @@ +/* 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/. + */ + +const FILENAME = "missing.txt"; + +var observer = { + onStartRequest(request) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_ERROR_FILE_NOT_FOUND); + zipW.close(); + Assert.equal(ZIP_EOCDR_HEADER_SIZE, tmpFile.fileSize); + do_test_finished(); + }, +}; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + zipW.removeEntry(FILENAME, true); + do_test_pending(); + zipW.processQueue(observer, null); + + // With nothing to actually do the queue would have completed immediately + Assert.ok(!zipW.inQueue); +} diff --git a/modules/libjar/zipwriter/test/unit/test_asyncremove.js b/modules/libjar/zipwriter/test/unit/test_asyncremove.js new file mode 100644 index 0000000000..7342e18e4f --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_asyncremove.js @@ -0,0 +1,40 @@ +/* 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/. + */ + +var TESTS = ["test.txt", "test.png"]; + +var observer = { + onStartRequest(request) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + + zipW.close(); + + // Empty zip file should just be the end of central directory marker + var newTmpFile = tmpFile.clone(); + Assert.equal(newTmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE); + do_test_finished(); + }, +}; + +function run_test() { + // Copy our test zip to the tmp dir so we can modify it + var testzip = do_get_file(DATA_DIR + "test.zip"); + testzip.copyTo(tmpDir, tmpFile.leafName); + + Assert.ok(tmpFile.exists()); + + zipW.open(tmpFile, PR_RDWR); + + for (var i = 0; i < TESTS.length; i++) { + Assert.ok(zipW.hasEntry(TESTS[i])); + zipW.removeEntry(TESTS[i], true); + } + + do_test_pending(); + zipW.processQueue(observer, null); + Assert.ok(zipW.inQueue); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug399727.js b/modules/libjar/zipwriter/test/unit/test_bug399727.js new file mode 100644 index 0000000000..2967f69737 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug399727.js @@ -0,0 +1,107 @@ +/* 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/. + */ + +function BinaryComparer(file, callback) { + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + this.length = file.fileSize; + this.fileStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + this.fileStream.setInputStream(fstream); + this.offset = 0; + this.callback = callback; +} + +BinaryComparer.prototype = { + fileStream: null, + offset: null, + length: null, + callback: null, + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + this.fileStream.close(); + Assert.equal(aStatusCode, Cr.NS_OK); + Assert.equal(this.offset, this.length); + this.callback(); + }, + + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + stream.setInputStream(aInputStream); + var source, actual; + for (var i = 0; i < aCount; i++) { + try { + source = this.fileStream.read8(); + } catch (e) { + do_throw("Unable to read from file at offset " + this.offset + " " + e); + } + try { + actual = stream.read8(); + } catch (e) { + do_throw( + "Unable to read from converted stream at offset " + + this.offset + + " " + + e + ); + } + if (source != actual) { + do_throw( + "Invalid value " + + actual + + " at offset " + + this.offset + + ", should have been " + + source + ); + } + this.offset++; + } + }, +}; + +function comparer_callback() { + do_test_finished(); +} + +function run_test() { + var source = do_get_file(DATA_DIR + "test_bug399727.html"); + var comparer = new BinaryComparer( + do_get_file(DATA_DIR + "test_bug399727.zlib"), + comparer_callback + ); + + // Prepare the stream converter + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData( + "uncompressed", + "deflate", + comparer, + null + ); + + // Open the expected output file + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(source, -1, 0, 0); + + // Set up a pump to push data from the file to the stream converter + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(fstream, 0, 0, true); + pump.asyncRead(converter); + do_test_pending(); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug419769_1.js b/modules/libjar/zipwriter/test/unit/test_bug419769_1.js new file mode 100644 index 0000000000..0e4e70fe01 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug419769_1.js @@ -0,0 +1,75 @@ +/* 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/. + */ + +const DATA = ""; +const FILENAME = "test.txt"; +const CRC = 0x00000000; +// XXX Must use a constant time here away from DST changes. See bug 402434. +const time = 1199145600000; // Jan 1st 2008 + +function testpass(source) { + // Should exist. + Assert.ok(source.hasEntry(FILENAME)); + + var entry = source.getEntry(FILENAME); + Assert.notEqual(entry, null); + + Assert.ok(!entry.isDirectory); + + // Should be stored + Assert.equal(entry.compression, ZIP_METHOD_DEFLATE); + + Assert.equal(entry.lastModifiedTime / PR_USEC_PER_MSEC, time); + + // File size should match our data size. + Assert.equal(entry.realSize, DATA.length); + + // Check that the CRC is accurate + Assert.equal(entry.CRC32, CRC); +} + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + // Shouldn't be there to start with. + Assert.ok(!zipW.hasEntry(FILENAME)); + + Assert.ok(!zipW.inQueue); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(DATA, DATA.length); + zipW.addEntryStream( + FILENAME, + time * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_BEST, + stream, + false + ); + + // Check that zip state is right at this stage. + testpass(zipW); + zipW.close(); + + // Check to see if we get the same results loading afresh. + zipW.open(tmpFile, PR_RDWR); + testpass(zipW); + zipW.close(); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + testpass(zipR); + zipR.test(FILENAME); + stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(zipR.getInputStream(FILENAME)); + var result = stream.read(DATA.length); + stream.close(); + zipR.close(); + + Assert.equal(result, DATA); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug419769_2.js b/modules/libjar/zipwriter/test/unit/test_bug419769_2.js new file mode 100644 index 0000000000..9841f52d2e --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug419769_2.js @@ -0,0 +1,62 @@ +/* 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/. + */ + +const DATA = ""; +const FILENAME = "test.txt"; +const CRC = 0x00000000; + +function testpass(source) { + // Should exist. + Assert.ok(source.hasEntry(FILENAME)); + + var entry = source.getEntry(FILENAME); + Assert.notEqual(entry, null); + + Assert.ok(!entry.isDirectory); + + // Should be stored + Assert.equal(entry.compression, ZIP_METHOD_DEFLATE); + + // File size should match our data size. + Assert.equal(entry.realSize, DATA.length); + + // Check that the CRC is accurate + Assert.equal(entry.CRC32, CRC); +} + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + // Shouldn't be there to start with. + Assert.ok(!zipW.hasEntry(FILENAME)); + + Assert.ok(!zipW.inQueue); + + var file = do_get_file(DATA_DIR + "emptyfile.txt"); + zipW.addEntryFile(FILENAME, Ci.nsIZipWriter.COMPRESSION_BEST, file, false); + + // Check that zip state is right at this stage. + testpass(zipW); + zipW.close(); + + // Check to see if we get the same results loading afresh. + zipW.open(tmpFile, PR_RDWR); + testpass(zipW); + zipW.close(); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + testpass(zipR); + zipR.test(FILENAME); + var stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(zipR.getInputStream(FILENAME)); + var result = stream.read(DATA.length); + stream.close(); + zipR.close(); + + Assert.equal(result, DATA); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug425768.js b/modules/libjar/zipwriter/test/unit/test_bug425768.js new file mode 100644 index 0000000000..d3a9b30ba6 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug425768.js @@ -0,0 +1,33 @@ +/* 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/. + */ + +const DIRNAME = "test/"; +const time = Date.now(); + +function run_test() { + // Copy in the test file. + var source = do_get_file("data/test.zip"); + source.copyTo(tmpFile.parent, tmpFile.leafName); + + // Open it and add something so the CDS is rewritten. + zipW.open(tmpFile, PR_RDWR | PR_APPEND); + zipW.addEntryDirectory(DIRNAME, time * PR_USEC_PER_MSEC, false); + Assert.ok(zipW.hasEntry(DIRNAME)); + zipW.close(); + + var zipR = new ZipReader(tmpFile); + Assert.ok(zipR.hasEntry(DIRNAME)); + zipR.close(); + + // Adding the directory would have added a fixed amount to the file size. + // Any difference suggests the CDS was written out incorrectly. + var extra = + ZIP_FILE_HEADER_SIZE + + ZIP_CDS_HEADER_SIZE + + DIRNAME.length * 2 + + ZIP_EXTENDED_TIMESTAMP_SIZE * 2; + + Assert.equal(source.fileSize + extra, tmpFile.fileSize); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug433248.js b/modules/libjar/zipwriter/test/unit/test_bug433248.js new file mode 100644 index 0000000000..7b8c0525b8 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug433248.js @@ -0,0 +1,68 @@ +/* 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/. + */ + +function run_test() { + // zipW is an uninitialised zipwriter at this point. + try { + zipW.file; + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.comment; + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.comment = "test"; + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.addEntryDirectory("test", 0, false); + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.addEntryFile( + "test", + Ci.nsIZipWriter.COMPRESSION_DEFAULT, + tmpDir, + false + ); + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.removeEntry("test", false); + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.processQueue(null, null); + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + try { + zipW.close(); + do_throw("Should have thrown uninitialized error."); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug446708.js b/modules/libjar/zipwriter/test/unit/test_bug446708.js new file mode 100644 index 0000000000..cff67d848a --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug446708.js @@ -0,0 +1,39 @@ +function run_test() { + var testBundle = do_get_file("data/test_bug446708"); + + RecursivelyZipDirectory(testBundle); +} + +// Add |file| to the zip. |path| is the current path for the file. +function AddToZip(zipWriter, path, file) { + var currentPath = path + file.leafName; + + if (file.isDirectory()) { + currentPath += "/"; + } + + // THIS IS WHERE THE ERROR OCCURS, FOR THE FILE "st14-1.tiff" IN "test_bug446708" + zipWriter.addEntryFile( + currentPath, + Ci.nsIZipWriter.COMPRESSION_DEFAULT, + file, + false + ); + + // if it's a dir, continue adding its contents recursively... + if (file.isDirectory()) { + var entries = file.QueryInterface(Ci.nsIFile).directoryEntries; + while (entries.hasMoreElements()) { + var entry = entries.nextFile; + AddToZip(zipWriter, currentPath, entry); + } + } + + // ...otherwise, we're done +} + +function RecursivelyZipDirectory(bundle) { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + AddToZip(zipW, "", bundle); + zipW.close(); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug467740.js b/modules/libjar/zipwriter/test/unit/test_bug467740.js new file mode 100644 index 0000000000..4108003bd3 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug467740.js @@ -0,0 +1,35 @@ +/* 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/. + */ + +function run_test() { + // In this test we try to open some files that aren't archives: + // - An empty file, that is certainly not an archive. + // - A file that couldn't be mistaken for archive, since it is too small. + // - A file that could be mistaken for archive, if we checked only the file + // size, but is invalid since it contains no ZIP signature. + var invalidArchives = ["emptyfile.txt", "smallfile.txt", "test.png"]; + + invalidArchives.forEach(function (invalidArchive) { + // Get a reference to the invalid file + var invalidFile = do_get_file(DATA_DIR + invalidArchive); + + // Opening the invalid file should fail (but not crash) + try { + zipW.open(invalidFile, PR_RDWR); + do_throw( + "Should have thrown NS_ERROR_FILE_CORRUPTED on " + invalidArchive + " !" + ); + } catch (e) { + if ( + !( + e instanceof Ci.nsIException && e.result == Cr.NS_ERROR_FILE_CORRUPTED + ) + ) { + throw e; + } + // do nothing + } + }); +} diff --git a/modules/libjar/zipwriter/test/unit/test_bug717061.js b/modules/libjar/zipwriter/test/unit/test_bug717061.js new file mode 100644 index 0000000000..cb3faa3aa7 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_bug717061.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0 + */ + +function BinaryComparer(file, callback) { + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + this.length = file.fileSize; + this.fileStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + this.fileStream.setInputStream(fstream); + this.offset = 0; + this.callback = callback; +} + +BinaryComparer.prototype = { + fileStream: null, + offset: null, + length: null, + callback: null, + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + this.fileStream.close(); + Assert.equal(aStatusCode, Cr.NS_OK); + Assert.equal(this.offset, this.length); + this.callback(); + }, + + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + stream.setInputStream(aInputStream); + var source, actual; + for (var i = 0; i < aCount; i++) { + try { + source = this.fileStream.read8(); + } catch (e) { + do_throw("Unable to read from file at offset " + this.offset + " " + e); + } + try { + actual = stream.read8(); + } catch (e) { + do_throw( + "Unable to read from converted stream at offset " + + this.offset + + " " + + e + ); + } + // The byte at offset 9 is the OS byte (see RFC 1952, section 2.3), which + // can legitimately differ when the source is compressed on different + // operating systems. The actual .gz for this test was created on a Unix + // system, but we want the test to work correctly everywhere. So ignore + // the byte at offset 9. + if (this.offset != 9 && source != actual) { + do_throw( + "Invalid value " + + actual + + " at offset " + + this.offset + + ", should have been " + + source + ); + } + this.offset++; + } + }, +}; + +function comparer_callback() { + do_test_finished(); +} + +function run_test() { + var source = do_get_file(DATA_DIR + "test_bug717061.html"); + var comparer = new BinaryComparer( + do_get_file(DATA_DIR + "test_bug717061.gz"), + comparer_callback + ); + + // Prepare the stream converter + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData("uncompressed", "gzip", comparer, null); + + // Open the expected output file + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(source, -1, 0, 0); + + // Set up a pump to push data from the file to the stream converter + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(fstream, 0, 0, true); + pump.asyncRead(converter); + do_test_pending(); +} diff --git a/modules/libjar/zipwriter/test/unit/test_createempty.js b/modules/libjar/zipwriter/test/unit/test_createempty.js new file mode 100644 index 0000000000..464c3b243f --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_createempty.js @@ -0,0 +1,15 @@ +/* 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/. + */ + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + zipW.close(); + + // Should have created a zip file + Assert.ok(tmpFile.exists()); + + // Empty zip file should just be the end of central directory marker + Assert.equal(tmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE); +} diff --git a/modules/libjar/zipwriter/test/unit/test_deflatedata.js b/modules/libjar/zipwriter/test/unit/test_deflatedata.js new file mode 100644 index 0000000000..a8a5a0c536 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_deflatedata.js @@ -0,0 +1,59 @@ +/* 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/. + */ + +const DATA = "ZIP WRITER TEST DATA"; +const FILENAME = "test.txt"; +const CRC = 0xe6164331; +// XXX Must use a constant time here away from DST changes. See bug 402434. +const time = 1199145600000; // Jan 1st 2008 + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + // Shouldn't be there to start with. + Assert.ok(!zipW.hasEntry(FILENAME)); + + Assert.ok(!zipW.inQueue); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(DATA, DATA.length); + zipW.addEntryStream( + FILENAME, + time * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_BEST, + stream, + false + ); + + var entry = zipW.getEntry(FILENAME); + + Assert.ok(entry != null); + + // Check entry seems right. + Assert.equal(entry.compression, ZIP_METHOD_DEFLATE); + Assert.equal(entry.CRC32, CRC); + Assert.equal(entry.realSize, DATA.length); + Assert.equal(entry.lastModifiedTime / PR_USEC_PER_MSEC, time); + + zipW.close(); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + Assert.ok(zipR.hasEntry(FILENAME)); + + zipR.test(FILENAME); + + stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(zipR.getInputStream(FILENAME)); + var result = stream.read(DATA.length); + stream.close(); + zipR.close(); + + Assert.equal(result, DATA); +} diff --git a/modules/libjar/zipwriter/test/unit/test_directory.js b/modules/libjar/zipwriter/test/unit/test_directory.js new file mode 100644 index 0000000000..70e93bbf08 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_directory.js @@ -0,0 +1,26 @@ +/* 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/. + */ + +const DIRNAME1 = "test"; +const DIRNAME1_CORRECT = "test/"; +const DIRNAME2 = "test2/"; +const time = Date.now(); + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + zipW.addEntryDirectory(DIRNAME1, time * PR_USEC_PER_MSEC, false); + Assert.ok(!zipW.hasEntry(DIRNAME1)); + Assert.ok(zipW.hasEntry(DIRNAME1_CORRECT)); + var entry = zipW.getEntry(DIRNAME1_CORRECT); + Assert.ok(entry.isDirectory); + + zipW.addEntryDirectory(DIRNAME2, time * PR_USEC_PER_MSEC, false); + Assert.ok(zipW.hasEntry(DIRNAME2)); + entry = zipW.getEntry(DIRNAME2); + Assert.ok(entry.isDirectory); + + zipW.close(); +} diff --git a/modules/libjar/zipwriter/test/unit/test_editexisting.js b/modules/libjar/zipwriter/test/unit/test_editexisting.js new file mode 100644 index 0000000000..c58447260e --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_editexisting.js @@ -0,0 +1,60 @@ +/* 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/. + */ + +// Values taken from using zipinfo to list the test.zip contents +var TESTS = [ + { + name: "test.txt", + size: 232, + crc: 0x0373ac26, + time: Date.UTC(2007, 4, 1, 20, 44, 55), + }, + { + name: "test.png", + size: 3402, + crc: 0x504a5c30, + time: Date.UTC(2007, 4, 1, 20, 49, 39), + }, +]; +var BADENTRY = "unknown.txt"; + +function run_test() { + // Copy our test zip to the tmp dir so we can modify it + var testzip = do_get_file(DATA_DIR + "test.zip"); + testzip.copyTo(tmpDir, tmpFile.leafName); + + Assert.ok(tmpFile.exists()); + + zipW.open(tmpFile, PR_RDWR); + + for (let i = 0; i < TESTS.length; i++) { + Assert.ok(zipW.hasEntry(TESTS[i].name)); + var entry = zipW.getEntry(TESTS[i].name); + Assert.ok(entry != null); + + Assert.equal(entry.realSize, TESTS[i].size); + Assert.equal(entry.CRC32, TESTS[i].crc); + Assert.equal(entry.lastModifiedTime / PR_USEC_PER_MSEC, TESTS[i].time); + } + + try { + zipW.removeEntry(BADENTRY, false); + do_throw("shouldn't be able to remove an entry that doesn't exist"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_NOT_FOUND); + } + + for (let i = 0; i < TESTS.length; i++) { + zipW.removeEntry(TESTS[i].name, false); + } + + zipW.close(); + + // Certain platforms cache the file size so get a fresh file to check. + tmpFile = tmpFile.clone(); + + // Empty zip file should just be the end of central directory marker + Assert.equal(tmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE); +} diff --git a/modules/libjar/zipwriter/test/unit/test_storedata.js b/modules/libjar/zipwriter/test/unit/test_storedata.js new file mode 100644 index 0000000000..8983f7be42 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_storedata.js @@ -0,0 +1,87 @@ +/* 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/. + */ + +const DATA = "ZIP WRITER TEST DATA"; +const FILENAME = "test.txt"; +const CRC = 0xe6164331; +// XXX Must use a constant time here away from DST changes. See bug 402434. +const time = 1199145600000; // Jan 1st 2008 + +function testpass(source) { + // Should exist. + Assert.ok(source.hasEntry(FILENAME)); + + var entry = source.getEntry(FILENAME); + Assert.notEqual(entry, null); + + Assert.ok(!entry.isDirectory); + + // Should be stored + Assert.equal(entry.compression, ZIP_METHOD_STORE); + + Assert.equal(entry.lastModifiedTime / PR_USEC_PER_MSEC, time); + + // File size should match our data size. + Assert.equal(entry.realSize, DATA.length); + // When stored sizes should match. + Assert.equal(entry.size, entry.realSize); + + // Check that the CRC is accurate + Assert.equal(entry.CRC32, CRC); +} + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + // Shouldn't be there to start with. + Assert.ok(!zipW.hasEntry(FILENAME)); + + Assert.ok(!zipW.inQueue); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(DATA, DATA.length); + zipW.addEntryStream( + FILENAME, + time * PR_USEC_PER_MSEC, + Ci.nsIZipWriter.COMPRESSION_NONE, + stream, + false + ); + + // Check that zip state is right at this stage. + testpass(zipW); + zipW.close(); + + Assert.equal( + tmpFile.fileSize, + DATA.length + + ZIP_FILE_HEADER_SIZE + + ZIP_CDS_HEADER_SIZE + + ZIP_EXTENDED_TIMESTAMP_SIZE * 2 + + FILENAME.length * 2 + + ZIP_EOCDR_HEADER_SIZE + ); + + // Check to see if we get the same results loading afresh. + zipW.open(tmpFile, PR_RDWR); + testpass(zipW); + zipW.close(); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + testpass(zipR); + zipR.test(FILENAME); + stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(zipR.getInputStream(FILENAME)); + var result = stream.read(DATA.length); + stream.close(); + zipR.close(); + + Assert.equal(result, DATA); +} diff --git a/modules/libjar/zipwriter/test/unit/test_sync.js b/modules/libjar/zipwriter/test/unit/test_sync.js new file mode 100644 index 0000000000..39a27db548 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_sync.js @@ -0,0 +1,65 @@ +/* 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/. + */ + +// Values taken from using zipinfo to list the test.zip contents +var TESTS = [ + { + name: "test.txt", + size: 232, + crc: 0x0373ac26, + }, + { + name: "test.png", + size: 3402, + crc: 0x504a5c30, + }, +]; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + var size = 0; + for (let i = 0; i < TESTS.length; i++) { + let source = do_get_file(DATA_DIR + TESTS[i].name); + zipW.addEntryFile( + TESTS[i].name, + Ci.nsIZipWriter.COMPRESSION_NONE, + source, + false + ); + size += + ZIP_FILE_HEADER_SIZE + + ZIP_CDS_HEADER_SIZE + + ZIP_EXTENDED_TIMESTAMP_SIZE * 2 + + TESTS[i].name.length * 2 + + TESTS[i].size; + } + + zipW.close(); + size += ZIP_EOCDR_HEADER_SIZE; + + Assert.equal(size, tmpFile.fileSize); + + // Test the stored data with the zipreader + var zipR = new ZipReader(tmpFile); + + for (let i = 0; i < TESTS.length; i++) { + let source = do_get_file(DATA_DIR + TESTS[i].name); + Assert.ok(zipR.hasEntry(TESTS[i].name)); + + var entry = zipR.getEntry(TESTS[i].name); + Assert.equal(entry.realSize, TESTS[i].size); + Assert.equal(entry.size, TESTS[i].size); + Assert.equal(entry.CRC32, TESTS[i].crc); + Assert.equal( + Math.floor(entry.lastModifiedTime / PR_USEC_PER_SEC), + Math.floor(source.lastModifiedTime / PR_MSEC_PER_SEC) + ); + + zipR.test(TESTS[i].name); + } + + zipR.close(); +} diff --git a/modules/libjar/zipwriter/test/unit/test_undochange.js b/modules/libjar/zipwriter/test/unit/test_undochange.js new file mode 100644 index 0000000000..bb1de36b1d --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_undochange.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +// Values taken from using zipinfo to list the test.zip contents +var TESTS = ["test.txt", "test.png"]; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + for (let i = 0; i < TESTS.length; i++) { + let source = do_get_file(DATA_DIR + TESTS[i]); + zipW.addEntryFile( + TESTS[i], + Ci.nsIZipWriter.COMPRESSION_NONE, + source, + false + ); + } + + try { + let source = do_get_file(DATA_DIR + TESTS[0]); + zipW.addEntryFile( + TESTS[0], + Ci.nsIZipWriter.COMPRESSION_NONE, + source, + false + ); + do_throw("Should not be able to add the same file twice"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_ALREADY_EXISTS); + } + + // Remove all the tests and see if we are left with an empty zip + for (let i = 0; i < TESTS.length; i++) { + zipW.removeEntry(TESTS[i], false); + } + + zipW.close(); + + // Empty zip file should just be the end of central directory marker + var newTmpFile = tmpFile.clone(); + Assert.equal(newTmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE); +} diff --git a/modules/libjar/zipwriter/test/unit/test_zipcomment.js b/modules/libjar/zipwriter/test/unit/test_zipcomment.js new file mode 100644 index 0000000000..afe0fd7886 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_zipcomment.js @@ -0,0 +1,33 @@ +/* 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/. + */ + +const DATA = "ZIP WRITER TEST COMMENT"; +const DATA2 = "ANOTHER ONE"; + +function run_test() { + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + zipW.comment = DATA; + zipW.close(); + + // Should have created a zip file + Assert.ok(tmpFile.exists()); + + // Empty zip file should just be the end of central directory marker + // and comment + Assert.equal(tmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE + DATA.length); + + zipW.open(tmpFile, PR_RDWR); + // Should have the set comment + Assert.equal(zipW.comment, DATA); + zipW.comment = DATA2; + zipW.close(); + + // Certain platforms cache the file size so get a fresh file to check. + tmpFile = tmpFile.clone(); + + // Empty zip file should just be the end of central directory marker + // and comment. This should now be shorter + Assert.equal(tmpFile.fileSize, ZIP_EOCDR_HEADER_SIZE + DATA2.length); +} diff --git a/modules/libjar/zipwriter/test/unit/test_zippermissions.js b/modules/libjar/zipwriter/test/unit/test_zippermissions.js new file mode 100644 index 0000000000..2b094c0570 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/test_zippermissions.js @@ -0,0 +1,102 @@ +/* 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/. + */ + +var TESTS = []; + +function build_tests() { + var id = 0; + + // Minimum mode is 0o400 + for (let u = 4; u <= 7; u++) { + for (let g = 0; g <= 7; g++) { + for (let o = 0; o <= 7; o++) { + TESTS[id] = { + name: "test" + u + g + o, + permission: (u << 6) + (g << 3) + o, + }; + id++; + } + } + } +} + +function run_test() { + build_tests(); + + var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + + var tmp = tmpDir.clone(); + tmp.append("temp-permissions"); + tmp.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + var file = tmp.clone(); + file.append("tempfile"); + + zipW.open(tmpFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + for (let i = 0; i < TESTS.length; i++) { + // Open the file with the permissions to match how the zipreader extracts + // This obeys the umask + foStream.init(file, 0x02 | 0x08 | 0x20, TESTS[i].permission, 0); + foStream.close(); + + // umask may have altered the permissions so test against what they really were. + // This reduces the coverage of the test but there isn't much we can do + var perm = file.permissions & 0xfff; + if (TESTS[i].permission != perm) { + dump( + "File permissions for " + + TESTS[i].name + + " were " + + perm.toString(8) + + "\n" + ); + TESTS[i].permission = perm; + } + + zipW.addEntryFile( + TESTS[i].name, + Ci.nsIZipWriter.COMPRESSION_NONE, + file, + false + ); + Assert.equal( + zipW.getEntry(TESTS[i].name).permissions, + TESTS[i].permission | 0o400 + ); + file.permissions = 0o600; + file.remove(true); + } + zipW.close(); + + zipW.open(tmpFile, PR_RDWR); + for (let i = 0; i < TESTS.length; i++) { + dump("Testing zipwriter file permissions for " + TESTS[i].name + "\n"); + Assert.equal( + zipW.getEntry(TESTS[i].name).permissions, + TESTS[i].permission | 0o400 + ); + } + zipW.close(); + + var zipR = new ZipReader(tmpFile); + for (let i = 0; i < TESTS.length; i++) { + dump("Testing zipreader file permissions for " + TESTS[i].name + "\n"); + Assert.equal( + zipR.getEntry(TESTS[i].name).permissions, + TESTS[i].permission | 0o400 + ); + dump("Testing extracted file permissions for " + TESTS[i].name + "\n"); + zipR.extract(TESTS[i].name, file); + Assert.equal(file.permissions & 0xfff, TESTS[i].permission); + Assert.ok(!file.isDirectory()); + file.permissions = 0o600; + file.remove(true); + } + zipR.close(); + + tmp.remove(true); +} diff --git a/modules/libjar/zipwriter/test/unit/xpcshell.ini b/modules/libjar/zipwriter/test/unit/xpcshell.ini new file mode 100644 index 0000000000..4873a92c17 --- /dev/null +++ b/modules/libjar/zipwriter/test/unit/xpcshell.ini @@ -0,0 +1,36 @@ +[DEFAULT] +head = head_zipwriter.js +support-files = + data/test_bug446708/thumbs/st14-1.tiff + data/emptyfile.txt + data/smallfile.txt + data/test.png + data/test.txt + data/test.zip + data/test_bug399727.html + data/test_bug399727.zlib + data/test_bug717061.gz + data/test_bug717061.html + +[test_asyncadd.js] +[test_asyncbadadd.js] +[test_asyncbadremove.js] +[test_asyncremove.js] +[test_bug399727.js] +[test_bug419769_1.js] +[test_bug419769_2.js] +[test_bug425768.js] +[test_bug433248.js] +[test_bug446708.js] +[test_bug467740.js] +[test_createempty.js] +[test_deflatedata.js] +[test_directory.js] +[test_editexisting.js] +[test_storedata.js] +[test_sync.js] +[test_undochange.js] +[test_zipcomment.js] +[test_zippermissions.js] +[test_bug717061.js] +[test_alignment.js] |