// File archiver class ZipFile { // Instance fields #files; // Active collection of files in the archive ////////////////////////////// Constants ////////////////////////////// // CRC32 lookup table static #CRC_LOOKUP = new Uint32Array(256); static { for (let x = 0; x <= 255; x++) { let l = x; for (let j = 7; j >= 0; j--) l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); this.#CRC_LOOKUP[x] = l; } } ///////////////////////// Initialization Methods ////////////////////////// constructor() { this.#files = new Map(); } ///////////////////////////// Public Methods ////////////////////////////// // Iterator *[Symbol.iterator]() { let names = this.list(); for (let name of names) yield name; } // Add a file to the archive add(filename, data) { if (Array.from(filename).findIndex(c=>c.codePointAt(0) > 126) != -1) throw new Error("Filename must be ASCII."); if (this.#files.has(filename)) throw new Error("File with given name already exists."); this.#files.set(filename, Uint8Array.from(data)); } // Retrieve the file data for a given filename get(filename) { if (!this.#files.has(filename)) throw new Error("No file exists with the given name."); return this.#files.get(filename); } // Retrieve a sorted list of contained filenames list() { return [... this.#files.keys()].sort(); } // Remove a file from the archive remove(filename) { if (!this.#files.has(filename)) throw new Error("No file exists with the given name."); this.#files.delete(filename); } // Produce a Blob representation of the compiled .zip file async toBlob() { let comps = new Map(); let count = this.#files.size; let crc32s = new Map(); let filenames = this.list(); let offsets = new Map(); let output = []; // Preprocessing for (let name of filenames) { let data = this.#files.get(name); comps .set(name, this.#deflate(data)); crc32s.set(name, this.#crc32 (data)); } // Local files for (let name of filenames) { let data = this.#files.get(name); let comp = await comps.get(name); let deflate = comp.length < data.length; comps .set(name, deflate ? comp.length : null); offsets.set(name, output.length); this.#zipHeader(output, name, data.length, comps.get(name), crc32s.get(name)); this.#bytes(output, deflate ? comp : data); } // Central directory let centralOffset = output.length; for (let name of filenames) { this.#zipHeader( output, name, this.#files.get(name).length, comps .get(name), crc32s .get(name), offsets .get(name) ); } let centralSize = output.length - centralOffset; // End of central directory this.#u32(output, 0x06054B50); // Signature this.#u16(output, 0); // This disk number this.#u16(output, 0); // Central start disk number this.#u16(output, count); // Number of items this disk this.#u16(output, count); // Number of items total this.#u32(output, centralSize); // Size of central directory this.#u32(output, centralOffset); // Offset of central directory this.#u16(output, 0); // Comment length return new Blob([Uint8Array.from(output)], {type:"application/zip"}); } ///////////////////////////// Private Methods ///////////////////////////// // Output an array of bytes #bytes(output, x) { for (let b of x) output.push(b); } // Calculate the CRC32 checksum for a byte array #crc32(data) { let c = 0xFFFFFFFF; for (let x = 0; x < data.length; x++) c = ((c >>> 8) ^ ZipFile.#CRC_LOOKUP[(c ^ data[x]) & 0xFF]); return ~c & 0xFFFFFFFF; } // Compress a data buffer via DEFLATE #deflate(data) { return new Response(new Blob([data]).stream() .pipeThrough(new CompressionStream("deflate-raw"))).bytes(); } // Output an ASCII string #string(output, x) { this.#bytes(output, Array.from(x).map(c=>c.codePointAt(0))); } // Output an 8-bit integer #u8(output, x) { output.push(x & 0xFF); } // Output a 16-bit integer #u16(output, x) { this.#u8(output, x); this.#u8(output, x >> 8); } // Output a 32-bit integer #u32(output, x) { this.#u16(output, x); this.#u16(output, x >> 16); } // Output a ZIP header #zipHeader(output, name, dataLength, compLength, crc32, offset = null) { let central = offset != null; let method = compLength == null ? 0 : 8; let signature = central ? 0x02014B50 : 0x04034B50; compLength ??= dataLength; this.#u32(output, signature); // Signature if (central) this.#u16(output, 20); // Version made by this.#u16(output, 20); // Version extracted by this.#u16(output, 0); // General-purpose flags this.#u16(output, method); // Compression method this.#u16(output, 0); // Modified time this.#u16(output, 0); // Modified date this.#u32(output, crc32); // CRC32 checksum this.#u32(output, compLength); // Compressed size this.#u32(output, dataLength); // Uncompressed size this.#u16(output, name.length); // Filename length this.#u16(output, 0); // Extra field length if (central) { this.#u16(output, 0); // File comment length this.#u16(output, 0); // Disk number start this.#u16(output, 0); // Internal file attributes this.#u32(output, 0); // External file attributes this.#u32(output, offset); // Offset of local header } this.#string(output, name); // File name } }; export default ZipFile;