← Files ModRetro Chromatic PluginARCHIVED FILE

scripts/setup-io.mjs

46.7 KB · Oct 2, 2026 · 00:37 UTC

↓ Download file

/** I/O for explicitly selected setup operations. Never called by passive doctor. */
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { constants, createReadStream, createWriteStream } from "node:fs";
import { chmod, lstat, mkdir, open, readdir, realpath, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createGunzip, createInflateRaw } from "node:zlib";

export const SETUP_IO_LIMITS = Object.freeze({ archiveBytes: 512 * 1024 * 1024, entries: 32768, extractedBytes: 2 * 1024 * 1024 * 1024, directoryBytes: 64 * 1024 * 1024, fileBytes: 512 * 1024 * 1024 });
const HTTPS_HOSTS = new Set(["github.com", "codeload.github.com", "objects.githubusercontent.com", "release-assets.githubusercontent.com", "registry.npmjs.org", "nodejs.org"]);
const WINDOWS_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu;
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const key = (filename) => process.platform === "win32" ? filename.toLowerCase() : filename;
const progress = (context, event) => { context.onProgress?.(event); };

export class SetupIoError extends Error {
  constructor(code, message, details = {}, cause) {
    super(message, cause ? { cause } : undefined);
    this.name = "SetupIoError";
    this.code = code;
    this.details = details;
  }
}

export function checkSetupAbort(signal) {
  if (signal?.aborted) throw new SetupIoError("ABORT_ERR", "Setup was cancelled; completed components and owned partial files are preserved.", {}, signal.reason);
}

function within(root, filename) {
  const relative = path.relative(key(path.resolve(root)), key(path.resolve(filename)));
  return relative === "" || (relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative));
}

async function metadata(filename) {
  try { return await lstat(filename); }
  catch (error) { if (error.code === "ENOENT") return undefined; throw error; }
}

/** A parent-held lock/root marker grants ownership; this helper checks path confinement. */
export async function assertOwnedPath(root, filename, { allowMissing = false } = {}) {
  if (!path.isAbsolute(root) || !path.isAbsolute(filename) || !within(root, filename)) throw new SetupIoError("UNSAFE_PATH", "Setup write path must remain inside its explicit owned root.", { root, path: filename });
  const resolvedRoot = path.resolve(root);
  const rootEntry = await metadata(resolvedRoot);
  if (!rootEntry?.isDirectory() || rootEntry.isSymbolicLink() || key(await realpath(resolvedRoot)) !== key(resolvedRoot)) throw new SetupIoError("UNSAFE_PATH", "The owned setup root must be a real directory, without links or junctions.", { root });
  let current = resolvedRoot;
  for (const component of path.relative(resolvedRoot, path.resolve(filename)).split(path.sep).filter(Boolean)) {
    current = path.join(current, component);
    const entry = await metadata(current);
    if (!entry) {
      if (allowMissing) return path.resolve(filename);
      throw new SetupIoError("MISSING_PATH", "Required setup path is missing.", { path: current });
    }
    if (entry.isSymbolicLink() || key(await realpath(current)) !== key(current)) throw new SetupIoError("UNSAFE_PATH", "Setup paths cannot traverse a symbolic link or junction.", { path: current });
  }
  return path.resolve(filename);
}

export async function ensureOwnedDirectory(root, filename) {
  await assertOwnedPath(root, filename, { allowMissing: true });
  let current = path.resolve(root);
  for (const component of path.relative(current, path.resolve(filename)).split(path.sep).filter(Boolean)) {
    current = path.join(current, component);
    const entry = await metadata(current);
    if (!entry) await mkdir(current, { mode: 0o700 });
    else if (!entry.isDirectory() || entry.isSymbolicLink()) throw new SetupIoError("UNSAFE_PATH", "A setup directory is occupied by a non-directory or link.", { path: current });
    await assertOwnedPath(root, current);
  }
  return path.resolve(filename);
}

export async function setupSha256File(filename, maximumBytes = SETUP_IO_LIMITS.archiveBytes) {
  const before = await lstat(filename);
  if (!before.isFile() || before.isSymbolicLink() || before.size > maximumBytes) throw new SetupIoError("INVALID_FILE", "Setup requires a bounded regular file.", { path: filename });
  const handle = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
  try {
    const opened = await handle.stat();
    if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size) throw new SetupIoError("FILE_CHANGED", "A setup input changed while opening.", { path: filename });
    const hash = createHash("sha256");
    let bytes = 0;
    for await (const chunk of handle.createReadStream({ autoClose: false })) { bytes += chunk.length; if (bytes > maximumBytes) throw new SetupIoError("SIZE_LIMIT", "Setup input exceeds its size limit."); hash.update(chunk); }
    const after = await handle.stat();
    if (bytes !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new SetupIoError("FILE_CHANGED", "A setup input changed while reading.", { path: filename });
    return { sha256: hash.digest("hex"), bytes };
  } finally { await handle.close(); }
}

function trustedUrl(value) {
  const selected = new URL(value);
  if (selected.protocol !== "https:" || selected.username || selected.password || (selected.port && selected.port !== "443") || !HTTPS_HOSTS.has(selected.hostname)) throw new SetupIoError("UNTRUSTED_DOWNLOAD", "Setup refused a non-official HTTPS download or redirect.", { origin: selected.origin });
  return selected;
}

async function quarantine(context, filename, reason) {
  const destinationRoot = await ensureOwnedDirectory(context.setupRoot, path.join(context.setupRoot, "quarantine", "downloads"));
  await assertOwnedPath(context.setupRoot, filename);
  const destination = path.join(destinationRoot, path.basename(filename) + "." + randomUUID() + "." + reason);
  await rename(filename, destination);
  progress(context, { phase: "download-quarantined", path: destination, reason });
  return destination;
}

