What's Changed in v2.8.54
Breaking changes
ZipWriter#prependZip()now reads an array of readers as the disks of a split zip file, which is what an array denotes everywhere else in the API, and accepts aSplitDataReaderinstance the same way. The disks are read in order and the entries are relocated to the positions they get in the output. An array used to be concatenated and read as a single archive, which produced wrong offsets for a real split zip file. If you were passing an array of byte ranges of one zip file, concatenate them yourself and pass a single readerZipEntry#moveTo()is removed. It was deprecated and undeclared in the TypeScript definitions, and was a one-line alias ofZipFS#move(), which is the method to use- The undeclared
ZipFS#addData()andZipDirectoryEntry#addData()methods are removed. They were internal, never documented and never declared. The typedaddText(),addBlob(),addUint8Array(),addData64URI(),addHttpContent(),addReadable(),addFile(),addFileSystemEntry()andaddFileSystemHandle()methods cover what they did
Security
Both fixes below are reachable from an untrusted zip file read with ZipReader. Upgrading is recommended for anyone reading archives they did not produce.
FileEntry#getData()now throws the newERR_ENTRY_DATA_OUT_OF_BOUNDSwhen the declared data of an entry, i.e. its offset plus its compressed size, ends past the end of the zip file. Such an entry used to makegetData()hang for ever, with no error and no CPU use, so nothing timed out and nothing showed up in a profile. Honestly truncated archives are affected as much as malformed ones. A read past the end of the source now ends the stream instead of stalling it, which also covers the entries the bounds check cannot detect in advance- The output of
FileEntry#getData()is no longer allocated from the declared uncompressed size of the entry. The size was reserved before a byte was read, so a 131-byte archive declaring 3 GiB reserved 3 GiB. The allocation is clamped to what the compressed data can decode to,compressedSize * 1032for a compressed entry andcompressedSizefor a stored one, 1032 being the maximum expansion ratio of Deflate. The clamp never binds on real data, a legitimate archive still preallocates exactly its uncompressed size
New features
ZipFS,ZipEntry,ZipFileEntryandZipDirectoryEntryare now exported at the top level, and thefsnamespace is deprecated. Replacenew zip.fs.FS()withnew zip.ZipFS(), andzip.fs.ZipFileEntrywithzip.ZipFileEntry.zip.fskeeps working and the library emits no runtime warning, the deprecation is documentation only.ZipEntryis now a value as well, soentry instanceof zip.ZipEntryworks. The three entry classes were already declared as top-level exports but existed at runtime underzip.fs.*only, so importing them type-checked and then failed. In TypeScript, theFStype is deprecated and kept as an alias ofZipFS, solet fs: FSkeeps compiling- New
ZipEntry#setOptions()method andZipEntry#optionsproperty in the filesystem API.setOptions()merges the options into the ones the entry was added with, an option set toundefinedbeing removed instead of stored, and they are applied when the zip file is exported. It is the way to set the options of an entry imported from a zip file, which has none until it is called. The options describing the data of an entry exported withpassThrough, e.g.compressionMethodanduncompressedSize, are ignored, they are always the ones of the original entry, and so aredirectoryand the progress callbacks ZipWriter#prependZip()now writes a correct split zip file when the writer is a split zip file writer. The whole prepended archive used to be copied into the first disk, so every entry recorded an offset on the wrong disk. The data is copied disk by disk now, a disk is closed before an entry whose local file header would not fit in what is left of it, and each entry records the disk it starts on and its offset in that disk. The output also starts with the split zip file signature, unless the prepended zip file already carries oneTextWriternow decodes CP437.new TextWriter("cp437")used to return the data decoded as UTF-8, since the encoding was handed toFileReader#readAsText(), which falls back to UTF-8 for a label it does not know. It goes through the same decoder as the filenames and the comments now. It also decodes withTextDecoderinstead ofFileReader, which removes the last dependency on that class, missing from some worker scopes. The byte order mark is still removed, whichever branch decodes the data- The second argument of the codec stream constructors is now typed, by the exported
CompressionStreamOptionsandDecompressionStreamOptionsinterfaces. They document which members are set for which class, e.g.deflate64only for the deflate implementations, andrawBitFlag,compressionMethodanduncompressedSizeonly for the codecs registered withregisterCodec().Configuration#CompressionStream,Configuration#DecompressionStream, their*Fallbackand deprecated*Zlibforms andCodecDefinitionare declared with them instead of the untypedTransformStreamLike. This only concerns you if you pass a custom stream implementation or callregisterCodec() Configuration#baseURIis now declared. It resolves the relativeworkerURI,wasmURIandcodecURIvalues, and defaults to the URL of the module of zip.jsWritableWriter#sizeis now declared. zip.js sets it to 0 before the first write and keeps it updated, so a customWritercan read how many bytes have been written so far, e.g. to compute the offset of a disk. It is declared onWriter,TextWriter,BlobWriter,SplitDataWriterandUint8ArrayWriteras well- Members that existed and were not declared:
HttpReader#url,TextWriter#encoding,BlobWriter#contentType,Data64URIWriter#contentType,EntryError#overlappingEntryandEntryError#reason.overlappingEntryis the only way to identify the other entry of the pair reported byERR_OVERLAPPING_ENTRY, andreasondescribes the ambiguity reported byERR_AMBIGUOUS_ARCHIVE
Behavior changes
The options listed first used to accept values of the wrong type and produced a wrong, empty or silently dropped result. They throw now. If your code passes the documented types, nothing changes.
lastModDate,lastAccessDateandcreationDatemust beDateinstances and throw the newERR_INVALID_DATEotherwise. An invalidDateused to be written as an entry carrying no timestamp at all. A timestamp expressed in milliseconds is the natural mistake and is rejected as well: passnew Date(file.lastModified), notfile.lastModified- The
commentoption of an entry must be a string and throws the newERR_INVALID_ENTRY_COMMENT_TYPEotherwise. AUint8Arrayused to be coerced and its textual representation written into the archive. Decode the bytes to a string before passing them - The
extraFieldoption must be aMap, and throws the newERR_INVALID_EXTRAFIELDotherwise. Its keys must be integers between 0 and 65535, andERR_INVALID_EXTRAFIELD_TYPEnow covers a non-integer or a negative key as well as a key above 65535. Its values must beUint8Arrayinstances, and throw the newERR_INVALID_EXTRAFIELD_DATA_TYPEotherwise - The
readerOptionsoption ofZipDirectoryEntry#export*(),ZipDirectoryEntry#getExportedSize()andZipDirectoryEntry#exportFileSystemHandle()must be an object and throws the newERR_INVALID_READER_OPTIONSotherwise. A value of another type was silently ignored: a password passed as a string instead of an object failed with the unrelatedERR_ENCRYPTED, while the other options were dropped without any error. An unknown property of areaderOptionsobject is still ignored, as everywhere else in the API - The options expecting a function throw the new
ERR_INVALID_FUNCTION_OPTIONwhen they are given a value of another type:encodeText,decodeText,createTempStream,signCentralDirectoryanddecryptCentralDirectory. A falsy value keeps meaning "use the default" - The
signaloption throws the newERR_INVALID_SIGNALwhen it does not look like anAbortSignal, i.e. when it does not expose anaddEventListener()method and a booleanabortedproperty. Duck-typed signals and signals coming from another realm keep working - The
passwordandrawPasswordoptions are now checked on the reader side as well, throwingERR_INVALID_PASSWORD_TYPE. A value of another type used to fail with the unrelatedERR_ENCRYPTEDorERR_INVALID_PASSWORD msdosAttributesRawthrowsERR_INVALID_MSDOS_ATTRIBUTESwhen the value is not an integer, and accepts a numeric string like the other numeric options. The range check used to be the only one, and the bitwise arithmetic folding the value into the external file attributes did the rest quietly: a fractional value was truncated, and a value that is not a number at all passed both comparisons and was written as 0.msdosAttributesthrowsERR_INVALID_MSDOS_DATAon an array, which used to be accepted as an object and wrote 0 as well, since none of the flag properties exist on itconfigure()andsetDefaultConfiguration()reject two kinds of bad input instead of storing them.maxWorkersmust be an integer greater than 0 and throws the newERR_INVALID_MAX_WORKERSotherwise: a value lower than 1 used to deadlockZipWriter#add()for ever, since no entry could start and none could release the next one.createWorkerand theCompressionStreamandDecompressionStreamoptions, including their*Fallbackand deprecated*Zlibforms, must be functions and throwERR_INVALID_FUNCTION_OPTIONotherwise, a falsy value still meaning "use the default". The numeric options accept a numeric string and are coerced, like the numeric options of the reader and the writer. Nothing is stored unless the whole call passes, so a rejected call leaves the configuration untouchedchunkSizeis normalized wherever it is read. A value lower than 64 is raised to 64, as before, and a value that is not an integer greater than 0 now falls back to the default of 65536 instead of being used as it is. This applies to the global configuration and to thechunkSizeoption ofReader#createReadable()
The rest of this section changes results rather than rejecting input.
- The configuration is read when it is used instead of when the reader or the writer is constructed. A
configure()call made betweennew ZipWriter()and the firstadd(), or betweennew ZipReader()and the firstgetData(), used to be ignored and is honored now. It affectsmaxWorkers,chunkSize, the compression stream implementations and the deflate support detection - The
executableoption now counts as Unix metadata, likeunixMode. It means a mode of0o755, and it was the only Unix metadata option taking the MS-DOS branch, so an entry written withexecutableset next tomsDosCompatible,msdosAttributesormsdosAttributesRawlost its executable bit without a word. It wins over the three of them now, and selects the Unix platform for the "Version made by" field.executableset tofalsechanges nothing, as before - The entries of the filesystem API are dated when they are added, not when they are written. An entry added without a
lastModDateoption used to be stamped with the current date at export time, so exporting an unchanged tree twice produced different bytes. The four sources of the date of an exported entry now rank as follows, weakest first: the moment the entry was added, the date of the entry the tree was imported from, thelastModDateoption passed to the export, and thelastModDateoption passed when the entry was added - The directories implied by the name of an imported entry are no longer written back when the tree is exported. Importing a zip file storing
"a/b.txt"and no directory entry creates a navigable"a"entry, which used to be exported as an entry of its own, so a round trip gained one entry per path component. Only the directories carried by the source zip file and the ones created withaddDirectory()are written now.getExportedSize()and the progress callbacks count them the same way - The options passed when an entry was added no longer override the values describing the data of an entry exported with
passThrough.compressionMethod,uncompressedSizeand the other pass-through values now win over the per-entry options, as they already did over the options passed to the export - The options passed when an entry is added are now copied. The object was stored as it was, so mutating it afterwards, or reusing one object for several entries and mutating it in between, changed entries that had already been added
- The split zip file signature at the start of a zip file is no longer reported as prepended data. A single-disk archive written by
SplitDataWriterstarts with the four bytesPK\x07\x08, which used to be read as prepended data byextractPrependedDataand rejected as an ambiguous archive bystrictnessset to"strict"or bycheckAmbiguity. The temporary spanning markerPK00, which PKZIP writes at the start of the first disk while a spanned archive is being created, is accepted the same way. If you read archives produced bySplitDataWriterwith either of those options, they are no longer misreported
Bug fixes
ZipWriter#prependZip()no longer copies the central directory of the zip file it prepends. The whole source was piped into the output, then the entries were rewritten after it, so every prepended archive carried its old central directory as dead bytes in the middle of the result. Only the data region is copied now, i.e. everything before the first byte of the central directory. Archives already produced this way are still readable, they are only larger than they need to beZipWriter#prependZip()reads the zip file it prepends once instead of twice. A reader providingreadUint8Array()was buffered into aBlobbecause the central directory was read throughreader.readable, so aBlobReaderor anHttpRangeReaderwas fully downloaded into memory before anything was written- The transferred streams are no longer broken in a web worker installing a polyfill of the Streams API, the setup documented under
Configuration#createWorkerbelow. The worker received a nativeReadableStreamand a nativeWritableStreamthroughpostMessage()and piped them into streams of the polyfill, which rejected them. They go through the same compatibility wrappers as the streams created in the worker now
Documentation
msdosAttributesRawandmsdosAttributesnow describe the platform they select. The behavior is unchanged: setting either of them selects the MS-DOS platform for the entry exactly asmsDosCompatibleset totruedoes, and overrides that option when it is explicitly set tofalse, soversionMadeByloses its Unix upper byte and no Unix mode is written. What counts is that the option is provided, not its value, so0and{}select it too. Any Unix metadata option wins over the three of them, with the MS-DOS attributes written into the low byte, see theexecutablechange aboveZipWriter#prependZip()now states that the data of the zip file is copied, its central directory rebuilt and its entries relocated, so the disks of a split zip file passed as input are unrelated to the disks of the outputZipReader#commentand thecommentoption of an entry now explain why one is bytes and the other a string. The encoding of the comment of an entry is recorded in its header by the general purpose bit 11, the encoding of the global comment is recorded nowhere, so it can only be decoded with the encoding agreed with the producer of the zip fileConfiguration#createWorkernow documents how to install a polyfill of the Streams API in the scope of the worker, which is the way to run the web workers on the engines whereTransformStreamis missing from that scope, e.g. Firefox before version 102. A polyfill imported by the page does not help, because the worker reads the globals of the Streams API from its own scopeConfiguration#workerURInow states that the worker is created as a module worker, unless the URI is a Data URI or a Blob URI, in which case it is created as a classic workerZipDirectoryEntryExportOptionsnow documents the precedence of the four sources of the last modification date of an exported entry, andZipDirectoryEntry#importZip()documents that the directories implied by an entry name are not written backFile#lastModifiedis now a link to MDN in the generated documentation, instead of an unresolved reference
Tests and continuous integration
- A new audit checks that every property name reaching the public API is either declared in
index.d.tsor mangled on purpose. It reads the terser configuration and the declarations, walks the objects the library builds at runtime and reports the names belonging to neither list. It found eight internal fields shipped under their source names in the minified builds, e.g. the pending characters ofData64URIWriterand the source blob ofBlobReader, which are mangled now, two of them renamed on the way. The parameter names of the declarations are no longer reserved either: a parameter name is not a property name, and reserving it kept a field of the same name readable in every build - The internal methods writing the records of a zip file are renamed away from the DOM property names they shared, so that they are mangled instead of being kept by the terser reserved list
- Every public class is instantiated by the audit, so a member appearing only on an instance is covered. The classes reached only through a subclass, e.g.
ZipEntry, are matched by walking the prototype chain - The deprecated
checkSignatureoption is covered by a test of its own, and the rest of the suite usescheckCrc32. The option had lost all its usages when the tests were swept, so nothing exercised it any more - The
zip.fsnamespace is covered by a test of its own, for the same reason - The polyfill of the Streams API in the worker is tested with a classic worker, a module worker and the native build, one test per build since the WASM worker and the native worker bundle different codecs. The web runner gained a
nativeBuildfeature probe and caches the build probe instead of running it per test - New tests: the entries whose declared data extends past the end of the archive, the options of an entry of the filesystem API, the pass-through values against the per-entry options, the stability of the dates of an exported tree, the directories implied by an imported entry name, the encoding of
TextWriter, the single read ofprependZip(), the bytes it copies, its split zip file output, and the strictness of the split zip file signature - An option validation assertion is split into a test file of its own, since it needs the
signaloption ofpipeTo(), which Chrome 76 to 79 ignore. The runner skips the file there rather than reporting a failure - The release is now scripted.
npm run bump-patchbumps the version inpackage.json,package-lock.jsonanddeno.jsonand commits it, the version script of npm syncingdeno.json. A workflow step verifies that the three declared versions agree, and the release workflow verifies that the released tag matches the version declared inpackage.json
Credits
- Andrew Chin, Brian J Lee and Youngjoon Kim, SSLab at Georgia Tech, reported the declared uncompressed size of an entry driving the allocation of the output
- Claude (Opus 5) contributed to every change listed above
Full Changelog: v2.8.53...v2.8.54