diff --git a/archive/common.js b/archive/common.js index 4635c66..53e2fc3 100644 --- a/archive/common.js +++ b/archive/common.js @@ -10,6 +10,12 @@ // Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports. +/** + * @typedef Implementation + * @property {MessagePort} hostPort The port the host uses to communicate with the implementation. + * @property {Function} disconnectFn A function to call when the port has been disconnected. + */ + /** * Connects a host to a compress/decompress implementation via MessagePorts. The implementation must * have an exported connect() function that accepts a MessagePort. If the runtime support Workers @@ -17,8 +23,9 @@ * dynamically imports the implementation inside the current JS context (node, bun). * @param {string} implFilename The compressor/decompressor implementation filename relative to this * path (e.g. './unzip.js'). - * @returns {Promise} The Promise resolves to the MessagePort connected to the - * implementation that the host should use. + * @param {Function} disconnectFn A function to run when the port is disconnected. + * @returns {Promise} The Promise resolves to the Implementation, which includes the + * MessagePort connected to the implementation that the host should use. */ export async function getConnectedPort(implFilename) { const messageChannel = new MessageChannel(); @@ -28,13 +35,19 @@ export async function getConnectedPort(implFilename) { if (typeof Worker === 'undefined') { const implModule = await import(`${implFilename}`); await implModule.connect(implPort); - return hostPort; + return { + hostPort, + disconnectFn: () => implModule.disconnect(), + }; } return new Promise((resolve, reject) => { const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href; const worker = new Worker(workerScriptPath, { type: 'module' }); worker.postMessage({ implSrc: implFilename }, [implPort]); - resolve(hostPort); + resolve({ + hostPort, + disconnectFn: () => worker.postMessage({ disconnect: true }), + }); }); } diff --git a/archive/decompress.js b/archive/decompress.js index 4bd0579..c777e88 100644 --- a/archive/decompress.js +++ b/archive/decompress.js @@ -56,6 +56,13 @@ export class Unarchiver extends EventTarget { */ port_; + /** + * A function to call to disconnect the implementation from the host. + * @type {Function} + * @private + */ + disconnectFn_; + /** * @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be * referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent @@ -87,6 +94,16 @@ export class Unarchiver extends EventTarget { this.debugMode_ = !!(options.debug); } + /** + * Overridden so that the type hints for eventType are specific. + * @param {'progress'|'extract'|'finish'} eventType + * @param {EventListenerOrEventListenerObject} listener + * @override + */ + addEventListener(eventType, listener) { + super.addEventListener(eventType, listener); + } + /** * This method must be overridden by the subclass to return the script filename. * @returns {string} The MIME type of the archive. @@ -164,7 +181,9 @@ export class Unarchiver extends EventTarget { * using the update() method. */ async start() { - this.port_ = await getConnectedPort(this.getScriptFileName()); + const impl = await getConnectedPort(this.getScriptFileName()); + this.port_ = impl.hostPort; + this.disconnectFn_ = impl.disconnectFn; return new Promise((resolve, reject) => { this.port_.onerror = (evt) => { console.log('Impl error: message = ' + evt.message); @@ -221,7 +240,9 @@ export class Unarchiver extends EventTarget { stop() { if (this.port_) { this.port_.close(); + this.disconnectFn_(); this.port_ = null; + this.disconnectFn_ = null; } } } diff --git a/archive/unrar.js b/archive/unrar.js index f12c84f..51b37b4 100644 --- a/archive/unrar.js +++ b/archive/unrar.js @@ -1466,8 +1466,28 @@ const onmessage = function (event) { */ export function connect(port) { if (hostPort) { - throw `hostPort already connected`; + throw `hostPort already connected in unrar.js`; } hostPort = port; port.onmessage = onmessage; } + +export function disconnect() { + if (!hostPort) { + throw `hostPort was not connected in unzip.js`; + } + + hostPort = null; + + unarchiveState = UnarchiveState.NOT_STARTED; + bytestream = null; + allLocalFiles = null; + logToConsole = false; + + currentFilename = ''; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; +} diff --git a/archive/untar.js b/archive/untar.js index dea10e7..829fe80 100644 --- a/archive/untar.js +++ b/archive/untar.js @@ -221,8 +221,28 @@ const onmessage = function (event) { */ export function connect(port) { if (hostPort) { - throw `hostPort already connected`; + throw `hostPort already connected in untar.js`; } hostPort = port; port.onmessage = onmessage; } + +export function disconnect() { + if (!hostPort) { + throw `hostPort was not connected in unzip.js`; + } + + hostPort = null; + + unarchiveState = UnarchiveState.NOT_STARTED; + bytestream = null; + allLocalFiles = null; + logToConsole = false; + + currentFilename = ''; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; +} diff --git a/archive/unzip.js b/archive/unzip.js index 3385772..c4b29f6 100644 --- a/archive/unzip.js +++ b/archive/unzip.js @@ -781,8 +781,30 @@ const onmessage = function (event) { */ export function connect(port) { if (hostPort) { - throw `hostPort already connected`; + throw `hostPort already connected in unzip.js`; } + hostPort = port; port.onmessage = onmessage; } + +export function disconnect() { + if (!hostPort) { + throw `hostPort was not connected in unzip.js`; + } + + hostPort = null; + + unarchiveState = UnarchiveState.NOT_STARTED; + bytestream = null; + allLocalFiles = null; + logToConsole = false; + + // Progress variables. + currentFilename = ''; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; +} diff --git a/archive/webworker-wrapper.js b/archive/webworker-wrapper.js index b9763b3..e334bdc 100644 --- a/archive/webworker-wrapper.js +++ b/archive/webworker-wrapper.js @@ -16,6 +16,10 @@ let implPort; onmessage = async (evt) => { - const module = await import(evt.data.implSrc); - module.connect(evt.ports[0]); + if (evt.data.implSrc) { + const module = await import(evt.data.implSrc); + module.connect(evt.ports[0]); + } else if (evt.data.disconnect) { + module.disconnect(); + } }; diff --git a/archive/zip.js b/archive/zip.js index c6e7696..12f64b1 100644 --- a/archive/zip.js +++ b/archive/zip.js @@ -80,7 +80,6 @@ const CompressorState = { FINISHED: 3, }; let state = CompressorState.NOT_STARTED; -let lastFileReceived = false; const crc32Table = createCRC32Table(); /** Helper functions. */ @@ -280,8 +279,21 @@ const onmessage = function(evt) { */ export function connect(port) { if (hostPort) { - throw `hostPort already connected`; + throw `hostPort already connected in zip.js`; } hostPort = port; port.onmessage = onmessage; } + +export function disconnect() { + if (!hostPort) { + throw `hostPort was not connected in unzip.js`; + } + + hostPort = null; + + centralDirectoryInfos = []; + numBytesWritten = 0; + state = CompressorState.NOT_STARTED; + lastFileReceived = false; +} diff --git a/io/bytestream.js b/io/bytestream.js index a18d7b2..d1ae24f 100644 --- a/io/bytestream.js +++ b/io/bytestream.js @@ -22,7 +22,6 @@ export class ByteStream { */ constructor(ab, opt_offset, opt_length) { if (!(ab instanceof ArrayBuffer)) { - console.error(typeof ab); throw 'Error! ByteStream constructed with an invalid ArrayBuffer object'; } diff --git a/tests/archive-testfiles/archive-rar.rar b/tests/archive-testfiles/archive-rar.rar new file mode 100644 index 0000000..4591ae5 Binary files /dev/null and b/tests/archive-testfiles/archive-rar.rar differ diff --git a/tests/archive-testfiles/archive-tar.tar b/tests/archive-testfiles/archive-tar.tar new file mode 100644 index 0000000..4333926 Binary files /dev/null and b/tests/archive-testfiles/archive-tar.tar differ diff --git a/tests/archive-testfiles/archive-zip-faster.zip b/tests/archive-testfiles/archive-zip-faster.zip new file mode 100644 index 0000000..6aa2010 Binary files /dev/null and b/tests/archive-testfiles/archive-zip-faster.zip differ diff --git a/tests/archive-testfiles/archive-zip-smaller.zip b/tests/archive-testfiles/archive-zip-smaller.zip new file mode 100644 index 0000000..e2b656f Binary files /dev/null and b/tests/archive-testfiles/archive-zip-smaller.zip differ diff --git a/tests/archive-testfiles/archive-zip-store.zip b/tests/archive-testfiles/archive-zip-store.zip new file mode 100644 index 0000000..84c1e2f Binary files /dev/null and b/tests/archive-testfiles/archive-zip-store.zip differ diff --git a/tests/archive-testfiles/sample-1.txt b/tests/archive-testfiles/sample-1.txt new file mode 100644 index 0000000..b7bac92 --- /dev/null +++ b/tests/archive-testfiles/sample-1.txt @@ -0,0 +1,20 @@ +This is a sample text file. This text file is large enough to make creating +zip files more interesting, since compression has a chance to work on a larger +sample file that has duplicate words in it. + +This is a sample text file. This text file is large enough to make creating +zip files more interesting, since compression has a chance to work on a larger +sample file that has duplicate words in it. + +This is a sample text file. This text file is large enough to make creating +zip files more interesting, since compression has a chance to work on a larger +sample file that has duplicate words in it. + +This is a sample text file. This text file is large enough to make creating +zip files more interesting, since compression has a chance to work on a larger +sample file that has duplicate words in it. + +This is a sample text file. This text file is large enough to make creating +zip files more interesting, since compression has a chance to work on a larger +sample file that has duplicate words in it. + diff --git a/tests/archive-testfiles/sample-2.csv b/tests/archive-testfiles/sample-2.csv new file mode 100644 index 0000000..dd1a5c9 --- /dev/null +++ b/tests/archive-testfiles/sample-2.csv @@ -0,0 +1,4 @@ +filetype,extension +"text file","txt" +"JSON file","json" +"CSV file","csv" diff --git a/tests/archive-testfiles/sample-3.json b/tests/archive-testfiles/sample-3.json new file mode 100644 index 0000000..2259b5e --- /dev/null +++ b/tests/archive-testfiles/sample-3.json @@ -0,0 +1,6 @@ +{ + "file formats": ["csv", "json", "txt"], + "tv shows": { + "it's": ["monty", "python's", "flying", "circus"] + } +} diff --git a/tests/decompress.spec.js b/tests/decompress.spec.js new file mode 100644 index 0000000..4a0c936 --- /dev/null +++ b/tests/decompress.spec.js @@ -0,0 +1,57 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; + +import { Unarchiver, Unrarrer, Untarrer, Unzipper, getUnarchiver } from '../archive/decompress.js'; + +const PATH = `tests/archive-testfiles/`; + +const INPUT_FILES = [ + 'sample-1.txt', + 'sample-2.csv', + 'sample-3.json', +]; + +const ARCHIVE_FILES = [ + 'archive-rar.rar', + 'archive-tar.tar', + 'archive-zip-store.zip', + 'archive-zip-faster.zip', + 'archive-zip-smaller.zip', +]; + +describe('bitjs.archive.decompress', () => { + /** @type {Map} */ + let inputArrayBuffers = new Map(); + + before(() => { + for (const inputFile of INPUT_FILES) { + const nodeBuf = fs.readFileSync(`${PATH}${inputFile}`); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + inputArrayBuffers.set(inputFile, ab); + } + }); + + for (const outFile of ARCHIVE_FILES) { + it(outFile, (done) => { + const bufs = new Map(inputArrayBuffers); + const nodeBuf = fs.readFileSync(`${PATH}${outFile}`); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + let unarchiver = getUnarchiver(ab); + expect(unarchiver instanceof Unarchiver).equals(true); + + unarchiver.addEventListener('extract', evt => { + const {filename, fileData} = evt.unarchivedFile; + expect(bufs.has(filename)).equals(true); + const ab = bufs.get(filename); + expect(fileData.byteLength).equals(ab.byteLength); + for (let b = 0; b < fileData.byteLength; ++b) { + expect(fileData[b] === ab[b]); + } + // Remove the value from the map so that it is only used once. + bufs.delete(filename); + }); + unarchiver.start().then(() => { done() }); + }); + } +});