/** Authenticate complete files only; never reuse a .partial or retain a corrupt cache hit. */
export async function downloadArchive(release, context, { fetchImpl = globalThis.fetch, timeoutMs = 20 * 60_000, maximumBytes = SETUP_IO_LIMITS.archiveBytes, attempts = 2 } = {}) {
  checkSetupAbort(context.signal);
  if (!release || !/^[a-f0-9]{64}$/u.test(release.sha256) || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/u.test(release.archive)) throw new SetupIoError("INVALID_RELEASE", "Setup download requires a pinned SHA-256 and safe archive basename.");
  trustedUrl(release.url);
  const cache = await ensureOwnedDirectory(context.setupRoot, path.join(context.setupRoot, "downloads"));
  const destination = path.join(cache, release.sha256 + "-" + release.archive);
  await assertOwnedPath(context.setupRoot, destination, { allowMissing: true });
  const quarantined = [];
  if (await metadata(destination)) {
    const cached = await lstat(destination);
    if (!cached.isFile() || cached.isSymbolicLink()) throw new SetupIoError("UNSAFE_PATH", "The owned archive cache contains a non-file entry.", { path: destination });
    const existing = cached.size <= maximumBytes ? await setupSha256File(destination, maximumBytes) : undefined;
    if (existing?.sha256 === release.sha256) return { path: destination, ...existing, source: release.url, reused: true, quarantined };
    try { quarantined.push(await quarantine(context, destination, "checksum-mismatch")); }
    catch (cleanupError) {
      const mismatch = new SetupIoError("CHECKSUM_MISMATCH", "The cached archive does not match its packaged SHA-256 or size limit.", { source: release.url, expected: release.sha256, actual: existing?.sha256 });
      throw new SetupIoError(mismatch.code, mismatch.message, {
        ...mismatch.details, cachePath: destination, quarantined, quarantineConfirmed: false, retryStopped: true,
        quarantineError: { code: cleanupError.code ?? "QUARANTINE_FAILED", message: String(cleanupError.message ?? cleanupError) },
      }, mismatch);
    }
  }
  let lastError;
  for (let attempt = 1; attempt <= Math.min(Math.max(attempts, 1), 3); attempt++) {
    checkSetupAbort(context.signal);
    const partial = destination + "." + randomUUID() + ".partial";
    const timeout = new AbortController();
    const timer = setTimeout(() => timeout.abort(new Error("Download timeout")), timeoutMs);
    timer.unref?.();
    const signal = context.signal ? AbortSignal.any([context.signal, timeout.signal]) : timeout.signal;
    try {
      let url = trustedUrl(release.url);
      let response;
      for (let redirects = 0; redirects < 8; redirects++) {
        checkSetupAbort(signal);
        response = await fetchImpl(url, { redirect: "manual", signal, headers: { "Accept-Encoding": "identity" } });
        if (response.status < 300 || response.status >= 400) break;
        const location = response.headers.get("location");
        await response.body?.cancel();
        if (!location) throw new SetupIoError("DOWNLOAD_FAILED", "Official download redirected without a destination.");
        url = trustedUrl(new URL(location, url));
        response = undefined;
      }
      if (!response?.ok || !response.body) throw new SetupIoError("DOWNLOAD_FAILED", "Official archive download failed.", { source: release.url, status: response?.status ?? "redirect-limit", attempt });
      const declared = response.headers.get("content-length");
      if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > maximumBytes)) { await response.body.cancel(); throw new SetupIoError("SIZE_LIMIT", "The official archive exceeds its download size limit."); }
      let bytes = 0;
      const hash = createHash("sha256");
      const bounded = new Transform({ transform(chunk, _encoding, callback) { bytes += chunk.length; if (bytes > maximumBytes) callback(new SetupIoError("SIZE_LIMIT", "The official archive exceeds its download size limit.")); else { hash.update(chunk); callback(null, chunk); } } });
      progress(context, { phase: "download-start", source: release.url, attempt });
      await pipeline(Readable.fromWeb(response.body), bounded, createWriteStream(partial, { flags: "wx", mode: 0o600 }), { signal });
      const sha256 = hash.digest("hex");
      if (sha256 !== release.sha256) throw new SetupIoError("CHECKSUM_MISMATCH", "The downloaded archive does not match its packaged SHA-256.", { source: release.url, expected: release.sha256, actual: sha256 });
      await assertOwnedPath(context.setupRoot, partial);
      await assertOwnedPath(context.setupRoot, destination, { allowMissing: true });
      if (await metadata(destination)) throw new SetupIoError("DESTINATION_EXISTS", "Setup cache changed during its locked download.", { path: destination });
      await rename(partial, destination);
      progress(context, { phase: "download-complete", source: release.url, sha256, bytes });
      return { path: destination, sha256, bytes, source: release.url, reused: false, quarantined };
    } catch (error) {
      let quarantineError;
      try { if (await metadata(partial)) quarantined.push(await quarantine(context, partial, "incomplete")); }
      catch (cleanupError) { quarantineError = { code: cleanupError.code ?? "QUARANTINE_FAILED", message: String(cleanupError.message ?? cleanupError) }; }
      const cancelled = context.signal?.aborted;
      const timedOut = timeout.signal.aborted;
      const code = cancelled ? "ABORT_ERR" : timedOut ? "DOWNLOAD_TIMEOUT" : error.code ?? "DOWNLOAD_FAILED";
      const message = cancelled
        ? (quarantineError ? "Setup download was cancelled; quarantine could not be confirmed. Inspect the recorded partial path." : "Setup download was cancelled; any partial file was quarantined.")
        : timedOut ? "The official archive download exceeded its time limit." : error.message ?? "Archive download failed.";
      const failure = new SetupIoError(code, message, {
        ...(error.details ?? {}), source: release.url, attempt, partialPath: partial, quarantined,
        ...(timedOut ? { timeoutMs } : {}),
        ...(quarantineError ? { quarantineError, quarantineConfirmed: false, retryStopped: true } : {}),
      }, error);
      // An unconfirmed quarantine is never a reason to retry or replace the
      // original failure. Keep the failed operation and cleanup error separate.
      if (cancelled || quarantineError) throw failure;
      lastError = failure;
      if (["UNTRUSTED_DOWNLOAD", "SIZE_LIMIT", "UNSAFE_PATH", "DESTINATION_EXISTS"].includes(code)) break;
      progress(context, { phase: "download-failed", source: release.url, attempt, code });
    } finally { clearTimeout(timer); }
  }
  throw new SetupIoError(lastError?.code ?? "DOWNLOAD_FAILED", lastError?.message ?? "Archive download failed.", { ...(lastError?.details ?? {}), quarantined }, lastError);
}

