1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 09:39:16 +02:00
bitjs/archive/compress.js

183 lines
No EOL
5.1 KiB
JavaScript

/**
* compress.js
*
* Provides base functionality for compressing.
*
* Licensed under the MIT License
*
* Copyright(c) 2023 Google Inc.
*/
import { getConnectedPort } from './common.js';
// NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
/**
* @typedef FileInfo An object that is sent to the implementation to represent a file to zip.
* @property {string} fileName The name of the file. TODO: Includes the path?
* @property {number} lastModTime The number of ms since the Unix epoch (1970-01-01 at midnight).
* @property {Uint8Array} fileData The bytes of the file.
*/
/**
* @readonly
* @enum {number}
*/
export const ZipCompressionMethod = {
STORE: 0, // Default.
// DEFLATE: 8,
};
// export const DeflateCompressionMethod = {
// NO_COMPRESSION: 0,
// COMPRESSION_FIXED_HUFFMAN: 1,
// COMPRESSION_DYNAMIC_HUFFMAN: 2,
// }
/**
* Data elements are packed into bytes in order of increasing bit number within the byte,
i.e., starting with the least-significant bit of the byte.
* Data elements other than Huffman codes are packed starting with the least-significant bit of the
data element.
* Huffman codes are packed starting with the most-significant bit of the code.
*/
/**
* @typedef CompressorOptions
* @property {ZipCompressionMethod} zipCompressionMethod
* @property {DeflateCompressionMethod=} deflateCompressionMethod Only present if
* zipCompressionMethod is set to DEFLATE.
*/
/**
* @readonly
* @enum {string}
*/
export const CompressStatus = {
NOT_STARTED: 'not_started',
READY: 'ready',
WORKING: 'working',
COMPLETE: 'complete',
ERROR: 'error',
};
/**
* A thing that zips files.
* NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
* TODO: Make a streaming / event-driven API.
*/
export class Zipper {
/**
* The client-side port that sends messages to, and receives messages from the
* decompressor implementation.
* @type {MessagePort}
* @private
*/
port_;
/**
* A function to call to disconnect the implementation from the host.
* @type {Function}
* @private
*/
disconnectFn_;
/**
* @param {CompressorOptions} options
*/
constructor(options) {
/**
* @type {ZipCompressionMethod}
* @private
*/
this.zipCompressionMethod = options.zipCompressionMethod || ZipCompressionMethod.STORE;
/**
* @type {CompressStatus}
* @private
*/
this.compressState = CompressStatus.NOT_STARTED;
/**
* @type {Uint8Array}
* @private
*/
this.byteArray = new Uint8Array(0);
}
/**
* Must only be called on a Zipper that has been started. See start().
* @param {FileInfo[]} files
* @param {boolean} isLastFile
*/
appendFiles(files, isLastFile) {
if (!this.port_) {
throw `Port not initialized. Did you forget to call start() ?`;
}
if (![CompressStatus.READY, CompressStatus.WORKING].includes(this.compressState)) {
throw `Zipper not in the right state: ${this.compressState}`;
}
this.port_.postMessage({ files, isLastFile });
}
/**
* Send in a set of files to be compressed. Set isLastFile to true if no more files are to added
* at some future state. The Promise will not resolve until isLastFile is set to true either in
* this method or in appendFiles().
* @param {FileInfo[]} files
* @param {boolean} isLastFile
* @returns {Promise<Uint8Array>} A Promise that will contain the entire zipped archive as an array
* of bytes.
*/
async start(files, isLastFile) {
const impl = await getConnectedPort('./zip.js');
this.port_ = impl.hostPort;
this.disconnectFn_ = impl.disconnectFn;
return new Promise((resolve, reject) => {
this.port_.onerror = (evt) => {
console.log('Impl error: message = ' + evt.message);
reject(evt.message);
};
this.port_.onmessage = (evt) => {
if (typeof evt.data == 'string') {
// Just log any strings the implementation pumps our way.
console.log(evt.data);
} else {
switch (evt.data.type) {
case 'start':
this.compressState = CompressStatus.WORKING;
break;
case 'finish':
this.compressState = CompressStatus.COMPLETE;
this.port_.close();
this.disconnectFn_();
this.port_ = null;
this.disconnectFn_ = null;
resolve(this.byteArray);
break;
case 'compress':
this.addBytes_(evt.data.bytes);
break;
}
}
};
this.compressState = CompressStatus.READY;
this.appendFiles(files, isLastFile);
});
}
/**
* Updates the internal byte array with new bytes (by allocating a new array and copying).
* @param {Uint8Array} newBytes
* @private
*/
addBytes_(newBytes) {
const oldArray = this.byteArray;
this.byteArray = new Uint8Array(oldArray.byteLength + newBytes.byteLength);
this.byteArray.set(oldArray);
this.byteArray.set(newBytes, oldArray.byteLength);
}
}