Skip to content

v2.8.53

Latest

Choose a tag to compare

@gildas-lormeau gildas-lormeau released this 19 Aug 01:32
· 4 commits to master since this release

What's Changed in v2.8.53

New features

  • New checkLocalDirectory option in the reader options. It compares the local file header of an entry against its central directory record when FileEntry#getData() is called, and throws ERR_AMBIGUOUS_ARCHIVE when the two disagree. true compares the filename, the general purpose bit flag, the compression method, the CRC-32 checksum and the sizes, like strictness set to "strict"; false compares nothing, like "tolerant". Setting it explicitly always wins over strictness, whether strictness was passed to the constructor of ZipReader or to the call, so it is the way to ask for this one check without the archive-level checks of checkAmbiguity, and the way to drop it without giving up the other checks strictness performs. It is also the only way to validate the local file headers of a self-extracting archive, since checkAmbiguity rejects prepended data outright
  • The local file header of an entry now reports the two records the reader had already read and dropped. LocalDirectory#rawFilename holds the filename stored in the local file header, which is allowed to differ from EntryMetaData#rawFilename, and is defined when strictness is "strict" or checkLocalDirectory is true. LocalDirectory#dataDescriptor holds the data descriptor record written after the content, described by the new LocalDataDescriptor interface, and is defined when checkOverlappingEntry or checkOverlappingEntryOnly is set. It carries the CRC-32 checksum and the sizes stored in the record, each of which is allowed to differ from the central directory, and a signature flag telling whether the record is preceded by its optional signature. That signature is not part of the original format, it is a later convention writers are free to follow. When the four bytes look like the signature but the values behind them disagree with the central directory, the flag is false and the record is read as starting at those four bytes instead
  • The parsed extra field records are now typed instead of being declared as the bare EntryExtraField. EntryExtraFieldZip64, EntryExtraFieldNTFS, EntryExtraFieldExtendedTimestamp and EntryExtraFieldUnix describe the members the reader fills in, EntryExtraFieldUnicode gains version, filename and comment, and EntryExtraFieldAES gains compressionMethod, the real compression method of the entry, next to originalCompressionMethod, which is the 99 a WinZip AES header is required to carry in its place
  • SplitDataReader now accepts an array of Reader instances, of ReadableReader instances or of ReadableStream instances. The last two were declared in the TypeScript definitions and worked nowhere: reading a split archive requires the size of every disk to map a global offset onto one of them, so an element that only provides a stream is now buffered when the reader is initialized. This applies wherever an array of readers is accepted, i.e. the constructor of ZipReader, ZipWriter#add(), ZipWriter#prependZip(), ZipDirectoryEntry#importZip() and the reader property of a ZipFileEntry instance
  • ZipWriter#prependZip() now accepts a reader that only provides a ReadableStream. It reads the central directory of the archive it prepends before piping it, so passing a stream used to fail with TypeError: ReadableStream is already locked. The stream is buffered once, like the disks above
  • New ERR_INVALID_COMMENT_TYPE error constant

Behavior changes

  • The local file header of an entry is now compared against its central directory record by default, except for the filename. strictness set to "balanced", the default, used to trust the central directory record entirely; getData() now throws ERR_AMBIGUOUS_ARCHIVE when the general purpose bit flag, the compression method, the CRC-32 checksum or the sizes disagree. getEntries() is unaffected, the local file header is only read when the data is. This costs nothing: every one of those fields is read from the local file header anyway to locate the entry data. Only the filename is left out, because comparing it reads the filename bytes as well, which costs one extra read per entry whenever the local file header carries no extra field, the common case. "strict" still compares the filename too, "tolerant" still compares nothing, and checkLocalDirectory set to false restores the previous behavior. The new default was verified against 458,000 entries of real archives, where it rejects none of them
  • An explicit checkAmbiguity now wins over an inherited strictness. checkAmbiguity is the boolean form of strictness, true meaning "strict", and the two used to be resolved without regard to where they came from, so a checkAmbiguity passed to getEntries() or to getData() could not relax a strictness passed to the constructor of ZipReader. A value passed to the call now wins over a value passed to the constructor, and strictness still wins over checkAmbiguity when both are passed to the same one. checkAmbiguity set to false means "not strict" rather than "trust everything", so it downgrades an inherited "strict" to "balanced" and leaves an inherited "tolerant" alone; pass strictness set to "tolerant" to compare nothing. Code that passes strictness and never checkAmbiguity resolves exactly as before
  • The platform byte of the "Version made by" field is now forced instead of being merged into the value given by the versionMadeBy option. It is set to Unix (3) when the entry carries Unix metadata, i.e. when uid, gid, unixMode or unixExtraFieldType is set, and to MS-DOS (0) when msdosAttributes or msdosAttributesRaw is set. Only the lower byte of the given value survives in both cases. It used to be combined with the byte already present, so a versionMadeBy carrying another platform produced a value belonging to neither
  • ZipWriter#close() now throws the new ERR_INVALID_COMMENT_TYPE error when the comment it is given is not a Uint8Array. Passing a string, the natural mistake, used to fail deep inside the writer with TypeError: Cannot read properties of undefined (reading 'byteLength'), after the entries had been written. getExportedSize() performs the same check on the globalComment option