function archiveName(name) {
  if (typeof name !== "string" || !name || name.length > 4096 || /[\\:<>"|?*\u0000-\u001f]/u.test(name) || name.startsWith("/")) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive contains an unsafe path.", { path: name });
  const normalized = name.replace(/\/$/u, "");
  const parts = normalized.split("/");
  if (parts.length > 128 || parts.some((part) => !part || part === "." || part === ".." || part.length > 255 || /[. ]$/u.test(part) || WINDOWS_NAME.test(part))) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive contains an unsafe path component.", { path: name });
  return normalized;
}

function completeManifest(manifest, limits) {
  const paths = new Map();
  let extractedBytes = 0;
  for (const entry of manifest) {
    entry.path = archiveName(entry.path);
    const identity = entry.path.toLowerCase();
    if (paths.has(identity)) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive contains duplicate or case-aliased paths.", { path: entry.path });
    paths.set(identity, entry.directory);
    extractedBytes += entry.size;
    if (entry.size > limits.fileBytes || extractedBytes > limits.extractedBytes || paths.size > limits.entries) throw new SetupIoError("SIZE_LIMIT", "Archive exceeds its extraction limits.");
  }
  for (const entry of manifest) {
    const parts = entry.path.toLowerCase().split("/");
    for (let count = 1; count < parts.length; count++) if (paths.get(parts.slice(0, count).join("/")) === false) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive uses a file as a directory.", { path: entry.path });
  }
  if (!manifest.length) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive is empty.");
  return { entries: manifest.length, extractedBytes, manifest };
}

class StreamReader {
  constructor(stream, signal) { this.iterator = stream[Symbol.asyncIterator](); this.buffer = Buffer.alloc(0); this.offset = 0; this.signal = signal; }
  async read(size, optional = false) {
    checkSetupAbort(this.signal);
    const chunks = [];
    let remaining = size;
    while (remaining) {
      if (this.offset === this.buffer.length) {
        const next = await this.iterator.next();
        if (next.done) { if (optional && remaining === size) return undefined; throw new SetupIoError("INVALID_ARCHIVE", "Archive ended inside an entry."); }
        this.buffer = next.value; this.offset = 0;
      }
      const count = Math.min(remaining, this.buffer.length - this.offset);
      chunks.push(this.buffer.subarray(this.offset, this.offset + count));
      this.offset += count; remaining -= count;
    }
    return chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size);
  }
}

const textField = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, bytes.indexOf(0) < 0 ? bytes.length : bytes.indexOf(0)));
function tarNumber(bytes) {
  const value = bytes.toString("ascii").replace(/\0.*$/su, "").trim();
  if (!/^[0-7]*$/u.test(value)) throw new SetupIoError("INVALID_ARCHIVE", "Unsupported TAR numeric field.");
  const number = value ? parseInt(value, 8) : 0;
  if (!Number.isSafeInteger(number) || number < 0) throw new SetupIoError("SIZE_LIMIT", "TAR numeric field exceeds limits.");
  return number;
}

function paxValues(bytes) {
  const values = {};
  let offset = 0;
  while (offset < bytes.length) {
    const space = bytes.indexOf(32, offset);
    const rawLength = bytes.subarray(offset, space).toString("ascii");
    if (space < 0 || !/^[1-9]\d*$/u.test(rawLength)) throw new SetupIoError("INVALID_ARCHIVE", "Malformed TAR extended header.");
    const length = Number(rawLength);
    if (!Number.isSafeInteger(length) || length < space - offset + 3 || offset + length > bytes.length || bytes[offset + length - 1] !== 10) throw new SetupIoError("INVALID_ARCHIVE", "Malformed TAR extended-header length.");
    const record = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(space + 1, offset + length - 1));
    const equal = record.indexOf("=");
    if (equal < 1) throw new SetupIoError("INVALID_ARCHIVE", "Malformed TAR extended-header value.");
    const name = record.slice(0, equal);
    if (!["path", "mtime", "atime", "ctime", "comment", "uid", "gid", "uname", "gname"].includes(name)) throw new SetupIoError("UNSAFE_ARCHIVE", "Unsupported TAR extended-header semantics.", { field: name });
    values[name] = record.slice(equal + 1);
    offset += length;
  }
  return values;
}

