From 45fdedd663303f3f9f49fed8ae8cd66accd58bb1 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sat, 20 Jan 2024 16:12:55 -0800 Subject: [PATCH] Move some common type definitions into archive/common.js and some comments. --- archive/common.js | 29 +++++++++++++++++++++++++-- archive/compress.js | 18 ++++------------- archive/events.js | 34 ++++++++++---------------------- archive/unrar.js | 1 + archive/untar.js | 1 + archive/unzip.js | 36 ++++++++++++++-------------------- archive/zip.js | 28 +++++++++----------------- io/bitbuffer.js | 2 ++ io/bytebuffer.js | 2 ++ package.json | 2 +- tests/archive-compress.spec.js | 5 +++-- 11 files changed, 75 insertions(+), 83 deletions(-) diff --git a/archive/common.js b/archive/common.js index 53e2fc3..b589041 100644 --- a/archive/common.js +++ b/archive/common.js @@ -1,14 +1,19 @@ /** * common.js * - * Provides common functionality for compressing and decompressing. + * Provides common definitions or functionality needed by multiple modules. * * Licensed under the MIT License * * Copyright(c) 2023 Google Inc. */ -// Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports. +/** + * @typedef FileInfo An object that is sent to the implementation representing a file to compress. + * @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. + */ /** * @typedef Implementation @@ -51,3 +56,23 @@ export async function getConnectedPort(implFilename) { }); }); } + +// Zip-specific things. + +export const LOCAL_FILE_HEADER_SIG = 0x04034b50; +export const CENTRAL_FILE_HEADER_SIG = 0x02014b50; +export const END_OF_CENTRAL_DIR_SIG = 0x06054b50; +export const CRC32_MAGIC_NUMBER = 0xedb88320; +export const ARCHIVE_EXTRA_DATA_SIG = 0x08064b50; +export const DIGITAL_SIGNATURE_SIG = 0x05054b50; +export const END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50; +export const DATA_DESCRIPTOR_SIG = 0x08074b50; + +/** + * @readonly + * @enum {number} + */ +export const ZipCompressionMethod = { + STORE: 0, // Default. + DEFLATE: 8, // As per http://tools.ietf.org/html/rfc1951. +}; diff --git a/archive/compress.js b/archive/compress.js index 89beb1c..73ef1fe 100644 --- a/archive/compress.js +++ b/archive/compress.js @@ -8,7 +8,7 @@ * Copyright(c) 2023 Google Inc. */ -import { getConnectedPort } from './common.js'; +import { ZipCompressionMethod, getConnectedPort } from './common.js'; // NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK! @@ -19,15 +19,6 @@ import { getConnectedPort } from './common.js'; * @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, @@ -36,17 +27,15 @@ export const ZipCompressionMethod = { /** * 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. + * 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. + * 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. */ /** @@ -91,6 +80,7 @@ export class Zipper { * @private */ this.zipCompressionMethod = options.zipCompressionMethod || ZipCompressionMethod.STORE; + if (this.zipCompressionMethod === ZipCompressionMethod.DEFLATE) throw `DEFLATE not supported.`; /** * @type {CompressStatus} diff --git a/archive/events.js b/archive/events.js index 1ae6bad..8367bc3 100644 --- a/archive/events.js +++ b/archive/events.js @@ -19,9 +19,9 @@ export const UnarchiveEventType = { ERROR: 'error' }; -/** - * An unarchive event. - */ +// TODO: Use CustomEvent and a @template and remove these boilerplate events. + +/** An unarchive event. */ export class UnarchiveEvent extends Event { /** * @param {string} type The event type. @@ -31,9 +31,7 @@ export class UnarchiveEvent extends Event { } } -/** - * Updates all Archiver listeners that an append has occurred. - */ +/** Updates all Unarchiver listeners that an append has occurred. */ export class UnarchiveAppendEvent extends UnarchiveEvent { /** * @param {number} numBytes The number of bytes appended. @@ -49,9 +47,7 @@ export class UnarchiveAppendEvent extends UnarchiveEvent { } } -/** - * Useful for passing info up to the client (for debugging). - */ +/** Useful for passing info up to the client (for debugging). */ export class UnarchiveInfoEvent extends UnarchiveEvent { /** * @param {string} msg The info message. @@ -67,9 +63,7 @@ export class UnarchiveInfoEvent extends UnarchiveEvent { } } -/** - * An unrecoverable error has occured. - */ +/** An unrecoverable error has occured. */ export class UnarchiveErrorEvent extends UnarchiveEvent { /** * @param {string} msg The error message. @@ -85,18 +79,14 @@ export class UnarchiveErrorEvent extends UnarchiveEvent { } } -/** - * Start event. - */ +/** Start event. */ export class UnarchiveStartEvent extends UnarchiveEvent { constructor() { super(UnarchiveEventType.START); } } -/** - * Finish event. - */ +/** Finish event. */ export class UnarchiveFinishEvent extends UnarchiveEvent { /** * @param {Object} metadata A collection fo metadata about the archive file. @@ -108,9 +98,7 @@ export class UnarchiveFinishEvent extends UnarchiveEvent { } // TODO(bitjs): Fully document these. They are confusing. -/** - * Progress event. - */ +/** Progress event. */ export class UnarchiveProgressEvent extends UnarchiveEvent { /** * @param {string} currentFilename @@ -136,9 +124,7 @@ export class UnarchiveProgressEvent extends UnarchiveEvent { } } -/** - * Extract event. - */ +/** Extract event. */ export class UnarchiveExtractEvent extends UnarchiveEvent { /** * @param {UnarchivedFile} unarchivedFile diff --git a/archive/unrar.js b/archive/unrar.js index 51b37b4..2f35ce8 100644 --- a/archive/unrar.js +++ b/archive/unrar.js @@ -28,6 +28,7 @@ let hostPort; // State - consider putting these into a class. let unarchiveState = UnarchiveState.NOT_STARTED; +/** @type {ByteStream} */ let bytestream = null; let allLocalFiles = null; let logToConsole = false; diff --git a/archive/untar.js b/archive/untar.js index 829fe80..51adf64 100644 --- a/archive/untar.js +++ b/archive/untar.js @@ -24,6 +24,7 @@ let hostPort; // State - consider putting these into a class. let unarchiveState = UnarchiveState.NOT_STARTED; +/** @type {ByteStream} */ let bytestream = null; let allLocalFiles = null; let logToConsole = false; diff --git a/archive/unzip.js b/archive/unzip.js index c4b29f6..346077b 100644 --- a/archive/unzip.js +++ b/archive/unzip.js @@ -15,6 +15,9 @@ import { BitStream } from '../io/bitstream.js'; import { ByteBuffer } from '../io/bytebuffer.js'; import { ByteStream } from '../io/bytestream.js'; +import { ARCHIVE_EXTRA_DATA_SIG, CENTRAL_FILE_HEADER_SIG, CRC32_MAGIC_NUMBER, + DATA_DESCRIPTOR_SIG, DIGITAL_SIGNATURE_SIG, END_OF_CENTRAL_DIR_SIG, + LOCAL_FILE_HEADER_SIG } from './common.js'; const UnarchiveState = { NOT_STARTED: 0, @@ -28,6 +31,7 @@ let hostPort; // State - consider putting these into a class. let unarchiveState = UnarchiveState.NOT_STARTED; +/** @type {ByteStream} */ let bytestream = null; let allLocalFiles = null; let logToConsole = false; @@ -60,14 +64,6 @@ const postProgress = function () { }); }; -const zLocalFileHeaderSignature = 0x04034b50; -const zArchiveExtraDataSignature = 0x08064b50; -const zCentralFileHeaderSignature = 0x02014b50; -const zDigitalSignatureSignature = 0x05054b50; -const zEndOfCentralDirSignature = 0x06054b50; -const zEndOfCentralDirLocatorSignature = 0x07064b50; -const zDataDescriptorSignature = 0x08074b50; - // mask for getting the Nth bit (zero-based) const BIT = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, @@ -75,9 +71,7 @@ const BIT = [0x01, 0x02, 0x04, 0x08, 0x1000, 0x2000, 0x4000, 0x8000]; class ZipLocalFile { - /** - * @param {ByteStream} bstream - */ + /** @param {ByteStream} bstream */ constructor(bstream) { if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function () { }) { return null; @@ -125,9 +119,9 @@ class ZipLocalFile { let foundDataDescriptor = false; let numBytesSeeked = 0; while (!foundDataDescriptor) { - while (bstream.peekNumber(4) !== zLocalFileHeaderSignature && - bstream.peekNumber(4) !== zArchiveExtraDataSignature && - bstream.peekNumber(4) !== zCentralFileHeaderSignature) { + while (bstream.peekNumber(4) !== LOCAL_FILE_HEADER_SIG && + bstream.peekNumber(4) !== ARCHIVE_EXTRA_DATA_SIG && + bstream.peekNumber(4) !== CENTRAL_FILE_HEADER_SIG) { numBytesSeeked++; bstream.readBytes(1); } @@ -143,7 +137,7 @@ class ZipLocalFile { // From the PKZIP App Note: "The signature value 0x08074b50 is also used by some ZIP // implementations as a marker for the Data Descriptor record". - if (maybeDescriptorSig === zDataDescriptorSignature) { + if (maybeDescriptorSig === DATA_DESCRIPTOR_SIG) { if (maybeCompressedSize === (numBytesSeeked - 16)) { foundDataDescriptor = true; descriptorSize = 16; @@ -606,7 +600,7 @@ function archiveUnzip() { let bstream = bytestream.tee(); // loop until we don't see any more local files or we find a data descriptor. - while (bstream.peekNumber(4) == zLocalFileHeaderSignature) { + while (bstream.peekNumber(4) == LOCAL_FILE_HEADER_SIG) { // Note that this could throw an error if the bstream overflows, which is caught in the // message handler. const oneLocalFile = new ZipLocalFile(bstream); @@ -636,7 +630,7 @@ function archiveUnzip() { totalFilesInArchive = allLocalFiles.length; // archive extra data record - if (bstream.peekNumber(4) == zArchiveExtraDataSignature) { + if (bstream.peekNumber(4) == ARCHIVE_EXTRA_DATA_SIG) { if (logToConsole) { info(' Found an Archive Extra Data Signature'); } @@ -649,13 +643,13 @@ function archiveUnzip() { // central directory structure // TODO: handle the rest of the structures (Zip64 stuff) - if (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + if (bstream.peekNumber(4) == CENTRAL_FILE_HEADER_SIG) { if (logToConsole) { info(' Found a Central File Header'); } // read all file headers - while (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + while (bstream.peekNumber(4) == CENTRAL_FILE_HEADER_SIG) { bstream.readNumber(4); // signature const cdfh = { versionMadeBy: bstream.readNumber(2), @@ -688,7 +682,7 @@ function archiveUnzip() { } // digital signature - if (bstream.peekNumber(4) == zDigitalSignatureSignature) { + if (bstream.peekNumber(4) == DIGITAL_SIGNATURE_SIG) { if (logToConsole) { info(' Found a Digital Signature'); } @@ -699,7 +693,7 @@ function archiveUnzip() { } let metadata = {}; - if (bstream.peekNumber(4) == zEndOfCentralDirSignature) { + if (bstream.peekNumber(4) == END_OF_CENTRAL_DIR_SIG) { bstream.readNumber(4); // signature const eocds = { numberOfThisDisk: bstream.readNumber(2), diff --git a/archive/zip.js b/archive/zip.js index 930ceb5..5be7038 100644 --- a/archive/zip.js +++ b/archive/zip.js @@ -12,6 +12,10 @@ */ import { ByteBuffer } from '../io/bytebuffer.js'; +import { CENTRAL_FILE_HEADER_SIG, CRC32_MAGIC_NUMBER, END_OF_CENTRAL_DIR_SIG, + LOCAL_FILE_HEADER_SIG } from './common.js'; + +/** @typedef {import('./common.js').FileInfo} FileInfo */ /** @type {MessagePort} */ let hostPort; @@ -28,14 +32,6 @@ let hostPort; * The client should append the bytes to a single buffer in the order they were received. */ -// TODO(bitjs): De-dupe this typedef and the one in compress.js. -/** - * @typedef FileInfo An object that is sent by the client to represent a file. - * @property {string} fileName The name of this 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 raw bytes of the file. - */ - // TODO(bitjs): Figure out where this typedef should live. /** * @typedef CompressFilesMessage A message the client sends to the implementation. @@ -46,12 +42,6 @@ let hostPort; // TODO: Support DEFLATE. // TODO: Support options that can let client choose levels of compression/performance. -// TODO(bitjs): These constants should be defined in a common isomorphic ES module. -const zLocalFileHeaderSignature = 0x04034b50; -const zCentralFileHeaderSignature = 0x02014b50; -const zEndOfCentralDirSignature = 0x06054b50; -const zCRC32MagicNumber = 0xedb88320; // 0xdebb20e3; - /** * @typedef CentralDirectoryFileHeaderInfo An object to be used to construct the central directory. * @property {string} fileName @@ -93,7 +83,7 @@ function createCRC32Table() { for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) { - c = ((c & 1) ? (zCRC32MagicNumber ^ (c >>> 1)) : (c >>> 1)); + c = ((c & 1) ? (CRC32_MAGIC_NUMBER ^ (c >>> 1)) : (c >>> 1)); } table[n] = c; } @@ -157,7 +147,7 @@ function zipOneFile(file) { /** @type {ByteBuffer} */ const buffer = new ByteBuffer(fileHeaderSize + file.fileData.byteLength); - buffer.writeNumber(zLocalFileHeaderSignature, 4); // Magic number. + buffer.writeNumber(LOCAL_FILE_HEADER_SIG, 4); // Magic number. buffer.writeNumber(0x0A, 2); // Version. buffer.writeNumber(0, 2); // General Purpose Flags. buffer.writeNumber(0, 2); // Compression Method. 0 = Store only. @@ -201,7 +191,7 @@ function writeCentralFileDirectory() { const buffer = new ByteBuffer(cdsLength + 22); for (const cdInfo of centralDirectoryInfos) { - buffer.writeNumber(zCentralFileHeaderSignature, 4); // Magic number. + buffer.writeNumber(CENTRAL_FILE_HEADER_SIG, 4); // Magic number. buffer.writeNumber(0, 2); // Version made by. // 0x31e buffer.writeNumber(0, 2); // Version needed to extract (minimum). // 0x14 buffer.writeNumber(0, 2); // General purpose bit flag @@ -222,7 +212,7 @@ function writeCentralFileDirectory() { } // 22 more bytes. - buffer.writeNumber(zEndOfCentralDirSignature, 4); // Magic number. + buffer.writeNumber(END_OF_CENTRAL_DIR_SIG, 4); // Magic number. buffer.writeNumber(0, 2); // Number of this disk. buffer.writeNumber(0, 2); // Disk where central directory starts. buffer.writeNumber(filesCompressed.length, 2); // Number of central directory records on this disk. @@ -287,7 +277,7 @@ export function connect(port) { export function disconnect() { if (!hostPort) { - throw `hostPort was not connected in unzip.js`; + throw `hostPort was not connected in zip.js`; } hostPort = null; diff --git a/io/bitbuffer.js b/io/bitbuffer.js index d445363..d94f64b 100644 --- a/io/bitbuffer.js +++ b/io/bitbuffer.js @@ -65,6 +65,8 @@ export class BitBuffer { this.bitPtr = this.mtl ? 7 : 0; } + // TODO: Be consistent with naming across classes (big-endian and little-endian). + /** @returns {boolean} */ getPackingDirection() { return this.mtl; diff --git a/io/bytebuffer.js b/io/bytebuffer.js index 3533176..0ef3135 100644 --- a/io/bytebuffer.js +++ b/io/bytebuffer.js @@ -9,6 +9,8 @@ * Copyright(c) 2011 antimatter15 */ +// TODO: Allow big-endian and little-endian, with consistent naming. + /** * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. */ diff --git a/package.json b/package.json index 521b2a0..f7dfbdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@codedread/bitjs", - "version": "1.2.1", + "version": "1.2.2", "description": "Binary Tools for JavaScript", "homepage": "https://github.com/codedread/bitjs", "author": "Jeff Schiller", diff --git a/tests/archive-compress.spec.js b/tests/archive-compress.spec.js index f978ed1..e6cbcd8 100644 --- a/tests/archive-compress.spec.js +++ b/tests/archive-compress.spec.js @@ -1,8 +1,9 @@ import * as fs from 'node:fs'; import 'mocha'; import { expect } from 'chai'; -import { Unarchiver, getUnarchiver } from '../archive/decompress.js'; -import { CompressStatus, Zipper, ZipCompressionMethod } from '../archive/compress.js'; +import { getUnarchiver } from '../archive/decompress.js'; +import { CompressStatus, Zipper } from '../archive/compress.js'; +import { ZipCompressionMethod } from '../archive/common.js'; /** * @typedef {import('./archive/compress.js').FileInfo} FileInfo