src/ui/hosting-archive.js
Rama main · réplica de lectura del repositorio original.
const MAX_FILES = 128;
const MAX_FILE_BYTES = 2 * 1024 * 1024;
const MAX_ARCHIVE_BYTES = 8 * 1024 * 1024;
const SEGMENT = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*|\.dockerignore)$/u;
const encoder = new TextEncoder();
const crcTable = new Uint32Array(256);
for (let index = 0; index < crcTable.length; index++) {
let crc = index;
for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
crcTable[index] = crc >>> 0;
}
function crc32(bytes) {
let crc = 0xffffffff;
for (const byte of bytes) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
return (crc ^ 0xffffffff) >>> 0;
}
function archiveFile(file) {
if (!file || typeof file.name !== "string" || file.name.length > 128 ||
!file.name.split("/").every((segment) => SEGMENT.test(segment)) ||
typeof file.content !== "string" || file.content.length > MAX_FILE_BYTES) {
throw new TypeError("A hosting archive file has an unsafe path or content.");
}
const name = encoder.encode(file.name);
const content = encoder.encode(file.content);
if (content.length > MAX_FILE_BYTES) throw new RangeError("A hosting archive file exceeds the size limit.");
return { name, content, checksum: crc32(content), offset: 0 };
}
/** Builds a bounded ZIP with stored UTF-8 files and their original relative paths. */
export function createHostingArchive(files) {
if (!Array.isArray(files) || files.length < 1 || files.length > MAX_FILES) {
throw new RangeError("The hosting archive file count is invalid.");
}
const names = new Set();
const entries = [];
let localSize = 0;
let directorySize = 0;
for (const file of files) {
if (names.has(file?.name)) throw new TypeError("The hosting archive has a duplicate path.");
const entry = archiveFile(file);
names.add(file.name);
entry.offset = localSize;
localSize += 30 + entry.name.length + entry.content.length;
directorySize += 46 + entry.name.length;
if (localSize + directorySize + 22 > MAX_ARCHIVE_BYTES) {
throw new RangeError("The hosting archive exceeds the size limit.");
}
entries.push(entry);
}
const totalSize = localSize + directorySize + 22;
const bytes = new Uint8Array(totalSize);
const view = new DataView(bytes.buffer);
let position = 0;
const word = (value) => { view.setUint16(position, value, true); position += 2; };
const dword = (value) => { view.setUint32(position, value, true); position += 4; };
for (const entry of entries) {
dword(0x04034b50); word(20); word(0x0800); word(0);
word(0); word(0x0021); dword(entry.checksum);
dword(entry.content.length); dword(entry.content.length);
word(entry.name.length); word(0);
bytes.set(entry.name, position); position += entry.name.length;
bytes.set(entry.content, position); position += entry.content.length;
}
for (const entry of entries) {
dword(0x02014b50); word(20); word(20); word(0x0800); word(0);
word(0); word(0x0021); dword(entry.checksum);
dword(entry.content.length); dword(entry.content.length);
word(entry.name.length); word(0); word(0); word(0);
word(0); dword(0); dword(entry.offset);
bytes.set(entry.name, position); position += entry.name.length;
}
dword(0x06054b50); word(0); word(0);
word(entries.length); word(entries.length);
dword(directorySize); dword(localSize); word(0);
return bytes;
}