mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-07 18:03:02 +00:00
402 lines
13 KiB
JavaScript
402 lines
13 KiB
JavaScript
import { createReadStream } from "node:fs";
|
|
import { open, stat } from "node:fs/promises";
|
|
import { posix } from "node:path";
|
|
import { pipeline } from "node:stream/promises";
|
|
import { Transform } from "node:stream";
|
|
import { createInflateRaw } from "node:zlib";
|
|
import {
|
|
MAX_CENTRAL_DIRECTORY_BYTES,
|
|
MAX_STREAMED_ENTRY_BYTES,
|
|
MAX_ZIP_ENTRIES,
|
|
} from "./constants.mjs";
|
|
|
|
const EOCD_SIGNATURE = 0x06054b50;
|
|
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
const LOCAL_SIGNATURE = 0x04034b50;
|
|
const ZIP64_U16 = 0xffff;
|
|
const ZIP64_U32 = 0xffffffff;
|
|
|
|
const crcTable = new Uint32Array(256);
|
|
for (let index = 0; index < 256; index++) {
|
|
let value = index;
|
|
for (let bit = 0; bit < 8; bit++) {
|
|
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
}
|
|
crcTable[index] = value >>> 0;
|
|
}
|
|
|
|
function crc32(buffer) {
|
|
let value = 0xffffffff;
|
|
for (const byte of buffer) {
|
|
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
}
|
|
return (value ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
async function readExactly(handle, length, position) {
|
|
const buffer = Buffer.allocUnsafe(length);
|
|
let offset = 0;
|
|
while (offset < length) {
|
|
const result = await handle.read(buffer, offset, length - offset, position + offset);
|
|
if (result.bytesRead === 0) {
|
|
throw new Error("ZIP archive ended unexpectedly.");
|
|
}
|
|
offset += result.bytesRead;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
function safeEntryPath(rawName) {
|
|
if (!rawName || rawName.includes("\0")) {
|
|
throw new Error("ZIP archive contains an invalid file name.");
|
|
}
|
|
const slashed = rawName.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
const normalized = posix.normalize(slashed);
|
|
if (
|
|
normalized === "." ||
|
|
normalized === ".." ||
|
|
normalized.startsWith("../") ||
|
|
normalized.startsWith("/") ||
|
|
/^[A-Za-z]:\//.test(normalized)
|
|
) {
|
|
throw new Error(`ZIP archive contains an unsafe path: ${rawName}`);
|
|
}
|
|
if (normalized.length > 2_048) {
|
|
throw new Error("ZIP archive contains an excessively long file name.");
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function dosDateTime(date, time) {
|
|
if (!date) return null;
|
|
const year = 1980 + ((date >> 9) & 0x7f);
|
|
const month = (date >> 5) & 0x0f;
|
|
const day = date & 0x1f;
|
|
const hour = (time >> 11) & 0x1f;
|
|
const minute = (time >> 5) & 0x3f;
|
|
const second = (time & 0x1f) * 2;
|
|
const value = new Date(Date.UTC(year, Math.max(0, month - 1), day, hour, minute, second));
|
|
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
}
|
|
|
|
function decodeName(buffer, utf8) {
|
|
// GitHub-generated artifacts use UTF-8. For legacy archives, decoding as
|
|
// latin1 preserves a stable byte-for-byte lookup instead of replacing bytes.
|
|
return buffer.toString(utf8 ? "utf8" : "latin1");
|
|
}
|
|
|
|
export async function readZipIndex(zipPath) {
|
|
const archive = await stat(zipPath);
|
|
if (!archive.isFile() || archive.size < 22) {
|
|
throw new Error("Artifact is not a valid ZIP archive.");
|
|
}
|
|
|
|
const handle = await open(zipPath, "r");
|
|
try {
|
|
const tailLength = Math.min(archive.size, 65_557);
|
|
const tailOffset = archive.size - tailLength;
|
|
const tail = await readExactly(handle, tailLength, tailOffset);
|
|
let eocdOffset = -1;
|
|
for (let index = tail.length - 22; index >= 0; index--) {
|
|
if (tail.readUInt32LE(index) === EOCD_SIGNATURE) {
|
|
if (index + 22 <= tail.length) {
|
|
const commentLength = tail.readUInt16LE(index + 20);
|
|
if (index + 22 + commentLength === tail.length) {
|
|
eocdOffset = index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (eocdOffset < 0) {
|
|
throw new Error("ZIP end-of-directory record was not found.");
|
|
}
|
|
|
|
const disk = tail.readUInt16LE(eocdOffset + 4);
|
|
const centralDisk = tail.readUInt16LE(eocdOffset + 6);
|
|
const entriesOnDisk = tail.readUInt16LE(eocdOffset + 8);
|
|
const entryCount = tail.readUInt16LE(eocdOffset + 10);
|
|
const centralSize = tail.readUInt32LE(eocdOffset + 12);
|
|
const centralOffset = tail.readUInt32LE(eocdOffset + 16);
|
|
if (disk !== 0 || centralDisk !== 0 || entriesOnDisk !== entryCount) {
|
|
throw new Error("Multi-disk ZIP artifacts are not supported.");
|
|
}
|
|
if (
|
|
entryCount === ZIP64_U16 ||
|
|
centralSize === ZIP64_U32 ||
|
|
centralOffset === ZIP64_U32
|
|
) {
|
|
throw new Error("ZIP64 artifacts larger than 4 GiB are not supported.");
|
|
}
|
|
if (entryCount > MAX_ZIP_ENTRIES) {
|
|
throw new Error(`ZIP contains too many entries (${entryCount.toLocaleString()}).`);
|
|
}
|
|
if (centralSize > MAX_CENTRAL_DIRECTORY_BYTES) {
|
|
throw new Error("ZIP central directory exceeds the 64 MiB safety limit.");
|
|
}
|
|
if (centralOffset + centralSize > archive.size) {
|
|
throw new Error("ZIP central directory points outside the archive.");
|
|
}
|
|
|
|
const central = await readExactly(handle, centralSize, centralOffset);
|
|
const entries = [];
|
|
let cursor = 0;
|
|
while (cursor < central.length) {
|
|
if (central.length - cursor < 46 || central.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
|
|
throw new Error("ZIP central directory is malformed.");
|
|
}
|
|
const flags = central.readUInt16LE(cursor + 8);
|
|
const method = central.readUInt16LE(cursor + 10);
|
|
const modifiedTime = central.readUInt16LE(cursor + 12);
|
|
const modifiedDate = central.readUInt16LE(cursor + 14);
|
|
const expectedCrc32 = central.readUInt32LE(cursor + 16);
|
|
const compressedSize = central.readUInt32LE(cursor + 20);
|
|
const uncompressedSize = central.readUInt32LE(cursor + 24);
|
|
const nameLength = central.readUInt16LE(cursor + 28);
|
|
const extraLength = central.readUInt16LE(cursor + 30);
|
|
const commentLength = central.readUInt16LE(cursor + 32);
|
|
const diskStart = central.readUInt16LE(cursor + 34);
|
|
const externalAttributes = central.readUInt32LE(cursor + 38);
|
|
const localHeaderOffset = central.readUInt32LE(cursor + 42);
|
|
const recordLength = 46 + nameLength + extraLength + commentLength;
|
|
if (cursor + recordLength > central.length) {
|
|
throw new Error("ZIP central directory entry is truncated.");
|
|
}
|
|
if (
|
|
compressedSize === ZIP64_U32 ||
|
|
uncompressedSize === ZIP64_U32 ||
|
|
localHeaderOffset === ZIP64_U32 ||
|
|
diskStart === ZIP64_U16
|
|
) {
|
|
throw new Error("ZIP64 entries are not supported.");
|
|
}
|
|
|
|
const rawName = decodeName(
|
|
central.subarray(cursor + 46, cursor + 46 + nameLength),
|
|
Boolean(flags & 0x0800),
|
|
);
|
|
const name = safeEntryPath(rawName);
|
|
const unixMode = externalAttributes >>> 16;
|
|
const directory = name.endsWith("/");
|
|
const symlink = (unixMode & 0o170000) === 0o120000;
|
|
const encrypted = Boolean(flags & 0x0001);
|
|
entries.push({
|
|
name,
|
|
directory,
|
|
symlink,
|
|
encrypted,
|
|
supported: !directory && !symlink && !encrypted && (method === 0 || method === 8),
|
|
method,
|
|
flags,
|
|
crc32: expectedCrc32,
|
|
compressedSize,
|
|
uncompressedSize,
|
|
localHeaderOffset,
|
|
modifiedAt: dosDateTime(modifiedDate, modifiedTime),
|
|
});
|
|
cursor += recordLength;
|
|
}
|
|
if (entries.length !== entryCount) {
|
|
throw new Error(
|
|
`ZIP entry count mismatch. Expected ${entryCount}, found ${entries.length}.`,
|
|
);
|
|
}
|
|
return {
|
|
path: zipPath,
|
|
size: archive.size,
|
|
entries,
|
|
totalUncompressedBytes: entries.reduce(
|
|
(total, entry) => total + (entry.directory ? 0 : entry.uncompressedSize),
|
|
0,
|
|
),
|
|
};
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
|
|
async function entryDataRange(zipPath, entry) {
|
|
const handle = await open(zipPath, "r");
|
|
try {
|
|
const header = await readExactly(handle, 30, entry.localHeaderOffset);
|
|
if (header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
|
|
throw new Error(`ZIP local header is missing for ${entry.name}.`);
|
|
}
|
|
const nameLength = header.readUInt16LE(26);
|
|
const extraLength = header.readUInt16LE(28);
|
|
const start = entry.localHeaderOffset + 30 + nameLength + extraLength;
|
|
const endExclusive = start + entry.compressedSize;
|
|
const archive = await stat(zipPath);
|
|
if (endExclusive > archive.size) {
|
|
throw new Error(`ZIP entry data points outside the archive: ${entry.name}`);
|
|
}
|
|
return { start, endExclusive };
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
|
|
export function findZipEntry(index, requestedName) {
|
|
const normalized = safeEntryPath(String(requestedName ?? ""));
|
|
return index.entries.find((entry) => entry.name === normalized) ?? null;
|
|
}
|
|
|
|
export async function readZipEntry(zipPath, entry, maxBytes) {
|
|
if (!entry?.supported) {
|
|
throw new Error(`ZIP entry cannot be previewed: ${entry?.name ?? "unknown"}`);
|
|
}
|
|
if (entry.uncompressedSize > maxBytes) {
|
|
throw new Error(
|
|
`ZIP entry is too large for an inline preview (${entry.uncompressedSize.toLocaleString()} bytes).`,
|
|
);
|
|
}
|
|
const { start } = await entryDataRange(zipPath, entry);
|
|
const handle = await open(zipPath, "r");
|
|
let compressed;
|
|
try {
|
|
compressed = await readExactly(handle, entry.compressedSize, start);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
let output;
|
|
if (entry.method === 0) {
|
|
output = compressed;
|
|
} else {
|
|
const cap = entry.uncompressedSize;
|
|
output = await new Promise((resolve, reject) => {
|
|
const inflate = createInflateRaw();
|
|
const chunks = [];
|
|
let total = 0;
|
|
inflate.on("data", (chunk) => {
|
|
total += chunk.length;
|
|
if (total > cap) {
|
|
inflate.destroy(new Error(`ZIP entry decompressed past declared size: ${entry.name}`));
|
|
return;
|
|
}
|
|
chunks.push(chunk);
|
|
});
|
|
inflate.once("end", () => resolve(Buffer.concat(chunks)));
|
|
inflate.once("error", reject);
|
|
inflate.end(compressed);
|
|
});
|
|
}
|
|
if (output.length !== entry.uncompressedSize) {
|
|
throw new Error(`ZIP entry size check failed: ${entry.name}`);
|
|
}
|
|
if (crc32(output) !== entry.crc32) {
|
|
throw new Error(`ZIP entry checksum failed: ${entry.name}`);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function createVerifyTransform(entry) {
|
|
const cap = Math.min(entry.uncompressedSize, MAX_STREAMED_ENTRY_BYTES);
|
|
let total = 0;
|
|
let crcValue = 0xffffffff;
|
|
return new Transform({
|
|
transform(chunk, _encoding, callback) {
|
|
total += chunk.length;
|
|
if (total > cap) {
|
|
callback(new Error(`ZIP entry decompressed past declared size: ${entry.name}`));
|
|
return;
|
|
}
|
|
for (const byte of chunk) {
|
|
crcValue = crcTable[(crcValue ^ byte) & 0xff] ^ (crcValue >>> 8);
|
|
}
|
|
callback(null, chunk);
|
|
},
|
|
flush(callback) {
|
|
if (total !== entry.uncompressedSize) {
|
|
callback(new Error(`ZIP entry size check failed: ${entry.name}`));
|
|
return;
|
|
}
|
|
const actual = (crcValue ^ 0xffffffff) >>> 0;
|
|
if (actual !== entry.crc32) {
|
|
callback(new Error(`ZIP entry checksum failed: ${entry.name}`));
|
|
return;
|
|
}
|
|
callback();
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function streamZipEntry(zipPath, entry, writable, transform = null) {
|
|
if (!entry?.supported) {
|
|
throw new Error(`ZIP entry cannot be served: ${entry?.name ?? "unknown"}`);
|
|
}
|
|
if (entry.uncompressedSize > MAX_STREAMED_ENTRY_BYTES) {
|
|
throw new Error("ZIP entry exceeds the 512 MiB streaming safety limit.");
|
|
}
|
|
if (entry.compressedSize === 0) {
|
|
writable.end();
|
|
return;
|
|
}
|
|
const { start, endExclusive } = await entryDataRange(zipPath, entry);
|
|
const source = createReadStream(zipPath, { start, end: endExclusive - 1 });
|
|
const streams = [source];
|
|
if (entry.method !== 0) streams.push(createInflateRaw());
|
|
streams.push(createVerifyTransform(entry));
|
|
if (transform) streams.push(transform);
|
|
streams.push(writable);
|
|
await pipeline(...streams);
|
|
}
|
|
|
|
export async function readEntryPrefix(zipPath, entry, maxBytes) {
|
|
if (!entry?.supported) return null;
|
|
if (entry.uncompressedSize === 0) return Buffer.alloc(0);
|
|
const { start } = await entryDataRange(zipPath, entry);
|
|
if (entry.method === 0) {
|
|
const readBytes = Math.min(maxBytes, entry.compressedSize);
|
|
const handle = await open(zipPath, "r");
|
|
try {
|
|
return await readExactly(handle, readBytes, start);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
const handle = await open(zipPath, "r");
|
|
let compressed;
|
|
try {
|
|
compressed = await readExactly(handle, entry.compressedSize, start);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const inflate = createInflateRaw();
|
|
const chunks = [];
|
|
let total = 0;
|
|
let done = false;
|
|
inflate.on("data", (chunk) => {
|
|
if (done) return;
|
|
const remaining = maxBytes - total;
|
|
if (remaining <= 0) {
|
|
done = true;
|
|
inflate.destroy();
|
|
resolve(Buffer.concat(chunks));
|
|
return;
|
|
}
|
|
const slice = remaining < chunk.length ? chunk.subarray(0, remaining) : chunk;
|
|
chunks.push(slice);
|
|
total += slice.length;
|
|
if (total >= maxBytes) {
|
|
done = true;
|
|
inflate.destroy();
|
|
resolve(Buffer.concat(chunks));
|
|
}
|
|
});
|
|
inflate.once("end", () => {
|
|
if (!done) {
|
|
done = true;
|
|
resolve(Buffer.concat(chunks));
|
|
}
|
|
});
|
|
inflate.once("error", (err) => {
|
|
if (!done) {
|
|
done = true;
|
|
reject(err);
|
|
}
|
|
});
|
|
inflate.end(compressed);
|
|
});
|
|
}
|