What's Changed in v2.12.0
Breaking changes
- The language encoding flag (general purpose bit 11) is now set only when the encoded filename or the encoded comment of an entry holds a byte outside printable ASCII, where it used to be set on every entry. Nearly every archive zip.js writes therefore differs from v2.11.4 byte for byte. Nothing is decoded differently, printable ASCII being spelled identically in UTF-8 and in Code Page 437, and it is how every other writer decides the flag. Control characters are excluded deliberately: the Code Page 437 table maps 0x01 to 0x1f and 0x7f to the IBM graphic characters rather than to the control characters themselves, so a name or a comment holding one of them keeps the flag and keeps round-tripping
EntryMetaData#filenameUTF8andEntryMetaData#commentUTF8returned byZipWriter#add()now report the flag that was actually written, where they always reportedtrue. They agree with the values read back from the archive, which the previous behaviour contradicted for every entry whose name is printable ASCIIHttpReadernow throws anERR_HTTP_STATUSerror for any status outside 2xx, where it accepted everything below 400. A 304 answered by a caching proxy to a conditional request was read as a successful empty response, and the archive failed later with an unrelated end of central directory error- The
checkPasswordOnlyoption combined withpassThroughset totruenow verifies the password. It reported every password as valid, wrong ones included, and streamed the whole entry instead of stopping after the first block: passing the encryption stage through left nothing able to raise the sentinel that means "the password is correct" ZipDirectoryEntryImportOptions#passThroughand thepassThroughoption ofZipDirectoryEntryExportOptions#readerOptionsare typedbooleaninstead ofboolean | "compressed". The filesystem API has always refused"compressed"at runtime, the types now say so and code passing it stops compiling
New features
- New
passThroughvalue,"compressed", which passes the compression stage through and still runs the encryption stage. It is what re-encrypting an entry without recompressing it needs: reading with it decrypts and does not inflate, writing with it encrypts and does not deflate.passThroughis a depth rather than a pair of switches, and encryption is the outer stage - New
entryoption onZipWriter#add(), which takes the entry read from another archive and derives the ten or so values a copy needs from it. Copying an entry by forwarding the obvious subset used to produce silent corruption, an encrypted entry landing markedencrypted=falseover untouched ciphertext. The oracle the option is built on is byte identity: an archive copied entry by entry with{ passThrough: true, entry }is the archive it was read from - New
checkLocalFilenameoption, which selects whether the filename of the local header is compared, independently of whether a difference throws. The two axes were conflated:strictness: "strict"compared the filename and rejected, everything else did neither, so a mismatched filename could never be reported as a warning.{ checkLocalFilename: true, checkLocalDirectory: false }now reports it onEntryMetaData#warnings ZipDirectoryEntry#exportZip()accepts aZipWriterinstance, the symmetric counterpart ofZipDirectoryEntry#importZip()accepting aZipReader. The entries are added to that writer and the archive is left open, so the caller closes it, can add entries of its own before or after, can export several trees into one archive, and can readZipWriter#warnings- New
ZipWriter#warningschannel, reporting the adjustments the writer makes silently: an entry stored because no deflate implementation is reachable, a date clamped to the MS-DOS range with no extra field left to carry the original value. Each reason is deposited once, with the filename of the first entry it applied to ZipReaderandZipWriterimplementSymbol.asyncDispose, soawait usingfinalizes the archive on scope exit- New
outputSizeproperty on the errors thrown by the codecs, holding the number of bytes that reached the writer before the failure. It is what a caller salvaging an archive after a mid-entry failure needs to know ZipWriter#add()acceptsnullas the reader of an entry with no content- Every user-visible error message is now exported as a constant,
ERR_ABORTEDincluded, and an audit in the test suite keeps it that way across the fourteen builds configure()checks the type ofbaseURI,workerURIandwasmURI, which used to fail much later and quietly
Bug fixes
- The codecs of a reader stream are now released on demand and on cancel, instead of being held until the whole archive was read
- A failure raised on the main thread while a codec task runs over the worker message protocol now carries
outputSize, so the writer advances by the bytes that really reached it. It did not, and the central directory of an archive salvaged after such a failure was written short of the real positions, by 655360 bytes in the measured case - Terminating the workers no longer leaves the WebAssembly module torn down while an operation that reloaded it believes it is loaded, which silently substituted the native gzip path for the rest of the process
preventCloseno longer makesFileEntry#arrayBuffer()andZipReaderStreamhang forever. Both build their writable internally, exactly like theWriterinstances the option is documented to ignore- A copy made with the
entryoption now derives the deflate level bits, the language encoding flag and the unix extra field type of the source entry. It dropped the level bits, cleared bit 11 on an ASCII-named source that carried it, and rewrote a 0x7855 record as a 0x7875 one - The refusal to change the last modification date of a ZipCrypto entry no longer fires under
passThrough: "compressed", where the entry is encrypted again or not encrypted at all and the date is free to change ZipDirectoryEntry#exportFileSystemHandle()now refuses the"compressed"value ofpassThroughspelled at the top level, as it already did throughreaderOptions. It wrote the raw deflated bytes into the extracted files insteadZipDirectoryEntry#getExportedSize()isasync, so its option validation errors reject instead of throwing synchronously out of a method declared to return a promise- The
ERR_INVALID_UNCOMPRESSED_SIZEerror now carriesoutputSize, which the most common corruption shape did not set - An entry written with
passThroughand no crc32 to declare is refused instead of being written with a wrong one, and AE-2 is written when encrypting passthrough content - The local header and the central directory now agree on the extended timestamp record
Documentation
- The
extendedTimestampremark no longer claims that EPUB allows an extra field on itsmimetypeentry. OCF states that there must not be one, which is what pins the byte offset ofapplication/epub+zipso a reader can sniff the format without parsing the archive, and epubcheck reports it as an error. ODF and EPUB need the same pair of options - Twelve other claims corrected against what the code does, among them: the precedence of the values the
entryoption reads, which beat the options of theZipWriterrather than being defaults against them; the condition under whichWARNING_CLAMPED_LAST_MODIFICATION_DATEappears; the combinations under whichLocalDirectory#rawFilenameis defined; the status codesERR_HTTP_STATUScovers; whatERR_INVALID_URIvalidates; and the determinability ofgetExportedSize()for names holding a slash, obsolete since implicit directories were exempted - The options of
ZipWriter#close()are documented as belonging to the caller closing the archive when aZipWriteris passed toexportZip(),globalCommentandsignCentralDirectoryincluded - The
entryoption points at the branch it selects for an encrypted source: it suppliesencrypted, so copying such an entry withpassThroughset totruewrites the ciphertext as-is and keeps the password it was encrypted with, while a password in scope encrypts the other entries only. Re-keying is what"compressed"is for
Tests and continuous integration
- The whole lint script runs in CI,
deno lintincluded, and the fidelity harness runs intest-ci - New fixture and test covering a Windows Explorer zip64 archive: a Deflate64 entry too large for 32 bits, in an archive carrying no zip64 end of central directory, with the two headers sentinelling different size fields
- New tests for the passthrough stages, the copy entry option, the async dispose path, the reader stream release, the writer warnings and the filesystem export into a
ZipWriter - The type tests are checked to be registered in a tsconfig, and the API export surface is audited across every build
Full Changelog: v2.11.4...v2.12.0