v2.23.0
- Add the
filteroption to theimport*()methods ofZipFSandZipDirectoryEntry: the function receives each entry read from the zip file and returnstrue(or a promise resolving totrue) to import it. It can read the data of the entry to decide, and the entries left out never reach theduplicatespolicy - Add the
filteroption to theexport*()methods and togetExportedSize(): the function receives eachZipEntryof the tree and returnstrueto export it. Leaving out a directory leaves out its subtree, the entries left out are not read and are not counted byonprogressandonentryprogress - Add the
filteroption toexportFileSystemHandle(), with the same semantics as the zip exports - Add the
filteroption toaddFileSystemHandle()andaddFileSystemEntry(): the function receives each handle found and its path, so a directory likenode_modulesor hidden files can be left out without walking them
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.22.1...v2.23.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.22.1
- A zip file written behind a prefix which was removed together with its whole first entry, like a self-extracting page whose first entry is the face shown by the host, is read again: the entries are shifted to their real positions and their data can be read. Since v2.21.0, the shift was decided on the first central directory record only, and a first record whose local file header lies in the removed bytes proves nothing, so the entries kept their stored offsets and
getData()failed withERR_LOCAL_FILE_HEADER_NOT_FOUND, whatever thestrictnessoption. The reader now checks the following records until one settles the question, and keeps the protection against a damaged end of central directory record whose local file headers are right - An entry which lies before the start of the zip file after such a shift is no longer reported as
WARNING_UNSORTED_CENTRAL_DIRECTORY
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.22.0...v2.22.1
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.22.0
- New
readerOptionsoption: the options of theZipReaderwhich reads the zip file to copy. The zip file used to be read with the default options only, so a zip file the reader rejects by default could not be appended as-is. SetfilenameValidationorstrictnessto copy the entries of a zip file holding unsafe or unusual filenames,filenameEncodingto decode the filenames the duplicate check and thefilteroption see, andpasswordto letfilterread the data of encrypted entries withgetData(). The bytes of the entries are copied as-is whatever the options are. A value which is neither an object nor unset throwsERR_INVALID_READER_OPTIONS, now exported by the core builds as well - The
filterfunction receives a second argument: the entry of the current zip which has the same filename, asadd()or a previousappendZip()call left it, orundefinedwhen there is none. It is the way to apply a duplicate filename policy, since keeping both entries throwsERR_DUPLICATED_NAME: return!existingEntryto keep the entry of the current zip, callremove(existingEntry)and returntrueto replace it, or comparecrc32,uncompressedSizeorlastModDateto decide. A removed entry leaves its bytes in the output, asremove()always did, and a strictZipReaderreports them as prepended data.remove()now accepts theEntryMetaDatareturned byadd()in its type declaration - The zip file being copied is closed when
filterthrows, and the documentation ofappendZip()now states thatadd()calls made whilefilterruns are written before the copied entries, while those made once the copy has started are written after it - The entries passed to
filterare not modified any more once the callback has returned: the copy used to rewrite theiroffsetwith the position in the output and to hang the fields of the rebuilt central directory on them - A zip file whose own entries share a filename is rejected with
ERR_DUPLICATED_NAMEbefore anything is written, as before, and this is now documented: aZipWriterholds one entry per filename, so thefilteroption is the way to keep one of them
new ZipReader(reader, null)reads the zip file with the default options instead of failing with aTypeErrorwhen the entries are read; anulloptions argument is treated as unset, like the other falsy values everywhere in the API
- The
transferStreamsconfiguration option is removed. It had been a no-op since v2.19.0, when the path transferring the streams to the web workers was removed: the data always crosses the worker boundary chunk by chunk.configure()ignores the key silently, so a call still passing it keeps working; TypeScript reports the key as unknown inWorkerConfiguration, delete it from the call
- A real byte overlap between two entries, stretched consistently in the local file header and the central directory record so that the default
checkLocalDirectorycheck passes, is detected withcheckOverlappingEntrywhatever the order the entries are read in; the existing overlap fixtures overlapped only through a phantom data descriptor
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.21.0...v2.22.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.21.0
- An entry whose declared data extent (its offset plus its compressed size) ends past the central directory is rejected by
getData()withERR_ENTRY_DATA_OUT_OF_BOUNDSat every strictness level, with or withoutcheckOverlappingEntry. Only the end of the file used to bound it, so a stored entry stretched over the directory returned the directory bytes as its content, and onlycheckCrc32could catch it ZipWriter#appendZip()refuses to copy an entry listed withWARNING_MISSING_ZIP64_EXTRA_FIELD, i.e. whose central directory record holds a Zip64 sentinel with no Zip64 extra field resolving it: the method throwsERR_EXTRAFIELD_ZIP64_NOT_FOUNDbefore writing anything, unless thefilteroption leaves the entry out. Such an entry used to be copied with zero sizes in the rebuilt central directory, or only its local file header through a filter, so the output entry was silently unreadable- An entry whose central directory record lacks its Zip64 extra field is now parsed to the end of the record before the defect is reported, so its compression method, dates and other fields are listed with it. An entry whose local file header offset is the unresolved sentinel no longer makes the archive report "prepended data" from the offsets of the other entries
- The options passed to
ZipReader#getEntries()andgetEntriesGenerator()now reach the entries:entry.getData()reads them after its own options and before those of theZipReaderconstructor, so astrictness,checkLocalDirectory,passwordorfilenameEncodinggiven togetEntries()applies to the data as the type declarations described - A central directory offset stored past the end of the file, e.g. in a damaged or truncated end of central directory record, is reconciled with the directory found before the record instead of failing with
ERR_BAD_FORMAT, with theWARNING_MISMATCHED_CENTRAL_DIRECTORY_OFFSETreason deposited and the"strict"level rejecting the archive as before. The same reconciliation now covers a Zip64 end of central directory record stored at the wrong offset: the record is looked for right before its locator, then by its signature within the range the locator allows - Before shifting the entries of an archive whose stored central directory offset does not match the directory found, the reader checks that the local file header of the first entry is found at the shifted position and not at the stored one; an archive whose stored offset is short of the directory while its entries sit at the shifted positions is diagnosed as
WARNING_PREPENDED_DATA. The shift used to be decided on the direction of the mismatch alone, and a damaged offset could move the entries away from their local file headers - An empty archive, i.e. one with no entry, behind prepended data is diagnosed with
WARNING_PREPENDED_DATAand its prefix is extracted withextractPrependedDataas for a non-empty one; it used to be read without a warning - The CRC-32 checksum and the sizes of the data descriptor are compared with the central directory record when the descriptor is read, i.e. when
checkOverlappingEntryis set, and a mismatch is reported asWARNING_MISMATCHED_LOCAL_FILE_HEADER_CRC32_OR_SIZES, an error or a warning depending oncheckLocalDirectory. A local file header whose CRC-32 checksum and sizes are all zero without the data descriptor flag is tolerated, because some streaming writers leave these fields blank - Reading an archive whose end of central directory record points into entry data holding the archive extra data signature no longer takes that data for an encrypted central directory: the record is only looked for at the start of the central directory, where the specification places it
EntryMetaData#lastAccessDateandcreationDatekeep the values of the central directory record when it holds them; the values of the NTFS extra field of the local file header used to overwrite them once the data of the entry was read. When the central directory record holds none, e.g. with the extended timestamp field, whose central form stores the modification time only, the local file header stays their source
- Each kept entry is copied from its local file header up to the exact end of its data or, when it has one, of its data descriptor, whose layout is read back from the zip file. The bytes outside the kept entries are dropped: a self-extracting stub, the data of entries removed earlier and the padding between entries, so a zip file aligned with the
usdzoption is not aligned any more once filtered. The copy used to stop at the next entry or at the central directory, so the data of a removed entry that followed a kept one was carried into the output - Before anything is written, each kept entry is checked to start with a local file header and to end before the next entry or the central directory; otherwise the method throws
ERR_LOCAL_FILE_HEADER_NOT_FOUNDorERR_OVERLAPPING_ENTRYand leaves the current zip unchanged. The same checks apply when the output is a split zip file, whose entries are copied one by one as well - The general purpose bit flag of a copied entry is written as-is in the rebuilt central directory, including the bits zip.js never sets itself. A filtered copy into a split zip file writer now starts the first disk with the split zip file signature whatever the source starts with
- A copy that fails after some bytes were written marks the
ZipWriterwithhasCorruptedEntriesand the error withcorruptedEntry, asadd()does; a failure before the first byte leaves the writer clean. Everyfiltercall completes before any data is copied
appendZip()states that the comment and the digital signature of the zip file are not copied, since its central directory is rebuilt, and that a source copied as a whole into a split zip file writer carries its self-extracting stub after the split zip file signature of the first disk, where no system runs it;ERR_LOCAL_FILE_HEADER_NOT_FOUND,ERR_OVERLAPPING_ENTRY,ERR_EXTRAFIELD_ZIP64_NOT_FOUNDandERR_ENTRY_DATA_OUT_OF_BOUNDSsay when the method andgetData()throw themWARNING_MISMATCHED_CENTRAL_DIRECTORY_OFFSETcovers both directions of the mismatch and names the local file header check that decides the shift;WARNING_MULTIPLE_END_OF_CENTRAL_DIRECTORYstates that it is never deposited as a warning, since"balanced"rejects it like"strict"and"tolerant"reads the last record and reports the stale one asWARNING_TRAILING_CENTRAL_DIRECTORY_DATA; the reasons ofERR_AMBIGUOUS_ARCHIVElist"mismatched central directory offset"checkLocalDirectorydescribes the data descriptor comparison and the blank local file header tolerance;lastAccessDateandcreationDatesay which record they are read from
- The transitive development dependency brace-expansion is updated in the lock file of the benchmarks; no runtime dependency changed, zip.js has none
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.20.0...v2.21.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.20.0
ZipWriter#appendZip()accepts afilteroption: a function called once per entry of the appended zip file, in central directory order, and the entry is copied when it returns or resolves totrue. The entries left out leave no bytes behind. The duplicate filename check applies to the kept entries only. Together, this edits an existing zip file into a new one without decompressing its data:appendZip(reader, { filter })copies the entries to keep as-is, thenadd()writes the replacements and the additions. With the option, the data is copied entry by entry and the bytes outside the entries, e.g. a self-extracting stub, are not copied. Without it, the zip file is copied as a whole as before. The option also works with split zip file writers
- An archive carrying a digital signature record, i.e. written with the
signCentralDirectoryoption, failed to open withERR_CENTRAL_DIRECTORY_NOT_FOUNDonce data was prepended to it, e.g. a self-extracting stub: the relocation of the central directory assumed nothing lies between the directory and the end of central directory record. The reader now looks for a signature record ending right before the end of central directory record, and takes the directory to end where that record starts - An end of central directory record holding the Zip64 sentinel in its offset, size or disk number field with no Zip64 locator in front of it is rejected with
ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUNDat every strictness level. It used to be opened with a "prepended data" warning and entries whose data could not be read, since the sentinel was taken for an offset - A central directory record holding the Zip64 sentinel in a size or offset field with no Zip64 extra field resolving it no longer makes the whole archive fail
getEntries()at the"balanced"and"tolerant"levels: the entry is listed, the newWARNING_MISSING_ZIP64_EXTRA_FIELDreason names it onZipReader#warnings, reading its data throwsERR_EXTRAFIELD_ZIP64_NOT_FOUND, and the other entries stay readable. The"strict"level keeps throwingERR_EXTRAFIELD_ZIP64_NOT_FOUNDfromgetEntries(), as every level did before EntryMetaData#lastModDatetakes the value of the NTFS extra field (0x000a, 100 ns) over the value of the extended timestamp field (0x5455, 1 s) when a record carries both, as the type declarations already said forrawLastModDate. The extended timestamp used to win because it was read last, so an entry zip.js itself writes with a sub-second date andlastAccessDate,creationDateorntfsTimestamp: trueread back with the milliseconds dropped
- When the offset stored in the end of central directory record points past the central directory actually found, the reader deposits the new
WARNING_MISMATCHED_CENTRAL_DIRECTORY_OFFSETreason and the"strict"level rejects the archive withERR_AMBIGUOUS_ARCHIVE; the relocation used to be silent at every level. This is the shape of an archive written with absolute offsets for a prefix that is no longer there: when the local file header of the first entry is found at the same shifted position, the entries are now read from the shifted positions instead of failing withERR_LOCAL_FILE_HEADER_NOT_FOUND EntryMetaData#executableisfalsefor directories, as it already was for symbolic links: the execute bits of a directory mean it can be searched, and every directory carries them, so the flag wastruefor every folder written by zip.js.unixModestill holds the bits
WARNING_MISMATCHED_CENTRAL_DIRECTORY_OFFSET,WARNING_MISSING_ZIP64_EXTRA_FIELDandZipWriterAppendZipOptionsare documented;ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUNDsays when it is thrown;rawLastModDateandexecutabledescribe the precedence rules above;GetEntriesOptions#filenameValidationstates that the filename reported is the decoded central directory name or the one of a valid Unicode Path extra field, and that the name validated is that final name- The site documents how to read and write Zstandard entries (compression method 93) with
registerCodec(): a codec built on thenode:zlibstreams for Node.js, the platformCompressionStreamclasses on Bun, and a JavaScript decoder such as fzstd elsewhere
- The ZIP API added to
node:zlibin Node.js 26.8 is measured next to jszip, fflate and archiver in BENCHMARKS.md, and the page was re-run on Node.js 26.10, except its browser encryption table, measured on 2026-09-26. The page now says which version of zip.js each table was measured with
- The transitive development dependencies markdown-it and brace-expansion are updated in the lock file; no runtime dependency changed, zip.js has none
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.19.0...v2.20.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.19.0
- A Unicode Path extra field (0x7075) no longer bypasses
filenameValidation: the name of a valid field replaced the decoded central directory name after that name had been validated, so an entry namedsafe.txtin its record and../evil.txtin the field was listed as../evil.txtat the"strict"and"balanced"levels. The validation andnormalizeFilenamenow run on the final name, so such an entry failsgetEntries()withERR_UNSAFE_FILENAMEcarrying the name of the field,normalizeFilenamereceives that name and can repair it, and"tolerant"still keeps it. Conversely, an unsafe record name overridden by a safe field is now accepted, since the name reported is the safe one;rawFilenamestill holds the bytes of the record - With
unixExtraFieldType: "unix", the Info-ZIP Unix type 2 extra field (0x7855) is written as Info-ZIP'szipwrites it and itsextrafld.txtspecifies: the 2-byte uid and gid in the local file header, and the tag with a size of 0 in the central directory record, where the 4 bytes used to be repeated. As for the archives Info-ZIP writes,entry.uidandentry.gidof such an entry areundefinedaftergetEntries()and filled in once the entry data has been read: code reading them right aftergetEntries()on an archive written this way by 2.19.0 has to read the data first, or readentry.localDirectory.uidaftergetData(), or write the entries with the default"infozip"type. Archives written with"unix"by earlier versions keep reporting the ids atgetEntries(), since their central directory copy holds them ZipFSexports keep re-emitting imported ids as a New Unix extra field (0x7875), which stores them in both records, unlessunixExtraFieldTypeis set on the export- When a record carries both a New Unix extra field (0x7875) and a type 2 field (0x7855) holding ids, the ids come from the New Unix field, to which Info-ZIP gives precedence and which is not limited to 16 bits; the type 2 ids used to win, so a uid of 70000 next to its truncated 4464 was reported as 4464.
extraFieldUnix.uidandextraFieldUnix.gidstill report the type 2 values ZipWriter#add()no longer accesses thereadablegetter of a reader that implementsreadUint8Array(), e.g. aBlobReader, aUint8ArrayReaderor anHttpRangeReader: the check for a usable reader readreadablefirst, which on these classes creates a new stream on each access, and that stream pulled its first chunk before being discarded, one wasted range request peradd()for a remote reader
- The default
chunkSizeis 256 KiB instead of 64 KiB. Every stage of the pipeline of an entry holds up to one chunk and the data crosses the boundary of a web worker one chunk per message, so the change buys fewer messages for more memory per entry in progress. Where it shows: concurrentadd()on Bun 1.4.2 takes 0.24 s instead of 1.20 s, since its nativeCompressionStreamleaves the JavaScript thread only for writes larger than 128 KB, and the worker paths gain in proportion to the number of messages saved. What it costs, measured in isolation on Node.js in-process: about 20 MB more peak memory on a 256 MB stream and on a 20 MB text entry. Applications on memory-tight targets, e.g. browser extensions or mobile pages with many entries in flight, can keep the previous value withconfigure({ chunkSize: 64 * 1024 }). BENCHMARKS.md was re-run on the new default - The streams of an entry are never transferred to a web worker any more: the data always crosses the boundary chunk by chunk, which was never slower and is faster in the browsers, measured with a warmed worker on a 32 MiB deflated entry: Safari 27 reads in 38 ms against 80 with the transfer and writes in 93 against 156, Firefox 156 reads in 75 to 80 ms against 86 to 89 and writes in 32 to 36 against 79 to 87, Chrome 153 writes in 69 against 85 and reads in the same time, Deno is on par, and Bun does not support transferable streams. The
transferStreamsoption ofconfigure()and of the per-call options is accepted and ignored, and deprecated in the type declarations; an application that settransferStreams: falseto work around a failure has nothing to change and can drop the option
GetEntriesOptions#filenameValidationandGetEntriesOptions#normalizeFilenamestate that the name validated and normalized is the final one, after a valid Unicode Path extra field has replaced it;extraFieldUnixandextraFieldInfoZipofEntryMetaDataand ofLocalDirectory, andEntryMetaData#localDirectory, describe which field the ids come from and when the local file header fills them in;ZipWriterConstructorOptions#unixExtraFieldTypesays where the"unix"ids are stored and when a reader sees them;Configuration#chunkSizegains a remark on its memory and message cost;Reader#readablesays it returns a new stream reading from the start on each access;WorkerConfiguration#transferStreamsis marked deprecated- The tests README and two test comments no longer describe streams being transferred to the worker
- The filename validation test builds an entry whose Unicode Path extra field escapes the directory and checks it at every level and through
normalizeFilename; the Unix extra field layout test pins the empty central directory copy of the type 2 field and reads the ids after the data; the local header ids test adds a record carrying both Unix fields with ids and a central directory carrying only the New Unix field while the local file header carries only the type 2 one; a new test counts the reads a reader receives fromadd()
QUICK=1runs the benchmark scripts on a reduced matrix in about 50 seconds, for a smoke check of the harness rather than for the page;bench-crc.jsfollows the current options of the deflate stream
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.18.2...v2.19.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.18.2
- With
checkOverlappingEntryset, a data descriptor whose CRC-32 disagrees with the central directory, a corrupt CRC-32 for instance, is now reported inlocalDirectory.dataDescriptorwith its own fields, its CRC-32 differing fromentry.crc32asLocalDataDescriptor#crc32documents. Until now the signed layout was kept only when its CRC-32 and sizes agreed with the central directory, since 2.18.1 among 4- or 8-byte sizes, and otherwise the record was read at the width the Zip64 extra field of either record announces without the signature, i.e. at a layout known not to match, so a signed descriptor with a corrupt CRC-32 came back withsignaturefalse, the signature bytes ascrc32and its sizes shifted by four bytes. The layout is now the one whose sizes agree with the central directory, its CRC-32 breaking ties when the central directory stores one; when no layout agrees, the record is read at the announced width as before, with the signature when it starts with one. The tie-break applies to AES entries that store a CRC-32 (AE-1, what WinZip writes for most files) like to any other entry; the CRC-32 of an AES entry used to be ignored. Reading the data of the entry is unchanged, and reading withoutcheckOverlappingEntrynever consulted the descriptor
- The remarks of
LocalDataDescriptor#signatureandLocalDataDescriptor#zip64describe how the layout is chosen
- The data descriptor width test now reads a signed descriptor whose CRC-32 alone is corrupt and one whose compressed size is corrupt, and a new test writes an AE-2 entry with a signed data descriptor, patches it into an AE-1 entry with a disagreeing and then an agreeing descriptor CRC-32, and checks the layout reported for each and for the AE-2 entry
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.18.1...v2.18.2
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.18.1
LocalDataDescriptorhas azip64property,truewhen the sizes of the data descriptor read withcheckOverlappingEntryare stored as 8-byte values
- With
checkOverlappingEntryset, an entry placed past 4 GB whose data descriptor stores 4-byte sizes now reads;getData()used to fail withERR_UNSUPPORTED_UINT64. The width of the sizes was taken from the presence of a Zip64 extra field in either record, but neither record tells it reliably: the local file header is written before a streaming writer knows the sizes, and the Zip64 extra field of the central directory record, written last, describes that record, not the descriptor. Go'sarchive/zip, for instance, gives a small entry placed past 4 GB a Zip64 extra field in its central directory record for the offset alone and a 4-byte descriptor, while its large streamed entries get an 8-byte descriptor with no local Zip64 extra field, which is why the central directory record was consulted; the descriptor of the small entry was therefore read as 8 bytes wide, across the next record. The descriptor is now read with the layout, among the two widths with and without the signature, whose CRC-32, when the entry stores one, and sizes agree with the central directory; when none does, it is read at the width the Zip64 extra field of either record announces, without the signature, as before. Reading withoutcheckOverlappingEntrynever depended on the descriptor and is unchanged
- A new test builds the archives by hand, behind a reader that fakes a 4 GB prefix so the offsets are real, and reads a 4-byte descriptor below and past 4 GB, signed and unsigned, and an 8-byte descriptor whose Zip64 extra field is in the central directory only or in neither record, each in both read orders, and a descriptor agreeing with no layout
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.18.0...v2.18.1
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.18.0
- A Zip64 extra field of a local file header that is too short for the
0xFFFFFFFFsentinels of the header or holds a value aboveNumber.MAX_SAFE_INTEGERno longer failsgetData()withERR_EXTRAFIELD_ZIP64_NOT_FOUNDorERR_UNSUPPORTED_UINT64: the sizes of an entry come from the central directory, so the field is reported asWARNING_MALFORMED_EXTRA_FIELDonentry.warningsand the entry is read. A local file header whose sizes hold the sentinels with no Zip64 extra field behind them, which used to pass silently, is reported the same way. In the three cases an entry without a data descriptor keeps the sentinels as its local sizes, which the local file header check reports asWARNING_MISMATCHED_LOCAL_FILE_HEADER_CRC32_OR_SIZES, i.e.ERR_AMBIGUOUS_ARCHIVEunder the default strictness and a warning withstrictness: "tolerant". Both errors are still raised bygetEntries()for a central directory record whose field is too short or holds such a value, where the sizes and the offset have no other source, and a record whose sizes, offset or disk number hold the sentinel with no Zip64 extra field behind it now failsgetEntries()withERR_EXTRAFIELD_ZIP64_NOT_FOUNDtoo, at every strictness, so none of the entries is listed: it used to be listed with sizes of 4 GB and fail later, with a local file header mismatch orERR_ENTRY_DATA_OUT_OF_BOUNDS, or withERR_LOCAL_FILE_HEADER_NOT_FOUNDfor an offset - An AES extra field on a record that is not encrypted and whose compression method is not 99 is ignored and reported as
WARNING_MALFORMED_EXTRA_FIELD, onZipReader#warningswith thefilenamefor the central directory record and onentry.warningsfor the local file header, and the entry is read with the method its record declares; the field used to override the method andgetData()failed withERR_UNSUPPORTED_COMPRESSION. An AES extra field shorter than 7 bytes, which was ignored silently, is reported the same way. On an encrypted record the field still overrides the method and the conflict is still rejected withERR_UNSUPPORTED_COMPRESSION, since the data may be AES behind a wrong method - The encrypted flag of an entry follows its central directory record. A local file header whose bit 0 is cleared is still
ERR_AMBIGUOUS_ARCHIVEunder the default strictness, and a reader withstrictness: "tolerant"now decrypts the entry and depositsWARNING_MISMATCHED_LOCAL_FILE_HEADER_BIT_FLAG, where it used to follow the local file header, read the ciphertext as plaintext and fail - An entry whose strong encryption bit (bit 6 of the general purpose bit flag) differs between the two records is
ERR_AMBIGUOUS_ARCHIVEunder the default strictness, like the encrypted bit, and theERR_UNSUPPORTED_ENCRYPTIONcheck reads the central directory record. A bit set in the local file header only used to reject an encrypted entry, AES or ZipCrypto, as unsupported, and a bit set in the central directory only was ignored; a reader withstrictness: "tolerant"now decrypts the first withWARNING_MISMATCHED_LOCAL_FILE_HEADER_BIT_FLAGand reports the second asERR_UNSUPPORTED_ENCRYPTION - The
causeof an error raised in a worker keeps itscodeproperty, e.g."Z_MEM_ERROR"on the cause ofERR_CODEC_OUT_OF_MEMORY. A structured clone never copies that property, so it wasundefinedwith workers on every host, while thecodeof the error itself was already carried by the message posted by the worker
ERR_EXTRAFIELD_ZIP64_NOT_FOUND,ERR_UNSUPPORTED_UINT64,WARNING_MALFORMED_EXTRA_FIELDandWARNING_MISMATCHED_LOCAL_FILE_HEADER_BIT_FLAGdocument the cases above, andERR_INVALID_COMPRESSED_DATA,ERR_INVALID_CRC32,ERR_INVALID_UNCOMPRESSED_SIZEandERR_CODEC_OUT_OF_MEMORYstate more precisely the routes on which they are raised: theERR_INVALID_CRC32case of Node.js for bytes trailing the deflate stream withcheckCrc32, the gzip route of an AE-2 entry running only on a host without"deflate-raw"whose bundled codec cannot take over either, e.g. because the WebAssembly module failed to load, and, on a host without"deflate-raw", a bundled codec that cannot allocate its state when the entry starts not being reported asERR_CODEC_OUT_OF_MEMORY, since the native codec takes over through a gzip container unlessuseCompressionStreamisfalse
- New tests cover the three ways a Zip64 extra field of a local file header is unreadable, and their central directory counterparts, the stray AES extra field on a deflated and on an encrypted entry, the encrypted bit and the strong encryption bit disagreeing between the two records, and a worker whose codec cannot allocate its state, a test that runs on the browsers with module workers, Deno and Bun
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.17.0...v2.18.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
v2.17.0
ERR_CODEC_OUT_OF_MEMORYis a new exported constant, thrown when the codec of an entry cannot allocate the memory it needs, e.g. when the 16 MB heap of the bundled WebAssembly module is exhausted by too many entries processed concurrently in the same worker, or on the page when workers are off, with the error of the codec as thecause. The failure is recognized by thecodeproperty of that error,"Z_MEM_ERROR", which the bundled zlib-streams 1.4.0 sets on its allocation failures and which the nativeDecompressionStreamof Node.js would set. It is raised when reading an entry, both when the codec cannot allocate its state and when it fails while inflating, and when writing an entry whose codec cannot allocate its state; a failure while compressing keeps the error of the codec. Such a failure used to surface as theallocation failederror of the codec, so code matching that message should catch the constant instead
- A zip file whose
compressedSizeovershoots the deflate stream of an entry, which the bundled codecs used to read by dropping the extra bytes, now fails on every codec, as it already did with a nativeDecompressionStream. The error isERR_INVALID_COMPRESSED_DATA, with the error of the codec as thecause; on Node.js withcheckCrc32set it isERR_INVALID_CRC32, since its native inflater reports the extra bytes only once the gzip trailer built by zip.js has been written. The bundled codecs are zlib-streams 1.4.0, the WebAssembly one, and zlib-streams-ts 1.2.0, the pure-JS one of the*-nativebuilds - A corrupted deflated entry rejects with
ERR_INVALID_COMPRESSED_DATAwhatever inflates it, with the error the codec raised as thecause: theTypeErrorof a nativeDecompressionStream, whose message depends on the engine, or the error of the bundled WebAssembly or pure-JS codec. Only the message-less error of Node.js used to be mapped, so the same corrupted zip file raisedInvalid compressed dataon Node.js,corrupt deflate streamon Deno,process error:-3wherever the WebAssembly codec inflates, e.g. in a browser withuseCompressionStreamoff, andinflate failedon Bun, and a caller comparing against the constant was right on Node.js only. An error raised while the data is being read, by the reader of the zip file or by the decryption of the entry, is not a codec failure and reaches the caller unchanged, with its own cause. On Safari and Bun, whose structured clone drops thecauseof an error posted by a worker, the cause is rebuilt from its name and message - With
checkCrc32set, a stored uncompressed size larger than the data now fails withERR_INVALID_CRC32instead ofERR_INVALID_UNCOMPRESSED_SIZE, on every codec, with the error of the inflater as thecause: the inflater verifies the checksum through a gzip trailer built from the stored CRC-32 and size, and rejects that trailer as a whole. WithcheckCrc32off it is stillERR_INVALID_UNCOMPRESSED_SIZE, and a size smaller than the data isERR_INVALID_UNCOMPRESSED_SIZEeither way, as soon as the output exceeds it - A
signalaborted while an entry is being read bygetData()or added byadd()rejects the operation with the reason of the signal, whether the compressed data is still being consumed or the content still being written. The signal used to guard the pipe feeding the codec alone, so an abort landing once the input had been consumed, e.g. while a large content was still being written to the writer, was ignored and the operation completed. On the oldest supported engines, which ignore thesignaloption ofpipeTo(), the data is written to the end before the operation is rejected - With the
passwordsandrequestPasswordoptions of the filesystem API, theERR_INVALID_PASSWORDerror raised when every candidate has failed, or whenrequestPasswordgives up, carries the error raised by the last candidate as itscause. A ZipCrypto entry whose read fails for a reason other than a false accept, i.e. a wrong password slipping past the one-byte header check of ZipCrypto, as one in 256 does, reports that failure instead of trying the next candidate: onlyERR_INVALID_CRC32,ERR_INVALID_COMPRESSED_DATAandERR_INVALID_UNCOMPRESSED_SIZEcount as a wrong password, since such a password produces content that the CRC-32 check or the inflater rejects, while a failure of the reader, e.g. a network error, or anERR_CODEC_OUT_OF_MEMORYerror reaches the caller as-is. Any failure of the read used to count as a wrong password, so a reader failure surfaced asERR_INVALID_PASSWORDonce the candidates were exhausted. A corrupted entry read with the right password is still reported as a wrong password, since nothing tells it from a wrong password passing the check - On Chrome before 103 or Node.js before 20.12, whose native inflater lacks
"deflate-raw", when the WebAssembly codec cannot take over because its module failed to load, e.g. an*-externalbuild deployed withoutzip-module.wasmnext to it, an entry encrypted with AES that stores no CRC-32 (AE-2, what the writer emits for every encrypted entry) is inflated through a gzip container whose trailer carries the CRC-32 of the output received so far. A valid entry failed withERR_INVALID_CRC32now and then, more often on a loaded machine and on Node.js, because the trailer was written on a timing guess. It is now written once the inflater has returned everything, and the entry fails withERR_INVALID_UNCOMPRESSED_SIZEwhen it inflates to fewer bytes as well as to more - The same wrapper serves the
checkCrc32route on every host, where the rewrite costs about 3 µs per entry with the native codec, measured with the benchmark harness; the tables of BENCHMARKS.md are unchanged - The WebAssembly codec (zlib-streams 1.4.0) and the pure-JS codec of the
*-nativebuilds (zlib-streams-ts 1.2.0) reject an unknown format with aTypeErrorwhen the stream is constructed, as the platformCompressionStreamandDecompressionStreamdo; they used to treat it as"deflate". Nothing changes for a zip file; code that tests a format by constructing the stream now gets the right answer
ERR_INVALID_COMPRESSED_DATA,ERR_INVALID_CRC32andERR_INVALID_UNCOMPRESSED_SIZEdocument when they are raised, thecausethey carry and the routes on which one is raised in place of another, thesignaloption of the reader and of the writer documents the abort landing during the output, andPasswordCandidatesOptionsdocuments the errors counted as a wrong password and thecauseof the finalERR_INVALID_PASSWORD
- New tests cover the corrupted deflated entry on every inflate route, with the native and the WebAssembly codecs, with and without workers and with and without
checkCrc32, the bytes trailing a deflate stream, the memory failure of the codec on both sides with the heap of the WebAssembly module actually exhausted, the gzip route with a large AE-2 entry read several times, which is what caught the race, the abort landing during the output of a read and of a write, and the reader failure and the cause under the password candidates - The password candidates fixture is generated until none of the wrong candidates passes the one-byte check of ZipCrypto by chance, which one in 256 did and failed a job on Chrome 87 once
- The README of the tests names both compression globals that the polyfill runner removes
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.16.1...v2.17.0
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com