shrooms-vb-web/util/ZipFile.js

203 lines
6.3 KiB
JavaScript
Raw Normal View History

2025-02-18 22:39:36 +00:00
// 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;