mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 10:45:11 +08:00
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>
187 lines
5.9 KiB
JavaScript
187 lines
5.9 KiB
JavaScript
'use strict';
|
|
|
|
// `ZipBuffer`: an in-memory, writable view over the entries of an archive
|
|
// held in a `Buffer`, serializing the current set back out with
|
|
// `toBuffer()`/`toBufferSync()`.
|
|
|
|
const {
|
|
ArrayPrototypePush,
|
|
FunctionPrototypeCall,
|
|
Map,
|
|
MapPrototypeClear,
|
|
MapPrototypeDelete,
|
|
MapPrototypeEntries,
|
|
MapPrototypeGet,
|
|
MapPrototypeGetSize,
|
|
MapPrototypeHas,
|
|
MapPrototypeKeys,
|
|
MapPrototypeSet,
|
|
SymbolDispose,
|
|
SymbolIterator,
|
|
SymbolToStringTag,
|
|
} = primordials;
|
|
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_ZIP_ENTRY_NOT_FOUND,
|
|
},
|
|
} = require('internal/errors');
|
|
const {
|
|
validateFunction,
|
|
validateString,
|
|
} = require('internal/validators');
|
|
const { Buffer } = require('buffer');
|
|
const { toBuffer } = require('internal/zip/binary');
|
|
const { decodeZipText } = require('internal/zip/dos');
|
|
const { findArchiveEnd } = require('internal/zip/headers');
|
|
const {
|
|
readArchiveEntries,
|
|
ZipEntry,
|
|
} = require('internal/zip/entry');
|
|
const {
|
|
createZipArchive,
|
|
createZipArchiveSync,
|
|
normalizeArchiveOptions,
|
|
} = require('internal/zip/archive');
|
|
|
|
/**
|
|
* An in-memory view over the entries of a ZIP archive, writable in place:
|
|
* entries can be added or removed, and `toBuffer()` serializes the current
|
|
* set of entries into a fresh archive.
|
|
*/
|
|
class ZipBuffer {
|
|
#entries = new Map();
|
|
#comment;
|
|
|
|
/**
|
|
* Parses an existing archive's central directory into an in-memory,
|
|
* editable map of entries keyed by name.
|
|
* @param {Buffer | TypedArray | DataView | ArrayBuffer} buffer
|
|
*/
|
|
constructor(buffer) {
|
|
const buf = toBuffer(buffer, 'buffer');
|
|
// Locate the archive end once; it supplies both the comment and the
|
|
// central-directory bounds for the entry walk.
|
|
const end = findArchiveEnd(buf);
|
|
this.#comment = end.comment;
|
|
for (const entry of readArchiveEntries(buf, end)) {
|
|
MapPrototypeSet(this.#entries, entry.name, entry);
|
|
}
|
|
}
|
|
get writable() { return true; }
|
|
// The EOCD comment has no encoding flag; apply the same UTF-8/CP437
|
|
// heuristic as unflagged member names and comments.
|
|
get comment() { return decodeZipText(this.#comment, 0); }
|
|
has(name) {
|
|
validateString(name, 'name');
|
|
return MapPrototypeHas(this.#entries, name);
|
|
}
|
|
get(name) {
|
|
validateString(name, 'name');
|
|
const entry = MapPrototypeGet(this.#entries, name);
|
|
if (entry === undefined) throw new ERR_ZIP_ENTRY_NOT_FOUND(name);
|
|
return entry;
|
|
}
|
|
/**
|
|
* Adds an already-built entry, keyed by its own name (replacing any
|
|
* existing entry of that name).
|
|
* @param {ZipEntry} entry
|
|
* @returns {ZipEntry}
|
|
*/
|
|
addEntry(entry) {
|
|
if (!(entry instanceof ZipEntry)) {
|
|
throw new ERR_INVALID_ARG_TYPE('entry', 'ZipEntry', entry);
|
|
}
|
|
MapPrototypeSet(this.#entries, entry.name, entry);
|
|
return entry;
|
|
}
|
|
/**
|
|
* Builds an entry from in-memory `data` and adds it (replacing any entry of
|
|
* the same name).
|
|
* @param {string} filename
|
|
* @param {Buffer | TypedArray | DataView | ArrayBuffer} data
|
|
* @param {{ comment?: string, mode?: number, modified?: Date, method?: 'deflate' | 'store' | 'zstd' }} [options]
|
|
* @returns {Promise<ZipEntry>}
|
|
*/
|
|
async add(filename, data, options) {
|
|
return this.addEntry(await ZipEntry.create(filename, data, options));
|
|
}
|
|
/**
|
|
* The synchronous counterpart of `add()`. Blocks the event loop and
|
|
* further JavaScript execution until done; see `zipEntry.contentSync()`.
|
|
* @param {string} filename
|
|
* @param {Buffer | TypedArray | DataView | ArrayBuffer} data
|
|
* @param {{ comment?: string, mode?: number, modified?: Date, method?: 'deflate' | 'store' | 'zstd' }} [options]
|
|
* @returns {ZipEntry}
|
|
*/
|
|
addSync(filename, data, options) {
|
|
return this.addEntry(ZipEntry.createSync(filename, data, options));
|
|
}
|
|
/**
|
|
* @param {string} name
|
|
* @returns {boolean}
|
|
*/
|
|
delete(name) {
|
|
validateString(name, 'name');
|
|
return MapPrototypeDelete(this.#entries, name);
|
|
}
|
|
clear() {
|
|
MapPrototypeClear(this.#entries);
|
|
}
|
|
keys() { return MapPrototypeKeys(this.#entries); }
|
|
*values() {
|
|
for (const name of this.keys()) yield this.get(name);
|
|
}
|
|
*entries() {
|
|
for (const name of this.keys()) yield [name, this.get(name)];
|
|
}
|
|
get size() { return MapPrototypeGetSize(this.#entries); }
|
|
[SymbolIterator]() { return this.entries(); }
|
|
get [SymbolToStringTag]() { return 'ZipBuffer'; }
|
|
forEach(callback, thisArg) {
|
|
validateFunction(callback, 'callback');
|
|
for (const { 0: key, 1: value } of MapPrototypeEntries(this.#entries)) {
|
|
FunctionPrototypeCall(callback, thisArg === undefined ? this : thisArg, value, key, this);
|
|
}
|
|
}
|
|
/**
|
|
* Serializes the current set of entries into a fresh archive.
|
|
* @param {string | { comment?: string, baseOffset?: number }} [options]
|
|
* @returns {Promise<Buffer>}
|
|
*/
|
|
async toBuffer(options) {
|
|
const { comment, baseOffset } = normalizeArchiveOptions(options);
|
|
const chunks = [];
|
|
// Defaulting to the raw #comment bytes (not the decoded string)
|
|
// round-trips a non-UTF-8 archive comment unchanged.
|
|
for await (const chunk of createZipArchive(this.values(), { comment: comment ?? this.#comment, baseOffset })) {
|
|
ArrayPrototypePush(chunks, chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
/**
|
|
* The synchronous counterpart of `toBuffer()`. Blocks the event loop and
|
|
* further JavaScript execution until the whole archive has been
|
|
* serialized; see `zipEntry.contentSync()`.
|
|
* @param {string | { comment?: string, baseOffset?: number }} [options]
|
|
* @returns {Buffer}
|
|
*/
|
|
toBufferSync(options) {
|
|
const { comment, baseOffset } = normalizeArchiveOptions(options);
|
|
const chunks = [];
|
|
for (const chunk of createZipArchiveSync(this.values(), { comment: comment ?? this.#comment, baseOffset })) {
|
|
ArrayPrototypePush(chunks, chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
// Dispose: drop all entries (this view holds no fd of its own).
|
|
[SymbolDispose]() {
|
|
MapPrototypeClear(this.#entries);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ZipBuffer,
|
|
};
|