Skip to content

Commit 0f2b879

Browse files
committed
zlib,vfs: add ZIP archive support
Add ZIP archive read/write support to zlib, plus a VFS provider that mounts a ZIP archive as a browsable, read/write file system. zlib gains three classes and a pair of helper functions: * ZipEntry represents a single archive member: name, metadata, and content, created from a Buffer/stream (`create()`/`createStream()`) or read back out of raw entry bytes (`read()`). It is otherwise immutable; every accessor and static creator has a synchronous and an asynchronous form. * ZipFile wraps a real ZIP file on disk (`open()`/`openSync()`), exposing get/add/delete/stream by name plus `compact()` to reclaim space left behind by deleted entries. Opened read-only unless `{ writable: true }` is passed. * ZipBuffer is the equivalent fully in-memory representation, serializable back to a Buffer with `toBuffer()`/`toBufferSync()`. Always writable, since nothing is written until asked to serialize. * `createZipArchive()`/`createZipArchiveSync()` build a brand-new archive (as a stream or a Buffer) from a list of entries. Every read path enforces `getMaxZipContentSize()`/ `setMaxZipContentSize()` limits and rejects malformed local/central directory records, guarding against zip-bomb and corrupt-archive inputs. New ERR_ZIP_* error codes cover corrupt entries, missing entries, oversized entries, invalid archives, non-writable archives, and unsupported ZIP features. node:vfs gains ArchiveProvider, a VirtualProvider backed by an already-open ZipBuffer or ZipFile: mounting one exposes the archive's entries through the same fs-like API any other VFS provider offers. Directories are inferred from entry-name prefixes rather than stored explicitly. Because a ZIP member can only be written or read in full, never edited in place, a file opened for writing is buffered and only committed as a new archive entry when its handle is closed. The provider's readonly flag mirrors the underlying archive's own writable state.
1 parent f91697a commit 0f2b879

19 files changed

Lines changed: 5572 additions & 4 deletions

doc/api/errors.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3674,6 +3674,50 @@ All attempts at serializing an uncaught exception from a worker thread failed.
36743674

36753675
The requested functionality is not supported in worker threads.
36763676

3677+
<a id="ERR_ZIP_ENTRY_CORRUPT"></a>
3678+
3679+
### `ERR_ZIP_ENTRY_CORRUPT`
3680+
3681+
A ZIP archive entry failed CRC-32 verification, or produced more or fewer
3682+
bytes than its declared uncompressed size, while being read.
3683+
3684+
<a id="ERR_ZIP_ENTRY_NOT_FOUND"></a>
3685+
3686+
### `ERR_ZIP_ENTRY_NOT_FOUND`
3687+
3688+
A named entry was requested from a [`ZipFile`][] or [`ZipBuffer`][] that does
3689+
not contain an entry with that name.
3690+
3691+
<a id="ERR_ZIP_ENTRY_TOO_LARGE"></a>
3692+
3693+
### `ERR_ZIP_ENTRY_TOO_LARGE`
3694+
3695+
A ZIP archive entry's declared size exceeds the configured limit, or a
3696+
provided entry name or comment exceeds the 65,535-byte encoded length that
3697+
the ZIP format allows.
3698+
3699+
<a id="ERR_ZIP_INVALID_ARCHIVE"></a>
3700+
3701+
### `ERR_ZIP_INVALID_ARCHIVE`
3702+
3703+
Data that was expected to be a ZIP archive, or a structure within one, is
3704+
missing, out of bounds, or otherwise inconsistent with the ZIP format.
3705+
3706+
<a id="ERR_ZIP_NOT_WRITABLE"></a>
3707+
3708+
### `ERR_ZIP_NOT_WRITABLE`
3709+
3710+
A mutating method (such as `zipFile.addEntry()` or `zipFile.delete()`) was
3711+
called on a [`ZipFile`][] that was not opened with `{ writable: true }`.
3712+
3713+
<a id="ERR_ZIP_UNSUPPORTED_FEATURE"></a>
3714+
3715+
### `ERR_ZIP_UNSUPPORTED_FEATURE`
3716+
3717+
A ZIP archive uses a feature outside of what this implementation supports,
3718+
such as entry encryption, an unsupported compression method, or a multi-disk
3719+
archive.
3720+
36773721
<a id="ERR_ZLIB_INITIALIZATION_FAILED"></a>
36783722

