Files
node/test/parallel/test-zlib-zip-sync.js
T
Philipp Dunkel 5ba72ae7bc zlib: add ZipEntry, ZipFile, and ZipBuffer
Add ZIP archive support to the node:zlib module through three classes
and a set of helpers:

- ZipEntry: a single archive member, with buffered reads (content()),
  bounded-memory streaming reads (contentIterator()), and
  create()/createStream() for building members.
- ZipFile: random access to an archive backed by a file descriptor,
  reading members lazily without retaining their content and writing
  new members in place; opened with open()/openSync().
- ZipBuffer: a zero-copy, in-memory view over an archive already held
  in a Buffer.

createZipArchive() serializes a sequence of entries into an archive
byte stream, and setMaxZipContentSize() bounds the default in-memory
decompression size. Every operation has both an asynchronous and a
synchronous form.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
PR-URL: https://github.com/nodejs/node/pull/64339
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
2026-08-01 08:47:28 +00:00

248 lines
9.8 KiB
JavaScript

'use strict';
require('../common');
const assert = require('node:assert');
const zlib = require('node:zlib');
const fs = require('node:fs');
const path = require('node:path');
const tmpdir = require('../common/tmpdir');
const { test } = require('node:test');
tmpdir.refresh();
function buildArchiveSync(entries, comment) {
const chunks = [];
for (const chunk of zlib.createZipArchiveSync(entries, comment)) chunks.push(chunk);
return Buffer.concat(chunks);
}
function createTempZipSync(entries, comment) {
const dir = fs.mkdtempSync(path.join(tmpdir.path, 'zlib-zip-sync-'));
const filePath = path.join(dir, 'archive.zip');
fs.writeFileSync(filePath, buildArchiveSync(entries, comment));
return { filePath, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
}
// -- ZipEntry -----------------------------------------------------------------
test('ZipEntry.createSync()/contentSync() round-trip, deflate and store', () => {
const deflateEntry = zlib.ZipEntry.createSync('a.txt', Buffer.from('a'.repeat(1000)));
assert.strictEqual(deflateEntry.method, 8);
assert.strictEqual(deflateEntry.contentSync().toString(), 'a'.repeat(1000));
const storeEntry = zlib.ZipEntry.createSync('b.bin', Buffer.from([1, 2, 3]), { method: 'store' });
assert.strictEqual(storeEntry.method, 0);
assert.deepStrictEqual(storeEntry.contentSync(), Buffer.from([1, 2, 3]));
// Matches the crc32/size that the async ZipEntry.create() would produce.
const data = Buffer.from('some content to compare');
assert.strictEqual(zlib.ZipEntry.createSync('x', data).crc32, zlib.crc32(data));
});
test('ZipEntry.createSync()/contentSync() round-trip, zstd', () => {
const zstdEntry = zlib.ZipEntry.createSync('z.txt', Buffer.from('z'.repeat(1000)), { method: 'zstd' });
assert.strictEqual(zstdEntry.method, 93);
assert.strictEqual(zstdEntry.contentSync().toString(), 'z'.repeat(1000));
});
test('ZipEntry.contentSync() enforces maxSize and CRC verification like content()', () => {
const entry = zlib.ZipEntry.createSync('a.txt', Buffer.from('hello world'), { method: 'store' });
assert.throws(() => entry.contentSync({ maxSize: 1 }), { code: 'ERR_ZIP_ENTRY_TOO_LARGE' });
const archive = buildArchiveSync([entry]);
const tampered = Buffer.from(archive);
tampered[30 + 'a.txt'.length] ^= 0xff; // Flip a content byte.
const [tamperedEntry] = zlib.ZipEntry.read(tampered);
assert.throws(() => tamperedEntry.contentSync(), { code: 'ERR_ZIP_ENTRY_CORRUPT' });
assert.strictEqual(tamperedEntry.contentSync({ verify: false }).length, 'hello world'.length);
});
test('createZipArchiveSync() throws for a streaming (pending) entry', () => {
const streaming = zlib.ZipEntry.createStream('big.bin', (async function* () {})());
assert.throws(() => [...zlib.createZipArchiveSync([streaming])], { code: 'ERR_INVALID_STATE' });
});
// -- ZipBuffer ----------------------------------------------------------------
test('ZipBuffer.addSync()/toBufferSync() round-trip', () => {
const archive = buildArchiveSync([zlib.ZipEntry.createSync('a.txt', Buffer.from('a'))], 'a comment');
using zip = new zlib.ZipBuffer(archive);
const added = zip.addSync('b.txt', Buffer.from('b'));
assert.strictEqual(added.name, 'b.txt');
assert.strictEqual(zip.size, 2);
const rebuilt = zip.toBufferSync();
using reread = new zlib.ZipBuffer(rebuilt);
assert.deepStrictEqual([...reread.keys()].sort(), ['a.txt', 'b.txt']);
assert.strictEqual(reread.get('a.txt').contentSync().toString(), 'a');
assert.strictEqual(reread.get('b.txt').contentSync().toString(), 'b');
assert.strictEqual(reread.comment, 'a comment');
});
// -- ZipFile ------------------------------------------------------------------
test('ZipFile.openSync() reads the same entries as open()', async () => {
const { filePath, cleanup } = createTempZipSync([
zlib.ZipEntry.createSync('a.txt', Buffer.from('a')),
zlib.ZipEntry.createSync('b.bin', Buffer.from([1, 2, 3]), { method: 'store' }),
]);
try {
const zip = zlib.ZipFile.openSync(filePath);
try {
assert.strictEqual(zip.writable, false);
assert.strictEqual(zip.size, 2);
assert.strictEqual(zip.getSync('a.txt').contentSync().toString(), 'a');
assert.deepStrictEqual(zip.getSync('b.bin').contentSync(), Buffer.from([1, 2, 3]));
assert.deepStrictEqual([...zip.valuesSync()].map((e) => e.name).sort(), ['a.txt', 'b.bin']);
assert.deepStrictEqual([...zip.entriesSync()].map(([n]) => n).sort(), ['a.txt', 'b.bin']);
const seen = [];
zip.forEachSync((entry, name) => seen.push(name));
assert.deepStrictEqual(seen.sort(), ['a.txt', 'b.bin']);
} finally {
zip.closeSync();
}
const asyncZip = await zlib.ZipFile.open(filePath);
try {
assert.deepStrictEqual([...asyncZip.keys()].sort(), ['a.txt', 'b.bin']);
} finally {
await asyncZip.close();
}
} finally {
cleanup();
}
});
test('ZipFile opened via openSync supports `using` (Symbol.dispose)', () => {
const { filePath, cleanup } = createTempZipSync([zlib.ZipEntry.createSync('a.txt', Buffer.from('a'))]);
try {
{
using zip = zlib.ZipFile.openSync(filePath);
assert.strictEqual(zip.getSync('a.txt').contentSync().toString(), 'a');
}
// Disposed: the fd should be closed. Re-opening the same path must still work.
const reopened = zlib.ZipFile.openSync(filePath);
reopened.closeSync();
} finally {
cleanup();
}
});
test('ZipFile.addEntrySync()/addSync()/deleteSync() alter the file synchronously', () => {
const { filePath, cleanup } = createTempZipSync([
zlib.ZipEntry.createSync('a.txt', Buffer.from('AAAA'.repeat(20))),
zlib.ZipEntry.createSync('b.bin', Buffer.from([1, 2, 3]), { method: 'store' }),
]);
try {
const zip = zlib.ZipFile.openSync(filePath, { writable: true });
try {
assert.strictEqual(zip.writable, true);
const sizeBefore = fs.statSync(filePath).size;
const added = zip.addSync('c.txt', Buffer.from('a fresh member'));
assert.strictEqual(added.name, 'c.txt');
assert.strictEqual(zip.size, 3);
assert.ok(fs.statSync(filePath).size > sizeBefore);
assert.strictEqual(zip.getSync('c.txt').contentSync().toString(), 'a fresh member');
// Original entries untouched.
assert.strictEqual(zip.getSync('a.txt').contentSync().toString(), 'AAAA'.repeat(20));
const entry = zlib.ZipEntry.createSync('d.txt', Buffer.from('d'));
assert.strictEqual(zip.addEntrySync(entry), entry);
const sizeBeforeDelete = fs.statSync(filePath).size;
assert.strictEqual(zip.deleteSync('b.bin'), true);
assert.ok(fs.statSync(filePath).size < sizeBeforeDelete);
assert.strictEqual(zip.deleteSync('does-not-exist'), false);
} finally {
zip.closeSync();
}
const reread = zlib.ZipFile.openSync(filePath);
try {
assert.deepStrictEqual([...reread.keys()].sort(), ['a.txt', 'c.txt', 'd.txt']);
} finally {
reread.closeSync();
}
} finally {
cleanup();
}
});
test('ZipFile.addEntrySync() rejects a pending streaming entry', () => {
const { filePath, cleanup } = createTempZipSync([zlib.ZipEntry.createSync('a.txt', Buffer.from('a'))]);
try {
const zip = zlib.ZipFile.openSync(filePath, { writable: true });
try {
const streaming = zlib.ZipEntry.createStream('big.bin', (async function* () {})());
assert.throws(() => zip.addEntrySync(streaming), { code: 'ERR_INVALID_STATE' });
} finally {
zip.closeSync();
}
} finally {
cleanup();
}
});
test('ZipFile sync mutators throw ERR_ZIP_NOT_WRITABLE when not opened writable', () => {
const { filePath, cleanup } = createTempZipSync([zlib.ZipEntry.createSync('a.txt', Buffer.from('a'))]);
try {
const zip = zlib.ZipFile.openSync(filePath);
try {
assert.throws(() => zip.addSync('b.txt', Buffer.from('b')), { code: 'ERR_ZIP_NOT_WRITABLE' });
assert.throws(() => zip.deleteSync('a.txt'), { code: 'ERR_ZIP_NOT_WRITABLE' });
} finally {
zip.closeSync();
}
} finally {
cleanup();
}
});
test('ZipFile.compactSync() matches compact() and does not touch the open file', () => {
const { filePath, cleanup } = createTempZipSync([
zlib.ZipEntry.createSync('a.txt', Buffer.from('a'.repeat(1000))),
zlib.ZipEntry.createSync('b.txt', Buffer.from('b'.repeat(1000))),
]);
try {
const zip = zlib.ZipFile.openSync(filePath, { writable: true });
try {
zip.deleteSync('b.txt');
const sizeWithDeadSpace = fs.statSync(filePath).size;
const compacted = zip.compactSync();
assert.strictEqual(fs.statSync(filePath).size, sizeWithDeadSpace);
assert.ok(compacted.length < sizeWithDeadSpace);
using reread = new zlib.ZipBuffer(compacted);
assert.deepStrictEqual([...reread.keys()], ['a.txt']);
} finally {
zip.closeSync();
}
} finally {
cleanup();
}
});
test('a sync ZipFile method throws while an async mutation has not settled yet', async () => {
const { filePath, cleanup } = createTempZipSync([zlib.ZipEntry.createSync('seed.txt', Buffer.from('seed'))]);
try {
const zip = await zlib.ZipFile.open(filePath, { writable: true });
try {
// addEntry() (unlike add()) increments the busy counter synchronously,
// at call time - before any internal awaiting - so the checks below
// are guaranteed to observe the archive as busy.
const entry = zlib.ZipEntry.createSync('slow.txt', Buffer.from('x'));
const pending = zip.addEntry(entry);
assert.throws(() => zip.getSync('seed.txt'), { code: 'ERR_INVALID_STATE' });
assert.throws(() => zip.addSync('y.txt', Buffer.from('y')), { code: 'ERR_INVALID_STATE' });
assert.throws(() => zip.closeSync(), { code: 'ERR_INVALID_STATE' });
await pending;
// Once settled, sync methods work again.
assert.strictEqual(zip.getSync('seed.txt').contentSync().toString(), 'seed');
} finally {
await zip.close();
}
} finally {
cleanup();
}
});