async function visitTar(filename, { signal, limits }, visitor) {
  const input = createReadStream(filename, { signal });
  const gunzip = createGunzip();
  let expanded = 0;
  const bounded = new Transform({ transform(chunk, _encoding, callback) { expanded += chunk.length; callback(expanded > limits.extractedBytes + limits.entries * 2048 ? new SetupIoError("SIZE_LIMIT", "Expanded TAR exceeds its bound.") : null, chunk); } });
  // The pending pipeline is always observed, including a validation failure mid-stream.
  const completion = pipeline(input, gunzip, bounded).catch((error) => error);
  const reader = new StreamReader(bounded, signal);
  let pending = {};
  let longName;
  let physicalEntries = 0;
  try {
    for (;;) {
      const header = await reader.read(512, true);
      if (!header) throw new SetupIoError("INVALID_ARCHIVE", "TAR is missing its end marker.");
      if (header.every((byte) => byte === 0)) {
        const second = await reader.read(512);
        if (!second.every((byte) => byte === 0)) throw new SetupIoError("INVALID_ARCHIVE", "TAR contains an invalid end marker.");
        for (;;) { const trailing = await reader.read(512, true); if (!trailing) break; if (!trailing.every((byte) => byte === 0)) throw new SetupIoError("INVALID_ARCHIVE", "TAR contains data after its end marker."); }
        if (Object.keys(pending).length || longName !== undefined) throw new SetupIoError("INVALID_ARCHIVE", "TAR ends with an unapplied extended header.");
        break;
      }
      if (++physicalEntries > limits.entries * 3) throw new SetupIoError("SIZE_LIMIT", "TAR contains too many physical entries.");
      const expectedChecksum = tarNumber(header.subarray(148, 156));
      let checksum = 0;
      for (let index = 0; index < 512; index++) checksum += index >= 148 && index < 156 ? 32 : header[index];
      if (checksum !== expectedChecksum) throw new SetupIoError("INVALID_ARCHIVE", "TAR header checksum mismatch.");
      const size = tarNumber(header.subarray(124, 136));
      if (size > limits.fileBytes) throw new SetupIoError("SIZE_LIMIT", "TAR member exceeds its size limit.");
      const type = String.fromCharCode(header[156] || 48);
      let file;
      if (["x", "g", "L"].includes(type)) {
        if (size > 65536) throw new SetupIoError("SIZE_LIMIT", "TAR extended header is oversized.");
        const bytes = size ? await reader.read(size) : Buffer.alloc(0);
        if (type === "L") longName = textField(bytes).replace(/\n$/u, "");
        else {
          const values = paxValues(bytes);
          if (type === "g" && values.path !== undefined) throw new SetupIoError("UNSAFE_ARCHIVE", "Global TAR path overrides are unsupported.");
          if (type === "x") pending = { ...pending, ...values };
        }
      } else {
        if (["1", "2", "K"].includes(type)) throw new SetupIoError("UNSUPPORTED_ARCHIVE_LINK", "Archive contains a link; generic setup extraction does not follow links.");
        if (!["0", "5"].includes(type)) throw new SetupIoError("UNSAFE_ARCHIVE", "Archive contains a non-file entry.", { type });
        const prefix = header.subarray(257, 263).toString("ascii").startsWith("ustar") ? textField(header.subarray(345, 500)) : "";
        const name = pending.path ?? longName ?? [prefix, textField(header.subarray(0, 100))].filter(Boolean).join("/");
        const entry = { path: archiveName(name), directory: type === "5", size, mode: tarNumber(header.subarray(100, 108)) & 0o111 ? 0o755 : 0o644 };
        if (entry.directory && size) throw new SetupIoError("INVALID_ARCHIVE", "TAR directory has a payload.");
        pending = {}; longName = undefined;
        file = await visitor(entry);
        try {
          for (let remaining = size; remaining > 0;) {
            const bytes = await reader.read(Math.min(65536, remaining));
            if (file) await writeAll(file, bytes);
            remaining -= bytes.length;
          }
        } finally { await file?.close(); }
      }
      if (size % 512) await reader.read(512 - (size % 512));
    }
    const failure = await completion;
    if (failure instanceof Error) throw failure;
  } finally { input.destroy(); gunzip.destroy(); bounded.destroy(); await completion; }
}

async function writeAll(handle, bytes) {
  for (let offset = 0; offset < bytes.length;) { const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset); if (!bytesWritten) throw new SetupIoError("WRITE_FAILED", "Setup write did not make progress."); offset += bytesWritten; }
}

export async function inspectTarArchive(filename, { signal, limits = SETUP_IO_LIMITS } = {}) {
  const manifest = [];
  await setupSha256File(filename, limits.archiveBytes);
  await visitTar(filename, { signal, limits }, async (entry) => { manifest.push(entry); if (manifest.length > limits.entries) throw new SetupIoError("SIZE_LIMIT", "Archive has too many entries."); });
  return completeManifest(manifest, limits);
}

async function readAt(handle, count, offset) {
  const bytes = Buffer.alloc(count);
  let read = 0;
  while (read < count) { const result = await handle.read(bytes, read, count - read, offset + read); if (!result.bytesRead) throw new SetupIoError("INVALID_ARCHIVE", "ZIP ended inside an entry."); read += result.bytesRead; }
  return bytes;
}

