203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
// 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;
|