diff options
Diffstat (limited to 'public/bluebey-studio/src/zip.js')
| -rw-r--r-- | public/bluebey-studio/src/zip.js | 218 |
1 files changed, 0 insertions, 218 deletions
diff --git a/public/bluebey-studio/src/zip.js b/public/bluebey-studio/src/zip.js deleted file mode 100644 index 21a1955..0000000 --- a/public/bluebey-studio/src/zip.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Store-only ZIP writer. - * - * The studio exports a whole batch of images at once (one PNG per comic panel - * or sticker), and a single download is far nicer to handle than a dozen files, - * so the batch is packed into one archive here. - * - * PNG payloads are already DEFLATE-compressed, so running deflate over them a - * second time costs time and saves nothing measurable: every entry is stored - * verbatim (compression method 0, `compressedSize === uncompressedSize`) and no - * data descriptor is needed. - * - * The module has no imports, never touches the DOM beyond `Blob`, and stamps - * entries with a fixed DOS date unless one is passed in, so the same input - * always produces a byte-for-byte identical archive. That keeps the output safe - * to cache, diff, hash and test. - * - * Archive layout (every integer little-endian, no extra fields, no comments, no - * directory entries): - * - * [local file header + stored data] one per file - * [central directory entry] one per file - * [end of central directory record] - */ - -/** Signature of a local file header ("PK\x03\x04"). */ -const LOCAL_SIGNATURE = 0x04034b50; -/** Signature of a central directory entry ("PK\x01\x02"). */ -const CENTRAL_SIGNATURE = 0x02014b50; -/** Signature of the end of central directory record ("PK\x05\x06"). */ -const EOCD_SIGNATURE = 0x06054b50; - -/** ZIP 2.0 is the oldest version that covers everything this writer emits. */ -const VERSION_NEEDED = 20; -/** General purpose bit 11: the entry name is UTF-8, not CP437. */ -const FLAG_UTF8 = 0x0800; -/** Compression method 0: stored. */ -const METHOD_STORE = 0; - -/** Largest value a 32-bit ZIP field can hold. Nothing may reach 4 GiB. */ -const MAX_FIELD = 0xffffffff; -/** Largest value a 16-bit ZIP field can hold (entry count, name length). */ -const MAX_SHORT = 0xffff; - -const LOCAL_HEADER_SIZE = 30; -const CENTRAL_HEADER_SIZE = 46; -const EOCD_SIZE = 22; - -/** 1980-01-01 00:00 in DOS form: year offset 0, month 1, day 1, midnight. */ -const DEFAULT_TIME = 0; -const DEFAULT_DATE = (1 << 5) | 1; - -/** - * CRC-32 (polynomial 0xEDB88320, reflected), the checksum every ZIP entry must - * carry. The table is built once at module load; eight table-driven bits per - * byte is fast enough that a several-megabyte PNG batch stays imperceptible. - */ -const CRC_TABLE = (() => { - const table = new Uint32Array(256); - for (let i = 0; i < 256; i++) { - let c = i; - for (let bit = 0; bit < 8; bit++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; - table[i] = c >>> 0; - } - return table; -})(); - -/** - * @param {Uint8Array} bytes - * @returns {number} unsigned 32-bit CRC-32 - */ -export function crc32(bytes) { - let c = 0xffffffff; - for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8); - return (c ^ 0xffffffff) >>> 0; -} - -/** Clip a Date into the DOS date/time pair stored in the headers. */ -function toDosDateTime(date) { - const year = date.getFullYear(); - // The DOS epoch starts in 1980; anything older is clamped to the epoch. - if (year < 1980) return { time: DEFAULT_TIME, date: DEFAULT_DATE }; - const time = (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1); - const day = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(); - return { time: time & MAX_SHORT, date: day & MAX_SHORT }; -} - -/** - * Entry names live inside the archive, where the separator is always "/": a - * backslash (what Windows paths use) would be taken as part of the name, and a - * leading slash would look like an absolute path to some extractors. - */ -function normalizeName(name) { - return String(name ?? '') - .replace(/\\/g, '/') - .replace(/^\/+/, ''); -} - -/** Accept any byte source, plus plain strings meaning UTF-8 text. */ -function toBytes(data, encoder) { - if (typeof data === 'string') return encoder.encode(data); - if (data instanceof Uint8Array) return data; - if (ArrayBuffer.isView(data)) { - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - } - if (data instanceof ArrayBuffer) return new Uint8Array(data); - throw new TypeError('ZIP: file data must be a Uint8Array, an ArrayBuffer or a string'); -} - -function writeU16(view, offset, value) { - view.setUint16(offset, value & MAX_SHORT, true); -} - -function writeU32(view, offset, value) { - view.setUint32(offset, value >>> 0, true); -} - -/** - * Pack `files` into a stored (uncompressed) ZIP archive. - * - * @param {Array<{name: string, data: Uint8Array | string}>} files - * a string `data` is encoded as UTF-8 text - * @param {object} [options] - * @param {Date} [options.date] - * timestamp for every entry; defaults to 1980-01-01 00:00 so that the same - * input always yields the same bytes - * @returns {Blob} an `application/zip` blob - */ -export function createZip(files, { date } = {}) { - const encoder = new TextEncoder(); - const stamp = date ? toDosDateTime(date) : { time: DEFAULT_TIME, date: DEFAULT_DATE }; - - // First pass: normalise the input and measure it, so the output buffer can be - // allocated exactly once instead of being grown and copied. - const entries = []; - let localTotal = 0; - let centralTotal = 0; - - for (const file of files ?? []) { - const nameBytes = encoder.encode(normalizeName(file.name)); - const data = toBytes(file.data, encoder); - - if (data.length > MAX_FIELD) { - throw new Error('ZIP: a file is 4 GiB or larger; zip64 is not supported'); - } - if (nameBytes.length > MAX_SHORT) { - throw new Error('ZIP: a file name is longer than 65535 bytes'); - } - - const length = LOCAL_HEADER_SIZE + nameBytes.length + data.length; - localTotal += length; - centralTotal += CENTRAL_HEADER_SIZE + nameBytes.length; - entries.push({ nameBytes, data, crc: crc32(data), length, offset: 0 }); - } - - if (entries.length > MAX_SHORT) { - throw new Error('ZIP: more than 65535 files; zip64 is not supported'); - } - if (localTotal + centralTotal + EOCD_SIZE > MAX_FIELD) { - throw new Error('ZIP: the archive is 4 GiB or larger; zip64 is not supported'); - } - - const bytes = new Uint8Array(localTotal + centralTotal + EOCD_SIZE); - const view = new DataView(bytes.buffer); - - let offset = 0; - for (const entry of entries) { - entry.offset = offset; - writeU32(view, offset + 0, LOCAL_SIGNATURE); - writeU16(view, offset + 4, VERSION_NEEDED); - writeU16(view, offset + 6, FLAG_UTF8); - writeU16(view, offset + 8, METHOD_STORE); - writeU16(view, offset + 10, stamp.time); - writeU16(view, offset + 12, stamp.date); - writeU32(view, offset + 14, entry.crc); - writeU32(view, offset + 18, entry.data.length); - writeU32(view, offset + 22, entry.data.length); - writeU16(view, offset + 26, entry.nameBytes.length); - writeU16(view, offset + 28, 0); // extra field length - bytes.set(entry.nameBytes, offset + LOCAL_HEADER_SIZE); - bytes.set(entry.data, offset + LOCAL_HEADER_SIZE + entry.nameBytes.length); - offset += entry.length; - } - - const centralOffset = offset; - for (const entry of entries) { - writeU32(view, offset + 0, CENTRAL_SIGNATURE); - writeU16(view, offset + 4, VERSION_NEEDED); // version made by (host 0 = MS-DOS) - writeU16(view, offset + 6, VERSION_NEEDED); - writeU16(view, offset + 8, FLAG_UTF8); - writeU16(view, offset + 10, METHOD_STORE); - writeU16(view, offset + 12, stamp.time); - writeU16(view, offset + 14, stamp.date); - writeU32(view, offset + 16, entry.crc); - writeU32(view, offset + 20, entry.data.length); - writeU32(view, offset + 24, entry.data.length); - writeU16(view, offset + 28, entry.nameBytes.length); - writeU16(view, offset + 30, 0); // extra field length - writeU16(view, offset + 32, 0); // file comment length - writeU16(view, offset + 34, 0); // disk number start - writeU16(view, offset + 36, 0); // internal attributes - writeU32(view, offset + 38, 0); // external attributes - writeU32(view, offset + 42, entry.offset); - bytes.set(entry.nameBytes, offset + CENTRAL_HEADER_SIZE); - offset += CENTRAL_HEADER_SIZE + entry.nameBytes.length; - } - - writeU32(view, offset + 0, EOCD_SIGNATURE); - writeU16(view, offset + 4, 0); // number of this disk - writeU16(view, offset + 6, 0); // disk holding the central directory - writeU16(view, offset + 8, entries.length); - writeU16(view, offset + 10, entries.length); - writeU32(view, offset + 12, offset - centralOffset); - writeU32(view, offset + 16, centralOffset); - writeU16(view, offset + 20, 0); // archive comment length - - return new Blob([bytes], { type: 'application/zip' }); -} |