Bug fixes

  • The Unix user and group ids are now read from the local file header when the central directory has none. The Info-ZIP Unix type 2 extra field (0x7855) stores them in the local file header only and leaves a zero-length copy in the central directory, so uid and gid were undefined on every archive written by Info-ZIP. They are filled in when the data of the entry is read: they are still undefined after getEntries() and appear once getData() has run, since that is when the local file header is read, and they are also readable on EntryMetaData#localDirectory. A value read from the central directory is never overwritten by the local file header, since the type 2 field truncates the ids to 16 bits while the New Unix field (0x7875) does not
  • An empty Info-ZIP Unix type 2 extra field no longer hides the ids of the Info-ZIP New Unix extra field next to it. The reader looked at 0x7875 only when 0x7855 was absent, so an entry carrying both, which is what Info-ZIP writes, reported no ids at all although 0x7875 held them
  • EntryMetaData#rawLastAccessDate and EntryMetaData#rawCreationDate are now filled from the NTFS extra field. They were declared but never set: the raw FILETIME values were stored on the extra field record only. EntryMetaData#rawLastModDate is unaffected, it remains the MS-DOS date and time stored in the header
  • The entries returned by ZipReader#getEntries() now carry rawBitFlag, filenameLength, extraFieldLength and unixExternalUpper. The four properties were declared on EntryMetaData and read from the central directory, they were simply dropped when the entry object was built
  • unixExternalUpper is now the upper half of the externalFileAttributes the entry was written with, on the entry returned by ZipWriter#add(). It was computed before the unixMode option and the Unix file type were folded in, so it reported the default 0o644 for every entry, whatever the mode: an entry written with 0o120777 disagreed both with its own externalFileAttributes and with what the reader reports for it
  • A worker that fails to load now falls back to the main scope instead of throwing a TypeError. The codec pool builds the worker and its interface, then calls it back one turn later; when the error event of the worker arrived in that interval, the error was dropped and the pool went on to post a message to a worker it had already discarded, which failed with Cannot read properties of null (reading 'postMessage'). The designed fallback now runs in that case too, with the error of the worker as the reason. This affects the engines where a worker cannot be started at all, e.g. Firefox extensions using manifest v2 and Chromium 76 to 79
  • The entry returned by ZipWriter#add() now defines the same members as the entries returned by ZipReader#getEntries(): zip64, symlink, encrypted, zipCrypto and msDosCompatible were left undefined instead of false on one side or the other, and the deprecated internalFileAttribute and externalFileAttribute aliases were missing from it

Documentation

  • The strictness option now lists the fields each level compares, and states which of them are read from the local file header anyway
  • The versionMadeBy, msDosCompatible and unixMode options now describe how the platform byte and the Unix file type are chosen, including the fact that a folder entry is always written with S_IFDIR whatever type the mode carries
  • The symlink property now points at the option that writes a symbolic link, since there is no option of that name: the file type goes in unixMode, i.e. 0o120777 with the path of the target as the content of the entry
  • Several documented defaults disagreed with the code and were corrected: maxWorkers falls back to 2 when the environment provides no navigator.hardwareConcurrency, workerURI points at the worker of the build that was imported, the four CompressionStream and DecompressionStream options default to the global implementations or to the one embedded in the entry point, lastAccessDate and creationDate have no default at all so that the entries do not carry a meaningless time, and versionMadeBy defaults to 768 rather than 20
  • preventHeadRequest now states that leaving it unset is not the same as setting it to false when useRangeHeader or forceRangeRequests is set: the size is then read from a ranged GET request, and only an explicit false restores the HEAD request
  • rawLastModDate now states that it is the MS-DOS date and time of the header and is not replaced by the value of the NTFS extra field, unlike lastModDate

Tests and continuous integration

  • A workflow step rebuilds the project on every push and fails when the committed build output differs. The release workflow publishes the committed files as they are, it never rebuilds, so a stale dist/ or index.min.js at a tag would ship to npm and JSR. Pull requests are exempt, asking outside contributors to commit build output would add an unreviewable diff to every change
  • The release workflow now publishes only when the test suite passed on the released commit
  • A new audit compares the shapes of the objects the library builds at runtime against the interfaces declared in index.d.ts, in addition to the audit of the read and write surfaces added in v2.8.52. It is what found the members left undefined and the extra field records declared as the bare EntryExtraField
  • A regression test covers the backpressure of the writer on the web worker path
  • The browser runner restarts the browser and runs the suite again when the session is lost, and its --headful option was renamed to --headed
  • The Safari job runs on macOS 15 instead of macOS latest, where the browser loses its window or its session in the middle of the suite more often
  • The workers are terminated between the tests in the Node.js, Deno and Bun runners, and the Bun runner sets its own timeout
  • A test reproduces the ordering that made a failing worker throw instead of falling back, i.e. the error of the worker arriving before the pool sends it its first message

Credits

  • Claude (Opus 5) contributed to every change listed above

Full Changelog: v2.8.52...v2.8.53