/** Validate central AND local headers before any extraction writes. */
export async function inspectZipArchive(filename, { signal, limits = SETUP_IO_LIMITS } = {}) {
  await setupSha256File(filename, limits.archiveBytes);
  const handle = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
  try {
    const metadata = await handle.stat();
    if (metadata.size < 22) throw new SetupIoError("INVALID_ARCHIVE", "ZIP is too short.");
    const tail = await readAt(handle, Math.min(metadata.size, 65557), Math.max(0, metadata.size - 65557));
    let end = -1;
    for (let index = tail.length - 22; index >= 0; index--) if (tail.readUInt32LE(index) === 0x06054b50 && index + 22 + tail.readUInt16LE(index + 20) === tail.length) { end = index; break; }
    if (end < 0) throw new SetupIoError("INVALID_ARCHIVE", "ZIP has no valid central-directory trailer.");
    const count = tail.readUInt16LE(end + 10);
    const centralSize = tail.readUInt32LE(end + 12);
    const centralOffset = tail.readUInt32LE(end + 16);
    if (tail.readUInt16LE(end + 4) || tail.readUInt16LE(end + 6) || count !== tail.readUInt16LE(end + 8) || !count || count > limits.entries || count === 0xffff || centralSize > limits.directoryBytes || centralOffset === 0xffffffff || centralOffset + centralSize !== metadata.size - tail.length + end) throw new SetupIoError("INVALID_ARCHIVE", "ZIP uses an unsupported multi-disk, ZIP64, or oversized layout.");
    const central = await readAt(handle, centralSize, centralOffset);
    const manifest = [];
    const spans = [];
    let offset = 0;
    for (let index = 0; index < count; index++) {
      checkSetupAbort(signal);
      if (offset + 46 > central.length || central.readUInt32LE(offset) !== 0x02014b50) throw new SetupIoError("INVALID_ARCHIVE", "Malformed ZIP central-directory entry.");
      const flags = central.readUInt16LE(offset + 8);
      const method = central.readUInt16LE(offset + 10);
      const crc = central.readUInt32LE(offset + 16);
      const compressedSize = central.readUInt32LE(offset + 20);
      const size = central.readUInt32LE(offset + 24);
      const nameLength = central.readUInt16LE(offset + 28);
      const extraLength = central.readUInt16LE(offset + 30);
      const next = offset + 46 + nameLength + extraLength + central.readUInt16LE(offset + 32);
      const attributes = central.readUInt32LE(offset + 38);
      const localOffset = central.readUInt32LE(offset + 42);
      const unixType = (attributes >>> 16) & 0o170000;
      if (unixType === 0o120000 || (attributes & 0x400)) throw new SetupIoError("UNSUPPORTED_ARCHIVE_LINK", "ZIP contains a link or reparse point; generic setup extraction does not follow links.");
      if (unixType && unixType !== 0o100000 && unixType !== 0o040000) throw new SetupIoError("UNSAFE_ARCHIVE", "ZIP contains a non-file entry.");
      if (next > central.length || flags & ~0x080e || ![0, 8].includes(method) || central.readUInt16LE(offset + 34) || compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff || localOffset + 30 > centralOffset || size > limits.fileBytes) throw new SetupIoError("INVALID_ARCHIVE", "ZIP member uses unsupported encryption, compression, or size semantics.");
      const rawName = central.subarray(offset + 46, offset + 46 + nameLength);
      const name = new TextDecoder("utf-8", { fatal: true }).decode(rawName);
      const directory = name.endsWith("/");
      if ((directory && size) || (!directory && unixType === 0o040000)) throw new SetupIoError("INVALID_ARCHIVE", "ZIP directory metadata disagrees with its path.");
      const local = await readAt(handle, 30, localOffset);
      if (local.readUInt32LE(0) !== 0x04034b50 || local.readUInt16LE(6) !== flags || local.readUInt16LE(8) !== method || local.readUInt16LE(26) !== nameLength) throw new SetupIoError("INVALID_ARCHIVE", "ZIP local and central headers disagree.");
      const localName = await readAt(handle, nameLength, localOffset + 30);
      if (!localName.equals(rawName)) throw new SetupIoError("UNSAFE_ARCHIVE", "ZIP local and central paths disagree.");
      const dataOffset = localOffset + 30 + nameLength + local.readUInt16LE(28);
      let dataEnd = dataOffset + compressedSize;
      if (dataEnd > centralOffset) throw new SetupIoError("INVALID_ARCHIVE", "ZIP member data overlaps its central directory.");
      if (flags & 8) {
        const first = await readAt(handle, 4, dataEnd);
        const descriptor = await readAt(handle, first.readUInt32LE(0) === 0x08074b50 ? 16 : 12, dataEnd);
        const start = descriptor.length === 16 ? 4 : 0;
        if (descriptor.readUInt32LE(start) !== crc || descriptor.readUInt32LE(start + 4) !== compressedSize || descriptor.readUInt32LE(start + 8) !== size) throw new SetupIoError("INVALID_ARCHIVE", "ZIP data descriptor disagrees with its central directory.");
        dataEnd += descriptor.length;
      } else if (local.readUInt32LE(14) !== crc || local.readUInt32LE(18) !== compressedSize || local.readUInt32LE(22) !== size) throw new SetupIoError("INVALID_ARCHIVE", "ZIP local sizes or checksum disagree with its central directory.");
      if (dataEnd > centralOffset || (method === 0 && compressedSize !== size)) throw new SetupIoError("INVALID_ARCHIVE", "ZIP has invalid payload bounds.");
      spans.push([localOffset, dataEnd]);
      manifest.push({ path: name, directory, size, compressedSize, dataOffset, method, crc, mode: ((attributes >>> 16) & 0o111) ? 0o755 : 0o644 });
      offset = next;
    }
    if (offset !== central.length) throw new SetupIoError("INVALID_ARCHIVE", "ZIP central directory contains trailing entries.");
    spans.sort(([left], [right]) => left - right);
    for (let index = 1; index < spans.length; index++) if (spans[index][0] < spans[index - 1][1]) throw new SetupIoError("UNSAFE_ARCHIVE", "ZIP entries overlap.");
    return completeManifest(manifest, limits);
  } finally { await handle.close(); }
}

const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, number) => { let value = number; for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; return value >>> 0; });
export function setupCrc32(bytes, previous = 0) { let crc = previous ^ 0xffffffff; for (const value of bytes) crc = CRC_TABLE[(crc ^ value) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; }

async function extractionFile(ownedRoot, destination, entry) {
  const filename = path.join(destination, ...entry.path.split("/"));
  await ensureOwnedDirectory(ownedRoot, entry.directory ? filename : path.dirname(filename));
  if (entry.directory) return undefined;
  await assertOwnedPath(ownedRoot, filename, { allowMissing: true });
  return open(filename, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), entry.mode);
}

