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