UNPKG

simplezip.js

Version:

Lightweight uncompressed browser-compatible ZIP generation

266 lines (265 loc) 11.7 kB
"use strict"; function isNullOrUndefined(obj) { return obj === null || obj === undefined; } var SimpleBuffer = /** @class */ (function () { function SimpleBuffer(initialSize) { this.array = new Uint8Array(initialSize); this.index = 0; } SimpleBuffer.prototype.ensureRemaining = function (size) { var newCapacity = this.index + size; if (newCapacity > this.array.byteLength) { console.log("Reallocate Zip From", this.array.byteLength, "to", newCapacity); var newBuf = new Uint8Array(newCapacity); newBuf.set(this.array.slice(0, this.index), 0); delete this.array; this.array = newBuf; } }; SimpleBuffer.prototype.getArrayBuffer = function () { 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); }; SimpleBuffer.prototype.getCapacity = function () { return this.array.byteLength; }; SimpleBuffer.prototype.getIndex = function () { return this.index; }; SimpleBuffer.prototype.writeStr = function (v) { for (var i = 0; i < v.length; i++) { this.array[this.index++] = v.charCodeAt(i) & 0xFF; } }; ; SimpleBuffer.prototype.append = function (v) { this.array.set(v, this.index); this.index += v.length; }; SimpleBuffer.prototype.writeu8 = function (v) { this.array[this.index++] = v & 0xFF; }; SimpleBuffer.prototype.writeu16 = function (v) { this.array[this.index++] = v & 0xFF; this.array[this.index++] = (v >> 8) & 0xFF; }; SimpleBuffer.prototype.writeu32 = function (v) { 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; }; SimpleBuffer.prototype.writeu64 = function (v) { this.writeu32(v[1]); this.writeu32(v[0]); }; return SimpleBuffer; }()); var NTFS_EPOCH = new Date("01-Jan-1601 00:00:00 UTC"); var BYTES_PER_RECORD = 46 + 36; // 36 for extra NTFS timestamp field var SimpleZip = /** @class */ (function () { function SimpleZip(expectedMaxSize) { this.files = []; this.ntfsTimeSinceEpoch = []; // How much space at the end for the CentralDiRectory is required this.cdrAccumulate = 0; this.buf = new SimpleBuffer(expectedMaxSize); // Compute timestamp var now = new Date(); var hour = now.getHours(); var minute = now.getMinutes(); var second = Math.floor(now.getSeconds() / 2); var year = now.getFullYear() - 1980; var month = now.getMonth() + 1; var 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 var 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 var upper = Math.floor(ntfsMilliseconds / 429496.7296); // =/1000/429.4967296 // Truncate upper bits before multiplying to ensure we stay within the realm of an int var lower = ((ntfsMilliseconds & 0xFFFFFFFF) * 10000) & 0xFFFFFFFF; this.ntfsTimeSinceEpoch.push(upper); this.ntfsTimeSinceEpoch.push(lower); } SimpleZip.prototype.ensureRemainingSpace = function (remaining) { this.buf.ensureRemaining(remaining); }; SimpleZip.prototype.appendFile = function (file) { this.appendFiles([file]); }; SimpleZip.prototype.appendFiles = function (files) { var zips = []; var headerAccumulate = 0; // Parse all the files, work out how much space is required to store them for (var _i = 0, files_1 = files; _i < files_1.length; _i++) { var file = files_1[_i]; 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"); } var data = void 0; if (typeof file.data === 'string') { data = new Uint8Array(file.data.length); for (var 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); } var zEntry = { 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 (var _a = 0, zips_1 = zips; _a < zips_1.length; _a++) { var entry = zips_1[_a]; var zEntry = entry.zEntry; var 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); } }; SimpleZip.prototype.generate = function () { var centralDirStart = this.buf.getIndex(); var requiredRemaining = 22; for (var _i = 0, _a = this.files; _i < _a.length; _i++) { var file = _a[_i]; requiredRemaining += BYTES_PER_RECORD + file.name.length; } this.buf.ensureRemaining(requiredRemaining); // Write central directory file headers for (var _b = 0, _c = this.files; _b < _c.length; _b++) { var file = _c[_b]; 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 } var 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(); }; SimpleZip.GenerateZipFrom = function (files) { var requiredSize = 22; //EOCD for (var _i = 0, files_2 = files; _i < files_2.length; _i++) { var file = files_2[_i]; // 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; } var zip = new SimpleZip(requiredSize); zip.appendFiles(files); return zip.generate(); }; SimpleZip.calcCRC32 = function (data) { SimpleZip.genCRCTable(); if (SimpleZip.crcTable !== null) { var crc = 0 ^ (-1); for (var i = 0; i < data.length; i++) { crc = (crc >>> 8) ^ SimpleZip.crcTable[(crc ^ data[i]) & 0xFF]; } return (crc ^ (-1)) >>> 0; } return -1; }; SimpleZip.genCRCTable = function () { if (SimpleZip.crcTable === null) { SimpleZip.crcTable = Array(256); var c = void 0; for (var 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; } } }; SimpleZip.crcTable = null; return SimpleZip; }()); module.exports = SimpleZip;