/** No extraction begins until the entire archive's entry map passes preflight. */
export async function extractArchive(filename, destination, context, { format, limits = SETUP_IO_LIMITS } = {}) {
  checkSetupAbort(context.signal);
  const kind = format ?? (/\.zip$/iu.test(filename) ? "zip" : /\.(?:tar\.gz|tgz)$/iu.test(filename) ? "tar.gz" : undefined);
  if (!kind) throw new SetupIoError("INVALID_ARCHIVE", "Setup archive format must be ZIP or gzip TAR.");
  await assertOwnedPath(context.setupRoot, filename);
  await assertOwnedPath(context.setupRoot, destination, { allowMissing: true });
  if (await metadata(destination)) throw new SetupIoError("DESTINATION_EXISTS", "Archive extraction requires a fresh, absent destination.", { path: destination });
  const before = await setupSha256File(filename, limits.archiveBytes);
  const options = { signal: context.signal, limits };
  const inspected = kind === "zip" ? await inspectZipArchive(filename, options) : await inspectTarArchive(filename, options);
  await ensureOwnedDirectory(context.setupRoot, path.dirname(destination));
  await mkdir(destination, { mode: 0o700 });
  progress(context, { phase: "extract-start", archive: path.basename(filename), entries: inspected.entries, extractedBytes: inspected.extractedBytes });
  if (kind === "tar.gz") {
    let index = 0;
    await visitTar(filename, options, async (entry) => {
      if (JSON.stringify(entry) !== JSON.stringify(inspected.manifest[index++])) throw new SetupIoError("FILE_CHANGED", "TAR changed after preflight.");
      return extractionFile(context.setupRoot, destination, entry);
    });
    if (index !== inspected.entries) throw new SetupIoError("FILE_CHANGED", "TAR entry count changed after preflight.");
  } else {
    for (const entry of inspected.manifest) {
      checkSetupAbort(context.signal);
      const file = await extractionFile(context.setupRoot, destination, entry);
      if (!file) continue;
      let input;
      let inflate;
      let bounded;
      try {
        let bytes = 0; let crc = 0;
        input = entry.compressedSize ? createReadStream(filename, { start: entry.dataOffset, end: entry.dataOffset + entry.compressedSize - 1, signal: context.signal }) : Readable.from([]);
        bounded = new Transform({ transform(chunk, _encoding, callback) { bytes += chunk.length; crc = setupCrc32(chunk, crc); callback(bytes > entry.size ? new SetupIoError("SIZE_LIMIT", "ZIP inflated beyond its declared size.") : null, chunk); } });
        const stream = entry.method === 8 ? [input, inflate = createInflateRaw(), bounded] : [input, bounded];
        const completion = pipeline(...stream).catch((error) => error);
        try { for await (const chunk of bounded) await writeAll(file, chunk); } finally { input.destroy(); inflate?.destroy(); bounded.destroy(); const error = await completion; if (error instanceof Error) throw error; }
        if (bytes !== entry.size || crc !== entry.crc) throw new SetupIoError("CHECKSUM_MISMATCH", "ZIP member size or CRC does not match its header.", { path: entry.path });
      } finally { input?.destroy(); inflate?.destroy(); bounded?.destroy(); await file.close(); }
    }
  }
  const after = await setupSha256File(filename, limits.archiveBytes);
  if (after.sha256 !== before.sha256 || after.bytes !== before.bytes) throw new SetupIoError("FILE_CHANGED", "Archive changed while extracting.");
  return { path: destination, entries: inspected.entries, extractedBytes: inspected.extractedBytes, archiveSha256: before.sha256 };
}

/** Copy only regular files/directories, never links; destination must not exist. */
export async function copySetupTree(source, destination, context, { sourceRoot = context.setupRoot, limits = SETUP_IO_LIMITS } = {}) {
  await assertOwnedPath(sourceRoot, source);
  await assertOwnedPath(context.setupRoot, destination, { allowMissing: true });
  if (await metadata(destination)) throw new SetupIoError("DESTINATION_EXISTS", "Setup copy requires a fresh destination.", { path: destination });
  const records = [];
  let totalBytes = 0;
  async function visit(current, relative) {
    checkSetupAbort(context.signal);
    await assertOwnedPath(sourceRoot, current);
    const entry = await lstat(current);
    if (!entry.isDirectory() && !entry.isFile()) throw new SetupIoError("UNSAFE_PATH", "Setup copy refuses non-file entries.", { path: current });
    if (relative) archiveName(relative);
    records.push({ relative, directory: entry.isDirectory(), size: entry.size, mode: entry.mode & 0o111 ? 0o755 : 0o644 });
    if (records.length > limits.entries) throw new SetupIoError("SIZE_LIMIT", "Setup tree has too many entries.");
    if (entry.isDirectory()) for (const name of (await readdir(current)).sort()) await visit(path.join(current, name), relative ? relative + "/" + name : name);
    else { totalBytes += entry.size; if (entry.size > limits.fileBytes || totalBytes > limits.extractedBytes) throw new SetupIoError("SIZE_LIMIT", "Setup tree exceeds its copy limits."); }
  }
  await visit(source, "");
  for (const entry of records) {
    checkSetupAbort(context.signal);
    const output = path.join(destination, ...entry.relative.split("/").filter(Boolean));
    if (entry.directory) await ensureOwnedDirectory(context.setupRoot, output);
    else {
      await ensureOwnedDirectory(context.setupRoot, path.dirname(output));
      const input = path.join(source, ...entry.relative.split("/").filter(Boolean));
      await assertOwnedPath(sourceRoot, input);
      const sourceFile = await open(input, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
      let outputFile;
      try { outputFile = await open(output, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), entry.mode); const before = await sourceFile.stat(); let count = 0; for await (const chunk of sourceFile.createReadStream({ autoClose: false })) { count += chunk.length; if (count > entry.size) throw new SetupIoError("FILE_CHANGED", "Setup source changed during copy."); await writeAll(outputFile, chunk); } const after = await sourceFile.stat(); if (count !== entry.size || before.mtimeMs !== after.mtimeMs || before.size !== after.size) throw new SetupIoError("FILE_CHANGED", "Setup source changed during copy."); }
      finally { await sourceFile.close(); await outputFile?.close(); }
    }
  }
  return { path: destination, entries: records.length, bytes: totalBytes };
}

