Establish application infrastructure
This commit is contained in:
+3926
File diff suppressed because it is too large
Load Diff
+202
@@ -0,0 +1,202 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user