@ 261106.2
current
'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,
};