/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/zip/compression.js
303 строки
10 KB
Philipp Dunkel
zlib: add ZipEntry, ZipFile, and ZipBuffer
01 авг 2026, 11:47
Не верифицирован
01 авг 2026, 11:47
5ba72ae
Код
Авторство
О чём код?
'use strict'; // Compression/decompression plumbing over the lazily-required `zlib` facade: // one-shot (async and sync) and streaming deflate/inflate/zstd helpers, plus // the member decoders that add method dispatch, size bounding, and CRC-32 // verification on top. const { JSONStringify, MathMin, Promise, } = primordials; const { codes: { ERR_ZIP_ENTRY_CORRUPT, ERR_ZIP_ENTRY_TOO_LARGE, ERR_ZIP_UNSUPPORTED_FEATURE, }, } = require('internal/errors'); const { kMaxLength } = require('buffer'); const { compose } = require('stream'); const { crc32: crc32Native } = internalBinding('zlib'); const { FLAG_ENCRYPTED, METHOD_STORE, METHOD_DEFLATE, METHOD_ZSTD, } = require('internal/zip/constants'); // `internal/zip` is required from `lib/zlib.js`, so it must not require the // public `zlib` facade at load time (its module.exports is not yet // populated). Compression is only needed once an entry is actually read or // written, well after `zlib.js` has finished loading, so a lazy reference is // enough to break the cycle. let zlib; function lazyZlib() { zlib ??= require('zlib'); return zlib; } // -- compression plumbing ------------------------------------------------------ // The one-shot (async/sync) and streaming helpers below are thin adapters that // promisify or stream-wrap the lazy `zlib` facade; individually trivial, they // exist only so the rest of the module never touches `zlib` directly. function deflateRawAsync(buffer) { return new Promise((resolve, reject) => { lazyZlib().deflateRaw(buffer, (err, result) => { if (err) reject(err); else resolve(result); }); }); } function inflateRawAsync(buffer, options) { return new Promise((resolve, reject) => { lazyZlib().inflateRaw(buffer, options, (err, result) => { if (err) reject(err); else resolve(result); }); }); } // Drive `source` through a zlib transform stream, returning an async-iterable // stream of its output. `compose()` (pipeline-backed) wires error propagation // both ways and tears the whole chain down when the consumer errors or stops // early, so an abandoned iteration cannot leak the pipeline. function pumpThroughTransform(source, transform) { return compose(source, transform); } function deflateRawStream(source) { return pumpThroughTransform(source, lazyZlib().createDeflateRaw()); } function inflateRawStream(source) { return pumpThroughTransform(source, lazyZlib().createInflateRaw()); } function zstdCompressAsync(buffer) { return new Promise((resolve, reject) => { lazyZlib().zstdCompress(buffer, (err, result) => { if (err) reject(err); else resolve(result); }); }); } function zstdDecompressAsync(buffer, options) { return new Promise((resolve, reject) => { lazyZlib().zstdDecompress(buffer, options, (err, result) => { if (err) reject(err); else resolve(result); }); }); } function zstdCompressStream(source) { return pumpThroughTransform(source, lazyZlib().createZstdCompress()); } function zstdDecompressStream(source) { return pumpThroughTransform(source, lazyZlib().createZstdDecompress()); } function deflateRawSync(buffer) { return lazyZlib().deflateRawSync(buffer); } function zstdCompressSync(buffer) { return lazyZlib().zstdCompressSync(buffer); } /** * @typedef {{ * name: string, * flags: number, * method: number, * crc32: number, * uncompressedSize: number, * }} ZipMemberInfo */ // Shared entry guards for the member decoders below: encryption and // unsupported compression methods are rejected up front, and when the caller // bounds the output, a declared size beyond that bound fails before anything // is decompressed or allocated. function assertDecodable(info, options) { if (info.flags & FLAG_ENCRYPTED) { throw new ERR_ZIP_UNSUPPORTED_FEATURE( `entry ${JSONStringify(info.name)} is encrypted`); } if (info.method !== METHOD_STORE && info.method !== METHOD_DEFLATE && info.method !== METHOD_ZSTD) { throw new ERR_ZIP_UNSUPPORTED_FEATURE( `entry ${JSONStringify(info.name)} uses compression method ${info.method}`); } if (options?.maxSize !== undefined && info.uncompressedSize > options.maxSize) { throw new ERR_ZIP_ENTRY_TOO_LARGE( `entry ${JSONStringify(info.name)} declares ${info.uncompressedSize} bytes, ` + `exceeding the ${options.maxSize} byte limit`); } } // Bound one-shot decompression by the declared size, not just the caller's // limit: a member that decompresses to more than it declares is corrupt by // definition, so there is no reason to materialize more than `declared + 1` // bytes (the +1 makes an overrun detectable) no matter how generous `maxSize` // is. This keeps a tiny archive from forcing a `maxSize`-sized allocation. function outputCap(info, options) { return MathMin( info.uncompressedSize + 1, options?.maxSize ?? kMaxLength, kMaxLength); } // Map a one-shot decompression failure to a corrupt-entry error. // `assertDecodable()` has already ensured `declared <= maxSize`, so hitting // the output cap always means the stream produced more than the member // declared. function rethrowDecodeFailure(err, info, method) { if (err?.code === 'ERR_BUFFER_TOO_LARGE') { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} ` + `${method === METHOD_DEFLATE ? 'inflates' : 'decompresses'} beyond its ` + `declared size of ${info.uncompressedSize} bytes`); } throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} failed to ` + `${method === METHOD_DEFLATE ? 'inflate' : 'decompress'}: ${err.message}`); } // Enforce the declared size and (unless opted out) the CRC-32 on a fully // decoded member. function checkDecoded(data, info, verify) { if (data.length !== info.uncompressedSize) { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} produced ${data.length} bytes, expected ` + `${info.uncompressedSize}`); } if (verify && crc32Native(data, 0) !== info.crc32) { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} failed CRC-32 verification`); } } /** * Decodes one member's compressed byte stream: rejects encrypted entries and * unsupported compression methods, inflates method 8 or decompresses method * 93 (Zstandard), enforces the declared uncompressed size and verifies * CRC-32 (on by default). * @param {AsyncIterable<Buffer>} source * @param {ZipMemberInfo} info * @param {{ verify?: boolean, maxSize?: number }} [options] * @yields {Buffer} */ async function* decodeMemberStream(source, info, options) { assertDecodable(info, options); const verify = options?.verify !== false; let produced = 0; let state = 0; const decoded = info.method === METHOD_DEFLATE ? inflateRawStream(source) : info.method === METHOD_ZSTD ? zstdDecompressStream(source) : source; for await (const chunk of decoded) { produced += chunk.length; if (produced > info.uncompressedSize) { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} inflates beyond its declared size of ` + `${info.uncompressedSize} bytes`); } if (verify) state = crc32Native(chunk, state); yield chunk; } if (produced !== info.uncompressedSize) { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} is truncated: got ${produced} of ` + `${info.uncompressedSize} bytes`); } if (verify && state !== info.crc32) { throw new ERR_ZIP_ENTRY_CORRUPT( `entry ${JSONStringify(info.name)} failed CRC-32 verification`); } } /** * Decodes one member held completely in `compressed`, in one shot: the same * guards, declared-size bounding, and CRC-32 verification as * `decodeMemberStream()`, returning the whole decoded member. For the store * method the input buffer itself is returned - callers that must hand out * caller-owned memory copy it (see `zipEntry.content()`). * @param {Buffer} compressed * @param {ZipMemberInfo} info * @param {{ verify?: boolean, maxSize?: number }} [options] * @returns {Promise<Buffer>} */ async function decodeMemberAsync(compressed, info, options) { assertDecodable(info, options); const cap = outputCap(info, options); let data; if (info.method === METHOD_DEFLATE) { try { data = await inflateRawAsync(compressed, { maxOutputLength: cap }); } catch (err) { rethrowDecodeFailure(err, info, METHOD_DEFLATE); } } else if (info.method === METHOD_ZSTD) { try { data = await zstdDecompressAsync(compressed, { maxOutputLength: cap }); } catch (err) { rethrowDecodeFailure(err, info, METHOD_ZSTD); } } else { data = compressed; } checkDecoded(data, info, options?.verify !== false); return data; } /** * The synchronous counterpart of `decodeMemberAsync()`. There is no public * synchronous incremental inflate API, so - unlike the streaming path - * `compressed` must already be the member's complete compressed byte * stream, and the whole result is produced (and verified) in one call * rather than yielded incrementally. * @param {Buffer} compressed * @param {ZipMemberInfo} info * @param {{ verify?: boolean, maxSize?: number }} [options] * @returns {Buffer} */ function decodeMemberSync(compressed, info, options) { assertDecodable(info, options); const cap = outputCap(info, options); let data; if (info.method === METHOD_DEFLATE) { try { data = lazyZlib().inflateRawSync(compressed, { maxOutputLength: cap }); } catch (err) { rethrowDecodeFailure(err, info, METHOD_DEFLATE); } } else if (info.method === METHOD_ZSTD) { try { data = lazyZlib().zstdDecompressSync(compressed, { maxOutputLength: cap }); } catch (err) { rethrowDecodeFailure(err, info, METHOD_ZSTD); } } else { data = compressed; } checkDecoded(data, info, options?.verify !== false); return data; } module.exports = { deflateRawAsync, deflateRawSync, zstdCompressAsync, zstdCompressSync, deflateRawStream, zstdCompressStream, decodeMemberStream, decodeMemberAsync, decodeMemberSync, };