@gitcobra/anzip-es6
Version:
create an uncompressed zip file
579 lines (553 loc) • 22.6 kB
JavaScript
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/*
anzip-es6.js
https://github.com/gitcobra/anzip-es6.js
ZIP File Format Specification
https://pkwaredownloads.blob.core.windows.net/pem/APPNOTE.txt
4.3.6 Overall .ZIP file format:
[local file header 1]
[encryption header 1]
[file data 1]
[data descriptor 1]
.
.
.
[local file header n]
[encryption header n]
[file data n]
[data descriptor n]
[archive decryption header]
[archive extra data record]
[central directory header 1]
.
.
.
[central directory header n]
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]
*/
// header length constants
const LOCAL_HEADER_BASE_LENGTH = 30;
const CENTRAL_HEADER_BASE_LENGTH = 46;
// lfh: signature(4) version(2) flag(2) comp_method(2)
const LocalFileHeaderStart = new Blob([Uint8Array.from([0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x08, 0x00, 0x00])]);
// cdh: signature(4) app version(2) version(2) flag(2) comp_method(2)
const CentralDirHeaderStart = new Blob([Uint8Array.from([0x50, 0x4B, 0x01, 0x02, 0x0A, 0x00, 0x0A, 0x00, 0x00, 0x08, 0x00, 0x00])]);
// cdh parts: comment length(2) disk(2) in-attr(2) ex-attr(4) offset(4)
const CentralDirHeaderRest_File = new Blob([Uint8Array.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])]);
const CentralDirHeaderRest_NotFile = new Blob([Uint8Array.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00])]);
const txtenc = new TextEncoder();
class AnZip {
static getCRC32(dat) {
}
/**
* AnZip constructor
*/
constructor() {
this.clear();
}
/**
* initialize data
* @memberof AnZip
* @param { boolean } [blobMode]
* @return {void}
*/
clear() {
this._dirRecords = new Map();
this._dirRecList = [];
this._dirRecFileList = [];
this._localFileChunks = [];
this._curLocalFileHeaderOffset = 0;
this._centralDirHeaderChunks = [];
this._centralDirHeaderLen = 0;
this._pathCount = 0;
this._fileCount = 0;
this._dataSize = 0;
this._zippedBlob = null;
this._resolvingCRCPromise = null;
}
/**
* add path and data
*/
add(path, data, dateArg, precalculatedCRC) {
if (this._zippedBlob)
throw new Error('the AnZip object was already zipped. create a new instance or execute clear() before adding a file.');
// check path
if (!path)
throw new Error('path is empty');
// replace backslash with forward slash
path = String(path).replace(/\\/g, '/').replace(/^\//, ''); // erase leading slash
// check characters
if (/\/{2,}|\\|^\/|^[a-z]+:/i.test(path))
throw new Error('invalid path. containing a drive letter, a leading slash, or empty directory name: "' + path + '"');
// check file
let dataSize = 0;
let crc;
let storeData = null;
if (typeof data !== 'undefined') {
// file name has to be specified
if (!/[^/]+$/.test(path))
throw new Error('needs a file name: "' + path + '"');
// check for duplication
if (this.has(path))
throw new Error('the path already exists: "' + path + '"');
// check if precalculated CRC exists
if (typeof precalculatedCRC !== 'undefined') {
if (typeof precalculatedCRC !== 'number')
throw new Error('precalculatedCRC must be a number');
crc = precalculatedCRC;
}
if (!(data instanceof Blob)) {
if (data instanceof Uint8Array || typeof Buffer === 'function' && data instanceof Buffer)
storeData = data;
else if (typeof data === 'string') {
// convert string to utf-8 binary
storeData = txtenc.encode(data);
}
else if (data instanceof ArrayBuffer || data instanceof Array) {
storeData = new Uint8Array(data);
}
else {
throw new Error('data must be one of the following types Array, TypedArray, ArrayBuffer, Buffer, string, or Blob.');
}
// calculate CRC synchronously
if (crc === undefined) {
crc = calculateCRC32(storeData);
}
dataSize = storeData.byteLength;
}
else {
storeData = data;
dataSize = data.size;
// calculating CRC is postponed when it is a Blob
if (crc === undefined)
crc = 0;
}
}
// generate time stamp
let date = null;
if (dateArg instanceof Date)
date = dateArg;
else if (typeof dateArg === 'undefined' || dateArg === -1)
date = new Date();
else if (dateArg >= 0)
date = new Date(dateArg);
const dateArray = !date ? [0, 0, 0, 0] : getAs32ArrayLE((date.getFullYear() - 1980) << 25 | (date.getMonth() + 1) << 21 | date.getDate() << 16 | date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() / 2);
// construct directories
let dirs = path.replace(/\/+$/, '').split("/");
let pathstack = '';
let samePartLocalAndCentral;
let lastLocalFileBlock;
while (dirs.length) {
// check whether the path already exists
pathstack += dirs.shift();
const isFile = Boolean(data && (dirs.length === 0));
pathstack += (isFile ? '' : '/');
// already exists
if (this._dirRecords.has(pathstack))
continue;
// create path binary
const pathArray = txtenc.encode(pathstack); // Uint8Array
const pathLength = pathArray.length;
// create data size binary
const dataSizeArray32 = isFile && dataSize ? getAs32ArrayLE(dataSize) : [0, 0, 0, 0];
// it's a new path
const record = {
offset: this._curLocalFileHeaderOffset,
size: dataSize,
path: pathstack,
pathLength,
isFile,
};
this._dirRecords.set(pathstack, record);
this._dirRecList.push(record);
if (isFile)
this._dirRecFileList.push(record);
this._pathCount++;
/*
4.3.7 Local file header:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
file name (variable size)
extra field (variable size)
*/
// *same part as cdh*
// date(4) CRC(4) size(4) size(4) pathLength(2) extraLength(2)
samePartLocalAndCentral = Uint8Array.from([].concat(dateArray, isFile ? getAs32ArrayLE(crc) : [0, 0, 0, 0], dataSizeArray32, dataSizeArray32, [pathLength & 0xFF, pathLength >> 8 & 0xFF, 0x00, 0x00]));
lastLocalFileBlock = [LocalFileHeaderStart, samePartLocalAndCentral, pathArray];
this._localFileChunks.push(lastLocalFileBlock);
/*
4.3.12 Central directory structure:
[central directory header 1]
.
.
.
[central directory header n]
[digital signature]
File header:
central file header signature 4 bytes (0x02014b50)
version made by 2 bytes
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
file comment length 2 bytes
disk number start 2 bytes
internal file attributes 2 bytes
external file attributes 4 bytes
relative offset of local header 4 bytes
file name (variable size)
extra field (variable size)
file comment (variable size)
*/
const centralRest = isFile ? CentralDirHeaderRest_File : CentralDirHeaderRest_NotFile;
const localOffset = Uint8Array.from(getAs32ArrayLE(this._curLocalFileHeaderOffset));
this._centralDirHeaderChunks.push([
CentralDirHeaderStart,
samePartLocalAndCentral,
centralRest,
localOffset,
pathArray
]);
// increase offset
this._centralDirHeaderLen += CENTRAL_HEADER_BASE_LENGTH + pathLength;
this._curLocalFileHeaderOffset += LOCAL_HEADER_BASE_LENGTH + pathLength + (isFile ? dataSize : 0);
}
// add file data
let blobPromise;
if (storeData) {
// asynchronously calculate CRC when it is a Blob
if (precalculatedCRC === undefined) {
if (storeData instanceof Blob) {
blobPromise = this._resolveBlobCRC(storeData, samePartLocalAndCentral, path);
}
}
lastLocalFileBlock.push(storeData);
this._dataSize += dataSize;
this._fileCount++;
}
let pathCount = this._pathCount;
return (blobPromise || Promise.resolve()).then(() => pathCount);
}
_resolveBlobCRC(blob, header, path) {
let resolve;
let reject;
const prevPromise = this._resolvingCRCPromise || Promise.resolve();
const currentPromise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this._resolvingCRCPromise = currentPromise;
// wait until previous promise is fulfilled
let success = false;
prevPromise.finally(() => {
// calculate CRC
getBlobCRC(blob).then(crc => {
// write CRC asynchronously
new DataView(header.buffer).setUint32(4, crc, true);
success = true;
}).catch(e => {
this.remove(path);
throw new Error(`${e.message}\ncould not resolve the blob CRC.`);
}).finally(() => {
if (this._resolvingCRCPromise === currentPromise) {
this._resolvingCRCPromise = null;
}
success ? resolve() : reject();
});
});
return currentPromise;
}
_checkPending() {
//if( this._isPending )
if (this._resolvingCRCPromise)
throw new Error(`the AnZip object is still pending. execute wait() and await the fulfillment of the Promise beforehand.`);
}
_getFileRecord(index) {
let rec;
if (typeof index === 'string') {
rec = this._dirRecords.get(index);
}
else if (typeof index === 'number') {
rec = this._dirRecFileList[index];
}
return rec || null;
}
/**
* return whether the path exists
*/
has(path) {
return !!this._dirRecords.has(path.replace(/\/+$/, ''));
}
size() {
return this._dataSize;
}
count(all) {
return all ? this._pathCount : this._fileCount;
}
get(index, blobType) {
const rec = this._getFileRecord(index);
if (rec && rec.isFile) {
if (this._zippedBlob) {
const { offset, size, pathLength } = rec;
const headerlen = offset + pathLength + LOCAL_HEADER_BASE_LENGTH;
return this._zippedBlob.slice(headerlen, headerlen + size, blobType);
}
else {
const num = this._dirRecList.indexOf(rec);
const dat = this._localFileChunks[num][3];
if (typeof blobType === 'string') {
if (!(dat instanceof Blob) || blobType && dat.type !== blobType)
return new Blob([dat], { type: blobType });
}
return dat;
}
}
return null;
}
getPathByIndex(index) {
var _a;
return ((_a = this._dirRecFileList[index]) === null || _a === void 0 ? void 0 : _a.path) || '';
}
remove(index) {
if (this._zippedBlob)
throw new Error(`could not remove the file. the AnZip object is already zipped. ${index}`);
const rec = this._getFileRecord(index);
if (!rec || !rec.isFile)
return false;
const num = this._dirRecList.indexOf(rec);
// delete chunk from array
this._localFileChunks.splice(num, 1)[0];
this._centralDirHeaderChunks.splice(num, 1)[0];
// decrease offsets
const localChunkSize = LOCAL_HEADER_BASE_LENGTH + rec.pathLength + rec.size;
this._curLocalFileHeaderOffset -= localChunkSize;
const centralChunkSize = CENTRAL_HEADER_BASE_LENGTH + rec.pathLength;
this._centralDirHeaderLen -= centralChunkSize;
// decrease offset values on CentralDirectoryHeader
for (let i = num; i < this._centralDirHeaderChunks.length; i++) {
const offsetU8 = this._centralDirHeaderChunks[i][3];
const dv = new DataView(offsetU8.buffer);
const offset = dv.getUint32(0, true);
// *replace with new Uint8Array instead of editing because it could break snapshoot blobParts in zip() method
//dv.setUint32(0, offset - localChunkSize, true);
this._centralDirHeaderChunks[i][3] = Uint8Array.from(getAs32ArrayLE(offset - localChunkSize));
}
this._pathCount--;
this._fileCount--;
this._dataSize -= rec.size;
this._dirRecords.delete(rec.path);
this._dirRecFileList.splice(this._dirRecFileList.indexOf(rec), 1);
this._dirRecList.splice(this._dirRecList.indexOf(rec), 1);
return true;
}
list(includeDirFlag) {
const list = [];
for (const [key, val] of this._dirRecords) {
if (!includeDirFlag && !val.isFile)
continue;
list.push({
path: val.path,
size: val.size,
isFile: val.isFile,
});
}
return list;
}
buffer() {
return this.zip().then(blob => blobToArrayBuffer(blob));
}
blob() {
return this.zip();
}
/**
* output as dataURL
*/
url() {
return this.zip().then(blob => URL.createObjectURL(blob));
}
urlSync() {
return URL.createObjectURL(this.zipSync());
}
/*
* wait until the last CRC calculation is done
*/
wait(deepWait) {
return __awaiter(this, void 0, void 0, function* () {
while (this._resolvingCRCPromise) {
yield this._resolvingCRCPromise;
if (!deepWait)
break;
}
});
}
/**
* construct a zip structure as a Blob
*/
_buildZipBlob(blobParts) {
return new Blob(blobParts || this._createCurrentBlobParts(), { type: 'application/zip' });
}
_createCurrentBlobParts() {
/*
4.3.16 End of central directory record:
end of central dir signature 4 bytes (0x06054b50)
number of this disk 2 bytes
number of the disk with the
start of the central directory 2 bytes
total number of entries in the
central directory on this disk 2 bytes
total number of entries in
the central directory 2 bytes
size of the central directory 4 bytes
offset of start of central
directory with respect to
the starting disk number 4 bytes
.ZIP file comment length 2 bytes
.ZIP file comment (variable size)
*/
const endOfCentralDirRecord = new Uint8Array([].concat([
// 4: end of central dir signature
0x50, 0x4B, 0x05, 0x06,
// 2: number of this disk
0x00, 0x00,
// 2: number of the disk with the start of the central directory
0x00, 0x00,
// 2: total number of entries in the central directory on this disk
this._pathCount & 0xFF, this._pathCount >> 8,
// 2: total number of entries in the central directory
this._pathCount & 0xFF, this._pathCount >> 8
],
// 4: size of the central directory
getAs32ArrayLE(this._centralDirHeaderLen),
// 4: offset of start of central directory with respect to the starting disk number
getAs32ArrayLE(this._curLocalFileHeaderOffset),
// 2: .ZIP file comment length
[0x00, 0x00]
// variable size: .ZIP file comment ...
));
// join all binary data
//const chain = this._localFiles.concat(this._centralDirHeaders, endOfCentralDirRecord);
let chain = [];
chain = chain.concat(...this._localFileChunks, ...this._centralDirHeaderChunks, endOfCentralDirRecord);
return chain;
}
/**
* output zip as Blob
*/
zip(close) {
return __awaiter(this, void 0, void 0, function* () {
if (this._zippedBlob)
return this._zippedBlob;
// snapshot current binary chain
const blobParts = this._createCurrentBlobParts();
// wait pending crc
yield this._resolvingCRCPromise;
const blob = this._buildZipBlob(blobParts);
if (close)
this._close(blob);
return blob;
});
}
zipSync(close) {
if (this._zippedBlob)
return this._zippedBlob;
this._checkPending();
const blob = this._buildZipBlob(this._createCurrentBlobParts());
if (close)
this._close(blob);
return blob;
}
_close(blob) {
this._zippedBlob = blob;
// clear constructed parts
this._localFileChunks = [];
this._centralDirHeaderChunks = [];
}
}
// create CRC32 table
const CRC32Table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let val = i;
for (let j = 0; j < 8; j++) {
val = (val & 1) ? (0xEDB88320 ^ (val >>> 1)) : (val >>> 1);
}
CRC32Table[i] = val;
}
function calculateCRC32(dat) {
let crc = 0xFFFFFFFF;
for (let i = 0, len = dat.length; i < len; i++) {
crc = CRC32Table[(crc ^ dat[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
function getBlobCRC(blob) {
// calculate CRC
return blobToArrayBuffer(blob).then((buffer) => {
// write CRC asynchronously
return calculateCRC32(new Uint8Array(buffer));
});
}
function blobToArrayBuffer(blob) {
var _a;
// create ArrayBuffer from Blob
return ((_a = blob.arrayBuffer) === null || _a === void 0 ? void 0 : _a.call(blob)) || new Promise((res) => {
let fr = new FileReader();
fr.onload = (ev) => {
res(ev.target.result);
};
fr.onerror = () => {
throw new Error('could not read it as ArrayBuffer');
};
fr.readAsArrayBuffer(blob);
fr = null;
});
}
/**
* 32bit number to little-endian byte array
*/
function getAs32ArrayLE(num) {
return [num & 0xFF, num >> 8 & 0xFF, num >> 16 & 0xFF, num >> 24 & 0xFF];
}
export { AnZip as default };
//# sourceMappingURL=anzip-es6.js.map