36793723
### `ERR_ZLIB_INITIALIZATION_FAILED`
@@ -4617,6 +4661,8 @@ An error occurred trying to allocate memory. This should never happen.
46174661
[`ServerResponse`]: http.md#class-httpserverresponse
46184662
[`Temporal`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
46194663
[`Writable`]: stream.md#class-streamwritable
4664+
[`ZipBuffer`]: zlib.md#class-zlibzipbuffer
4665+
[`ZipFile`]: zlib.md#class-zlibzipfile
46204666
[`child_process`]: child_process.md
46214667
[`cipher.getAuthTag()`]: crypto.md#ciphergetauthtag
46224668
[`crypto.getDiffieHellman()`]: crypto.md#cryptogetdiffiehellmangroupname

doc/api/vfs.md

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ Mounting a VFS only redirects supported [`node:fs`][] calls whose resolved paths
3838
are under the mount point. It does not prevent code from using other paths or
3939
other Node.js APIs to access resources available to the process.
4040
[`RealFSProvider`][] maps VFS paths under its configured root and rejects paths
41-
that resolve outside that root, but that check is not a security boundary. Do
42-
not rely on VFS to run untrusted code; use operating-system-level isolation,
43-
such as separate users, containers, or platform sandboxes, when a security
44-
boundary is required.
41+
that resolve outside that root, but that check is not a security boundary.
42+
[`ArchiveProvider`][] has no real file-system paths of its own to escape; its
43+
entries only ever exist within the archive's own namespace. Do not rely on VFS
44+
to run untrusted code; use operating-system-level isolation, such as separate
45+
users, containers, or platform sandboxes, when a security boundary is
46+
required.
4547

4648
## Basic usage
4749

@@ -304,6 +306,55 @@ added: v26.4.0
304306

305307
The resolved absolute path used as the root.
306308

309+
## Class: `ArchiveProvider`
310+
311+
<!-- YAML
312+
added: REPLACEME
313+
-->
314+
315+
A provider that exposes the entries of a ZIP archive - either a
316+
[`zlib.ZipBuffer`][] (in memory) or a [`zlib.ZipFile`][] (on disk) - through
317+
the VFS API. `provider.readonly` reflects the archive's own
318+
[`zipFile.writable`][] flag: a `ZipBuffer` is always writable, and a
319+
`ZipFile` is writable only when opened with `{ writable: true }`.
320+
321+
Directories are recognized both explicitly (an entry whose name ends in `/`)
322+
and implicitly (any entry name starting with `"<dir>/"`). `readdir()` does
323+
not support `{ recursive: true }`. Because a ZIP member cannot be edited or
324+
read in place - only fully written or fully decompressed - a file opened for
325+
writing only commits its content (as a new archive entry) when the handle is
326+
closed.
327+
328+
Every method has a synchronous counterpart (`openSync()`, `statSync()`,
329+
`readdirSync()`, and so on), backed by the equally complete synchronous
330+
surface [`zlib.ZipBuffer`][]/[`zlib.ZipFile`][] expose. As with those, the
331+
synchronous methods here block the Node.js event loop and further JavaScript
332+
execution until the operation - including any deflate/inflate pass -
333+
completes.
334+
335+
```cjs
336+
const vfs = require('node:vfs');
337+
const zlib = require('node:zlib');
338+
const { readFileSync } = require('node:fs');
339+
340+
async function main() {
341+
const zip = new zlib.ZipBuffer(readFileSync('archive.zip'));
342+
const archiveVfs = vfs.create(new vfs.ArchiveProvider(zip));
343+
344+
console.log(await archiveVfs.promises.readdir('/'));
345+
await archiveVfs.promises.writeFile('/new.txt', 'hello');
346+
}
347+
main();
348+
```
349+
350+
### `new ArchiveProvider(source)`
351+
352+
<!-- YAML
353+
added: REPLACEME
354+
-->
355+
356+
* `source` {zlib.ZipBuffer|zlib.ZipFile} An already-open archive.
357+
307358
## Implementation details
308359

309360
### `Stats` objects
@@ -318,10 +369,14 @@ fields use synthetic but stable values:
318369
* `blocks` is `Math.ceil(size / 512)`.
319370
* Times default to the moment the entry was created/last modified.
320371

372+
[`ArchiveProvider`]: #class-archiveprovider
321373
[`MemoryProvider`]: #class-memoryprovider
322374
[`RealFSProvider`]: #class-realfsprovider
323375
[`VirtualFileSystem`]: #class-virtualfilesystem
324376
[`VirtualProvider`]: #class-virtualprovider
325377
[`fs.BigIntStats`]: fs.md#class-fsbigintstats
326378
[`fs.Stats`]: fs.md#class-fsstats
327379
[`node:fs`]: fs.md
380+
[`zipFile.writable`]: zlib.md#zipfilewritable
381+
[`zlib.ZipBuffer`]: zlib.md#class-zlibzipbuffer
382+
[`zlib.ZipFile`]: zlib.md#class-zlibzipfile

0 commit comments

Comments
 (0)