UNPKG

simplezip.js

Version:

Lightweight uncompressed browser-compatible ZIP generation

303 lines (259 loc) 11.5 kB
type ZipFile = {name:string,data:ArrayBuffer|Uint8Array|string}; type ZipEntry = {name:string,size:number,crc:number,offset:number}; function isNullOrUndefined(obj:any) { return obj === null || obj === undefined; } class SimpleBuffer { private array : Uint8Array; private index : number; constructor(initialSize:number) { this.array = new Uint8Array(initialSize); this.index = 0; } ensureRemaining(size:number) { let newCapacity = this.index+size; if(newCapacity > this.array.byteLength) { console.log("Reallocate Zip From", this.array.byteLength,"to",newCapacity); let newBuf = new Uint8Array(newCapacity); newBuf.set(this.array.slice(0, this.index), 0); delete this.array; this.array = newBuf; } } getArrayBuffer() { if(this.array.buffer.byteLength === this.index) { return this.array.buffer; } // Not ideal but necessary. How to slice without copying the data? I don't know... // Not without NodeJS buffers return this.array.buffer.slice(0, this.index); } getCapacity() { return this.array.byteLength; } getIndex() { return this.index; } writeStr(v:string) { for(var i = 0; i < v.length; i++){ this.array[this.index++] = v.charCodeAt(i) & 0xFF; } }; append(v:Uint8Array) { this.array.set(v, this.index); this.index += v.length; } writeu8(v:number) { this.array[this.index++] = v&0xFF; } writeu16(v:number) { this.array[this.index++] = v&0xFF; this.array[this.index++] = (v>>8)&0xFF; } writeu32(v:number) { this.array[this.index++] = v&0xFF; this.array[this.index++] = (v>>8)&0xFF; this.array[this.index++] = (v>>16)&0xFF; this.array[this.index++] = (v>>24)&0xFF; } writeu64(v:Array<number>) { this.writeu32(v[1]) this.writeu32(v[0]) } } const NTFS_EPOCH = new Date("01-Jan-1601 00:00:00 UTC"); const BYTES_PER_RECORD = 46 + 36; // 36 for extra NTFS timestamp field class SimpleZip { private buf : SimpleBuffer; private files : Array<ZipEntry> = []; private createdTime : number; private createdDate : number; private ntfsTimeSinceEpoch : Array<number> = []; private static crcTable : Array<number>|null = null; // How much space at the end for the CentralDiRectory is required private cdrAccumulate : number = 0; constructor(expectedMaxSize:number) { this.buf = new SimpleBuffer(expectedMaxSize); // Compute timestamps const now = new Date(); let hour = now.getHours(); let minute = now.getMinutes(); let second = Math.floor(now.getSeconds() / 2); let year = now.getFullYear() - 1980; let month = now.getMonth() + 1; let day = now.getDate() // https://www.mindprod.com/jgloss/zip.html this.createdTime = ((hour & 0x1F) << 11) | ((minute & 0x3F) << 5) | (second & 0x1F); this.createdDate = ((year & 0x7F) << 9) | ((month & 0xF) << 5) | (day & 0x1F); // https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld // NTFS timestap is 10ths of useconds since epoch 01-Jan-1601 // (2^32/1e-7) = every 429.4967296 seconds, the 'upper' 32-bits should increase by 1 let ntfsMilliseconds = (now.getTime() - NTFS_EPOCH.getTime()); // Precision of 1.0E-07 seconds, ms is 1.0E-03, so we must scale by 1.0E+04 // Max safe int in JS is 2^53, that's only 21 upper bits // 429 * 2^21 / 3600 / 24 / 365 = ~ can only represent 28.6 years after epoch with JS integer // need to carefully handle the separation of the time into two 32-bit integers // This seems to work well let upper = Math.floor(ntfsMilliseconds / 429496.7296); // =/1000/429.4967296 // Truncate upper bits before multiplying to ensure we stay within the realm of an int let lower = ((ntfsMilliseconds & 0xFFFFFFFF) * 10000) & 0xFFFFFFFF; this.ntfsTimeSinceEpoch.push(upper); this.ntfsTimeSinceEpoch.push(lower); } ensureRemainingSpace(remaining:number) { this.buf.ensureRemaining(remaining); } appendFile(file:ZipFile) { this.appendFiles([file]); } appendFiles(files:Array<ZipFile>) { let zips:Array<{zEntry:ZipEntry,data:Uint8Array}> = []; let headerAccumulate = 0; // Parse all the files, work out how much space is required to store them for(let file of files) { if(isNullOrUndefined(file) || isNullOrUndefined(file.name) || isNullOrUndefined(file.data)) { throw new Error("Zip file entry missing 'name' or 'data' props"); } if(typeof file.name !== 'string' || file.name.length <= 0) { throw new Error("File name must be a string with >0 characters"); } let data : Uint8Array; if(typeof file.data === 'string') { data = new Uint8Array(file.data.length); for(let i = 0; i < file.data.length; i++) { data[i] = file.data.charCodeAt(i); } } else if(file.data instanceof Uint8Array) { data = file.data; } else if(file.data instanceof ArrayBuffer) { data = new Uint8Array(file.data); } else { throw new Error("Unknown typeof data: " + typeof file.data); } let zEntry:ZipEntry = { name: file.name, size: data.byteLength, crc: SimpleZip.calcCRC32(data), offset: -1 }; zips.push({ zEntry: zEntry, data: data }); headerAccumulate += 30+zEntry.name.length+data.byteLength; this.cdrAccumulate += 46 + file.name.length; } // +22 for EOCD this.buf.ensureRemaining(headerAccumulate + this.cdrAccumulate + 22); for(let entry of zips) { let zEntry = entry.zEntry; let data = entry.data; zEntry.offset = this.buf.getIndex(); this.buf.writeu32(0x04034b50); //Header sig this.buf.writeu16(0x000a); //Version this.buf.writeu16(0x0000); //Bit Flag this.buf.writeu16(0x0000); //Compression Method(none) this.buf.writeu16(this.createdTime); //Last mod Time this.buf.writeu16(this.createdDate); //Last mod Date this.buf.writeu32(zEntry.crc); //CRC32 this.buf.writeu32(zEntry.size); //Compressed Size this.buf.writeu32(zEntry.size); //Uncompressed Size this.buf.writeu16(zEntry.name.length); //Filename Length this.buf.writeu16(0); //Extra Field Length this.buf.writeStr(zEntry.name); // Name // no extra field this.buf.append(data); // Uncompressed Data this.files.push(zEntry); } } generate() { let centralDirStart = this.buf.getIndex(); let requiredRemaining = 22; for(let file of this.files) { requiredRemaining += BYTES_PER_RECORD + file.name.length; } this.buf.ensureRemaining(requiredRemaining); // Write central directory file headers for (let file of this.files) { this.buf.writeu32(0x02014b50); //Header sig this.buf.writeu16(0x003f); //Version made by Windows this.buf.writeu16(0x000a); //Version needed this.buf.writeu16(0x0000); //Bit Flag this.buf.writeu16(0x0000); //Compression Method(none) this.buf.writeu16(this.createdTime); //Last mod Time this.buf.writeu16(this.createdDate); //Last mod Date this.buf.writeu32(file.crc); //CRC32 this.buf.writeu32(file.size); //Compressed Size this.buf.writeu32(file.size); //Uncompressed Size this.buf.writeu16(file.name.length); //Filename Length this.buf.writeu16(0x0024); //Extra Field Length this.buf.writeu16(0x0000); //File Comment Length this.buf.writeu16(0x0000); //Disk no. this.buf.writeu16(0x0000); //Internal Attributes this.buf.writeu32(0x00000020); //External Attributes this.buf.writeu32(file.offset); //Relative Offset this.buf.writeStr(file.name); //File Name // Write more accurate timestamp, using NTFS extra field this.buf.writeu16(0x000a) // NTFS timestamp this.buf.writeu16(0x0020) // 32 bytes of data this.buf.writeu32(0x00000000) // reserved this.buf.writeu16(0x0001) // Tag for attribute #1 this.buf.writeu16(0x0018) // Size of attribute 1 (24 bytes) this.buf.writeu64(this.ntfsTimeSinceEpoch) // last modified this.buf.writeu64(this.ntfsTimeSinceEpoch) // last accessed this.buf.writeu64(this.ntfsTimeSinceEpoch) // last created // No comment } let centralDirEnd = this.buf.getIndex(); // Write EOCD (22 Bytes) this.buf.writeu32(0x06054b50); //Header sig this.buf.writeu16(0x0000); //Header sig this.buf.writeu16(0x0000); //Header sig this.buf.writeu16(this.files.length); //Header sig this.buf.writeu16(this.files.length); //Header sig this.buf.writeu32(centralDirEnd - centralDirStart); this.buf.writeu32(centralDirStart); this.buf.writeu16(0x00); return this.buf.getArrayBuffer(); } public static GenerateZipFrom(files:Array<ZipFile>) { let requiredSize = 22; //EOCD for(let file of files) { // 1xData, 2xName, 1xCentral Directory File Header and 1xLocal File Header if(typeof file.data === "string") { requiredSize += file.data.length; } else { requiredSize += file.data.byteLength; } requiredSize += file.name.length*2 + BYTES_PER_RECORD + 30; } let zip = new SimpleZip(requiredSize); zip.appendFiles(files); return zip.generate(); } private static calcCRC32(data:Uint8Array) { SimpleZip.genCRCTable(); if(SimpleZip.crcTable !== null) { let crc = 0 ^ (-1); for (let i = 0; i < data.length; i++ ) { crc = (crc >>> 8) ^ SimpleZip.crcTable[(crc ^ data[i]) & 0xFF]; } return (crc ^ (-1)) >>> 0; } return -1; } private static genCRCTable() { if(SimpleZip.crcTable === null) { SimpleZip.crcTable = Array(256); let c; for(let n = 0; n < 256; n++){ c = n; for(var k =0; k < 8; k++){ c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); } SimpleZip.crcTable[n] = c; } } } } export = SimpleZip;