mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05: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>
252 lines
9.6 KiB
JavaScript
252 lines
9.6 KiB
JavaScript
'use strict';
|
|
|
|
// Write-path header builders: local/central file headers, the Zip64 data
|
|
// descriptor, and the (Zip64) end-of-central-directory records, plus the
|
|
// version-needed and extra-length helpers they share.
|
|
|
|
const {
|
|
ArrayPrototypePush,
|
|
MathMax,
|
|
MathMin,
|
|
} = primordials;
|
|
|
|
const {
|
|
codes: {
|
|
ERR_ZIP_ENTRY_TOO_LARGE,
|
|
},
|
|
} = require('internal/errors');
|
|
const { Buffer } = require('buffer');
|
|
const {
|
|
EMPTY_BUFFER,
|
|
SIG_LOCAL_FILE_HEADER,
|
|
SIG_DATA_DESCRIPTOR,
|
|
SIG_CENTRAL_FILE_HEADER,
|
|
SIG_ZIP64_EOCD_RECORD,
|
|
SIG_ZIP64_EOCD_LOCATOR,
|
|
SIG_EOCD,
|
|
MADE_BY_UNIX,
|
|
ZIP64_EXTRA_ID,
|
|
SENTINEL16,
|
|
SENTINEL32,
|
|
FLAG_DATA_DESCRIPTOR,
|
|
METHOD_ZSTD,
|
|
VERSION_DEFAULT,
|
|
VERSION_ZIP64,
|
|
VERSION_ZSTD,
|
|
} = require('internal/zip/constants');
|
|
const { encodeDosDateTime } = require('internal/zip/dos');
|
|
const { writeSafeUint64 } = require('internal/zip/binary');
|
|
const { buildExtTimestampExtra } = require('internal/zip/extra-fields');
|
|
|
|
// The "version needed to extract" (sec. 4.4.3): the highest version any
|
|
// feature of the member demands - 6.3 for Zstandard, 4.5 for Zip64
|
|
// structures, 2.0 otherwise.
|
|
function versionNeeded(meta, zip64) {
|
|
return MathMax(
|
|
zip64 ? VERSION_ZIP64 : VERSION_DEFAULT,
|
|
meta.method === METHOD_ZSTD ? VERSION_ZSTD : 0);
|
|
}
|
|
|
|
// Guard the combined extra-field length against the 16-bit field it is stored
|
|
// in (sec. 4.4.11).
|
|
function checkExtraLength(extraLength) {
|
|
if (extraLength > SENTINEL16) {
|
|
throw new ERR_ZIP_ENTRY_TOO_LARGE(
|
|
'the entry extra fields must not exceed 65535 bytes');
|
|
}
|
|
}
|
|
|
|
// Build a local file header (sec. 4.3.7). Streamed entries (unknown sizes/CRC)
|
|
// zero those fields and always carry a Zip64 extra so the trailing data
|
|
// descriptor can hold 64-bit sizes; oversized non-streamed entries get one too.
|
|
function buildLocalHeader(meta) {
|
|
const streaming = (meta.flags & FLAG_DATA_DESCRIPTOR) !== 0;
|
|
const zip64 =
|
|
streaming ||
|
|
meta.compressedSize >= SENTINEL32 ||
|
|
meta.uncompressedSize >= SENTINEL32;
|
|
const ts = meta.extendedMtime !== null ? buildExtTimestampExtra(meta.extendedMtime) : EMPTY_BUFFER;
|
|
const extraLength = (zip64 ? 20 : 0) + ts.length + meta.extra.length;
|
|
checkExtraLength(extraLength);
|
|
const buffer = Buffer.allocUnsafe(30 + meta.name.length + extraLength);
|
|
buffer.writeUInt32LE(SIG_LOCAL_FILE_HEADER, 0);
|
|
buffer.writeUInt16LE(versionNeeded(meta, zip64), 4);
|
|
buffer.writeUInt16LE(meta.flags, 6);
|
|
buffer.writeUInt16LE(meta.method, 8);
|
|
const { time, date } = encodeDosDateTime(meta.modified);
|
|
buffer.writeUInt16LE(time, 10);
|
|
buffer.writeUInt16LE(date, 12);
|
|
buffer.writeUInt32LE(streaming ? 0 : meta.crc, 14);
|
|
buffer.writeUInt32LE(zip64 ? SENTINEL32 : meta.compressedSize, 18);
|
|
buffer.writeUInt32LE(zip64 ? SENTINEL32 : meta.uncompressedSize, 22);
|
|
buffer.writeUInt16LE(meta.name.length, 26);
|
|
buffer.writeUInt16LE(extraLength, 28);
|
|
meta.name.copy(buffer, 30);
|
|
let pos = 30 + meta.name.length;
|
|
if (zip64) {
|
|
buffer.writeUInt16LE(ZIP64_EXTRA_ID, pos);
|
|
buffer.writeUInt16LE(16, pos + 2);
|
|
writeSafeUint64(buffer, pos + 4, streaming ? 0 : meta.uncompressedSize);
|
|
writeSafeUint64(buffer, pos + 12, streaming ? 0 : meta.compressedSize);
|
|
pos += 20;
|
|
}
|
|
ts.copy(buffer, pos);
|
|
pos += ts.length;
|
|
meta.extra.copy(buffer, pos);
|
|
return buffer;
|
|
}
|
|
|
|
// Build a central directory file header (sec. 4.3.12). Each of the
|
|
// uncompressed size, compressed size, and local-header offset that overflows
|
|
// 32 bits is moved into the Zip64 extra field (sec. 4.5.3) in that order.
|
|
function buildCentralHeader(meta, localOffset) {
|
|
const zip64Streaming = (meta.flags & FLAG_DATA_DESCRIPTOR) !== 0;
|
|
const u64 = meta.uncompressedSize >= SENTINEL32;
|
|
const c64 = meta.compressedSize >= SENTINEL32;
|
|
const o64 = localOffset >= SENTINEL32;
|
|
const zip64Fields = (u64 ? 1 : 0) + (c64 ? 1 : 0) + (o64 ? 1 : 0);
|
|
const ts = meta.extendedMtime !== null ? buildExtTimestampExtra(meta.extendedMtime) : EMPTY_BUFFER;
|
|
const extraLength = (zip64Fields ? 4 + 8 * zip64Fields : 0) + ts.length + meta.extra.length;
|
|
checkExtraLength(extraLength);
|
|
const zip64 = zip64Streaming || zip64Fields > 0;
|
|
const version = versionNeeded(meta, zip64);
|
|
const buffer = Buffer.allocUnsafe(
|
|
46 + meta.name.length + extraLength + meta.comment.length);
|
|
buffer.writeUInt32LE(SIG_CENTRAL_FILE_HEADER, 0);
|
|
// "Version made by" (sec. 4.4.2): the upper byte is the creator's host
|
|
// system, which determines how the external attributes (sec. 4.4.15) are
|
|
// interpreted. Preserve the original host for round-tripped entries -
|
|
// stamping everything as Unix would turn, say, a DOS entry's zeroed high
|
|
// bits into Unix mode 0000. Entries built by `createEntryMeta()` are Unix.
|
|
buffer.writeUInt16LE((meta.madeBy << 8) | version, 4);
|
|
buffer.writeUInt16LE(version, 6);
|
|
buffer.writeUInt16LE(meta.flags, 8);
|
|
buffer.writeUInt16LE(meta.method, 10);
|
|
const { time, date } = encodeDosDateTime(meta.modified);
|
|
buffer.writeUInt16LE(time, 12);
|
|
buffer.writeUInt16LE(date, 14);
|
|
buffer.writeUInt32LE(meta.crc, 16);
|
|
buffer.writeUInt32LE(c64 ? SENTINEL32 : meta.compressedSize, 20);
|
|
buffer.writeUInt32LE(u64 ? SENTINEL32 : meta.uncompressedSize, 24);
|
|
buffer.writeUInt16LE(meta.name.length, 28);
|
|
buffer.writeUInt16LE(extraLength, 30);
|
|
buffer.writeUInt16LE(meta.comment.length, 32);
|
|
buffer.writeUInt16LE(0, 34); // disk number
|
|
buffer.writeUInt16LE(meta.internal, 36);
|
|
buffer.writeUInt32LE(meta.external, 38);
|
|
buffer.writeUInt32LE(o64 ? SENTINEL32 : localOffset, 42);
|
|
meta.name.copy(buffer, 46);
|
|
let pos = 46 + meta.name.length;
|
|
if (zip64Fields) {
|
|
buffer.writeUInt16LE(ZIP64_EXTRA_ID, pos);
|
|
buffer.writeUInt16LE(8 * zip64Fields, pos + 2);
|
|
pos += 4;
|
|
if (u64) {
|
|
writeSafeUint64(buffer, pos, meta.uncompressedSize);
|
|
pos += 8;
|
|
}
|
|
if (c64) {
|
|
writeSafeUint64(buffer, pos, meta.compressedSize);
|
|
pos += 8;
|
|
}
|
|
if (o64) {
|
|
writeSafeUint64(buffer, pos, localOffset);
|
|
pos += 8;
|
|
}
|
|
}
|
|
ts.copy(buffer, pos);
|
|
pos += ts.length;
|
|
meta.extra.copy(buffer, pos);
|
|
pos += meta.extra.length;
|
|
meta.comment.copy(buffer, pos);
|
|
return buffer;
|
|
}
|
|
|
|
// Zip64 data descriptor (sec. 4.3.9): emitted after a streamed entry, whose
|
|
// local header always carries a Zip64 extra field.
|
|
function buildDataDescriptor64(crc, compressedSize, uncompressedSize) {
|
|
const buffer = Buffer.allocUnsafe(24);
|
|
buffer.writeUInt32LE(SIG_DATA_DESCRIPTOR, 0);
|
|
buffer.writeUInt32LE(crc, 4);
|
|
writeSafeUint64(buffer, 8, compressedSize);
|
|
writeSafeUint64(buffer, 16, uncompressedSize);
|
|
return buffer;
|
|
}
|
|
|
|
// Build the end of central directory record (sec. 4.3.16); fields that
|
|
// overflow their 16/32-bit slots are written as sentinels and the true values
|
|
// live in the Zip64 EOCD record.
|
|
function buildEndOfCentralDirectory(count, size, offset, comment) {
|
|
const buffer = Buffer.allocUnsafe(22 + comment.length);
|
|
buffer.writeUInt32LE(SIG_EOCD, 0);
|
|
buffer.writeUInt16LE(0, 4); // disk number
|
|
buffer.writeUInt16LE(0, 6); // Central directory disk number
|
|
buffer.writeUInt16LE(MathMin(count, SENTINEL16), 8);
|
|
buffer.writeUInt16LE(MathMin(count, SENTINEL16), 10);
|
|
buffer.writeUInt32LE(MathMin(size, SENTINEL32), 12);
|
|
buffer.writeUInt32LE(MathMin(offset, SENTINEL32), 16);
|
|
buffer.writeUInt16LE(comment.length, 20);
|
|
comment.copy(buffer, 22);
|
|
return buffer;
|
|
}
|
|
|
|
// Build the Zip64 end of central directory record (sec. 4.3.14): the 64-bit
|
|
// counterpart to the EOCD, holding the real record count/size/offset.
|
|
function buildZip64EndRecord(count, size, offset) {
|
|
const buffer = Buffer.allocUnsafe(56);
|
|
buffer.writeUInt32LE(SIG_ZIP64_EOCD_RECORD, 0);
|
|
writeSafeUint64(buffer, 4, 44); // Size of the remainder of this record
|
|
buffer.writeUInt16LE((MADE_BY_UNIX << 8) | VERSION_ZIP64, 12);
|
|
buffer.writeUInt16LE(VERSION_ZIP64, 14);
|
|
buffer.writeUInt32LE(0, 16); // disk number
|
|
buffer.writeUInt32LE(0, 20); // Central directory disk number
|
|
writeSafeUint64(buffer, 24, count);
|
|
writeSafeUint64(buffer, 32, count);
|
|
writeSafeUint64(buffer, 40, size);
|
|
writeSafeUint64(buffer, 48, offset);
|
|
return buffer;
|
|
}
|
|
|
|
// Build the Zip64 end of central directory locator (sec. 4.3.15): points the
|
|
// reader from just before the EOCD to the Zip64 EOCD record.
|
|
function buildZip64EndLocator(recordOffset) {
|
|
const buffer = Buffer.allocUnsafe(20);
|
|
buffer.writeUInt32LE(SIG_ZIP64_EOCD_LOCATOR, 0);
|
|
buffer.writeUInt32LE(0, 4); // Disk with the Zip64 EOCD record
|
|
writeSafeUint64(buffer, 8, recordOffset);
|
|
buffer.writeUInt32LE(1, 16); // total disks
|
|
return buffer;
|
|
}
|
|
|
|
// The archive trailer: the Zip64 EOCD record and locator
|
|
// (sec. 4.3.14/4.3.15) when the record count, central directory offset, or
|
|
// central directory size overflows its classic 16-/32-bit field, followed
|
|
// by the end of central directory record (sec. 4.3.16). Shared by the
|
|
// streaming serializers and ZipFile's in-place rewrite so the Zip64
|
|
// switchover rule lives in exactly one place.
|
|
function buildArchiveTrailer(count, centralDirectorySize, centralDirectoryOffset, comment) {
|
|
const chunks = [];
|
|
const zip64 =
|
|
count >= SENTINEL16 ||
|
|
centralDirectoryOffset >= SENTINEL32 ||
|
|
centralDirectorySize >= SENTINEL32;
|
|
if (zip64) {
|
|
const recordOffset = centralDirectoryOffset + centralDirectorySize;
|
|
ArrayPrototypePush(chunks, buildZip64EndRecord(count, centralDirectorySize, centralDirectoryOffset));
|
|
ArrayPrototypePush(chunks, buildZip64EndLocator(recordOffset));
|
|
}
|
|
ArrayPrototypePush(chunks,
|
|
buildEndOfCentralDirectory(count, centralDirectorySize, centralDirectoryOffset, comment));
|
|
return chunks;
|
|
}
|
|
|
|
module.exports = {
|
|
buildLocalHeader,
|
|
buildCentralHeader,
|
|
buildDataDescriptor64,
|
|
buildEndOfCentralDirectory,
|
|
buildZip64EndRecord,
|
|
buildZip64EndLocator,
|
|
buildArchiveTrailer,
|
|
};
|