/** Child-specific environment: no user package-manager config, hooks, or injection. */
export async function setupEnvironment(context) {
  const names = ["tmp", "cache", "cache/npm", "cache/node-gyp", "cache/yarn-global", "cache/uv", "config", "config/xdg", "data", "data/xdg", "python", "bin", "uv-tools"];
  for (const name of names) await ensureOwnedDirectory(context.setupRoot, path.join(context.setupRoot, ...name.split("/")));
  // npm refuses loading one physical file at both user and global precedence.
  const userConfig = path.join(context.setupRoot, "config", "empty-user-npmrc");
  const globalConfig = path.join(context.setupRoot, "config", "empty-global-npmrc");
  for (const config of [userConfig, globalConfig]) {
    if (!(await metadata(config))) await writeFile(config, "", { flag: "wx", mode: 0o600 });
    await assertOwnedPath(context.setupRoot, config);
    const configuration = await lstat(config);
    if (!configuration.isFile() || configuration.size !== 0) throw new SetupIoError("UNSAFE_CONFIGURATION", "Owned empty package-manager configuration was modified.", { path: config });
  }
  const original = new Map();
  for (const [name, value] of Object.entries(context.environment ?? process.env)) {
    if (value === undefined) continue;
    const upper = name.toUpperCase();
    if (context.platform === "win32" && original.has(upper) && original.get(upper) !== value) throw new SetupIoError("UNSAFE_CONFIGURATION", "Conflicting Windows environment aliases.", { name: upper });
    original.set(upper, String(value));
  }
  const env = {};
  for (const name of ["LANG", "LC_ALL", "LC_CTYPE", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"]) if (original.has(name)) env[name] = original.get(name);
  const windows = context.platform === "win32";
  let executables;
  if (windows) {
    const system = original.get("SYSTEMROOT");
    if (!system || !path.isAbsolute(system)) throw new SetupIoError("MISSING_PREREQUISITE", "Windows setup requires an explicit absolute SystemRoot.");
    env.SystemRoot = system; env.WINDIR = system; env.COMSPEC = path.join(system, "System32", "cmd.exe");
    executables = [path.dirname(context.nodeExecutable), path.join(system, "System32"), system];
  } else executables = [path.dirname(context.nodeExecutable), "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
  Object.assign(env, {
    PATH: executables.join(windows ? ";" : ":"),
    TMP: path.join(context.setupRoot, "tmp"), TEMP: path.join(context.setupRoot, "tmp"), TMPDIR: path.join(context.setupRoot, "tmp"),
    XDG_CACHE_HOME: path.join(context.setupRoot, "cache"), XDG_CONFIG_HOME: path.join(context.setupRoot, "config", "xdg"), XDG_DATA_HOME: path.join(context.setupRoot, "data", "xdg"),
    APPDATA: path.join(context.setupRoot, "config"), LOCALAPPDATA: path.join(context.setupRoot, "cache"),
    npm_config_cache: path.join(context.setupRoot, "cache", "npm"), npm_config_userconfig: userConfig, npm_config_globalconfig: globalConfig, npm_config_devdir: path.join(context.setupRoot, "cache", "node-gyp"), npm_config_registry: "https://registry.npmjs.org/", npm_config_audit: "false", npm_config_fund: "false",
    YARN_RC_FILENAME: ".codex-setup-yarnrc.yml", YARN_ENABLE_GLOBAL_CACHE: "true", YARN_GLOBAL_FOLDER: path.join(context.setupRoot, "cache", "yarn-global"), YARN_NODE_LINKER: "node-modules", YARN_NPM_REGISTRY_SERVER: "https://registry.npmjs.org", YARN_ENABLE_TELEMETRY: "0", YARN_CHECKSUM_BEHAVIOR: "throw", YARN_ENABLE_IMMUTABLE_INSTALLS: "true",
    UV_CACHE_DIR: path.join(context.setupRoot, "cache", "uv"), UV_PYTHON_INSTALL_DIR: path.join(context.setupRoot, "python"), UV_PYTHON_BIN_DIR: path.join(context.setupRoot, "bin"), UV_PYTHON_INSTALL_BIN: "0", UV_PYTHON_INSTALL_REGISTRY: "0", UV_TOOL_DIR: path.join(context.setupRoot, "uv-tools"), UV_TOOL_BIN_DIR: path.join(context.setupRoot, "bin"), UV_NO_CONFIG: "1", UV_NO_SOURCES: "1", UV_NO_PROGRESS: "1", UV_LINK_MODE: "copy", UV_PYTHON_PREFERENCE: "only-managed", UV_INDEX_URL: "https://pypi.org/simple",
    PIP_CONFIG_FILE: userConfig, PIP_DISABLE_PIP_VERSION_CHECK: "1", PYTHONNOUSERSITE: "1", PYTHONDONTWRITEBYTECODE: "1", PYTHONSAFEPATH: "1", ELECTRON_SKIP_BINARY_DOWNLOAD: "1", NO_COLOR: "1", CI: "1",
  });
  return env;
}

async function killTree(child, force, options) {
  if (!child.pid) return;
  if (options.platform !== "win32") {
    const alive = () => { try { process.kill(-child.pid, 0); return true; } catch (error) { if (error.code === "ESRCH") return false; throw error; } };
    const send = (signal) => { try { process.kill(-child.pid, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };
    if (!alive()) return;
    const started = Date.now();
    send(force ? "SIGKILL" : "SIGTERM");
    let escalated = force;
    while (alive()) {
      const elapsed = Date.now() - started;
      if (!escalated && elapsed >= 1500) { send("SIGKILL"); escalated = true; }
      if (elapsed >= 6500) throw new SetupIoError("PROCESS_CLEANUP_UNCONFIRMED", "The owned setup process group is still present after cancellation.", { pid: child.pid });
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
    return;
  }
  const command = path.join(options.env.SystemRoot, "System32", "taskkill.exe");
  await new Promise((resolve, reject) => {
    // Windows has no POSIX group signal. Force the explicitly owned tree rather
    // than relying on GUI close messages that cannot stop headless installers.
    const killer = spawn(command, ["/PID", String(child.pid), "/T", "/F"], { shell: false, windowsHide: true, stdio: "ignore", env: options.env });
    const timer = setTimeout(() => { killer.kill(); reject(new Error("taskkill did not complete")); }, 5000);
    killer.once("error", (error) => { clearTimeout(timer); reject(error); });
    killer.once("close", (code) => { clearTimeout(timer); if (code === 0 || code === 128) resolve(); else reject(new Error("taskkill exited " + code)); });
  });
}

/** Bounded logs and cancellation of the owned process group, not only its shell. */
export async function runSetupCommand(command, args, options = {}) {
  const { cwd, env, signal, onProgress, timeoutMs = 30 * 60_000, maxOutputBytes = 65536, platform = process.platform, spawnImpl = spawn, terminateTree = killTree } = options;
  checkSetupAbort(signal);
  if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 1024 * 1024 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60 * 60_000) throw new SetupIoError("INVALID_COMMAND", "Setup command output/time limits are invalid.");
  if (!path.isAbsolute(command) || !path.isAbsolute(cwd) || !Array.isArray(args) || args.some((arg) => typeof arg !== "string" || arg.includes("\0"))) throw new SetupIoError("INVALID_COMMAND", "Setup subprocesses require explicit absolute executable/cwd paths and string arguments.");
  return new Promise((resolve, reject) => {
    let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); let truncated = false; let primary; let closed = false; let settled = false; let escalation; let cleanupDeadline;
    const cleanupErrors = []; const cleanup = [];
    let child;
    try { child = spawnImpl(command, args, { cwd, env, shell: false, windowsHide: true, detached: platform !== "win32", stdio: ["ignore", "pipe", "pipe"] }); }
    catch (error) { reject(new SetupIoError("COMMAND_SPAWN_FAILED", "The selected setup executable could not be started.", { exitObserved: true, execution: "not-started", cleanupErrors: [] }, error)); return; }
    const label = options.label ?? path.basename(command);
    const started = Date.now();
    const capture = (which, chunk) => { const combined = Buffer.concat([which === "stdout" ? stdout : stderr, Buffer.from(chunk)]); if (combined.length > maxOutputBytes) truncated = true; const tail = combined.subarray(Math.max(0, combined.length - maxOutputBytes)); if (which === "stdout") stdout = tail; else stderr = tail; };
    child.stdout?.on("data", (chunk) => capture("stdout", chunk)); child.stderr?.on("data", (chunk) => capture("stderr", chunk));
    const finish = async (code, exitSignal, exitObserved) => {
      if (settled) return; settled = true;
      clearTimeout(timeout); clearInterval(heartbeat); clearTimeout(escalation); clearTimeout(cleanupDeadline); signal?.removeEventListener("abort", abort);
      await Promise.allSettled(cleanup);
      const result = { stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8"), outputTruncated: truncated, exitCode: code, signal: exitSignal, leaderExitObserved: exitObserved, exitObserved: exitObserved && cleanupErrors.length === 0, pid: child.pid, cleanupErrors, cleanupMechanism: platform === "win32" ? "windows-taskkill-tree" : "unix-process-group" };
      if (primary || code !== 0 || cleanupErrors.length) reject(new SetupIoError(primary?.code ?? (cleanupErrors.length ? "PROCESS_CLEANUP_UNCONFIRMED" : "COMMAND_FAILED"), primary?.message ?? `${label} exited with ${code ?? exitSignal ?? "unknown status"}; cleanup failures: ${cleanupErrors.join("; ")}.`, { ...result, label }, primary));
      else resolve(result);
    };
    const terminate = (force) => { const promise = Promise.resolve().then(() => terminateTree(child, force, { ...options, platform, env })).catch((error) => { cleanupErrors.push(String(error.message ?? error)); try { child.kill(force ? "SIGKILL" : "SIGTERM"); } catch (fallback) { cleanupErrors.push(String(fallback.message ?? fallback)); } }); cleanup.push(promise); };
    const stop = (error) => { if (primary || closed) return; primary = error; terminate(false); escalation = setTimeout(() => { if (!closed) terminate(true); }, 1500); cleanupDeadline = setTimeout(() => { if (!closed) void finish(undefined, undefined, false); }, 8000); };
    const abort = () => stop(new SetupIoError("ABORT_ERR", `${label} was cancelled; waiting for owned subprocess cleanup.`));
    const emit = (event) => { try { onProgress?.(event); } catch (error) { stop(new SetupIoError("PROGRESS_FAILED", "Setup progress reporting failed; the owned subprocess is being stopped.", {}, error)); } };
    const timeout = setTimeout(() => stop(new SetupIoError("COMMAND_TIMEOUT", `${label} exceeded its setup time limit.`)), timeoutMs);
    const heartbeat = setInterval(() => emit({ phase: "command-running", label, elapsedMs: Date.now() - started }), 15000);
    timeout.unref?.(); heartbeat.unref?.();
    child.once("error", (error) => {
      if (!child.pid) { primary ??= error; void finish(undefined, undefined, true); }
      else stop(error);
    });
    child.once("close", (code, exitSignal) => {
      closed = true;
      // A normal leader exit must not leave daemonized children in its owned
      // Unix process group. Cancellation already has a group cleanup in flight.
      if (child.pid && cleanup.length === 0 && platform !== "win32") terminate(true);
      void finish(code, exitSignal, true);
    });
    signal?.addEventListener("abort", abort, { once: true });
    emit({ phase: "command-start", label, pid: child.pid });
    if (signal?.aborted) abort();
  });
}

export async function writeSetupJson(root, filename, value) {
  await assertOwnedPath(root, filename, { allowMissing: true });
  await ensureOwnedDirectory(root, path.dirname(filename));
  await writeFile(filename, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 0o600 });
  return { path: filename, sha256: digest(Buffer.from(JSON.stringify(value, null, 2) + "\n")) };
}

export async function makeSetupExecutable(root, filename) { await assertOwnedPath(root, filename); if (!(await lstat(filename)).isFile()) throw new SetupIoError("INVALID_FILE", "Expected a regular executable file.", { path: filename }); await chmod(filename, 0o755); }

SHA-256: 94b38e2e2c4ba2ca6372436c136b26876e79634ab3588848af1638bc002d453b