Files
Philipp Dunkel ad87559e7e zlib: reject local/central ZIP header mismatch
The reader treats the central directory as authoritative for a member's
method, sizes, CRC, and name, but read the local header only for its
signature. An archive whose local file header disagrees with the
central directory therefore extracts different bytes here than in a
reader that uses the local header (e.g. Info-ZIP unzip), and its
encrypted bit was read from the local header while its identity came
from the central directory - a parser-confusion split that defeats
inspect-then-consume pipelines and can slip an encrypted member past a
central-directory scanner.

Cross-check the local header against the central entry when a member is
read: method, CRC, and both sizes (exempting a data-descriptor entry,
whose local crc/sizes are legitimately zero), plus the encryption flag.
Reject a mismatch with ERR_ZIP_INVALID_ARCHIVE, consistent with the
reject-rather-than-silently-choose stance already taken for ambiguous
archive ends.

The existing hardening/coverage tests that forged a decode-time size,
CRC, or Zip64 lie in the central header alone now trip this earlier
check; update them to patch both headers consistently so they still
exercise the decode-time guards they target.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
PR-URL: https://github.com/nodejs/node/pull/65016
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
2026-08-14 03:39:42 +00:00

624 lines
26 KiB
JavaScript

'use strict';
// Reader-side header structures (sec. 4.3): the end-of-central-directory
// record, the Zip64 EOCD record/locator, and the central/local file headers,
// plus `findArchiveEnd()` which locates them and `readCentralDirectory()`
// which walks the central directory into an array of headers.
const {
ArrayPrototypePush,
MathMax,
Number,
} = primordials;
const {
codes: {
ERR_ZIP_INVALID_ARCHIVE,
ERR_ZIP_UNSUPPORTED_FEATURE,
},
} = require('internal/errors');
const {
BIGINT_MAX_SAFE_INTEGER,
SIG_LOCAL_FILE_HEADER,
SIG_CENTRAL_FILE_HEADER,
SIG_ZIP64_EOCD_RECORD,
SIG_ZIP64_EOCD_LOCATOR,
SIG_EOCD,
MADE_BY_UNIX,
FLAG_ENCRYPTED,
FLAG_DATA_DESCRIPTOR,
SENTINEL16,
SENTINEL32,
TAIL_LENGTH,
ZIP64_EOCD_MAX_LENGTH,
S_IFLNK,
S_IFMT,
} = require('internal/zip/constants');
const {
validateArchiveRange,
readSafeUint64,
} = require('internal/zip/binary');
const { parseZip64Extra } = require('internal/zip/extra-fields');
const {
decodeDosDateTime,
decodeZipName,
decodeZipText,
} = require('internal/zip/dos');
// End of central directory record (sec. 4.3.16).
// Offset Bytes Description
// 0 4 Signature = 0x06054b50
// 4 2 Number of this disk
// 6 2 Disk where central directory starts
// 8 2 Number of central directory records on this disk
// 10 2 Total number of central directory records
// 12 4 Size of central directory (bytes)
// 16 4 Offset of start of central directory
// 20 2 Comment length (n)
// 22 n Comment
class CentralEndHeader {
#buffer;
#offset;
constructor(buffer, offset = 0) {
validateArchiveRange(buffer, offset, 22, 'end of central directory record');
if (buffer.readUInt32LE(offset) !== SIG_EOCD) {
throw new ERR_ZIP_INVALID_ARCHIVE('end of central directory signature is invalid');
}
this.#buffer = buffer;
this.#offset = offset;
if (offset + this.byteLength > buffer.length) {
throw new ERR_ZIP_INVALID_ARCHIVE('end of central directory record is truncated');
}
}
get byteLength() { return 22 + this.commentLength; }
get diskNumber() { return this.#buffer.readUInt16LE(this.#offset + 4); }
get centralDirectoryDiskNumber() { return this.#buffer.readUInt16LE(this.#offset + 6); }
get centralDirectoryDiskRecords() { return this.#buffer.readUInt16LE(this.#offset + 8); }
get centralDirectoryTotalRecords() { return this.#buffer.readUInt16LE(this.#offset + 10); }
get centralDirectorySize() { return this.#buffer.readUInt32LE(this.#offset + 12); }
get centralDirectoryOffset() { return this.#buffer.readUInt32LE(this.#offset + 16); }
get commentLength() { return this.#buffer.readUInt16LE(this.#offset + 20); }
get commentBuffer() {
const start = this.#offset + 22;
return this.#buffer.subarray(start, start + this.commentLength);
}
}
// Zip64 end of central directory record (sec. 4.3.14).
// 0 4 Signature = 0x06064b50
// 4 8 Size of remainder of this record
// 12 2 Version made by
// 14 2 Version needed to extract
// 16 4 Number of this disk
// 20 4 Disk where central directory starts
// 24 8 Number of central directory records on this disk
// 32 8 Total number of central directory records
// 40 8 Size of central directory
// 48 8 Offset of start of central directory
class Zip64EndRecord {
#buffer;
#offset;
constructor(buffer, offset = 0) {
validateArchiveRange(buffer, offset, 56, 'Zip64 end of central directory record');
if (buffer.readUInt32LE(offset) !== SIG_ZIP64_EOCD_RECORD) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'Zip64 end of central directory signature is invalid');
}
this.#buffer = buffer;
this.#offset = offset;
}
get diskNumber() { return this.#buffer.readUInt32LE(this.#offset + 16); }
get centralDirectoryDiskNumber() { return this.#buffer.readUInt32LE(this.#offset + 20); }
get centralDirectoryDiskRecords() { return readSafeUint64(this.#buffer, this.#offset + 24); }
get centralDirectoryTotalRecords() { return readSafeUint64(this.#buffer, this.#offset + 32); }
get centralDirectorySize() { return readSafeUint64(this.#buffer, this.#offset + 40); }
get centralDirectoryOffset() { return readSafeUint64(this.#buffer, this.#offset + 48); }
}
// Zip64 end of central directory locator (sec. 4.3.15).
// 0 4 Signature = 0x07064b50
// 4 4 Disk with the Zip64 end of central directory record
// 8 8 Offset of the Zip64 end of central directory record
// 16 4 Total number of disks
class Zip64EndLocator {
#buffer;
#offset;
constructor(buffer, offset = 0) {
validateArchiveRange(buffer, offset, 20, 'Zip64 end of central directory locator');
if (buffer.readUInt32LE(offset) !== SIG_ZIP64_EOCD_LOCATOR) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'Zip64 end of central directory locator signature is invalid');
}
this.#buffer = buffer;
this.#offset = offset;
}
get recordDiskNumber() { return this.#buffer.readUInt32LE(this.#offset + 4); }
// Spec field (sec. 4.3.15); not consumed as a getter - findArchiveEnd()
// reads the offset leniently via readBigUInt64LE instead, so a locator
// signature that is really comment data cannot turn into a hard parse
// error on an out-of-range value.
// get recordOffset() { return readSafeUint64(this.#buffer, this.#offset + 8); }
get totalDisks() { return this.#buffer.readUInt32LE(this.#offset + 16); }
}
// Central directory file header (sec. 4.3.12).
// 0 4 Signature = 0x02014b50
// 4 2 Version made by
// 6 2 Version needed to extract
// 8 2 General purpose bit flag
// 10 2 Compression method
// 12 2 Last modification time
// 14 2 Last modification date
// 16 4 CRC-32
// 20 4 Compressed size
// 24 4 Uncompressed size
// 28 2 File name length (n)
// 30 2 Extra field length (m)
// 32 2 File comment length (k)
// 34 2 Disk number where file starts
// 36 2 Internal file attributes
// 38 4 External file attributes
// 42 4 Relative offset of local file header
// 46 n File name
// 46+n m Extra field
// 46+n+m k File comment
class CentralFileHeader {
#buffer;
#offset;
#zip64 = null;
constructor(buffer, offset = 0) {
validateArchiveRange(buffer, offset, 46, 'central directory header');
if (buffer.readUInt32LE(offset) !== SIG_CENTRAL_FILE_HEADER) {
throw new ERR_ZIP_INVALID_ARCHIVE('central directory header signature is invalid');
}
this.#buffer = buffer;
this.#offset = offset;
if (offset + this.byteLength > buffer.length) {
throw new ERR_ZIP_INVALID_ARCHIVE('central directory header is truncated');
}
}
get byteOffset() { return this.#offset; }
get byteLength() {
return 46 + this.fileNameLength + this.extraFieldLength + this.fileCommentLength;
}
get version() { return this.#buffer.readUInt16LE(this.#offset + 4); }
// Spec field "version needed to extract" (sec. 4.4.3); not consumed today.
// get versionNeeded() { return this.#buffer.readUInt16LE(this.#offset + 6); }
get flags() { return this.#buffer.readUInt16LE(this.#offset + 8); }
get compressionMethod() { return this.#buffer.readUInt16LE(this.#offset + 10); }
get lastModified() {
return decodeDosDateTime(
this.#buffer.readUInt16LE(this.#offset + 12),
this.#buffer.readUInt16LE(this.#offset + 14));
}
get crc32() { return this.#buffer.readUInt32LE(this.#offset + 16); }
// Lazily parse the Zip64 extended-information extra field (sec. 4.5.3),
// supplying the true 64-bit values only for the classic fields that hold an
// overflow sentinel. Cached across getters.
#resolveZip64() {
if (this.#zip64 === null) {
this.#zip64 = parseZip64Extra(this.extraField, {
uncompressedSize: this.#buffer.readUInt32LE(this.#offset + 24) === SENTINEL32,
compressedSize: this.#buffer.readUInt32LE(this.#offset + 20) === SENTINEL32,
localFileHeaderOffset: this.#buffer.readUInt32LE(this.#offset + 42) === SENTINEL32,
diskNumber: this.#buffer.readUInt16LE(this.#offset + 34) === SENTINEL16,
});
}
return this.#zip64;
}
get compressedSize() {
const value = this.#buffer.readUInt32LE(this.#offset + 20);
return value === SENTINEL32 ? this.#resolveZip64().compressedSize : value;
}
get uncompressedSize() {
const value = this.#buffer.readUInt32LE(this.#offset + 24);
return value === SENTINEL32 ? this.#resolveZip64().uncompressedSize : value;
}
get fileNameLength() { return this.#buffer.readUInt16LE(this.#offset + 28); }
get extraFieldLength() { return this.#buffer.readUInt16LE(this.#offset + 30); }
get fileCommentLength() { return this.#buffer.readUInt16LE(this.#offset + 32); }
get diskNumber() {
const value = this.#buffer.readUInt16LE(this.#offset + 34);
return value === SENTINEL16 ? this.#resolveZip64().diskNumber : value;
}
get internalFileAttributes() { return this.#buffer.readUInt16LE(this.#offset + 36); }
get externalFileAttributes() { return this.#buffer.readUInt32LE(this.#offset + 38); }
get localFileHeaderOffset() {
const value = this.#buffer.readUInt32LE(this.#offset + 42);
return value === SENTINEL32 ? this.#resolveZip64().localFileHeaderOffset : value;
}
get fileNameBuffer() {
const start = this.#offset + 46;
return this.#buffer.subarray(start, start + this.fileNameLength);
}
get fileName() { return decodeZipName(this.fileNameBuffer, this.flags, this.extraField); }
get extraField() {
const start = this.#offset + 46 + this.fileNameLength;
return this.#buffer.subarray(start, start + this.extraFieldLength);
}
get fileCommentBuffer() {
const start = this.#offset + 46 + this.fileNameLength + this.extraFieldLength;
return this.#buffer.subarray(start, start + this.fileCommentLength);
}
get fileComment() { return decodeZipText(this.fileCommentBuffer, this.flags); }
get isUnixMode() { return (this.version >>> 8) === MADE_BY_UNIX; }
get mode() {
// Low 12 bits: permissions plus setuid/setgid/sticky (sec. 4.4.15).
return this.isUnixMode ? (this.externalFileAttributes >>> 16) & 0o7777 : 0;
}
get isSymlink() {
return this.isUnixMode &&
((this.externalFileAttributes >>> 16) & S_IFMT) === S_IFLNK;
}
}
// Local file header (sec. 4.3.7).
// 0 4 Signature = 0x04034b50
// 4 2 Version needed to extract
// 6 2 General purpose bit flag
// 8 2 Compression method
// 10 2 Last modification time
// 12 2 Last modification date
// 14 4 CRC-32
// 18 4 Compressed size
// 22 4 Uncompressed size
// 26 2 File name length (n)
// 28 2 Extra field length (m)
// 30 n File name
// 30+n m Extra field
class LocalFileHeader {
#buffer;
#offset;
#zip64 = null;
constructor(buffer, offset = 0) {
validateArchiveRange(buffer, offset, 30, 'local file header');
if (buffer.readUInt32LE(offset) !== SIG_LOCAL_FILE_HEADER) {
throw new ERR_ZIP_INVALID_ARCHIVE('local file header signature is invalid');
}
this.#buffer = buffer;
this.#offset = offset;
if (offset + this.byteLength > buffer.length) {
throw new ERR_ZIP_INVALID_ARCHIVE('local file header is truncated');
}
}
get byteLength() { return 30 + this.fileNameLength + this.extraFieldLength; }
get flags() { return this.#buffer.readUInt16LE(this.#offset + 6); }
// The central directory is authoritative, but these local fields are read to
// reject an archive whose local header contradicts it (assertConsistentLocalHeader).
get compressionMethod() { return this.#buffer.readUInt16LE(this.#offset + 8); }
get crc32() { return this.#buffer.readUInt32LE(this.#offset + 14); }
// The Zip64 extra supplies the true 64-bit sizes for whichever classic size
// field holds an overflow sentinel; a local header has no offset/disk field.
#resolveZip64() {
if (this.#zip64 === null) {
this.#zip64 = parseZip64Extra(this.extraField, {
uncompressedSize: this.#buffer.readUInt32LE(this.#offset + 22) === SENTINEL32,
compressedSize: this.#buffer.readUInt32LE(this.#offset + 18) === SENTINEL32,
localFileHeaderOffset: false,
diskNumber: false,
});
}
return this.#zip64;
}
get compressedSize() {
const value = this.#buffer.readUInt32LE(this.#offset + 18);
return value === SENTINEL32 ? this.#resolveZip64().compressedSize : value;
}
get uncompressedSize() {
const value = this.#buffer.readUInt32LE(this.#offset + 22);
return value === SENTINEL32 ? this.#resolveZip64().uncompressedSize : value;
}
get fileNameLength() { return this.#buffer.readUInt16LE(this.#offset + 26); }
get extraFieldLength() { return this.#buffer.readUInt16LE(this.#offset + 28); }
get extraField() {
const start = this.#offset + 30 + this.fileNameLength;
return this.#buffer.subarray(start, start + this.extraFieldLength);
}
// The local header also carries the name and modification time (sec. 4.3.7),
// but the central directory is authoritative for both - a mismatching local
// name is ignored by design - so only the flags and the extra field (which
// may hold a higher-resolution timestamp) are consumed here.
// get fileName() {
// const start = this.#offset + 30;
// return decodeZipName(
// this.#buffer.subarray(start, start + this.fileNameLength), this.flags, this.extraField);
// }
// get lastModified() {
// return decodeDosDateTime(this.#buffer.readUInt16LE(this.#offset + 10),
// this.#buffer.readUInt16LE(this.#offset + 12));
// }
// Total on-disk length of the local header at `offset` (sec. 4.3.7), read
// straight from the length fields without constructing a header; 0 if the
// fixed part does not fit. Lets the reader skip to the entry data.
static length(buffer, offset) {
if (offset + 30 > buffer.length) return 0;
return 30 + buffer.readUInt16LE(offset + 26) + buffer.readUInt16LE(offset + 28);
}
}
// Reject an archive whose local file header contradicts the central directory
// on a field that decides which bytes a member yields. This reader treats the
// central directory as authoritative, but a divergent local header lets a
// local-header-based extractor (Info-ZIP unzip, and others) read a different
// member, method, or size from the same archive - a parser-confusion split that
// defeats inspect-then-consume pipelines. Reject rather than silently diverge.
function assertConsistentLocalHeader(local, central) {
if (local.compressionMethod !== central.compressionMethod) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'local and central compression methods disagree');
}
if ((local.flags & FLAG_ENCRYPTED) !== (central.flags & FLAG_ENCRYPTED)) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'local and central encryption flags disagree');
}
// A streamed entry (data-descriptor bit) writes zero crc and sizes in the
// local header, deferring the true values to the central directory; only then
// is a difference on those three fields expected.
const deferred = (local.flags & FLAG_DATA_DESCRIPTOR) !== 0 &&
local.crc32 === 0 && local.compressedSize === 0 && local.uncompressedSize === 0;
if (!deferred &&
(local.crc32 !== central.crc32 ||
local.compressedSize !== central.compressedSize ||
local.uncompressedSize !== central.uncompressedSize)) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'local and central header crc-32 or sizes disagree');
}
}
// Returns whether an EOCD-looking record could describe an archive this
// implementation supports. This is deliberately only a cheap preflight: the
// selected record still receives the complete Zip64 and central-directory
// validation below.
function isPlausibleArchiveEnd(buffer, eocdPos, scanStart) {
const diskNumber = buffer.readUInt16LE(eocdPos + 4);
const centralDirectoryDiskNumber = buffer.readUInt16LE(eocdPos + 6);
const diskRecords = buffer.readUInt16LE(eocdPos + 8);
const totalRecords = buffer.readUInt16LE(eocdPos + 10);
const centralDirectorySize = buffer.readUInt32LE(eocdPos + 12);
const centralDirectoryOffset = buffer.readUInt32LE(eocdPos + 16);
const needsZip64 =
diskNumber === SENTINEL16 ||
centralDirectoryDiskNumber === SENTINEL16 ||
diskRecords === SENTINEL16 ||
totalRecords === SENTINEL16 ||
centralDirectorySize === SENTINEL32 ||
centralDirectoryOffset === SENTINEL32;
const locatorPos = eocdPos - 20;
const hasZip64Locator = locatorPos >= 0 &&
buffer.readUInt32LE(locatorPos) === SIG_ZIP64_EOCD_LOCATOR;
if (needsZip64) return hasZip64Locator;
// A Zip64 end record may accompany authoritative, non-sentinel classic
// fields. Its central directory does not immediately precede this EOCD.
if (hasZip64Locator) return true;
if (
diskNumber !== 0 ||
centralDirectoryDiskNumber !== 0 ||
diskRecords !== totalRecords ||
totalRecords * 46 > centralDirectorySize
) {
return false;
}
const centralDirectoryPos = eocdPos - centralDirectorySize;
if (centralDirectoryPos < scanStart) {
// Use the same logical tail window for memory- and file-backed archives.
// The directory is not available for this cheap preflight in either case.
return true;
}
if (totalRecords === 0) return centralDirectorySize === 0;
return buffer.readUInt32LE(centralDirectoryPos) === SIG_CENTRAL_FILE_HEADER;
}
/**
* Locates and validates the end-of-archive structures (EOCD, and the Zip64
* EOCD locator/record when present) in `buffer`. `base` is the absolute
* offset of `buffer[0]` when `buffer` is only the tail of a larger file; all
* returned offsets are absolute. `buffer` must extend to the end of the
* archive.
*
* When a required Zip64 EOCD record starts before `buffer[0]` (its
* extensible data sector can push it beyond any fixed-size tail read),
* returns `{ needTailFrom }` instead: the caller must retry with a buffer
* that starts at that absolute offset (still ending at the end of the
* archive). Never returned when `base` is 0.
* @returns {{
* prefix: number,
* totalRecords: number,
* centralDirectoryOffset: number,
* centralDirectorySize: number,
* comment: Buffer,
* } | { needTailFrom: number }}
*/
function findArchiveEnd(buffer, base = 0) {
if (buffer.length < 22) {
throw new ERR_ZIP_INVALID_ARCHIVE('no end of central directory record found');
}
// Use the same tail-sized search window for full buffers and ZipFile's tail
// reads. Besides keeping candidate selection consistent, the extra tail
// slack permits a maximum-length comment followed by modest writer padding.
const min = MathMax(0, buffer.length - TAIL_LENGTH);
let eocdPos = -1;
let fallbackPos = -1;
let exactFallbackPos = -1;
for (let pos = buffer.length - 22; pos >= min; pos--) {
if (buffer.readUInt32LE(pos) !== SIG_EOCD) continue;
const end = pos + 22 + buffer.readUInt16LE(pos + 20);
if (end > buffer.length) continue;
if (fallbackPos < 0) fallbackPos = pos;
if (end === buffer.length && exactFallbackPos < 0) exactFallbackPos = pos;
if (!isPlausibleArchiveEnd(buffer, pos, min)) continue;
if (eocdPos >= 0) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'ambiguous end of central directory records');
}
eocdPos = pos;
}
// Preserve the targeted validation errors for a sole malformed or
// unsupported candidate. Plausible candidates always take precedence.
if (eocdPos < 0) {
eocdPos = exactFallbackPos >= 0 ? exactFallbackPos : fallbackPos;
}
if (eocdPos < 0) {
throw new ERR_ZIP_INVALID_ARCHIVE('no end of central directory record found');
}
const eocd = new CentralEndHeader(buffer, eocdPos);
let totalRecords = eocd.centralDirectoryTotalRecords;
let centralDirectorySize = eocd.centralDirectorySize;
let centralDirectoryOffset = eocd.centralDirectoryOffset;
let prefix;
// A classic field at its maximum is an overflow sentinel that makes the
// Zip64 record mandatory. Otherwise the classic fields are authoritative,
// and a Zip64 locator signature in the preceding bytes is not proof of a
// Zip64 archive - it may be the tail of a file comment that happens to
// contain those four bytes - so a failed Zip64 lookup falls back to the
// classic fields instead of rejecting the archive.
const needsZip64 =
eocd.diskNumber === SENTINEL16 ||
eocd.centralDirectoryDiskNumber === SENTINEL16 ||
eocd.centralDirectoryDiskRecords === SENTINEL16 ||
totalRecords === SENTINEL16 ||
centralDirectorySize === SENTINEL32 ||
centralDirectoryOffset === SENTINEL32;
let zip64 = null;
let recordPos = -1;
const locatorPos = eocdPos - 20;
if (
locatorPos >= 0 &&
buffer.readUInt32LE(locatorPos) === SIG_ZIP64_EOCD_LOCATOR
) {
const locator = new Zip64EndLocator(buffer, locatorPos);
// Read the recorded offset leniently: on a coincidental signature these
// eight bytes are arbitrary comment data, which must not turn into a
// hard parse error.
const rawRecordOffset = buffer.readBigUInt64LE(locatorPos + 8);
const recordOffset =
rawRecordOffset <= BIGINT_MAX_SAFE_INTEGER ? Number(rawRecordOffset) : -1;
recordPos = recordOffset >= 0 ? recordOffset - base : -1;
if (
!(recordPos >= 0 &&
recordPos + 56 <= locatorPos &&
buffer.readUInt32LE(recordPos) === SIG_ZIP64_EOCD_RECORD)
) {
// Data was prepended to the archive, shifting the record; scan
// backward from the locator instead of trusting its recorded offset.
recordPos = -1;
const floor = MathMax(0, locatorPos - 56 - SENTINEL16);
for (let pos = locatorPos - 56; pos >= floor; pos--) {
if (buffer.readUInt32LE(pos) !== SIG_ZIP64_EOCD_RECORD) continue;
const size = buffer.readBigUInt64LE(pos + 4);
if (size >= 44n && pos + 12 + Number(size) === locatorPos) {
recordPos = pos;
break;
}
}
}
if (recordPos < 0 && needsZip64) {
// The record is required but does not lie inside `buffer`: its
// extensible data sector may extend it beyond any fixed-size tail
// read. When the locator points (plausibly) before the bytes at
// hand, ask the caller for a longer tail instead of failing.
if (
recordOffset >= 0 &&
recordOffset < base &&
base + locatorPos - recordOffset <= ZIP64_EOCD_MAX_LENGTH
) {
return { needTailFrom: recordOffset };
}
throw new ERR_ZIP_INVALID_ARCHIVE('Zip64 end of central directory record not found');
}
if (recordPos >= 0) {
if (locator.totalDisks > 1 || locator.recordDiskNumber !== 0) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
zip64 = new Zip64EndRecord(buffer, recordPos);
}
}
// The `prefix` math assumes nothing sits between the central directory and
// the archive-end records. APPNOTE sec. 4.3.13 allows a digital-signature
// record there; such an archive shifts `prefix` by the signature's length
// and fails with a central-directory signature error rather than a targeted
// message - signed archives are essentially extinct, so none is emitted.
if (zip64 !== null) {
if (zip64.diskNumber !== 0 || zip64.centralDirectoryDiskNumber !== 0) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
if (zip64.centralDirectoryDiskRecords !== zip64.centralDirectoryTotalRecords) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
// A classic field either holds the overflow sentinel (its real value lives
// in the Zip64 record) or its own real value, which must then match the
// Zip64 record. A non-sentinel classic field that disagrees with Zip64 is a
// parser differential - a classic-only reader and this one would see
// different archives - so reject it rather than silently preferring Zip64.
if (
(totalRecords !== SENTINEL16 &&
totalRecords !== zip64.centralDirectoryTotalRecords) ||
(centralDirectorySize !== SENTINEL32 &&
centralDirectorySize !== zip64.centralDirectorySize) ||
(centralDirectoryOffset !== SENTINEL32 &&
centralDirectoryOffset !== zip64.centralDirectoryOffset)
) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'Zip64 and classic end-of-central-directory records disagree');
}
totalRecords = zip64.centralDirectoryTotalRecords;
centralDirectorySize = zip64.centralDirectorySize;
centralDirectoryOffset = zip64.centralDirectoryOffset;
prefix = base + recordPos - (centralDirectoryOffset + centralDirectorySize);
} else {
if (eocd.diskNumber !== 0 || eocd.centralDirectoryDiskNumber !== 0) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
if (eocd.centralDirectoryDiskRecords !== totalRecords) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
prefix = base + eocdPos - (centralDirectoryOffset + centralDirectorySize);
}
if (prefix < 0) {
throw new ERR_ZIP_INVALID_ARCHIVE('central directory does not fit inside the archive');
}
if (
(totalRecords === 0 && centralDirectorySize !== 0) ||
totalRecords * 46 > centralDirectorySize
) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'central directory record count is inconsistent with its size');
}
return {
prefix,
totalRecords,
centralDirectoryOffset: centralDirectoryOffset + prefix,
centralDirectorySize,
comment: eocd.commentBuffer,
};
}
// Walk the contiguous run of `count` central directory file headers
// (sec. 4.3.12) into an array, rejecting multi-disk archives.
function readCentralDirectory(buffer, count) {
const result = [];
let pos = 0;
for (let index = 0; index < count; index++) {
const header = new CentralFileHeader(buffer, pos);
if (header.diskNumber !== 0) {
throw new ERR_ZIP_UNSUPPORTED_FEATURE('multi-disk archives are not supported');
}
ArrayPrototypePush(result, header);
pos += header.byteLength;
}
if (pos !== buffer.length) {
throw new ERR_ZIP_INVALID_ARCHIVE(
'central directory record count is inconsistent with its size');
}
return result;
}
module.exports = {
CentralFileHeader,
LocalFileHeader,
assertConsistentLocalHeader,
findArchiveEnd,
readCentralDirectory,
};