diff --git a/.c8rc b/.c8rc index 60ed579..c1becbc 100644 --- a/.c8rc +++ b/.c8rc @@ -2,11 +2,15 @@ "check-coverage": true, "all": true, "include": [ + "archive/*.js", "codecs/*.js", + "image/parsers/*.js", "file/*.js", "io/*.js" ], "exclude": [ + "archive/archive.js", + "archive/webworker-wrapper.js", "io/*-worker.js" ], "reporter": [ diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5f93857..decf3c2 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,13 +16,13 @@ jobs: strategy: matrix: - node-version: [17.x, 18.x, 19.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + node-version: [19.x, 20.x, 21.x] + # See NodeJS release schedule at https://nodejs.org/en/about/previous-releases. steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - name: Use NodeJS ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci diff --git a/.gitignore b/.gitignore index a128d44..3077f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ +.DS_Store coverage node_modules -tests/archive.spec.notready.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af74450 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,145 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.2.6] - 2026-03-18 + +### Added + +- codecs: Add support for ffprobe's "extradata" + +## [1.2.5] - 2025-10-05 + +### Fixed + +- archive: [Issue #52](https://github.com/codedread/bitjs/issues/47) Fixed a race condition in Zipper. + +## [1.2.4] - 2024-12-08 + +### Fixed + +- Fixed import error in unrar/rarvm. + +## [1.2.3] - 2024-02-04 + +### Added + +- archive: Support semantic methods for subscribing to unarchive events (onExtract), + [Issue #47](https://github.com/codedread/bitjs/issues/47). +- archive: Added Gunzipper to decompress gzip files. Only supported on runtimes that supported + DecompressionStream('gzip') for now. [Issue #48](https://github.com/codedread/bitjs/issues/48). +- io: Added a getData() method to ByteBuffer to retrieve a copy of the bytes that have been written. + +### Changed + +- archive: Unrarrer throws an explicit error when encountering RAR5 files. +- io: ByteBuffer.insertXXX() now throws an error if trying to write past the end of the buffer. + +## [1.2.2] - 2024-01-26 + +### Added + +- archive: Support DEFLATE in Zipper where JS implementations support it in CompressionStream. + [Issue #40](https://github.com/codedread/bitjs/issues/40) +- archive: Support DEFLATE in Unzipper where JS implementations support it in DecompressionStream. + [Issue #38](https://github.com/codedread/bitjs/issues/38) +- file: Added detection of GZIP files. +- io: Added a skip() method to BitStream to match ByteStream. + +### Fixed + +- Fixed a benign JS error in the Web Worker wrapper + +## [1.2.1] - 2024-01-19 + +### Added + +- image: Added PNG event-based parser (all critical and most ancillary chunks). + +### Changed + +- io: Fix ByteStream bug where skip(0) did not return the ByteStream. + +### Removed + +- image: Removed all custom parser Events and just use CustomEvent. + +## [1.2.0] - 2024-01-15 + +### Added + +- image: Added GIF and JPEG event-based parsers. +- io: Added a skip() method to ByteStream. + +## [1.1.7] - 2023-12-16 + +### Changed + +- archive: Enable unarchiving/archiving in NodeJS. +- Update unit test coverage and documentation. + +## [1.1.6] - 2023-10-25 + +### Changed + +- codecs: Special handling for mp3 streams inside mp4 containers. +- codecs: Handle ffprobe level -99 in mp4 files. + +## [1.1.5] - 2023-10-22 + +### Changed + +- codecs: Add support for HE-AAC profile in mp4a. + +## [1.1.4] - 2023-10-19 + +### Changed + +- codecs: Add support for DTS audio codec and AV1 video codec. +- codecs: Update how Matroska video/audio files are detected (video/x-matroska). + [Issue #43](https://github.com/codedread/bitjs/issues/43) +- untar: Fix long path/filenames in 'ustar' format. [Issue #42](https://github.com/codedread/bitjs/issues/43) + +## [1.1.3] - 2023-10-15 + +### Changed + +- codecs: Add support for WAV files to getShortMIMEString() and getFullMIMEString(). +- codecs: Fix support for AVI files in getFullMIMEString(). + +## [1.1.2] - 2023-09-30 + +### Changed + +- codecs: Handle m4a files as audio/mp4. + +## [1.1.1] - 2023-06-21 + +### Changed + +- Fix missing RarVM import in unrar.js. + +## [1.1.0] - 2023-05-28 + +### Added + +- Starter thinking around a Media API. + +### Changed + +- Change console.warn to a console.error when importing archive.js. + +### Removed + +- Removed build step for bitjs.io now that all browsers (Firefox 114+) support ES Module Workers. + +## [1.0.11] - 2023-02-15 + +### Added + +- Add sniffer support for the ICO format. +- Add unit test coverage via c8. + +### Fixed + +- Fixes for the audio/flac codec type. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..391c1e7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +codedread at gmail dot com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index c3cc9e9..e4bb0b8 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,35 @@ ## Introduction -A set of dependency-free JavaScript modules to handle binary data in JS (using Typed Arrays). Includes: +A set of dependency-free JavaScript modules to work with binary data in JS (using +[Typed Arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)). +Includes: - * bitjs/archive: Unarchiving files (unzip, unrar, untar) in the browser, implemented as Web Workers and allowing progressively unarchiving while streaming. - * bitjs/codecs: Get the codec info of media containers in a ISO RFC6381 MIME type string + * bitjs/archive: Decompressing files (unzip, unrar, untar, gunzip) in JavaScript, implemented as + Web Workers where supported, and allowing progressive unarchiving while streaming. + * bitjs/codecs: Get the codec info of media containers in a ISO RFC6381 MIME type string. * bitjs/file: Detect the type of file from its binary signature. - * bitjs/image: Conversion of WebP images to PNG or JPEG. - * bitjs/io: Low-level classes for interpreting binary data (BitStream, ByteStream). For example, reading or peeking at N bits at a time. + * bitjs/image: Parsing GIF, JPEG, PNG. Conversion of WebP to PNG or JPEG. + * bitjs/io: Low-level classes for interpreting binary data (BitStream, ByteStream). For example, + reading or peeking at N bits at a time. ## Installation -Install it using your favourite package manager, the package is registered under `@codedread/bitjs`. +Install it using your favourite package manager, the package is registered under `@codedread/bitjs`. ```bash -$ npm install @codedread/bitjs +npm install @codedread/bitjs ``` or ```bash -$ yarn add @codedread/bitjs +yarn add @codedread/bitjs ``` -### Using in Node +### CommonJS/ESM in Node -This module is an ES Module, which should work as expected in other projects using ES Modules. However, -if you are using a project that uses CommonJs modules, it's a little tricker to use. One valid example -of this is if a TypeScript project compiles to CommonJS, it will try to turn imports into require() -statements, which will break. The fix for this (unfortunately) is to update your tsconfig.json: +This module is an ES Module. If your project uses CommonJS modules, it's a little trickier to use. +One example of this is if a TypeScript project compiles to CommonJS, it will try to turn imports +into require() statements, which will break. The fix for this (unfortunately) is to update your +tsconfig.json: ```json "moduleResolution": "Node16", @@ -44,75 +48,24 @@ const { getFullMIMEString } = await import('@codedread/bitjs'); ### bitjs.archive -This package includes objects for unarchiving binary data in popular archive formats (zip, rar, tar) providing unzip, unrar and untar capabilities via JavaScript in the browser. A prototype version of a compressor that creates Zip files is also present. The decompression/compression actually happens inside a Web Worker. +This package includes objects for decompressing and compressing binary data in popular archive +formats (zip, rar, tar, gzip). Here is a simple example of unrar: #### Decompressing ```javascript -import { Unzipper } from './bitjs/archive/decompress.js'; -const unzipper = new Unzipper(zipFileArrayBuffer); -unzipper.addEventListener('progress', updateProgress); -unzipper.addEventListener('extract', receiveOneFile); -unzipper.addEventListener('finish', displayZipContents); -unzipper.start(); - -function updateProgress(e) { - // e.currentFilename is the file currently being unarchived/scanned. - // e.totalCompressedBytesRead has how many bytes have been unzipped so far -} - -function receiveOneFile(e) { - // e.unarchivedFile.filename: string - // e.unarchivedFile.fileData: Uint8Array -} - -function displayZipContents() { - // Now sort your received files and show them or whatever... -} -``` - -The unarchivers also support progressively decoding while streaming the file, if you are receiving the zipped file from a slow place (a Cloud API, for instance). For example: - -```javascript -import { Unzipper } from './bitjs/archive/decompress.js'; -const unzipper = new Unzipper(anArrayBufferWithStartingBytes); -unzipper.addEventListener('progress', updateProgress); -unzipper.addEventListener('extract', receiveOneFile); -unzipper.addEventListener('finish', displayZipContents); -unzipper.start(); -... -// after some time -unzipper.update(anArrayBufferWithMoreBytes); -... -// after some more time -unzipper.update(anArrayBufferWithYetMoreBytes); +import { Unrarrer } from './bitjs/archive/decompress.js'; +const unrar = new Unrarrer(rarFileArrayBuffer); +unrar.addEventListener('extract', (e) => { + const {filename, fileData} = e.unarchivedFile; + console.log(`Extracted ${filename} (${fileData.byteLength} bytes)`); + // Do something with fileData... +}); +unrar.addEventListener('finish', () => console.log('Done')); +unrar.start(); ``` -#### Compressing - -The Zipper only supports creating zip files without compression (story only) for now. The interface -is pretty straightforward and there is no event-based / streaming API. - -```javascript -import { Zipper } from './bitjs/archive/compress.js'; -const zipper = new Zipper(); -const now = Date.now(); -// Zip files foo.jpg and bar.txt. -const zippedArrayBuffer = await zipper.start( - [ - { - fileName: 'foo.jpg', - lastModTime: now, - fileData: fooArrayBuffer, - }, - { - fileName: 'bar.txt', - lastModTime: now, - fileData: barArrayBuffer, - } - ], - true /* isLastFile */); -``` +More details and examples are located on [the API page](./docs/bitjs.archive.md). ### bitjs.codecs @@ -142,7 +95,8 @@ exec(cmd, (error, stdout) => { ### bitjs.file -This package includes code for dealing with files. It includes a sniffer which detects the type of file, given an ArrayBuffer. +This package includes code for dealing with files. It includes a sniffer which detects the type of +file, given an ArrayBuffer. ```javascript import { findMimeType } from './bitjs/file/sniffer.js'; @@ -151,8 +105,52 @@ const mimeType = findMimeType(someArrayBuffer); ### bitjs.image -This package includes code for dealing with binary images. It includes a module for converting WebP images into alternative raster graphics formats (PNG/JPG). +This package includes code for dealing with image files. It includes low-level, event-based +parsers for GIF, JPEG, and PNG images. +It also includes a module for converting WebP images into +alternative raster graphics formats (PNG/JPG), though this latter module is deprecated, now that +WebP images are well-supported in all browsers. + +#### GIF Parser +```javascript +import { GifParser } from './bitjs/image/parsers/gif.js' + +const parser = new GifParser(someArrayBuffer); +parser.onApplicationExtension(evt => { + const appId = evt.detail.applicationIdentifier; + const appAuthCode = new TextDecoder().decode(evt.detail.applicationAuthenticationCode); + if (appId === 'XMP Data' && appAuthCode === 'XMP') { + /** @type {Uint8Array} */ + const appData = evt.detail.applicationData; + // Do something with appData (parse the XMP). + } +}); +parser.start(); +``` + +#### JPEG Parser +```javascript +import { JpegParser } from './bitjs/image/parsers/jpeg.js' +import { ExifTagNumber } from './bitjs/image/parsers/exif.js'; + +const parser = new JpegParser(someArrayBuffer) + .onApp1Exif(evt => console.log(evt.detail.get(ExifTagNumber.IMAGE_DESCRIPTION).stringValue)); +await parser.start(); +``` + +#### PNG Parser +```javascript +import { PngParser } from './bitjs/image/parsers/png.js' +import { ExifTagNumber } from './bitjs/image/parsers/exif.js'; + +const parser = new PngParser(someArrayBuffer); + .onExifProfile(evt => console.log(evt.detail.get(ExifTagNumber.IMAGE_DESCRIPTION).stringValue)) + .onTextualData(evt => console.dir(evt.detail)); +await parser.start(); +``` + +#### WebP Converter ```javascript import { convertWebPtoPNG, convertWebPtoJPG } from './bitjs/image/webp-shim/webp-shim.js'; // convertWebPtoPNG() takes in an ArrayBuffer containing the bytes of a WebP @@ -166,19 +164,26 @@ convertWebPtoPNG(webpBuffer).then(pngBuf => { ### bitjs.io -This package includes stream objects for reading and writing binary data at the bit and byte level: BitStream, ByteStream. +This package includes stream objects for reading and writing binary data at the bit and byte level: +BitStream, ByteStream. ```javascript import { BitStream } from './bitjs/io/bitstream.js'; -const bstream = new BitStream(someArrayBuffer, true, offset, length); -const crc = bstream.readBits(12); // read in 12 bits as CRC, advancing the pointer -const flagbits = bstream.peekBits(6); // look ahead at next 6 bits, but do not advance the pointer +const bstream = new BitStream(someArrayBuffer, true /** most-significant-bit-to-least */ ); +const crc = bstream.readBits(12); // Read in 12 bits as CRC. Advance pointer. +const flagbits = bstream.peekBits(6); // Look ahead at next 6 bits. Do not advance pointer. ``` +More details and examples are located on [the API page](./docs/bitjs.io.md). + ## Reference -* [UnRar](http://codedread.github.io/bitjs/docs/unrar.html): A work-in-progress description of the RAR file format. +* [UnRar](http://codedread.github.io/bitjs/docs/unrar.html): A work-in-progress description of the +RAR file format. ## History -This project grew out of another project of mine, [kthoom](https://github.com/codedread/kthoom) (a comic book reader implemented in the browser). This repository was automatically exported from [my original repository on GoogleCode](https://code.google.com/p/bitjs) and has undergone considerable changes and improvements since then, including adding streaming support, starter RarVM support, tests, many bug fixes, and updating the code to ES6. +This project grew out of another project of mine, [kthoom](https://github.com/codedread/kthoom) (a +comic book reader implemented in the browser). This repository was automatically exported from +[my original repository on GoogleCode](https://code.google.com/p/bitjs) and has undergone +considerable changes and improvements since then. diff --git a/archive/archive.js b/archive/archive.js index b6552bc..7cfa3c7 100644 --- a/archive/archive.js +++ b/archive/archive.js @@ -9,11 +9,12 @@ * Copyright(c) 2011 Google Inc. */ +// TODO(2.0): When up-revving to a major new version, remove this module. + import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType, UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent, - UnarchiveProgressEvent, UnarchiveStartEvent, Unarchiver, - UnrarrerInternal, UntarrerInternal, UnzipperInternal, - getUnarchiverInternal } from './decompress-internal.js'; + UnarchiveProgressEvent, UnarchiveStartEvent } from './events.js'; +import { Unarchiver, Unzipper, Unrarrer, Untarrer, getUnarchiver } from './decompress.js'; export { UnarchiveAppendEvent, @@ -26,65 +27,7 @@ export { UnarchiveProgressEvent, UnarchiveStartEvent, Unarchiver, + Unzipper, Unrarrer, Untarrer, getUnarchiver } -/** - * All extracted files returned by an Unarchiver will implement - * the following interface: - */ - -/** - * @typedef UnarchivedFile - * @property {string} filename - * @property {Uint8Array} fileData - */ - -/** - * The goal is to make this testable - send getUnarchiver() an array buffer of - * an archive, call start on the unarchiver, expect the returned result. - * - * Problem: It relies on Web Workers, and that won't work in a nodejs context. - * Solution: Make archive.js very thin, have it feed web-specific things into - * an internal module that is isomorphic JavaScript. - * - * TODO: - * - write unit tests for archive-internal.js that use the nodejs Worker - * equivalent. - * - maybe use @pgriess/node-webworker or @audreyt/node-webworker-threads or - * just node's worker_threads ? - */ - -const createWorkerFn = (scriptFilename) => new Worker(scriptFilename); - -function warn() { - console.warn(`Stop using archive.js and use decompress.js instead. This module will be removed.`); -} - -// Thin wrappers of unarchivers for clients who want to construct a specific -// unarchiver themselves rather than use getUnarchiver(). -export class Unzipper extends UnzipperInternal { - constructor(ab, options) { warn(); super(ab, createWorkerFn, options); } -} - -export class Unrarrer extends UnrarrerInternal { - constructor(ab, options) { warn(); super(ab, createWorkerFn, options); } -} - -export class Untarrer extends UntarrerInternal { - constructor(ab, options) { warn(); super(ab, createWorkerFn, options); } -} - -/** - * Factory method that creates an unarchiver based on the byte signature found - * in the arrayBuffer. - * @param {ArrayBuffer} ab The ArrayBuffer to unarchive. Note that this ArrayBuffer - * must not be referenced after calling this method, as the ArrayBuffer is marked - * as Transferable and sent to a Worker thread once start() is called. - * @param {Object|string} options An optional object of options, or a string - * representing where the path to the unarchiver script files. - * @returns {Unarchiver} - */ -export function getUnarchiver(ab, options = {}) { - warn(); - return getUnarchiverInternal(ab, createWorkerFn, options); -} +console.error(`bitjs: Stop importing archive.js, this module will be removed. Import decompress.js instead.`); diff --git a/archive/common.js b/archive/common.js new file mode 100644 index 0000000..b589041 --- /dev/null +++ b/archive/common.js @@ -0,0 +1,78 @@ +/** + * common.js + * + * Provides common definitions or functionality needed by multiple modules. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +/** + * @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 + * @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 + * (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it + * 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'). + * @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(); + const hostPort = messageChannel.port1; + const implPort = messageChannel.port2; + + if (typeof Worker === 'undefined') { + const implModule = await import(`${implFilename}`); + await implModule.connect(implPort); + 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, + disconnectFn: () => worker.postMessage({ disconnect: true }), + }); + }); +} + +// 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 ab73397..42ba89d 100644 --- a/archive/compress.js +++ b/archive/compress.js @@ -1,42 +1,39 @@ +/** + * compress.js + * + * Provides base functionality for compressing. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +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! +// TODO(2.0): Remove this comment. +// NOTE: THIS IS A WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK! /** - * @typedef FileInfo An object that is sent to the worker to represent a file to zip. + * @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 {ArrayBuffer} fileData The bytes of the file. + * @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, -// } +/** The number of milliseconds to periodically send any pending files to the Worker. */ +const FLUSH_TIMER_MS = 50; /** * 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 {string} pathToBitJS A string indicating where the BitJS files are located. * @property {ZipCompressionMethod} zipCompressionMethod - * @property {DeflateCompressionMethod=} deflateCompressionMethod Only present if - * zipCompressionMethod is set to DEFLATE. */ /** @@ -51,47 +48,89 @@ export const CompressStatus = { ERROR: 'error', }; +// TODO: Extend EventTarget and introduce subscribe methods (onProgress, onInsert, onFinish, etc). + /** * 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. + * NOTE: THIS IS A WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK! + * TODO(2.0): Add semantic onXXX methods for an event-driven API. */ export class Zipper { /** - * @param {CompressorOptions} options + * @type {Uint8Array} + * @private */ - constructor(options) { - /** - * The path to the BitJS files. - * @type {string} - * @private - */ - this.pathToBitJS = options.pathToBitJS || '/'; + byteArray = new Uint8Array(0); - /** - * @type {ZipCompressionMethod} - * @private - */ - this.zipCompressionMethod = options.zipCompressionMethod || ZipCompressionMethod.STORE; + /** + * The overall state of the Zipper. + * @type {CompressStatus} + * @private + */ + compressStatus_ = CompressStatus.NOT_STARTED; + // Naming of this property preserved for compatibility with 1.2.4-. + get compressState() { return this.compressStatus_; } - /** - * Private web worker initialized during start(). - * @type {Worker} - * @private - */ - this.worker_ = null; + /** + * The client-side port that sends messages to, and receives messages from the + * decompressor implementation. + * @type {MessagePort} + * @private + */ + port_; - /** - * @type {CompressStatus} - * @private - */ - this.compressState = CompressStatus.NOT_STARTED; + /** + * A function to call to disconnect the implementation from the host. + * @type {Function} + * @private + */ + disconnectFn_; + /** + * A timer that periodically flushes pending files to the Worker. Set upon start() and stopped + * upon the last file being compressed by the Worker. + * @type {Number} + * @private + */ + flushTimer_ = 0; + + /** + * Whether the last files have been added by the client. + * @type {boolean} + * @private + */ + lastFilesReceived_ = false; + + /** + * The pending files to be sent to the Worker. + * @type {FileInfo[]} + * @private + */ + pendingFilesToSend_ = []; + + /** + * @param {CompressorOptions} options + */ + constructor(options) { /** - * @type {Uint8Array} + * @type {CompressorOptions} * @private */ - this.byteArray = new Uint8Array(0); + this.zipOptions = options; + this.zipCompressionMethod = options.zipCompressionMethod || ZipCompressionMethod.STORE; + if (!Object.values(ZipCompressionMethod).includes(this.zipCompressionMethod)) { + throw `Compression method ${this.zipCompressionMethod} not supported`; + } + + if (this.zipCompressionMethod === ZipCompressionMethod.DEFLATE) { + // As per https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream, NodeJS only + // supports deflate-raw from 21.2.0+ (Nov 2023). https://nodejs.org/en/blog/release/v21.2.0. + try { + new CompressionStream('deflate-raw'); + } catch (err) { + throw `CompressionStream with deflate-raw not supported by JS runtime: ${err}`; + } + } } /** @@ -99,46 +138,76 @@ export class Zipper { * @param {FileInfo[]} files * @param {boolean} isLastFile */ - appendFiles(files, isLastFile) { - if (!this.worker_) { - throw `Worker 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}`; + appendFiles(files, isLastFile = false) { + if (this.compressStatus_ === CompressStatus.NOT_STARTED) { + throw `appendFiles() called, but Zipper not started.`; } + if (this.lastFilesReceived_) throw `appendFiles() called, but last file already received.`; - this.worker_.postMessage({ files, isLastFile }); + this.lastFilesReceived_ = isLastFile; + this.pendingFilesToSend_.push(...files); } /** - * 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(). + * Send in a set of files to be compressed. Set isLastFile to true if no more files are to be + * added in the future. The return Promise will not resolve until isLastFile is set to true either + * in this method or in an appendFiles() call. * @param {FileInfo[]} files * @param {boolean} isLastFile - * @returns {Promise} A Promise that will contain the entire zipped archive as an array - * of bytes. + * @returns {Promise} A Promise that will resolve once the final file has been sent. + * The Promise resolves to an array of bytes of the entire zipped archive. */ - start(files, isLastFile) { + async start(files = [], isLastFile = false) { + if (this.compressStatus_ !== CompressStatus.NOT_STARTED) { + throw `start() called, but Zipper already started.`; + } + + // We optimize for the case where isLastFile=true in a start() call by posting to the Worker + // immediately upon async resolving below. Otherwise, we push these files into the pending set + // and rely on the flush timer to send them into the Worker. + if (!isLastFile) { + this.pendingFilesToSend_.push(...files); + this.flushTimer_ = setInterval(() => this.flushAnyPendingFiles_(), FLUSH_TIMER_MS); + } + this.compressStatus_ = CompressStatus.READY; + this.lastFilesReceived_ = isLastFile; + + // After this point, the function goes async, so appendFiles() may run before anything else in + // this function. + const impl = await getConnectedPort('./zip.js'); + this.port_ = impl.hostPort; + this.disconnectFn_ = impl.disconnectFn; return new Promise((resolve, reject) => { - this.worker_ = new Worker(this.pathToBitJS + `archive/zip.js`); - this.worker_.onerror = (evt) => { - console.log('Worker error: message = ' + evt.message); - throw evt.message; + this.port_.onerror = (evt) => { + console.log('Impl error: message = ' + evt.message); + reject(evt.message); }; - this.worker_.onmessage = (evt) => { + + this.port_.onmessage = (evt) => { if (typeof evt.data == 'string') { - // Just log any strings the worker pumps our way. + // Just log any strings the implementation pumps our way. console.log(evt.data); } else { switch (evt.data.type) { + // Message sent back upon the first message the Worker receives, which may or may not + // have sent any files for compression, e.g. start([]). case 'start': - this.compressState = CompressStatus.WORKING; + this.compressStatus_ = CompressStatus.WORKING; break; + // Message sent back when the last file has been compressed by the Worker. case 'finish': - this.compressState = CompressStatus.COMPLETE; + if (this.flushTimer_) { + clearInterval(this.flushTimer_); + this.flushTimer_ = 0; + } + this.compressStatus_ = CompressStatus.COMPLETE; + this.port_.close(); + this.disconnectFn_(); + this.port_ = null; + this.disconnectFn_ = null; resolve(this.byteArray); break; + // Message sent back when the Worker has written some bytes to the zip file. case 'compress': this.addBytes_(evt.data.bytes); break; @@ -146,8 +215,10 @@ export class Zipper { } }; - this.compressState = CompressStatus.READY; - this.appendFiles(files, isLastFile); + // See note above about optimizing for the start(files, true) case. + if (isLastFile) { + this.port_.postMessage({ files, isLastFile, compressionMethod: this.zipCompressionMethod }); + } }); } @@ -162,4 +233,27 @@ export class Zipper { this.byteArray.set(oldArray); this.byteArray.set(newBytes, oldArray.byteLength); } -} \ No newline at end of file + + /** + * Called internally by the async machinery to send any pending files to the Worker. This method + * sends at most one message to the Worker. + * @private + */ + flushAnyPendingFiles_() { + if (this.compressStatus_ === CompressStatus.NOT_STARTED) { + throw `flushAppendFiles_() called but Zipper not started.`; + } + // If the port is not initialized or we have no pending files, just return immediately and + // try again on the next flush. + if (!this.port_ || this.pendingFilesToSend_.length === 0) return; + + // Send all files to the worker. If we have received the last file, then let the Worker know. + this.port_.postMessage({ + files: this.pendingFilesToSend_, + isLastFile: this.lastFilesReceived_, + compressionMethod: this.zipCompressionMethod, + }); + // Release the memory from the browser's main thread. + this.pendingFilesToSend_ = []; + } +} diff --git a/archive/decompress-internal.js b/archive/decompress-internal.js deleted file mode 100644 index f8422c0..0000000 --- a/archive/decompress-internal.js +++ /dev/null @@ -1,411 +0,0 @@ -/** - * decompress-internal.js - * - * Provides base functionality for unarchiving, extracted here as an internal - * module for unit testing. Import decompress.js instead. - * - * Licensed under the MIT License - * - * Copyright(c) 2021 Google Inc. - */ - -import { findMimeType } from '../file/sniffer.js'; - -/** - * @typedef UnarchivedFile - * @property {string} filename - * @property {Uint8Array} fileData - */ - -/** - * The UnarchiveEvent types. - */ -export const UnarchiveEventType = { - START: 'start', - APPEND: 'append', - PROGRESS: 'progress', - EXTRACT: 'extract', - FINISH: 'finish', - INFO: 'info', - ERROR: 'error' -}; - -/** - * An unarchive event. - */ - export class UnarchiveEvent extends Event { - /** - * @param {string} type The event type. - */ - constructor(type) { - super(type); - } -} - -/** - * Updates all Archiver listeners that an append has occurred. - */ - export class UnarchiveAppendEvent extends UnarchiveEvent { - /** - * @param {number} numBytes The number of bytes appended. - */ - constructor(numBytes) { - super(UnarchiveEventType.APPEND); - - /** - * The number of appended bytes. - * @type {number} - */ - this.numBytes = numBytes; - } -} - -/** - * Useful for passing info up to the client (for debugging). - */ -export class UnarchiveInfoEvent extends UnarchiveEvent { - /** - * @param {string} msg The info message. - */ - constructor(msg) { - super(UnarchiveEventType.INFO); - - /** - * The information message. - * @type {string} - */ - this.msg = msg; - } -} - -/** - * An unrecoverable error has occured. - */ -export class UnarchiveErrorEvent extends UnarchiveEvent { - /** - * @param {string} msg The error message. - */ - constructor(msg) { - super(UnarchiveEventType.ERROR); - - /** - * The information message. - * @type {string} - */ - this.msg = msg; - } -} - -/** - * Start event. - */ -export class UnarchiveStartEvent extends UnarchiveEvent { - constructor() { - super(UnarchiveEventType.START); - } -} - -/** - * Finish event. - */ -export class UnarchiveFinishEvent extends UnarchiveEvent { - /** - * @param {Object} metadata A collection fo metadata about the archive file. - */ - constructor(metadata = {}) { - super(UnarchiveEventType.FINISH); - this.metadata = metadata; - } -} - -/** - * Progress event. - */ -export class UnarchiveProgressEvent extends UnarchiveEvent { - /** - * @param {string} currentFilename - * @param {number} currentFileNumber - * @param {number} currentBytesUnarchivedInFile - * @param {number} currentBytesUnarchived - * @param {number} totalUncompressedBytesInArchive - * @param {number} totalFilesInArchive - * @param {number} totalCompressedBytesRead - */ - constructor(currentFilename, currentFileNumber, currentBytesUnarchivedInFile, - currentBytesUnarchived, totalUncompressedBytesInArchive, totalFilesInArchive, - totalCompressedBytesRead) { - super(UnarchiveEventType.PROGRESS); - - this.currentFilename = currentFilename; - this.currentFileNumber = currentFileNumber; - this.currentBytesUnarchivedInFile = currentBytesUnarchivedInFile; - this.totalFilesInArchive = totalFilesInArchive; - this.currentBytesUnarchived = currentBytesUnarchived; - this.totalUncompressedBytesInArchive = totalUncompressedBytesInArchive; - this.totalCompressedBytesRead = totalCompressedBytesRead; - } -} - -/** - * Extract event. - */ -export class UnarchiveExtractEvent extends UnarchiveEvent { - /** - * @param {UnarchivedFile} unarchivedFile - */ - constructor(unarchivedFile) { - super(UnarchiveEventType.EXTRACT); - - /** - * @type {UnarchivedFile} - */ - this.unarchivedFile = unarchivedFile; - } -} - -/** - * Base class for all Unarchivers. - */ - export class Unarchiver extends EventTarget { - /** - * @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 - * to the Worker. - * @param {Function(string):Worker} createWorkerFn A function that creates a Worker from a script file. - * @param {Object|string} options An optional object of options, or a string representing where - * the BitJS files are located. The string version of this argument is deprecated. - * Available options: - * 'pathToBitJS': A string indicating where the BitJS files are located. - * 'debug': A boolean where true indicates that the archivers should log debug output. - */ - constructor(arrayBuffer, createWorkerFn, options = {}) { - super(); - - if (typeof options === 'string') { - console.warn(`Deprecated: Don't send a raw string to Unarchiver()`); - console.warn(` send {'pathToBitJS':'${options}'} instead`); - options = { 'pathToBitJS': options }; - } - - /** - * The ArrayBuffer object. - * @type {ArrayBuffer} - * @protected - */ - this.ab = arrayBuffer; - - /** - * A factory method that creates a Worker that does the unarchive work. - * @type {Function(string): Worker} - * @private - */ - this.createWorkerFn_ = createWorkerFn; - - /** - * The path to the BitJS files. - * @type {string} - * @private - */ - this.pathToBitJS_ = options.pathToBitJS || '/'; - - /** - * @orivate - * @type {boolean} - */ - this.debugMode_ = !!(options.debug); - - /** - * Private web worker initialized during start(). - * @private - * @type {Worker} - */ - this.worker_ = null; - } - - /** - * This method must be overridden by the subclass to return the script filename. - * @returns {string} The MIME type of the archive. - * @protected. - */ - getMIMEType() { - throw 'Subclasses of Unarchiver must overload getMIMEType()'; - } - - /** - * This method must be overridden by the subclass to return the script filename. - * @returns {string} The script filename. - * @protected. - */ - getScriptFileName() { - throw 'Subclasses of Unarchiver must overload getScriptFileName()'; - } - - /** - * Create an UnarchiveEvent out of the object sent back from the Worker. - * @param {Object} obj - * @returns {UnarchiveEvent} - * @private - */ - createUnarchiveEvent_(obj) { - switch (obj.type) { - case UnarchiveEventType.START: - return new UnarchiveStartEvent(); - case UnarchiveEventType.PROGRESS: - return new UnarchiveProgressEvent( - obj.currentFilename, - obj.currentFileNumber, - obj.currentBytesUnarchivedInFile, - obj.currentBytesUnarchived, - obj.totalUncompressedBytesInArchive, - obj.totalFilesInArchive, - obj.totalCompressedBytesRead); - case UnarchiveEventType.EXTRACT: - return new UnarchiveExtractEvent(obj.unarchivedFile); - case UnarchiveEventType.FINISH: - return new UnarchiveFinishEvent(obj.metadata); - case UnarchiveEventType.INFO: - return new UnarchiveInfoEvent(obj.msg); - case UnarchiveEventType.ERROR: - return new UnarchiveErrorEvent(obj.msg); - } - } - - /** - * Receive an event and pass it to the listener functions. - * - * @param {Object} obj - * @private - */ - handleWorkerEvent_(obj) { - const type = obj.type; - if (type && Object.values(UnarchiveEventType).includes(type)) { - const evt = this.createUnarchiveEvent_(obj); - this.dispatchEvent(evt); - if (evt.type == UnarchiveEventType.FINISH) { - this.worker_.terminate(); - } - } else { - console.log(`Unknown object received from worker: ${obj}`); - } - } - - /** - * Starts the unarchive in a separate Web Worker thread and returns immediately. - */ - start() { - const me = this; - const scriptFileName = this.pathToBitJS_ + this.getScriptFileName(); - if (scriptFileName) { - this.worker_ = this.createWorkerFn_(scriptFileName); - - this.worker_.onerror = function (e) { - console.log('Worker error: message = ' + e.message); - throw e; - }; - - this.worker_.onmessage = function (e) { - if (typeof e.data == 'string') { - // Just log any strings the workers pump our way. - console.log(e.data); - } else { - me.handleWorkerEvent_(e.data); - } - }; - - const ab = this.ab; - this.worker_.postMessage({ - file: ab, - logToConsole: this.debugMode_, - }, [ab]); - this.ab = null; - } - } - - // TODO: Create a startSync() method that does not use a worker for Node. - - /** - * Adds more bytes to the unarchiver's Worker thread. - * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is - * set to true, this ArrayBuffer must not be referenced after calling update(), since it - * is marked as Transferable and sent to the Worker. - * @param {boolean=} opt_transferable Optional boolean whether to mark this ArrayBuffer - * as a Tranferable object, which means it can no longer be referenced outside of - * the Worker thread. - */ - update(ab, opt_transferable = false) { - const numBytes = ab.byteLength; - if (this.worker_) { - // Send the ArrayBuffer over, and mark it as a Transferable object if necessary. - if (opt_transferable) { - this.worker_.postMessage({ bytes: ab }, [ab]); - } else { - this.worker_.postMessage({ bytes: ab }); - } - } - - this.dispatchEvent(new UnarchiveAppendEvent(numBytes)); - } - - /** - * Terminates the Web Worker for this Unarchiver and returns immediately. - */ - stop() { - if (this.worker_) { - this.worker_.terminate(); - } - } -} - -export class UnzipperInternal extends Unarchiver { - constructor(arrayBuffer, createWorkerFn, options) { - super(arrayBuffer, createWorkerFn, options); - } - - getMIMEType() { return 'application/zip'; } - getScriptFileName() { return 'archive/unzip.js'; } -} - -export class UnrarrerInternal extends Unarchiver { - constructor(arrayBuffer, createWorkerFn, options) { - super(arrayBuffer, createWorkerFn, options); - } - - getMIMEType() { return 'application/x-rar-compressed'; } - getScriptFileName() { return 'archive/unrar.js'; } -} - -export class UntarrerInternal extends Unarchiver { - constructor(arrayBuffer, createWorkerFn, options) { - super(arrayBuffer, createWorkerFn, options); - } - - getMIMEType() { return 'application/x-tar'; } - getScriptFileName() { return 'archive/untar.js'; }; -} - -/** - * Factory method that creates an unarchiver based on the byte signature found - * in the arrayBuffer. - * @param {ArrayBuffer} ab - * @param {Function(string):Worker} createWorkerFn A function that creates a Worker from a script file. - * @param {Object|string} options An optional object of options, or a string representing where - * the path to the unarchiver script files. - * @returns {Unarchiver} - */ - export function getUnarchiverInternal(ab, createWorkerFn, options = {}) { - if (ab.byteLength < 10) { - return null; - } - - let unarchiver = null; - const mimeType = findMimeType(ab); - - if (mimeType === 'application/x-rar-compressed') { // Rar! - unarchiver = new UnrarrerInternal(ab, createWorkerFn, options); - } else if (mimeType === 'application/zip') { // PK (Zip) - unarchiver = new UnzipperInternal(ab, createWorkerFn, options); - } else { // Try with tar - unarchiver = new UntarrerInternal(ab, createWorkerFn, options); - } - return unarchiver; -} diff --git a/archive/decompress.js b/archive/decompress.js index 0802975..3d7dcfb 100644 --- a/archive/decompress.js +++ b/archive/decompress.js @@ -8,12 +8,14 @@ * Copyright(c) 2021 Google Inc. */ - import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType, - UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent, - UnarchiveProgressEvent, UnarchiveStartEvent, Unarchiver, - UnrarrerInternal, UntarrerInternal, UnzipperInternal, - getUnarchiverInternal } from './decompress-internal.js'; +import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType, + UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent, + UnarchiveProgressEvent, UnarchiveStartEvent } from './events.js'; +import { getConnectedPort } from './common.js'; +import { findMimeType } from '../file/sniffer.js'; +// Exported as a convenience (and also because this module used to contain these). +// TODO(2.0): Remove this export, since they have moved to events.js? export { UnarchiveAppendEvent, UnarchiveErrorEvent, @@ -24,13 +26,13 @@ export { UnarchiveInfoEvent, UnarchiveProgressEvent, UnarchiveStartEvent, - Unarchiver, } /** -* All extracted files returned by an Unarchiver will implement -* the following interface: -*/ + * All extracted files returned by an Unarchiver will implement + * the following interface: + * TODO: Move this interface into common.js? + */ /** * @typedef UnarchivedFile @@ -39,46 +41,358 @@ export { */ /** -* The goal is to make this testable - send getUnarchiver() an array buffer of -* an archive, call start on the unarchiver, expect the returned result. -* -* Problem: It relies on Web Workers, and that won't work in a nodejs context. -* Solution: Make archive.js very thin, have it feed web-specific things into -* an internal module that is isomorphic JavaScript. -* -* TODO: -* - write unit tests for archive-internal.js that use the nodejs Worker -* equivalent. -* - maybe use @pgriess/node-webworker or @audreyt/node-webworker-threads or -* just node's worker_threads ? -*/ - -const createWorkerFn = (scriptFilename) => new Worker(scriptFilename); - -// Thin wrappers of compressors for clients who want to construct a specific + * @typedef UnarchiverOptions + * @property {boolean=} debug Set to true for verbose unarchiver logging. + */ + +/** + * Base class for all Unarchivers. + */ +export class Unarchiver extends EventTarget { + /** + * 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 {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 + * to the decompress implementation. + * @param {UnarchiverOptions|string} options An optional object of options, or a string + * representing where the BitJS files are located. The string version of this argument is + * deprecated. + */ + constructor(arrayBuffer, options = {}) { + super(); + + // TODO(2.0): Remove this. + if (typeof options === 'string') { + console.warn(`Deprecated: Don't send a raw string to Unarchiver()`); + console.warn(` send UnarchiverOptions instead.`); + options = { }; + } + + /** + * The ArrayBuffer object. + * @type {ArrayBuffer} + * @protected + */ + this.ab = arrayBuffer; + + /** + * @orivate + * @type {boolean} + */ + this.debugMode_ = !!(options.debug); + } + + /** + * Overridden so that the type hints for eventType are specific. Prefer onExtract(), etc. + * @param {'progress'|'extract'|'finish'} eventType + * @param {EventListenerOrEventListenerObject} listener + * @override + */ + addEventListener(eventType, listener) { + super.addEventListener(eventType, listener); + } + + /** + * Type-safe way to subscribe to an UnarchiveExtractEvent. + * @param {function(UnarchiveExtractEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onExtract(listener) { + super.addEventListener(UnarchiveEventType.EXTRACT, listener); + return this; + } + + /** + * Type-safe way to subscribe to an UnarchiveFinishEvent. + * @param {function(UnarchiveFinishEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onFinish(listener) { + super.addEventListener(UnarchiveEventType.FINISH, listener); + return this; + } + + /** + * Type-safe way to subscribe to an UnarchiveProgressEvent. + * @param {function(UnarchiveProgressEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onProgress(listener) { + super.addEventListener(UnarchiveEventType.PROGRESS, listener); + return this; + } + + /** + * This method must be overridden by the subclass to return the script filename. + * @returns {string} The MIME type of the archive. + * @protected. + */ + getMIMEType() { + throw 'Subclasses of Unarchiver must overload getMIMEType()'; + } + + /** + * This method must be overridden by the subclass to return the script filename. + * @returns {string} The script filename. + * @protected. + */ + getScriptFileName() { + throw 'Subclasses of Unarchiver must overload getScriptFileName()'; + } + + /** + * Create an UnarchiveEvent out of the object sent back from the implementation. + * @param {Object} obj + * @returns {UnarchiveEvent} + * @private + */ + createUnarchiveEvent_(obj) { + switch (obj.type) { + case UnarchiveEventType.START: + return new UnarchiveStartEvent(); + case UnarchiveEventType.PROGRESS: + return new UnarchiveProgressEvent( + obj.currentFilename, + obj.currentFileNumber, + obj.currentBytesUnarchivedInFile, + obj.currentBytesUnarchived, + obj.totalUncompressedBytesInArchive, + obj.totalFilesInArchive, + obj.totalCompressedBytesRead); + case UnarchiveEventType.EXTRACT: + return new UnarchiveExtractEvent(obj.unarchivedFile); + case UnarchiveEventType.FINISH: + return new UnarchiveFinishEvent(obj.metadata); + case UnarchiveEventType.INFO: + return new UnarchiveInfoEvent(obj.msg); + case UnarchiveEventType.ERROR: + return new UnarchiveErrorEvent(obj.msg); + } + } + + /** + * Receive an event and pass it to the listener functions. + * @param {Object} obj + * @returns {boolean} Returns true if the decompression is finished. + * @private + */ + handlePortEvent_(obj) { + const type = obj.type; + if (type && Object.values(UnarchiveEventType).includes(type)) { + const evt = this.createUnarchiveEvent_(obj); + this.dispatchEvent(evt); + if (evt.type == UnarchiveEventType.FINISH) { + this.stop(); + return true; + } + } else { + console.log(`Unknown object received from port: ${obj}`); + } + return false; + } + + /** + * Starts the unarchive by connecting the ports and sending the first ArrayBuffer. + * @returns {Promise} A Promise that resolves when the decompression is complete. While the + * decompression is proceeding, you can send more bytes of the archive to the decompressor + * using the update() method. + */ + async start() { + 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); + reject(evt); + }; + + this.port_.onmessage = (evt) => { + if (typeof evt.data == 'string') { + // Just log any strings the implementation pumps our way. + console.log(evt.data); + } else { + if (this.handlePortEvent_(evt.data)) { + resolve(); + } + } + }; + + const ab = this.ab; + this.port_.postMessage({ + file: ab, + logToConsole: this.debugMode_, + }, [ab]); + this.ab = null; + }); + } + + // TODO(bitjs): Test whether ArrayBuffers must be transferred... + /** + * Adds more bytes to the unarchiver. + * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is + * set to true, this ArrayBuffer must not be referenced after calling update(), since it + * is marked as Transferable and sent to the implementation. + * @param {boolean=} opt_transferable Optional boolean whether to mark this ArrayBuffer + * as a Tranferable object, which means it can no longer be referenced outside of + * the implementation context. + */ + update(ab, opt_transferable = false) { + const numBytes = ab.byteLength; + if (this.port_) { + // Send the ArrayBuffer over, and mark it as a Transferable object if necessary. + if (opt_transferable) { + this.port_.postMessage({ bytes: ab }, [ab]); + } else { + this.port_.postMessage({ bytes: ab }); + } + } + + this.dispatchEvent(new UnarchiveAppendEvent(numBytes)); + } + + /** + * Closes the port to the decompressor implementation and terminates it. + */ + stop() { + if (this.port_) { + this.port_.close(); + this.disconnectFn_(); + this.port_ = null; + this.disconnectFn_ = null; + } + } +} + +// Thin wrappers of decompressors for clients who want to construct a specific // unarchiver themselves rather than use getUnarchiver(). -export class Unzipper extends UnzipperInternal { - constructor(ab, options) { super(ab, createWorkerFn, options); } +export class Unzipper extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab, options = {}) { + super(ab, options); + } + + getMIMEType() { return 'application/zip'; } + getScriptFileName() { return './unzip.js'; } } -export class Unrarrer extends UnrarrerInternal { - constructor(ab, options) { super(ab, createWorkerFn, options); } +export class Unrarrer extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab, options = {}) { + super(ab, options); + } + + getMIMEType() { return 'application/x-rar-compressed'; } + getScriptFileName() { return './unrar.js'; } } -export class Untarrer extends UntarrerInternal { - constructor(ab, options) { super(ab, createWorkerFn, options); } +export class Untarrer extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab, options = {}) { + super(ab, options); + } + + getMIMEType() { return 'application/x-tar'; } + getScriptFileName() { return './untar.js'; }; } /** -* Factory method that creates an unarchiver based on the byte signature found -* in the arrayBuffer. -* @param {ArrayBuffer} ab The ArrayBuffer to unarchive. Note that this ArrayBuffer -* must not be referenced after calling this method, as the ArrayBuffer is marked -* as Transferable and sent to a Worker thread once start() is called. -* @param {Object|string} options An optional object of options, or a string -* representing where the path to the unarchiver script files. -* @returns {Unarchiver} -*/ + * IMPORTANT NOTES for Gunzipper: + * 1) A Gunzipper will only ever emit one EXTRACT event, because a gzipped file only ever contains + * a single file. + * 2) If the gzipped file does not include the original filename as a FNAME block, then the + * UnarchivedFile in the UnarchiveExtractEvent will not include a filename. It will be up to the + * client to re-assemble the filename (if needed). + * 3) update() is not supported on a Gunzipper, since the current implementation relies on runtime + * support for DecompressionStream('gzip') which can throw hard-to-detect errors reading only + * only part of a file. + * 4) PROGRESS events are not yet supported in Gunzipper. + */ +export class Gunzipper extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab, options = {}) { + super(ab, options); + } + + getMIMEType() { return 'application/gzip'; } + getScriptFileName() { return './gunzip.js'; } +} + +// TODO(2.0): When up-revving to a major new version, remove the string type for options. + +/** + * Factory method that creates an unarchiver based on the byte signature found + * in the ArrayBuffer. + * @param {ArrayBuffer} ab The ArrayBuffer to unarchive. Note that this ArrayBuffer + * must not be referenced after calling this method, as the ArrayBuffer may be + * transferred to a different JS context once start() is called. + * @param {UnarchiverOptions|string} options An optional object of options, or a + * string representing where the path to the unarchiver script files. The latter + * is now deprecated (use UnarchiverOptions). + * @returns {Unarchiver} + */ export function getUnarchiver(ab, options = {}) { - return getUnarchiverInternal(ab, createWorkerFn, options); + if (ab.byteLength < 10) { + return null; + } + + let unarchiver = null; + const mimeType = findMimeType(ab); + + if (mimeType === 'application/x-rar-compressed') { // Rar! + unarchiver = new Unrarrer(ab, options); + } else if (mimeType === 'application/zip') { // PK (Zip) + unarchiver = new Unzipper(ab, options); + } else if (mimeType === 'application/gzip') { // GZIP + unarchiver = new Gunzipper(ab, options); + } else { // Try with tar + unarchiver = new Untarrer(ab, options); + } + return unarchiver; } + +// import * as fs from 'node:fs'; +// async function main() { +// const nodeBuf = fs.readFileSync(`./tests/archive-testfiles/archive-rar-store.rar`); +// const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); +// const then = Date.now(); +// const unarchiver = getUnarchiver(ab, {debug: true}) +// unarchiver.addEventListener('extract', evt => { +// console.dir(evt); +// const f = evt.unarchivedFile; +// fs.writeFileSync(f.filename, Buffer.from(f.fileData)); +// }); +// unarchiver.addEventListener('finish', evt => { +// console.dir(evt); +// console.log(`Took ${(Date.now() - then)}ms`); +// }); +// await unarchiver.start(); +// } + +// main(); diff --git a/archive/events.js b/archive/events.js new file mode 100644 index 0000000..b7b582b --- /dev/null +++ b/archive/events.js @@ -0,0 +1,146 @@ +/** + * events.js + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +// TODO(2.0): Consider deprecating the Event subclasses here and: +// 1) Make @typedef structures in jsdoc for all the payloads +// 2) Use CustomEvent for payload event propagation +// 3) Add semantic methods to the archivers (onExtract, onProgress) like the image parsers. +// 4) Move everything into common.js ? + +/** + * The UnarchiveEvent types. + */ +export const UnarchiveEventType = { + START: 'start', + APPEND: 'append', + PROGRESS: 'progress', + EXTRACT: 'extract', + FINISH: 'finish', + INFO: 'info', + ERROR: 'error' +}; + +// 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. + */ + constructor(type) { + super(type); + } +} + +/** Updates all Unarchiver listeners that an append has occurred. */ +export class UnarchiveAppendEvent extends UnarchiveEvent { + /** + * @param {number} numBytes The number of bytes appended. + */ + constructor(numBytes) { + super(UnarchiveEventType.APPEND); + + /** + * The number of appended bytes. + * @type {number} + */ + this.numBytes = numBytes; + } +} + +/** Useful for passing info up to the client (for debugging). */ +export class UnarchiveInfoEvent extends UnarchiveEvent { + /** + * @param {string} msg The info message. + */ + constructor(msg) { + super(UnarchiveEventType.INFO); + + /** + * The information message. + * @type {string} + */ + this.msg = msg; + } +} + +/** An unrecoverable error has occured. */ +export class UnarchiveErrorEvent extends UnarchiveEvent { + /** + * @param {string} msg The error message. + */ + constructor(msg) { + super(UnarchiveEventType.ERROR); + + /** + * The information message. + * @type {string} + */ + this.msg = msg; + } +} + +/** Start event. */ +export class UnarchiveStartEvent extends UnarchiveEvent { + constructor() { + super(UnarchiveEventType.START); + } +} + +/** Finish event. */ +export class UnarchiveFinishEvent extends UnarchiveEvent { + /** + * @param {Object} metadata A collection of metadata about the archive file. + */ + constructor(metadata = {}) { + super(UnarchiveEventType.FINISH); + this.metadata = metadata; + } +} + +// TODO(bitjs): Fully document these. They are confusing. +/** Progress event. */ +export class UnarchiveProgressEvent extends UnarchiveEvent { + /** + * @param {string} currentFilename + * @param {number} currentFileNumber + * @param {number} currentBytesUnarchivedInFile + * @param {number} currentBytesUnarchived + * @param {number} totalUncompressedBytesInArchive + * @param {number} totalFilesInArchive + * @param {number} totalCompressedBytesRead + */ + constructor(currentFilename, currentFileNumber, currentBytesUnarchivedInFile, + currentBytesUnarchived, totalUncompressedBytesInArchive, totalFilesInArchive, + totalCompressedBytesRead) { + super(UnarchiveEventType.PROGRESS); + + this.currentFilename = currentFilename; + this.currentFileNumber = currentFileNumber; + this.currentBytesUnarchivedInFile = currentBytesUnarchivedInFile; + this.totalFilesInArchive = totalFilesInArchive; + this.currentBytesUnarchived = currentBytesUnarchived; + this.totalUncompressedBytesInArchive = totalUncompressedBytesInArchive; + this.totalCompressedBytesRead = totalCompressedBytesRead; + } +} + +/** Extract event. */ +export class UnarchiveExtractEvent extends UnarchiveEvent { + /** + * @param {UnarchivedFile} unarchivedFile + */ + constructor(unarchivedFile) { + super(UnarchiveEventType.EXTRACT); + + /** + * @type {UnarchivedFile} + */ + this.unarchivedFile = unarchivedFile; + } +} diff --git a/archive/gunzip.js b/archive/gunzip.js new file mode 100644 index 0000000..2f32631 --- /dev/null +++ b/archive/gunzip.js @@ -0,0 +1,125 @@ +/** + * gunzip.js + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + * + * Reference Documentation: + * + * https://www.ietf.org/rfc/rfc1952.txt + */ + +import { BitStream } from '../io/bitstream.js'; +import { ByteStream } from '../io/bytestream.js'; + +/** @type {MessagePort} */ +let hostPort; + +/** @type {ByteStream} */ +let bstream = null; +// undefined unless a FNAME block is present. +let filename; + +const err = str => hostPort.postMessage({ type: 'error', msg: str }); + +async function gunzip() { + const sig = bstream.readBytes(2); + if (sig[0] !== 0x1F || sig[1] !== 0x8B) { + const errMsg = `First two bytes not 0x1F, 0x8B: ${sig[0].toString(16)} ${sig[1].toString(16)}`; + err(errMsg); + return; + } + const compressionMethod = bstream.readNumber(1); + if (compressionMethod !== 8) { + const errMsg = `Compression method ${compressionMethod} not supported`; + err(errMsg); + return; + } + + // Parse the GZIP header to see if we can find a filename (FNAME block). + const flags = new BitStream(bstream.readBytes(1).buffer); + flags.skip(1); // skip FTEXT bit + const fhcrc = flags.readBits(1); + const fextra = flags.readBits(1); + const fname = flags.readBits(1); + const fcomment = flags.readBits(1); + + bstream.skip(4); // MTIME + bstream.skip(1); // XFL + bstream.skip(1); // OS + + if (fextra) { + const xlen = bstream.readNumber(2); + bstream.skip(xlen); + } + + if (fname) { + // Find the null-terminator byte. + let numBytes = 0; + const findNull = bstream.tee(); + while (findNull.readNumber(1) !== 0) numBytes++; + filename = bstream.readString(numBytes); + } + + if (fcomment) { + // Find the null-terminator byte. + let numBytes = 0; + const findNull = bstream.tee(); + while (findNull.readNumber(1) !== 0) numBytes++; + bstream.skip(numBytes); // COMMENT + } + + if (fhcrc) { + bstream.readNumber(2); // CRC16 + } + + // Now try to use native implementation of INFLATE, if supported by the runtime. + const blob = new Blob([bstream.bytes.buffer]); + const decompressedStream = blob.stream().pipeThrough(new DecompressionStream('gzip')); + const fileData = new Uint8Array(await new Response(decompressedStream).arrayBuffer()); + const unarchivedFile = { filename, fileData }; + hostPort.postMessage({ type: 'extract', unarchivedFile }, [fileData.buffer]); + + // TODO: Supported chunked decompression? + // TODO: Fall through to non-native implementation via inflate() ? + + hostPort.postMessage({ type: 'finish', metadata: {} }); +} + +// event.data.file has the first ArrayBuffer. +const onmessage = async function (event) { + const bytes = event.data.file; + + if (!bstream) { + bstream = new ByteStream(bytes); + bstream.setLittleEndian(true); + } else { + throw `Gunzipper does not calling update() with more bytes. Send the whole file with start().` + } + + await gunzip(); +}; + +/** + * Connect the host to the gunzip implementation with the given MessagePort. + * @param {MessagePort} port + */ +export function connect(port) { + if (hostPort) { + throw `connect(): hostPort already connected in gunzip.js`; + } + + hostPort = port; + port.onmessage = onmessage; +} + +export function disconnect() { + if (!hostPort) { + throw `disconnect(): hostPort was not connected in gunzip.js`; + } + + hostPort = null; + bstream = null; + filename = undefined; +} diff --git a/archive/inflate.js b/archive/inflate.js new file mode 100644 index 0000000..c0f0ab3 --- /dev/null +++ b/archive/inflate.js @@ -0,0 +1,410 @@ +/** + * inflate.js + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + * + * Implementation of INFLATE. Uses DecompressionStream, if the runtime supports it, otherwise uses + * an implementation purely in JS. + * + * Reference Documentation: + * + * DEFLATE format: http://tools.ietf.org/html/rfc1951 + */ + +import { BitStream } from '../io/bitstream.js'; +import { ByteBuffer } from '../io/bytebuffer.js'; + +/** + * @typedef SymbolLengthPair + * @property {number} length + * @property {number} symbol + */ + +/** + * Returns a table of Huffman codes. Each entry's key is its code and its value is a JavaScript + * object containing {length: 6, symbol: X}. + * @param {number[]} bitLengths An array representing the bit lengths of the codes, in order. + * See section 3.2.2 of https://datatracker.ietf.org/doc/html/rfc1951. + * @returns {Map} + */ +function getHuffmanCodes(bitLengths) { + // ensure bitLengths is an array containing at least one element + if (typeof bitLengths != typeof [] || bitLengths.length < 1) { + err('Error! getHuffmanCodes() called with an invalid array'); + return null; + } + + // Reference: http://tools.ietf.org/html/rfc1951#page-8 + const numLengths = bitLengths.length; + const bl_count = []; + let MAX_BITS = 1; + + // Step 1: count up how many codes of each length we have + for (let i = 0; i < numLengths; ++i) { + const length = bitLengths[i]; + // test to ensure each bit length is a positive, non-zero number + if (typeof length != typeof 1 || length < 0) { + err(`bitLengths contained an invalid number in getHuffmanCodes(): ${length} of type ${typeof length}`); + return null; + } + // increment the appropriate bitlength count + if (bl_count[length] == undefined) bl_count[length] = 0; + // a length of zero means this symbol is not participating in the huffman coding + if (length > 0) bl_count[length]++; + if (length > MAX_BITS) MAX_BITS = length; + } + + // Step 2: Find the numerical value of the smallest code for each code length + const next_code = []; + let code = 0; + for (let bits = 1; bits <= MAX_BITS; ++bits) { + const length = bits - 1; + // ensure undefined lengths are zero + if (bl_count[length] == undefined) bl_count[length] = 0; + code = (code + bl_count[bits - 1]) << 1; + next_code[bits] = code; + } + + // Step 3: Assign numerical values to all codes + /** @type Map */ + const table = new Map(); + for (let n = 0; n < numLengths; ++n) { + const len = bitLengths[n]; + if (len != 0) { + table.set(next_code[len], { length: len, symbol: n }); + next_code[len]++; + } + } + + return table; +} + +/* + The Huffman codes for the two alphabets are fixed, and are not + represented explicitly in the data. The Huffman code lengths + for the literal/length alphabet are: + + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 +*/ +// fixed Huffman codes go from 7-9 bits, so we need an array whose index can hold up to 9 bits +let fixedHCtoLiteral = null; +let fixedHCtoDistance = null; +/** @returns {Map} */ +function getFixedLiteralTable() { + // create once + if (!fixedHCtoLiteral) { + const bitlengths = new Array(288); + for (let i = 0; i <= 143; ++i) bitlengths[i] = 8; + for (let i = 144; i <= 255; ++i) bitlengths[i] = 9; + for (let i = 256; i <= 279; ++i) bitlengths[i] = 7; + for (let i = 280; i <= 287; ++i) bitlengths[i] = 8; + + // get huffman code table + fixedHCtoLiteral = getHuffmanCodes(bitlengths); + } + return fixedHCtoLiteral; +} + +/** @returns {Map} */ +function getFixedDistanceTable() { + // create once + if (!fixedHCtoDistance) { + const bitlengths = new Array(32); + for (let i = 0; i < 32; ++i) { bitlengths[i] = 5; } + + // get huffman code table + fixedHCtoDistance = getHuffmanCodes(bitlengths); + } + return fixedHCtoDistance; +} + +/** + * Extract one bit at a time until we find a matching Huffman Code + * then return that symbol. + * @param {BitStream} bstream + * @param {Map} hcTable + * @returns {number} + */ +function decodeSymbol(bstream, hcTable) { + let code = 0; + let len = 0; + + // loop until we match + for (; ;) { + // read in next bit + const bit = bstream.readBits(1); + code = (code << 1) | bit; + ++len; + + // check against Huffman Code table and break if found + if (hcTable.has(code) && hcTable.get(code).length == len) { + break; + } + if (len > hcTable.length) { + err(`Bit stream out of sync, didn't find a Huffman Code, length was ${len} ` + + `and table only max code length of ${hcTable.length}`); + break; + } + } + return hcTable.get(code).symbol; +} + + +const CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + +/* + Extra Extra Extra +Code Bits Length(s) Code Bits Lengths Code Bits Length(s) +---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 +*/ +const LengthLookupTable = [ + [0, 3], [0, 4], [0, 5], [0, 6], + [0, 7], [0, 8], [0, 9], [0, 10], + [1, 11], [1, 13], [1, 15], [1, 17], + [2, 19], [2, 23], [2, 27], [2, 31], + [3, 35], [3, 43], [3, 51], [3, 59], + [4, 67], [4, 83], [4, 99], [4, 115], + [5, 131], [5, 163], [5, 195], [5, 227], + [0, 258] +]; + +/* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 +*/ +const DistLookupTable = [ + [0, 1], [0, 2], [0, 3], [0, 4], + [1, 5], [1, 7], + [2, 9], [2, 13], + [3, 17], [3, 25], + [4, 33], [4, 49], + [5, 65], [5, 97], + [6, 129], [6, 193], + [7, 257], [7, 385], + [8, 513], [8, 769], + [9, 1025], [9, 1537], + [10, 2049], [10, 3073], + [11, 4097], [11, 6145], + [12, 8193], [12, 12289], + [13, 16385], [13, 24577] +]; + +/** + * @param {BitStream} bstream + * @param {Map} hcLiteralTable + * @param {Map} hcDistanceTable + * @param {ByteBuffer} buffer + * @returns + */ +function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { + /* + loop (until end of block code recognized) + decode literal/length value from input stream + if value < 256 + copy value (literal byte) to output stream + otherwise + if value = end of block (256) + break from loop + otherwise (value = 257..285) + decode distance from input stream + + move backwards distance bytes in the output + stream, and copy length bytes from this + position to the output stream. + */ + let blockSize = 0; + for (; ;) { + const symbol = decodeSymbol(bstream, hcLiteralTable); + if (symbol < 256) { + // copy literal byte to output + buffer.insertByte(symbol); + blockSize++; + } else { + // end of block reached + if (symbol == 256) { + break; + } else { + const lengthLookup = LengthLookupTable[symbol - 257]; + let length = lengthLookup[1] + bstream.readBits(lengthLookup[0]); + const distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)]; + let distance = distLookup[1] + bstream.readBits(distLookup[0]); + + // now apply length and distance appropriately and copy to output + + // TODO: check that backward distance < data.length? + + // http://tools.ietf.org/html/rfc1951#page-11 + // "Note also that the referenced string may overlap the current + // position; for example, if the last 2 bytes decoded have values + // X and Y, a string reference with + // adds X,Y,X,Y,X to the output stream." + // + // loop for each character + let ch = buffer.ptr - distance; + blockSize += length; + if (length > distance) { + const data = buffer.data; + while (length--) { + buffer.insertByte(data[ch++]); + } + } else { + buffer.insertBytes(buffer.data.subarray(ch, ch + length)) + } + } // length-distance pair + } // length-distance pair or end-of-block + } // loop until we reach end of block + return blockSize; +} + +/** + * Compression method 8. Deflate: http://tools.ietf.org/html/rfc1951 + * @param {Uint8Array} compressedData A Uint8Array of the compressed file data. + * @param {number} numDecompressedBytes + * @returns {Promise} The decompressed array. + */ +export async function inflate(compressedData, numDecompressedBytes) { + // Try to use native implementation of DEFLATE if it exists. + try { + const blob = new Blob([compressedData.buffer]); + const decompressedStream = blob.stream().pipeThrough(new DecompressionStream('deflate-raw')); + return new Uint8Array(await new Response(decompressedStream).arrayBuffer()); + } catch (err) { + // Fall through to non-native implementation of DEFLATE. + } + + // Bit stream representing the compressed data. + /** @type {BitStream} */ + const bstream = new BitStream(compressedData.buffer, + false /* mtl */, + compressedData.byteOffset, + compressedData.byteLength); + /** @type {ByteBuffer} */ + const buffer = new ByteBuffer(numDecompressedBytes); + let blockSize = 0; + + // block format: http://tools.ietf.org/html/rfc1951#page-9 + let bFinal = 0; + do { + bFinal = bstream.readBits(1); + let bType = bstream.readBits(2); + blockSize = 0; + // no compression + if (bType == 0) { + // skip remaining bits in this byte + while (bstream.bitPtr != 0) bstream.readBits(1); + const len = bstream.readBits(16); + const nlen = bstream.readBits(16); + // TODO: check if nlen is the ones-complement of len? + if (len > 0) buffer.insertBytes(bstream.readBytes(len)); + blockSize = len; + } + // fixed Huffman codes + else if (bType == 1) { + blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); + } + // dynamic Huffman codes + else if (bType == 2) { + const numLiteralLengthCodes = bstream.readBits(5) + 257; + const numDistanceCodes = bstream.readBits(5) + 1; + const numCodeLengthCodes = bstream.readBits(4) + 4; + + // populate the array of code length codes (first de-compaction) + const codeLengthsCodeLengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (let i = 0; i < numCodeLengthCodes; ++i) { + codeLengthsCodeLengths[CodeLengthCodeOrder[i]] = bstream.readBits(3); + } + + // get the Huffman Codes for the code lengths + const codeLengthsCodes = getHuffmanCodes(codeLengthsCodeLengths); + + // now follow this mapping + /* + 0 - 15: Represent code lengths of 0 - 15 + 16: Copy the previous code length 3 - 6 times. + The next 2 bits indicate repeat length + (0 = 3, ... , 3 = 6) + Example: Codes 8, 16 (+2 bits 11), + 16 (+2 bits 10) will expand to + 12 code lengths of 8 (1 + 6 + 5) + 17: Repeat a code length of 0 for 3 - 10 times. + (3 bits of length) + 18: Repeat a code length of 0 for 11 - 138 times + (7 bits of length) + */ + // to generate the true code lengths of the Huffman Codes for the literal + // and distance tables together + const literalCodeLengths = []; + let prevCodeLength = 0; + const maxCodeLengths = numLiteralLengthCodes + numDistanceCodes; + while (literalCodeLengths.length < maxCodeLengths) { + const symbol = decodeSymbol(bstream, codeLengthsCodes); + if (symbol <= 15) { + literalCodeLengths.push(symbol); + prevCodeLength = symbol; + } else if (symbol === 16) { + let repeat = bstream.readBits(2) + 3; + while (repeat--) { + literalCodeLengths.push(prevCodeLength); + } + } else if (symbol === 17) { + let repeat = bstream.readBits(3) + 3; + while (repeat--) { + literalCodeLengths.push(0); + } + } else if (symbol == 18) { + let repeat = bstream.readBits(7) + 11; + while (repeat--) { + literalCodeLengths.push(0); + } + } + } + + // now split the distance code lengths out of the literal code array + const distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes); + + // now generate the true Huffman Code tables using these code lengths + const hcLiteralTable = getHuffmanCodes(literalCodeLengths); + const hcDistanceTable = getHuffmanCodes(distanceCodeLengths); + blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); + } else { // error + err('Error! Encountered deflate block of type 3'); + return null; + } + } while (bFinal != 1); + // we are done reading blocks if the bFinal bit was set for this block + + // return the buffer data bytes + return buffer.data; +} diff --git a/archive/rarvm.js b/archive/rarvm.js index ebc8663..da0782f 100644 --- a/archive/rarvm.js +++ b/archive/rarvm.js @@ -6,6 +6,8 @@ * Copyright(c) 2017 Google Inc. */ +import { BitStream } from '../io/bitstream.js'; + /** * CRC Implementation. */ @@ -104,13 +106,13 @@ function CRC(startCRC, arr) { /** * RarVM Implementation. */ -const VM_MEMSIZE = 0x40000; -const VM_MEMMASK = (VM_MEMSIZE - 1); -const VM_GLOBALMEMADDR = 0x3C000; -const VM_GLOBALMEMSIZE = 0x2000; -const VM_FIXEDGLOBALSIZE = 64; -const MAXWINSIZE = 0x400000; -const MAXWINMASK = (MAXWINSIZE - 1); +export const VM_MEMSIZE = 0x40000; +export const VM_MEMMASK = (VM_MEMSIZE - 1); +export const VM_GLOBALMEMADDR = 0x3C000; +export const VM_GLOBALMEMSIZE = 0x2000; +export const VM_FIXEDGLOBALSIZE = 64; +export const MAXWINSIZE = 0x400000; +export const MAXWINMASK = (MAXWINSIZE - 1); /** */ @@ -334,7 +336,7 @@ class VM_PreparedProgram { /** */ -class UnpackFilter { +export class UnpackFilter { constructor() { /** @type {number} */ this.BlockStart = 0; @@ -448,7 +450,7 @@ const StdList = [ /** * @constructor */ -class RarVM { +export class RarVM { constructor() { /** @private {Uint8Array} */ this.mem_ = null; @@ -486,7 +488,7 @@ class RarVM { /** * @param {VM_PreparedOperand} op * @param {boolean} byteMode - * @param {bitjs.io.BitStream} bstream A rtl bit stream. + * @param {BitStream} bstream A rtl bit stream. */ decodeArg(op, byteMode, bstream) { const data = bstream.peekBits(16); @@ -808,7 +810,7 @@ class RarVM { //InitBitInput(); //memcpy(InBuf,Code,Min(CodeSize,BitInput::MAX_SIZE)); - const bstream = new bitjs.io.BitStream(code.buffer, true /* rtl */); + const bstream = new BitStream(code.buffer, true /* rtl */); // Calculate the single byte XOR checksum to check validity of VM code. let xorSum = 0; @@ -970,7 +972,7 @@ class RarVM { /** * Static function that reads in the next set of bits for the VM * (might return 4, 8, 16 or 32 bits). - * @param {bitjs.io.BitStream} bstream A RTL bit stream. + * @param {BitStream} bstream A RTL bit stream. * @returns {number} The value of the bits read. */ static readData(bstream) { diff --git a/archive/unrar.js b/archive/unrar.js index 28a2659..8a75c59 100644 --- a/archive/unrar.js +++ b/archive/unrar.js @@ -11,11 +11,11 @@ // of a BitStream so that it throws properly when not enough bytes are // present. -// This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bitstream-worker.js'); -importScripts('../io/bytestream-worker.js'); -importScripts('../io/bytebuffer-worker.js'); -importScripts('rarvm.js'); +import { BitStream } from '../io/bitstream.js'; +import { ByteStream } from '../io/bytestream.js'; +import { ByteBuffer } from '../io/bytebuffer.js'; +import { RarVM, UnpackFilter, VM_GLOBALMEMADDR, VM_GLOBALMEMSIZE, + VM_FIXEDGLOBALSIZE, MAXWINMASK } from './rarvm.js'; const UnarchiveState = { NOT_STARTED: 0, @@ -24,8 +24,12 @@ const UnarchiveState = { FINISHED: 3, }; +/** @type {MessagePort} */ +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; @@ -40,13 +44,13 @@ let totalFilesInArchive = 0; // Helper functions. const info = function (str) { - postMessage({ type: 'info', msg: str }); + hostPort.postMessage({ type: 'info', msg: str }); }; const err = function (str) { - postMessage({ type: 'error', msg: str }); + hostPort.postMessage({ type: 'error', msg: str }); }; const postProgress = function () { - postMessage({ + hostPort.postMessage({ type: 'progress', currentFilename, currentFileNumber, @@ -86,7 +90,7 @@ const ENDARC_HEAD = 0x7b; */ class RarVolumeHeader { /** - * @param {bitjs.io.ByteStream} bstream + * @param {ByteStream} bstream */ constructor(bstream) { let headBytesRead = 0; @@ -316,19 +320,19 @@ const RD = { //rep decode }; /** - * @type {Array} + * @type {Array} */ const rOldBuffers = []; /** * The current buffer we are unpacking to. - * @type {bitjs.io.ByteBuffer} + * @type {ByteBuffer} */ let rBuffer; /** * The buffer of the final bytes after filtering (only used in Unpack29). - * @type {bitjs.io.ByteBuffer} + * @type {ByteBuffer} */ let wBuffer; @@ -346,7 +350,7 @@ let wBuffer; /** * Read in Huffman tables for RAR - * @param {bitjs.io.BitStream} bstream + * @param {BitStream} bstream */ function RarReadTables(bstream) { const BitLength = new Array(rBC); @@ -491,7 +495,7 @@ function RarMakeDecodeTables(BitLength, offset, dec, size) { // TODO: implement /** - * @param {bitjs.io.BitStream} bstream + * @param {BitStream} bstream * @param {boolean} Solid */ function Unpack15(bstream, Solid) { @@ -500,12 +504,13 @@ function Unpack15(bstream, Solid) { /** * Unpacks the bit stream into rBuffer using the Unpack20 algorithm. - * @param {bitjs.io.BitStream} bstream + * @param {BitStream} bstream * @param {boolean} Solid */ function Unpack20(bstream, Solid) { const destUnpSize = rBuffer.data.length; let oldDistPtr = 0; + let Bits; if (!Solid) { RarReadTables20(bstream); @@ -694,7 +699,7 @@ function InitFilters() { */ function RarAddVMCode(firstByte, vmCode) { VM.init(); - const bstream = new bitjs.io.BitStream(vmCode.buffer, true /* rtl */); + const bstream = new BitStream(vmCode.buffer, true /* rtl */); let filtPos; if (firstByte & 0x80) { @@ -864,7 +869,7 @@ function RarAddVMCode(firstByte, vmCode) { /** - * @param {!bitjs.io.BitStream} bstream + * @param {!BitStream} bstream */ function RarReadVMCode(bstream) { const firstByte = bstream.readBits(8); @@ -886,7 +891,7 @@ function RarReadVMCode(bstream) { /** * Unpacks the bit stream into rBuffer using the Unpack29 algorithm. - * @param {bitjs.io.BitStream} bstream + * @param {BitStream} bstream * @param {boolean} Solid */ function Unpack29(bstream, Solid) { @@ -1258,9 +1263,9 @@ function unpack(v) { // TODO: implement what happens when unpVer is < 15 const Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer; const Solid = v.header.flags.LHD_SOLID; - const bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength); + const bstream = new BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength); - rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize); + rBuffer = new ByteBuffer(v.header.unpackedSize); if (logToConsole) { info('Unpacking ' + v.filename + ' RAR v' + Ver); @@ -1276,7 +1281,7 @@ function unpack(v) { break; case 29: // rar 3.x compression case 36: // alternative hash - wBuffer = new bitjs.io.ByteBuffer(rBuffer.data.length); + wBuffer = new ByteBuffer(rBuffer.data.length); Unpack29(bstream, Solid); break; } // switch(method) @@ -1290,7 +1295,7 @@ function unpack(v) { */ class RarLocalFile { /** - * @param {bitjs.io.ByteStream} bstream + * @param {ByteStream} bstream */ constructor(bstream) { this.header = new RarVolumeHeader(bstream); @@ -1325,7 +1330,7 @@ class RarLocalFile { // Create a new buffer and copy it over. const len = this.header.packSize; - const newBuffer = new bitjs.io.ByteBuffer(len); + const newBuffer = new ByteBuffer(len); newBuffer.insertBytes(this.fileData); this.fileData = newBuffer.data; } else { @@ -1340,19 +1345,20 @@ class RarLocalFile { function unrar_start() { let bstream = bytestream.tee(); const header = new RarVolumeHeader(bstream); - if (header.crc == 0x6152 && - header.headType == 0x72 && - header.flags.value == 0x1A21 && - header.headSize == 7) { - if (logToConsole) { - info('Found RAR signature'); - } + if (header.crc == 0x6152 && header.headType == 0x72 && header.flags.value == 0x1A21) { + if (header.headSize == 7) { + if (logToConsole) { + info('Found RAR signature'); + } - const mhead = new RarVolumeHeader(bstream); - if (mhead.headType != MAIN_HEAD) { - info('Error! RAR did not include a MAIN_HEAD header'); - } else { - bytestream = bstream.tee(); + const mhead = new RarVolumeHeader(bstream); + if (mhead.headType != MAIN_HEAD) { + info('Error! RAR did not include a MAIN_HEAD header'); + } else { + bytestream = bstream.tee(); + } + } else if (header.headSize === 0x107) { + throw 'Error! RAR5 files not supported yet. See https://github.com/codedread/bitjs/issues/25'; } } } @@ -1378,7 +1384,7 @@ function unrar() { localFile.unrar(); if (localFile.isValid) { - postMessage({ type: 'extract', unarchivedFile: localFile }, [localFile.fileData.buffer]); + hostPort.postMessage({ type: 'extract', unarchivedFile: localFile }, [localFile.fileData.buffer]); postProgress(); } } else if (localFile.header.packSize == 0 && localFile.header.unpackedSize == 0) { @@ -1396,13 +1402,13 @@ function unrar() { // event.data.file has the first ArrayBuffer. // event.data.bytes has all subsequent ArrayBuffers. -onmessage = function (event) { +const onmessage = function (event) { const bytes = event.data.file || event.data.bytes; logToConsole = !!event.data.logToConsole; // This is the very first time we have been called. Initialize the bytestream. if (!bytestream) { - bytestream = new bitjs.io.ByteStream(bytes); + bytestream = new ByteStream(bytes); currentFilename = ''; currentFileNumber = 0; @@ -1411,7 +1417,7 @@ onmessage = function (event) { totalUncompressedBytesInArchive = 0; totalFilesInArchive = 0; allLocalFiles = []; - postMessage({ type: 'start' }); + hostPort.postMessage({ type: 'start' }); } else { bytestream.push(bytes); } @@ -1441,7 +1447,7 @@ onmessage = function (event) { try { unrar(); unarchiveState = UnarchiveState.FINISHED; - postMessage({ type: 'finish', metadata: {} }); + hostPort.postMessage({ type: 'finish', metadata: {} }); } catch (e) { if (typeof e === 'string' && e.startsWith('Error! Overflowed')) { if (logToConsole) { @@ -1457,3 +1463,35 @@ onmessage = function (event) { } } }; + +/** + * Connect the host to the unrar implementation with the given MessagePort. + * @param {MessagePort} port + */ +export function connect(port) { + if (hostPort) { + 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 ed63a47..51adf64 100644 --- a/archive/untar.js +++ b/archive/untar.js @@ -10,8 +10,7 @@ * TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html */ -// This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bytestream-worker.js'); +import { ByteStream } from '../io/bytestream.js'; const UnarchiveState = { NOT_STARTED: 0, @@ -20,14 +19,18 @@ const UnarchiveState = { FINISHED: 3, }; +/** @type {MessagePort} */ +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; // Progress variables. -let currentFilename = ""; +let currentFilename = ''; let currentFileNumber = 0; let currentBytesUnarchivedInFile = 0; let currentBytesUnarchived = 0; @@ -36,13 +39,13 @@ let totalFilesInArchive = 0; // Helper functions. const info = function (str) { - postMessage({ type: 'info', msg: str }); + hostPort.postMessage({ type: 'info', msg: str }); }; const err = function (str) { - postMessage({ type: 'error', msg: str }); + hostPort.postMessage({ type: 'error', msg: str }); }; const postProgress = function () { - postMessage({ + hostPort.postMessage({ type: 'progress', currentFilename, currentFileNumber, @@ -80,7 +83,7 @@ class TarLocalFile { this.linkname = readCleanString(bstream, 100); this.maybeMagic = readCleanString(bstream, 6); - if (this.maybeMagic == "ustar") { + if (this.maybeMagic == 'ustar') { this.version = readCleanString(bstream, 2); this.uname = readCleanString(bstream, 32); this.gname = readCleanString(bstream, 32); @@ -88,8 +91,15 @@ class TarLocalFile { this.devminor = readCleanString(bstream, 8); this.prefix = readCleanString(bstream, 155); + // From https://linux.die.net/man/1/ustar: + // "The name field (100 chars) an inserted slash ('/') and the prefix field (155 chars) + // produce the pathname of the file. When recreating the original filename, name and prefix + // are concatenated, using a slash character in the middle. If a pathname does not fit in the + // space provided or may not be split at a slash character so that the parts will fit into + // 100 + 155 chars, the file may not be archived. Linknames longer than 100 chars may not be + // archived too." if (this.prefix.length) { - this.name = this.prefix + this.name; + this.name = `${this.prefix}/${this.name}`; } bstream.readBytes(12); // 512 - 500 } else { @@ -103,13 +113,13 @@ class TarLocalFile { /** @type {Uint8Array} */ this.fileData = null; - info("Untarring file '" + this.filename + "'"); - info(" size = " + this.size); - info(" typeflag = " + this.typeflag); + info(`Untarring file '${this.filename}'`); + info(` size = ${this.size}`); + info(` typeflag = ${this.typeflag}`); // A regular file. if (this.typeflag == 0) { - info(" This is a regular file."); + info(' This is a regular file.'); const sizeInBytes = parseInt(this.size); this.fileData = new Uint8Array(bstream.readBytes(sizeInBytes)); bytesRead += sizeInBytes; @@ -123,7 +133,7 @@ class TarLocalFile { bstream.readBytes(remaining); } } else if (this.typeflag == 5) { - info(" This is a directory.") + info(' This is a directory.') } } } @@ -147,7 +157,7 @@ const untar = function () { currentFileNumber = totalFilesInArchive++; currentBytesUnarchivedInFile = oneLocalFile.size; currentBytesUnarchived += oneLocalFile.size; - postMessage({ type: 'extract', unarchivedFile: oneLocalFile }, [oneLocalFile.fileData.buffer]); + hostPort.postMessage({ type: 'extract', unarchivedFile: oneLocalFile }, [oneLocalFile.fileData.buffer]); postProgress(); } } @@ -160,19 +170,19 @@ const untar = function () { // event.data.file has the first ArrayBuffer. // event.data.bytes has all subsequent ArrayBuffers. -onmessage = function (event) { +const onmessage = function (event) { const bytes = event.data.file || event.data.bytes; logToConsole = !!event.data.logToConsole; // This is the very first time we have been called. Initialize the bytestream. if (!bytestream) { - bytestream = new bitjs.io.ByteStream(bytes); + bytestream = new ByteStream(bytes); } else { bytestream.push(bytes); } if (unarchiveState === UnarchiveState.NOT_STARTED) { - currentFilename = ""; + currentFilename = ''; currentFileNumber = 0; currentBytesUnarchivedInFile = 0; currentBytesUnarchived = 0; @@ -180,7 +190,7 @@ onmessage = function (event) { totalFilesInArchive = 0; allLocalFiles = []; - postMessage({ type: 'start' }); + hostPort.postMessage({ type: 'start' }); unarchiveState = UnarchiveState.UNARCHIVING; @@ -192,7 +202,7 @@ onmessage = function (event) { try { untar(); unarchiveState = UnarchiveState.FINISHED; - postMessage({ type: 'finish', metadata: {} }); + hostPort.postMessage({ type: 'finish', metadata: {} }); } catch (e) { if (typeof e === 'string' && e.startsWith('Error! Overflowed')) { // Overrun the buffer. @@ -205,3 +215,35 @@ onmessage = function (event) { } } }; + +/** + * Connect the host to the untar implementation with the given MessagePort. + * @param {MessagePort} port + */ +export function connect(port) { + if (hostPort) { + 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 473de5b..215fe23 100644 --- a/archive/unzip.js +++ b/archive/unzip.js @@ -12,10 +12,11 @@ * DEFLATE format: http://tools.ietf.org/html/rfc1951 */ -// This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bitstream-worker.js'); -importScripts('../io/bytebuffer-worker.js'); -importScripts('../io/bytestream-worker.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'; +import { inflate } from './inflate.js'; const UnarchiveState = { NOT_STARTED: 0, @@ -24,8 +25,12 @@ const UnarchiveState = { FINISHED: 3, }; +/** @type {MessagePort} */ +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; @@ -40,13 +45,13 @@ let totalFilesInArchive = 0; // Helper functions. const info = function (str) { - postMessage({ type: 'info', msg: str }); + hostPort.postMessage({ type: 'info', msg: str }); }; const err = function (str) { - postMessage({ type: 'error', msg: str }); + hostPort.postMessage({ type: 'error', msg: str }); }; const postProgress = function () { - postMessage({ + hostPort.postMessage({ type: 'progress', currentFilename, currentFileNumber, @@ -58,14 +63,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, @@ -73,9 +70,7 @@ const BIT = [0x01, 0x02, 0x04, 0x08, 0x1000, 0x2000, 0x4000, 0x8000]; class ZipLocalFile { - /** - * @param {bitjs.io.ByteStream} bstream - */ + /** @param {ByteStream} bstream */ constructor(bstream) { if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function () { }) { return null; @@ -123,9 +118,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); } @@ -133,7 +128,7 @@ class ZipLocalFile { // Copy all the read bytes into a buffer and examine the last 16 bytes to see if they are the // data descriptor. let bufferedByteArr = savedBstream.peekBytes(numBytesSeeked); - const descriptorStream = new bitjs.io.ByteStream(bufferedByteArr.buffer, numBytesSeeked - 16, 16); + const descriptorStream = new ByteStream(bufferedByteArr.buffer, numBytesSeeked - 16, 16); const maybeDescriptorSig = descriptorStream.readNumber(4); const maybeCrc32 = descriptorStream.readNumber(4); const maybeCompressedSize = descriptorStream.readNumber(4); @@ -141,7 +136,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; @@ -182,7 +177,7 @@ class ZipLocalFile { } // determine what kind of compressed data we have and decompress - unzip() { + async unzip() { if (!this.fileData) { err('unzip() called on a file with out compressed file data'); } @@ -200,7 +195,7 @@ class ZipLocalFile { if (logToConsole) { info(`ZIP v2.0, DEFLATE: ${this.filename} (${this.compressedSize} bytes)`); } - this.fileData = inflate(this.fileData, this.uncompressedSize); + this.fileData = await inflate(this.fileData, this.uncompressedSize); } else { err(`UNSUPPORTED VERSION/FORMAT: ZIP v${this.version}, ` + @@ -211,400 +206,11 @@ class ZipLocalFile { } } -/** - * @typedef SymbolLengthPair - * @property {number} length - * @property {number} symbol - */ - -/** - * Returns a table of Huffman codes. Each entry's key is its code and its value is a JavaScript - * object containing {length: 6, symbol: X}. - * @param {number[]} bitLengths An array representing the bit lengths of the codes, in order. - * See section 3.2.2 of https://datatracker.ietf.org/doc/html/rfc1951. - * @returns {Map} - */ -function getHuffmanCodes(bitLengths) { - // ensure bitLengths is an array containing at least one element - if (typeof bitLengths != typeof [] || bitLengths.length < 1) { - err('Error! getHuffmanCodes() called with an invalid array'); - return null; - } - - // Reference: http://tools.ietf.org/html/rfc1951#page-8 - const numLengths = bitLengths.length; - const bl_count = []; - let MAX_BITS = 1; - - // Step 1: count up how many codes of each length we have - for (let i = 0; i < numLengths; ++i) { - const length = bitLengths[i]; - // test to ensure each bit length is a positive, non-zero number - if (typeof length != typeof 1 || length < 0) { - err(`bitLengths contained an invalid number in getHuffmanCodes(): ${length} of type ${typeof length}`); - return null; - } - // increment the appropriate bitlength count - if (bl_count[length] == undefined) bl_count[length] = 0; - // a length of zero means this symbol is not participating in the huffman coding - if (length > 0) bl_count[length]++; - if (length > MAX_BITS) MAX_BITS = length; - } - - // Step 2: Find the numerical value of the smallest code for each code length - const next_code = []; - let code = 0; - for (let bits = 1; bits <= MAX_BITS; ++bits) { - const length = bits - 1; - // ensure undefined lengths are zero - if (bl_count[length] == undefined) bl_count[length] = 0; - code = (code + bl_count[bits - 1]) << 1; - next_code[bits] = code; - } - - // Step 3: Assign numerical values to all codes - /** @type Map */ - const table = new Map(); - for (let n = 0; n < numLengths; ++n) { - const len = bitLengths[n]; - if (len != 0) { - table.set(next_code[len], { length: len, symbol: n }); - next_code[len]++; - } - } - - return table; -} - -/* - The Huffman codes for the two alphabets are fixed, and are not - represented explicitly in the data. The Huffman code lengths - for the literal/length alphabet are: - - Lit Value Bits Codes - --------- ---- ----- - 0 - 143 8 00110000 through - 10111111 - 144 - 255 9 110010000 through - 111111111 - 256 - 279 7 0000000 through - 0010111 - 280 - 287 8 11000000 through - 11000111 -*/ -// fixed Huffman codes go from 7-9 bits, so we need an array whose index can hold up to 9 bits -let fixedHCtoLiteral = null; -let fixedHCtoDistance = null; -/** @returns {Map} */ -function getFixedLiteralTable() { - // create once - if (!fixedHCtoLiteral) { - const bitlengths = new Array(288); - for (let i = 0; i <= 143; ++i) bitlengths[i] = 8; - for (let i = 144; i <= 255; ++i) bitlengths[i] = 9; - for (let i = 256; i <= 279; ++i) bitlengths[i] = 7; - for (let i = 280; i <= 287; ++i) bitlengths[i] = 8; - - // get huffman code table - fixedHCtoLiteral = getHuffmanCodes(bitlengths); - } - return fixedHCtoLiteral; -} - -/** @returns {Map} */ -function getFixedDistanceTable() { - // create once - if (!fixedHCtoDistance) { - const bitlengths = new Array(32); - for (let i = 0; i < 32; ++i) { bitlengths[i] = 5; } - - // get huffman code table - fixedHCtoDistance = getHuffmanCodes(bitlengths); - } - return fixedHCtoDistance; -} - -/** - * Extract one bit at a time until we find a matching Huffman Code - * then return that symbol. - * @param {bitjs.io.BitStream} bstream - * @param {Map} hcTable - * @returns {number} - */ -function decodeSymbol(bstream, hcTable) { - let code = 0; - let len = 0; - - // loop until we match - for (; ;) { - // read in next bit - const bit = bstream.readBits(1); - code = (code << 1) | bit; - ++len; - - // check against Huffman Code table and break if found - if (hcTable.has(code) && hcTable.get(code).length == len) { - break; - } - if (len > hcTable.length) { - err(`Bit stream out of sync, didn't find a Huffman Code, length was ${len} ` + - `and table only max code length of ${hcTable.length}`); - break; - } - } - return hcTable.get(code).symbol; -} - - -const CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; - -/* - Extra Extra Extra -Code Bits Length(s) Code Bits Lengths Code Bits Length(s) ----- ---- ------ ---- ---- ------- ---- ---- ------- - 257 0 3 267 1 15,16 277 4 67-82 - 258 0 4 268 1 17,18 278 4 83-98 - 259 0 5 269 2 19-22 279 4 99-114 - 260 0 6 270 2 23-26 280 4 115-130 - 261 0 7 271 2 27-30 281 5 131-162 - 262 0 8 272 2 31-34 282 5 163-194 - 263 0 9 273 3 35-42 283 5 195-226 - 264 0 10 274 3 43-50 284 5 227-257 - 265 1 11,12 275 3 51-58 285 0 258 - 266 1 13,14 276 3 59-66 -*/ -const LengthLookupTable = [ - [0, 3], [0, 4], [0, 5], [0, 6], - [0, 7], [0, 8], [0, 9], [0, 10], - [1, 11], [1, 13], [1, 15], [1, 17], - [2, 19], [2, 23], [2, 27], [2, 31], - [3, 35], [3, 43], [3, 51], [3, 59], - [4, 67], [4, 83], [4, 99], [4, 115], - [5, 131], [5, 163], [5, 195], [5, 227], - [0, 258] -]; - -/* - Extra Extra Extra - Code Bits Dist Code Bits Dist Code Bits Distance - ---- ---- ---- ---- ---- ------ ---- ---- -------- - 0 0 1 10 4 33-48 20 9 1025-1536 - 1 0 2 11 4 49-64 21 9 1537-2048 - 2 0 3 12 5 65-96 22 10 2049-3072 - 3 0 4 13 5 97-128 23 10 3073-4096 - 4 1 5,6 14 6 129-192 24 11 4097-6144 - 5 1 7,8 15 6 193-256 25 11 6145-8192 - 6 2 9-12 16 7 257-384 26 12 8193-12288 - 7 2 13-16 17 7 385-512 27 12 12289-16384 - 8 3 17-24 18 8 513-768 28 13 16385-24576 - 9 3 25-32 19 8 769-1024 29 13 24577-32768 -*/ -const DistLookupTable = [ - [0, 1], [0, 2], [0, 3], [0, 4], - [1, 5], [1, 7], - [2, 9], [2, 13], - [3, 17], [3, 25], - [4, 33], [4, 49], - [5, 65], [5, 97], - [6, 129], [6, 193], - [7, 257], [7, 385], - [8, 513], [8, 769], - [9, 1025], [9, 1537], - [10, 2049], [10, 3073], - [11, 4097], [11, 6145], - [12, 8193], [12, 12289], - [13, 16385], [13, 24577] -]; - -/** - * @param {bitjs.io.BitStream} bstream - * @param {Map} hcLiteralTable - * @param {Map} hcDistanceTable - * @param {bitjs.io.ByteBuffer} buffer - * @returns - */ -function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { - /* - loop (until end of block code recognized) - decode literal/length value from input stream - if value < 256 - copy value (literal byte) to output stream - otherwise - if value = end of block (256) - break from loop - otherwise (value = 257..285) - decode distance from input stream - - move backwards distance bytes in the output - stream, and copy length bytes from this - position to the output stream. - */ - let blockSize = 0; - for (; ;) { - const symbol = decodeSymbol(bstream, hcLiteralTable); - if (symbol < 256) { - // copy literal byte to output - buffer.insertByte(symbol); - blockSize++; - } else { - // end of block reached - if (symbol == 256) { - break; - } else { - const lengthLookup = LengthLookupTable[symbol - 257]; - let length = lengthLookup[1] + bstream.readBits(lengthLookup[0]); - const distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)]; - let distance = distLookup[1] + bstream.readBits(distLookup[0]); - - // now apply length and distance appropriately and copy to output - - // TODO: check that backward distance < data.length? - - // http://tools.ietf.org/html/rfc1951#page-11 - // "Note also that the referenced string may overlap the current - // position; for example, if the last 2 bytes decoded have values - // X and Y, a string reference with - // adds X,Y,X,Y,X to the output stream." - // - // loop for each character - let ch = buffer.ptr - distance; - blockSize += length; - if (length > distance) { - const data = buffer.data; - while (length--) { - buffer.insertByte(data[ch++]); - } - } else { - buffer.insertBytes(buffer.data.subarray(ch, ch + length)) - } - } // length-distance pair - } // length-distance pair or end-of-block - } // loop until we reach end of block - return blockSize; -} - -/** - * Compression method 8. Deflate: http://tools.ietf.org/html/rfc1951 - * @param {Uint8Array} compressedData A Uint8Array of the compressed file data. - * @param {number} numDecompressedBytes - * @returns {Uint8Array} The decompressed array. - */ -function inflate(compressedData, numDecompressedBytes) { - // Bit stream representing the compressed data. - /** @type {bitjs.io.BitStream} */ - const bstream = new bitjs.io.BitStream(compressedData.buffer, - false /* mtl */, - compressedData.byteOffset, - compressedData.byteLength); - /** @type {bitjs.io.ByteBuffer} */ - const buffer = new bitjs.io.ByteBuffer(numDecompressedBytes); - let blockSize = 0; - - // block format: http://tools.ietf.org/html/rfc1951#page-9 - let bFinal = 0; - do { - bFinal = bstream.readBits(1); - let bType = bstream.readBits(2); - blockSize = 0; - // no compression - if (bType == 0) { - // skip remaining bits in this byte - while (bstream.bitPtr != 0) bstream.readBits(1); - const len = bstream.readBits(16); - const nlen = bstream.readBits(16); - // TODO: check if nlen is the ones-complement of len? - if (len > 0) buffer.insertBytes(bstream.readBytes(len)); - blockSize = len; - } - // fixed Huffman codes - else if (bType == 1) { - blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); - } - // dynamic Huffman codes - else if (bType == 2) { - const numLiteralLengthCodes = bstream.readBits(5) + 257; - const numDistanceCodes = bstream.readBits(5) + 1; - const numCodeLengthCodes = bstream.readBits(4) + 4; - - // populate the array of code length codes (first de-compaction) - const codeLengthsCodeLengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for (let i = 0; i < numCodeLengthCodes; ++i) { - codeLengthsCodeLengths[CodeLengthCodeOrder[i]] = bstream.readBits(3); - } - - // get the Huffman Codes for the code lengths - const codeLengthsCodes = getHuffmanCodes(codeLengthsCodeLengths); - - // now follow this mapping - /* - 0 - 15: Represent code lengths of 0 - 15 - 16: Copy the previous code length 3 - 6 times. - The next 2 bits indicate repeat length - (0 = 3, ... , 3 = 6) - Example: Codes 8, 16 (+2 bits 11), - 16 (+2 bits 10) will expand to - 12 code lengths of 8 (1 + 6 + 5) - 17: Repeat a code length of 0 for 3 - 10 times. - (3 bits of length) - 18: Repeat a code length of 0 for 11 - 138 times - (7 bits of length) - */ - // to generate the true code lengths of the Huffman Codes for the literal - // and distance tables together - const literalCodeLengths = []; - let prevCodeLength = 0; - const maxCodeLengths = numLiteralLengthCodes + numDistanceCodes; - while (literalCodeLengths.length < maxCodeLengths) { - const symbol = decodeSymbol(bstream, codeLengthsCodes); - if (symbol <= 15) { - literalCodeLengths.push(symbol); - prevCodeLength = symbol; - } else if (symbol === 16) { - let repeat = bstream.readBits(2) + 3; - while (repeat--) { - literalCodeLengths.push(prevCodeLength); - } - } else if (symbol === 17) { - let repeat = bstream.readBits(3) + 3; - while (repeat--) { - literalCodeLengths.push(0); - } - } else if (symbol == 18) { - let repeat = bstream.readBits(7) + 11; - while (repeat--) { - literalCodeLengths.push(0); - } - } - } - - // now split the distance code lengths out of the literal code array - const distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes); - - // now generate the true Huffman Code tables using these code lengths - const hcLiteralTable = getHuffmanCodes(literalCodeLengths); - const hcDistanceTable = getHuffmanCodes(distanceCodeLengths); - blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); - } else { // error - err('Error! Encountered deflate block of type 3'); - return null; - } - - // update progress - currentBytesUnarchivedInFile += blockSize; - currentBytesUnarchived += blockSize; - postProgress(); - } while (bFinal != 1); - // we are done reading blocks if the bFinal bit was set for this block - - // return the buffer data bytes - return buffer.data; -} - -function archiveUnzip() { +async 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); @@ -623,10 +229,10 @@ function archiveUnzip() { currentBytesUnarchivedInFile = 0; // Actually do the unzipping. - oneLocalFile.unzip(); + await oneLocalFile.unzip(); if (oneLocalFile.fileData != null) { - postMessage({ type: 'extract', unarchivedFile: oneLocalFile }, [oneLocalFile.fileData.buffer]); + hostPort.postMessage({ type: 'extract', unarchivedFile: oneLocalFile }, [oneLocalFile.fileData.buffer]); postProgress(); } } @@ -634,7 +240,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'); } @@ -647,13 +253,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), @@ -686,7 +292,7 @@ function archiveUnzip() { } // digital signature - if (bstream.peekNumber(4) == zDigitalSignatureSignature) { + if (bstream.peekNumber(4) == DIGITAL_SIGNATURE_SIG) { if (logToConsole) { info(' Found a Digital Signature'); } @@ -697,7 +303,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), @@ -723,18 +329,18 @@ function archiveUnzip() { bytestream = bstream.tee(); unarchiveState = UnarchiveState.FINISHED; - postMessage({ type: 'finish', metadata }); + hostPort.postMessage({ type: 'finish', metadata }); } // event.data.file has the first ArrayBuffer. // event.data.bytes has all subsequent ArrayBuffers. -onmessage = function (event) { +const onmessage = async function (event) { const bytes = event.data.file || event.data.bytes; logToConsole = !!event.data.logToConsole; // This is the very first time we have been called. Initialize the bytestream. if (!bytestream) { - bytestream = new bitjs.io.ByteStream(bytes); + bytestream = new ByteStream(bytes); } else { bytestream.push(bytes); } @@ -749,7 +355,7 @@ onmessage = function (event) { currentBytesUnarchived = 0; allLocalFiles = []; - postMessage({ type: 'start' }); + hostPort.postMessage({ type: 'start' }); unarchiveState = UnarchiveState.UNARCHIVING; @@ -757,9 +363,9 @@ onmessage = function (event) { } if (unarchiveState === UnarchiveState.UNARCHIVING || - unarchiveState === UnarchiveState.WAITING) { + unarchiveState === UnarchiveState.WAITING) { try { - archiveUnzip(); + await archiveUnzip(); } catch (e) { if (typeof e === 'string' && e.startsWith('Error! Overflowed')) { // Overrun the buffer. @@ -772,3 +378,37 @@ onmessage = function (event) { } } }; + +/** + * Connect the host to the unzip implementation with the given MessagePort. + * @param {MessagePort} port + */ +export function connect(port) { + if (hostPort) { + 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 new file mode 100644 index 0000000..db6ca2b --- /dev/null +++ b/archive/webworker-wrapper.js @@ -0,0 +1,27 @@ +/** + * webworker-wrapper.js + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +/** + * A WebWorker wrapper for a decompress/compress implementation. Upon creation and being sent its + * first message, it dynamically imports the decompressor / compressor implementation and connects + * the message port. All other communication takes place over the MessageChannel. + */ + +/** @type {MessagePort} */ +let implPort; + +let module; + +onmessage = async (evt) => { + if (evt.data.implSrc) { + 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 e0c4108..cbcc46c 100644 --- a/archive/zip.js +++ b/archive/zip.js @@ -11,44 +11,38 @@ * DEFLATE format: http://tools.ietf.org/html/rfc1951 */ -// This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bitstream-worker.js'); -importScripts('../io/bytebuffer-worker.js'); -importScripts('../io/bytestream-worker.js'); +import { ByteBuffer } from '../io/bytebuffer.js'; +import { CENTRAL_FILE_HEADER_SIG, CRC32_MAGIC_NUMBER, END_OF_CENTRAL_DIR_SIG, + LOCAL_FILE_HEADER_SIG, ZipCompressionMethod } from './common.js'; + +/** @typedef {import('./common.js').FileInfo} FileInfo */ + +/** @type {MessagePort} */ +let hostPort; /** - * The client sends messages to this Worker containing files to archive in order. The client - * indicates to the Worker when the last file has been sent to be compressed. + * The client sends a set of CompressFilesMessage to the MessagePort containing files to archive in + * order. The client sets isLastFile to true to indicate to the implementation when the last file + * has been sent to be compressed. * - * The Worker emits an event to indicate compression has started: { type: 'start' } - * As the files compress, bytes are sent back in order: { type: 'compress', bytes: Uint8Array } - * After the last file compresses, the Worker indicates finish by: { type 'finish' } + * The impl posts an event to the port indicating compression has started: { type: 'start' }. + * As each file compresses, bytes are sent back in order: { type: 'compress', bytes: Uint8Array }. + * After the last file compresses, we indicate finish by: { type 'finish' } * - * Clients should append the bytes to a single buffer in the order they were received. + * The client should append the bytes to a single buffer in the order they were received. */ +// TODO(bitjs): Figure out where this typedef should live. /** - * @typedef FileInfo An object that is sent to this worker 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. + * @typedef CompressFilesMessage A message the client sends to the implementation. + * @property {FileInfo[]} files A set of files to add to the zip file. + * @property {boolean} isLastFile Indicates this is the last set of files to add to the zip file. + * @property {ZipCompressionMethod=} compressionMethod The compression method to use. Ignored except + * for the first message sent. */ -// TODO: Support DEFLATE. // TODO: Support options that can let client choose levels of compression/performance. -/** - * Ideally these constants should be defined in a common isomorphic ES module. Unfortunately, the - * state of JavaScript is such that modules cannot be shared easily across browsers, worker threads, - * NodeJS environments, etc yet. Thus, these constants, as well as logic that should be extracted to - * common modules and shared with unzip.js are not yet easily possible. - */ - -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 @@ -61,6 +55,9 @@ const zCRC32MagicNumber = 0xedb88320; // 0xdebb20e3; * @property {number} byteOffset (4 bytes) */ +/** @type {ZipCompressionMethod} */ +let compressionMethod = ZipCompressionMethod.STORE; + /** @type {FileInfo[]} */ let filesCompressed = []; @@ -77,7 +74,6 @@ const CompressorState = { FINISHED: 3, }; let state = CompressorState.NOT_STARTED; -let lastFileReceived = false; const crc32Table = createCRC32Table(); /** Helper functions. */ @@ -91,7 +87,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; } @@ -146,30 +142,39 @@ function dateToDosTime(jsDate) { /** * @param {FileInfo} file - * @returns {bitjs.io.ByteBuffer} + * @returns {Promise} */ -function zipOneFile(file) { +async function zipOneFile(file) { + /** @type {Uint8Array} */ + let compressedBytes; + if (compressionMethod === ZipCompressionMethod.STORE) { + compressedBytes = file.fileData; + } else if (compressionMethod === ZipCompressionMethod.DEFLATE) { + const blob = new Blob([file.fileData.buffer]); + const compressedStream = blob.stream().pipeThrough(new CompressionStream('deflate-raw')); + compressedBytes = new Uint8Array(await new Response(compressedStream).arrayBuffer()); + } + // Zip Local File Header has 30 bytes and then the filename and extrafields. const fileHeaderSize = 30 + file.fileName.length; - /** @type {bitjs.io.ByteBuffer} */ - const buffer = new bitjs.io.ByteBuffer(fileHeaderSize + file.fileData.length); + /** @type {ByteBuffer} */ + const buffer = new ByteBuffer(fileHeaderSize + compressedBytes.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. + buffer.writeNumber(compressionMethod, 2); // Compression Method. const jsDate = new Date(file.lastModTime); /** @type {CentralDirectoryFileHeaderInfo} */ const centralDirectoryInfo = { - compressionMethod: 0, + compressionMethod, lastModFileTime: dateToDosTime(jsDate), lastModFileDate: dateToDosDate(jsDate), crc32: calculateCRC32(0, file.fileData), - // TODO: For now, this is easy. Later when we do DEFLATE, we will have to calculate. - compressedSize: file.fileData.byteLength, + compressedSize: compressedBytes.byteLength, uncompressedSize: file.fileData.byteLength, fileName: file.fileName, byteOffset: numBytesWritten, @@ -184,26 +189,26 @@ function zipOneFile(file) { buffer.writeNumber(centralDirectoryInfo.fileName.length, 2); // Filename length. buffer.writeNumber(0, 2); // Extra field length. buffer.writeASCIIString(centralDirectoryInfo.fileName); // Filename. Assumes ASCII. - buffer.insertBytes(file.fileData); // File data. + buffer.insertBytes(compressedBytes); return buffer; } /** - * @returns {bitjs.io.ByteBuffer} + * @returns {ByteBuffer} */ function writeCentralFileDirectory() { // Each central directory file header is 46 bytes + the filename. let cdsLength = filesCompressed.map(f => f.fileName.length + 46).reduce((a, c) => a + c); // 22 extra bytes for the end-of-central-dir header. - const buffer = new bitjs.io.ByteBuffer(cdsLength + 22); + 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 - buffer.writeNumber(0, 2); // Compression method. + buffer.writeNumber(compressionMethod, 2); // Compression method. buffer.writeNumber(cdInfo.lastModFileTime, 2); // Last Mod File Time. buffer.writeNumber(cdInfo.lastModFileDate, 2); // Last Mod Date. buffer.writeNumber(cdInfo.crc32, 4); // crc32. @@ -220,7 +225,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. @@ -233,39 +238,73 @@ function writeCentralFileDirectory() { } /** - * @param {{data: {isLastFile?: boolean, files: FileInfo[]}}} evt The event for the Worker - * to process. It is an error to send any more events to the Worker if a previous event had - * isLastFile is set to true. + * @param {{data: CompressFilesMessage}} evt The event for the implementation to process. It is an + * error to send any more events after a previous event had isLastFile is set to true. */ -onmessage = function(evt) { +const onmessage = async function(evt) { if (state === CompressorState.FINISHED) { - throw `The zip worker was sent a message after last file received.`; + throw `The zip implementation was sent a message after last file received.`; } if (state === CompressorState.NOT_STARTED) { - postMessage({ type: 'start' }); + hostPort.postMessage({ type: 'start' }); } state = CompressorState.COMPRESSING; - /** @type {FileInfo[]} */ - const filesToCompress = evt.data.files; + if (filesCompressed.length === 0 && evt.data.compressionMethod !== undefined) { + if (!Object.values(ZipCompressionMethod).includes(evt.data.compressionMethod)) { + throw `Do not support compression method ${evt.data.compressionMethod}`; + } + + compressionMethod = evt.data.compressionMethod; + } + + const msg = evt.data; + const filesToCompress = msg.files; while (filesToCompress.length > 0) { const fileInfo = filesToCompress.shift(); - const fileBuffer = zipOneFile(fileInfo); + const fileBuffer = await zipOneFile(fileInfo); filesCompressed.push(fileInfo); numBytesWritten += fileBuffer.data.byteLength; - this.postMessage({ type: 'compress', bytes: fileBuffer.data }, [ fileBuffer.data.buffer ]); + hostPort.postMessage({ type: 'compress', bytes: fileBuffer.data }, [ fileBuffer.data.buffer ]); } if (evt.data.isLastFile) { const centralBuffer = writeCentralFileDirectory(); numBytesWritten += centralBuffer.data.byteLength; - this.postMessage({ type: 'compress', bytes: centralBuffer.data }, [ centralBuffer.data.buffer ]); + hostPort.postMessage({ type: 'compress', bytes: centralBuffer.data }, + [ centralBuffer.data.buffer ]); state = CompressorState.FINISHED; - this.postMessage({ type: 'finish' }); + hostPort.postMessage({ type: 'finish' }); } else { state = CompressorState.WAITING; } }; + + +/** + * Connect the host to the zip implementation with the given MessagePort. + * @param {MessagePort} port + */ +export function connect(port) { + if (hostPort) { + throw `hostPort already connected in zip.js`; + } + hostPort = port; + port.onmessage = onmessage; +} + +export function disconnect() { + if (!hostPort) { + throw `hostPort was not connected in zip.js`; + } + + hostPort = null; + + filesCompressed = []; + centralDirectoryInfos = []; + numBytesWritten = 0; + state = CompressorState.NOT_STARTED; +} diff --git a/build/Makefile b/build/Makefile index 5555daa..224db92 100644 --- a/build/Makefile +++ b/build/Makefile @@ -1,13 +1,9 @@ -all: bitjs-io bitjs-image-webp-shim +all: bitjs-image-webp-shim clean: - $(MAKE) -C io clean $(MAKE) -C image/webp-shim clean -bitjs-io: - $(MAKE) -C io - # Make webp-shim/ bitjs-image-webp-shim: $(MAKE) -C image/webp-shim diff --git a/build/README.md b/build/README.md index 80c886c..1f96a6a 100644 --- a/build/README.md +++ b/build/README.md @@ -1,7 +1,8 @@ # Build This folder contains files needed to build various pieces of the library. You only need to worry -about this if you intend on patching / modifying the library in some way. +about this if you intend on patching / modifying the library in some way. It is only needed for the +WebP Shim parts of bitjs. ## Prerequisites * Install Docker @@ -19,3 +20,5 @@ Assuming you have cloned the repository in /path/to/bitjs: Various library files will be output to /path/to/bitjs/. For example, the /path/to/bitjs/image/webp-shim/ directory will now contain update webp-shim-module.js and webp-shim-module.wasm files. + +# TODO(2.0): Remove this. diff --git a/build/image/webp-shim/src/webp.c b/build/image/webp-shim/src/webp.c index 5deebc2..3b3925e 100644 --- a/build/image/webp-shim/src/webp.c +++ b/build/image/webp-shim/src/webp.c @@ -9,6 +9,8 @@ * Copyright(c) 2020 Google Inc. */ +// TODO(2.0): Remove this. It seems unnecessary given WebP is universally supported now. + #include #include #include "emscripten.h" diff --git a/build/io/Makefile b/build/io/Makefile deleted file mode 100644 index dbe4c30..0000000 --- a/build/io/Makefile +++ /dev/null @@ -1,83 +0,0 @@ -OUT_PATH=../../io - -BITSTREAM_MODULE=${OUT_PATH}/bitstream.js -BITSTREAM_WORKER=${OUT_PATH}/bitstream-worker.js - -BYTESTREAM_MODULE=${OUT_PATH}/bytestream.js -BYTESTREAM_WORKER=${OUT_PATH}/bytestream-worker.js - -BITBUFFER_MODULE=${OUT_PATH}/bitbuffer.js -BITBUFFER_WORKER=${OUT_PATH}/bitbuffer-worker.js - -BYTEBUFFER_MODULE=${OUT_PATH}/bytebuffer.js -BYTEBUFFER_WORKER=${OUT_PATH}/bytebuffer-worker.js - -BITSTREAM_DEF=bitstream-def.js -BYTESTREAM_DEF=bytestream-def.js -BITBUFFER_DEF=bitbuffer-def.js -BYTEBUFFER_DEF=bytebuffer-def.js - -DISCLAIMER="// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io." - -all: ${BITSTREAM_MODULE} ${BITSTREAM_WORKER} \ - ${BYTESTREAM_MODULE} ${BYTESTREAM_WORKER} \ - ${BITBUFFER_MODULE} ${BITBUFFER_WORKER} \ - ${BYTEBUFFER_MODULE} ${BYTEBUFFER_WORKER} - -clean: - rm -rf ${BITSTREAM_MODULE} - rm -rf ${BITSTREAM_WORKER} - rm -rf ${BYTESTREAM_MODULE} - rm -rf ${BYTESTREAM_WORKER} - rm -rf ${BITBUFFER_MODULE} - rm -rf ${BITBUFFER_WORKER} - rm -rf ${BYTEBUFFER_MODULE} - rm -rf ${BYTEBUFFER_WORKER} - -${BITSTREAM_MODULE}: Makefile ${BITSTREAM_DEF} - echo ${DISCLAIMER} > ${BITSTREAM_MODULE} - echo "export const BitStream =" >> ${BITSTREAM_MODULE} - cat ${BITSTREAM_DEF} >> ${BITSTREAM_MODULE} - -${BITSTREAM_WORKER}: Makefile ${BITSTREAM_DEF} - echo ${DISCLAIMER} > ${BITSTREAM_WORKER} - echo "var bitjs = bitjs || {};" >> ${BITSTREAM_WORKER} - echo "bitjs.io = bitjs.io || {};" >> ${BITSTREAM_WORKER} - echo "bitjs.io.BitStream =" >> ${BITSTREAM_WORKER} - cat ${BITSTREAM_DEF} >> ${BITSTREAM_WORKER} - -${BYTESTREAM_MODULE}: Makefile ${BYTESTREAM_DEF} - echo ${DISCLAIMER} > ${BYTESTREAM_MODULE} - echo "export const ByteStream =" >> ${BYTESTREAM_MODULE} - cat ${BYTESTREAM_DEF} >> ${BYTESTREAM_MODULE} - -${BYTESTREAM_WORKER}: Makefile ${BYTESTREAM_DEF} - echo ${DISCLAIMER} > ${BYTESTREAM_WORKER} - echo "var bitjs = bitjs || {};" >> ${BYTESTREAM_WORKER} - echo "bitjs.io = bitjs.io || {};" >> ${BYTESTREAM_WORKER} - echo "bitjs.io.ByteStream =" >> ${BYTESTREAM_WORKER} - cat ${BYTESTREAM_DEF} >> ${BYTESTREAM_WORKER} - -${BITBUFFER_MODULE}: Makefile ${BITBUFFER_DEF} - echo ${DISCLAIMER} > ${BITBUFFER_MODULE} - echo "export const BitBuffer =" >> ${BITBUFFER_MODULE} - cat ${BITBUFFER_DEF} >> ${BITBUFFER_MODULE} - -${BITBUFFER_WORKER}: Makefile ${BITBUFFER_DEF} - echo ${DISCLAIMER} > ${BITBUFFER_WORKER} - echo "var bitjs = bitjs || {};" >> ${BITBUFFER_WORKER} - echo "bitjs.io = bitjs.io || {};" >> ${BITBUFFER_WORKER} - echo "bitjs.io.BitBuffer =" >> ${BITBUFFER_WORKER} - cat ${BITBUFFER_DEF} >> ${BITBUFFER_WORKER} - -${BYTEBUFFER_MODULE}: Makefile ${BYTEBUFFER_DEF} - echo ${DISCLAIMER} > ${BYTEBUFFER_MODULE} - echo "export const ByteBuffer =" >> ${BYTEBUFFER_MODULE} - cat ${BYTEBUFFER_DEF} >> ${BYTEBUFFER_MODULE} - -${BYTEBUFFER_WORKER}: Makefile ${BYTEBUFFER_DEF} - echo ${DISCLAIMER} > ${BYTEBUFFER_WORKER} - echo "var bitjs = bitjs || {};" >> ${BYTEBUFFER_WORKER} - echo "bitjs.io = bitjs.io || {};" >> ${BYTEBUFFER_WORKER} - echo "bitjs.io.ByteBuffer =" >> ${BYTEBUFFER_WORKER} - cat ${BYTEBUFFER_DEF} >> ${BYTEBUFFER_WORKER} diff --git a/build/io/bitbuffer-def.js b/build/io/bitbuffer-def.js deleted file mode 100644 index 55c45b3..0000000 --- a/build/io/bitbuffer-def.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - * bytebuffer-def.js - * - * Provides a writer for bits. - * - * Licensed under the MIT License - * - * Copyright(c) 2021 Google Inc. - */ - -(function () { - const BITMASK = [ - 0, - 0b00000001, - 0b00000011, - 0b00000111, - 0b00001111, - 0b00011111, - 0b00111111, - 0b01111111, - 0b11111111, - ] - - /** - * A write-only Bit buffer which uses a Uint8Array as a backing store. - */ - class BitBuffer { - /** - * @param {number} numBytes The number of bytes to allocate. - * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to - * least-significant (0). Defaults false: least-significant (0) to most-significant (8). - */ - constructor(numBytes, mtl = false) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); - - /** - * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most - * significant bit packing). - * @type {boolean} - * @private - */ - this.mtl = mtl; - - /** - * The current byte we are filling with bits. - * @type {number} - * @public - */ - this.bytePtr = 0; - - /** - * Points at the bit within the current byte where the next bit will go. This number ranges - * from 0 to 7 and the direction of packing is indicated by the mtl property. - * @type {number} - * @public - */ - this.bitPtr = this.mtl ? 7 : 0; - } - - /** @returns {boolean} */ - getPackingDirection() { - return this.mtl; - } - - /** - * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to - * most-significant (7). Changing the bit-packing direction when the bit pointer is in the - * middle of a byte will fill the rest of that byte with 0s using the current bit-packing - * direction and then set the bit pointer to the appropriate bit of the next byte. If there - * are no more bytes left in this buffer, it will throw an error. - */ - setPackingDirection(mtl = false) { - if (this.mtl !== mtl) { - if (this.mtl && this.bitPtr !== 7) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 7; - } else if (!this.mtl && this.bitPtr !== 0) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 0; - } - } - - this.mtl = mtl; - } - - /** - * writeBits(3, 6) is the same as writeBits(0b000011, 6). - * Will throw an error (without writing) if this would over-flow the buffer. - * @param {number} val The bits to pack into the buffer. Negative values are not allowed. - * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since - * JavaScript can only support 53-bit integers. - */ - writeBits(val, numBits) { - if (val < 0 || typeof val !== typeof 1) { - throw `Trying to write an invalid value into the BitBuffer: ${val}`; - } - if (numBits < 0 || numBits > 53) { - throw `Trying to write ${numBits} bits into the BitBuffer`; - } - - const totalBitsInBuffer = this.data.byteLength * 8; - const writtenBits = this.bytePtr * 8 + this.bitPtr; - const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; - if (numBits > bitsLeftInBuffer) { - throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; - } - - // Least-to-most-significant bit packing method (LTM). - if (!this.mtl) { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = 8 - this.bitPtr; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in LTM bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at LSB. */ - let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - actualBitsToWrite <<= this.bitPtr; - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; - } - // Update the bit/byte pointers and remaining bits to write. - this.bitPtr += numBitsToWriteIntoThisByte; - if (this.bitPtr > 7) { - if (this.bitPtr !== 8) { - throw `Error in LTM bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 0; - } - // Remove bits that have been written from LSB end. - val >>= numBitsToWriteIntoThisByte; - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; - } - } - // Most-to-least-significant bit packing method (MTL). - else { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = this.bitPtr + 1; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in MTL bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at MSB. */ - let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - // If the number of bits left to write do not fill up this byte, we need to shift these - // bits to the left so they are written into the proper place in the buffer. - if (numBitsLeftToWrite < bitCapacityInThisByte) { - actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); - } - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; - } - // Update the bit/byte pointers and remaining bits to write - this.bitPtr -= numBitsToWriteIntoThisByte; - if (this.bitPtr < 0) { - if (this.bitPtr !== -1) { - throw `Error in MTL bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 7; - } - // Remove bits that have been written from MSB end. - val -= (actualBitsToWrite << numExcessBits); - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; - } - } - } - } - - return BitBuffer; -})(); diff --git a/build/io/bitstream-def.js b/build/io/bitstream-def.js deleted file mode 100644 index f73ec97..0000000 --- a/build/io/bitstream-def.js +++ /dev/null @@ -1,312 +0,0 @@ -/* - * bitstream-def.js - * - * Provides readers for bitstreams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - // mask for getting N number of bits (0-8) - const BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; - - /** - * This object allows you to peek and consume bits and bytes out of a stream. - * Note that this stream is optimized, and thus, will *NOT* throw an error if - * the end of the stream is reached. Only use this in scenarios where you - * already have all the bits you need. - * - * Bit reading always proceeds from the first byte in the buffer, to the - * second byte, and so on. The MTL flag controls which bit is considered - * first *inside* the byte. - * - * An Example for how Most-To-Least vs Least-to-Most mode works: - * - * If you have an ArrayBuffer with the following two Uint8s: - * 185 (0b10111001) and 66 (0b01000010) - * and you perform a series of readBits: 2 bits, then 3, then 5, then 6. - * - * A BitStream in "mtl" mode will yield the following: - * - readBits(2) => 2 ('10') - * - readBits(3) => 7 ('111') - * - readBits(5) => 5 ('00101') - * - readBits(6) => 2 ('000010') - * - * A BitStream in "ltm" mode will yield the following: - * - readBits(2) => 1 ('01') - * - readBits(3) => 6 ('110') - * - readBits(5) => 21 ('10101') - * - readBits(6) => 16 ('010000') - */ - class BitStream { - /** - * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} mtl Whether the stream reads bits from the byte starting with the - * most-significant-bit (bit 7) to least-significant (bit 0). False means the direction is - * from least-significant-bit (bit 0) to most-significant (bit 7). - * @param {Number} opt_offset The offset into the ArrayBuffer - * @param {Number} opt_length The length of this BitStream - */ - constructor(ab, mtl, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The byte in the stream that we are currently on. - * @type {Number} - * @private - */ - this.bytePtr = 0; - - /** - * The bit in the current byte that we will read next (can have values 0 through 7). - * @type {Number} - * @private - */ - this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bitsRead_ = 0; - - this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; - } - - /** - * Returns how many bites have been read in the stream since the beginning of time. - * @returns {number} - */ - getNumBitsRead() { - return this.bitsRead_; - } - - /** - * Returns how many bits are currently in the stream left to be read. - * @returns {number} - */ - getNumBitsLeft() { - const bitsLeftInByte = 8 - this.bitPtr; - return (this.bytes.byteLength - this.bytePtr - 1) * 8 + bitsLeftInByte; - } - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek, must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_ltm(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } - - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - let bitsIn = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return what we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - const mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bytePtr++; - bitPtr = 0; - bitsIn += numBitsLeftInThisByte; - num -= numBitsLeftInThisByte; - } else { - const mask = (BITMASK[num] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bitPtr += num; - break; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; - } - - return result; - } - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_mtl(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } - - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return the bits we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - result <<= numBitsLeftInThisByte; - result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); - bytePtr++; - bitPtr = 0; - num -= numBitsLeftInThisByte; - } else { - result <<= num; - const numBits = 8 - num - bitPtr; - result |= ((bytes[bytePtr] & (BITMASK[num] << numBits)) >> numBits); - - bitPtr += num; - break; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; - } - - return result; - } - - /** - * Peek at 16 bits from current position in the buffer. - * Bit at (bytePtr,bitPtr) has the highest position in returning data. - * Taken from getbits.hpp in unrar. - * TODO: Move this out of BitStream and into unrar. - * @returns {number} - */ - getBits() { - return (((((this.bytes[this.bytePtr] & 0xff) << 16) + - ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + - ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); - } - - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. Must be a positive integer. - * @returns {number} The read bits, as an unsigned number. - */ - readBits(n) { - return this.peekBits(n, true); - } - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, opt_movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer: ' + n; - } else if (num === 0) { - return new Uint8Array(); - } - - // Flush bits until we are byte-aligned. - // from http://tools.ietf.org/html/rfc1951#page-11 - // "Any bits of input up to the next byte boundary are ignored." - while (this.bitPtr != 0) { - this.readBits(1); - } - - const numBytesLeft = this.getNumBitsLeft() / 8; - if (num > numBytesLeft) { - throw 'Error! Overflowed the bit stream! n=' + num + ', bytePtr=' + this.bytePtr + - ', bytes.length=' + this.bytes.length + ', bitPtr=' + this.bitPtr; - } - - const movePointers = opt_movePointers || false; - const result = new Uint8Array(num); - let bytes = this.bytes; - let ptr = this.bytePtr; - let bytesLeftToCopy = num; - while (bytesLeftToCopy > 0) { - const bytesLeftInStream = bytes.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInStream); - - result.set(bytes.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); - - ptr += sourceLength; - // Overflowed the stream, just return what we got. - if (ptr >= bytes.length) { - break; - } - - bytesLeftToCopy -= sourceLength; - } - - if (movePointers) { - this.bytePtr += num; - this.bitsRead_ += (num * 8); - } - - return result; - } - - /** - * @param {number} n The number of bytes to read. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); - } - } - - return BitStream; -})(); diff --git a/build/io/bytebuffer-def.js b/build/io/bytebuffer-def.js deleted file mode 100644 index 4f010f8..0000000 --- a/build/io/bytebuffer-def.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * bytebuffer-def.js - * - * Provides a writer for bytes. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - /** - * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. - */ - class ByteBuffer { - /** - * @param {number} numBytes The number of bytes to allocate. - */ - constructor(numBytes) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); - - /** - * @type {number} - * @public - */ - this.ptr = 0; - } - - - /** - * @param {number} b The byte to insert. - */ - insertByte(b) { - // TODO: throw if byte is invalid? - this.data[this.ptr++] = b; - } - - /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. - */ - insertBytes(bytes) { - // TODO: throw if bytes is invalid? - this.data.set(bytes, this.ptr); - this.ptr += bytes.length; - } - - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeNumber(num, numBytes) { - if (numBytes < 1 || !numBytes) { - throw 'Trying to write into too few bytes: ' + numBytes; - } - if (num < 0) { - throw 'Trying to write a negative number (' + num + - ') as an unsigned number to an ArrayBuffer'; - } - if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } - - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - } - - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeSignedNumber(num, numBytes) { - if (numBytes < 1) { - throw 'Trying to write into too few bytes: ' + numBytes; - } - - const HALF = Math.pow(2, (numBytes * 8) - 1); - if (num >= HALF || num < -HALF) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } - - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - } - - /** - * @param {string} str The ASCII string to write. - */ - writeASCIIString(str) { - for (let i = 0; i < str.length; ++i) { - const curByte = str.charCodeAt(i); - if (curByte < 0 || curByte > 255) { - throw 'Trying to write a non-ASCII string!'; - } - this.insertByte(curByte); - } - } - } - - return ByteBuffer; -})(); diff --git a/build/io/bytestream-def.js b/build/io/bytestream-def.js deleted file mode 100644 index 888d2cb..0000000 --- a/build/io/bytestream-def.js +++ /dev/null @@ -1,308 +0,0 @@ -/* - * bytestream-def.js - * - * Provides readers for byte streams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - /** - * This object allows you to peek and consume bytes as numbers and strings out - * of a stream. More bytes can be pushed into the back of the stream via the - * push() method. - */ - class ByteStream { - /** - * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} opt_offset The offset into the ArrayBuffer - * @param {number=} opt_length The length of this ByteStream - */ - constructor(ab, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The current page of bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The next pages of bytes in the stream. - * @type {Array} - * @private - */ - this.pages_ = []; - - /** - * The byte in the current page that we will read next. - * @type {Number} - * @private - */ - this.ptr = 0; - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bytesRead_ = 0; - } - - /** - * Returns how many bytes have been read in the stream since the beginning of time. - */ - getNumBytesRead() { - return this.bytesRead_; - } - - /** - * Returns how many bytes are currently in the stream left to be read. - */ - getNumBytesLeft() { - const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); - return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); - } - - /** - * Move the pointer ahead n bytes. If the pointer is at the end of the current array - * of bytes and we have another page of bytes, point at the new page. This is a private - * method, no validation is done. - * @param {number} n Number of bytes to increment. - * @private - */ - movePointer_(n) { - this.ptr += n; - this.bytesRead_ += n; - while (this.ptr >= this.bytes.length && this.pages_.length > 0) { - this.ptr -= this.bytes.length; - this.bytes = this.pages_.shift(); - } - } - - /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - peekNumber(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekNumber() with a non-positive integer'; - } else if (num === 0) { - return 0; - } - - if (n > 4) { - throw 'Error! Called peekNumber(' + n + - ') but this method can only reliably read numbers up to 4 bytes long'; - } - - if (this.getNumBytesLeft() < num) { - throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - let result = 0; - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result |= (curPage[ptr++] << (i * 8)); - - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } - - return result; - } - - - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - readNumber(n) { - const num = this.peekNumber(n); - this.movePointer_(n); - return num; - } - - - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - peekSignedNumber(n) { - let num = this.peekNumber(n); - const HALF = Math.pow(2, (n * 8) - 1); - const FULL = HALF * 2; - - if (num >= HALF) num -= FULL; - - return num; - } - - - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - readSignedNumber(n) { - const num = this.peekSignedNumber(n); - this.movePointer_(n); - return num; - } - - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @param {boolean} movePointers Whether to move the pointers. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer'; - } else if (num === 0) { - return new Uint8Array(); - } - - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - const result = new Uint8Array(num); - let curPage = this.bytes; - let ptr = this.ptr; - let bytesLeftToCopy = num; - let pageIndex = 0; - while (bytesLeftToCopy > 0) { - const bytesLeftInPage = curPage.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); - - result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); - - ptr += sourceLength; - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - - bytesLeftToCopy -= sourceLength; - } - - if (movePointers) { - this.movePointer_(num); - } - - return result; - } - - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); - } - - /** - * Peeks at the next n bytes as an ASCII string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - peekString(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekString() with a non-positive integer'; - } else if (num === 0) { - return ''; - } - - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream while peekString()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - let result = new Array(num); - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result[i] = String.fromCharCode(curPage[ptr++]); - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } - - return result.join(''); - } - - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - readString(n) { - const strToReturn = this.peekString(n); - this.movePointer_(n); - return strToReturn; - } - - /** - * Feeds more bytes into the back of the stream. - * @param {ArrayBuffer} ab - */ - push(ab) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; - } - - this.pages_.push(new Uint8Array(ab)); - // If the pointer is at the end of the current page of bytes, this will advance - // to the next page. - this.movePointer_(0); - } - - /** - * Creates a new ByteStream from this ByteStream that can be read / peeked. - * @returns {ByteStream} A clone of this ByteStream. - */ - tee() { - const clone = new ByteStream(this.bytes.buffer); - clone.bytes = this.bytes; - clone.ptr = this.ptr; - clone.pages_ = this.pages_.slice(); - clone.bytesRead_ = this.bytesRead_; - return clone; - } - } - - return ByteStream; -})(); diff --git a/codecs/codecs.js b/codecs/codecs.js index 7329860..e9eabea 100644 --- a/codecs/codecs.js +++ b/codecs/codecs.js @@ -42,6 +42,24 @@ * @property {ProbeFormat} format */ +/** + * Maps the ffprobe format.format_name string to a short MIME type. + * @type {Object} + */ +const FORMAT_NAME_TO_SHORT_TYPE = { + 'avi': 'video/x-msvideo', + 'flac': 'audio/flac', + 'mp3': 'audio/mpeg', + 'wav': 'audio/wav', +} + +// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers#webm says that only +// the following codecs are supported for webm: +// - video: AV1, VP8, VP9 +// - audio: Opus, Vorbis +const WEBM_AUDIO_CODECS = [ 'opus', 'vorbis' ]; +const WEBM_VIDEO_CODECS = [ 'av1', 'vp8', 'vp9' ]; + /** * TODO: Reconcile this with file/sniffer.js findMimeType() which does signature matching. * @param {ProbeInfo} info @@ -51,11 +69,14 @@ export function getShortMIMEString(info) { if (!info) throw `Invalid ProbeInfo`; if (!info.streams || info.streams.length === 0) throw `No streams in ProbeInfo`; - // mp3/flac files are always considered audio/ (even with mjpeg video streams). - if (info.format.format_name === 'mp3') { - return 'audio/mpeg'; - } else if (info.format.format_name === 'flac') { - return 'audio/flac'; + const formatName = info?.format?.format_name; + if (formatName && Object.keys(FORMAT_NAME_TO_SHORT_TYPE).includes(formatName)) { + return FORMAT_NAME_TO_SHORT_TYPE[formatName]; + } + + // M4A files are specifically audio/mp4. + if (info?.format?.filename?.toLowerCase().endsWith('.m4a')) { + return 'audio/mp4'; } // Otherwise, any file with at least 1 video stream is considered video/. @@ -70,10 +91,7 @@ export function getShortMIMEString(info) { /** @type {string} */ let subType; - switch (info.format.format_name) { - case 'avi': - subType = 'x-msvideo'; - break; + switch (formatName) { case 'mpeg': subType = 'mpeg'; break; @@ -83,12 +101,19 @@ export function getShortMIMEString(info) { case 'ogg': subType = 'ogg'; break; - // Should we detect .mkv files as x-matroska? case 'matroska,webm': - subType = 'webm'; + let isWebM = true; + for (const stream of info.streams) { + if ( (stream.codec_type === 'audio' && !WEBM_AUDIO_CODECS.includes(stream.codec_name)) + || (stream.codec_type === 'video' && !WEBM_VIDEO_CODECS.includes(stream.codec_name))) { + isWebM = false; + break; + } + } + subType = isWebM ? 'webm' : 'x-matroska'; break; default: - throw `Cannot handle format ${info.format.format_name} yet. ` + + throw `Cannot handle format ${formatName} yet. ` + `Please file a bug https://github.com/codedread/bitjs/issues/new`; } @@ -108,9 +133,7 @@ export function getShortMIMEString(info) { export function getFullMIMEString(info) { /** A string like 'video/mp4' */ let contentType = `${getShortMIMEString(info)}`; - // If MP3/FLAC, just send back the type. - if (contentType === 'audio/mpeg' - || contentType === 'audio/flac') { + if (Object.values(FORMAT_NAME_TO_SHORT_TYPE).includes(contentType)) { return contentType; } @@ -118,16 +141,23 @@ export function getFullMIMEString(info) { for (const stream of info.streams) { if (stream.codec_type === 'audio') { + // MP3 can sometimes have codec_tag_string=mp4a, so we check for it first. + if (stream.codec_name === 'mp3') { + codecFrags.add('mp3'); + continue; + } + switch (stream.codec_tag_string) { case 'mp4a': codecFrags.add(getMP4ACodecString(stream)); break; default: switch (stream.codec_name) { case 'aac': codecFrags.add(getMP4ACodecString(stream)); break; - case 'vorbis': codecFrags.add('vorbis'); break; - case 'opus': codecFrags.add('opus'); break; // I'm going off of what Chromium calls this one, with the dash. case 'ac3': codecFrags.add('ac-3'); break; + case 'dts': codecFrags.add('dts'); break; case 'flac': codecFrags.add('flac'); break; + case 'opus': codecFrags.add('opus'); break; + case 'vorbis': codecFrags.add('vorbis'); break; default: throw `Could not handle audio codec_name ${stream.codec_name}, ` + `codec_tag_string ${stream.codec_tag_string} for file ${info.format.filename} yet. ` + @@ -144,10 +174,12 @@ export function getFullMIMEString(info) { case 'png': continue; default: switch (stream.codec_name) { + case 'av1': codecFrags.add('av1'); break; case 'h264': codecFrags.add(getAVC1CodecString(stream)); break; - case 'mpeg2video': codecFrags.add('mpeg2video'); break; // Skip mjpeg as a video stream for the codecs string. case 'mjpeg': break; + case 'mpeg2video': codecFrags.add('mpeg2video'); break; + case 'vp8': codecFrags.add('vp8'); break; case 'vp9': codecFrags.add(getVP09CodecString(stream)); break; default: throw `Could not handle video codec_name ${stream.codec_name}, ` + @@ -165,6 +197,17 @@ export function getFullMIMEString(info) { // TODO: Consider whether any of these should be exported. +function getCleanHex(extradata) { + // 1. Split into lines + // 2. Extract only the middle hex section (after ':' and before ' ') + // 3. Remove all spaces + return extradata.split('\n') + .map(line => line.split(':')[1]?.split(' ')[0]) + .filter(Boolean) + .join('') + .replace(/\s/g, ''); +} + /** * AVC1 is the same thing as H264. * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#iso_base_media_file_format_mp4_quicktime_and_3gp @@ -172,6 +215,15 @@ export function getFullMIMEString(info) { * @returns {string} */ function getAVC1CodecString(stream) { + if (stream.extradata && stream.codec_tag_string === 'avc1') { + const hex = getCleanHex(stream.extradata); // results in "014d4028..." + + // Bytes 1, 2, and 3 are the Profile, Constraints, and Level. + // In the hex string, these are characters at index 2 through 8. + const codecPart = hex.substring(2, 8).toUpperCase(); + return `avc1.${codecPart}`; + } + if (!stream.profile) throw `No profile found in AVC1 stream`; let frag = 'avc1'; @@ -241,8 +293,8 @@ function getVP09CodecString(stream) { } // Add LL hex digits. - // If ffprobe is spitting out -99 as level... Just return 'vp9'. - if (stream.level === -99) { return 'vp9'; } + // If ffprobe is spitting out -99 as level... it means unknown, so we will guess level=1. + if (stream.level === -99) { frag += `.10`; } else { const levelAsHex = Number(stream.level).toString(16).toUpperCase().padStart(2, '0'); if (levelAsHex.length !== 2) { @@ -253,8 +305,8 @@ function getVP09CodecString(stream) { } // Add DD hex digits. - // TODO: This is just a guess at DD (16?), need to try and extract this info from - // ffprobe JSON output instead. + // TODO: This is just a guess at DD (10-bit color depth), need to try and extract this info + // from ffprobe JSON output instead. frag += '.10'; return frag; @@ -268,10 +320,14 @@ function getVP09CodecString(stream) { */ function getMP4ACodecString(stream) { let frag = 'mp4a.40'; + // https://dashif.org/codecs/audio/ switch (stream.profile) { case 'LC': frag += '.2'; break; + case 'HE-AAC': + frag += '.5'; + break; // TODO: more! default: throw `Cannot handle AAC stream with profile ${stream.profile} yet. ` + diff --git a/docs/bitjs.archive.md b/docs/bitjs.archive.md new file mode 100644 index 0000000..22a9b01 --- /dev/null +++ b/docs/bitjs.archive.md @@ -0,0 +1,198 @@ +# bitjs.archive + +This package includes objects for unarchiving binary data in popular archive formats (zip, rar, +tar, gzip) providing unzip, unrar, untar, gunzip capabilities via JavaScript in the browser or +various JavaScript runtimes (node, deno, bun). + +A compressor that creates Zip files is also present. + +The decompression / compression happens inside a Web Worker, if the runtime supports it (browsers, +deno). The library uses native decompression, if supported by the browser +(via [DecompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream/DecompressionStream)), +and falls back to JavaScript implementations otherwise. + +The API is event-based, you will want to subscribe to some of these events: + * 'progress': Periodic updates on the progress (bytes processed). + * 'extract': Sent whenever a single file in the archive was fully decompressed. + * 'finish': Sent when decompression/compression is complete. + +## Decompressing + +### Simple Example of unzip + +Here is a simple example of unzipping a file. It is assumed the zip file exists as an +[`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), +which you can get via +[`XHR`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Sending_and_Receiving_Binary_Data), +from a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer), +[`Fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer), +[`FileReader`](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsArrayBuffer), +etc. + +```javascript + import { Unzipper } from './bitjs/archive/decompress.js'; + const unzipper = new Unzipper(zipFileArrayBuffer); + unzipper.onExtract(evt => { + const {filename, fileData} = evt.unarchivedFile; + console.log(`unzipped ${filename} (${fileData.byteLength} bytes)`); + // Do something with fileData... + }); + unzipper.addEventListener('finish', () => console.log(`Finished!`)); + unzipper.start(); +``` + +`start()` is an async method that resolves a `Promise` when the decompression is complete, so you can +`await` on it, if you need to. + +### Progressive unzipping + +The unarchivers also support progressively decoding while streaming the file, if you are receiving +the zipped file from a slow place (a Cloud API, for instance). Send the first `ArrayBuffer` in the +constructor, and send subsequent `ArrayBuffers` using the `update()` method. + +```javascript + import { Unzipper } from './bitjs/archive/decompress.js'; + const unzipper = new Unzipper(anArrayBufferWithStartingBytes); + unzipper.addEventListener('extract', () => {...}); + unzipper.addEventListener('finish', () => {...}); + unzipper.start(); + ... + // after some time + unzipper.update(anArrayBufferWithMoreBytes); + ... + // after some more time + unzipper.update(anArrayBufferWithYetMoreBytes); +``` + +### getUnarchiver() + +If you don't want to bother with figuring out if you have a zip, rar, tar, or gz file, you can use +the convenience method `getUnarchiver()`, which sniffs the bytes for you and creates the appropriate +unarchiver. + +```javascript + import { getUnarchiver } from './bitjs/archive/decompress.js'; + const unarchiver = getUnarchiver(anArrayBuffer); + unarchiver.onExtract(evt => {...}); + // etc... + unarchiver.start(); +``` + +### Non-Browser JavaScript Runtime Examples + +The API works in other JavaScript runtimes too (Node, Deno, Bun). + +#### NodeJS + +```javascript + import * as fs from 'fs'; + import { getUnarchiver } from './archive/decompress.js'; + + const nodeBuf = fs.readFileSync('comic.cbz'); + // NOTE: Small files may not have a zero byte offset in Node, so we slice(). + // See https://nodejs.org/api/buffer.html#bufbyteoffset. + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + const unarchiver = getUnarchiver(ab); + unarchiver.addEventListener('progress', () => process.stdout.write('.')); + unarchiver.addEventListener('extract', (evt) => { + const {filename, fileData} = evt.unarchivedFile; + console.log(`${filename} (${fileData.byteLength} bytes)`); + }); + unarchiver.addEventListener('finish', () => console.log(`Done!`)); + unarchiver.start(); +``` + +#### Deno + +```typescript + import { UnarchiveExtractEvent } from './archive/events.js'; + import { getUnarchiver} from './archive/decompress.js'; + + const print = (s: string) => Deno.writeAll(Deno.stdout, new TextEncoder().encode(s)); + + async function go() { + const arr: Uint8Array = await Deno.readFile('example.zip'); + const unarchiver = getUnarchiver(arr.buffer); + unarchiver.addEventListener('extract', (evt) => { + const {filename, fileData} = (evt as UnarchiveExtractEvent).unarchivedFile; + print(`\n${filename} (${fileData.byteLength} bytes)\n`); + // Do something with fileData... + }); + unarchiver.addEventListener('finish', () => { console.log(`Done!`); Deno.exit(); }); + unarchiver.addEventListener('progress', (evt) => print('.')); + unarchiver.start(); + } + + await go(); +``` + +## Compressing + +The Zipper only supports creating zip files without compression (store only) for now. The interface +is pretty straightforward and there is no event-based / streaming API. + +```javascript + import { Zipper } from './bitjs/archive/compress.js'; + const zipper = new Zipper(); + const now = Date.now(); + // Create a zip file with files foo.jpg and bar.txt. + const zippedArrayBuffer = await zipper.start( + [ + { + fileName: 'foo.jpg', + lastModTime: now, + fileData: fooArrayBuffer, + }, + { + fileName: 'bar.txt', + lastModTime: now, + fileData: barArrayBuffer, + } + ], + true /* isLastFile */); +``` + +If you don't want to have all files in memory at once, you can zip progressively: + +```javascript + import { Zipper } from './bitjs/archive/compress.js'; + const zipper = new Zipper(); + const zipPromise = zipper.start(); + // ... some time later + zipper.appendFiles([file1]); + // ... more time later + zipper.appendFiles([file2]); + // ... now we add the last file + zipper.appendFiles([file3], true /* isLastFile */); + + const zippedArrayBuffer = await zipPromise; + ... +``` + +## Implementation Details + +All you generally need to worry about is calling getUnarchiver(), listen for events, and then `start()`. However, if you are interested in how it works under the covers, read on... + +The implementations are written in pure JavaScript and communicate with the host software (the thing that wants to do the unzipping) via a MessageChannel. The host and implementation each own a MessagePort and pass messages to each other through it. In a web browser, the implementation is invoked as a Web Worker to save the main UI thread from getting the CPU spins. + +```mermaid +sequenceDiagram + participant Host Code + participant Port1 + box Any JavaScript Context (could be a Web Worker) + participant Port2 + participant unrar.js + end + Host Code->>Port1: postMessage(rar bytes) + Port1-->>Port2: (MessageChannel) + Port2->>unrar.js: onmessage(rar bytes) + Note right of unrar.js: unrar the thing + + unrar.js->>Port2: postMessage(an extracted file) + Port2-->>Port1: (MessageChannel) + Port1->>Host Code: onmessage(an extracted file) + + unrar.js->>Port2: postMessage(2nd extracted file) + Port2-->>Port1: (MessageChannel) + Port1->>Host Code: onmessage(2nd extracted file) +``` diff --git a/docs/bitjs.io.md b/docs/bitjs.io.md new file mode 100644 index 0000000..401cd42 --- /dev/null +++ b/docs/bitjs.io.md @@ -0,0 +1,88 @@ +# bitjs.io + +This package includes stream objects for reading and writing binary data at the bit and byte level: +BitStream, ByteStream. + +Streams are given an ArrayBuffer of bytes and keeps track of where in the stream you are. As you +read through the stream, the pointer is advanced through the buffer. If you need to peek at a number +without advancing the pointer, use the `peek` methods. + +## BitStream + +A bit stream is a way to read a variable number of bits from a series of bytes. This is useful for +parsing certain protocols (for example pkzip or rar algorithm). Note that the order of reading +bits can go from least-to-most significant bit, or the reverse. + +### Least-to-Most Direction + +![BitStream reading from least-to-most significant bit](bitstream-ltm.png) + +```javascript +const bstream = new BitStream(ab, false /* mtl */); +bstream.readBits(6); // (blue) 0b001011 = 11 +bstream.readBits(5); // (red) 0b11001 = 25 +bstream.readBits(8); // (green) 0b10000010 = 130 +``` + +### Most-to-Least Direction + +![BitStream reading from most-to-least significant bit](bitstream-mtl.png) + +```javascript +const bstream = new BitStream(ab, true /* mtl */); +bstream.readBits(6); // (blue) 0b010010 = 18 +bstream.readBits(5); // (red) 0b11000 = 24 +bstream.readBits(8); // (green) 0b10110100 = 180 +``` + +## ByteStream + +A ByteStream is a convenient way to read numbers and ASCII strings from a set of bytes. For example, +interpreting 2 bytes in the stream as a number is done by calling `someByteStream.readNumber(2)`. By +default, the byte stream is considered Little Endian, but can be changed at any point using +`someByteStream.setBigEndian()` and toggled back with `someByteStream.setLittleEndian()`. + +If you need to peek at bytes without advancing the pointer, use the `peek` methods. + +By default, numbers are unsigned, but `peekSignedNumber(n)` and `readSignedNumber(n)` exist for +signed numbers. + +```javascript + const byteStream = new ByteStream(someArrayBuffer); + byteStream.setBigEndian(); + byteStream.skip(2); // skip two bytes. + // Interpret next 2 bytes as the string length. + const strLen = byteStream.readNumber(2); + // Read in bytes as an ASCII string. + const someString = byteStream.readString(strLen); + // Interpret next byte as an int8 (0xFF would be -1). + const someVal = byteStream.readSignedNumber(1); + ... +``` + +### Appending to the Stream + +If you get more bytes (for example, from an asynchronous process), you can add them to the end of +the byte stream by using `someByteStream.push(nextBytesAsAnArrayBuffer)`. + +### Forking / Teeing the stream. + +If you have a need to seek ahead to a different section of the stream of bytes, and want to later +return to where you left off, you should use `tee()` method to make a copy of the ByteStream. This +will let you seek to the appropriate spot to grab some bytes using the teed stream, while you can +pick up where you left off with the original stream. + +```javascript + const origStream = new ByteStream(someArrayBuffer); + const strLen = origStream.readNumber(4); // Bytes 0-3. + const strOffset = origStream.readNumber(4); // Bytes 4-7. + + const teedStream = origStream.tee(); + const description = teedStream.skip(strOffset).readString(strLen); + + const someOtherVal = origStream.readNumber(4); // Bytes 8-11 +``` + +Note that the teed stream is not "connected" to the original stream. If you push new bytes to the +original stream, the teed stream does not see them. If you find this behavior unexpected, please +file a bug. diff --git a/docs/bitstream-ltm.png b/docs/bitstream-ltm.png new file mode 100644 index 0000000..7ed03be Binary files /dev/null and b/docs/bitstream-ltm.png differ diff --git a/docs/bitstream-mtl.png b/docs/bitstream-mtl.png new file mode 100644 index 0000000..56b0349 Binary files /dev/null and b/docs/bitstream-mtl.png differ diff --git a/docs/unrar.html b/docs/unrar.html old mode 100755 new mode 100644 diff --git a/file/sniffer.js b/file/sniffer.js index 5092372..0d9bf13 100644 --- a/file/sniffer.js +++ b/file/sniffer.js @@ -8,7 +8,9 @@ */ // https://mimesniff.spec.whatwg.org/ is a good resource. -// https://github.com/h2non/filetype is an easy target for reverse-engineering. +// Easy targets for reverse-engineering: +// - https://github.com/h2non/filetype +// - https://github.com/gabriel-vasile/mimetype (particularly internal/magic/ftyp.go) // NOTE: Because the ICO format also starts with a couple zero bytes, this tree will rely on the // File Type box never going beyond 255 bytes in length which, seems unlikely according to @@ -24,6 +26,7 @@ const fileSignatures = { 'application/pdf': [[0x25, 0x50, 0x44, 0x46, 0x2d]], // '%PDF-' // Archive formats: + 'application/gzip': [[0x1F, 0x8B, 0x08]], 'application/x-tar': [ // 'ustar' [0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30], [0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00], diff --git a/image/parsers/README.md b/image/parsers/README.md new file mode 100644 index 0000000..17b0168 --- /dev/null +++ b/image/parsers/README.md @@ -0,0 +1,6 @@ +General-purpose, event-based parsers for digital images. + +Currently supports GIF, JPEG, and PNG. + +Some nice implementations of Exif parsing for HEIF, TIFF here: +https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers \ No newline at end of file diff --git a/image/parsers/exif.js b/image/parsers/exif.js new file mode 100644 index 0000000..e09d182 --- /dev/null +++ b/image/parsers/exif.js @@ -0,0 +1,316 @@ +/* + * exif.js + * + * Parse EXIF. + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + */ + +import { ByteStream } from '../../io/bytestream.js'; + +/** @enum {number} */ +export const ExifTagNumber = { + // Tags used by IFD0. + IMAGE_DESCRIPTION: 0x010e, + MAKE: 0x010f, + MODEL: 0x0110, + ORIENTATION: 0x0112, + X_RESOLUTION: 0x011a, + Y_RESOLUTION: 0x011b, + RESOLUTION_UNIT: 0x0128, + SOFTWARE: 0x0131, + DATE_TIME: 0x0132, + WHITE_POINT: 0x013e, + PRIMARY_CHROMATICITIES: 0x013f, + Y_CB_CR_COEFFICIENTS: 0x0211, + Y_CB_CR_POSITIONING: 0x0213, + REFERENCE_BLACK_WHITE: 0x0214, + COPYRIGHT: 0x8298, + EXIF_OFFSET: 0x8769, + + // Tags used by Exif SubIFD. + EXPOSURE_TIME: 0x829a, + F_NUMBER: 0x829d, + EXPOSURE_PROGRAM: 0x8822, + ISO_SPEED_RATINGS: 0x8827, + EXIF_VERSION: 0x9000, + DATE_TIME_ORIGINAL: 0x9003, + DATE_TIME_DIGITIZED: 0x9004, + COMPONENT_CONFIGURATION: 0x9101, + COMPRESSED_BITS_PER_PIXEL: 0x9102, + SHUTTER_SPEED_VALUE: 0x9201, + APERTURE_VALUE: 0x9202, + BRIGHTNESS_VALUE: 0x9203, + EXPOSURE_BIAS_VALUE: 0x9204, + MAX_APERTURE_VALUE: 0x9205, + SUBJECT_DISTANCE: 0x9206, + METERING_MODE: 0x9207, + LIGHT_SOURCE: 0x9208, + FLASH: 0x9209, + FOCAL_LENGTH: 0x920a, + MAKER_NOTE: 0x927c, + USER_COMMENT: 0x9286, + FLASH_PIX_VERSION: 0xa000, + COLOR_SPACE: 0xa001, + EXIF_IMAGE_WIDTH: 0xa002, + EXIF_IMAGE_HEIGHT: 0xa003, + RELATED_SOUND_FILE: 0xa004, + EXIF_INTEROPERABILITY_OFFSET: 0xa005, + FOCAL_PLANE_X_RESOLUTION: 0xa20e, + FOCAL_PLANE_Y_RESOLUTION: 0x20f, + FOCAL_PLANE_RESOLUTION_UNIT: 0xa210, + SENSING_METHOD: 0xa217, + FILE_SOURCE: 0xa300, + SCENE_TYPE: 0xa301, + + // Tags used by IFD1. + IMAGE_WIDTH: 0x0100, + IMAGE_LENGTH: 0x0101, + BITS_PER_SAMPLE: 0x0102, + COMPRESSION: 0x0103, + PHOTOMETRIC_INTERPRETATION: 0x0106, + STRIP_OFFSETS: 0x0111, + SAMPLES_PER_PIXEL: 0x0115, + ROWS_PER_STRIP: 0x0116, + STRIP_BYTE_COUNTS: 0x0117, + // X_RESOLUTION, Y_RESOLUTION + PLANAR_CONFIGURATION: 0x011c, + // RESOLUTION_UNIT + JPEG_IF_OFFSET: 0x0201, + JPEG_IF_BYTE_COUNT: 0x0202, + // Y_CB_CR_COEFFICIENTS + Y_CB_CR_SUB_SAMPLING: 0x0212, + // Y_CB_CR_POSITIONING, REFERENCE_BLACK_WHITE +}; + +/** @enum {number} */ +export const ExifDataFormat = { + UNSIGNED_BYTE: 1, + ASCII_STRING: 2, + UNSIGNED_SHORT: 3, + UNSIGNED_LONG: 4, + UNSIGNED_RATIONAL: 5, + SIGNED_BYTE: 6, + UNDEFINED: 7, + SIGNED_SHORT: 8, + SIGNED_LONG: 9, + SIGNED_RATIONAL: 10, + SINGLE_FLOAT: 11, + DOUBLE_FLOAT: 12, +}; + +/** + * @typedef ExifValue + * @property {ExifTagNumber} tagNumber The numerical value of the tag. + * @property {string=} tagName A string representing the tag number. + * @property {ExifDataFormat} dataFormat The data format. + * @property {number=} numericalValue Populated for SIGNED/UNSIGNED BYTE/SHORT/LONG/FLOAT. + * @property {string=} stringValue Populated only for ASCII_STRING. + * @property {number=} numeratorValue Populated only for SIGNED/UNSIGNED RATIONAL. + * @property {number=} denominatorValue Populated only for SIGNED/UNSIGNED RATIONAL. + * @property {number=} numComponents Populated only for UNDEFINED data format. + * @property {number=} offsetValue Populated only for UNDEFINED data format. + */ + +/** + * @param {number} tagNumber + * @param {string} type + * @param {number} len + * @param {number} dataVal + */ +function warnBadLength(tagNumber, type, len, dataVal) { + const hexTag = tagNumber.toString(16); + console.warn(`Tag 0x${hexTag} is ${type} with len=${len} and data=${dataVal}`); +} + +/** + * @param {ByteStream} stream + * @param {ByteStream} lookAheadStream + * @param {boolean} debug + * @returns {ExifValue} + */ +export function getExifValue(stream, lookAheadStream, DEBUG = false) { + const tagNumber = stream.readNumber(2); + let tagName = findNameWithValue(ExifTagNumber, tagNumber); + if (!tagName) { + tagName = `UNKNOWN (0x${tagNumber.toString(16)})`; + } + + let dataFormat = stream.readNumber(2); + + // Handle bad types for special tags. + if (tagNumber === ExifTagNumber.EXIF_OFFSET) { + dataFormat = ExifDataFormat.UNSIGNED_LONG; + } + + const dataFormatName = findNameWithValue(ExifDataFormat, dataFormat); + if (!dataFormatName) throw `Invalid data format: ${dataFormat}`; + + /** @type {ExifValue} */ + const exifValue = { + tagNumber, + tagName, + dataFormat, + }; + + let len = stream.readNumber(4); + switch (dataFormat) { + case ExifDataFormat.UNSIGNED_BYTE: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4)); + } + exifValue.numericalValue = stream.readNumber(1); + stream.skip(3); + break; + case ExifDataFormat.ASCII_STRING: + if (len <= 4) { + exifValue.stringValue = stream.readString(4); + } else { + const strOffset = stream.readNumber(4); + exifValue.stringValue = lookAheadStream.tee().skip(strOffset).readString(len - 1); + } + break; + case ExifDataFormat.UNSIGNED_SHORT: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4)); + } + exifValue.numericalValue = stream.readNumber(2); + stream.skip(2); + break; + case ExifDataFormat.UNSIGNED_LONG: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4)); + } + exifValue.numericalValue = stream.readNumber(4); + break; + case ExifDataFormat.UNSIGNED_RATIONAL: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4)); + } + + const uratStream = lookAheadStream.tee().skip(stream.readNumber(4)); + exifValue.numeratorValue = uratStream.readNumber(4); + exifValue.denominatorValue = uratStream.readNumber(4); + break; + case ExifDataFormat.SIGNED_BYTE: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4)); + } + exifValue.numericalValue = stream.readSignedNumber(1); + stream.skip(3); + break; + case ExifDataFormat.UNDEFINED: + exifValue.numComponents = len; + exifValue.offsetValue = stream.readNumber(4); + break; + case ExifDataFormat.SIGNED_SHORT: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4)); + } + exifValue.numericalValue = stream.readSignedNumber(2); + stream.skip(2); + break; + case ExifDataFormat.SIGNED_LONG: + if (len !== 1) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekSignedNumber(4)); + } + exifValue.numericalValue = stream.readSignedNumber(4); + break; + case ExifDataFormat.SIGNED_RATIONAL: + if (len !== 1 && DEBUG) { + warnBadLength(tagNumber, dataFormatName, len, stream.peekNumber(4)); + } + + const ratStream = lookAheadStream.tee().skip(stream.readNumber(4)); + exifValue.numeratorValue = ratStream.readSignedNumber(4); + exifValue.denominatorValue = ratStream.readSignedNumber(4); + break; + default: + throw `Bad data format: ${dataFormat}`; + } + return exifValue; +} + +/** + * Reads an Image File Directory from stream, populating the map. + * @param {ByteStream} stream The stream to extract the Exif value descriptors. + * @param {ByteStream} lookAheadStream The lookahead stream if the offset is used. + * @param {Map v === valToFind); + return entry ? entry[0] : null; +} diff --git a/image/parsers/gif.js b/image/parsers/gif.js new file mode 100644 index 0000000..6283e60 --- /dev/null +++ b/image/parsers/gif.js @@ -0,0 +1,485 @@ +/* + * gif.js + * + * An event-based parser for GIF images. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +import { BitStream } from '../../io/bitstream.js'; +import { ByteStream } from '../../io/bytestream.js'; +import { createEvent } from './parsers.js'; + +// https://www.w3.org/Graphics/GIF/spec-gif89a.txt + +export const GifParseEventType = { + APPLICATION_EXTENSION: 'application_extension', + COMMENT_EXTENSION: 'comment_extension', + GRAPHIC_CONTROL_EXTENSION: 'graphic_control_extension', + HEADER: 'header', + LOGICAL_SCREEN: 'logical_screen', + PLAIN_TEXT_EXTENSION: 'plain_text_extension', + TABLE_BASED_IMAGE: 'table_based_image', + TRAILER: 'trailer', +}; + +/** + * @typedef GifHeader + * @property {string} version + */ + +/** + * @typedef GifColor + * @property {number} red + * @property {number} green + * @property {number} blue + */ + +/** + * @typedef GifLogicalScreen + * @property {number} logicalScreenWidth + * @property {number} logicalScreenHeight + * @property {boolean} globalColorTableFlag + * @property {number} colorResolution + * @property {boolean} sortFlag + * @property {number} globalColorTableSize + * @property {number} backgroundColorIndex + * @property {number} pixelAspectRatio + * @property {GifColor[]=} globalColorTable Only if globalColorTableFlag is true. + */ + +/** + * @typedef GifTableBasedImage + * @property {number} imageLeftPosition + * @property {number} imageTopPosition + * @property {number} imageWidth + * @property {number} imageHeight + * @property {boolean} localColorTableFlag + * @property {boolean} interlaceFlag + * @property {boolean} sortFlag + * @property {number} localColorTableSize + * @property {GifColor[]=} localColorTable Only if localColorTableFlag is true. + * @property {number} lzwMinimumCodeSize + * @property {Uint8Array} imageData + */ + +/** + * @typedef GifGraphicControlExtension + * @property {number} disposalMethod + * @property {boolean} userInputFlag + * @property {boolean} transparentColorFlag + * @property {number} delayTime + * @property {number} transparentColorIndex + */ + +/** + * @typedef GifCommentExtension + * @property {string} comment + */ + +/** + * @typedef GifPlainTextExtension + * @property {number} textGridLeftPosition + * @property {number} textGridTopPosition + * @property {number} textGridWidth + * @property {number} textGridHeight + * @property {number} characterCellWidth + * @property {number} characterCellHeight + * @property {number} textForegroundColorIndex + * @property {number} textBackgroundColorIndex + * @property {string} plainText + */ + +/** + * @typedef GifApplicationExtension + * @property {string} applicationIdentifier + * @property {Uint8Array} applicationAuthenticationCode + * @property {Uint8Array} applicationData + */ + +/** + * The Grammar. + * + * ::= Header * Trailer + * ::= Logical Screen Descriptor [Global Color Table] + * ::= | + * + * ::= [Graphic Control Extension] + * ::= | + * Plain Text Extension + * ::= Image Descriptor [Local Color Table] Image Data + * ::= Application Extension | + * Comment Extension + */ + +export class GifParser extends EventTarget { + /** + * @type {ByteStream} + * @private + */ + bstream; + + /** + * @type {string} + * @private + */ + version; + + /** @param {ArrayBuffer} ab */ + constructor(ab) { + super(); + this.bstream = new ByteStream(ab); + // The entire GIF structure is Little-Endian (which is actually ByteStream's default). + this.bstream.setLittleEndian(); + } + + /** + * Type-safe way to bind a listener for a GifApplicationExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onApplicationExtension(listener) { + super.addEventListener(GifParseEventType.APPLICATION_EXTENSION, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifCommentExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onCommentExtension(listener) { + super.addEventListener(GifParseEventType.COMMENT_EXTENSION, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifGraphicControlExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onGraphicControlExtension(listener) { + super.addEventListener(GifParseEventType.GRAPHIC_CONTROL_EXTENSION, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifHeader. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onHeader(listener) { + super.addEventListener(GifParseEventType.HEADER, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifLogicalScreen. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onLogicalScreen(listener) { + super.addEventListener(GifParseEventType.LOGICAL_SCREEN, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifPlainTextExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onPlainTextExtension(listener) { + super.addEventListener(GifParseEventType.PLAIN_TEXT_EXTENSION, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a GifTableBasedImage. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onTableBasedImage(listener) { + super.addEventListener(GifParseEventType.TABLE_BASED_IMAGE, listener); + return this; + } + + /** + * Type-safe way to bind a listener for the GifTrailer. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onTrailer(listener) { + super.addEventListener(GifParseEventType.TRAILER, listener); + return this; + } + + /** + * @returns {Promise} A Promise that resolves when the parsing is complete. + */ + async start() { + // Header. + const gif = this.bstream.readString(3); // "GIF" + if (gif !== "GIF") throw `Not GIF: ${gif}`; + + const version = this.version = this.bstream.readString(3); // "87a" or "89a" + if (!["87a", "89a"].includes(version)) throw `Bad version: ${version}`; + + this.dispatchEvent(createEvent(GifParseEventType.HEADER, {version})); + + // Logical Screen Descriptor. + const logicalScreenWidth = this.bstream.readNumber(2); + const logicalScreenHeight = this.bstream.readNumber(2); + + const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true); + const globalColorTableFlag = !!bitstream.readBits(1); + const colorResolution = bitstream.readBits(3) + 1; + const sortFlag = !!bitstream.readBits(1); // sortFlag + const globalColorTableSize = 2 ** (bitstream.readBits(3) + 1); + const backgroundColorIndex = this.bstream.readNumber(1); + const pixelAspectRatio = this.bstream.readNumber(1); + + // Global Color Table + let globalColorTable = undefined; + if (globalColorTableFlag) { + globalColorTable = []; + // Series of R,G,B. + for (let c = 0; c < globalColorTableSize; ++c) { + globalColorTable.push(/** @type {GifColor} */ ({ + red: this.bstream.readNumber(1), + green: this.bstream.readNumber(1), + blue: this.bstream.readNumber(1), + })); + } + } + this.dispatchEvent(createEvent(GifParseEventType.LOGICAL_SCREEN, + /** @type {GifLogicalScreen} */ ({ + logicalScreenWidth, + logicalScreenHeight, + globalColorTableFlag, + colorResolution, + sortFlag, + globalColorTableSize, + backgroundColorIndex, + pixelAspectRatio, + globalColorTable, + }) + )); + + while (this.readGraphicBlock()) { + // Read a graphic block + } + } + + /** + * @private + * @returns {boolean} True if this was not the last block. + */ + readGraphicBlock() { + let nextByte = this.bstream.readNumber(1); + + // Image Descriptor. + if (nextByte === 0x2C) { + const imageLeftPosition = this.bstream.readNumber(2); + const imageTopPosition = this.bstream.readNumber(2); + const imageWidth = this.bstream.readNumber(2); + const imageHeight = this.bstream.readNumber(2); + + const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true); + const localColorTableFlag = !!bitstream.readBits(1); + const interlaceFlag = !!bitstream.readBits(1); + const sortFlag = !!bitstream.readBits(1); + bitstream.readBits(2); // reserved + const localColorTableSize = 2 ** (bitstream.readBits(3) + 1); + + let localColorTable = undefined; + if (localColorTableFlag) { + // this.bstream.readBytes(3 * localColorTableSize); + localColorTable = []; + // Series of R,G,B. + for (let c = 0; c < localColorTableSize; ++c) { + localColorTable.push(/** @type {GifColor} */ ({ + red: this.bstream.readNumber(1), + green: this.bstream.readNumber(1), + blue: this.bstream.readNumber(1), + })); + } + } + + // Table-Based Image. + const lzwMinimumCodeSize = this.bstream.readNumber(1); + const bytesArr = []; + let bytes; + let totalNumBytes = 0; + while ((bytes = this.readSubBlock())) { + totalNumBytes += bytes.byteLength; + bytesArr.push(bytes); + } + + const imageData = new Uint8Array(totalNumBytes); + let ptr = 0; + for (const arr of bytesArr) { + imageData.set(arr, ptr); + ptr += arr.byteLength; + } + + this.dispatchEvent(createEvent(GifParseEventType.TABLE_BASED_IMAGE, + /** @type {GifTableBasedImage} */ ({ + imageLeftPosition, + imageTopPosition, + imageWidth, + imageHeight, + localColorTableFlag, + interlaceFlag, + sortFlag, + localColorTableSize, + localColorTable, + lzwMinimumCodeSize, + imageData, + }) + )); + + return true; + } + // Extensions. + else if (nextByte === 0x21) { + if (this.version !== '89a') { + throw `Found Extension Introducer (0x21) but was not GIF 89a: ${this.version}`; + } + + const label = this.bstream.readNumber(1); + + // Graphic Control Extension. + if (label === 0xF9) { + const blockSize = this.bstream.readNumber(1); + if (blockSize !== 4) throw `GCE: Block size of ${blockSize}`; + + // Packed Fields. + const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true); + bitstream.readBits(3); // Reserved + const disposalMethod = bitstream.readBits(3); + const userInputFlag = !!bitstream.readBits(1); + const transparentColorFlag = !!bitstream.readBits(1); + + const delayTime = this.bstream.readNumber(2); + const transparentColorIndex = this.bstream.readNumber(1); + const blockTerminator = this.bstream.readNumber(1); + if (blockTerminator !== 0) throw `GCE: Block terminator of ${blockTerminator}`; + + this.dispatchEvent(createEvent(GifParseEventType.GRAPHIC_CONTROL_EXTENSION, + /** @type {GifGraphicControlExtension} */ ({ + disposalMethod, + userInputFlag, + transparentColorFlag, + delayTime, + transparentColorIndex, + }) + )); + return true; + } + + // Comment Extension. + else if (label === 0xFE) { + let bytes; + let comment = ''; + while ((bytes = this.readSubBlock())) { + comment += new TextDecoder().decode(bytes); + } + this.dispatchEvent(createEvent(GifParseEventType.COMMENT_EXTENSION, comment)); + return true; + } + + // Plain Text Extension. + else if (label === 0x01) { + const blockSize = this.bstream.readNumber(1); + if (blockSize !== 12) throw `PTE: Block size of ${blockSize}`; + + const textGridLeftPosition = this.bstream.readNumber(2); + const textGridTopPosition = this.bstream.readNumber(2); + const textGridWidth = this.bstream.readNumber(2); + const textGridHeight = this.bstream.readNumber(2); + const characterCellWidth = this.bstream.readNumber(1); + const characterCellHeight = this.bstream.readNumber(1); + const textForegroundColorIndex = this.bstream.readNumber(1); + const textBackgroundColorIndex = this.bstream.readNumber(1); + let bytes; + let plainText = '' + while ((bytes = this.readSubBlock())) { + plainText += new TextDecoder().decode(bytes); + } + + this.dispatchEvent(createEvent(GifParseEventType.PLAIN_TEXT_EXTENSION, + /** @type {GifPlainTextExtension} */ ({ + textGridLeftPosition, + textGridTopPosition, + textGridWidth, + textGridHeight, + characterCellWidth, + characterCellHeight, + textForegroundColorIndex, + textBackgroundColorIndex, + plainText, + }) + )); + + return true; + } + + // Application Extension. + else if (label === 0xFF) { + const blockSize = this.bstream.readNumber(1); + if (blockSize !== 11) throw `AE: Block size of ${blockSize}`; + + const applicationIdentifier = this.bstream.readString(8); + const applicationAuthenticationCode = this.bstream.readBytes(3); + const bytesArr = []; + let bytes; + let totalNumBytes = 0; + while ((bytes = this.readSubBlock())) { + totalNumBytes += bytes.byteLength; + bytesArr.push(bytes); + } + + const applicationData = new Uint8Array(totalNumBytes); + let ptr = 0; + for (const arr of bytesArr) { + applicationData.set(arr, ptr); + ptr += arr.byteLength; + } + + this.dispatchEvent(createEvent(GifParseEventType.APPLICATION_EXTENSION, + /** {@type GifApplicationExtension} */ ({ + applicationIdentifier, + applicationAuthenticationCode, + applicationData, + }) + )); + + return true; + } + + else { + throw `Unrecognized extension label=0x${label.toString(16)}`; + } + } + else if (nextByte === 0x3B) { + this.dispatchEvent(createEvent(GifParseEventType.TRAILER)); + // Read the trailer. + return false; + } + else { + throw `Unknown marker: 0x${nextByte.toString(16)}`; + } + } + + /** + * @private + * @returns {Uint8Array} Data from the sub-block, or null if this was the last, zero-length block. + */ + readSubBlock() { + let subBlockSize = this.bstream.readNumber(1); + if (subBlockSize === 0) return null; + return this.bstream.readBytes(subBlockSize); + } +} diff --git a/image/parsers/jpeg.js b/image/parsers/jpeg.js new file mode 100644 index 0000000..d3e62e3 --- /dev/null +++ b/image/parsers/jpeg.js @@ -0,0 +1,490 @@ +/* + * jpeg.js + * + * An event-based parser for JPEG images. + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + */ + +import { ByteStream } from '../../io/bytestream.js'; +import { getExifProfile } from './exif.js'; +import { createEvent } from './parsers.js'; + +/** @typedef {import('./exif.js').ExifValue} ExifValue */ + +// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format +// https://www.media.mit.edu/pia/Research/deepview/exif.html +// https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm +// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf + +let DEBUG = false; + +/** @enum {string} */ +export const JpegParseEventType = { + APP0_MARKER: 'app0_marker', + APP0_EXTENSION: 'app0_extension', + APP1_EXIF: 'app1_exif', + DEFINE_QUANTIZATION_TABLE: 'define_quantization_table', + DEFINE_HUFFMAN_TABLE: 'define_huffman_table', + START_OF_FRAME: 'start_of_frame', + START_OF_SCAN: 'start_of_scan', +} + +/** @enum {number} */ +export const JpegSegmentType = { + SOF0: 0xC0, + SOF1: 0xC1, + SOF2: 0xC2, + DHT: 0xC4, + SOI: 0xD8, + EOI: 0xD9, + SOS: 0xDA, + DQT: 0xDB, + APP0: 0xE0, + APP1: 0xE1, +}; + +/** + * @param {Uint8Array} bytes An array of bytes of size 2. + * @returns {JpegSegmentType} Returns the second byte in bytes. + */ +function getJpegMarker(bytes) { + if (bytes.byteLength < 2) throw `Bad bytes length: ${bytes.byteLength}`; + if (bytes[0] !== 0xFF) throw `Bad marker, first byte=0x${bytes[0].toString(16)}`; + return bytes[1]; +} + +/** @enum {number} */ +export const JpegDensityUnits = { + NO_UNITS: 0, + PIXELS_PER_INCH: 1, + PIXELS_PER_CM: 2, +}; + +/** + * @typedef JpegApp0Marker + * @property {string} jfifVersion Like '1.02'. + * @property {JpegDensityUnits} densityUnits + * @property {number} xDensity + * @property {number} yDensity + * @property {number} xThumbnail + * @property {number} yThumbnail + * @property {Uint8Array} thumbnailData RGB data. Size is 3 x thumbnailWidth x thumbnailHeight. + */ + +/** @enum {number} */ +export const JpegExtensionThumbnailFormat = { + JPEG: 0x10, + ONE_BYTE_PER_PIXEL_PALETTIZED: 0x11, + THREE_BYTES_PER_PIXEL_RGB: 0x13, +}; + +/** + * @typedef JpegApp0Extension + * @property {JpegExtensionThumbnailFormat} thumbnailFormat + * @property {Uint8Array} thumbnailData Raw thumbnail data + */ + +/** @typedef {Map} JpegExifProfile */ + +/** + * @typedef JpegDefineQuantizationTable + * @property {number} tableNumber Table/component number. + * @property {number} precision (0=byte, 1=word). + * @property {number[]} tableValues 64 numbers representing the quantization table. + */ + +/** @enum {number} */ +export const JpegHuffmanTableType = { + DC: 0, + AC: 1, +}; + +/** + * @typedef JpegDefineHuffmanTable + * @property {number} tableNumber Table/component number (0-3). + * @property {JpegHuffmanTableType} tableType Either DC or AC. + * @property {number[]} numberOfSymbols A 16-byte array specifying the # of symbols of each length. + * @property {number[]} symbols + */ + +/** @enum {number} */ +export const JpegDctType = { + BASELINE: 0, + EXTENDED_SEQUENTIAL: 1, + PROGRESSIVE: 2, +}; + +/** @enum {number} */ +export const JpegComponentType = { + Y: 1, + CB: 2, + CR: 3, + I: 4, + Q: 5, +}; + +/** + * @typedef JpegComponentDetail + * @property {JpegComponentType} componentId + * @property {number} verticalSamplingFactor + * @property {number} horizontalSamplingFactor + * @property {number} quantizationTableNumber + */ + +/** + * @typedef JpegStartOfFrame + * @property {JpegDctType} dctType + * @property {number} dataPrecision + * @property {number} imageHeight + * @property {number} imageWidth + * @property {number} numberOfComponents Usually 1, 3, or 4. + * @property {JpegComponentDetail[]} componentDetails + */ + +/** + * @typedef JpegStartOfScan + * @property {number} componentsInScan + * @property {number} componentSelectorY + * @property {number} huffmanTableSelectorY + * @property {number} componentSelectorCb + * @property {number} huffmanTableSelectorCb + * @property {number} componentSelectorCr + * @property {number} huffmanTableSelectorCr + * @property {number} scanStartPositionInBlock + * @property {number} scanEndPositionInBlock + * @property {number} successiveApproximationBitPosition + * @property {Uint8Array} rawImageData + */ + +export class JpegParser extends EventTarget { + /** + * @type {ByteStream} + * @private + */ + bstream; + + /** + * @type {boolean} + * @private + */ + hasApp0MarkerSegment = false; + + /** @param {ArrayBuffer} ab */ + constructor(ab) { + super(); + this.bstream = new ByteStream(ab); + } + + /** + * Type-safe way to bind a listener for a JpegApp0Marker. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp0Marker(listener) { + super.addEventListener(JpegParseEventType.APP0_MARKER, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegApp0Extension. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp0Extension(listener) { + super.addEventListener(JpegParseEventType.APP0_EXTENSION, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegExifProfile. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp1Exif(listener) { + super.addEventListener(JpegParseEventType.APP1_EXIF, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegDefineQuantizationTable. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onDefineQuantizationTable(listener) { + super.addEventListener(JpegParseEventType.DEFINE_QUANTIZATION_TABLE, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegDefineHuffmanTable. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onDefineHuffmanTable(listener) { + super.addEventListener(JpegParseEventType.DEFINE_HUFFMAN_TABLE, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegStartOfFrame. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onStartOfFrame(listener) { + super.addEventListener(JpegParseEventType.START_OF_FRAME, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a JpegStartOfScan. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onStartOfScan(listener) { + super.addEventListener(JpegParseEventType.START_OF_SCAN, listener); + return this; + } + + /** @returns {Promise} A Promise that resolves when the parsing is complete. */ + async start() { + const segmentType = getJpegMarker(this.bstream.readBytes(2)); + if (segmentType !== JpegSegmentType.SOI) throw `Did not start with a SOI`; + + let jpegMarker; + do { + jpegMarker = getJpegMarker(this.bstream.readBytes(2)); + + if (jpegMarker === JpegSegmentType.APP0) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + const skipAheadStream = this.bstream.tee().skip(length - 2); + + const identifier = this.bstream.readString(4); + if (identifier === 'JFIF') { + if (this.hasApp0MarkerSegment) throw `JFIF found after JFIF`; + if (this.bstream.readNumber(1) !== 0) throw 'No null byte terminator for JFIF'; + + this.hasApp0MarkerSegment = true; + const majorVer = `${this.bstream.readNumber(1)}.`; + const minorVer = `${this.bstream.readNumber(1)}`.padStart(2, '0'); + const densityUnits = this.bstream.readNumber(1); + const xDensity = this.bstream.readNumber(2); + const yDensity = this.bstream.readNumber(2); + const xThumbnail = this.bstream.readNumber(1); + const yThumbnail = this.bstream.readNumber(1); + + /** @type {JpegApp0Marker} */ + let app0MarkerSegment = { + jfifVersion: `${majorVer}${minorVer}`, + densityUnits, + xDensity, + yDensity, + xThumbnail, + yThumbnail, + thumbnailData: this.bstream.readBytes(3 * xThumbnail * yThumbnail), + }; + this.dispatchEvent(createEvent(JpegParseEventType.APP0_MARKER, app0MarkerSegment)); + } + else if (identifier === 'JFXX') { + if (!this.hasApp0MarkerSegment) throw `JFXX found without JFIF`; + if (this.bstream.readNumber(1) !== 0) throw 'No null byte terminator for JFXX'; + + const thumbnailFormat = this.bstream.readNumber(1); + if (!Object.values(JpegExtensionThumbnailFormat).includes(thumbnailFormat)) { + throw `Bad Extension Thumbnail Format: ${thumbnailFormat}`; + } + + // The JFXX segment has length (2), 'JFXX' (4), null byte (1), thumbnail format (1) + const thumbnailData = this.bstream.readBytes(length - 8); + + /** @type {JpegApp0Extension} */ + let app0ExtensionSegment = { + thumbnailFormat, + thumbnailData, + }; + this.dispatchEvent(createEvent(JpegParseEventType.APP0_EXTENSION, app0ExtensionSegment)); + } + else { + throw `Bad APP0 identifier: ${identifier}`; + } + + this.bstream = skipAheadStream; + } // End of APP0 + else if (jpegMarker === JpegSegmentType.APP1) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + const skipAheadStream = this.bstream.tee().skip(length - 2); + + const identifier = this.bstream.readString(4); + if (identifier !== 'Exif') { + // TODO: Handle XMP. + // console.log(identifier + this.bstream.readString(length - 2 - 4)); + this.bstream = skipAheadStream; + continue; + } + if (this.bstream.readNumber(2) !== 0) throw `No null byte termination`; + + const exifValueMap = getExifProfile(this.bstream); + this.dispatchEvent(createEvent(JpegParseEventType.APP1_EXIF, exifValueMap)); + + this.bstream = skipAheadStream; + } // End of APP1 + else if (jpegMarker === JpegSegmentType.DQT) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + + const dqtLength = length - 2; + let ptr = 0; + while (ptr < dqtLength) { + // https://gist.github.com/FranckFreiburger/d8e7445245221c5cf38e69a88f22eeeb#file-getjpegquality-js-L76 + const firstByte = this.bstream.readNumber(1); + // Lower 4 bits are the component index. + const tableNumber = (firstByte & 0xF); + // Upper 4 bits are the precision (0=byte, 1=word). + const precision = ((firstByte & 0xF0) >> 4); + if (precision !== 0 && precision !== 1) throw `Weird value for DQT precision: ${precision}`; + + const valSize = precision === 0 ? 1 : 2; + const tableValues = new Array(64); + for (let v = 0; v < 64; ++v) { + tableValues[v] = this.bstream.readNumber(valSize); + } + + /** @type {JpegDefineQuantizationTable} */ + const table = { + tableNumber, + precision, + tableValues, + }; + this.dispatchEvent(createEvent(JpegParseEventType.DEFINE_QUANTIZATION_TABLE, table)); + + ptr += (1 + valSize * 64); + } + } // End of DQT + else if (jpegMarker === JpegSegmentType.DHT) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + let ptr = 2; + + while (ptr < length) { + const firstByte = this.bstream.readNumber(1); + const tableNumber = (firstByte & 0xF); + const tableType = ((firstByte & 0xF0) >> 4); + if (tableNumber > 3) throw `Weird DHT table number = ${tableNumber}`; + if (tableType !== 0 && tableType !== 1) throw `Weird DHT table type = ${tableType}`; + + const numberOfSymbols = Array.from(this.bstream.readBytes(16)); + let numCodes = 0; + for (let symbolLength = 1; symbolLength <= 16; ++symbolLength) { + const numSymbolsAtLength = numberOfSymbols[symbolLength - 1]; + numCodes += numSymbolsAtLength; + } + if (numCodes > 256) throw `Bad # of DHT codes: ${numCodes}`; + + const symbols = Array.from(this.bstream.readBytes(numCodes)); + + /** @type {JpegDefineHuffmanTable} */ + const table = { + tableNumber, + tableType, + numberOfSymbols, + symbols, + }; + this.dispatchEvent(createEvent(JpegParseEventType.DEFINE_HUFFMAN_TABLE, table)); + + ptr += (1 + 16 + numCodes); + } + if (ptr !== length) throw `Bad DHT ptr: ${ptr}!`; + } // End of DHT + else if (jpegMarker === JpegSegmentType.SOF0 + || jpegMarker === JpegSegmentType.SOF1 + || jpegMarker === JpegSegmentType.SOF2) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + + const dctType = (jpegMarker - JpegSegmentType.SOF0); + if (![0, 1, 2].includes(dctType)) throw `Weird DCT type: ${dctType}`; + + const dataPrecision = this.bstream.readNumber(1); + const imageHeight = this.bstream.readNumber(2); + const imageWidth = this.bstream.readNumber(2); + const numberOfComponents = this.bstream.readNumber(1); + const componentDetails = []; + for (let c = 0; c < numberOfComponents; ++c) { + const componentId = this.bstream.readNumber(1); + const nextByte = this.bstream.readNumber(1); + const verticalSamplingFactor = (nextByte & 0xF); + const horizontalSamplingFactor = ((nextByte & 0xF0) >> 4); + const quantizationTableNumber = this.bstream.readNumber(1); + + componentDetails.push({ + componentId, + verticalSamplingFactor, + horizontalSamplingFactor, + quantizationTableNumber, + }); + } + + /** @type {JpegStartOfFrame} */ + const sof = { + dctType, + dataPrecision, + imageHeight, + imageWidth, + numberOfComponents, + componentDetails, + }; + + this.dispatchEvent(createEvent(JpegParseEventType.START_OF_FRAME, sof)); + } // End of SOF0, SOF1, SOF2 + else if (jpegMarker === JpegSegmentType.SOS) { + this.bstream.setBigEndian(); + const length = this.bstream.readNumber(2); + // console.log(`Inside SOS with length = ${length}`); + if (length !== 12) throw `Bad length in SOS header: ${length}`; + + /** @type {JpegStartOfScan} */ + const sos = { + componentsInScan: this.bstream.readNumber(1), + componentSelectorY: this.bstream.readNumber(1), + huffmanTableSelectorY: this.bstream.readNumber(1), + componentSelectorCb: this.bstream.readNumber(1), + huffmanTableSelectorCb: this.bstream.readNumber(1), + componentSelectorCr: this.bstream.readNumber(1), + huffmanTableSelectorCr: this.bstream.readNumber(1), + scanStartPositionInBlock: this.bstream.readNumber(1), + scanEndPositionInBlock: this.bstream.readNumber(1), + successiveApproximationBitPosition: this.bstream.readNumber(1), + }; + + const rawImageDataStream = this.bstream.tee(); + let numBytes = 0; + // Immediately after SOS header is the compressed image data until the EOI marker is seen. + // Seek until we find the EOI marker. + while (true) { + if (this.bstream.readNumber(1) === 0xFF && + this.bstream.peekNumber(1) === JpegSegmentType.EOI) { + jpegMarker = this.bstream.readNumber(1); + break; + } else { + numBytes++; + } + } + + // NOTE: The below will have the null bytes after every 0xFF value. + sos.rawImageData = rawImageDataStream.readBytes(numBytes); + + this.dispatchEvent(createEvent(JpegParseEventType.START_OF_SCAN, sos)); + } // End of SOS + else { + this.bstream.setBigEndian(); + const length = this.bstream.peekNumber(2); + if (DEBUG) console.log(`Unsupported JPEG marker 0xff${jpegMarker.toString(16)} with length ${length}`); + this.bstream.skip(length); + } + } while (jpegMarker !== JpegSegmentType.EOI); + } +} diff --git a/image/parsers/parsers.js b/image/parsers/parsers.js new file mode 100644 index 0000000..44ff3b2 --- /dev/null +++ b/image/parsers/parsers.js @@ -0,0 +1,20 @@ +/* + * parsers.js + * + * Common functionality for all image parsers. + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + */ + +/** + * Creates a new event of the given type with the specified data. + * @template T + * @param {string} type The event type. + * @param {T} data The event data. + * @returns {CustomEvent} The new event. + */ +export function createEvent(type, data) { + return new CustomEvent(type, { detail: data }); +} diff --git a/image/parsers/png.js b/image/parsers/png.js new file mode 100644 index 0000000..3de45c4 --- /dev/null +++ b/image/parsers/png.js @@ -0,0 +1,731 @@ +/* + * png.js + * + * An event-based parser for PNG images. + * + * Licensed under the MIT License + * + * Copyright(c) 2024 Google Inc. + */ + +import { ByteStream } from '../../io/bytestream.js'; +import { getExifProfile } from './exif.js'; +import { createEvent } from './parsers.js'; + +/** @typedef {import('./exif.js').ExifValue} ExifValue */ + +// https://www.w3.org/TR/png-3/ +// https://en.wikipedia.org/wiki/PNG#File_format + +let DEBUG = false; + +const SIG = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + +/** @enum {string} */ +export const PngParseEventType = { + // Critical chunks. + IDAT: 'image_data', + IHDR: 'image_header', + PLTE: 'palette', + + // Ancillary chunks. + bKGD: 'background_color', + cHRM: 'chromaticities_white_point', + eXIf: 'exif_profile', + gAMA: 'image_gamma', + hIST: 'histogram', + iTXt: 'intl_text_data', + pHYs: 'physical_pixel_dims', + sBIT: 'significant_bits', + sPLT: 'suggested_palette', + tEXt: 'textual_data', + tIME: 'last_mod_time', + tRNS: 'transparency', + zTXt: 'compressed_textual_data', +}; + +/** @enum {number} */ +export const PngColorType = { + GREYSCALE: 0, + TRUE_COLOR: 2, + INDEXED_COLOR: 3, + GREYSCALE_WITH_ALPHA: 4, + TRUE_COLOR_WITH_ALPHA: 6, +}; + +/** @enum {number} */ +export const PngInterlaceMethod = { + NO_INTERLACE: 0, + ADAM7_INTERLACE: 1, +} + +/** + * @typedef PngImageHeader + * @property {number} width + * @property {number} height + * @property {number} bitDepth + * @property {PngColorType} colorType + * @property {number} compressionMethod + * @property {number} filterMethod + * @property {number} interlaceMethod + */ + +/** + * @typedef PngSignificantBits + * @property {number=} significant_greyscale Populated for color types 0, 4. + * @property {number=} significant_red Populated for color types 2, 3, 6. + * @property {number=} significant_green Populated for color types 2, 3, 6. + * @property {number=} significant_blue Populated for color types 2, 3, 6. + * @property {number=} significant_alpha Populated for color types 4, 6. + */ + +/** + * @typedef PngChromaticities + * @property {number} whitePointX + * @property {number} whitePointY + * @property {number} redX + * @property {number} redY + * @property {number} greenX + * @property {number} greenY + * @property {number} blueX + * @property {number} blueY + */ + +/** + * @typedef PngColor + * @property {number} red + * @property {number} green + * @property {number} blue + */ + +/** + * @typedef PngPalette + * @property {PngColor[]} entries + */ + +/** + * @typedef PngTransparency + * @property {number=} greySampleValue Populated for color type 0. + * @property {number=} redSampleValue Populated for color type 2. + * @property {number=} blueSampleValue Populated for color type 2. + * @property {number=} greenSampleValue Populated for color type 2. + * @property {number[]=} alphaPalette Populated for color type 3. + */ + +/** + * @typedef PngImageData + * @property {Uint8Array} rawImageData + */ + +/** + * @typedef PngTextualData + * @property {string} keyword + * @property {string=} textString + */ + +/** + * @typedef PngCompressedTextualData + * @property {string} keyword + * @property {number} compressionMethod Only value supported is 0 for deflate compression. + * @property {Uint8Array=} compressedText + */ + +/** + * @typedef PngIntlTextualData + * @property {string} keyword + * @property {number} compressionFlag 0 for uncompressed, 1 for compressed. + * @property {number} compressionMethod 0 means zlib defalt when compressionFlag is 1. + * @property {string=} languageTag + * @property {string=} translatedKeyword + * @property {Uint8Array} text The raw UTF-8 text (may be compressed). + */ + +/** + * @typedef PngBackgroundColor + * @property {number=} greyscale Only for color types 0 and 4. + * @property {number=} red Only for color types 2 and 6. + * @property {number=} green Only for color types 2 and 6. + * @property {number=} blue Only for color types 2 and 6. + * @property {number=} paletteIndex Only for color type 3. + */ + +/** + * @typedef PngLastModTime + * @property {number} year Four-digit year. + * @property {number} month One-based. Value from 1-12. + * @property {number} day One-based. Value from 1-31. + * @property {number} hour Zero-based. Value from 0-23. + * @property {number} minute Zero-based. Value from 0-59. + * @property {number} second Zero-based. Value from 0-60 to allow for leap-seconds. + */ + +export const PngUnitSpecifier = { + UNKNOWN: 0, + METRE: 1, +}; + +/** + * @typedef PngPhysicalPixelDimensions + * @property {number} pixelPerUnitX + * @property {number} pixelPerUnitY + * @property {PngUnitSpecifier} unitSpecifier + */ + +/** @typedef {Map} PngExifProfile */ + +/** + * @typedef PngHistogram + * @property {number[]} frequencies The # of frequencies matches the # of palette entries. + */ + +/** + * @typedef PngSuggestedPaletteEntry + * @property {number} red + * @property {number} green + * @property {number} blue + * @property {number} alpha + * @property {number} frequency + */ + +/** + * @typedef PngSuggestedPalette + * @property {string} paletteName + * @property {number} sampleDepth Either 8 or 16. + * @property {PngSuggestedPaletteEntry[]} entries + */ + +/** + * @typedef PngChunk Internal use only. + * @property {number} length + * @property {string} chunkType + * @property {ByteStream} chunkStream Do not read more than length! + * @property {number} crc + */ + +export class PngParser extends EventTarget { + /** + * @type {ByteStream} + * @private + */ + bstream; + + /** + * @type {PngColorType} + * @private + */ + colorType; + + /** + * @type {PngPalette} + * @private + */ + palette; + + /** @param {ArrayBuffer} ab */ + constructor(ab) { + super(); + this.bstream = new ByteStream(ab); + this.bstream.setBigEndian(); + } + + /** + * Type-safe way to bind a listener for a PngBackgroundColor. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onBackgroundColor(listener) { + super.addEventListener(PngParseEventType.bKGD, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngChromaticities. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onChromaticities(listener) { + super.addEventListener(PngParseEventType.cHRM, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngCompressedTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onCompressedTextualData(listener) { + super.addEventListener(PngParseEventType.zTXt, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngExifProfile. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onExifProfile(listener) { + super.addEventListener(PngParseEventType.eXIf, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngImageGamma. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onGamma(listener) { + super.addEventListener(PngParseEventType.gAMA, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngHistogram. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onHistogram(listener) { + super.addEventListener(PngParseEventType.hIST, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngImageData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onImageData(listener) { + super.addEventListener(PngParseEventType.IDAT, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngImageHeader. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onImageHeader(listener) { + super.addEventListener(PngParseEventType.IHDR, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngIntlTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onIntlTextualData(listener) { + super.addEventListener(PngParseEventType.iTXt, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngLastModTime. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onLastModTime(listener) { + super.addEventListener(PngParseEventType.tIME, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngPalette. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onPalette(listener) { + super.addEventListener(PngParseEventType.PLTE, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngPhysicalPixelDimensions. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onPhysicalPixelDimensions(listener) { + super.addEventListener(PngParseEventType.pHYs, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngSignificantBits. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onSignificantBits(listener) { + super.addEventListener(PngParseEventType.sBIT, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngSuggestedPalette. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onSuggestedPalette(listener) { + super.addEventListener(PngParseEventType.sPLT, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onTextualData(listener) { + super.addEventListener(PngParseEventType.tEXt, listener); + return this; + } + + /** + * Type-safe way to bind a listener for a PngTransparency. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onTransparency(listener) { + super.addEventListener(PngParseEventType.tRNS, listener); + return this; + } + + /** @returns {Promise} A Promise that resolves when the parsing is complete. */ + async start() { + const sigLength = SIG.byteLength; + const sig = this.bstream.readBytes(sigLength); + for (let sb = 0; sb < sigLength; ++sb) { + if (sig[sb] !== SIG[sb]) throw `Bad PNG signature: ${sig}`; + } + + /** @type {PngChunk} */ + let chunk; + do { + const length = this.bstream.readNumber(4); + chunk = { + length, + chunkType: this.bstream.readString(4), + chunkStream: this.bstream.tee(), + crc: this.bstream.skip(length).readNumber(4), + }; + + const chStream = chunk.chunkStream; + switch (chunk.chunkType) { + // https://www.w3.org/TR/png-3/#11IHDR + case 'IHDR': + if (this.colorType) throw `Found multiple IHDR chunks`; + /** @type {PngImageHeader} */ + const header = { + width: chStream.readNumber(4), + height: chStream.readNumber(4), + bitDepth: chStream.readNumber(1), + colorType: chStream.readNumber(1), + compressionMethod: chStream.readNumber(1), + filterMethod: chStream.readNumber(1), + interlaceMethod: chStream.readNumber(1), + }; + if (!Object.values(PngColorType).includes(header.colorType)) { + throw `Bad PNG color type: ${header.colorType}`; + } + if (header.compressionMethod !== 0) { + throw `Bad PNG compression method: ${header.compressionMethod}`; + } + if (header.filterMethod !== 0) { + throw `Bad PNG filter method: ${header.filterMethod}`; + } + if (!Object.values(PngInterlaceMethod).includes(header.interlaceMethod)) { + throw `Bad PNG interlace method: ${header.interlaceMethod}`; + } + + this.colorType = header.colorType; + + this.dispatchEvent(createEvent(PngParseEventType.IHDR, header)); + break; + + // https://www.w3.org/TR/png-3/#11gAMA + case 'gAMA': + if (length !== 4) throw `Bad length for gAMA: ${length}`; + this.dispatchEvent(createEvent(PngParseEventType.gAMA, chStream.readNumber(4))); + break; + + // https://www.w3.org/TR/png-3/#11bKGD + case 'bKGD': + if (this.colorType === undefined) throw `bKGD before IHDR`; + if (this.colorType === PngColorType.INDEXED_COLOR && !this.palette) throw `bKGD before PLTE`; + /** @type {PngBackgroundColor} */ + const bkgdColor = {}; + + if (this.colorType === PngColorType.GREYSCALE || + this.colorType === PngColorType.GREYSCALE_WITH_ALPHA) { + bkgdColor.greyscale = chStream.readNumber(2); + } else if (this.colorType === PngColorType.TRUE_COLOR || + this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) { + bkgdColor.red = chStream.readNumber(2); + bkgdColor.green = chStream.readNumber(2); + bkgdColor.blue = chStream.readNumber(2); + } else if (this.colorType === PngColorType.INDEXED_COLOR) { + bkgdColor.paletteIndex = chStream.readNumber(1); + } + + this.dispatchEvent(createEvent(PngParseEventType.bKGD, bkgdColor)); + break; + + // https://www.w3.org/TR/png-3/#11sBIT + case 'sBIT': + if (this.colorType === undefined) throw `sBIT before IHDR`; + /** @type {PngSignificantBits} */ + const sigBits = {}; + + const sbitBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`; + if (this.colorType === PngColorType.GREYSCALE) { + if (length !== 1) throw sbitBadLengthErr; + sigBits.significant_greyscale = chStream.readNumber(1); + } else if (this.colorType === PngColorType.TRUE_COLOR || + this.colorType === PngColorType.INDEXED_COLOR) { + if (length !== 3) throw sbitBadLengthErr; + sigBits.significant_red = chStream.readNumber(1); + sigBits.significant_green = chStream.readNumber(1); + sigBits.significant_blue = chStream.readNumber(1); + } else if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA) { + if (length !== 2) throw sbitBadLengthErr; + sigBits.significant_greyscale = chStream.readNumber(1); + sigBits.significant_alpha = chStream.readNumber(1); + } else if (this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) { + if (length !== 4) throw sbitBadLengthErr; + sigBits.significant_red = chStream.readNumber(1); + sigBits.significant_green = chStream.readNumber(1); + sigBits.significant_blue = chStream.readNumber(1); + sigBits.significant_alpha = chStream.readNumber(1); + } + + this.dispatchEvent(createEvent(PngParseEventType.sBIT, sigBits)); + break; + + // https://www.w3.org/TR/png-3/#11cHRM + case 'cHRM': + if (length !== 32) throw `Weird length for cHRM chunk: ${length}`; + + /** @type {PngChromaticities} */ + const chromaticities = { + whitePointX: chStream.readNumber(4), + whitePointY: chStream.readNumber(4), + redX: chStream.readNumber(4), + redY: chStream.readNumber(4), + greenX: chStream.readNumber(4), + greenY: chStream.readNumber(4), + blueX: chStream.readNumber(4), + blueY: chStream.readNumber(4), + }; + this.dispatchEvent(createEvent(PngParseEventType.cHRM, chromaticities)); + break; + + // https://www.w3.org/TR/png-3/#11PLTE + case 'PLTE': + if (this.colorType === undefined) throw `PLTE before IHDR`; + if (this.colorType === PngColorType.GREYSCALE || + this.colorType === PngColorType.GREYSCALE_WITH_ALPHA) throw `PLTE with greyscale`; + if (length % 3 !== 0) throw `PLTE length was not divisible by 3`; + + /** @type {PngColor[]} */ + const paletteEntries = []; + for (let p = 0; p < length / 3; ++p) { + paletteEntries.push({ + red: chStream.readNumber(1), + green: chStream.readNumber(1), + blue: chStream.readNumber(1), + }); + } + + /** @type {PngPalette} */ + this.palette = { + entries: paletteEntries, + }; + + this.dispatchEvent(createEvent(PngParseEventType.PLTE, this.palette)); + break; + + // https://www.w3.org/TR/png-3/#11pHYs + case 'pHYs': + /** @type {physicalPixelDimensions} */ + const pixelDims = { + pixelPerUnitX: chStream.readNumber(4), + pixelPerUnitY: chStream.readNumber(4), + unitSpecifier: chStream.readNumber(1), + }; + if (!Object.values(PngUnitSpecifier).includes(pixelDims.unitSpecifier)) { + throw `Bad pHYs unit specifier: ${pixelDims.unitSpecifier}`; + } + + this.dispatchEvent(createEvent(PngParseEventType.pHYs, pixelDims)); + break; + + // https://www.w3.org/TR/png-3/#11tEXt + case 'tEXt': + const byteArr = chStream.peekBytes(length); + const nullIndex = byteArr.indexOf(0); + /** @type {PngTextualData} */ + const textualData = { + keyword: chStream.readString(nullIndex), + textString: chStream.skip(1).readString(length - nullIndex - 1), + }; + this.dispatchEvent(createEvent(PngParseEventType.tEXt, textualData)); + break; + + // https://www.w3.org/TR/png-3/#11tIME + case 'tIME': + /** @type {PngLastModTime} */ + const lastModTime = { + year: chStream.readNumber(2), + month: chStream.readNumber(1), + day: chStream.readNumber(1), + hour: chStream.readNumber(1), + minute: chStream.readNumber(1), + second: chStream.readNumber(1), + }; + this.dispatchEvent(createEvent(PngParseEventType.tIME, lastModTime)); + break; + + // https://www.w3.org/TR/png-3/#11tRNS + case 'tRNS': + if (this.colorType === undefined) throw `tRNS before IHDR`; + if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA || + this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) { + throw `tRNS with color type ${this.colorType}`; + } + + /** @type {PngTransparency} */ + const transparency = {}; + + const trnsBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`; + if (this.colorType === PngColorType.GREYSCALE) { + if (length !== 2) throw trnsBadLengthErr; + transparency.greySampleValue = chStream.readNumber(2); + } else if (this.colorType === PngColorType.TRUE_COLOR) { + if (length !== 6) throw trnsBadLengthErr; + // Oddly the order is RBG instead of RGB :-/ + transparency.redSampleValue = chStream.readNumber(2); + transparency.blueSampleValue = chStream.readNumber(2); + transparency.greenSampleValue = chStream.readNumber(2); + } else if (this.colorType === PngColorType.INDEXED_COLOR) { + if (!this.palette) throw `tRNS before PLTE`; + if (length > this.palette.entries.length) throw `More tRNS entries than palette`; + + transparency.alphaPalette = []; + for (let a = 0; a < length; ++a) { + transparency.alphaPalette.push(chStream.readNumber(1)); + } + } + + this.dispatchEvent(createEvent(PngParseEventType.tRNS, transparency)); + break; + + // https://www.w3.org/TR/png-3/#11zTXt + case 'zTXt': + const compressedByteArr = chStream.peekBytes(length); + const compressedNullIndex = compressedByteArr.indexOf(0); + + /** @type {PngCompressedTextualData} */ + const compressedTextualData = { + keyword: chStream.readString(compressedNullIndex), + compressionMethod: chStream.skip(1).readNumber(1), + compressedText: chStream.readBytes(length - compressedNullIndex - 2), + }; + this.dispatchEvent(createEvent(PngParseEventType.zTXt, compressedTextualData)); + break; + + // https://www.w3.org/TR/png-3/#11iTXt + case 'iTXt': + const intlByteArr = chStream.peekBytes(length); + const intlNull0 = intlByteArr.indexOf(0); + const intlNull1 = intlByteArr.indexOf(0, intlNull0 + 1); + const intlNull2 = intlByteArr.indexOf(0, intlNull1 + 1); + if (intlNull0 === -1) throw `iTXt: Did not have one null`; + if (intlNull1 === -1) throw `iTXt: Did not have two nulls`; + if (intlNull2 === -1) throw `iTXt: Did not have three nulls`; + + /** @type {PngIntlTextualData} */ + const intlTextData = { + keyword: chStream.readString(intlNull0), + compressionFlag: chStream.skip(1).readNumber(1), + compressionMethod: chStream.readNumber(1), + languageTag: (intlNull1 - intlNull0 > 1) ? chStream.readString(intlNull1 - intlNull0 - 1) : undefined, + translatedKeyword: (intlNull2 - intlNull1 > 1) ? chStream.skip(1).readString(intlNull2 - intlNull1 - 1) : undefined, + text: chStream.skip(1).readBytes(length - intlNull2 - 1), + }; + + this.dispatchEvent(createEvent(PngParseEventType.iTXt, intlTextData)); + break; + + // https://www.w3.org/TR/png-3/#eXIf + case 'eXIf': + const exifValueMap = getExifProfile(chStream); + this.dispatchEvent(createEvent(PngParseEventType.eXIf, exifValueMap)); + break; + + // https://www.w3.org/TR/png-3/#11hIST + case 'hIST': + if (!this.palette) throw `hIST before PLTE`; + if (length !== this.palette.entries.length * 2) throw `Bad # of hIST frequencies: ${length / 2}`; + + /** @type {PngHistogram} */ + const hist = { frequencies: [] }; + for (let f = 0; f < this.palette.entries.length; ++f) { + hist.frequencies.push(chStream.readNumber(2)); + } + + this.dispatchEvent(createEvent(PngParseEventType.hIST, hist)); + break; + + // https://www.w3.org/TR/png-3/#11sPLT + case 'sPLT': + const spByteArr = chStream.peekBytes(length); + const spNameNullIndex = spByteArr.indexOf(0); + + /** @type {PngSuggestedPalette} */ + const sPalette = { + paletteName: chStream.readString(spNameNullIndex), + sampleDepth: chStream.skip(1).readNumber(1), + entries: [], + }; + + const sampleDepth = sPalette.sampleDepth; + if (![8, 16].includes(sampleDepth)) throw `Invalid sPLT sample depth: ${sampleDepth}`; + + const remainingByteLength = length - spNameNullIndex - 1 - 1; + const compByteLength = sPalette.sampleDepth === 8 ? 1 : 2; + const entryByteLength = 4 * compByteLength + 2; + if (remainingByteLength % entryByteLength !== 0) { + throw `Invalid # of bytes left in sPLT: ${remainingByteLength}`; + } + + const numEntries = remainingByteLength / entryByteLength; + for (let e = 0; e < numEntries; ++e) { + sPalette.entries.push({ + red: chStream.readNumber(compByteLength), + green: chStream.readNumber(compByteLength), + blue: chStream.readNumber(compByteLength), + alpha: chStream.readNumber(compByteLength), + frequency: chStream.readNumber(2), + }); + } + + this.dispatchEvent(createEvent(PngParseEventType.sPLT, sPalette)); + break; + + // https://www.w3.org/TR/png-3/#11IDAT + case 'IDAT': + /** @type {PngImageData} */ + const data = { + rawImageData: chStream.readBytes(chunk.length), + }; + this.dispatchEvent(createEvent(PngParseEventType.IDAT, data)); + break; + + case 'IEND': + break; + + default: + if (DEBUG) console.log(`Found an unhandled chunk: ${chunk.chunkType}`); + break; + } + } while (chunk.chunkType !== 'IEND'); + } +} diff --git a/image/webp-shim/webp-shim.js b/image/webp-shim/webp-shim.js index f9f607b..0a3122a 100644 --- a/image/webp-shim/webp-shim.js +++ b/image/webp-shim/webp-shim.js @@ -6,6 +6,8 @@ * Copyright(c) 2020 Google Inc. */ +// TODO(2.0): Remove this. It seems unnecessary given WebP is universally supported now. + const url = import.meta.url; if (!url.endsWith('/webp-shim.js')) { throw 'webp-shim must be loaded as webp-shim.js'; diff --git a/index.js b/index.js index e1b106b..37fff75 100644 --- a/index.js +++ b/index.js @@ -6,24 +6,60 @@ * Copyright(c) 2020 Google Inc. */ -/** - * @typedef {import('./codecs/codecs.js').ProbeStream} ProbeStream - */ -/** - * @typedef {import('./codecs/codecs.js').ProbeFormat} ProbeFormat - */ -/** - * @typedef {import('./codecs/codecs.js').ProbeInfo} ProbeInfo - */ +/** @typedef {import('./codecs/codecs.js').ProbeStream} ProbeStream */ +/** @typedef {import('./codecs/codecs.js').ProbeFormat} ProbeFormat */ +/** @typedef {import('./codecs/codecs.js').ProbeInfo} ProbeInfo */ + +/** @typedef {import('./image/parsers/gif.js').GifApplicationExtension} GifApplicationExtension */ +/** @typedef {import('./image/parsers/gif.js').GifColor} GifColor */ +/** @typedef {import('./image/parsers/gif.js').GifCommentExtension} GifCommentExtension */ +/** @typedef {import('./image/parsers/gif.js').GifGraphicControlExtension} GifGraphicControlExtension */ +/** @typedef {import('./image/parsers/gif.js').GifHeader} GifHeader */ +/** @typedef {import('./image/parsers/gif.js').GifLogicalScreen} GifLogicalScreen */ +/** @typedef {import('./image/parsers/gif.js').GifPlainTextExtension} GifPlainTextExtension */ +/** @typedef {import('./image/parsers/gif.js').GifTableBasedImage} GifTableBasedImage */ + +/** @typedef {import('./image/parsers/jpeg.js').JpegApp0Extension} JpegApp0Extension */ +/** @typedef {import('./image/parsers/jpeg.js').JpegApp0Marker} JpegApp0Marker */ +/** @typedef {import('./image/parsers/jpeg.js').JpegComponentDetail} JpegComponentDetail */ +/** @typedef {import('./image/parsers/jpeg.js').JpegDefineHuffmanTable} JpegDefineHuffmanTable */ +/** @typedef {import('./image/parsers/jpeg.js').JpegDefineQuantizationTable} JpegDefineQuantizationTable */ +/** @typedef {import('./image/parsers/jpeg.js').JpegStartOfFrame} JpegStartOfFrame */ +/** @typedef {import('./image/parsers/jpeg.js').JpegStartOfScan} JpegStartOfScan */ + +/** @typedef {import('./image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */ +/** @typedef {import('./image/parsers/png.js').PngChromaticities} PngChromaticies */ +/** @typedef {import('./image/parsers/png.js').PngColor} PngColor */ +/** @typedef {import('./image/parsers/png.js').PngCompressedTextualData} PngCompressedTextualData */ +/** @typedef {import('./image/parsers/png.js').PngHistogram} PngHistogram */ +/** @typedef {import('./image/parsers/png.js').PngImageData} PngImageData */ +/** @typedef {import('./image/parsers/png.js').PngImageGamma} PngImageGamma */ +/** @typedef {import('./image/parsers/png.js').PngImageHeader} PngImageHeader */ +/** @typedef {import('./image/parsers/png.js').PngIntlTextualData} PngIntlTextualData */ +/** @typedef {import('./image/parsers/png.js').PngLastModTime} PngLastModTime */ +/** @typedef {import('./image/parsers/png.js').PngPalette} PngPalette */ +/** @typedef {import('./image/parsers/png.js').PngPhysicalPixelDimensions} PngPhysicalPixelDimensions */ +/** @typedef {import('./image/parsers/png.js').PngSignificantBits} PngSignificantBits */ +/** @typedef {import('./image/parsers/png.js').PngSuggestedPalette} PngSuggestedPalette */ +/** @typedef {import('./image/parsers/png.js').PngSuggestedPaletteEntry} PngSuggestedPaletteEntry */ +/** @typedef {import('./image/parsers/png.js').PngTextualData} PngTextualData */ +/** @typedef {import('./image/parsers/png.js').PngTransparency} PngTransparency */ export { UnarchiveEvent, UnarchiveEventType, UnarchiveInfoEvent, UnarchiveErrorEvent, UnarchiveStartEvent, UnarchiveFinishEvent, UnarchiveProgressEvent, UnarchiveExtractEvent, Unarchiver, Unzipper, Unrarrer, Untarrer, getUnarchiver -} from './archive/archive.js'; +} from './archive/decompress.js'; export { getFullMIMEString, getShortMIMEString } from './codecs/codecs.js'; export { findMimeType } from './file/sniffer.js'; +export { GifParseEventType, GifParser } from './image/parsers/gif.js'; +export { JpegComponentType, JpegDctType, JpegDensityUnits, JpegExtensionThumbnailFormat, + JpegHuffmanTableType, JpegParseEventType, JpegParser, + JpegSegmentType } from './image/parsers/jpeg.js'; +export { PngColorType, PngInterlaceMethod, PngParseEventType, PngParser, + PngUnitSpecifier } from './image/parsers/png.js'; export { convertWebPtoPNG, convertWebPtoJPG } from './image/webp-shim/webp-shim.js'; +export { BitBuffer } from './io/bitbuffer.js'; export { BitStream } from './io/bitstream.js'; export { ByteBuffer } from './io/bytebuffer.js'; export { ByteStream } from './io/bytestream.js'; diff --git a/io/README.md b/io/README.md deleted file mode 100644 index 3aab370..0000000 --- a/io/README.md +++ /dev/null @@ -1,2 +0,0 @@ -These generated files exist because Firefox does not support Worker Modules yet. -See https://bugzilla.mozilla.org/show_bug.cgi?id=1247687. diff --git a/io/bitbuffer-worker.js b/io/bitbuffer-worker.js deleted file mode 100644 index 191439d..0000000 --- a/io/bitbuffer-worker.js +++ /dev/null @@ -1,204 +0,0 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; -bitjs.io.BitBuffer = -/* - * bytebuffer-def.js - * - * Provides a writer for bits. - * - * Licensed under the MIT License - * - * Copyright(c) 2021 Google Inc. - */ - -(function () { - const BITMASK = [ - 0, - 0b00000001, - 0b00000011, - 0b00000111, - 0b00001111, - 0b00011111, - 0b00111111, - 0b01111111, - 0b11111111, - ] - - /** - * A write-only Bit buffer which uses a Uint8Array as a backing store. - */ - class BitBuffer { - /** - * @param {number} numBytes The number of bytes to allocate. - * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to - * least-significant (0). Defaults false: least-significant (0) to most-significant (8). - */ - constructor(numBytes, mtl = false) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); - - /** - * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most - * significant bit packing). - * @type {boolean} - * @private - */ - this.mtl = mtl; - - /** - * The current byte we are filling with bits. - * @type {number} - * @public - */ - this.bytePtr = 0; - - /** - * Points at the bit within the current byte where the next bit will go. This number ranges - * from 0 to 7 and the direction of packing is indicated by the mtl property. - * @type {number} - * @public - */ - this.bitPtr = this.mtl ? 7 : 0; - } - - /** @returns {boolean} */ - getPackingDirection() { - return this.mtl; - } - - /** - * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to - * most-significant (7). Changing the bit-packing direction when the bit pointer is in the - * middle of a byte will fill the rest of that byte with 0s using the current bit-packing - * direction and then set the bit pointer to the appropriate bit of the next byte. If there - * are no more bytes left in this buffer, it will throw an error. - */ - setPackingDirection(mtl = false) { - if (this.mtl !== mtl) { - if (this.mtl && this.bitPtr !== 7) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 7; - } else if (!this.mtl && this.bitPtr !== 0) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 0; - } - } - - this.mtl = mtl; - } - - /** - * writeBits(3, 6) is the same as writeBits(0b000011, 6). - * Will throw an error (without writing) if this would over-flow the buffer. - * @param {number} val The bits to pack into the buffer. Negative values are not allowed. - * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since - * JavaScript can only support 53-bit integers. - */ - writeBits(val, numBits) { - if (val < 0 || typeof val !== typeof 1) { - throw `Trying to write an invalid value into the BitBuffer: ${val}`; - } - if (numBits < 0 || numBits > 53) { - throw `Trying to write ${numBits} bits into the BitBuffer`; - } - - const totalBitsInBuffer = this.data.byteLength * 8; - const writtenBits = this.bytePtr * 8 + this.bitPtr; - const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; - if (numBits > bitsLeftInBuffer) { - throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; - } - - // Least-to-most-significant bit packing method (LTM). - if (!this.mtl) { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = 8 - this.bitPtr; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in LTM bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at LSB. */ - let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - actualBitsToWrite <<= this.bitPtr; - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; - } - // Update the bit/byte pointers and remaining bits to write. - this.bitPtr += numBitsToWriteIntoThisByte; - if (this.bitPtr > 7) { - if (this.bitPtr !== 8) { - throw `Error in LTM bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 0; - } - // Remove bits that have been written from LSB end. - val >>= numBitsToWriteIntoThisByte; - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; - } - } - // Most-to-least-significant bit packing method (MTL). - else { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = this.bitPtr + 1; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in MTL bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at MSB. */ - let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - // If the number of bits left to write do not fill up this byte, we need to shift these - // bits to the left so they are written into the proper place in the buffer. - if (numBitsLeftToWrite < bitCapacityInThisByte) { - actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); - } - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; - } - // Update the bit/byte pointers and remaining bits to write - this.bitPtr -= numBitsToWriteIntoThisByte; - if (this.bitPtr < 0) { - if (this.bitPtr !== -1) { - throw `Error in MTL bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 7; - } - // Remove bits that have been written from MSB end. - val -= (actualBitsToWrite << numExcessBits); - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; - } - } - } - } - - return BitBuffer; -})(); diff --git a/io/bitbuffer.js b/io/bitbuffer.js index 836b4d1..d94f64b 100644 --- a/io/bitbuffer.js +++ b/io/bitbuffer.js @@ -1,202 +1,199 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -export const BitBuffer = /* - * bytebuffer-def.js + * bitbuffer.js * - * Provides a writer for bits. + * Provides writer for bits. * * Licensed under the MIT License * - * Copyright(c) 2021 Google Inc. + * Copyright(c) 2023 Google Inc. + * Copyright(c) 2011 antimatter15 */ -(function () { - const BITMASK = [ - 0, - 0b00000001, - 0b00000011, - 0b00000111, - 0b00001111, - 0b00011111, - 0b00111111, - 0b01111111, - 0b11111111, - ] +const BITMASK = [ + 0, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, +] +/** + * A write-only Bit buffer which uses a Uint8Array as a backing store. + */ +export class BitBuffer { /** - * A write-only Bit buffer which uses a Uint8Array as a backing store. + * @param {number} numBytes The number of bytes to allocate. + * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to + * least-significant (0). Defaults false: least-significant (0) to most-significant (8). */ - class BitBuffer { + constructor(numBytes, mtl = false) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! BitBuffer initialized with '" + numBytes + "'"; + } + /** - * @param {number} numBytes The number of bytes to allocate. - * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to - * least-significant (0). Defaults false: least-significant (0) to most-significant (8). + * @type {Uint8Array} + * @public */ - constructor(numBytes, mtl = false) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } + this.data = new Uint8Array(numBytes); - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); + /** + * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most + * significant bit packing). + * @type {boolean} + * @private + */ + this.mtl = mtl; - /** - * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most - * significant bit packing). - * @type {boolean} - * @private - */ - this.mtl = mtl; + /** + * The current byte we are filling with bits. + * @type {number} + * @public + */ + this.bytePtr = 0; - /** - * The current byte we are filling with bits. - * @type {number} - * @public - */ - this.bytePtr = 0; + /** + * Points at the bit within the current byte where the next bit will go. This number ranges + * from 0 to 7 and the direction of packing is indicated by the mtl property. + * @type {number} + * @public + */ + this.bitPtr = this.mtl ? 7 : 0; + } - /** - * Points at the bit within the current byte where the next bit will go. This number ranges - * from 0 to 7 and the direction of packing is indicated by the mtl property. - * @type {number} - * @public - */ - this.bitPtr = this.mtl ? 7 : 0; - } + // TODO: Be consistent with naming across classes (big-endian and little-endian). - /** @returns {boolean} */ - getPackingDirection() { - return this.mtl; - } + /** @returns {boolean} */ + getPackingDirection() { + return this.mtl; + } - /** - * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to - * most-significant (7). Changing the bit-packing direction when the bit pointer is in the - * middle of a byte will fill the rest of that byte with 0s using the current bit-packing - * direction and then set the bit pointer to the appropriate bit of the next byte. If there - * are no more bytes left in this buffer, it will throw an error. - */ - setPackingDirection(mtl = false) { - if (this.mtl !== mtl) { - if (this.mtl && this.bitPtr !== 7) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 7; - } else if (!this.mtl && this.bitPtr !== 0) { - this.bytePtr++; - if (this.bytePtr >= this.data.byteLength) { - throw `No more bytes left when switching packing direction`; - } - this.bitPtr = 0; + /** + * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to + * most-significant (7). Changing the bit-packing direction when the bit pointer is in the + * middle of a byte will fill the rest of that byte with 0s using the current bit-packing + * direction and then set the bit pointer to the appropriate bit of the next byte. If there + * are no more bytes left in this buffer, it will throw an error. + */ + setPackingDirection(mtl = false) { + if (this.mtl !== mtl) { + if (this.mtl && this.bitPtr !== 7) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; + } + this.bitPtr = 0; + } else if (!this.mtl && this.bitPtr !== 0) { + this.bytePtr++; + if (this.bytePtr >= this.data.byteLength) { + throw `No more bytes left when switching packing direction`; } + this.bitPtr = 7; } - - this.mtl = mtl; } - /** - * writeBits(3, 6) is the same as writeBits(0b000011, 6). - * Will throw an error (without writing) if this would over-flow the buffer. - * @param {number} val The bits to pack into the buffer. Negative values are not allowed. - * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since - * JavaScript can only support 53-bit integers. - */ - writeBits(val, numBits) { - if (val < 0 || typeof val !== typeof 1) { - throw `Trying to write an invalid value into the BitBuffer: ${val}`; - } - if (numBits < 0 || numBits > 53) { - throw `Trying to write ${numBits} bits into the BitBuffer`; - } + this.mtl = mtl; + } - const totalBitsInBuffer = this.data.byteLength * 8; - const writtenBits = this.bytePtr * 8 + this.bitPtr; - const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; - if (numBits > bitsLeftInBuffer) { - throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; - } + /** + * writeBits(3, 6) is the same as writeBits(0b000011, 6). + * Will throw an error (without writing) if this would over-flow the buffer. + * @param {number} val The bits to pack into the buffer. Negative values are not allowed. + * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since + * JavaScript can only support 53-bit integers. + */ + writeBits(val, numBits) { + if (val < 0 || typeof val !== typeof 1) { + throw `Trying to write an invalid value into the BitBuffer: ${val}`; + } + if (numBits < 0 || numBits > 53) { + throw `Trying to write ${numBits} bits into the BitBuffer`; + } - // Least-to-most-significant bit packing method (LTM). - if (!this.mtl) { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = 8 - this.bitPtr; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in LTM bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at LSB. */ - let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - actualBitsToWrite <<= this.bitPtr; - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; - } - // Update the bit/byte pointers and remaining bits to write. - this.bitPtr += numBitsToWriteIntoThisByte; - if (this.bitPtr > 7) { - if (this.bitPtr !== 8) { - throw `Error in LTM bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 0; + const totalBitsInBuffer = this.data.byteLength * 8; + const writtenBits = this.bytePtr * 8 + this.bitPtr; + const bitsLeftInBuffer = totalBitsInBuffer - writtenBits; + if (numBits > bitsLeftInBuffer) { + throw `Trying to write ${numBits} into the BitBuffer that only has ${bitsLeftInBuffer}`; + } + + // Least-to-most-significant bit packing method (LTM). + if (!this.mtl) { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = 8 - this.bitPtr; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in LTM bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at LSB. */ + let actualBitsToWrite = (val & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + actualBitsToWrite <<= this.bitPtr; + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write. + this.bitPtr += numBitsToWriteIntoThisByte; + if (this.bitPtr > 7) { + if (this.bitPtr !== 8) { + throw `Error in LTM bit packing. Tried to write more bits than it should have.`; } - // Remove bits that have been written from LSB end. - val >>= numBitsToWriteIntoThisByte; - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + this.bytePtr++; + this.bitPtr = 0; } + // Remove bits that have been written from LSB end. + val >>= numBitsToWriteIntoThisByte; + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; } - // Most-to-least-significant bit packing method (MTL). - else { - let numBitsLeftToWrite = numBits; - while (numBitsLeftToWrite > 0) { - /** The number of bits available to fill in this byte. */ - const bitCapacityInThisByte = this.bitPtr + 1; - /** The number of bits of val we will write into this byte. */ - const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); - /** The number of bits that fit in subsequent bytes. */ - const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; - if (numExcessBits < 0) { - throw `Error in MTL bit packing, # of excess bits is negative`; - } - /** The actual bits that need to be written into this byte. Starts at MSB. */ - let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); - // Only adjust and write bits if any are set to 1. - if (actualBitsToWrite > 0) { - // If the number of bits left to write do not fill up this byte, we need to shift these - // bits to the left so they are written into the proper place in the buffer. - if (numBitsLeftToWrite < bitCapacityInThisByte) { - actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); - } - // Now write into the buffer. - this.data[this.bytePtr] |= actualBitsToWrite; + } + // Most-to-least-significant bit packing method (MTL). + else { + let numBitsLeftToWrite = numBits; + while (numBitsLeftToWrite > 0) { + /** The number of bits available to fill in this byte. */ + const bitCapacityInThisByte = this.bitPtr + 1; + /** The number of bits of val we will write into this byte. */ + const numBitsToWriteIntoThisByte = Math.min(numBitsLeftToWrite, bitCapacityInThisByte); + /** The number of bits that fit in subsequent bytes. */ + const numExcessBits = numBitsLeftToWrite - numBitsToWriteIntoThisByte; + if (numExcessBits < 0) { + throw `Error in MTL bit packing, # of excess bits is negative`; + } + /** The actual bits that need to be written into this byte. Starts at MSB. */ + let actualBitsToWrite = ((val >> numExcessBits) & BITMASK[numBitsToWriteIntoThisByte]); + // Only adjust and write bits if any are set to 1. + if (actualBitsToWrite > 0) { + // If the number of bits left to write do not fill up this byte, we need to shift these + // bits to the left so they are written into the proper place in the buffer. + if (numBitsLeftToWrite < bitCapacityInThisByte) { + actualBitsToWrite <<= (bitCapacityInThisByte - numBitsLeftToWrite); } - // Update the bit/byte pointers and remaining bits to write - this.bitPtr -= numBitsToWriteIntoThisByte; - if (this.bitPtr < 0) { - if (this.bitPtr !== -1) { - throw `Error in MTL bit packing. Tried to write more bits than it should have.`; - } - this.bytePtr++; - this.bitPtr = 7; + // Now write into the buffer. + this.data[this.bytePtr] |= actualBitsToWrite; + } + // Update the bit/byte pointers and remaining bits to write + this.bitPtr -= numBitsToWriteIntoThisByte; + if (this.bitPtr < 0) { + if (this.bitPtr !== -1) { + throw `Error in MTL bit packing. Tried to write more bits than it should have.`; } - // Remove bits that have been written from MSB end. - val -= (actualBitsToWrite << numExcessBits); - numBitsLeftToWrite -= numBitsToWriteIntoThisByte; + this.bytePtr++; + this.bitPtr = 7; } + // Remove bits that have been written from MSB end. + val -= (actualBitsToWrite << numExcessBits); + numBitsLeftToWrite -= numBitsToWriteIntoThisByte; } } } - - return BitBuffer; -})(); +} diff --git a/io/bitstream-worker.js b/io/bitstream-worker.js deleted file mode 100644 index f59a658..0000000 --- a/io/bitstream-worker.js +++ /dev/null @@ -1,316 +0,0 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; -bitjs.io.BitStream = -/* - * bitstream-def.js - * - * Provides readers for bitstreams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - // mask for getting N number of bits (0-8) - const BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; - - /** - * This object allows you to peek and consume bits and bytes out of a stream. - * Note that this stream is optimized, and thus, will *NOT* throw an error if - * the end of the stream is reached. Only use this in scenarios where you - * already have all the bits you need. - * - * Bit reading always proceeds from the first byte in the buffer, to the - * second byte, and so on. The MTL flag controls which bit is considered - * first *inside* the byte. - * - * An Example for how Most-To-Least vs Least-to-Most mode works: - * - * If you have an ArrayBuffer with the following two Uint8s: - * 185 (0b10111001) and 66 (0b01000010) - * and you perform a series of readBits: 2 bits, then 3, then 5, then 6. - * - * A BitStream in "mtl" mode will yield the following: - * - readBits(2) => 2 ('10') - * - readBits(3) => 7 ('111') - * - readBits(5) => 5 ('00101') - * - readBits(6) => 2 ('000010') - * - * A BitStream in "ltm" mode will yield the following: - * - readBits(2) => 1 ('01') - * - readBits(3) => 6 ('110') - * - readBits(5) => 21 ('10101') - * - readBits(6) => 16 ('010000') - */ - class BitStream { - /** - * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} mtl Whether the stream reads bits from the byte starting with the - * most-significant-bit (bit 7) to least-significant (bit 0). False means the direction is - * from least-significant-bit (bit 0) to most-significant (bit 7). - * @param {Number} opt_offset The offset into the ArrayBuffer - * @param {Number} opt_length The length of this BitStream - */ - constructor(ab, mtl, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The byte in the stream that we are currently on. - * @type {Number} - * @private - */ - this.bytePtr = 0; - - /** - * The bit in the current byte that we will read next (can have values 0 through 7). - * @type {Number} - * @private - */ - this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bitsRead_ = 0; - - this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; - } - - /** - * Returns how many bites have been read in the stream since the beginning of time. - * @returns {number} - */ - getNumBitsRead() { - return this.bitsRead_; - } - - /** - * Returns how many bits are currently in the stream left to be read. - * @returns {number} - */ - getNumBitsLeft() { - const bitsLeftInByte = 8 - this.bitPtr; - return (this.bytes.byteLength - this.bytePtr - 1) * 8 + bitsLeftInByte; - } - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek, must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_ltm(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } - - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - let bitsIn = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return what we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - const mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bytePtr++; - bitPtr = 0; - bitsIn += numBitsLeftInThisByte; - num -= numBitsLeftInThisByte; - } else { - const mask = (BITMASK[num] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bitPtr += num; - break; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; - } - - return result; - } - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_mtl(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } - - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return the bits we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - result <<= numBitsLeftInThisByte; - result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); - bytePtr++; - bitPtr = 0; - num -= numBitsLeftInThisByte; - } else { - result <<= num; - const numBits = 8 - num - bitPtr; - result |= ((bytes[bytePtr] & (BITMASK[num] << numBits)) >> numBits); - - bitPtr += num; - break; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; - } - - return result; - } - - /** - * Peek at 16 bits from current position in the buffer. - * Bit at (bytePtr,bitPtr) has the highest position in returning data. - * Taken from getbits.hpp in unrar. - * TODO: Move this out of BitStream and into unrar. - * @returns {number} - */ - getBits() { - return (((((this.bytes[this.bytePtr] & 0xff) << 16) + - ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + - ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); - } - - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. Must be a positive integer. - * @returns {number} The read bits, as an unsigned number. - */ - readBits(n) { - return this.peekBits(n, true); - } - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, opt_movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer: ' + n; - } else if (num === 0) { - return new Uint8Array(); - } - - // Flush bits until we are byte-aligned. - // from http://tools.ietf.org/html/rfc1951#page-11 - // "Any bits of input up to the next byte boundary are ignored." - while (this.bitPtr != 0) { - this.readBits(1); - } - - const numBytesLeft = this.getNumBitsLeft() / 8; - if (num > numBytesLeft) { - throw 'Error! Overflowed the bit stream! n=' + num + ', bytePtr=' + this.bytePtr + - ', bytes.length=' + this.bytes.length + ', bitPtr=' + this.bitPtr; - } - - const movePointers = opt_movePointers || false; - const result = new Uint8Array(num); - let bytes = this.bytes; - let ptr = this.bytePtr; - let bytesLeftToCopy = num; - while (bytesLeftToCopy > 0) { - const bytesLeftInStream = bytes.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInStream); - - result.set(bytes.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); - - ptr += sourceLength; - // Overflowed the stream, just return what we got. - if (ptr >= bytes.length) { - break; - } - - bytesLeftToCopy -= sourceLength; - } - - if (movePointers) { - this.bytePtr += num; - this.bitsRead_ += (num * 8); - } - - return result; - } - - /** - * @param {number} n The number of bytes to read. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); - } - } - - return BitStream; -})(); diff --git a/io/bitstream.js b/io/bitstream.js index b2eabd6..22906fd 100644 --- a/io/bitstream.js +++ b/io/bitstream.js @@ -1,314 +1,338 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -export const BitStream = /* - * bitstream-def.js + * bitstream.js * - * Provides readers for bitstreams. + * A pull stream for binary bits. * * Licensed under the MIT License * - * Copyright(c) 2011 Google Inc. + * Copyright(c) 2023 Google Inc. * Copyright(c) 2011 antimatter15 */ -(function () { - // mask for getting N number of bits (0-8) - const BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; +// mask for getting N number of bits (0-8) +const BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; +/** + * This object allows you to peek and consume bits and bytes out of a stream. + * Note that this stream is optimized, and thus, will *NOT* throw an error if + * the end of the stream is reached. Only use this in scenarios where you + * already have all the bits you need. + * + * Bit reading always proceeds from the first byte in the buffer, to the + * second byte, and so on. The MTL flag controls which bit is considered + * first *inside* the byte. The default is least-to-most direction. + * + * An Example for how Most-To-Least vs Least-to-Most mode works: + * + * If you have an ArrayBuffer with the following two Uint8s: + * 185 (0b10111001) and 66 (0b01000010) + * and you perform a series of readBits: 2 bits, then 3, then 5, then 6. + * + * A BitStream in "mtl" mode will yield the following: + * - readBits(2) => 2 ('10') + * - readBits(3) => 7 ('111') + * - readBits(5) => 5 ('00101') + * - readBits(6) => 2 ('000010') + * + * A BitStream in "ltm" mode will yield the following: + * - readBits(2) => 1 ('01') + * - readBits(3) => 6 ('110') + * - readBits(5) => 21 ('10101') + * - readBits(6) => 16 ('010000') + */ +export class BitStream { /** - * This object allows you to peek and consume bits and bytes out of a stream. - * Note that this stream is optimized, and thus, will *NOT* throw an error if - * the end of the stream is reached. Only use this in scenarios where you - * already have all the bits you need. - * - * Bit reading always proceeds from the first byte in the buffer, to the - * second byte, and so on. The MTL flag controls which bit is considered - * first *inside* the byte. - * - * An Example for how Most-To-Least vs Least-to-Most mode works: - * - * If you have an ArrayBuffer with the following two Uint8s: - * 185 (0b10111001) and 66 (0b01000010) - * and you perform a series of readBits: 2 bits, then 3, then 5, then 6. - * - * A BitStream in "mtl" mode will yield the following: - * - readBits(2) => 2 ('10') - * - readBits(3) => 7 ('111') - * - readBits(5) => 5 ('00101') - * - readBits(6) => 2 ('000010') - * - * A BitStream in "ltm" mode will yield the following: - * - readBits(2) => 1 ('01') - * - readBits(3) => 6 ('110') - * - readBits(5) => 21 ('10101') - * - readBits(6) => 16 ('010000') + * @param {ArrayBuffer} ab An ArrayBuffer object. + * @param {boolean} mtl Whether the stream reads bits from the byte starting with the + * most-significant-bit (bit 7) to least-significant (bit 0). False means the direction is + * from least-significant-bit (bit 0) to most-significant (bit 7). + * @param {Number} opt_offset The offset into the ArrayBuffer + * @param {Number} opt_length The length of this BitStream */ - class BitStream { + constructor(ab, mtl, opt_offset, opt_length) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! BitStream constructed with an invalid ArrayBuffer object'; + } + + const offset = opt_offset || 0; + const length = opt_length || ab.byteLength; + /** - * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. - * @param {boolean} mtl Whether the stream reads bits from the byte starting with the - * most-significant-bit (bit 7) to least-significant (bit 0). False means the direction is - * from least-significant-bit (bit 0) to most-significant (bit 7). - * @param {Number} opt_offset The offset into the ArrayBuffer - * @param {Number} opt_length The length of this BitStream + * The bytes in the stream. + * @type {Uint8Array} + * @private */ - constructor(ab, mtl, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The byte in the stream that we are currently on. - * @type {Number} - * @private - */ - this.bytePtr = 0; - - /** - * The bit in the current byte that we will read next (can have values 0 through 7). - * @type {Number} - * @private - */ - this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bitsRead_ = 0; - - this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; - } + this.bytes = new Uint8Array(ab, offset, length); /** - * Returns how many bites have been read in the stream since the beginning of time. - * @returns {number} + * The byte in the stream that we are currently on. + * @type {Number} + * @private */ - getNumBitsRead() { - return this.bitsRead_; - } + this.bytePtr = 0; /** - * Returns how many bits are currently in the stream left to be read. - * @returns {number} + * The bit in the current byte that we will read next (can have values 0 through 7). + * @type {Number} + * @private */ - getNumBitsLeft() { - const bitsLeftInByte = 8 - this.bitPtr; - return (this.bytes.byteLength - this.bytePtr - 1) * 8 + bitsLeftInByte; - } + this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek, must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. + * An ever-increasing number. + * @type {Number} + * @private */ - peekBits_ltm(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } + this.bitsRead_ = 0; - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - let bitsIn = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return what we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - const mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bytePtr++; - bitPtr = 0; - bitsIn += numBitsLeftInThisByte; - num -= numBitsLeftInThisByte; - } else { - const mask = (BITMASK[num] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bitPtr += num; - break; - } - } + this.peekBits = mtl ? this.peekBits_mtl : this.peekBits_ltm; + } - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; - } + /** + * Returns how many bits have been read in the stream since the beginning of time. + * @returns {number} + */ + getNumBitsRead() { + return this.bitsRead_; + } - return result; - } + /** + * Returns how many bits are currently in the stream left to be read. + * @returns {number} + */ + getNumBitsLeft() { + const bitsLeftInByte = 8 - this.bitPtr; + return (this.bytes.byteLength - this.bytePtr - 1) * 8 + bitsLeftInByte; + } - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_mtl(n, opt_movePointers) { - const NUM = parseInt(n, 10); - let num = NUM; - if (n !== num || num <= 0) { - return 0; - } + /** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches + * bit7 of byte0, then jumps to bit0 of byte1, etc. + * @param {number} n The number of bits to peek, must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {number} The peeked bits, as an unsigned number. + */ + peekBits_ltm(n, opt_movePointers) { + const NUM = parseInt(n, 10); + let num = NUM; - const movePointers = opt_movePointers || false; - let bytes = this.bytes; - let bytePtr = this.bytePtr; - let bitPtr = this.bitPtr; - let result = 0; - - // keep going until we have no more bits left to peek at - while (num > 0) { - // We overflowed the stream, so just return the bits we got. - if (bytePtr >= bytes.length) { - break; - } - - const numBitsLeftInThisByte = (8 - bitPtr); - if (num >= numBitsLeftInThisByte) { - result <<= numBitsLeftInThisByte; - result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); - bytePtr++; - bitPtr = 0; - num -= numBitsLeftInThisByte; - } else { - result <<= num; - const numBits = 8 - num - bitPtr; - result |= ((bytes[bytePtr] & (BITMASK[num] << numBits)) >> numBits); - - bitPtr += num; - break; - } - } + // TODO: Handle this consistently between ByteStream and BitStream. ByteStream throws an error. + if (n !== num || num <= 0) { + return 0; + } - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - this.bitsRead_ += NUM; + const movePointers = opt_movePointers || false; + let bytes = this.bytes; + let bytePtr = this.bytePtr; + let bitPtr = this.bitPtr; + let result = 0; + let bitsIn = 0; + + // keep going until we have no more bits left to peek at + while (num > 0) { + // We overflowed the stream, so just return what we got. + if (bytePtr >= bytes.length) { + break; } - return result; + const numBitsLeftInThisByte = (8 - bitPtr); + if (num >= numBitsLeftInThisByte) { + const mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bytePtr++; + bitPtr = 0; + bitsIn += numBitsLeftInThisByte; + num -= numBitsLeftInThisByte; + } else { + const mask = (BITMASK[num] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bitPtr += num; + break; + } } - /** - * Peek at 16 bits from current position in the buffer. - * Bit at (bytePtr,bitPtr) has the highest position in returning data. - * Taken from getbits.hpp in unrar. - * TODO: Move this out of BitStream and into unrar. - * @returns {number} - */ - getBits() { - return (((((this.bytes[this.bytePtr] & 0xff) << 16) + - ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + - ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + this.bitsRead_ += NUM; } - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. Must be a positive integer. - * @returns {number} The read bits, as an unsigned number. - */ - readBits(n) { - return this.peekBits(n, true); + return result; + } + + /** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at bit7 of byte0 and moves right until it reaches + * bit0 of byte0, then goes to bit7 of byte1, etc. + * @param {number} n The number of bits to peek. Must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {number} The peeked bits, as an unsigned number. + */ + peekBits_mtl(n, opt_movePointers) { + const NUM = parseInt(n, 10); + let num = NUM; + + // TODO: Handle this consistently between ByteStream and BitStream. ByteStream throws an error. + if (n !== num || num <= 0) { + return 0; } - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, opt_movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer: ' + n; - } else if (num === 0) { - return new Uint8Array(); + const movePointers = opt_movePointers || false; + let bytes = this.bytes; + let bytePtr = this.bytePtr; + let bitPtr = this.bitPtr; + let result = 0; + + // keep going until we have no more bits left to peek at + while (num > 0) { + // We overflowed the stream, so just return the bits we got. + if (bytePtr >= bytes.length) { + break; } - // Flush bits until we are byte-aligned. - // from http://tools.ietf.org/html/rfc1951#page-11 - // "Any bits of input up to the next byte boundary are ignored." - while (this.bitPtr != 0) { - this.readBits(1); + const numBitsLeftInThisByte = (8 - bitPtr); + if (num >= numBitsLeftInThisByte) { + result <<= numBitsLeftInThisByte; + result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); + bytePtr++; + bitPtr = 0; + num -= numBitsLeftInThisByte; + } else { + result <<= num; + const numBits = 8 - num - bitPtr; + result |= ((bytes[bytePtr] & (BITMASK[num] << numBits)) >> numBits); + + bitPtr += num; + break; } + } - const numBytesLeft = this.getNumBitsLeft() / 8; - if (num > numBytesLeft) { - throw 'Error! Overflowed the bit stream! n=' + num + ', bytePtr=' + this.bytePtr + - ', bytes.length=' + this.bytes.length + ', bitPtr=' + this.bitPtr; - } + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + this.bitsRead_ += NUM; + } - const movePointers = opt_movePointers || false; - const result = new Uint8Array(num); - let bytes = this.bytes; - let ptr = this.bytePtr; - let bytesLeftToCopy = num; - while (bytesLeftToCopy > 0) { - const bytesLeftInStream = bytes.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInStream); + return result; + } - result.set(bytes.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + /** + * Peek at 16 bits from current position in the buffer. + * Bit at (bytePtr,bitPtr) has the highest position in returning data. + * Taken from getbits.hpp in unrar. + * TODO: Move this out of BitStream and into unrar. + * @returns {number} + */ + getBits() { + return (((((this.bytes[this.bytePtr] & 0xff) << 16) + + ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + + ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); + } - ptr += sourceLength; - // Overflowed the stream, just return what we got. - if (ptr >= bytes.length) { - break; - } + /** + * Reads n bits out of the stream, consuming them (moving the bit pointer). + * @param {number} n The number of bits to read. Must be a positive integer. + * @returns {number} The read bits, as an unsigned number. + */ + readBits(n) { + return this.peekBits(n, true); + } - bytesLeftToCopy -= sourceLength; - } + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. Only use this for uncompressed blocks as this throws away remaining + * bits in the current byte. + * @param {number} n The number of bytes to peek. Must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {Uint8Array} The subarray. + */ + peekBytes(n, opt_movePointers) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekBytes() with a non-positive integer: ' + n; + } else if (num === 0) { + return new Uint8Array(); + } + + // Flush bits until we are byte-aligned. + // from http://tools.ietf.org/html/rfc1951#page-11 + // "Any bits of input up to the next byte boundary are ignored." + while (this.bitPtr != 0) { + this.readBits(1); + } + + const numBytesLeft = this.getNumBitsLeft() / 8; + if (num > numBytesLeft) { + throw 'Error! Overflowed the bit stream! n=' + num + ', bytePtr=' + this.bytePtr + + ', bytes.length=' + this.bytes.length + ', bitPtr=' + this.bitPtr; + } - if (movePointers) { - this.bytePtr += num; - this.bitsRead_ += (num * 8); + const movePointers = opt_movePointers || false; + const result = new Uint8Array(num); + let bytes = this.bytes; + let ptr = this.bytePtr; + let bytesLeftToCopy = num; + while (bytesLeftToCopy > 0) { + const bytesLeftInStream = bytes.length - ptr; + const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInStream); + + result.set(bytes.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + + ptr += sourceLength; + // Overflowed the stream, just return what we got. + if (ptr >= bytes.length) { + break; } - return result; + bytesLeftToCopy -= sourceLength; } - /** - * @param {number} n The number of bytes to read. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); + if (movePointers) { + this.bytePtr += num; + this.bitsRead_ += (num * 8); } + + return result; + } + + /** + * @param {number} n The number of bytes to read. + * @returns {Uint8Array} The subarray. + */ + readBytes(n) { + return this.peekBytes(n, true); } - return BitStream; -})(); + /** + * Skips n bits in the stream. Will throw an error if n is < 0 or greater than the number of + * bits left in the stream. + * @param {number} n The number of bits to skip. Must be a positive integer. + * @returns {BitStream} Returns this BitStream for chaining. + */ + skip(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) throw `Error! Called skip(${n})`; + else if (num === 0) return this; + + const totalBitsLeft = this.getNumBitsLeft(); + if (n > totalBitsLeft) { + throw `Error! Overflowed the bit stream for skip(${n}), ptrs=${this.bytePtr}/${this.bitPtr}`; + } + + this.bitsRead_ += num; + this.bitPtr += num; + if (this.bitPtr >= 8) { + this.bytePtr += Math.floor(this.bitPtr / 8); + this.bitPtr %= 8; + } + + return this; + } +} diff --git a/io/bytebuffer-worker.js b/io/bytebuffer-worker.js deleted file mode 100644 index 0f6212b..0000000 --- a/io/bytebuffer-worker.js +++ /dev/null @@ -1,131 +0,0 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; -bitjs.io.ByteBuffer = -/* - * bytebuffer-def.js - * - * Provides a writer for bytes. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - /** - * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. - */ - class ByteBuffer { - /** - * @param {number} numBytes The number of bytes to allocate. - */ - constructor(numBytes) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); - - /** - * @type {number} - * @public - */ - this.ptr = 0; - } - - - /** - * @param {number} b The byte to insert. - */ - insertByte(b) { - // TODO: throw if byte is invalid? - this.data[this.ptr++] = b; - } - - /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. - */ - insertBytes(bytes) { - // TODO: throw if bytes is invalid? - this.data.set(bytes, this.ptr); - this.ptr += bytes.length; - } - - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeNumber(num, numBytes) { - if (numBytes < 1 || !numBytes) { - throw 'Trying to write into too few bytes: ' + numBytes; - } - if (num < 0) { - throw 'Trying to write a negative number (' + num + - ') as an unsigned number to an ArrayBuffer'; - } - if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } - - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - } - - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeSignedNumber(num, numBytes) { - if (numBytes < 1) { - throw 'Trying to write into too few bytes: ' + numBytes; - } - - const HALF = Math.pow(2, (numBytes * 8) - 1); - if (num >= HALF || num < -HALF) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } - - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - } - - /** - * @param {string} str The ASCII string to write. - */ - writeASCIIString(str) { - for (let i = 0; i < str.length; ++i) { - const curByte = str.charCodeAt(i); - if (curByte < 0 || curByte > 255) { - throw 'Trying to write a non-ASCII string!'; - } - this.insertByte(curByte); - } - } - } - - return ByteBuffer; -})(); diff --git a/io/bytebuffer.js b/io/bytebuffer.js index 68a7d35..e2a0078 100644 --- a/io/bytebuffer.js +++ b/io/bytebuffer.js @@ -1,129 +1,143 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -export const ByteBuffer = /* - * bytebuffer-def.js + * bytebuffer.js * * Provides a writer for bytes. * * Licensed under the MIT License * - * Copyright(c) 2011 Google Inc. + * Copyright(c) 2023 Google Inc. * Copyright(c) 2011 antimatter15 */ -(function () { +// 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. + */ +export class ByteBuffer { /** - * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. + * @param {number} numBytes The number of bytes to allocate. */ - class ByteBuffer { - /** - * @param {number} numBytes The number of bytes to allocate. - */ - constructor(numBytes) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - - /** - * @type {Uint8Array} - * @public - */ - this.data = new Uint8Array(numBytes); - - /** - * @type {number} - * @public - */ - this.ptr = 0; + constructor(numBytes) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; } - /** - * @param {number} b The byte to insert. + * @type {Uint8Array} + * @public */ - insertByte(b) { - // TODO: throw if byte is invalid? - this.data[this.ptr++] = b; - } + this.data = new Uint8Array(numBytes); /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. + * Points to the byte that will next be written. + * @type {number} + * @public */ - insertBytes(bytes) { - // TODO: throw if bytes is invalid? - this.data.set(bytes, this.ptr); - this.ptr += bytes.length; + this.ptr = 0; + } + + /** + * Returns an exact copy of all the data that has been written to the ByteBuffer. + * @returns {Uint8Array} + */ + getData() { + const dataCopy = new Uint8Array(this.ptr); + dataCopy.set(this.data.subarray(0, this.ptr)); + return dataCopy; + } + + /** + * @param {number} b The byte to insert. + */ + insertByte(b) { + if (this.ptr + 1 > this.data.byteLength) { + throw `Cannot insert a byte, the buffer is full.`; } - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeNumber(num, numBytes) { - if (numBytes < 1 || !numBytes) { - throw 'Trying to write into too few bytes: ' + numBytes; - } - if (num < 0) { - throw 'Trying to write a negative number (' + num + - ') as an unsigned number to an ArrayBuffer'; - } - if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } + // TODO: throw if byte is invalid? + this.data[this.ptr++] = b; + } - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } + /** + * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. + */ + insertBytes(bytes) { + if (this.ptr + bytes.length > this.data.byteLength) { + throw `Cannot insert ${bytes.length} bytes, the buffer is full.`; + } - this.insertBytes(bytes); + // TODO: throw if bytes is invalid? + this.data.set(bytes, this.ptr); + this.ptr += bytes.length; + } + + /** + * Writes an unsigned number into the next n bytes. If the number is too large + * to fit into n bytes or is negative, an error is thrown. + * @param {number} num The unsigned number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ + writeNumber(num, numBytes) { + if (numBytes < 1 || !numBytes) { + throw 'Trying to write into too few bytes: ' + numBytes; + } + if (num < 0) { + throw 'Trying to write a negative number (' + num + + ') as an unsigned number to an ArrayBuffer'; + } + if (num > (Math.pow(2, numBytes * 8) - 1)) { + throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; } - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeSignedNumber(num, numBytes) { - if (numBytes < 1) { - throw 'Trying to write into too few bytes: ' + numBytes; - } + // Roll 8-bits at a time into an array of bytes. + const bytes = []; + while (numBytes-- > 0) { + const eightBits = num & 255; + bytes.push(eightBits); + num >>= 8; + } - const HALF = Math.pow(2, (numBytes * 8) - 1); - if (num >= HALF || num < -HALF) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; - } + this.insertBytes(bytes); + } - // Roll 8-bits at a time into an array of bytes. - const bytes = []; - while (numBytes-- > 0) { - const eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } + /** + * Writes a signed number into the next n bytes. If the number is too large + * to fit into n bytes, an error is thrown. + * @param {number} num The signed number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ + writeSignedNumber(num, numBytes) { + if (numBytes < 1) { + throw 'Trying to write into too few bytes: ' + numBytes; + } - this.insertBytes(bytes); + const HALF = Math.pow(2, (numBytes * 8) - 1); + if (num >= HALF || num < -HALF) { + throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; } - /** - * @param {string} str The ASCII string to write. - */ - writeASCIIString(str) { - for (let i = 0; i < str.length; ++i) { - const curByte = str.charCodeAt(i); - if (curByte < 0 || curByte > 255) { - throw 'Trying to write a non-ASCII string!'; - } - this.insertByte(curByte); - } + // Roll 8-bits at a time into an array of bytes. + const bytes = []; + while (numBytes-- > 0) { + const eightBits = num & 255; + bytes.push(eightBits); + num >>= 8; } + + this.insertBytes(bytes); } - return ByteBuffer; -})(); + /** + * @param {string} str The ASCII string to write. + */ + writeASCIIString(str) { + for (let i = 0; i < str.length; ++i) { + const curByte = str.charCodeAt(i); + if (curByte < 0 || curByte > 127) { + throw 'Trying to write a non-ASCII string!'; + } + this.insertByte(curByte); + } + } +} diff --git a/io/bytestream-worker.js b/io/bytestream-worker.js deleted file mode 100644 index 57a0aa6..0000000 --- a/io/bytestream-worker.js +++ /dev/null @@ -1,312 +0,0 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; -bitjs.io.ByteStream = -/* - * bytestream-def.js - * - * Provides readers for byte streams. - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -(function () { - /** - * This object allows you to peek and consume bytes as numbers and strings out - * of a stream. More bytes can be pushed into the back of the stream via the - * push() method. - */ - class ByteStream { - /** - * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} opt_offset The offset into the ArrayBuffer - * @param {number=} opt_length The length of this ByteStream - */ - constructor(ab, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The current page of bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The next pages of bytes in the stream. - * @type {Array} - * @private - */ - this.pages_ = []; - - /** - * The byte in the current page that we will read next. - * @type {Number} - * @private - */ - this.ptr = 0; - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bytesRead_ = 0; - } - - /** - * Returns how many bytes have been read in the stream since the beginning of time. - */ - getNumBytesRead() { - return this.bytesRead_; - } - - /** - * Returns how many bytes are currently in the stream left to be read. - */ - getNumBytesLeft() { - const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); - return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); - } - - /** - * Move the pointer ahead n bytes. If the pointer is at the end of the current array - * of bytes and we have another page of bytes, point at the new page. This is a private - * method, no validation is done. - * @param {number} n Number of bytes to increment. - * @private - */ - movePointer_(n) { - this.ptr += n; - this.bytesRead_ += n; - while (this.ptr >= this.bytes.length && this.pages_.length > 0) { - this.ptr -= this.bytes.length; - this.bytes = this.pages_.shift(); - } - } - - /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - peekNumber(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekNumber() with a non-positive integer'; - } else if (num === 0) { - return 0; - } - - if (n > 4) { - throw 'Error! Called peekNumber(' + n + - ') but this method can only reliably read numbers up to 4 bytes long'; - } - - if (this.getNumBytesLeft() < num) { - throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - let result = 0; - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result |= (curPage[ptr++] << (i * 8)); - - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } - - return result; - } - - - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - readNumber(n) { - const num = this.peekNumber(n); - this.movePointer_(n); - return num; - } - - - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - peekSignedNumber(n) { - let num = this.peekNumber(n); - const HALF = Math.pow(2, (n * 8) - 1); - const FULL = HALF * 2; - - if (num >= HALF) num -= FULL; - - return num; - } - - - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - readSignedNumber(n) { - const num = this.peekSignedNumber(n); - this.movePointer_(n); - return num; - } - - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @param {boolean} movePointers Whether to move the pointers. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer'; - } else if (num === 0) { - return new Uint8Array(); - } - - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - const result = new Uint8Array(num); - let curPage = this.bytes; - let ptr = this.ptr; - let bytesLeftToCopy = num; - let pageIndex = 0; - while (bytesLeftToCopy > 0) { - const bytesLeftInPage = curPage.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); - - result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); - - ptr += sourceLength; - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - - bytesLeftToCopy -= sourceLength; - } - - if (movePointers) { - this.movePointer_(num); - } - - return result; - } - - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); - } - - /** - * Peeks at the next n bytes as an ASCII string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - peekString(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekString() with a non-positive integer'; - } else if (num === 0) { - return ''; - } - - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream while peekString()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } - - let result = new Array(num); - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result[i] = String.fromCharCode(curPage[ptr++]); - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } - - return result.join(''); - } - - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - readString(n) { - const strToReturn = this.peekString(n); - this.movePointer_(n); - return strToReturn; - } - - /** - * Feeds more bytes into the back of the stream. - * @param {ArrayBuffer} ab - */ - push(ab) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; - } - - this.pages_.push(new Uint8Array(ab)); - // If the pointer is at the end of the current page of bytes, this will advance - // to the next page. - this.movePointer_(0); - } - - /** - * Creates a new ByteStream from this ByteStream that can be read / peeked. - * @returns {ByteStream} A clone of this ByteStream. - */ - tee() { - const clone = new ByteStream(this.bytes.buffer); - clone.bytes = this.bytes; - clone.ptr = this.ptr; - clone.pages_ = this.pages_.slice(); - clone.bytesRead_ = this.bytesRead_; - return clone; - } - } - - return ByteStream; -})(); diff --git a/io/bytestream.js b/io/bytestream.js index 9337f46..97d716a 100644 --- a/io/bytestream.js +++ b/io/bytestream.js @@ -1,310 +1,365 @@ -// THIS IS A GENERATED FILE! DO NOT EDIT, INSTEAD EDIT THE FILE IN bitjs/build/io. -export const ByteStream = /* - * bytestream-def.js + * bytestream.js * - * Provides readers for byte streams. + * A pull stream for bytes. * * Licensed under the MIT License * - * Copyright(c) 2011 Google Inc. + * Copyright(c) 2023 Google Inc. * Copyright(c) 2011 antimatter15 */ -(function () { +/** + * This object allows you to peek and consume bytes as numbers and strings out + * of a stream. More bytes can be pushed into the back of the stream via the + * push() method. + * By default, the stream is Little Endian (that is the least significant byte + * is first). To change to Big Endian, use setBigEndian(). + */ +export class ByteStream { /** - * This object allows you to peek and consume bytes as numbers and strings out - * of a stream. More bytes can be pushed into the back of the stream via the - * push() method. + * @param {ArrayBuffer} ab The ArrayBuffer object. + * @param {number=} opt_offset The offset into the ArrayBuffer + * @param {number=} opt_length The length of this ByteStream */ - class ByteStream { + constructor(ab, opt_offset, opt_length) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! ByteStream constructed with an invalid ArrayBuffer object'; + } + + const offset = opt_offset || 0; + const length = opt_length || ab.byteLength; + /** - * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} opt_offset The offset into the ArrayBuffer - * @param {number=} opt_length The length of this ByteStream + * The current page of bytes in the stream. + * @type {Uint8Array} + * @private */ - constructor(ab, opt_offset, opt_length) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! BitArray constructed with an invalid ArrayBuffer object'; - } - - const offset = opt_offset || 0; - const length = opt_length || ab.byteLength; - - /** - * The current page of bytes in the stream. - * @type {Uint8Array} - * @private - */ - this.bytes = new Uint8Array(ab, offset, length); - - /** - * The next pages of bytes in the stream. - * @type {Array} - * @private - */ - this.pages_ = []; - - /** - * The byte in the current page that we will read next. - * @type {Number} - * @private - */ - this.ptr = 0; - - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - this.bytesRead_ = 0; - } + this.bytes = new Uint8Array(ab, offset, length); /** - * Returns how many bytes have been read in the stream since the beginning of time. + * The next pages of bytes in the stream. + * @type {Array} + * @private */ - getNumBytesRead() { - return this.bytesRead_; - } + this.pages_ = []; /** - * Returns how many bytes are currently in the stream left to be read. + * The byte in the current page that we will read next. + * @type {Number} + * @private */ - getNumBytesLeft() { - const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); - return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); - } + this.ptr = 0; /** - * Move the pointer ahead n bytes. If the pointer is at the end of the current array - * of bytes and we have another page of bytes, point at the new page. This is a private - * method, no validation is done. - * @param {number} n Number of bytes to increment. + * An ever-increasing number. + * @type {Number} * @private */ - movePointer_(n) { - this.ptr += n; - this.bytesRead_ += n; - while (this.ptr >= this.bytes.length && this.pages_.length > 0) { - this.ptr -= this.bytes.length; - this.bytes = this.pages_.shift(); - } - } + this.bytesRead_ = 0; /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. + * Whether the stream is little-endian (true) or big-endian (false). + * @type {boolean} + * @private */ - peekNumber(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekNumber() with a non-positive integer'; - } else if (num === 0) { - return 0; - } + this.littleEndian_ = true; + } - if (n > 4) { - throw 'Error! Called peekNumber(' + n + - ') but this method can only reliably read numbers up to 4 bytes long'; - } + /** @returns {boolean} Whether the stream is little-endian (least significant byte is first). */ + isLittleEndian() { + return this.littleEndian_; + } - if (this.getNumBytesLeft() < num) { - throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } + /** + * Big-Endian means the most significant byte is first. it is sometimes called Motorola-style. + * @param {boolean=} val The value to set. If not present, the stream is set to big-endian. + */ + setBigEndian(val = true) { + this.littleEndian_ = !val; + } - let result = 0; - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result |= (curPage[ptr++] << (i * 8)); - - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } + /** + * Little-Endian means the least significant byte is first. is sometimes called Intel-style. + * @param {boolean=} val The value to set. If not present, the stream is set to little-endian. + */ + setLittleEndian(val = true) { + this.littleEndian_ = val; + } - return result; - } + /** + * Returns how many bytes have been consumed (read or skipped) since the beginning of time. + * @returns {number} + */ + getNumBytesRead() { + return this.bytesRead_; + } + /** + * Returns how many bytes are currently in the stream left to be read. + * @returns {number} + */ + getNumBytesLeft() { + const bytesInCurrentPage = (this.bytes.byteLength - this.ptr); + return this.pages_.reduce((acc, arr) => acc + arr.length, bytesInCurrentPage); + } - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - readNumber(n) { - const num = this.peekNumber(n); - this.movePointer_(n); - return num; + /** + * Move the pointer ahead n bytes. If the pointer is at the end of the current array + * of bytes and we have another page of bytes, point at the new page. This is a private + * method, no validation is done. + * @param {number} n Number of bytes to increment. + * @private + */ + movePointer_(n) { + this.ptr += n; + this.bytesRead_ += n; + while (this.ptr >= this.bytes.length && this.pages_.length > 0) { + this.ptr -= this.bytes.length; + this.bytes = this.pages_.shift(); } + } + /** + * Peeks at the next n bytes as an unsigned number but does not advance the + * pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @returns {number} The n bytes interpreted as an unsigned number. + */ + peekNumber(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekNumber() with a non-positive integer'; + } else if (num === 0) { + return 0; + } - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - peekSignedNumber(n) { - let num = this.peekNumber(n); - const HALF = Math.pow(2, (n * 8) - 1); - const FULL = HALF * 2; + if (n > 4) { + throw 'Error! Called peekNumber(' + n + + ') but this method can only reliably read numbers up to 4 bytes long'; + } - if (num >= HALF) num -= FULL; + if (this.getNumBytesLeft() < num) { + throw 'Error! Overflowed the byte stream while peekNumber()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); + } - return num; + let result = 0; + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + const exp = (this.littleEndian_ ? i : (num - 1 - i)) * 8; + result |= (curPage[ptr++] << exp); + + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; + } } + return result; + } - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - readSignedNumber(n) { - const num = this.peekSignedNumber(n); - this.movePointer_(n); - return num; - } + /** + * Returns the next n bytes as an unsigned number (or -1 on error) + * and advances the stream pointer n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The n bytes interpreted as an unsigned number. + */ + readNumber(n) { + const num = this.peekNumber(n); + this.movePointer_(n); + return num; + } - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @param {boolean} movePointers Whether to move the pointers. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n, movePointers) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekBytes() with a non-positive integer'; - } else if (num === 0) { - return new Uint8Array(); - } - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } + /** + * Returns the next n bytes as a signed number but does not advance the + * pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The bytes interpreted as a signed number. + */ + peekSignedNumber(n) { + let num = this.peekNumber(n); + const HALF = Math.pow(2, (n * 8) - 1); + const FULL = HALF * 2; - const result = new Uint8Array(num); - let curPage = this.bytes; - let ptr = this.ptr; - let bytesLeftToCopy = num; - let pageIndex = 0; - while (bytesLeftToCopy > 0) { - const bytesLeftInPage = curPage.length - ptr; - const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); + if (num >= HALF) num -= FULL; - result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + return num; + } - ptr += sourceLength; - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - bytesLeftToCopy -= sourceLength; - } + /** + * Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The bytes interpreted as a signed number. + */ + readSignedNumber(n) { + const num = this.peekSignedNumber(n); + this.movePointer_(n); + return num; + } - if (movePointers) { - this.movePointer_(num); - } - return result; + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @param {boolean} movePointers Whether to move the pointers. + * @returns {Uint8Array} The subarray. + */ + peekBytes(n, movePointers) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekBytes() with a non-positive integer'; + } else if (num === 0) { + return new Uint8Array(); } - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {Uint8Array} The subarray. - */ - readBytes(n) { - return this.peekBytes(n, true); + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream during peekBytes! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); } - /** - * Peeks at the next n bytes as an ASCII string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - peekString(n) { - const num = parseInt(n, 10); - if (n !== num || num < 0) { - throw 'Error! Called peekString() with a non-positive integer'; - } else if (num === 0) { - return ''; + const result = new Uint8Array(num); + let curPage = this.bytes; + let ptr = this.ptr; + let bytesLeftToCopy = num; + let pageIndex = 0; + while (bytesLeftToCopy > 0) { + const bytesLeftInPage = curPage.length - ptr; + const sourceLength = Math.min(bytesLeftToCopy, bytesLeftInPage); + + result.set(curPage.subarray(ptr, ptr + sourceLength), num - bytesLeftToCopy); + + ptr += sourceLength; + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; } - const totalBytesLeft = this.getNumBytesLeft(); - if (num > totalBytesLeft) { - throw 'Error! Overflowed the byte stream while peekString()! n=' + num + - ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); - } + bytesLeftToCopy -= sourceLength; + } - let result = new Array(num); - let curPage = this.bytes; - let pageIndex = 0; - let ptr = this.ptr; - for (let i = 0; i < num; ++i) { - result[i] = String.fromCharCode(curPage[ptr++]); - if (ptr >= curPage.length) { - curPage = this.pages_[pageIndex++]; - ptr = 0; - } - } + if (movePointers) { + this.movePointer_(num); + } + + return result; + } + + /** + * Reads the next n bytes as a sub-array. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {Uint8Array} The subarray. + */ + readBytes(n) { + return this.peekBytes(n, true); + } - return result.join(''); + /** + * Peeks at the next n bytes as an ASCII string but does not advance the pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @returns {string} The next n bytes as a string. + */ + peekString(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called peekString() with a non-positive integer'; + } else if (num === 0) { + return ''; } - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - readString(n) { - const strToReturn = this.peekString(n); - this.movePointer_(n); - return strToReturn; + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream while peekString()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); } - /** - * Feeds more bytes into the back of the stream. - * @param {ArrayBuffer} ab - */ - push(ab) { - if (!(ab instanceof ArrayBuffer)) { - throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; + let result = new Array(num); + let curPage = this.bytes; + let pageIndex = 0; + let ptr = this.ptr; + for (let i = 0; i < num; ++i) { + result[i] = String.fromCharCode(curPage[ptr++]); + if (ptr >= curPage.length) { + curPage = this.pages_[pageIndex++]; + ptr = 0; } + } - this.pages_.push(new Uint8Array(ab)); - // If the pointer is at the end of the current page of bytes, this will advance - // to the next page. - this.movePointer_(0); + return result.join(''); + } + + /** + * Returns the next n bytes as an ASCII string and advances the stream pointer + * n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {string} The next n bytes as a string. + */ + readString(n) { + const strToReturn = this.peekString(n); + this.movePointer_(n); + return strToReturn; + } + + /** + * Skips n bytes in the stream. + * @param {number} n The number of bytes to skip. Must be a positive integer. + * @returns {ByteStream} Returns this ByteStream for chaining. + */ + skip(n) { + const num = parseInt(n, 10); + if (n !== num || num < 0) { + throw 'Error! Called skip() with a non-positive integer'; + } else if (num === 0) { + return this; } - /** - * Creates a new ByteStream from this ByteStream that can be read / peeked. - * @returns {ByteStream} A clone of this ByteStream. - */ - tee() { - const clone = new ByteStream(this.bytes.buffer); - clone.bytes = this.bytes; - clone.ptr = this.ptr; - clone.pages_ = this.pages_.slice(); - clone.bytesRead_ = this.bytesRead_; - return clone; + const totalBytesLeft = this.getNumBytesLeft(); + if (num > totalBytesLeft) { + throw 'Error! Overflowed the byte stream while skip()! n=' + num + + ', ptr=' + this.ptr + ', bytes.length=' + this.getNumBytesLeft(); } + + this.movePointer_(n); + return this; } - return ByteStream; -})(); + /** + * Feeds more bytes into the back of the stream. + * @param {ArrayBuffer} ab + */ + push(ab) { + if (!(ab instanceof ArrayBuffer)) { + throw 'Error! ByteStream.push() called with an invalid ArrayBuffer object'; + } + + this.pages_.push(new Uint8Array(ab)); + // If the pointer is at the end of the current page of bytes, this will advance + // to the next page. + this.movePointer_(0); + } + + /** + * Creates a new ByteStream from this ByteStream that can be read / peeked. + * Note that the teed stream is a disconnected copy. If you push more bytes to the original + * stream, the copy does not get them. + * TODO: Assess whether the above causes more bugs than it avoids. (It would feel weird to me if + * the teed stream shared some state with the original stream.) + * @returns {ByteStream} A clone of this ByteStream. + */ + tee() { + const clone = new ByteStream(this.bytes.buffer); + clone.bytes = this.bytes; + clone.ptr = this.ptr; + clone.pages_ = this.pages_.slice(); + clone.bytesRead_ = this.bytesRead_; + clone.littleEndian_ = this.littleEndian_; + return clone; + } +} diff --git a/media/media.js b/media/media.js index d194136..ef7c27d 100644 --- a/media/media.js +++ b/media/media.js @@ -3,7 +3,7 @@ console.warn(`This is not even an alpha-level API. Do not use.`); // A Container Format is a file that embeds multiple data streams into a single file. // Examples: // - the ZIP family (ZIP, JAR, CBZ, EPUB, ODF, OOXML) -// - the ISO-BMFF family (MP4, HEVC, AVIF, MOV/QT, etc) +// - the ISO-BMFF family (MP4, HEVC, HEIC, AVIF, MOV/QT, etc) // - the Matroska family (MKV, WebM) // - the RIFF family (WAV, AVI, WebP) // - the OGG family (OGV, OPUS) diff --git a/package-lock.json b/package-lock.json index 4b457c8..ba830be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@codedread/bitjs", - "version": "1.0.11", + "version": "1.2.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@codedread/bitjs", - "version": "1.0.11", + "version": "1.2.6", "license": "MIT", "devDependencies": { - "c8": "^7.12.0", + "c8": "^7.14.0", "chai": "^4.3.4", "mocha": "^10.1.0", "typescript": "^4.8.0" @@ -62,10 +62,11 @@ "dev": true }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -133,17 +134,18 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -156,9 +158,9 @@ "dev": true }, "node_modules/c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -181,15 +183,6 @@ "node": ">=10.12.0" } }, - "node_modules/c8/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/camelcase": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", @@ -383,12 +376,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -399,12 +393,6 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -430,10 +418,11 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -466,9 +455,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -596,9 +585,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -765,9 +754,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "dependencies": { "argparse": "^2.0.1" @@ -823,9 +812,9 @@ } }, "node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -835,32 +824,32 @@ } }, "node_modules/mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -868,10 +857,27 @@ }, "engines": { "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -880,18 +886,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -993,6 +987,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -1051,7 +1046,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { "version": "6.3.0", @@ -1063,10 +1059,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -1150,9 +1147,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -1225,10 +1222,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -1325,10 +1323,11 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -1447,9 +1446,9 @@ "dev": true }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-styles": { @@ -1505,12 +1504,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-stdout": { @@ -1520,9 +1519,9 @@ "dev": true }, "c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", @@ -1537,14 +1536,6 @@ "v8-to-istanbul": "^9.0.0", "yargs": "^16.2.0", "yargs-parser": "^20.2.9" - }, - "dependencies": { - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } } }, "camelcase": { @@ -1694,20 +1685,12 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "ms": "^2.1.3" } }, "decamelize": { @@ -1726,9 +1709,9 @@ } }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, "emoji-regex": { @@ -1750,9 +1733,9 @@ "dev": true }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -1834,9 +1817,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -1974,9 +1957,9 @@ } }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -2011,41 +1994,55 @@ } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" } }, "mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + } } }, "ms": { @@ -2054,12 +2051,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2169,9 +2160,9 @@ "dev": true }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -2235,9 +2226,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -2287,9 +2278,9 @@ } }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -2399,9 +2390,9 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, "yargs-unparser": { diff --git a/package.json b/package.json index e0fd33e..b3086aa 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,15 @@ { "name": "@codedread/bitjs", - "version": "1.0.11", + "version": "1.2.6", "description": "Binary Tools for JavaScript", "homepage": "https://github.com/codedread/bitjs", "author": "Jeff Schiller", "license": "MIT", - "keywords": [ - "binary", - "javascript", - "archive", - "pkzip", - "zip", - "rar", - "tar", - "unzip", - "unrar", - "untar", - "file", - "codecs", - "mp4", - "avc", - "webm" + "keywords": [ "binary", "javascript", "archive", "file", "image", + "pkzip", "zip", "rar", "tar", "gzip", + "unzip", "unrar", "untar", "gunzip", + "gif", "jpeg", "png", "webp", + "codecs", "mp4", "avc", "webm" ], "main": "./index.js", "type": "module", @@ -33,9 +22,8 @@ "bugs": { "url": "https://github.com/codedread/bitjs/issues" }, - "dependencies": {}, "devDependencies": { - "c8": "^7.12.0", + "c8": "^7.14.0", "chai": "^4.3.4", "mocha": "^10.1.0", "typescript": "^4.8.0" @@ -46,7 +34,7 @@ }, "scripts": { "build-webpshim": "cd build; make", - "coverage": "./node_modules/.bin/c8 npm test", + "coverage": "c8 npm run test", "test": "./node_modules/.bin/mocha tests/*.spec.js", "update-types": "tsc" } diff --git a/tests/archive-compress.spec.js b/tests/archive-compress.spec.js new file mode 100644 index 0000000..ae2e3a9 --- /dev/null +++ b/tests/archive-compress.spec.js @@ -0,0 +1,130 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; +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 + */ + +const PATH = `tests/archive-testfiles/`; + +const INPUT_FILENAMES = [ + 'sample-1.txt', + 'sample-2.csv', + 'sample-3.json', +]; + +describe('bitjs.archive.compress', () => { + /** @type {Map} */ + let inputFileInfos = new Map(); + let decompressedFileSize = 0; + + for (const fileName of INPUT_FILENAMES) { + const fullFilename = `${PATH}${fileName}`; + const fd = fs.openSync(fullFilename, 'r'); + const lastModTime = fs.fstatSync(fd).mtimeMs; + const nodeBuf = fs.readFileSync(fullFilename); + const fileData = new Uint8Array( + nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length)); + inputFileInfos.set(fileName, {fileName, lastModTime, fileData}); + decompressedFileSize += fileData.byteLength; + fs.closeSync(fd); + } + + it('zipper throws for invalid compression method', async () => { + expect(() => new Zipper({zipCompressionMethod: 42})).throws(); + }); + + it('zipper works for STORE', (done) => { + let extractCalled = false; + const files = new Map(inputFileInfos); + const zipper = new Zipper({zipCompressionMethod: ZipCompressionMethod.STORE}); + zipper.start(Array.from(files.values()), true).then(byteArray => { + expect(zipper.compressState).equals(CompressStatus.COMPLETE); + expect(byteArray.byteLength > decompressedFileSize).equals(true); + + const unarchiver = getUnarchiver(byteArray.buffer); + unarchiver.addEventListener('extract', evt => { + extractCalled = true; + const {filename, fileData} = evt.unarchivedFile; + expect(files.has(filename)).equals(true); + const inputFile = files.get(filename).fileData; + expect(inputFile.byteLength).equals(fileData.byteLength); + for (let b = 0; b < inputFile.byteLength; ++b) { + expect(inputFile[b]).equals(fileData[b]); + } + }); + unarchiver.addEventListener('finish', evt => done()); + unarchiver.start(); + expect(extractCalled).equals(true); + }); + }); + + try { + new CompressionStream('deflate-raw'); + + it('zipper works for DEFLATE, where deflate-raw is supported', async () => { + const files = new Map(inputFileInfos); + const zipper = new Zipper({zipCompressionMethod: ZipCompressionMethod.DEFLATE}); + const byteArray = await zipper.start(Array.from(files.values()), true); + + expect(zipper.compressState).equals(CompressStatus.COMPLETE); + expect(byteArray.byteLength < decompressedFileSize).equals(true); + + const unarchiver = getUnarchiver(byteArray.buffer); + unarchiver.addEventListener('extract', evt => { + const {filename, fileData} = evt.unarchivedFile; + expect(files.has(filename)).equals(true); + const inputFile = files.get(filename).fileData; + expect(inputFile.byteLength).equals(fileData.byteLength); + for (let b = 0; b < inputFile.byteLength; ++b) { + expect(inputFile[b]).equals(fileData[b]); + } + }); + await unarchiver.start(); + }); + } catch (err) { + // Do nothing. This runtime did not support DEFLATE. (Node < 21.2.0) + } + + it('zipper.start([file1]) and appendFiles(otherFiles, true) works', (done) => { + let extractCalled = false; + const files = new Map(inputFileInfos); + const zipper = new Zipper({zipCompressionMethod: ZipCompressionMethod.STORE}); + const fileArr = Array.from(files.values()); + zipper.start([fileArr.shift()]).then(byteArray => { + expect(zipper.compressState).equals(CompressStatus.COMPLETE); + expect(byteArray.byteLength > decompressedFileSize).equals(true); + + const unarchiver = getUnarchiver(byteArray.buffer); + unarchiver.addEventListener('extract', evt => { + extractCalled = true; + const {filename, fileData} = evt.unarchivedFile; + expect(files.has(filename)).equals(true); + const inputFile = files.get(filename).fileData; + expect(inputFile.byteLength).equals(fileData.byteLength); + for (let b = 0; b < inputFile.byteLength; ++b) { + expect(inputFile[b]).equals(fileData[b]); + } + }); + unarchiver.addEventListener('finish', evt => { + done(); + }); + unarchiver.start(); + expect(extractCalled).equals(true); + }); + zipper.appendFiles(fileArr, true); + }); + + it('appendFiles() throws an error if after last file', (done) => { + const files = new Map(inputFileInfos); + const zipper = new Zipper({zipCompressionMethod: ZipCompressionMethod.STORE}); + zipper.start(Array.from(files.values()), true); + expect(() => zipper.appendFiles(Array.from(files.values()), true)).throws(); + done(); + }); + +}); diff --git a/tests/archive-decompress.spec.js b/tests/archive-decompress.spec.js new file mode 100644 index 0000000..f09045b --- /dev/null +++ b/tests/archive-decompress.spec.js @@ -0,0 +1,96 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; + +import { Gunzipper, Unarchiver, 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 = [ + // rar a -m0 -ma4 archive-rar-store.rar sample* + 'archive-rar-store.rar', + // rar a -m3 -ma4 archive-rar-default.rar sample* + 'archive-rar-default.rar', + // rar a -m5 -ma4 archive-rar-smaller.rar sample* + 'archive-rar-smaller.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(); + + 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, async () => { + 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); + let extractEvtFiredForAddEventListener = false; + let extractEvtFiredForOnExtract = false; + + unarchiver.addEventListener('extract', evt => { + extractEvtFiredForAddEventListener = true; + 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.onExtract(evt => { + extractEvtFiredForOnExtract = true; + expect(evt.unarchivedFile.filename.length > 0).equals(true); + }) + + await unarchiver.start(); + expect(extractEvtFiredForAddEventListener).equals(true); + expect(extractEvtFiredForOnExtract).equals(true); + }); + } + + describe('gunzip', () => { + it('can gunzip a file', async () => { + const bufs = new Map(inputArrayBuffers); + const nodeBuf = fs.readFileSync(`${PATH}sample-1-slowest.txt.gz`); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + let gunzipper = getUnarchiver(ab, {debug: true}); + expect(gunzipper instanceof Gunzipper).equals(true); + let extractEvtFiredForOnExtract = false; + + gunzipper.onExtract(evt => { + extractEvtFiredForOnExtract = true; + const {filename, fileData} = evt.unarchivedFile; + expect(filename).equals('sample-1.txt'); + + const ab = bufs.get('sample-1.txt'); + expect(fileData.byteLength).equals(ab.byteLength); + for (let b = 0; b < fileData.byteLength; ++b) { + expect(fileData[b] === ab[b]); + } + }); + + await gunzipper.start(); + expect(extractEvtFiredForOnExtract).equals(true); + }); + }); +}); diff --git a/tests/archive-test.html b/tests/archive-test.html deleted file mode 100644 index 4320e01..0000000 --- a/tests/archive-test.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - Unit tests for bitjs.archive - - - - diff --git a/tests/archive-test.js b/tests/archive-test.js deleted file mode 100644 index e977891..0000000 --- a/tests/archive-test.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * archive-test.js - * - * Licensed under the MIT License - * - * Copyright(c) 2017 Google Inc. - */ - -import { getUnarchiver, UnarchiveEventType } from '../archive/archive.js'; -import { assertEquals, runTests } from './muther.js'; - -const testInputs = { - 'testUnzipDeflate': 'archive-testfiles/test-unzip-deflate.json', - 'testUnzipDescriptor': 'archive-testfiles/test-unzip-descriptor.json', - 'testUnzipStore': 'archive-testfiles/test-unzip-store.json', - 'testUnrarM1': 'archive-testfiles/test-unrar-m1.json', - 'testUnrarM2': 'archive-testfiles/test-unrar-m2.json', - 'testUnrarM3': 'archive-testfiles/test-unrar-m3.json', - 'testUnrarM4': 'archive-testfiles/test-unrar-m4.json', - 'testUnrarM5': 'archive-testfiles/test-unrar-m5.json', - 'testUnrarMA4': 'archive-testfiles/test-unrar-ma4.json', - // On a Mac, tar files contain hidden files. To disable this do: - // $ COPYFILE_DISABLE=1 tar cvf lorem.tar lorem.txt - 'testUntar': 'archive-testfiles/test-untar-1.json', -}; - -// TODO: It is an error for the Unarchiver worker not to terminate or send a FINISH event. -// We need to be able to test that here. - -const testSuite = { tests: {} }; -for (let testName in testInputs) { - const testInputFilename = testInputs[testName]; - testSuite.tests[testName] = function () { - return new Promise((resolve, reject) => { - const scriptEl = document.createElement('script'); - scriptEl.setAttribute('src', testInputFilename); - scriptEl.addEventListener('load', evt => { - const testFile = window.archiveTestFile; - try { - const archivedFile = new Uint8Array( - atob(testFile.archivedFile).split(',').map(str => parseInt(str))); - const unarchivedFile = new Uint8Array( - atob(testFile.unarchivedFile).split(',').map(str => parseInt(str))); - const unarchiver = getUnarchiver(archivedFile.buffer, { - pathToBitJS: '../', - }); - unarchiver.addEventListener(UnarchiveEventType.EXTRACT, evt => { - const theUnarchivedFile = evt.unarchivedFile.fileData; - try { - assertEquals(theUnarchivedFile.length, unarchivedFile.length, - 'The unarchived buffer was not the right length'); - for (let i = 0; i < theUnarchivedFile.length; ++i) { - assertEquals(theUnarchivedFile[i], unarchivedFile[i], - 'Byte #' + i + ' did not match'); - } - resolve(); - } catch (err) { - reject(err); - } - }); - unarchiver.start(); - } catch (err) { - reject(err); - } - }); - document.body.appendChild(scriptEl); - }); - } -} - -runTests(testSuite); diff --git a/tests/archive-testfiles/README.md b/tests/archive-testfiles/README.md deleted file mode 100644 index 38d1396..0000000 --- a/tests/archive-testfiles/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Test files for unarchivers. - - 1. Create a zip or rar file with just one file inside it. - 2. Use test-uploader.html and choose the archived file and the unarchived file. - 3. Paste that JSON output into a test json file. diff --git a/tests/archive-testfiles/archive-rar-default.rar b/tests/archive-testfiles/archive-rar-default.rar new file mode 100644 index 0000000..896e60a Binary files /dev/null and b/tests/archive-testfiles/archive-rar-default.rar differ diff --git a/tests/archive-testfiles/archive-rar-smaller.rar b/tests/archive-testfiles/archive-rar-smaller.rar new file mode 100644 index 0000000..291fc98 Binary files /dev/null and b/tests/archive-testfiles/archive-rar-smaller.rar differ diff --git a/tests/archive-testfiles/archive-rar-store.rar b/tests/archive-testfiles/archive-rar-store.rar new file mode 100644 index 0000000..8df29de Binary files /dev/null and b/tests/archive-testfiles/archive-rar-store.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-slowest.txt.gz b/tests/archive-testfiles/sample-1-slowest.txt.gz new file mode 100644 index 0000000..f284e4f Binary files /dev/null and b/tests/archive-testfiles/sample-1-slowest.txt.gz 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/archive-testfiles/test-unrar-m1.json b/tests/archive-testfiles/test-unrar-m1.json deleted file mode 100644 index 407e9b2..0000000 --- a/tests/archive-testfiles/test-unrar-m1.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwxNjEsMTM5LDExNiwzMiwxMjgsNDEsMCwxNiwxLDAsMCwxODksMSwwLDAsMywxODksMTk3LDE3OCwxNTIsNzEsOTYsODMsNzQsMjksNDksOSwwLDE2NCwxMjksMCwwLDEwOCwxMTEsMTE0LDEwMSwxMDksNDYsMTE2LDEyMCwxMTYsMTYsMjAsMjAwLDIxMywxNSwyMTMsNjUsNzgsMTQ5LDg0LDYsMjMxLDEwNCwxNjksNjIsMjMxLDIzNiwxNjAsOCwyMTcsMTg5LDEzOSw0OSwyNDgsMjM1LDk3LDE4NSwxODMsMjI4LDE2MCwxOTksNTksMzcsMzQsMTk4LDIsMTgsMjI5LDY0LDE1NSw2MCwxNDQsMjQsMTY5LDYzLDEzOSwyMjksMTA2LDE0LDUyLDI0NSwxOSwxODksNDcsMTQ1LDE2MiwxOTMsNDIsMTczLDIxNiw3NCwyNDYsMjEwLDIwMSwxNDksMTU5LDMzLDM4LDExMiwxMTcsMzUsMTEwLDY2LDIwOCwxOTcsMjA1LDQ0LDIyNyw2NCw4MiwyNTAsMTczLDIzMCwyNSwxMDcsMTM2LDIyMiw1NCwxNDUsMTUzLDE1NSwyMTcsMTMxLDE3NiwxNjIsMTI1LDY2LDE2MCwxNzMsMjEwLDIyNiwxMzgsMTY3LDU0LDY0LDE0OCw2MiwyOSw1NiwzMSw0NCwyMiwxOTgsMTc2LDE4Niw0MiwxMTksMjI3LDIwNCw4NCwxNDksMjM2LDYwLDU3LDIwOCwxODEsMjQ0LDIxNywxMjEsMTMzLDI0NCwxODUsMTQ1LDEwMCw5OSwxMjMsNywxNjUsMTgsMTc4LDE5MSw3NCwxMTksMTQyLDExNiwxMTksMTk1LDI1MCwxOTEsNTYsMTQwLDIyNCwxNjksMTUsMTg2LDE5NSw5MCwyNTMsNjQsNzEsNDksMTg0LDMwLDIxOSw5NSwxNjMsMTc4LDEzMSwxNTEsMTM0LDMwLDE2NSwxNjYsMTk1LDQzLDE0MCwxNzUsMjE4LDIxOCwxMDcsMTUyLDE1NywxOTksMTk5LDE1NSwyMDksMjQxLDI4LDc1LDEyLDgxLDM0LDIzLDkwLDgxLDM1LDc0LDEyNywxMzksMjA5LDE2LDIyNywxODcsMjQyLDgxLDEzMywxODIsMjA4LDQ4LDE1MywxMzgsMzEsNjAsMTUsMTI3LDgyLDkyLDUwLDE5MiwxOTgsMTQwLDEsMTc1LDQxLDM0LDIwMiw1MCwxNjQsMjQ4LDUxLDcyLDg4LDEzNiwyMDUsMTM0LDUxLDExNSw1MywxODIsMjAxLDI3LDE3LDE0MiwxODgsMTU4LDE1Miw3NywyMDYsMTMzLDI1MSwyNDcsNTAsNDAsMzgsMTg2LDkyLDIzOCwyMzcsMTExLDc3LDE3OCw4Myw5OCw2LDAsMTgxLDI1Miw5MCwxMjIsMjQxLDI0OCwxMjgsMTk2LDYxLDEyMywwLDY0LDcsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unrar-m2.json b/tests/archive-testfiles/test-unrar-m2.json deleted file mode 100644 index de1390e..0000000 --- a/tests/archive-testfiles/test-unrar-m2.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwyLDI0MywxMTYsMzIsMTI4LDQxLDAsMzgsMSwwLDAsMTg5LDEsMCwwLDMsMTg5LDE5NywxNzgsMTUyLDcxLDk2LDgzLDc0LDI5LDUwLDksMCwxNjQsMTI5LDAsMCwxMDgsMTExLDExNCwxMDEsMTA5LDQ2LDExNiwxMjAsMTE2LDEzLDY1LDEyLDE0NSwxNSwyMTMsNjUsMTksMTM4LDE1NiwyLDQ0LDIyNSw4MiwxMDcsMTU1LDEwMywwLDMyLDIzLDExMywxNjUsOTYsOTIsNjIsNDksMTAzLDEyNCwxNTQsNTAsNDMsMTkyLDQwLDE0MSwyMDMsMTQ3LDEzNiwxNDAsMTA5LDIxNSwyMDMsMjI3LDI0NCwyNTQsODMsMTE2LDIxOSw4NCwxNTEsMTg5LDE5NSwyNTMsOTksMzksNjEsNjksMTg4LDIzNSwyMjUsMTA4LDExOCwxODEsODQsMjAyLDEzOCw5MSwyMjEsNzksMzcsNTAsNDgsMTU1LDE1Myw3NSw0MiwyMjEsODYsMjIwLDEyLDE2NiwxOTQsMTM0LDE3MCwxMTgsMTk2LDExOSw0Nyw0NSw2MSwyMiwxMzMsODksMTkwLDU0LDg4LDQzLDI0Nyw2MSwyNTEsMjU0LDUsMTMyLDE4MiwxNzYsMSwyMTUsMTM5LDEwMSwyNTEsMTUyLDE3MiwxNCwxNTYsMTQyLDIxNiwxODksMTIyLDE5Myw5NiwxNzYsNywyMDUsODcsMjA5LDk0LDIxNSwxMTksNjQsMTU4LDk5LDIxNSwxMzAsNzksMjksNzgsMTYxLDIwOCwxMTAsMjUwLDQyLDg0LDM1LDU4LDc0LDE4MywxNjcsMTE4LDExNiwxMzAsMTQ4LDkzLDExLDM5LDExOSwxMDgsMTksMTI2LDg3LDIyOCw3NCwxMDAsMjQxLDE0NiwxNywyNTMsMjMsMjA0LDM5LDE1OSwyNDYsMTYsOTYsMTUsNTIsMTk2LDEyMSw5Niw1MCwyMTgsMTAsMjUyLDIwOCw5MywxMjAsNzgsNjcsMzIsMTYxLDE2MCwyNDIsMjM4LDI2LDE3MCwyMDcsMTI2LDQ3LDEyOSwxOTUsNjgsMTY2LDI1Miw2MywxNDIsMTgzLDE1NCwyMzgsMTAwLDE1NiwxMDQsNzAsMjI2LDE1LDE1MiwxMiwxOCw2NywxNDMsMjQ4LDE1MiwyMTgsNDIsMTczLDQzLDE3NCwxMDYsMTkwLDY1LDE4LDIxLDE4NiwyMjgsMTE2LDUsMTMyLDIyMiwxNTcsMTUyLDE1LDE5OCwxNDIsNDksMTQ5LDIwNiw4LDEzOSwyNCwxODMsMTEzLDg4LDQ4LDM2LDc3LDI0MSwxNjYsMTc3LDE3OCwxMzQsNDksMTgxLDI1LDM2LDE0OCwxNDUsMTksNTcsMTkxLDIzNiw0LDcxLDIyMiwzNywxNjYsMTMyLDQ1LDE3OSwxOTEsMTQxLDE2NCwxNTEsMTI3LDksMTcsMzksMjMsNzIsMTM0LDQyLDEwNCw5NSwyMTAsMjIzLDExOSw3OSwxNjQsMTk2LDYxLDEyMywwLDY0LDcsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unrar-m3.json b/tests/archive-testfiles/test-unrar-m3.json deleted file mode 100644 index e151403..0000000 --- a/tests/archive-testfiles/test-unrar-m3.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwxNDcsOTgsMTE2LDMyLDEyOCw0MSwwLDM4LDEsMCwwLDE4OSwxLDAsMCwzLDE4OSwxOTcsMTc4LDE1Miw3MSw5Niw4Myw3NCwyOSw1MSw5LDAsMTY0LDEyOSwwLDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiwxMywxMjksMTIsMTQ1LDE1LDIxMywxMjksNzgsMTQ5LDQwLDgsMTg2LDEzOCwxNDcsMjA2LDEyMiwyMDIsMCwxMjgsMTEwLDE5OCwxNTAsMTI5LDE3OSw2NCwyMjksMjE1LDIyOCwyNDAsMjE2LDE4MywxMjgsMTk0LDU1LDU1LDE4NiwxNDUsMjQsMjE5LDIwNywyMDMsMTY3LDI0MSwyNTQsODMsMTIwLDIxOSw4OCwxNTEsMjIzLDgxLDI1NCwyNDEsMjE3LDIwNyw4MSwxMDcsNDIsMTE5LDkxLDI5LDQzLDg1LDM0LDE1NCwxNTAsMjQ3LDc5LDIwOSw3NiwxNDAsMzYsMjMwLDc4LDE3MCwxNTAsODUsMTgyLDIyNyw0MSw0OCwxNjEsMTU0LDE1NywxNjEsMjksMjAzLDc1LDc5LDY5LDk3LDgyLDc5LDE0MSw4NSwyMDIsMjUzLDc5LDEyNiwyMjMsMTEzLDkzLDQ1LDE2NCwwLDExNiwyMjYsMjA5LDEyNywzOCw0MiwyMjcsMTU5LDM1LDE4MSw0Nyw5NCwxMTIsODgsNDQsMSwyNDcsNjksMjQ0LDg3LDE5NiwyNDcsNjQsMTU4LDk5LDIxNCwxMzAsNzksMjksNzgsMTYxLDIwOCwxMTAsMjE4LDQyLDg0LDM1LDQxLDIzNyw3NSw4MywxODcsNTksOTYsMTY0LDIxNSw1MCwxOTMsMjE5LDE4MCw5LDE5MSwxMSwyNDIsMzcsNTAsMTIwLDIwMSw4LDI1NSwxMzksMjI2LDE5LDIwNywyNTMsOCw0Niw3LDE1NCw5OCw2MCwxNzYsMjUsMTA4LDE5NywxMjYsMTY4LDQ2LDE4NywxNjcsMSwxNDQsODAsMjA4LDEyMSwxMTksMTMsODUsODcsMTc1LDIzLDIwOCwyMjUsMTYyLDgzLDEyNiwzMSwxOTksOTEsNzcsNTMsNTAsNzgsNTIsMzUsMTEzLDcsMjAwLDYsOSwzMywxOTksMTUyLDE1MiwyMTgsNDAsMTcyLDE2OSwxNTgsNDIsMTgyLDY1LDE4LDIxLDE3MCwyNDYsNTgsMiwxOTQsMTExLDExMCwyMDQsNywyMjMsNzEsMjQsMTk0LDIzMSw0LDY5LDE0MCw5MSwxODQsMTcwLDI0LDE4LDM4LDI0NywyMTEsODgsMjE3LDY3LDI0LDIxNCwxNDAsMTQ2LDc0LDcyLDEzNywxNTYsMjE5LDI0NCwyLDM1LDIzOCwyNDIsMjAzLDM0LDIyLDIxMywyMTksMTk4LDIxOSwzNywyMjMsMjI2LDY4LDczLDE5NywyMTAsMzMsMTIyLDE1NCwyMywxMzcsMTExLDExOSw3OSwxOTYsMTk2LDYxLDEyMywwLDY0LDcsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unrar-m4.json b/tests/archive-testfiles/test-unrar-m4.json deleted file mode 100644 index 3264862..0000000 --- a/tests/archive-testfiles/test-unrar-m4.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwyMzAsMTU0LDExNiwzMiwxMjgsNDEsMCwzOCwxLDAsMCwxODksMSwwLDAsMywxODksMTk3LDE3OCwxNTIsNzEsOTYsODMsNzQsMjksNTIsOSwwLDE2NCwxMjksMCwwLDEwOCwxMTEsMTE0LDEwMSwxMDksNDYsMTE2LDEyMCwxMTYsMTMsMTI5LDEyLDE0NSwxNSwyMTMsMTI5LDc4LDE0OSw0MCw4LDE4NiwxMzgsMTQ3LDIwNiwxMjIsMjAyLDAsMTI4LDExMCwxOTgsMTUwLDEyOSwxNzksNjQsMjI5LDIxNSwyMjgsMjQwLDIxNiwxODMsMTI4LDE5NCw1NSw1NSwxODYsMTQ1LDI0LDIxOSwyMDcsMjAzLDE2NywyNDEsMjU0LDgzLDEyMCwyMTksODgsMTUxLDIyMyw4MSwyNTQsMjQxLDIxNywyMDcsODEsMTA3LDQyLDExOSw5MSwyOSw0Myw4NSwzNCwxNTQsMTUwLDI0Nyw3OSwyMDksNzYsMTQwLDM2LDIzMCw3OCwxNzAsMTUwLDg1LDE4MiwyMjcsNDEsNDgsMTYxLDE1NCwxNTcsMTYxLDI5LDIwMyw3NSw3OSw2OSw5Nyw4Miw3OSwxNDEsODUsMjAyLDI1Myw3OSwxMjYsMjIzLDExMyw5Myw0NSwxNjQsMCwxMTYsMjI2LDIwOSwxMjcsMzgsNDIsMjI3LDE1OSwzNSwxODEsNDcsOTQsMTEyLDg4LDQ0LDEsMjQ3LDY5LDI0NCw4NywxOTYsMjQ3LDY0LDE1OCw5OSwyMTQsMTMwLDc5LDI5LDc4LDE2MSwyMDgsMTEwLDIxOCw0Miw4NCwzNSw0MSwyMzcsNzUsODMsMTg3LDU5LDk2LDE2NCwyMTUsNTAsMTkzLDIxOSwxODAsOSwxOTEsMTEsMjQyLDM3LDUwLDEyMCwyMDEsOCwyNTUsMTM5LDIyNiwxOSwyMDcsMjUzLDgsNDYsNywxNTQsOTgsNjAsMTc2LDI1LDEwOCwxOTcsMTI2LDE2OCw0NiwxODcsMTY3LDEsMTQ0LDgwLDIwOCwxMjEsMTE5LDEzLDg1LDg3LDE3NSwyMywyMDgsMjI1LDE2Miw4MywxMjYsMzEsMTk5LDkxLDc3LDUzLDUwLDc4LDUyLDM1LDExMyw3LDIwMCw2LDksMzMsMTk5LDE1MiwxNTIsMjE4LDQwLDE3MiwxNjksMTU4LDQyLDE4Miw2NSwxOCwyMSwxNzAsMjQ2LDU4LDIsMTk0LDExMSwxMTAsMjA0LDcsMjIzLDcxLDI0LDE5NCwyMzEsNCw2OSwxNDAsOTEsMTg0LDE3MCwyNCwxOCwzOCwyNDcsMjExLDg4LDIxNyw2NywyNCwyMTQsMTQwLDE0Niw3NCw3MiwxMzcsMTU2LDIxOSwyNDQsMiwzNSwyMzgsMjQyLDIwMywzNCwyMiwyMTMsMjE5LDE5OCwyMTksMzcsMjIzLDIyNiw2OCw3MywxOTcsMjEwLDMzLDEyMiwxNTQsMjMsMTM3LDExMSwxMTksNzksMTk2LDE5Niw2MSwxMjMsMCw2NCw3LDA=","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unrar-m5.json b/tests/archive-testfiles/test-unrar-m5.json deleted file mode 100644 index 5231370..0000000 --- a/tests/archive-testfiles/test-unrar-m5.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwxMTksMTEsMTE2LDMyLDEyOCw0MSwwLDM4LDEsMCwwLDE4OSwxLDAsMCwzLDE4OSwxOTcsMTc4LDE1Miw3MSw5Niw4Myw3NCwyOSw1Myw5LDAsMTY0LDEyOSwwLDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiwxMywxMjksMTIsMTQ1LDE1LDIxMywxMjksNzgsMTQ5LDQwLDgsMTg2LDEzOCwxNDcsMjA2LDEyMiwyMDIsMCwxMjgsMTEwLDE5OCwxNTAsMTI5LDE3OSw2NCwyMjksMjE1LDIyOCwyNDAsMjE2LDE4MywxMjgsMTk0LDU1LDU1LDE4NiwxNDUsMjQsMjE5LDIwNywyMDMsMTY3LDI0MSwyNTQsODMsMTIwLDIxOSw4OCwxNTEsMjIzLDgxLDI1NCwyNDEsMjE3LDIwNyw4MSwxMDcsNDIsMTE5LDkxLDI5LDQzLDg1LDM0LDE1NCwxNTAsMjQ3LDc5LDIwOSw3NiwxNDAsMzYsMjMwLDc4LDE3MCwxNTAsODUsMTgyLDIyNyw0MSw0OCwxNjEsMTU0LDE1NywxNjEsMjksMjAzLDc1LDc5LDY5LDk3LDgyLDc5LDE0MSw4NSwyMDIsMjUzLDc5LDEyNiwyMjMsMTEzLDkzLDQ1LDE2NCwwLDExNiwyMjYsMjA5LDEyNywzOCw0MiwyMjcsMTU5LDM1LDE4MSw0Nyw5NCwxMTIsODgsNDQsMSwyNDcsNjksMjQ0LDg3LDE5NiwyNDcsNjQsMTU4LDk5LDIxNCwxMzAsNzksMjksNzgsMTYxLDIwOCwxMTAsMjE4LDQyLDg0LDM1LDQxLDIzNyw3NSw4MywxODcsNTksOTYsMTY0LDIxNSw1MCwxOTMsMjE5LDE4MCw5LDE5MSwxMSwyNDIsMzcsNTAsMTIwLDIwMSw4LDI1NSwxMzksMjI2LDE5LDIwNywyNTMsOCw0Niw3LDE1NCw5OCw2MCwxNzYsMjUsMTA4LDE5NywxMjYsMTY4LDQ2LDE4NywxNjcsMSwxNDQsODAsMjA4LDEyMSwxMTksMTMsODUsODcsMTc1LDIzLDIwOCwyMjUsMTYyLDgzLDEyNiwzMSwxOTksOTEsNzcsNTMsNTAsNzgsNTIsMzUsMTEzLDcsMjAwLDYsOSwzMywxOTksMTUyLDE1MiwyMTgsNDAsMTcyLDE2OSwxNTgsNDIsMTgyLDY1LDE4LDIxLDE3MCwyNDYsNTgsMiwxOTQsMTExLDExMCwyMDQsNywyMjMsNzEsMjQsMTk0LDIzMSw0LDY5LDE0MCw5MSwxODQsMTcwLDI0LDE4LDM4LDI0NywyMTEsODgsMjE3LDY3LDI0LDIxNCwxNDAsMTQ2LDc0LDcyLDEzNywxNTYsMjE5LDI0NCwyLDM1LDIzOCwyNDIsMjAzLDM0LDIyLDIxMywyMTksMTk4LDIxOSwzNywyMjMsMjI2LDY4LDczLDE5NywyMTAsMzMsMTIyLDE1NCwyMywxMzcsMTExLDExOSw3OSwxOTYsMTk2LDYxLDEyMywwLDY0LDcsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unrar-ma4.json b/tests/archive-testfiles/test-unrar-ma4.json deleted file mode 100644 index e151403..0000000 --- a/tests/archive-testfiles/test-unrar-ma4.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODIsOTcsMTE0LDMzLDI2LDcsMCwyMDcsMTQ0LDExNSwwLDAsMTMsMCwwLDAsMCwwLDAsMCwxNDcsOTgsMTE2LDMyLDEyOCw0MSwwLDM4LDEsMCwwLDE4OSwxLDAsMCwzLDE4OSwxOTcsMTc4LDE1Miw3MSw5Niw4Myw3NCwyOSw1MSw5LDAsMTY0LDEyOSwwLDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiwxMywxMjksMTIsMTQ1LDE1LDIxMywxMjksNzgsMTQ5LDQwLDgsMTg2LDEzOCwxNDcsMjA2LDEyMiwyMDIsMCwxMjgsMTEwLDE5OCwxNTAsMTI5LDE3OSw2NCwyMjksMjE1LDIyOCwyNDAsMjE2LDE4MywxMjgsMTk0LDU1LDU1LDE4NiwxNDUsMjQsMjE5LDIwNywyMDMsMTY3LDI0MSwyNTQsODMsMTIwLDIxOSw4OCwxNTEsMjIzLDgxLDI1NCwyNDEsMjE3LDIwNyw4MSwxMDcsNDIsMTE5LDkxLDI5LDQzLDg1LDM0LDE1NCwxNTAsMjQ3LDc5LDIwOSw3NiwxNDAsMzYsMjMwLDc4LDE3MCwxNTAsODUsMTgyLDIyNyw0MSw0OCwxNjEsMTU0LDE1NywxNjEsMjksMjAzLDc1LDc5LDY5LDk3LDgyLDc5LDE0MSw4NSwyMDIsMjUzLDc5LDEyNiwyMjMsMTEzLDkzLDQ1LDE2NCwwLDExNiwyMjYsMjA5LDEyNywzOCw0MiwyMjcsMTU5LDM1LDE4MSw0Nyw5NCwxMTIsODgsNDQsMSwyNDcsNjksMjQ0LDg3LDE5NiwyNDcsNjQsMTU4LDk5LDIxNCwxMzAsNzksMjksNzgsMTYxLDIwOCwxMTAsMjE4LDQyLDg0LDM1LDQxLDIzNyw3NSw4MywxODcsNTksOTYsMTY0LDIxNSw1MCwxOTMsMjE5LDE4MCw5LDE5MSwxMSwyNDIsMzcsNTAsMTIwLDIwMSw4LDI1NSwxMzksMjI2LDE5LDIwNywyNTMsOCw0Niw3LDE1NCw5OCw2MCwxNzYsMjUsMTA4LDE5NywxMjYsMTY4LDQ2LDE4NywxNjcsMSwxNDQsODAsMjA4LDEyMSwxMTksMTMsODUsODcsMTc1LDIzLDIwOCwyMjUsMTYyLDgzLDEyNiwzMSwxOTksOTEsNzcsNTMsNTAsNzgsNTIsMzUsMTEzLDcsMjAwLDYsOSwzMywxOTksMTUyLDE1MiwyMTgsNDAsMTcyLDE2OSwxNTgsNDIsMTgyLDY1LDE4LDIxLDE3MCwyNDYsNTgsMiwxOTQsMTExLDExMCwyMDQsNywyMjMsNzEsMjQsMTk0LDIzMSw0LDY5LDE0MCw5MSwxODQsMTcwLDI0LDE4LDM4LDI0NywyMTEsODgsMjE3LDY3LDI0LDIxNCwxNDAsMTQ2LDc0LDcyLDEzNywxNTYsMjE5LDI0NCwyLDM1LDIzOCwyNDIsMjAzLDM0LDIyLDIxMywyMTksMTk4LDIxOSwzNywyMjMsMjI2LDY4LDczLDE5NywyMTAsMzMsMTIyLDE1NCwyMywxMzcsMTExLDExOSw3OSwxOTYsMTk2LDYxLDEyMywwLDY0LDcsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-untar-1.json b/tests/archive-testfiles/test-untar-1.json deleted file mode 100644 index acce644..0000000 --- a/tests/archive-testfiles/test-untar-1.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"MTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDQ4LDQ4LDQ4LDU0LDUyLDUyLDMyLDAsNTEsNTEsNTEsNTEsNTQsNTAsMzIsMCw0OCw0OSw0OSw1NCw0OSw0OCwzMiwwLDQ4LDQ4LDQ4LDQ4LDQ4LDQ4LDQ4LDQ4LDU0LDU1LDUzLDMyLDQ5LDUxLDQ4LDUzLDUwLDUxLDU0LDUyLDQ5LDQ5LDUwLDMyLDQ4LDQ5LDUyLDUwLDUwLDQ4LDAsMzIsNDgsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwxMTcsMTE1LDExNiw5NywxMTQsMCw0OCw0OCwxMDYsMTAxLDEwMiwxMDIsMTE1LDk5LDEwNCwxMDUsMTA4LDEwOCwxMDEsMTE0LDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwxMDEsMTEwLDEwMywwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsNDgsNDgsNDgsNDgsNDgsNDgsMzIsMCw0OCw0OCw0OCw0OCw0OCw0OCwzMiwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCw3NiwxMTEsMTE0LDEwMSwxMDksMzIsMTA1LDExMiwxMTUsMTE3LDEwOSwzMiwxMDAsMTExLDEwOCwxMTEsMTE0LDMyLDExNSwxMDUsMTE2LDMyLDk3LDEwOSwxMDEsMTE2LDQ0LDMyLDk5LDExMSwxMTAsMTE1LDEwMSw5OSwxMTYsMTAxLDExNiwxMTcsMTE0LDMyLDk3LDEwMCwxMDUsMTEyLDEwNSwxMTUsOTksMTA1LDExMCwxMDMsMzIsMTAxLDEwOCwxMDUsMTE2LDQ0LDMyLDExNSwxMDEsMTAwLDMyLDEwMCwxMTEsMzIsMTAxLDEwNSwxMTcsMTE1LDEwOSwxMTEsMTAwLDMyLDExNiwxMDEsMTA5LDExMiwxMTEsMTE0LDMyLDEwNSwxMTAsOTksMTA1LDEwMCwxMDUsMTAwLDExNywxMTAsMTE2LDMyLDExNywxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTAxLDMyLDEwMSwxMTYsMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTA5LDk3LDEwMywxMTAsOTcsMzIsOTcsMTA4LDEwNSwxMTMsMTE3LDk3LDQ2LDMyLDg1LDExNiwzMiwxMDEsMTEwLDEwNSwxMDksMzIsOTcsMTAwLDMyLDEwOSwxMDUsMTEwLDEwNSwxMDksMzIsMTE4LDEwMSwxMTAsMTA1LDk3LDEwOSw0NCwzMiwxMTMsMTE3LDEwNSwxMTUsMzIsMTEwLDExMSwxMTUsMTE2LDExNCwxMTcsMTAwLDMyLDEwMSwxMjAsMTAxLDExNCw5OSwxMDUsMTE2LDk3LDExNiwxMDUsMTExLDExMCwzMiwxMTcsMTA4LDEwOCw5NywxMDksOTksMTExLDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwNSwxMTUsMzIsMTEwLDEwNSwxMTUsMTA1LDMyLDExNywxMTYsMzIsOTcsMTA4LDEwNSwxMTMsMTE3LDEwNSwxMTIsMzIsMTAxLDEyMCwzMiwxMDEsOTcsMzIsOTksMTExLDEwOSwxMDksMTExLDEwMCwxMTEsMzIsOTksMTExLDExMCwxMTUsMTAxLDExMywxMTcsOTcsMTE2LDQ2LDMyLDY4LDExNywxMDUsMTE1LDMyLDk3LDExNywxMTYsMTAxLDMyLDEwNSwxMTQsMTE3LDExNCwxMDEsMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMDUsMTEwLDMyLDExNCwxMDEsMTEyLDExNCwxMDEsMTA0LDEwMSwxMTAsMTAwLDEwMSwxMTQsMTA1LDExNiwzMiwxMDUsMTEwLDMyLDExOCwxMTEsMTA4LDExNywxMTIsMTE2LDk3LDExNiwxMDEsMzIsMTE4LDEwMSwxMDgsMTA1LDExNiwzMiwxMDEsMTE1LDExNSwxMDEsMzIsOTksMTA1LDEwOCwxMDgsMTE3LDEwOSwzMiwxMDAsMTExLDEwOCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE3LDMyLDEwMiwxMTcsMTAzLDEwNSw5NywxMTYsMzIsMTEwLDExNywxMDgsMTA4LDk3LDMyLDExMiw5NywxMTQsMTA1LDk3LDExNiwxMTcsMTE0LDQ2LDMyLDY5LDEyMCw5OSwxMDEsMTEyLDExNiwxMDEsMTE3LDExNCwzMiwxMTUsMTA1LDExMCwxMTYsMzIsMTExLDk5LDk5LDk3LDEwMSw5OSw5NywxMTYsMzIsOTksMTE3LDExMiwxMDUsMTAwLDk3LDExNiw5NywxMTYsMzIsMTEwLDExMSwxMTAsMzIsMTEyLDExNCwxMTEsMTA1LDEwMCwxMDEsMTEwLDExNiw0NCwzMiwxMTUsMTE3LDExMCwxMTYsMzIsMTA1LDExMCwzMiw5OSwxMTcsMTA4LDExMiw5NywzMiwxMTMsMTE3LDEwNSwzMiwxMTEsMTAyLDEwMiwxMDUsOTksMTA1LDk3LDMyLDEwMCwxMDEsMTE1LDEwMSwxMTQsMTE3LDExMCwxMTYsMzIsMTA5LDExMSwxMDgsMTA4LDEwNSwxMTYsMzIsOTcsMTEwLDEwNSwxMDksMzIsMTA1LDEwMCwzMiwxMDEsMTE1LDExNiwzMiwxMDgsOTcsOTgsMTExLDExNCwxMTcsMTA5LDQ2LDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMA==","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unzip-deflate.json b/tests/archive-testfiles/test-unzip-deflate.json deleted file mode 100644 index 7340caf..0000000 --- a/tests/archive-testfiles/test-unzip-deflate.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODAsNzUsMyw0LDIwLDAsMCwwLDgsMCwxMTMsNzYsODIsNzQsNjAsMjcsMjM2LDIwOSwxOSwxLDAsMCwxMjQsMywwLDAsOSwwLDI4LDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiw4NSw4NCw5LDAsMywyMjksMTMzLDE2OCw4OCwyMTQsMTMzLDE2OCw4OCwxMTcsMTIwLDExLDAsMSw0LDI0MiwxODIsMSwwLDQsMTM2LDE5LDAsMCwyMzcsODEsMjA1LDEwOSwxMzEsNDksOCwxODksMTAzLDEzOCw1NSw2NCwxNDksNDEsMjE4LDkxLDE3NCwyOSwxMjgsOTgsMTQ2LDM0LDIxNywxOTgsMTc3LDMzLDIwMiwyNDgsMTk3LDI1MywyMTgsMzMsNDIsMjQ1LDEwMiwxMiwxODgsNjMsNDYsNTQsMTY1LDY1LDE5OSwxMzgsMTM0LDk4LDIxMywzOCwxNTAsNTgsMTY4LDEzNywxOTEsMTI4LDE3Myw0Nyw5NywyMywxNDMsOSw0Miw1OCwxMTYsMTc3LDI0NiwyNywxNjQsMTA2LDU0LDE1MSwxNDgsOTIsMTI4LDEwNCwxNzIsMTAyLDUsNDYsMTA5LDIyOCwxNzgsMTE4LDIxNCwxNjIsMzcsMTg2LDM1LDI4LDE0OSw2MiwxOCwzMCwyMjYsNywxODAsMTYwLDIwOSwxNzMsMTksMTY4LDIzNCw2MSwyMzIsMTQwLDExOSwxMzUsMTE2LDEwOSwxMzcsMTQxLDE2NiwyNTEsMjQxLDIwMCwxNDYsMjE4LDExLDIzOCwxNjEsMTEsMjIxLDE1MCwyMDcsNDAsMTQ0LDE2Nyw3Niw4NiwzOSw4NywyMzUsMTM2LDkwLDE2OSwxNzcsMjksMjAwLDEyMyw3MiwxNTEsMTEwLDE2NiwxMTEsNzIsMjksNTcsMTIsMTYxLDIwLDIyMiw4MiwxNDcsMjksNiwxNDYsMjAyLDIwNywxMjAsMjIxLDE0NCwyMCw0NiwyMDgsMjUsMTY5LDIyOCwyNDAsMTcwLDI5LDgzLDE5OCwxNDgsNzksMjMzLDY5LDEwMiwyNiwyMDcsMTQzLDEzNSwyMTMsMjQsNzMsMzksNDEsMzksMTU3LDY2LDIxNCwxOCwxNzYsMjE0LDI1MCwxNTUsODAsMjYsMTAsOTIsMjI3LDE2NiwyMjgsMjMyLDkxLDE2LDYsMjA1LDQ0LDk4LDE1OCwyNDEsMjQ2LDEwMCwyNSw0NiwxNzcsOTksMjA0LDEyLDE0MCwxNTMsMTMyLDExNSwxNDIsOTksMTA0LDMzLDIyMywyNywyMzMsOTgsNzYsMjExLDM0LDEyNSwxNjcsMTg0LDE0Nyw3NCw4MiwxNDIsNTgsMTA0LDI1MSwxMzQsOTMsMTc1LDIwMiw3NCw0MCwxNzgsMTAwLDIzOCwxMTAsMTc5LDE4NiwxMDEsMjA4LDE0LDcyLDUxLDE0MiwyNDUsMTQ3LDEwNywxODAsMjQzLDIzMywxMTYsMjQ5LDYzLDIyMywyMjMsNjEsMjIzLDIzLDgwLDc1LDEsMiwzMCwzLDIwLDAsMCwwLDgsMCwxMTMsNzYsODIsNzQsNjAsMjcsMjM2LDIwOSwxOSwxLDAsMCwxMjQsMywwLDAsOSwwLDI0LDAsMCwwLDAsMCwxLDAsMCwwLDE2NCwxMjksMCwwLDAsMCwxMDgsMTExLDExNCwxMDEsMTA5LDQ2LDExNiwxMjAsMTE2LDg1LDg0LDUsMCwzLDIyOSwxMzMsMTY4LDg4LDExNywxMjAsMTEsMCwxLDQsMjQyLDE4MiwxLDAsNCwxMzYsMTksMCwwLDgwLDc1LDUsNiwwLDAsMCwwLDEsMCwxLDAsNzksMCwwLDAsODYsMSwwLDAsMCww","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0NiwxMCwxMCw3NiwxMTEsMTE0LDEwMSwxMDksMzIsMTA1LDExMiwxMTUsMTE3LDEwOSwzMiwxMDAsMTExLDEwOCwxMTEsMTE0LDMyLDExNSwxMDUsMTE2LDMyLDk3LDEwOSwxMDEsMTE2LDQ0LDMyLDk5LDExMSwxMTAsMTE1LDEwMSw5OSwxMTYsMTAxLDExNiwxMTcsMTE0LDMyLDk3LDEwMCwxMDUsMTEyLDEwNSwxMTUsOTksMTA1LDExMCwxMDMsMzIsMTAxLDEwOCwxMDUsMTE2LDQ0LDMyLDExNSwxMDEsMTAwLDMyLDEwMCwxMTEsMzIsMTAxLDEwNSwxMTcsMTE1LDEwOSwxMTEsMTAwLDMyLDExNiwxMDEsMTA5LDExMiwxMTEsMTE0LDMyLDEwNSwxMTAsOTksMTA1LDEwMCwxMDUsMTAwLDExNywxMTAsMTE2LDMyLDExNywxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTAxLDMyLDEwMSwxMTYsMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTA5LDk3LDEwMywxMTAsOTcsMzIsOTcsMTA4LDEwNSwxMTMsMTE3LDk3LDQ2LDMyLDg1LDExNiwzMiwxMDEsMTEwLDEwNSwxMDksMzIsOTcsMTAwLDMyLDEwOSwxMDUsMTEwLDEwNSwxMDksMzIsMTE4LDEwMSwxMTAsMTA1LDk3LDEwOSw0NCwzMiwxMTMsMTE3LDEwNSwxMTUsMzIsMTEwLDExMSwxMTUsMTE2LDExNCwxMTcsMTAwLDMyLDEwMSwxMjAsMTAxLDExNCw5OSwxMDUsMTE2LDk3LDExNiwxMDUsMTExLDExMCwzMiwxMTcsMTA4LDEwOCw5NywxMDksOTksMTExLDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwNSwxMTUsMzIsMTEwLDEwNSwxMTUsMTA1LDMyLDExNywxMTYsMzIsOTcsMTA4LDEwNSwxMTMsMTE3LDEwNSwxMTIsMzIsMTAxLDEyMCwzMiwxMDEsOTcsMzIsOTksMTExLDEwOSwxMDksMTExLDEwMCwxMTEsMzIsOTksMTExLDExMCwxMTUsMTAxLDExMywxMTcsOTcsMTE2LDQ2LDMyLDY4LDExNywxMDUsMTE1LDMyLDk3LDExNywxMTYsMTAxLDMyLDEwNSwxMTQsMTE3LDExNCwxMDEsMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMDUsMTEwLDMyLDExNCwxMDEsMTEyLDExNCwxMDEsMTA0LDEwMSwxMTAsMTAwLDEwMSwxMTQsMTA1LDExNiwzMiwxMDUsMTEwLDMyLDExOCwxMTEsMTA4LDExNywxMTIsMTE2LDk3LDExNiwxMDEsMzIsMTE4LDEwMSwxMDgsMTA1LDExNiwzMiwxMDEsMTE1LDExNSwxMDEsMzIsOTksMTA1LDEwOCwxMDgsMTE3LDEwOSwzMiwxMDAsMTExLDEwOCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE3LDMyLDEwMiwxMTcsMTAzLDEwNSw5NywxMTYsMzIsMTEwLDExNywxMDgsMTA4LDk3LDMyLDExMiw5NywxMTQsMTA1LDk3LDExNiwxMTcsMTE0LDQ2LDMyLDY5LDEyMCw5OSwxMDEsMTEyLDExNiwxMDEsMTE3LDExNCwzMiwxMTUsMTA1LDExMCwxMTYsMzIsMTExLDk5LDk5LDk3LDEwMSw5OSw5NywxMTYsMzIsOTksMTE3LDExMiwxMDUsMTAwLDk3LDExNiw5NywxMTYsMzIsMTEwLDExMSwxMTAsMzIsMTEyLDExNCwxMTEsMTA1LDEwMCwxMDEsMTEwLDExNiw0NCwzMiwxMTUsMTE3LDExMCwxMTYsMzIsMTA1LDExMCwzMiw5OSwxMTcsMTA4LDExMiw5NywzMiwxMTMsMTE3LDEwNSwzMiwxMTEsMTAyLDEwMiwxMDUsOTksMTA1LDk3LDMyLDEwMCwxMDEsMTE1LDEwMSwxMTQsMTE3LDExMCwxMTYsMzIsMTA5LDExMSwxMDgsMTA4LDEwNSwxMTYsMzIsOTcsMTEwLDEwNSwxMDksMzIsMTA1LDEwMCwzMiwxMDEsMTE1LDExNiwzMiwxMDgsOTcsOTgsMTExLDExNCwxMTcsMTA5LDQ2"} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unzip-descriptor.json b/tests/archive-testfiles/test-unzip-descriptor.json deleted file mode 100644 index 71fc9a4..0000000 --- a/tests/archive-testfiles/test-unzip-descriptor.json +++ /dev/null @@ -1,4 +0,0 @@ -window.archiveTestFile = { - "archivedFile": "ODAsNzUsMyw0LDIwLDAsOCwwLDgsMCwyNiw0MCwzNyw3MSwwLDAsMCwwLDAsMCwwLDAsNjQsMSwwLDAsMTAsMCwyOCwwLDExNSwxMDEsOTksMTExLDExMCwxMDAsNDYsMTE2LDEyMCwxMTYsODUsODQsOSwwLDMsMTE2LDIxNywyMzQsODUsOTUsMTkwLDE3MSw5NCwxMTcsMTIwLDExLDAsMSw0LDI0MiwxODIsMSwwLDQsODMsOTUsMSwwLDExLDExOCwxMTcsMjQ2LDI0NywxMTUsODEsOCwzOCwxNDMsMjI2LDIyOSwzNCw4MywyMjcsMTc2LDIwOSwxNSwwLDgwLDc1LDcsOCwyMjQsOCwyLDkwLDE5LDAsMCwwLDY0LDEsMCwwLDgwLDc1LDEsMiwzMCwzLDIwLDAsOCwwLDgsMCwyNiw0MCwzNyw3MSwyMjQsOCwyLDkwLDE5LDAsMCwwLDY0LDEsMCwwLDEwLDAsMjQsMCwwLDAsMCwwLDEsMCwwLDAsMTY0LDEyOSwwLDAsMCwwLDExNSwxMDEsOTksMTExLDExMCwxMDAsNDYsMTE2LDEyMCwxMTYsODUsODQsNSwwLDMsMTE2LDIxNywyMzQsODUsMTE3LDEyMCwxMSwwLDEsNCwyNDIsMTgyLDEsMCw0LDgzLDk1LDEsMCw4MCw3NSw1LDYsMCwwLDAsMCwxLDAsMSwwLDgwLDAsMCwwLDEwMywwLDAsMCwwLDA=", - "unarchivedFile": "ODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMTMsMTAsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMTMsMTAsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMTMsMTAsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMTMsMTAsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMzIsODMsNjksNjcsNzksNzgsNjgsMTMsMTA=" -} \ No newline at end of file diff --git a/tests/archive-testfiles/test-unzip-store.json b/tests/archive-testfiles/test-unzip-store.json deleted file mode 100644 index 40d6b47..0000000 --- a/tests/archive-testfiles/test-unzip-store.json +++ /dev/null @@ -1 +0,0 @@ -window.archiveTestFile={"archivedFile":"ODAsNzUsMyw0LDEwLDAsMCwwLDAsMCwyNDMsODUsODMsNzQsMTg5LDE5NywxNzgsMTUyLDE4OSwxLDAsMCwxODksMSwwLDAsOSwwLDI4LDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiw4NSw4NCw5LDAsMyw3NCwyMzIsMTY5LDg4LDk2LDIzMiwxNjksODgsMTE3LDEyMCwxMSwwLDEsNCwyNDIsMTgyLDEsMCw0LDEzNiwxOSwwLDAsNzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Niw4MCw3NSwxLDIsMzAsMywxMCwwLDAsMCwwLDAsMjQzLDg1LDgzLDc0LDE4OSwxOTcsMTc4LDE1MiwxODksMSwwLDAsMTg5LDEsMCwwLDksMCwyNCwwLDAsMCwwLDAsMCwwLDAsMCwxNjQsMTI5LDAsMCwwLDAsMTA4LDExMSwxMTQsMTAxLDEwOSw0NiwxMTYsMTIwLDExNiw4NSw4NCw1LDAsMyw3NCwyMzIsMTY5LDg4LDExNywxMjAsMTEsMCwxLDQsMjQyLDE4MiwxLDAsNCwxMzYsMTksMCwwLDgwLDc1LDUsNiwwLDAsMCwwLDEsMCwxLDAsNzksMCwwLDAsMCwyLDAsMCwwLDA=","unarchivedFile":"NzYsMTExLDExNCwxMDEsMTA5LDMyLDEwNSwxMTIsMTE1LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwzMiwxMTUsMTA1LDExNiwzMiw5NywxMDksMTAxLDExNiw0NCwzMiw5OSwxMTEsMTEwLDExNSwxMDEsOTksMTE2LDEwMSwxMTYsMTE3LDExNCwzMiw5NywxMDAsMTA1LDExMiwxMDUsMTE1LDk5LDEwNSwxMTAsMTAzLDMyLDEwMSwxMDgsMTA1LDExNiw0NCwzMiwxMTUsMTAxLDEwMCwzMiwxMDAsMTExLDMyLDEwMSwxMDUsMTE3LDExNSwxMDksMTExLDEwMCwzMiwxMTYsMTAxLDEwOSwxMTIsMTExLDExNCwzMiwxMDUsMTEwLDk5LDEwNSwxMDAsMTA1LDEwMCwxMTcsMTEwLDExNiwzMiwxMTcsMTE2LDMyLDEwOCw5Nyw5OCwxMTEsMTE0LDEwMSwzMiwxMDEsMTE2LDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMTAxLDMyLDEwOSw5NywxMDMsMTEwLDk3LDMyLDk3LDEwOCwxMDUsMTEzLDExNyw5Nyw0NiwzMiw4NSwxMTYsMzIsMTAxLDExMCwxMDUsMTA5LDMyLDk3LDEwMCwzMiwxMDksMTA1LDExMCwxMDUsMTA5LDMyLDExOCwxMDEsMTEwLDEwNSw5NywxMDksNDQsMzIsMTEzLDExNywxMDUsMTE1LDMyLDExMCwxMTEsMTE1LDExNiwxMTQsMTE3LDEwMCwzMiwxMDEsMTIwLDEwMSwxMTQsOTksMTA1LDExNiw5NywxMTYsMTA1LDExMSwxMTAsMzIsMTE3LDEwOCwxMDgsOTcsMTA5LDk5LDExMSwzMiwxMDgsOTcsOTgsMTExLDExNCwxMDUsMTE1LDMyLDExMCwxMDUsMTE1LDEwNSwzMiwxMTcsMTE2LDMyLDk3LDEwOCwxMDUsMTEzLDExNywxMDUsMTEyLDMyLDEwMSwxMjAsMzIsMTAxLDk3LDMyLDk5LDExMSwxMDksMTA5LDExMSwxMDAsMTExLDMyLDk5LDExMSwxMTAsMTE1LDEwMSwxMTMsMTE3LDk3LDExNiw0NiwzMiw2OCwxMTcsMTA1LDExNSwzMiw5NywxMTcsMTE2LDEwMSwzMiwxMDUsMTE0LDExNywxMTQsMTAxLDMyLDEwMCwxMTEsMTA4LDExMSwxMTQsMzIsMTA1LDExMCwzMiwxMTQsMTAxLDExMiwxMTQsMTAxLDEwNCwxMDEsMTEwLDEwMCwxMDEsMTE0LDEwNSwxMTYsMzIsMTA1LDExMCwzMiwxMTgsMTExLDEwOCwxMTcsMTEyLDExNiw5NywxMTYsMTAxLDMyLDExOCwxMDEsMTA4LDEwNSwxMTYsMzIsMTAxLDExNSwxMTUsMTAxLDMyLDk5LDEwNSwxMDgsMTA4LDExNywxMDksMzIsMTAwLDExMSwxMDgsMTExLDExNCwxMDEsMzIsMTAxLDExNywzMiwxMDIsMTE3LDEwMywxMDUsOTcsMTE2LDMyLDExMCwxMTcsMTA4LDEwOCw5NywzMiwxMTIsOTcsMTE0LDEwNSw5NywxMTYsMTE3LDExNCw0NiwzMiw2OSwxMjAsOTksMTAxLDExMiwxMTYsMTAxLDExNywxMTQsMzIsMTE1LDEwNSwxMTAsMTE2LDMyLDExMSw5OSw5OSw5NywxMDEsOTksOTcsMTE2LDMyLDk5LDExNywxMTIsMTA1LDEwMCw5NywxMTYsOTcsMTE2LDMyLDExMCwxMTEsMTEwLDMyLDExMiwxMTQsMTExLDEwNSwxMDAsMTAxLDExMCwxMTYsNDQsMzIsMTE1LDExNywxMTAsMTE2LDMyLDEwNSwxMTAsMzIsOTksMTE3LDEwOCwxMTIsOTcsMzIsMTEzLDExNywxMDUsMzIsMTExLDEwMiwxMDIsMTA1LDk5LDEwNSw5NywzMiwxMDAsMTAxLDExNSwxMDEsMTE0LDExNywxMTAsMTE2LDMyLDEwOSwxMTEsMTA4LDEwOCwxMDUsMTE2LDMyLDk3LDExMCwxMDUsMTA5LDMyLDEwNSwxMDAsMzIsMTAxLDExNSwxMTYsMzIsMTA4LDk3LDk4LDExMSwxMTQsMTE3LDEwOSw0Ng=="} \ No newline at end of file diff --git a/tests/codecs.spec.js b/tests/codecs.spec.js index c84fa58..5e350c8 100644 --- a/tests/codecs.spec.js +++ b/tests/codecs.spec.js @@ -75,6 +75,13 @@ describe('codecs test suite', () => { })).equals('audio/mpeg'); }); + it('detects M4A audio', () => { + expect(getShortMIMEString({ + format: { filename: 'sample.m4a', format_name: 'mov,mp4,m4a,3gp,3g2,mj2' }, + streams: [ { codec_type: 'video', }, { codec_type: 'audio' } ], + })).equals('audio/mp4'); + }); + it('detects MP4 video', () => { expect(getShortMIMEString({ format: { format_name: 'mov,mp4,m4a,3gp,3g2,mj2' }, @@ -106,16 +113,62 @@ describe('codecs test suite', () => { it('detects WEBM video', () => { expect(getShortMIMEString({ format: { format_name: 'matroska,webm' }, - streams: [ { codec_type: 'video' } ], + streams: [ { codec_type: 'video', codec_name: 'vp8' } ], + })).equals('video/webm'); + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'video', codec_name: 'vp9' } ], + })).equals('video/webm'); + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'video', codec_name: 'av1' } ], })).equals('video/webm'); }); it('detects WEBM audio', () => { expect(getShortMIMEString({ format: { format_name: 'matroska,webm' }, - streams: [ { codec_type: 'audio' } ], + streams: [ { codec_type: 'audio', codec_name: 'opus' } ], + })).equals('audio/webm'); + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'audio', codec_name: 'vorbis' } ], })).equals('audio/webm'); }); + + it('detects Matroska Video', () => { + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'video', codec_name: 'h264' } ], + })).equals('video/x-matroska'); + + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ + { codec_type: 'audio', codec_name: 'aac' }, + { codec_type: 'video', codec_name: 'vp9' }, + ], + })).equals('video/x-matroska'); + + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ + { codec_type: 'audio', codec_name: 'vorbis' }, + { codec_type: 'video', codec_name: 'h264' }, + ], + })).equals('video/x-matroska'); + }); + + it('detects Matroska audio', () => { + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'audio', codec_name: 'aac' } ], + })).equals('audio/x-matroska'); + expect(getShortMIMEString({ + format: { format_name: 'matroska,webm' }, + streams: [ { codec_type: 'audio', codec_name: 'dts' } ], + })).equals('audio/x-matroska'); + }); }); describe('getFullMIMEString()', () => { @@ -218,112 +271,242 @@ describe('codecs test suite', () => { .to.be.a('string') .and.satisfy(s => s.startsWith('video/mp4; codecs="avc1.4D00')); }); + + describe('extradata tests', () => { + it('prefers extradata for codec string', () => { + info.streams[0].extradata = '00000000: 014d 4028 ffe1 001a 674d 4028 9a66 0140 .M@(...gM@(.f.@'; + expect(getFullMIMEString(info)) + .equals('video/mp4; codecs="avc1.4D4028"'); + }); + + it('handles multiline extradata', () => { + info.streams[0].extradata = + '00000000: 0164 0029 ffe1 001b 6764 0029 ac2b 4028 .d.)....gd.).+@(\n' + + '00000010: 2f82 1801 1000 0003 0010 0000 0303 20f1 /............. .'; + expect(getFullMIMEString(info)) + .equals('video/mp4; codecs="avc1.640029"'); + }); + + it('does not use extradata if codec_tag_string is not avc1', () => { + info.streams[0].codec_tag_string = 'not-avc1'; + info.streams[0].codec_name = 'h264'; + info.streams[0].profile = 'Main'; + info.streams[0].level = 20; + info.streams[0].extradata = '00000000: 014d 4028 ffe1 001a 674d 4028 9a66 0140 .M@(...gM@(.f.@'; + expect(getFullMIMEString(info)) + .equals('video/mp4; codecs="avc1.4D0014"'); + }); + }); }); }); - describe('VP09 / VP9', () => { - /** @type {ProbeInfo} */ - let info; + describe('WebM' ,() => { + describe('AV1', () => { + /** @type {ProbeInfo} */ + let info; + + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'video', codec_name: 'av1' }], + }; + }); - beforeEach(() => { - info = { - format: { format_name: 'matroska,webm' }, - streams: [{ - codec_type: 'video', - codec_tag_string: 'vp09', - }], - }; + it('outputs MIME string', () => { + expect(getShortMIMEString(info)).equals('video/webm'); + expect(getFullMIMEString(info)).equals('video/webm; codecs="av1"') + }); }); - describe('Profile tests', () => { + describe('VP8', () => { + /** @type {ProbeInfo} */ + let info; + beforeEach(() => { - info.streams[0].level = 20; + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'video', codec_name: 'vp8' }], + }; }); - it('detects Profile 0', () => { - info.streams[0].profile = 'Profile 0'; - expect(getFullMIMEString(info)) - .to.be.a('string') - .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.00.')); + it('outputs MIME string', () => { + expect(getShortMIMEString(info)).equals('video/webm'); + expect(getFullMIMEString(info)).equals('video/webm; codecs="vp8"') }); }); - describe('Level tests', () => { + describe('VP09 / VP9', () => { + /** @type {ProbeInfo} */ + let info; + beforeEach(() => { - info.streams[0].profile = 'Profile 0'; + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'video', codec_name: 'vp9', codec_tag_string: 'vp09' }], + }; }); - - it('detects 2-digit hex level', () => { + + describe('Profile tests', () => { + beforeEach(() => { + info.streams[0].level = 20; + }); + + it('detects Profile 0', () => { + info.streams[0].profile = 'Profile 0'; + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.00.')); + }); + }); + + describe('Level tests', () => { + beforeEach(() => { + info.streams[0].profile = 'Profile 0'; + }); + + it('detects 2-digit hex level', () => { + info.streams[0].level = 21; // 0x15 + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.')) + .and.satisfy(s => { + const matches = s.match(/vp09\.[0-9]{2}\.([0-9A-F]{2})\.[0-9A-F]{2}/); + return matches && matches.length === 2 && matches[1] === '15'; + }); + }); + + it('detects level = -99 as level 1', () => { + info.streams[0].level = -99; // I believe this is the "unknown" value from ffprobe. + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('video/webm; codecs="vp09.00.10.10"'); + }); + }); + + it('detects codec_name=vp9 but no codec_tag_string', () => { + info.streams[0].codec_name = 'vp9'; + info.streams[0].codec_tag_string = '[0][0][0][0]'; + info.streams[0].profile = 'Profile 0'; info.streams[0].level = 21; // 0x15 expect(getFullMIMEString(info)) - .to.be.a('string') - .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.')) - .and.satisfy(s => { - const matches = s.match(/vp09\.[0-9]{2}\.([0-9A-F]{2})\.[0-9A-F]{2}/); - return matches && matches.length === 2 && matches[1] === '15'; - }); - }); + .to.be.a('string') + .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.00.15')); + }); + }); - it('detects level = -99', () => { - info.streams[0].level = -99; // I'm not sure what ffprobe means by this. + describe('Vorbis', () => { + /** @type {ProbeInfo} */ + let info; + + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'audio', codec_name: 'vorbis' }], + }; + }); + + it('detects vorbis', () => { expect(getFullMIMEString(info)) .to.be.a('string') - .and.equals('video/webm; codecs="vp9"'); + .and.equals('audio/webm; codecs="vorbis"'); }); }); - - it('detects codec_name=vp9 but no codec_tag_string', () => { - info.streams[0].codec_name = 'vp9'; - info.streams[0].codec_tag_string = '[0][0][0][0]'; - info.streams[0].profile = 'Profile 0'; - info.streams[0].level = 21; // 0x15 - expect(getFullMIMEString(info)) - .to.be.a('string') - .and.satisfy(s => s.startsWith('video/webm; codecs="vp09.00.15')); - }); + + describe('Opus', () => { + /** @type {ProbeInfo} */ + let info; + + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ + codec_type: 'audio', + codec_name: 'opus', + }], + }; + }); + + it('detects opus', () => { + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('audio/webm; codecs="opus"'); + }); + }); }); - describe('MPEG2', () => { - /** @type {ProbeInfo} */ - let info; - - beforeEach(() => { - info = { - format: { format_name: 'matroska,webm' }, - streams: [{ - codec_type: 'video', - codec_name: 'mpeg2video', - }], - }; + describe('Matroska', () => { + describe('MPEG2 codec', () => { + /** @type {ProbeInfo} */ + let info; + + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'video', codec_name: 'mpeg2video' }], + }; + }); + + it('outputs full MIME string', () => { + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('video/x-matroska; codecs="mpeg2video"'); + }); + }); + + describe('AC-3', () => { + /** @type {ProbeInfo} */ + let info; + + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'audio', codec_name: 'ac3' }], + }; + }); + + it('detects AC-3', () => { + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('audio/x-matroska; codecs="ac-3"'); + }); }); + + describe('DTS', () => { + /** @type {ProbeInfo} */ + let info; - it('detects mpeg2video', () => { - expect(getFullMIMEString(info)) - .to.be.a('string') - .and.equals('video/webm; codecs="mpeg2video"'); + beforeEach(() => { + info = { + format: { format_name: 'matroska,webm' }, + streams: [{ codec_type: 'audio', codec_name: 'dts' }], + }; + }); + + it('outputs full MIME string', () => { + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('audio/x-matroska; codecs="dts"'); + }); }); }); describe('MP4A / AAC', () => { /** @type {ProbeInfo} */ - let info; - - beforeEach(() => { - info = { - format: { format_name: 'mov,mp4,m4a,3gp,3g2,mj2' }, - streams: [{ - codec_type: 'audio', - codec_tag_string: 'mp4a', - }], - }; - }); + let baseInfo = { + format: { format_name: 'mov,mp4,m4a,3gp,3g2,mj2' }, + streams: [{ + codec_type: 'audio', + codec_tag_string: 'mp4a', + }], + }; it('throws when unknown', () => { - expect(() => getFullMIMEString(info)).to.throw(); + expect(() => getFullMIMEString(baseInfo)).to.throw(); }); describe('Profile tests', () => { it('recognizes AAC-LC', () => { + const info = structuredClone(baseInfo); info.streams[0].profile = 'LC'; expect(getFullMIMEString(info)) .to.be.a('string') @@ -331,7 +514,17 @@ describe('codecs test suite', () => { }); }); + it('recognizes codec_name=HE-AAC', () => { + const info = structuredClone(baseInfo); + info.streams[0].profile = 'HE-AAC'; + info.streams[0].codec_name = 'aac'; + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('audio/mp4; codecs="mp4a.40.5"'); + }); + it('handles codec_name=aac but no codec_tag_string', () => { + const info = structuredClone(baseInfo); info.streams[0].profile = 'LC'; info.streams[0].codec_tag_string = '[0][0][0][0]'; info.streams[0].codec_name = 'aac'; @@ -339,6 +532,16 @@ describe('codecs test suite', () => { .to.be.a('string') .and.equals('audio/mp4; codecs="mp4a.40.2"'); }); + + it('handles m4a file with MJPEG stream', () => { + const info = structuredClone(baseInfo); + info.format.filename = 'sample.m4a'; + info.streams[0].profile = 'LC'; + info.streams.push({codec_name: 'mjpeg', codec_type: 'video', profile: 'Baseline'}); + expect(getFullMIMEString(info)) + .to.be.a('string') + .and.equals('audio/mp4; codecs="mp4a.40.2"'); + }) }); describe('MP4 / FLAC', () => { @@ -374,70 +577,55 @@ describe('codecs test suite', () => { expect(getFullMIMEString(vInfo)) .to.be.a('string') .and.equals('video/mp4; codecs="flac"'); - + }); }); - describe('Vorbis', () => { - /** @type {ProbeInfo} */ - let info; - - beforeEach(() => { - info = { - format: { format_name: 'matroska,webm' }, - streams: [{ - codec_type: 'audio', - codec_name: 'vorbis', - }], + describe('AVI', () => { + it('detects AVI', () => { + /** @type {ProbeInfo} */ + let info = { + format: { format_name: 'avi' }, + streams: [ + { codec_type: 'video', codec_name: 'mpeg4', codec_tag_string: 'XVID' }, + { codec_type: 'audio', codec_name: 'mp3' }, + { codec_type: 'audio', codec_name: 'mp3' }, + ], }; - }); - - it('detects vorbis', () => { + expect(getShortMIMEString(info)) + .to.be.a('string') + .and.equals('video/x-msvideo'); expect(getFullMIMEString(info)) .to.be.a('string') - .and.equals('audio/webm; codecs="vorbis"'); + .and.equals('video/x-msvideo'); }); }); - describe('Opus', () => { - /** @type {ProbeInfo} */ - let info; - - beforeEach(() => { - info = { - format: { format_name: 'matroska,webm' }, - streams: [{ - codec_type: 'audio', - codec_name: 'opus', - }], + describe('MP3', () => { + it('detects MP3', () => { + /** @type {ProbeInfo} */ + let info = { + format: { format_name: 'mov,mp4,m4a,3gp,3g2,mj2' }, + streams: [ { codec_type: 'audio', codec_name: 'mp3', codec_tag_string: 'mp4a' } ], }; - }); - - it('detects opus', () => { - expect(getFullMIMEString(info)) - .to.be.a('string') - .and.equals('audio/webm; codecs="opus"'); + expect(getShortMIMEString(info)).equals('audio/mp4'); + expect(getFullMIMEString(info)).equals('audio/mp4; codecs="mp3"'); }); }); - describe('AC-3', () => { - /** @type {ProbeInfo} */ - let info; - - beforeEach(() => { - info = { - format: { format_name: 'matroska,webm' }, - streams: [{ - codec_type: 'audio', - codec_name: 'ac3', - }], + describe('WAV', () => { + it('detects WAV', () => { + /** @type {ProbeInfo} */ + let info = { + format: { format_name: 'wav' }, + streams: [ { codec_type: 'audio', codec_name: 'pcm_s16le' } ], }; - }); - - it('detects AC-3', () => { + expect(getShortMIMEString(info)) + .to.be.a('string') + .and.equals('audio/wav'); expect(getFullMIMEString(info)) .to.be.a('string') - .and.equals('audio/webm; codecs="ac-3"'); + .and.equals('audio/wav'); }); }); }); diff --git a/tests/file-sniffer.spec.js b/tests/file-sniffer.spec.js index db29c84..6041e00 100644 --- a/tests/file-sniffer.spec.js +++ b/tests/file-sniffer.spec.js @@ -24,6 +24,7 @@ describe('bitjs.file.sniffer', () => { it('PNG', () => { sniffTest('image/png', [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF]); }); it('WebP', () => { sniffTest('image/webp', [0x52, 0x49, 0x46, 0x46, 0x01, 0x02, 0x03, 0x04, 0x57, 0x45, 0x42, 0x50, 0x81]); }); it('ICO', () => { sniffTest('image/x-icon', [0x00, 0x00, 0x01, 0x00, 0x69])}); + it('GZIP', () => { sniffTest('application/gzip', [0x1F, 0x8B, 0x08])}); it('ZIP', () => { sniffTest('application/zip', [0x50, 0x4B, 0x03, 0x04, 0x20]); }); it('ZIP_empty', () => { sniffTest('application/zip', [0x50, 0x4B, 0x05, 0x06, 0x20]); }); it('ZIP_spanned', () => { sniffTest('application/zip', [0x50, 0x4B, 0x07, 0x08, 0x20]); }); diff --git a/tests/image-parsers-gif.spec.js b/tests/image-parsers-gif.spec.js new file mode 100644 index 0000000..fc61a9f --- /dev/null +++ b/tests/image-parsers-gif.spec.js @@ -0,0 +1,56 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; +import { GifParser } from '../image/parsers/gif.js'; + +const COMMENT_GIF = `tests/image-testfiles/comment.gif`; +const XMP_GIF = 'tests/image-testfiles/xmp.gif'; + +describe('bitjs.image.parsers.GifParser', () => { + it('parses GIF with Comment Extension', async () => { + const nodeBuf = fs.readFileSync(COMMENT_GIF); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + + const parser = new GifParser(ab); + let trailerFound = false; + let comment; + parser.onLogicalScreen(evt => { + const {logicalScreenWidth, logicalScreenHeight} = evt.detail; + expect(logicalScreenWidth).equals(32); + expect(logicalScreenHeight).equals(52); + }); + parser.onTableBasedImage(evt => { + const {imageWidth, imageHeight} = evt.detail; + expect(imageWidth).equals(32); + expect(imageHeight).equals(52); + }); + parser.onCommentExtension(evt => comment = evt.detail); + parser.onTrailer(evt => trailerFound = true); + + await parser.start(); + + expect(trailerFound).equals(true); + expect(comment).equals('JEFF!'); + }); + + it('parses GIF with Application Extension', async () => { + const nodeBuf = fs.readFileSync(XMP_GIF); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + + const parser = new GifParser(ab); + let appId; + let appAuthCode; + let hasAppData = false; + parser.onApplicationExtension(evt => { + appId = evt.detail.applicationIdentifier + appAuthCode = new TextDecoder().decode( + evt.detail.applicationAuthenticationCode); + hasAppData = evt.detail.applicationData.byteLength > 0; + }); + + await parser.start(); + + expect(appId).equals("XMP Data"); + expect(appAuthCode).equals('XMP'); + }); +}); diff --git a/tests/image-parsers-jpeg.spec.js b/tests/image-parsers-jpeg.spec.js new file mode 100644 index 0000000..66b05ec --- /dev/null +++ b/tests/image-parsers-jpeg.spec.js @@ -0,0 +1,39 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; +import { JpegParser } from '../image/parsers/jpeg.js'; +import { ExifDataFormat, ExifTagNumber } from '../image/parsers/exif.js'; + +/** @typedef {import('../image/parsers/jpeg.js').JpegStartOfFrame} JpegStartOfFrame */ +/** @typedef {import('../image/parsers/exif.js').ExifValue} ExifValue */ + +const FILE_LONG_DESC = 'tests/image-testfiles/long_description.jpg' + +describe('bitjs.image.parsers.JpegParser', () => { + it('extracts Exif and SOF', async () => { + const nodeBuf = fs.readFileSync(FILE_LONG_DESC); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + + /** @type {Map { exif = evt.detail }) + .onStartOfFrame(evt => { sof = evt.detail }); + await parser.start(); + + const descVal = exif.get(ExifTagNumber.IMAGE_DESCRIPTION); + expect(descVal.dataFormat).equals(ExifDataFormat.ASCII_STRING); + + const LONG_DESC = 'Operation Mountain Viper put the soldiers of A Company, 2nd Battalion 22nd ' + + 'Infantry Division, 10th Mountain in the Afghanistan province of Daychopan to search for ' + + 'Taliban and or weapon caches that could be used against U.S. and allied forces. Soldiers ' + + 'quickly walk to the ramp of the CH-47 Chinook cargo helicopter that will return them to ' + + 'Kandahar Army Air Field. (U.S. Army photo by Staff Sgt. Kyle Davis) (Released)'; + expect(descVal.stringValue).equals(LONG_DESC); + expect(exif.get(ExifTagNumber.EXIF_IMAGE_HEIGHT).numericalValue).equals(sof.imageHeight); + expect(exif.get(ExifTagNumber.EXIF_IMAGE_WIDTH).numericalValue).equals(sof.imageWidth); + }); +}); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js new file mode 100644 index 0000000..c197446 --- /dev/null +++ b/tests/image-parsers-png.spec.js @@ -0,0 +1,321 @@ +import * as fs from 'node:fs'; +import 'mocha'; +import { expect } from 'chai'; +import { PngColorType, PngInterlaceMethod, PngUnitSpecifier, PngParser } from '../image/parsers/png.js'; +import { ExifDataFormat, ExifTagNumber } from '../image/parsers/exif.js'; + +/** @typedef {import('../image/parsers/exif.js').ExifValue} ExifValue */ + +/** @typedef {import('../image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */ +/** @typedef {import('../image/parsers/png.js').PngChromaticities} PngChromaticies */ +/** @typedef {import('../image/parsers/png.js').PngCompressedTextualData} PngCompressedTextualData */ +/** @typedef {import('../image/parsers/png.js').PngHistogram} PngHistogram */ +/** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */ +/** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */ +/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ +/** @typedef {import('../image/parsers/png.js').PngIntlTextualData} PngIntlTextualData */ +/** @typedef {import('../image/parsers/png.js').PngLastModTime} PngLastModTime */ +/** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */ +/** @typedef {import('../image/parsers/png.js').PngPhysicalPixelDimensions} PngPhysicalPixelDimensions */ +/** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */ +/** @typedef {import('../image/parsers/png.js').PngSuggestedPalette} PngSuggestedPalette */ +/** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */ +/** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */ + +function getPngParser(fileName) { + const nodeBuf = fs.readFileSync(fileName); + const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); + return new PngParser(ab); +} + +describe('bitjs.image.parsers.PngParser', () => { + describe('IHDR', () => { + it('extracts IHDR', async () => { + /** @type {PngImageHeader} */ + let header; + + await getPngParser('tests/image-testfiles/PngSuite.png') + .onImageHeader(evt => { header = evt.detail }) + .start(); + + expect(header.width).equals(256); + expect(header.height).equals(256); + expect(header.bitDepth).equals(8); + expect(header.colorType).equals(PngColorType.TRUE_COLOR); + expect(header.compressionMethod).equals(0); + expect(header.filterMethod).equals(0); + expect(header.interlaceMethod).equals(PngInterlaceMethod.NO_INTERLACE); + }); + + it('throws on corrupt signature', async () => { + /** @type {PngImageHeader} */ + let header; + + try { + await getPngParser('tests/image-testfiles/xs1n0g01.png') + .onImageHeader(evt => { header = evt.detail }) + .start(); + throw new Error(`PngParser did not throw an error for corrupt PNG signature`); + } catch (err) { + expect(err.startsWith('Bad PNG signature')).equals(true); + } + }); + }); + + it('extracts gAMA', async () => { + /** @type {number} */ + let gamma; + await getPngParser('tests/image-testfiles/g05n3p04.png') + .onGamma(evt => gamma = evt.detail) + .start(); + expect(gamma).equals(55000); + }); + + it('extracts sBIT', async () => { + /** @type {PngSignificantBits} */ + let sBits; + await getPngParser('tests/image-testfiles/cs3n2c16.png') + .onSignificantBits(evt => sBits = evt.detail) + .start(); + expect(sBits.significant_red).equals(13); + expect(sBits.significant_green).equals(13); + expect(sBits.significant_blue).equals(13); + expect(sBits.significant_greyscale).equals(undefined); + expect(sBits.significant_alpha).equals(undefined); + }); + + it('extracts cHRM', async () => { + /** @type {PngChromaticies} */ + let chromaticities; + await getPngParser('tests/image-testfiles/ccwn2c08.png') + .onChromaticities(evt => chromaticities = evt.detail) + .start(); + expect(chromaticities.whitePointX).equals(31270); + expect(chromaticities.whitePointY).equals(32900); + expect(chromaticities.redX).equals(64000); + expect(chromaticities.redY).equals(33000); + expect(chromaticities.greenX).equals(30000); + expect(chromaticities.greenY).equals(60000); + expect(chromaticities.blueX).equals(15000); + expect(chromaticities.blueY).equals(6000); + }); + + it('extracts PLTE', async () => { + /** @type {PngPalette} */ + let palette; + await getPngParser('tests/image-testfiles/tbbn3p08.png') + .onPalette(evt => palette = evt.detail) + .start(); + expect(palette.entries.length).equals(246); + const entry = palette.entries[1]; + expect(entry.red).equals(128); + expect(entry.green).equals(86); + expect(entry.blue).equals(86); + }); + + describe('tRNS', () => { + it('extracts alpha palette', async () => { + /** @type {PngTransparency} */ + let transparency; + await getPngParser('tests/image-testfiles/tbbn3p08.png') + .onTransparency(evt => transparency = evt.detail) + .start(); + + expect(transparency.alphaPalette.length).equals(1); + expect(transparency.alphaPalette[0]).equals(0); + }); + + it('extracts 8-bit RGB transparency', async () => { + /** @type {PngTransparency} */ + let transparency; + await getPngParser('tests/image-testfiles/tbrn2c08.png') + .onTransparency(evt => transparency = evt.detail) + .start(); + + expect(transparency.redSampleValue).equals(255); + expect(transparency.blueSampleValue).equals(255); + expect(transparency.greenSampleValue).equals(255); + }); + + it('extracts 16-bit RGB transparency', async () => { + /** @type {PngTransparency} */ + let transparency; + await getPngParser('tests/image-testfiles/tbgn2c16.png') + .onTransparency(evt => transparency = evt.detail) + .start(); + + expect(transparency.redSampleValue).equals(65535); + expect(transparency.blueSampleValue).equals(65535); + expect(transparency.greenSampleValue).equals(65535); + }); + }); + + it('extracts IDAT', async () => { + /** @type {PngImageData} */ + let data; + + await getPngParser('tests/image-testfiles/PngSuite.png') + .onImageData(evt => { data = evt.detail }) + .start(); + + expect(data.rawImageData.byteLength).equals(2205); + expect(data.rawImageData[0]).equals(120); + }); + + it('extracts tEXt', async () => { + /** @type {PngTextualData[]} */ + let textualDataArr = []; + + await getPngParser('tests/image-testfiles/ctzn0g04.png') + .onTextualData(evt => { textualDataArr.push(evt.detail) }) + .start(); + + expect(textualDataArr.length).equals(2); + expect(textualDataArr[0].keyword).equals('Title'); + expect(textualDataArr[0].textString).equals('PngSuite'); + expect(textualDataArr[1].keyword).equals('Author'); + expect(textualDataArr[1].textString).equals('Willem A.J. van Schaik\n(willem@schaik.com)'); + }); + + it('extracts zTXt', async () => { + /** @type {PngCompressedTextualData} */ + let data; + + await getPngParser('tests/image-testfiles/ctzn0g04.png') + .onCompressedTextualData(evt => { data = evt.detail }) + .start(); + + expect(data.keyword).equals('Disclaimer'); + expect(data.compressionMethod).equals(0); + expect(data.compressedText.byteLength).equals(17); + + const blob = new Blob([data.compressedText.buffer]); + const decompressedStream = blob.stream().pipeThrough(new DecompressionStream('deflate')); + const decompressedText = await new Response(decompressedStream).text(); + expect(decompressedText).equals('Freeware.'); + }); + + it('extracts iTXt', async () => { + /** @type {PngIntlTextualData[]} */ + let data = []; + + await getPngParser('tests/image-testfiles/ctjn0g04.png') + .onIntlTextualData(evt => { data.push(evt.detail) }) + .start(); + + expect(data.length).equals(6); + expect(data[1].keyword).equals('Author'); + expect(data[1].compressionFlag).equals(0) + expect(data[5].keyword).equals('Disclaimer'); + // TODO: Test this better! + }); + + describe('bKGD', () => { + it('greyscale', async () => { + /** @type {PngBackgroundColor} */ + let bc; + await getPngParser('tests/image-testfiles/bggn4a16.png') + .onBackgroundColor(evt => { bc = evt.detail }) + .start(); + expect(bc.greyscale).equals(43908); + }); + + it('rgb', async () => { + /** @type {PngBackgroundColor} */ + let bc; + await getPngParser('tests/image-testfiles/tbrn2c08.png') + .onBackgroundColor(evt => { bc = evt.detail }) + .start(); + expect(bc.red).equals(255); + expect(bc.green).equals(0); + expect(bc.blue).equals(0); + }); + + it('paletteIndex', async () => { + /** @type {PngBackgroundColor} */ + let bc; + await getPngParser('tests/image-testfiles/tbbn3p08.png') + .onBackgroundColor(evt => { bc = evt.detail }) + .start(); + expect(bc.paletteIndex).equals(245); + }); + }); + + it('extracts tIME', async () => { + /** @type {PngLastModTime} */ + let lastModTime; + await getPngParser('tests/image-testfiles/cm9n0g04.png') + .onLastModTime(evt => { lastModTime = evt.detail }) + .start(); + expect(lastModTime.year).equals(1999); + expect(lastModTime.month).equals(12); + expect(lastModTime.day).equals(31); + expect(lastModTime.hour).equals(23); + expect(lastModTime.minute).equals(59); + expect(lastModTime.second).equals(59); + }); + + it('extracts pHYs', async () => { + /** @type {PngPhysicalPixelDimensions} */ + let pixelDims; + await getPngParser('tests/image-testfiles/cdun2c08.png') + .onPhysicalPixelDimensions(evt => { pixelDims = evt.detail }) + .start(); + expect(pixelDims.pixelPerUnitX).equals(1000); + expect(pixelDims.pixelPerUnitY).equals(1000); + expect(pixelDims.unitSpecifier).equals(PngUnitSpecifier.METRE); + }); + + it('extracts eXIf', async () => { + /** @type {PngPhysicalPixelDimensions} */ + let exif; + await getPngParser('tests/image-testfiles/exif2c08.png') + .onExifProfile(evt => { exif = evt.detail }) + .start(); + + const descVal = exif.get(ExifTagNumber.COPYRIGHT); + expect(descVal.dataFormat).equals(ExifDataFormat.ASCII_STRING); + expect(descVal.stringValue).equals('2017 Willem van Schaik'); + }); + + it('extracts hIST', async () => { + /** @type {PngPalette} */ + let palette; + /** @type {PngHistogram} */ + let hist; + await getPngParser('tests/image-testfiles/ch1n3p04.png') + .onHistogram(evt => { hist = evt.detail }) + .onPalette(evt => { palette = evt.detail }) + .start(); + + expect(hist.frequencies.length).equals(palette.entries.length); + expect(hist.frequencies[0]).equals(64); + expect(hist.frequencies[1]).equals(112); + }); + + it('extracts sPLT', async () => { + /** @type {PngSuggestedPalette} */ + let sPalette; + await getPngParser('tests/image-testfiles/ps1n0g08.png') + .onSuggestedPalette(evt => { sPalette = evt.detail }) + .start(); + + expect(sPalette.entries.length).equals(216); + expect(sPalette.paletteName).equals('six-cube'); + expect(sPalette.sampleDepth).equals(8); + + const entry0 = sPalette.entries[0]; + expect(entry0.red).equals(0); + expect(entry0.green).equals(0); + expect(entry0.blue).equals(0); + expect(entry0.alpha).equals(255); + expect(entry0.frequency).equals(0); + + const entry1 = sPalette.entries[1]; + expect(entry1.red).equals(0); + expect(entry1.green).equals(0); + expect(entry1.blue).equals(51); + expect(entry1.alpha).equals(255); + expect(entry1.frequency).equals(0); + }); +}); diff --git a/tests/image-testfiles/PngSuite.png b/tests/image-testfiles/PngSuite.png new file mode 100644 index 0000000..205460d Binary files /dev/null and b/tests/image-testfiles/PngSuite.png differ diff --git a/tests/image-testfiles/bggn4a16.png b/tests/image-testfiles/bggn4a16.png new file mode 100644 index 0000000..13fd85b Binary files /dev/null and b/tests/image-testfiles/bggn4a16.png differ diff --git a/tests/image-testfiles/ccwn2c08.png b/tests/image-testfiles/ccwn2c08.png new file mode 100644 index 0000000..47c2481 Binary files /dev/null and b/tests/image-testfiles/ccwn2c08.png differ diff --git a/tests/image-testfiles/cdun2c08.png b/tests/image-testfiles/cdun2c08.png new file mode 100644 index 0000000..846033b Binary files /dev/null and b/tests/image-testfiles/cdun2c08.png differ diff --git a/tests/image-testfiles/ch1n3p04.png b/tests/image-testfiles/ch1n3p04.png new file mode 100644 index 0000000..17cd12d Binary files /dev/null and b/tests/image-testfiles/ch1n3p04.png differ diff --git a/tests/image-testfiles/cm9n0g04.png b/tests/image-testfiles/cm9n0g04.png new file mode 100644 index 0000000..dd70911 Binary files /dev/null and b/tests/image-testfiles/cm9n0g04.png differ diff --git a/tests/image-testfiles/comment.gif b/tests/image-testfiles/comment.gif new file mode 100644 index 0000000..c7ea4d0 Binary files /dev/null and b/tests/image-testfiles/comment.gif differ diff --git a/tests/image-testfiles/cs3n2c16.png b/tests/image-testfiles/cs3n2c16.png new file mode 100644 index 0000000..bf5fd20 Binary files /dev/null and b/tests/image-testfiles/cs3n2c16.png differ diff --git a/tests/image-testfiles/ctjn0g04.png b/tests/image-testfiles/ctjn0g04.png new file mode 100644 index 0000000..a77b8d2 Binary files /dev/null and b/tests/image-testfiles/ctjn0g04.png differ diff --git a/tests/image-testfiles/ctzn0g04.png b/tests/image-testfiles/ctzn0g04.png new file mode 100644 index 0000000..b4401c9 Binary files /dev/null and b/tests/image-testfiles/ctzn0g04.png differ diff --git a/tests/image-testfiles/exif2c08.png b/tests/image-testfiles/exif2c08.png new file mode 100644 index 0000000..56eb734 Binary files /dev/null and b/tests/image-testfiles/exif2c08.png differ diff --git a/tests/image-testfiles/g05n3p04.png b/tests/image-testfiles/g05n3p04.png new file mode 100644 index 0000000..9619930 Binary files /dev/null and b/tests/image-testfiles/g05n3p04.png differ diff --git a/tests/image-testfiles/long_description.jpg b/tests/image-testfiles/long_description.jpg new file mode 100644 index 0000000..c5dfe67 Binary files /dev/null and b/tests/image-testfiles/long_description.jpg differ diff --git a/tests/image-testfiles/ps1n0g08.png b/tests/image-testfiles/ps1n0g08.png new file mode 100644 index 0000000..99625fa Binary files /dev/null and b/tests/image-testfiles/ps1n0g08.png differ diff --git a/tests/image-testfiles/tbbn3p08.png b/tests/image-testfiles/tbbn3p08.png new file mode 100644 index 0000000..0ede357 Binary files /dev/null and b/tests/image-testfiles/tbbn3p08.png differ diff --git a/tests/image-testfiles/tbgn2c16.png b/tests/image-testfiles/tbgn2c16.png new file mode 100644 index 0000000..85cec39 Binary files /dev/null and b/tests/image-testfiles/tbgn2c16.png differ diff --git a/tests/image-testfiles/tbrn2c08.png b/tests/image-testfiles/tbrn2c08.png new file mode 100644 index 0000000..5cca0d6 Binary files /dev/null and b/tests/image-testfiles/tbrn2c08.png differ diff --git a/tests/image-testfiles/xmp.gif b/tests/image-testfiles/xmp.gif new file mode 100644 index 0000000..b6d8411 Binary files /dev/null and b/tests/image-testfiles/xmp.gif differ diff --git a/tests/image-testfiles/xs1n0g01.png b/tests/image-testfiles/xs1n0g01.png new file mode 100644 index 0000000..1817c51 Binary files /dev/null and b/tests/image-testfiles/xs1n0g01.png differ diff --git a/tests/io-bitbuffer.spec.js b/tests/io-bitbuffer.spec.js index ba5c8fb..295e24f 100644 --- a/tests/io-bitbuffer.spec.js +++ b/tests/io-bitbuffer.spec.js @@ -11,18 +11,35 @@ import 'mocha'; import { expect } from 'chai'; describe('bitjs.io.BitBuffer', () => { + /** @type {BitBuffer} */ let buffer; + it('throws when invalid numBytes', () => { + expect(() => new BitBuffer()).throws(); + }); + describe('least-to-most-significant bit-packing', () => { beforeEach(() => { buffer = new BitBuffer(4); }); it('bit/byte pointers initialized properly', () => { + expect(buffer.getPackingDirection()).equals(false); expect(buffer.bytePtr).equals(0); expect(buffer.bitPtr).equals(0); - }) + }); + it('throws when writing invalid values', () => { + expect(() => buffer.writeBits(-3, 2)).throws(); + expect(() => buffer.writeBits(3, -2)).throws(); + expect(() => buffer.writeBits(0, 54)).throws(); + }); + + it('throws when writing too many bits into the buffer', () => { + buffer.writeBits(0, 31); // thirty-one zeroes. + expect(() => buffer.writeBits(1, 2)).throws(); + }); + it('write multiple bits', () => { buffer.writeBits(0b01011, 5); // Should result in: 0b00001011. expect(buffer.bytePtr).equals(0); @@ -47,6 +64,26 @@ describe('bitjs.io.BitBuffer', () => { expect(Array.from(buffer.data)).to.have.ordered.members( [0xfe, 0xff, 0x03, 0x00]); }); + + it('properly changes bit-packing direction', () => { + buffer.writeBits(3, 2); + expect(buffer.data[0]).equals(3); + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(2); + + buffer.setPackingDirection(true /** most to least significant */); + expect(buffer.bytePtr).equals(1); + expect(buffer.bitPtr).equals(7); + + buffer.writeBits(7, 3); + expect(buffer.data[0]).equals(3); + expect(buffer.data[1]).equals(224); + }); + + it('throws when switching packing direction and no more bytes left', () => { + buffer.writeBits(0, 25); + expect(() => buffer.setPackingDirection(true)).throws(); + }); }); describe('most-to-least-significant bit-packing', () => { @@ -55,6 +92,7 @@ describe('bitjs.io.BitBuffer', () => { }); it('bit/byte pointers initialized properly', () => { + expect(buffer.getPackingDirection()).equals(true); expect(buffer.bytePtr).equals(0); expect(buffer.bitPtr).equals(7); }) @@ -84,5 +122,25 @@ describe('bitjs.io.BitBuffer', () => { expect(Array.from(buffer.data)).to.have.ordered.members( [0x7f, 0xff, 0xc0, 0x00]); }); + + it('properly changes bit-packing direction', () => { + buffer.writeBits(3, 2); + expect(buffer.bytePtr).equals(0); + expect(buffer.bitPtr).equals(5); + expect(buffer.data[0]).equals(192); + + buffer.setPackingDirection(false /** least to most significant */); + expect(buffer.bytePtr).equals(1); + expect(buffer.bitPtr).equals(0); + + buffer.writeBits(7, 3); + expect(buffer.data[0]).equals(192); + expect(buffer.data[1]).equals(7); + }); + + it('throws when switching packing direction and no more bytes left', () => { + buffer.writeBits(0, 25); + expect(() => buffer.setPackingDirection(false)).throws(); + }); }); }); diff --git a/tests/io-bitstream.spec.js b/tests/io-bitstream.spec.js index d4f863d..c38a377 100644 --- a/tests/io-bitstream.spec.js +++ b/tests/io-bitstream.spec.js @@ -19,54 +19,152 @@ describe('bitjs.io.BitStream', () => { } }); - it('BitPeekAndRead_RTL', () => { - const stream = new BitStream(array.buffer, true /* mtl */); - // 0110 = 2 + 4 = 6 - expect(stream.readBits(4)).equals(6); - // 0101 011 = 1 + 2 + 8 + 32 = 43 - expect(stream.readBits(7)).equals(43); - // 00101 01100101 01 = 1 + 4 + 16 + 128 + 256 + 1024 + 4096 = 5525 - expect(stream.readBits(15)).equals(5525); - // 10010 = 2 + 16 = 18 - expect(stream.readBits(5)).equals(18); - - // Ensure the last bit is read, even if we flow past the end of the stream. - expect(stream.readBits(2)).equals(1); + it('throws an error without an ArrayBuffer', () => { + expect(() => new BitStream()).throws(); }); - it('BitPeekAndRead_LTR', () => { - const stream = new BitStream(array.buffer, false /* mtl */); - - // 0101 = 2 + 4 = 6 - expect(stream.peekBits(4)).equals(5); - expect(stream.readBits(4)).equals(5); - // 101 0110 = 2 + 4 + 16 + 64 = 86 - expect(stream.readBits(7)).equals(86); - // 01 01100101 01100 = 4 + 8 + 32 + 128 + 1024 + 2048 + 8192 = 11436 - expect(stream.readBits(15)).equals(11436); - // 11001 = 1 + 8 + 16 = 25 - expect(stream.readBits(5)).equals(25); - - // Only 1 bit left in the buffer, make sure it reads in, even if we over-read. - expect(stream.readBits(2)).equals(0); + describe('Most-to-Least', () => { + it('peek() and read()', () => { + const stream = new BitStream(array.buffer, true /** mtl */); + + expect(stream.peekBits(0)).equals(0); + expect(stream.peekBits(-1)).equals(0); + expect(stream.bytePtr).equals(0); + expect(stream.bitPtr).equals(0); + + // 0110 + expect(stream.readBits(4)).equals(0b0110); + expect(stream.getNumBitsRead()).equals(4); + + // 0101 011 + expect(stream.readBits(7)).equals(0b0101011); + // 00101 01100101 01 + expect(stream.readBits(15)).equals(0b001010110010101); + // 10010 + expect(stream.readBits(5)).equals(0b10010); + + // Ensure the last bit is read, even if we flow past the end of the stream. + expect(stream.readBits(2)).equals(1); + + expect(stream.getNumBitsRead()).equals(33); + }); + + it('skip() works correctly', () => { + const stream = new BitStream(array.buffer, true /** mtl */); + + expect(stream.skip(0)).equals(stream); + expect(stream.getNumBitsRead()).equals(0); + expect(stream.skip(3)).equals(stream); + expect(stream.getNumBitsRead()).equals(3); + expect(stream.readBits(3)).equals(0b001); + expect(stream.getNumBitsRead()).equals(6); + }); + + it('skip() works over byte boundary', () => { + const stream = new BitStream(array.buffer, true /** mtl */); + expect(stream.readBits(5)).equals(0b01100); + stream.skip(5); + expect(stream.getNumBitsRead()).equals(10); + expect(stream.readBits(5)).equals(0b10010); + }); + + it('skip() throws errors if overflowed', () => { + const stream = new BitStream(array.buffer, true /** mtl */); + expect(() => stream.skip(-1)).throws(); + stream.readBits(30); + expect(() => stream.skip(3)).throws(); + }); }); - it('BitStreamReadBytes', () => { - array[1] = Number('0b01010110'); - const stream = new BitStream(array.buffer); + describe('Least-to-Most', () => { + it('peek() and read()', () => { + /** @type {BitStream} */ + const stream = new BitStream(array.buffer, false /** mtl */); + + expect(stream.peekBits(0)).equals(0); + expect(stream.peekBits(-1)).equals(0); + expect(stream.bytePtr).equals(0); + expect(stream.bitPtr).equals(0); + + // 0101 + expect(stream.peekBits(4)).equals(0b0101); + expect(stream.readBits(4)).equals(0b0101); + // 101 0110 + expect(stream.readBits(7)).equals(0b1010110); + // 01 01100101 01100 + expect(stream.readBits(15)).equals(0b010110010101100); + // 11001 + expect(stream.readBits(5)).equals(0b11001); + + // Only 1 bit left in the buffer, make sure it reads in, even if we over-read. + expect(stream.readBits(2)).equals(0); + }); + + it('skip() works correctly', () => { + const stream = new BitStream(array.buffer, false /** mtl */); + + expect(stream.skip(0)).equals(stream); + expect(stream.getNumBitsRead()).equals(0); + expect(stream.skip(3)).equals(stream); + expect(stream.getNumBitsRead()).equals(3); + expect(stream.readBits(3)).equals(0b100); + expect(stream.getNumBitsRead()).equals(6); + }); + + it('skip() works over byte boundary', () => { + const stream = new BitStream(array.buffer, false /** mtl */); + expect(stream.readBits(5)).equals(0b00101); + stream.skip(5); + expect(stream.getNumBitsRead()).equals(10); + expect(stream.readBits(5)).equals(0b11001); + }); + + it('skip() throws errors if overflowed', () => { + const stream = new BitStream(array.buffer, false /** mtl */); + expect(() => stream.skip(-1)).throws(); + stream.readBits(30); + expect(() => stream.skip(3)).throws(); + }); + }); + + describe('bytes', () => { + it('peekBytes() and readBytes()', () => { + array[1] = Number('0b01010110'); + const stream = new BitStream(array.buffer); + + let twoBytes = stream.peekBytes(2); + expect(twoBytes instanceof Uint8Array).true; + expect(twoBytes.byteLength).equals(2); + expect(twoBytes[0]).equals(Number('0b01100101')); + expect(twoBytes[1]).equals(Number('0b01010110')); + + twoBytes = stream.readBytes(2); + expect(twoBytes instanceof Uint8Array).true; + expect(twoBytes.byteLength).equals(2); + expect(twoBytes[0]).equals(Number('0b01100101')); + expect(twoBytes[1]).equals(Number('0b01010110')); + + expect(() => stream.readBytes(3)).throws(); + }); - let twoBytes = stream.peekBytes(2); - expect(twoBytes instanceof Uint8Array).true; - expect(twoBytes.byteLength).equals(2); - expect(twoBytes[0]).equals(Number('0b01100101')); - expect(twoBytes[1]).equals(Number('0b01010110')); + it('peekBytes(0) returns an empty array', () => { + const stream = new BitStream(array.buffer); + const arr = stream.peekBytes(0); + expect(arr.byteLength).equals(0); + }); - twoBytes = stream.readBytes(2); - expect(twoBytes instanceof Uint8Array).true; - expect(twoBytes.byteLength).equals(2); - expect(twoBytes[0]).equals(Number('0b01100101')); - expect(twoBytes[1]).equals(Number('0b01010110')); + it('peekBytes() should ignore bits until byte-aligned', () => { + array[1] = Number('0b01010110'); + const stream = new BitStream(array.buffer); + stream.skip(3); + const bytes = stream.readBytes(2); + expect(bytes[0]).equals(0b01010110); + expect(bytes[1]).equals(0b01100101); + }) - expect(() => stream.readBytes(3)).throws(); + it('throws an error with weird values of peekBytes()', () => { + const stream = new BitStream(array.buffer); + expect(() => stream.peekBytes(-1)).throws(); + }); }); }); diff --git a/tests/io-bytebuffer.spec.js b/tests/io-bytebuffer.spec.js index 54355d9..cee2379 100644 --- a/tests/io-bytebuffer.spec.js +++ b/tests/io-bytebuffer.spec.js @@ -7,31 +7,66 @@ */ import { ByteBuffer } from '../io/bytebuffer.js'; -import { ByteStream } from '../io/bytestream.js'; import 'mocha'; import { expect } from 'chai'; -// TODO: Only test ByteBuffer here. describe('bitjs.io.ByteBuffer', () => { + /** @type {ByteBuffer} */ let buffer; beforeEach(() => { buffer = new ByteBuffer(4); }); - it('Write_SingleByte', () => { + describe('getData()', () => { + it('returns an empty array when nothing has been written', () => { + expect(buffer.getData().byteLength).equals(0); + }); + + it('is sized correctly', () => { + buffer.insertByte(42); + buffer.insertByte(81); + const data = buffer.getData(); + expect(data.byteLength).equals(2); + expect(data[0]).equals(42); + expect(data[1]).equals(81); + }); + }); + + it('throws when initialized incorrectly', () => { + expect(() => new ByteBuffer()).throws(); + }); + + describe('Buffer overflow', () => { + it('insertByte() throws when buffer exceeded', () => { + buffer.insertBytes([0, 2, 4, 6]); + expect(() => buffer.insertByte(1)).throws(); + }); + it('insertBytes() throws when buffer exceeded', () => { + expect(() => buffer.insertBytes([0, 2, 4, 6, 8])).throws(); + }); + }); + + it('insertByte()', () => { + buffer.insertByte(192); + expect(buffer.ptr).equals(1); + expect(buffer.data[0]).equals(192); + }); + + it('writeNumber() with a single unsigned byte', () => { buffer.writeNumber(192, 1); expect(buffer.ptr).equals(1); + expect(buffer.data[0]).equals(192); }); - it('Write_SingleByteNegativeNumber', () => { + it('writeNumber() with a single negative number', () => { buffer.writeSignedNumber(-120, 1); expect(buffer.ptr).equals(1); + expect(buffer.data[0]).equals(-120 & 0xff); }); it('Write_MultiByteNumber', () => { buffer.writeNumber(1234, 4); - const stream = new ByteStream(buffer.data.buffer); expect(buffer.ptr).equals(4); }); @@ -51,4 +86,21 @@ describe('bitjs.io.ByteBuffer', () => { it('WriteOverflowSignedNegative', () => { expect(() => buffer.writeSignedNumber(-129, 1)).throws(); }); + + it('throws when trying to write invalid # of bytes', () => { + expect(() => buffer.writeNumber(3, -1)).throws(); + expect(() => buffer.writeNumber(-3, 1)).throws(); + expect(() => buffer.writeSignedNumber(-3, -1)).throws(); + }); + + it('writes an ASCII string', () => { + buffer.writeASCIIString('hi'); + expect(buffer.ptr).equals(2); + expect(buffer.data[0]).equals('h'.charCodeAt(0)); + expect(buffer.data[1]).equals('i'.charCodeAt(0)); + }); + + it('throws in a non-ASCII string', () => { + expect(() => buffer.writeASCIIString('Björk')).throws('Trying to write a non-ASCII string'); + }); }); \ No newline at end of file diff --git a/tests/io-bytestream.spec.js b/tests/io-bytestream.spec.js index 7d70af6..740e771 100644 --- a/tests/io-bytestream.spec.js +++ b/tests/io-bytestream.spec.js @@ -17,6 +17,35 @@ describe('bitjs.io.ByteStream', () => { array = new Uint8Array(4); }); + it('throws an error without an ArrayBuffer', () => { + expect(() => new ByteStream()).throws(); + expect(() => new ByteStream(array.buffer).push()).throws(); + }); + + it('getNumBytesRead() works', () => { + const stream = new ByteStream(array.buffer); + expect(stream.getNumBytesRead()).equals(0); + stream.readBytes(1); + expect(stream.getNumBytesRead()).equals(1); + stream.readBytes(2); + expect(stream.getNumBytesRead()).equals(3); + }); + + it('throws when peeking a weird numbers of bytes', () => { + array[0] = 255; + const stream = new ByteStream(array.buffer); + expect(stream.peekNumber(0)).equals(0); + expect(() => stream.peekNumber(-2)).throws(); + expect(() => stream.peekNumber(5)).throws(); + + expect(stream.peekBytes(0).length).equals(0); + expect(() => stream.peekBytes(-1)).throws(); + + expect(stream.peekString(0)).equals(''); + expect(() => stream.peekString(-1)).throws(); + expect(() => stream.peekString(5)).throws(); + }); + it('PeekAndRead_SingleByte', () => { array[0] = 192; const stream = new ByteStream(array.buffer); @@ -33,6 +62,42 @@ describe('bitjs.io.ByteStream', () => { expect(() => stream.readNumber(1)).to.throw(); }); + it('PeekAndRead_MultiByteNumber_BigEndian', () => { + array[3] = (1234 & 0xff); + array[2] = ((1234 >> 8) & 0xff); + const stream = new ByteStream(array.buffer); + stream.setBigEndian(); + expect(stream.peekNumber(4)).equals(1234); + expect(stream.readNumber(4)).equals(1234); + expect(() => stream.readNumber(1)).to.throw(); + }); + + it('PeekAndRead_MultiByteNumber_MultiEndian', () => { + array[1] = 1; + array[3] = 1; + // Stream now has 0, 1, 0, 1. + const stream = new ByteStream(array.buffer); + stream.setBigEndian(); + expect(stream.peekNumber(2)).equals(1); + stream.setBigEndian(false); + expect(stream.peekNumber(2)).equals(256); + stream.setBigEndian(true); + expect(stream.peekNumber(2)).equals(1); + + stream.skip(2); + + stream.setLittleEndian(); + expect(stream.peekNumber(2)).equals(256); + stream.setLittleEndian(false); + expect(stream.peekNumber(2)).equals(1); + stream.setLittleEndian(true); + expect(stream.peekNumber(2)).equals(256); + + stream.skip(2); + + expect(() => stream.readNumber(1)).to.throw(); + }); + it('PeekAndRead_SingleByteSignedNumber', () => { array[0] = -120; const stream = new ByteStream(array.buffer); @@ -149,19 +214,77 @@ describe('bitjs.io.ByteStream', () => { const str = stream.readString(12); expect(str).equals('ABCDEFGHIJKL'); + + const str2 = stream.readString(0); + expect(str2).equals(''); + }); + + describe('skip()', () => { + /** @type {ByteStream} */ + let stream; + + beforeEach(() => { + for (let i = 0; i < 4; ++i) array[i] = i; + stream = new ByteStream(array.buffer); + }); + + it('skips bytes', () => { + stream.skip(2); + expect(stream.getNumBytesRead()).equals(2); + expect(stream.getNumBytesLeft()).equals(2); + expect(stream.readNumber(1)).equals(2); + expect(stream.readNumber(1)).equals(3); + }); + + it('returns itself', () => { + const retVal = stream.skip(2); + expect(stream === retVal).equals(true); + }); + + it('skip(0) has no effect', () => { + const retVal = stream.skip(0); + expect(stream.getNumBytesRead()).equals(0); + expect(stream.getNumBytesLeft()).equals(4); + expect(stream.readNumber(1)).equals(0); + expect(retVal === stream).equals(true); + }); + + it('skip() with negative throws an error', () => { + expect(() => stream.skip(-1)).throws(); + expect(stream.getNumBytesLeft()).equals(4); + }); + + it('skip() past the end throws an error', () => { + expect(() => stream.skip(4)).does.not.throw(); + expect(stream.getNumBytesLeft()).equals(0); + expect(() => stream.skip(1)).throws(); + }); + + it('skip() works correct across pages of bytes', () => { + const extraArr = new Uint8Array(4); + for (let i = 0; i < 4; ++i) extraArr[i] = i + 4; + stream.push(extraArr.buffer); + expect(stream.readNumber(1)).equals(0); + stream.skip(5); + expect(stream.readNumber(1)).equals(6); + }); }); it('Tee', () => { for (let i = 0; i < 4; ++i) array[i] = 65 + i; const stream = new ByteStream(array.buffer); + // Set non-default endianness. + stream.setBigEndian(true); const anotherArray = new Uint8Array(4); for (let i = 0; i < 4; ++i) anotherArray[i] = 69 + i; stream.push(anotherArray.buffer); const teed = stream.tee(); + expect(teed !== stream).equals(true); teed.readBytes(5); expect(stream.getNumBytesLeft()).equals(8); expect(teed.getNumBytesLeft()).equals(3); + expect(teed.isLittleEndian()).equals(stream.isLittleEndian()); }); }); diff --git a/tests/muther.js b/tests/muther.js deleted file mode 100644 index 74662dc..0000000 --- a/tests/muther.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Minimal Unit Test Harness - * - * Licensed under the MIT License - * - * Copyright(c) 2014, Google Inc. - */ -function setOrCreate(id, style, innerHTML) { - let el = document.querySelector('#' + id); - if (!el) { - el = document.createElement('div'); - el.id = id; - document.body.appendChild(el); - } - el.setAttribute('style', style); - el.innerHTML = innerHTML; -} -export function assert(cond, err) { if (!cond) { throw err || 'Undefined error'; } } -export function assertEquals(a, b, err) { assert(a === b, err || (a + '!=' + b)); } -export function assertThrows(fn, err) { - let threw = false; - try { fn(); } catch (e) { threw = true; } - assert(threw, err || 'Code did not throw'); -} -export function runTests(spec) { - let prevResult = Promise.resolve(true); - for (let testName in spec['tests']) { - setOrCreate(testName, 'color:#F90', 'RUNNING: ' + testName); - try { - prevResult = prevResult.then(() => { - if (spec['setUp']) spec['setUp'](); - const thisResult = spec['tests'][testName]() || Promise.resolve(true); - return thisResult.then(() => { - if (spec['tearDown']) spec['tearDown'](); - setOrCreate(testName, 'color:#090', 'PASS: ' + testName); - }); - }).catch(err => setOrCreate(testName, 'color:#900', 'FAIL: ' + testName + ': ' + err)); - } catch (err) { - setOrCreate(testName, 'color:#900', 'FAIL: ' + testName + ': ' + err); - } - } -} diff --git a/tests/test-uploader.html b/tests/test-uploader.html deleted file mode 100644 index 5fe470e..0000000 --- a/tests/test-uploader.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - -
- - Select archived file -
-
- - Select unarchived file -
-
-
- - diff --git a/tests/test-uploader.js b/tests/test-uploader.js deleted file mode 100644 index f1b5c2f..0000000 --- a/tests/test-uploader.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * test-uploader.js - * - * Provides readers for byte streams. - * - * Licensed under the MIT License - * - * Copyright(c) 2017 Google Inc. - */ - -/** - * TODO: - * - ask user to choose the archived binary file - * - read it in as bytes, convert to text - * - ask user to choose the unarchived file - * - put the binary and text results together in a JSON blob: - { - "archivedFile": ..., - "unarchivedFIle": ... - } - */ - -let archiveUploaderEl = null; -let archivedFileAsText = null; -let unarchiveUploaderEl = null; -let unarchivedFileAsText = null; - -function init() { - archiveUploaderEl = document.querySelector('#archive-uploader'); - unarchiveUploaderEl = document.querySelector('#unarchive-uploader'); - - archiveUploaderEl.addEventListener('change', getArchivedFile, false); - unarchiveUploaderEl.addEventListener('change', getUnarchivedFile, false); -} - -function getArchivedFile(evt) { - const filelist = evt.target.files; - const fr = new FileReader(); - fr.onload = function () { - const arr = new Uint8Array(fr.result); - archivedFileAsText = btoa(arr); - archiveUploaderEl.setAttribute('disabled', 'true'); - unarchiveUploaderEl.removeAttribute('disabled'); - }; - fr.readAsArrayBuffer(filelist[0]); -} - -function getUnarchivedFile(evt) { - const filelist = evt.target.files; - const fr = new FileReader(); - fr.onload = function () { - const arr = new Uint8Array(fr.result); - unarchivedFileAsText = btoa(arr); - unarchiveUploaderEl.setAttribute('disabled', 'true'); - output(); - }; - fr.readAsArrayBuffer(filelist[0]); -} - -function output() { - let json = 'window.archiveTestFile = {\n'; - json += ' "archivedFile": "' + archivedFileAsText + '",\n'; - json += ' "unarchivedFile": "' + unarchivedFileAsText + '"\n'; - json += '}'; - document.getElementById('json').textContent = json; -} - -// To turn the base64 string back into bytes: -// new Uint8Array(atob(archivedFileAsText).split(',').map(s => parseInt(s))) diff --git a/tests/unzipper-test.html b/tests/unzipper-test.html deleted file mode 100644 index 5bc3983..0000000 --- a/tests/unzipper-test.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Tiny web inteface to test performance of the Unzipper. - - - -
- - Select a bunch of zip files -
-
- - \ No newline at end of file diff --git a/tests/unzipper-test.js b/tests/unzipper-test.js deleted file mode 100644 index 746bd07..0000000 --- a/tests/unzipper-test.js +++ /dev/null @@ -1,44 +0,0 @@ - -import { UnarchiveEventType, Unzipper } from '../archive/archive.js'; - -const result = document.querySelector('#result'); -const fileInputEl = document.querySelector('#unzip-tester'); - -async function getFiles(fileChangeEvt) { - result.innerHTML = `Starting to load files`; - const files = fileChangeEvt.target.files; - const buffers = []; - for (const file of files) { - buffers.push(await new Promise((resolve, reject) => { - const fr = new FileReader(); - fr.onload = () => { - resolve(new Uint8Array(fr.result)); - }; - fr.readAsArrayBuffer(file); - })); - } - - result.innerHTML = `Loaded files`; - - let fileNum = 0; - const INC = 100 / files.length; - const start = performance.now(); - - for (const b of buffers) { - await new Promise((resolve, reject) => { - const unzipper = new Unzipper(b.buffer, { pathToBitJS: '../' }); - unzipper.addEventListener(UnarchiveEventType.FINISH, () => { - fileNum++; - resolve(); - }); - result.innerHTML = `Unzipping file ${fileNum} / ${files.length}`; - unzipper.start(); - }); - } - - const end = performance.now(); - result.innerHTML = `Unzipping took ${end - start}ms`; -} - -fileInputEl.addEventListener('change', getFiles, false); - diff --git a/tests/zipper-test.html b/tests/zipper-test.html deleted file mode 100644 index 12ac530..0000000 --- a/tests/zipper-test.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - Tiny web inteface to test the Zipper. - - - -
- - Select a bunch of files to zip -
- -
- - \ No newline at end of file diff --git a/tests/zipper-test.js b/tests/zipper-test.js deleted file mode 100644 index e0437ff..0000000 --- a/tests/zipper-test.js +++ /dev/null @@ -1,67 +0,0 @@ - -import { Zipper, ZipCompressionMethod } from '../archive/compress.js'; - -const result = document.querySelector('#result'); -const fileInputEl = document.querySelector('#zip-tester'); -const saveButtonEl = document.querySelector('#save'); -let byteArray = null; - -/** - * @typedef FileInfo An object that is sent to this worker 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 bytes of the file. - */ - -/** - * @returns {Promise<} - */ -async function getFiles(fileChangeEvt) { - result.innerHTML = `Starting to load files`; - const files = fileChangeEvt.target.files; - const fileInfos = []; - for (const file of files) { - fileInfos.push(await new Promise((resolve, reject) => { - const fr = new FileReader(); - fr.onload = () => { - resolve({ - fileName: file.name, - lastModTime: file.lastModified, - fileData: new Uint8Array(fr.result), - }); - }; - fr.readAsArrayBuffer(file); - })); - } - - result.innerHTML = `Loaded files`; - - const zipper = new Zipper({ - pathToBitJS: '../', - zipCompressionMethod: ZipCompressionMethod.DEFLATE, - }); - byteArray = await zipper.start(fileInfos, true); - result.innerHTML = `Zipping done`; - saveButtonEl.style.display = ''; -} - -async function saveFile(evt) { - /** @type {FileSystemFileHandle} */ - const fileHandle = await window['showSaveFilePicker']({ - types: [ - { - accept: { - 'application/zip': ['.zip', '.cbz'], - }, - }, - ], - }); - - /** @type {FileSystemWritableFileStream} */ - const writableStream = await fileHandle.createWritable(); - writableStream.write(byteArray); - writableStream.close(); -} - -fileInputEl.addEventListener('change', getFiles, false); -saveButtonEl.addEventListener('click', saveFile, false); diff --git a/types/archive/common.d.ts b/types/archive/common.d.ts new file mode 100644 index 0000000..531b4f7 --- /dev/null +++ b/types/archive/common.d.ts @@ -0,0 +1,73 @@ +/** + * common.js + * + * Provides common definitions or functionality needed by multiple modules. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ +/** + * @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 + * @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 + * (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it + * 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'). + * @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 function getConnectedPort(implFilename: string): Promise; +export const LOCAL_FILE_HEADER_SIG: 67324752; +export const CENTRAL_FILE_HEADER_SIG: 33639248; +export const END_OF_CENTRAL_DIR_SIG: 101010256; +export const CRC32_MAGIC_NUMBER: 3988292384; +export const ARCHIVE_EXTRA_DATA_SIG: 134630224; +export const DIGITAL_SIGNATURE_SIG: 84233040; +export const END_OF_CENTRAL_DIR_LOCATOR_SIG: 117853008; +export const DATA_DESCRIPTOR_SIG: 134695760; +export type ZipCompressionMethod = number; +export namespace ZipCompressionMethod { + const STORE: number; + const DEFLATE: number; +} +/** + * An object that is sent to the implementation representing a file to compress. + */ +export type FileInfo = { + /** + * The name of the file. TODO: Includes the path? + */ + fileName: string; + /** + * The number of ms since the Unix epoch (1970-01-01 at midnight). + */ + lastModTime: number; + /** + * The bytes of the file. + */ + fileData: Uint8Array; +}; +export type Implementation = { + /** + * The port the host uses to communicate with the implementation. + */ + hostPort: MessagePort; + /** + * A function to call when the port has been disconnected. + */ + disconnectFn: Function; +}; +//# sourceMappingURL=common.d.ts.map \ No newline at end of file diff --git a/types/archive/common.d.ts.map b/types/archive/common.d.ts.map new file mode 100644 index 0000000..802e73b --- /dev/null +++ b/types/archive/common.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../archive/common.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AAEH;;;;;;;;;;GAUG;AACH,+CANW,MAAM,GAGJ,QAAQ,cAAc,CAAC,CA0BnC;AAID,6CAAgD;AAChD,+CAAkD;AAClD,+CAAiD;AACjD,4CAA6C;AAC7C,+CAAiD;AACjD,6CAAgD;AAChD,uDAAyD;AACzD,4CAA8C;mCAIpC,MAAM;;;;;;;;;;;;cA5DF,MAAM;;;;iBACN,MAAM;;;;cACN,UAAU;;;;;;cAKV,WAAW"} \ No newline at end of file diff --git a/types/archive/decompress-internal.d.ts b/types/archive/decompress-internal.d.ts deleted file mode 100644 index afa8a95..0000000 --- a/types/archive/decompress-internal.d.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Factory method that creates an unarchiver based on the byte signature found - * in the arrayBuffer. - * @param {ArrayBuffer} ab - * @param {Function(string):Worker} createWorkerFn A function that creates a Worker from a script file. - * @param {Object|string} options An optional object of options, or a string representing where - * the path to the unarchiver script files. - * @returns {Unarchiver} - */ -export function getUnarchiverInternal(ab: ArrayBuffer, createWorkerFn: any, options?: any | string): Unarchiver; -export namespace UnarchiveEventType { - const START: string; - const APPEND: string; - const PROGRESS: string; - const EXTRACT: string; - const FINISH: string; - const INFO: string; - const ERROR: string; -} -/** - * An unarchive event. - */ -export class UnarchiveEvent extends Event { - /** - * @param {string} type The event type. - */ - constructor(type: string); -} -/** - * Updates all Archiver listeners that an append has occurred. - */ -export class UnarchiveAppendEvent extends UnarchiveEvent { - /** - * @param {number} numBytes The number of bytes appended. - */ - constructor(numBytes: number); - /** - * The number of appended bytes. - * @type {number} - */ - numBytes: number; -} -/** - * Useful for passing info up to the client (for debugging). - */ -export class UnarchiveInfoEvent extends UnarchiveEvent { - /** - * The information message. - * @type {string} - */ - msg: string; -} -/** - * An unrecoverable error has occured. - */ -export class UnarchiveErrorEvent extends UnarchiveEvent { - /** - * The information message. - * @type {string} - */ - msg: string; -} -/** - * Start event. - */ -export class UnarchiveStartEvent extends UnarchiveEvent { - constructor(); -} -/** - * Finish event. - */ -export class UnarchiveFinishEvent extends UnarchiveEvent { - /** - * @param {Object} metadata A collection fo metadata about the archive file. - */ - constructor(metadata?: any); - metadata: any; -} -/** - * Progress event. - */ -export class UnarchiveProgressEvent extends UnarchiveEvent { - /** - * @param {string} currentFilename - * @param {number} currentFileNumber - * @param {number} currentBytesUnarchivedInFile - * @param {number} currentBytesUnarchived - * @param {number} totalUncompressedBytesInArchive - * @param {number} totalFilesInArchive - * @param {number} totalCompressedBytesRead - */ - constructor(currentFilename: string, currentFileNumber: number, currentBytesUnarchivedInFile: number, currentBytesUnarchived: number, totalUncompressedBytesInArchive: number, totalFilesInArchive: number, totalCompressedBytesRead: number); - currentFilename: string; - currentFileNumber: number; - currentBytesUnarchivedInFile: number; - totalFilesInArchive: number; - currentBytesUnarchived: number; - totalUncompressedBytesInArchive: number; - totalCompressedBytesRead: number; -} -/** - * Extract event. - */ -export class UnarchiveExtractEvent extends UnarchiveEvent { - /** - * @param {UnarchivedFile} unarchivedFile - */ - constructor(unarchivedFile: UnarchivedFile); - /** - * @type {UnarchivedFile} - */ - unarchivedFile: UnarchivedFile; -} -/** - * Base class for all Unarchivers. - */ -export class Unarchiver extends EventTarget { - /** - * @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 - * to the Worker. - * @param {Function(string):Worker} createWorkerFn A function that creates a Worker from a script file. - * @param {Object|string} options An optional object of options, or a string representing where - * the BitJS files are located. The string version of this argument is deprecated. - * Available options: - * 'pathToBitJS': A string indicating where the BitJS files are located. - * 'debug': A boolean where true indicates that the archivers should log debug output. - */ - constructor(arrayBuffer: ArrayBuffer, createWorkerFn: any, options?: any | string); - /** - * The ArrayBuffer object. - * @type {ArrayBuffer} - * @protected - */ - protected ab: ArrayBuffer; - /** - * A factory method that creates a Worker that does the unarchive work. - * @type {Function(string): Worker} - * @private - */ - private createWorkerFn_; - /** - * The path to the BitJS files. - * @type {string} - * @private - */ - private pathToBitJS_; - /** - * @orivate - * @type {boolean} - */ - debugMode_: boolean; - /** - * Private web worker initialized during start(). - * @private - * @type {Worker} - */ - private worker_; - /** - * This method must be overridden by the subclass to return the script filename. - * @returns {string} The MIME type of the archive. - * @protected. - */ - protected getMIMEType(): string; - /** - * This method must be overridden by the subclass to return the script filename. - * @returns {string} The script filename. - * @protected. - */ - protected getScriptFileName(): string; - /** - * Create an UnarchiveEvent out of the object sent back from the Worker. - * @param {Object} obj - * @returns {UnarchiveEvent} - * @private - */ - private createUnarchiveEvent_; - /** - * Receive an event and pass it to the listener functions. - * - * @param {Object} obj - * @private - */ - private handleWorkerEvent_; - /** - * Starts the unarchive in a separate Web Worker thread and returns immediately. - */ - start(): void; - /** - * Adds more bytes to the unarchiver's Worker thread. - * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is - * set to true, this ArrayBuffer must not be referenced after calling update(), since it - * is marked as Transferable and sent to the Worker. - * @param {boolean=} opt_transferable Optional boolean whether to mark this ArrayBuffer - * as a Tranferable object, which means it can no longer be referenced outside of - * the Worker thread. - */ - update(ab: ArrayBuffer, opt_transferable?: boolean | undefined): void; - /** - * Terminates the Web Worker for this Unarchiver and returns immediately. - */ - stop(): void; -} -export class UnzipperInternal extends Unarchiver { - constructor(arrayBuffer: any, createWorkerFn: any, options: any); -} -export class UnrarrerInternal extends Unarchiver { - constructor(arrayBuffer: any, createWorkerFn: any, options: any); -} -export class UntarrerInternal extends Unarchiver { - constructor(arrayBuffer: any, createWorkerFn: any, options: any); -} -export type UnarchivedFile = { - filename: string; - fileData: Uint8Array; -}; -//# sourceMappingURL=decompress-internal.d.ts.map \ No newline at end of file diff --git a/types/archive/decompress-internal.d.ts.map b/types/archive/decompress-internal.d.ts.map deleted file mode 100644 index 01c7ad3..0000000 --- a/types/archive/decompress-internal.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"decompress-internal.d.ts","sourceRoot":"","sources":["../../archive/decompress-internal.js"],"names":[],"mappings":"AAiYA;;;;;;;;GAQG;AACF,0CANU,WAAW,iCAEX,MAAO,MAAM,GAEX,UAAU,CAkBtB;;;;;;;;;;AA1XD;;GAEG;AACF;IACC;;OAEG;IACH,kBAFW,MAAM,EAIhB;CACF;AAED;;GAEG;AACF;IACC;;OAEG;IACH,sBAFW,MAAM,EAUhB;IALC;;;OAGG;IACH,UAFU,MAAM,CAEQ;CAE3B;AAED;;GAEG;AACH;IAOI;;;OAGG;IACH,KAFU,MAAM,CAEF;CAEjB;AAED;;GAEG;AACH;IAOI;;;OAGG;IACH,KAFU,MAAM,CAEF;CAEjB;AAED;;GAEG;AACH;IACE,cAEC;CACF;AAED;;GAEG;AACH;IACE;;OAEG;IACH,4BAGC;IADC,cAAwB;CAE3B;AAED;;GAEG;AACH;IACE;;;;;;;;OAQG;IACH,6BARW,MAAM,qBACN,MAAM,gCACN,MAAM,0BACN,MAAM,mCACN,MAAM,uBACN,MAAM,4BACN,MAAM,EAchB;IAPC,wBAAsC;IACtC,0BAA0C;IAC1C,qCAAgE;IAChE,4BAA8C;IAC9C,+BAAoD;IACpD,wCAAsE;IACtE,iCAAwD;CAE3D;AAED;;GAEG;AACH;IACE;;OAEG;IACH,4BAFW,cAAc,EASxB;IAJC;;OAEG;IACH,gBAFU,cAAc,CAEY;CAEvC;AAED;;GAEG;AACF;IACC;;;;;;;;;;OAUG;IACH,yBAVW,WAAW,iCAIX,MAAO,MAAM,EAgDvB;IAjCC;;;;OAIG;IACH,cAHU,WAAW,CAGA;IAErB;;;;OAIG;IACH,wBAAqC;IAErC;;;;OAIG;IACH,qBAA8C;IAE9C;;;OAGG;IACH,YAFU,OAAO,CAEkB;IAEnC;;;;OAIG;IACH,gBAAmB;IAGrB;;;;OAIG;IACH,yBAHa,MAAM,CAKlB;IAED;;;;OAIG;IACH,+BAHa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,8BAsBC;IAED;;;;;OAKG;IACH,2BAWC;IAED;;OAEG;IACH,cA2BC;IAID;;;;;;;;OAQG;IACH,WAPW,WAAW,qBAGX,OAAO,oBAgBjB;IAED;;OAEG;IACH,aAIC;CACF;AAED;IACE,iEAEC;CAIF;AAED;IACE,iEAEC;CAIF;AAED;IACE,iEAEC;CAIF;;cAhXa,MAAM;cACN,UAAU"} \ No newline at end of file diff --git a/types/archive/decompress.d.ts b/types/archive/decompress.d.ts new file mode 100644 index 0000000..4059430 --- /dev/null +++ b/types/archive/decompress.d.ts @@ -0,0 +1,197 @@ +/** + * Factory method that creates an unarchiver based on the byte signature found + * in the ArrayBuffer. + * @param {ArrayBuffer} ab The ArrayBuffer to unarchive. Note that this ArrayBuffer + * must not be referenced after calling this method, as the ArrayBuffer may be + * transferred to a different JS context once start() is called. + * @param {UnarchiverOptions|string} options An optional object of options, or a + * string representing where the path to the unarchiver script files. The latter + * is now deprecated (use UnarchiverOptions). + * @returns {Unarchiver} + */ +export function getUnarchiver(ab: ArrayBuffer, options?: UnarchiverOptions | string): Unarchiver; +/** + * All extracted files returned by an Unarchiver will implement + * the following interface: + * TODO: Move this interface into common.js? + */ +/** + * @typedef UnarchivedFile + * @property {string} filename + * @property {Uint8Array} fileData + */ +/** + * @typedef UnarchiverOptions + * @property {boolean=} debug Set to true for verbose unarchiver logging. + */ +/** + * Base class for all Unarchivers. + */ +export class Unarchiver extends EventTarget { + /** + * @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 + * to the decompress implementation. + * @param {UnarchiverOptions|string} options An optional object of options, or a string + * representing where the BitJS files are located. The string version of this argument is + * deprecated. + */ + constructor(arrayBuffer: ArrayBuffer, options?: UnarchiverOptions | string); + /** + * The client-side port that sends messages to, and receives messages from, the + * decompressor implementation. + * @type {MessagePort} + * @private + */ + private port_; + /** + * A function to call to disconnect the implementation from the host. + * @type {Function} + * @private + */ + private disconnectFn_; + /** + * The ArrayBuffer object. + * @type {ArrayBuffer} + * @protected + */ + protected ab: ArrayBuffer; + /** + * @orivate + * @type {boolean} + */ + debugMode_: boolean; + /** + * Overridden so that the type hints for eventType are specific. Prefer onExtract(), etc. + * @param {'progress'|'extract'|'finish'} eventType + * @param {EventListenerOrEventListenerObject} listener + * @override + */ + override addEventListener(eventType: 'progress' | 'extract' | 'finish', listener: EventListenerOrEventListenerObject): void; + /** + * Type-safe way to subscribe to an UnarchiveExtractEvent. + * @param {function(UnarchiveExtractEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onExtract(listener: (arg0: UnarchiveExtractEvent) => any): Unarchiver; + /** + * Type-safe way to subscribe to an UnarchiveFinishEvent. + * @param {function(UnarchiveFinishEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onFinish(listener: (arg0: UnarchiveFinishEvent) => any): Unarchiver; + /** + * Type-safe way to subscribe to an UnarchiveProgressEvent. + * @param {function(UnarchiveProgressEvent)} listener + * @returns {Unarchiver} for chaining. + */ + onProgress(listener: (arg0: UnarchiveProgressEvent) => any): Unarchiver; + /** + * This method must be overridden by the subclass to return the script filename. + * @returns {string} The MIME type of the archive. + * @protected. + */ + protected getMIMEType(): string; + /** + * This method must be overridden by the subclass to return the script filename. + * @returns {string} The script filename. + * @protected. + */ + protected getScriptFileName(): string; + /** + * Create an UnarchiveEvent out of the object sent back from the implementation. + * @param {Object} obj + * @returns {UnarchiveEvent} + * @private + */ + private createUnarchiveEvent_; + /** + * Receive an event and pass it to the listener functions. + * @param {Object} obj + * @returns {boolean} Returns true if the decompression is finished. + * @private + */ + private handlePortEvent_; + /** + * Starts the unarchive by connecting the ports and sending the first ArrayBuffer. + * @returns {Promise} A Promise that resolves when the decompression is complete. While the + * decompression is proceeding, you can send more bytes of the archive to the decompressor + * using the update() method. + */ + start(): Promise; + /** + * Adds more bytes to the unarchiver. + * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is + * set to true, this ArrayBuffer must not be referenced after calling update(), since it + * is marked as Transferable and sent to the implementation. + * @param {boolean=} opt_transferable Optional boolean whether to mark this ArrayBuffer + * as a Tranferable object, which means it can no longer be referenced outside of + * the implementation context. + */ + update(ab: ArrayBuffer, opt_transferable?: boolean | undefined): void; + /** + * Closes the port to the decompressor implementation and terminates it. + */ + stop(): void; +} +export class Unzipper extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab: ArrayBuffer, options?: UnarchiverOptions); +} +export class Unrarrer extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab: ArrayBuffer, options?: UnarchiverOptions); +} +export class Untarrer extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab: ArrayBuffer, options?: UnarchiverOptions); +} +/** + * IMPORTANT NOTES for Gunzipper: + * 1) A Gunzipper will only ever emit one EXTRACT event, because a gzipped file only ever contains + * a single file. + * 2) If the gzipped file does not include the original filename as a FNAME block, then the + * UnarchivedFile in the UnarchiveExtractEvent will not include a filename. It will be up to the + * client to re-assemble the filename (if needed). + * 3) update() is not supported on a Gunzipper, since the current implementation relies on runtime + * support for DecompressionStream('gzip') which can throw hard-to-detect errors reading only + * only part of a file. + * 4) PROGRESS events are not yet supported in Gunzipper. + */ +export class Gunzipper extends Unarchiver { + /** + * @param {ArrayBuffer} ab + * @param {UnarchiverOptions} options + */ + constructor(ab: ArrayBuffer, options?: UnarchiverOptions); +} +export type UnarchivedFile = { + filename: string; + fileData: Uint8Array; +}; +export type UnarchiverOptions = { + /** + * Set to true for verbose unarchiver logging. + */ + debug?: boolean | undefined; +}; +import { UnarchiveAppendEvent } from "./events.js"; +import { UnarchiveErrorEvent } from "./events.js"; +import { UnarchiveEvent } from "./events.js"; +import { UnarchiveEventType } from "./events.js"; +import { UnarchiveExtractEvent } from "./events.js"; +import { UnarchiveFinishEvent } from "./events.js"; +import { UnarchiveInfoEvent } from "./events.js"; +import { UnarchiveProgressEvent } from "./events.js"; +import { UnarchiveStartEvent } from "./events.js"; +export { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType, UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent, UnarchiveProgressEvent, UnarchiveStartEvent }; +//# sourceMappingURL=decompress.d.ts.map \ No newline at end of file diff --git a/types/archive/decompress.d.ts.map b/types/archive/decompress.d.ts.map new file mode 100644 index 0000000..6c2c8a8 --- /dev/null +++ b/types/archive/decompress.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"decompress.d.ts","sourceRoot":"","sources":["../../archive/decompress.js"],"names":[],"mappings":"AA4VA;;;;;;;;;;GAUG;AACH,kCARW,WAAW,YAGX,iBAAiB,GAAC,MAAM,GAGtB,UAAU,CAoBtB;AA3VD;;;;GAIG;AAEH;;;;GAIG;AAEH;;;GAGG;AAEH;;GAEG;AACH;IAgBE;;;;;;;OAOG;IACH,yBAPW,WAAW,YAGX,iBAAiB,GAAC,MAAM,EA0BlC;IA7CD;;;;;OAKG;IACH,cAAM;IAEN;;;;OAIG;IACH,sBAAc;IAoBZ;;;;OAIG;IACH,cAHU,WAAW,CAGA;IAErB;;;OAGG;IACH,YAFU,OAAO,CAEkB;IAGrC;;;;;OAKG;IACH,qCAJW,UAAU,GAAC,SAAS,GAAC,QAAQ,YAC7B,kCAAkC,QAK5C;IAED;;;;OAIG;IACH,2BAHoB,qBAAqB,WAC5B,UAAU,CAKtB;IAED;;;;OAIG;IACH,0BAHoB,oBAAoB,WAC3B,UAAU,CAKtB;IAED;;;;OAIG;IACH,4BAHoB,sBAAsB,WAC7B,UAAU,CAKtB;IAED;;;;OAIG;IACH,yBAHa,MAAM,CAKlB;IAED;;;;OAIG;IACH,+BAHa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,8BAsBC;IAED;;;;;OAKG;IACH,yBAaC;IAED;;;;;OAKG;IACH,SAJa,QAAQ,IAAI,CAAC,CAgCzB;IAGD;;;;;;;;OAQG;IACH,WAPW,WAAW,qBAGX,OAAO,oBAgBjB;IAED;;OAEG;IACH,aAOC;CACF;AAID;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;;;;;;;;;;;GAWG;AACH;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;;cAlTa,MAAM;cACN,UAAU;;;;;;YAKV,OAAO"} \ No newline at end of file diff --git a/types/archive/events.d.ts b/types/archive/events.d.ts new file mode 100644 index 0000000..98fa19c --- /dev/null +++ b/types/archive/events.d.ts @@ -0,0 +1,88 @@ +export namespace UnarchiveEventType { + const START: string; + const APPEND: string; + const PROGRESS: string; + const EXTRACT: string; + const FINISH: string; + const INFO: string; + const ERROR: string; +} +/** An unarchive event. */ +export class UnarchiveEvent extends Event { + /** + * @param {string} type The event type. + */ + constructor(type: string); +} +/** Updates all Unarchiver listeners that an append has occurred. */ +export class UnarchiveAppendEvent extends UnarchiveEvent { + /** + * @param {number} numBytes The number of bytes appended. + */ + constructor(numBytes: number); + /** + * The number of appended bytes. + * @type {number} + */ + numBytes: number; +} +/** Useful for passing info up to the client (for debugging). */ +export class UnarchiveInfoEvent extends UnarchiveEvent { + /** + * The information message. + * @type {string} + */ + msg: string; +} +/** An unrecoverable error has occured. */ +export class UnarchiveErrorEvent extends UnarchiveEvent { + /** + * The information message. + * @type {string} + */ + msg: string; +} +/** Start event. */ +export class UnarchiveStartEvent extends UnarchiveEvent { + constructor(); +} +/** Finish event. */ +export class UnarchiveFinishEvent extends UnarchiveEvent { + /** + * @param {Object} metadata A collection of metadata about the archive file. + */ + constructor(metadata?: any); + metadata: any; +} +/** Progress event. */ +export class UnarchiveProgressEvent extends UnarchiveEvent { + /** + * @param {string} currentFilename + * @param {number} currentFileNumber + * @param {number} currentBytesUnarchivedInFile + * @param {number} currentBytesUnarchived + * @param {number} totalUncompressedBytesInArchive + * @param {number} totalFilesInArchive + * @param {number} totalCompressedBytesRead + */ + constructor(currentFilename: string, currentFileNumber: number, currentBytesUnarchivedInFile: number, currentBytesUnarchived: number, totalUncompressedBytesInArchive: number, totalFilesInArchive: number, totalCompressedBytesRead: number); + currentFilename: string; + currentFileNumber: number; + currentBytesUnarchivedInFile: number; + totalFilesInArchive: number; + currentBytesUnarchived: number; + totalUncompressedBytesInArchive: number; + totalCompressedBytesRead: number; +} +/** Extract event. */ +export class UnarchiveExtractEvent extends UnarchiveEvent { + /** + * @param {UnarchivedFile} unarchivedFile + */ + constructor(unarchivedFile: UnarchivedFile); + /** + * @type {UnarchivedFile} + */ + unarchivedFile: UnarchivedFile; +} +//# sourceMappingURL=events.d.ts.map \ No newline at end of file diff --git a/types/archive/events.d.ts.map b/types/archive/events.d.ts.map new file mode 100644 index 0000000..5d7045f --- /dev/null +++ b/types/archive/events.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../archive/events.js"],"names":[],"mappings":";;;;;;;;;AA6BA,0BAA0B;AAC1B;IACE;;OAEG;IACH,kBAFW,MAAM,EAIhB;CACF;AAED,oEAAoE;AACpE;IACE;;OAEG;IACH,sBAFW,MAAM,EAUhB;IALC;;;OAGG;IACH,UAFU,MAAM,CAEQ;CAE3B;AAED,gEAAgE;AAChE;IAOI;;;OAGG;IACH,KAFU,MAAM,CAEF;CAEjB;AAED,0CAA0C;AAC1C;IAOI;;;OAGG;IACH,KAFU,MAAM,CAEF;CAEjB;AAED,mBAAmB;AACnB;IACE,cAEC;CACF;AAED,oBAAoB;AACpB;IACE;;OAEG;IACH,4BAGC;IADC,cAAwB;CAE3B;AAGD,sBAAsB;AACtB;IACE;;;;;;;;OAQG;IACH,6BARW,MAAM,qBACN,MAAM,gCACN,MAAM,0BACN,MAAM,mCACN,MAAM,uBACN,MAAM,4BACN,MAAM,EAchB;IAPC,wBAAsC;IACtC,0BAA0C;IAC1C,qCAAgE;IAChE,4BAA8C;IAC9C,+BAAoD;IACpD,wCAAsE;IACtE,iCAAwD;CAE3D;AAED,qBAAqB;AACrB;IACE;;OAEG;IACH,4CAOC;IAJC;;OAEG;IACH,+BAAoC;CAEvC"} \ No newline at end of file diff --git a/types/codecs/codecs.d.ts b/types/codecs/codecs.d.ts index bcddced..e9e6362 100644 --- a/types/codecs/codecs.d.ts +++ b/types/codecs/codecs.d.ts @@ -1,35 +1,3 @@ -/** - * This module helps interpret ffprobe -print_format json output. - * Its coverage is pretty sparse right now, so send me pull requests! - */ -/** - * @typedef ProbeStream ffprobe -show_streams -print_format json. Only the fields we care about. - * @property {number} index - * @property {string} codec_name - * @property {string} codec_long_name - * @property {string} profile - * @property {string} codec_type Either 'audio' or 'video'. - * @property {string} codec_tag_string - * @property {string} id - * @property {number?} level - * @property {number?} width - * @property {number?} height - * @property {string} r_frame_rate Like "60000/1001" - */ -/** - * @typedef ProbeFormat Only the fields we care about from the following command: - * ffprobe -show_format -show_streams -v quiet -print_format json -i file.mp4 - * @property {string} filename - * @property {string} format_name - * @property {string} duration Number of seconds, as a string like "473.506367". - * @property {string} size Number of bytes, as a string. - * @property {string} bit_rate Bit rate, as a string. - */ -/** - * @typedef ProbeInfo ffprobe -show_format -show_streams -print_format json - * @property {ProbeStream[]} streams - * @property {ProbeFormat} format - */ /** * TODO: Reconcile this with file/sniffer.js findMimeType() which does signature matching. * @param {ProbeInfo} info diff --git a/types/codecs/codecs.d.ts.map b/types/codecs/codecs.d.ts.map index cb57957..2a0a98b 100644 --- a/types/codecs/codecs.d.ts.map +++ b/types/codecs/codecs.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../../codecs/codecs.js"],"names":[],"mappings":"AAQA;;;GAGG;AAEH;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;GAQG;AAEH;;;;GAIG;AAEH;;;;GAIG;AACH,yCAHW,SAAS,GACP,MAAM,CAgDlB;AAED;;;;;;;;;GASG;AACH,wCAHW,SAAS,GACP,MAAM,CA0DlB;;;;;WApJa,MAAM;gBACN,MAAM;qBACN,MAAM;aACN,MAAM;;;;gBACN,MAAM;sBACN,MAAM;QACN,MAAM;WACN,MAAM;WACN,MAAM;YACN,MAAM;;;;kBACN,MAAM;;;;;;;cAMN,MAAM;iBACN,MAAM;;;;cACN,MAAM;;;;UACN,MAAM;;;;cACN,MAAM;;;;;;aAKN,WAAW,EAAE;YACb,WAAW"} \ No newline at end of file +{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../../codecs/codecs.js"],"names":[],"mappings":"AA8DA;;;;GAIG;AACH,yCAHW,SAAS,GACP,MAAM,CAuDlB;AAED;;;;;;;;;GASG;AACH,wCAHW,SAAS,GACP,MAAM,CAiElB;;;;;WApLa,MAAM;gBACN,MAAM;qBACN,MAAM;aACN,MAAM;;;;gBACN,MAAM;sBACN,MAAM;QACN,MAAM;WACN,MAAM;WACN,MAAM;YACN,MAAM;;;;kBACN,MAAM;;;;;;;cAMN,MAAM;iBACN,MAAM;;;;cACN,MAAM;;;;UACN,MAAM;;;;cACN,MAAM;;;;;;aAKN,WAAW,EAAE;YACb,WAAW"} \ No newline at end of file diff --git a/types/file/sniffer.d.ts.map b/types/file/sniffer.d.ts.map index 5c7afc8..e541a2f 100644 --- a/types/file/sniffer.d.ts.map +++ b/types/file/sniffer.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"sniffer.d.ts","sourceRoot":"","sources":["../../file/sniffer.js"],"names":[],"mappings":"AAuFA;;;GAGG;AACH,mCAqCC;AAED;;;;GAIG;AACH,iCAHW,WAAW,GACT,MAAM,CAoBlB"} \ No newline at end of file +{"version":3,"file":"sniffer.d.ts","sourceRoot":"","sources":["../../file/sniffer.js"],"names":[],"mappings":"AA0FA;;;GAGG;AACH,mCAqCC;AAED;;;;GAIG;AACH,iCAHW,WAAW,GACT,MAAM,CAoBlB"} \ No newline at end of file diff --git a/types/image/parsers/exif.d.ts b/types/image/parsers/exif.d.ts new file mode 100644 index 0000000..88edc96 --- /dev/null +++ b/types/image/parsers/exif.d.ts @@ -0,0 +1,133 @@ +/** + * @param {ByteStream} stream + * @param {ByteStream} lookAheadStream + * @param {boolean} debug + * @returns {ExifValue} + */ +export function getExifValue(stream: ByteStream, lookAheadStream: ByteStream, DEBUG?: boolean): ExifValue; +/** + * Reads the entire EXIF profile. The first 2 bytes in the stream must be the TIFF marker (II/MM). + * @param {ByteStream} stream + * @returns {Map; +export type ExifTagNumber = number; +export namespace ExifTagNumber { + const IMAGE_DESCRIPTION: number; + const MAKE: number; + const MODEL: number; + const ORIENTATION: number; + const X_RESOLUTION: number; + const Y_RESOLUTION: number; + const RESOLUTION_UNIT: number; + const SOFTWARE: number; + const DATE_TIME: number; + const WHITE_POINT: number; + const PRIMARY_CHROMATICITIES: number; + const Y_CB_CR_COEFFICIENTS: number; + const Y_CB_CR_POSITIONING: number; + const REFERENCE_BLACK_WHITE: number; + const COPYRIGHT: number; + const EXIF_OFFSET: number; + const EXPOSURE_TIME: number; + const F_NUMBER: number; + const EXPOSURE_PROGRAM: number; + const ISO_SPEED_RATINGS: number; + const EXIF_VERSION: number; + const DATE_TIME_ORIGINAL: number; + const DATE_TIME_DIGITIZED: number; + const COMPONENT_CONFIGURATION: number; + const COMPRESSED_BITS_PER_PIXEL: number; + const SHUTTER_SPEED_VALUE: number; + const APERTURE_VALUE: number; + const BRIGHTNESS_VALUE: number; + const EXPOSURE_BIAS_VALUE: number; + const MAX_APERTURE_VALUE: number; + const SUBJECT_DISTANCE: number; + const METERING_MODE: number; + const LIGHT_SOURCE: number; + const FLASH: number; + const FOCAL_LENGTH: number; + const MAKER_NOTE: number; + const USER_COMMENT: number; + const FLASH_PIX_VERSION: number; + const COLOR_SPACE: number; + const EXIF_IMAGE_WIDTH: number; + const EXIF_IMAGE_HEIGHT: number; + const RELATED_SOUND_FILE: number; + const EXIF_INTEROPERABILITY_OFFSET: number; + const FOCAL_PLANE_X_RESOLUTION: number; + const FOCAL_PLANE_Y_RESOLUTION: number; + const FOCAL_PLANE_RESOLUTION_UNIT: number; + const SENSING_METHOD: number; + const FILE_SOURCE: number; + const SCENE_TYPE: number; + const IMAGE_WIDTH: number; + const IMAGE_LENGTH: number; + const BITS_PER_SAMPLE: number; + const COMPRESSION: number; + const PHOTOMETRIC_INTERPRETATION: number; + const STRIP_OFFSETS: number; + const SAMPLES_PER_PIXEL: number; + const ROWS_PER_STRIP: number; + const STRIP_BYTE_COUNTS: number; + const PLANAR_CONFIGURATION: number; + const JPEG_IF_OFFSET: number; + const JPEG_IF_BYTE_COUNT: number; + const Y_CB_CR_SUB_SAMPLING: number; +} +export type ExifDataFormat = number; +export namespace ExifDataFormat { + const UNSIGNED_BYTE: number; + const ASCII_STRING: number; + const UNSIGNED_SHORT: number; + const UNSIGNED_LONG: number; + const UNSIGNED_RATIONAL: number; + const SIGNED_BYTE: number; + const UNDEFINED: number; + const SIGNED_SHORT: number; + const SIGNED_LONG: number; + const SIGNED_RATIONAL: number; + const SINGLE_FLOAT: number; + const DOUBLE_FLOAT: number; +} +export type ExifValue = { + /** + * The numerical value of the tag. + */ + tagNumber: ExifTagNumber; + /** + * A string representing the tag number. + */ + tagName?: string | undefined; + /** + * The data format. + */ + dataFormat: ExifDataFormat; + /** + * Populated for SIGNED/UNSIGNED BYTE/SHORT/LONG/FLOAT. + */ + numericalValue?: number | undefined; + /** + * Populated only for ASCII_STRING. + */ + stringValue?: string | undefined; + /** + * Populated only for SIGNED/UNSIGNED RATIONAL. + */ + numeratorValue?: number | undefined; + /** + * Populated only for SIGNED/UNSIGNED RATIONAL. + */ + denominatorValue?: number | undefined; + /** + * Populated only for UNDEFINED data format. + */ + numComponents?: number | undefined; + /** + * Populated only for UNDEFINED data format. + */ + offsetValue?: number | undefined; +}; +import { ByteStream } from "../../io/bytestream.js"; +//# sourceMappingURL=exif.d.ts.map \ No newline at end of file diff --git a/types/image/parsers/exif.d.ts.map b/types/image/parsers/exif.d.ts.map new file mode 100644 index 0000000..c82e655 --- /dev/null +++ b/types/image/parsers/exif.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"exif.d.ts","sourceRoot":"","sources":["../../../image/parsers/exif.js"],"names":[],"mappings":"AA+HA;;;;;GAKG;AACH,qCALW,UAAU,mBACV,UAAU,oBAER,SAAS,CAsGrB;AA6BD;;;;GAIG;AACH,uCAHW,UAAU,0BAyCpB;4BArSU,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BA2EN,MAAM;;;;;;;;;;;;;;;;;;;eAkBH,aAAa;;;;cACb,MAAM;;;;gBACN,cAAc;;;;qBACd,MAAM;;;;kBACN,MAAM;;;;qBACN,MAAM;;;;uBACN,MAAM;;;;oBACN,MAAM;;;;kBACN,MAAM"} \ No newline at end of file diff --git a/types/image/parsers/gif.d.ts b/types/image/parsers/gif.d.ts new file mode 100644 index 0000000..bdd0b88 --- /dev/null +++ b/types/image/parsers/gif.d.ts @@ -0,0 +1,231 @@ +export namespace GifParseEventType { + const APPLICATION_EXTENSION: string; + const COMMENT_EXTENSION: string; + const GRAPHIC_CONTROL_EXTENSION: string; + const HEADER: string; + const LOGICAL_SCREEN: string; + const PLAIN_TEXT_EXTENSION: string; + const TABLE_BASED_IMAGE: string; + const TRAILER: string; +} +/** + * @typedef GifHeader + * @property {string} version + */ +/** + * @typedef GifColor + * @property {number} red + * @property {number} green + * @property {number} blue + */ +/** + * @typedef GifLogicalScreen + * @property {number} logicalScreenWidth + * @property {number} logicalScreenHeight + * @property {boolean} globalColorTableFlag + * @property {number} colorResolution + * @property {boolean} sortFlag + * @property {number} globalColorTableSize + * @property {number} backgroundColorIndex + * @property {number} pixelAspectRatio + * @property {GifColor[]=} globalColorTable Only if globalColorTableFlag is true. + */ +/** + * @typedef GifTableBasedImage + * @property {number} imageLeftPosition + * @property {number} imageTopPosition + * @property {number} imageWidth + * @property {number} imageHeight + * @property {boolean} localColorTableFlag + * @property {boolean} interlaceFlag + * @property {boolean} sortFlag + * @property {number} localColorTableSize + * @property {GifColor[]=} localColorTable Only if localColorTableFlag is true. + * @property {number} lzwMinimumCodeSize + * @property {Uint8Array} imageData + */ +/** + * @typedef GifGraphicControlExtension + * @property {number} disposalMethod + * @property {boolean} userInputFlag + * @property {boolean} transparentColorFlag + * @property {number} delayTime + * @property {number} transparentColorIndex + */ +/** + * @typedef GifCommentExtension + * @property {string} comment + */ +/** + * @typedef GifPlainTextExtension + * @property {number} textGridLeftPosition + * @property {number} textGridTopPosition + * @property {number} textGridWidth + * @property {number} textGridHeight + * @property {number} characterCellWidth + * @property {number} characterCellHeight + * @property {number} textForegroundColorIndex + * @property {number} textBackgroundColorIndex + * @property {string} plainText + */ +/** + * @typedef GifApplicationExtension + * @property {string} applicationIdentifier + * @property {Uint8Array} applicationAuthenticationCode + * @property {Uint8Array} applicationData + */ +/** + * The Grammar. + * + * ::= Header * Trailer + * ::= Logical Screen Descriptor [Global Color Table] + * ::= | + * + * ::= [Graphic Control Extension] + * ::= | + * Plain Text Extension + * ::= Image Descriptor [Local Color Table] Image Data + * ::= Application Extension | + * Comment Extension + */ +export class GifParser extends EventTarget { + /** @param {ArrayBuffer} ab */ + constructor(ab: ArrayBuffer); + /** + * @type {ByteStream} + * @private + */ + private bstream; + /** + * @type {string} + * @private + */ + private version; + /** + * Type-safe way to bind a listener for a GifApplicationExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onApplicationExtension(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifCommentExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onCommentExtension(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifGraphicControlExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onGraphicControlExtension(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifHeader. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onHeader(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifLogicalScreen. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onLogicalScreen(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifPlainTextExtension. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onPlainTextExtension(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for a GifTableBasedImage. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onTableBasedImage(listener: (arg0: CustomEvent) => void): GifParser; + /** + * Type-safe way to bind a listener for the GifTrailer. + * @param {function(CustomEvent): void} listener + * @returns {GifParser} for chaining + */ + onTrailer(listener: (arg0: CustomEvent) => void): GifParser; + /** + * @returns {Promise} A Promise that resolves when the parsing is complete. + */ + start(): Promise; + /** + * @private + * @returns {boolean} True if this was not the last block. + */ + private readGraphicBlock; + /** + * @private + * @returns {Uint8Array} Data from the sub-block, or null if this was the last, zero-length block. + */ + private readSubBlock; +} +export type GifHeader = { + version: string; +}; +export type GifColor = { + red: number; + green: number; + blue: number; +}; +export type GifLogicalScreen = { + logicalScreenWidth: number; + logicalScreenHeight: number; + globalColorTableFlag: boolean; + colorResolution: number; + sortFlag: boolean; + globalColorTableSize: number; + backgroundColorIndex: number; + pixelAspectRatio: number; + /** + * Only if globalColorTableFlag is true. + */ + globalColorTable?: GifColor[] | undefined; +}; +export type GifTableBasedImage = { + imageLeftPosition: number; + imageTopPosition: number; + imageWidth: number; + imageHeight: number; + localColorTableFlag: boolean; + interlaceFlag: boolean; + sortFlag: boolean; + localColorTableSize: number; + /** + * Only if localColorTableFlag is true. + */ + localColorTable?: GifColor[] | undefined; + lzwMinimumCodeSize: number; + imageData: Uint8Array; +}; +export type GifGraphicControlExtension = { + disposalMethod: number; + userInputFlag: boolean; + transparentColorFlag: boolean; + delayTime: number; + transparentColorIndex: number; +}; +export type GifCommentExtension = { + comment: string; +}; +export type GifPlainTextExtension = { + textGridLeftPosition: number; + textGridTopPosition: number; + textGridWidth: number; + textGridHeight: number; + characterCellWidth: number; + characterCellHeight: number; + textForegroundColorIndex: number; + textBackgroundColorIndex: number; + plainText: string; +}; +export type GifApplicationExtension = { + applicationIdentifier: string; + applicationAuthenticationCode: Uint8Array; + applicationData: Uint8Array; +}; +//# sourceMappingURL=gif.d.ts.map \ No newline at end of file diff --git a/types/image/parsers/gif.d.ts.map b/types/image/parsers/gif.d.ts.map new file mode 100644 index 0000000..01f4c31 --- /dev/null +++ b/types/image/parsers/gif.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"gif.d.ts","sourceRoot":"","sources":["../../../image/parsers/gif.js"],"names":[],"mappings":";;;;;;;;;;AA2BA;;;GAGG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;GAWG;AAEH;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AAEH;;;GAGG;AAEH;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;GAaG;AAEH;IAaE,8BAA8B;IAC9B,gBADY,WAAW,EAMtB;IAlBD;;;OAGG;IACH,gBAAQ;IAER;;;OAGG;IACH,gBAAQ;IAUR;;;;OAIG;IACH,wCAHoB,YAAY,uBAAuB,CAAC,KAAG,IAAI,GAClD,SAAS,CAKrB;IAED;;;;OAIG;IACH,oCAHoB,YAAY,mBAAmB,CAAC,KAAG,IAAI,GAC9C,SAAS,CAKrB;IAED;;;;OAIG;IACH,2CAHoB,YAAY,0BAA0B,CAAC,KAAG,IAAI,GACrD,SAAS,CAKrB;IAED;;;;OAIG;IACH,0BAHoB,YAAY,SAAS,CAAC,KAAG,IAAI,GACpC,SAAS,CAKrB;IAED;;;;OAIG;IACH,iCAHoB,YAAY,gBAAgB,CAAC,KAAG,IAAI,GAC3C,SAAS,CAKrB;IAED;;;;OAIG;IACH,sCAHoB,YAAY,qBAAqB,CAAC,KAAG,IAAI,GAChD,SAAS,CAKrB;IAED;;;;OAIG;IACH,mCAHoB,YAAY,kBAAkB,CAAC,KAAG,IAAI,GAC7C,SAAS,CAKrB;IAED;;;;OAIG;IACH,2BAHoB,WAAW,KAAG,IAAI,GACzB,SAAS,CAKrB;IAED;;OAEG;IACH,SAFa,QAAQ,IAAI,CAAC,CAsDzB;IAED;;;OAGG;IACH,yBAmMC;IAED;;;OAGG;IACH,qBAIC;CACF;;aAvca,MAAM;;;SAKN,MAAM;WACN,MAAM;UACN,MAAM;;;wBAKN,MAAM;yBACN,MAAM;0BACN,OAAO;qBACP,MAAM;cACN,OAAO;0BACP,MAAM;0BACN,MAAM;sBACN,MAAM;;;;uBACN,QAAQ,EAAE;;;uBAKV,MAAM;sBACN,MAAM;gBACN,MAAM;iBACN,MAAM;yBACN,OAAO;mBACP,OAAO;cACP,OAAO;yBACP,MAAM;;;;sBACN,QAAQ,EAAE;wBACV,MAAM;eACN,UAAU;;;oBAKV,MAAM;mBACN,OAAO;0BACP,OAAO;eACP,MAAM;2BACN,MAAM;;;aAKN,MAAM;;;0BAKN,MAAM;yBACN,MAAM;mBACN,MAAM;oBACN,MAAM;wBACN,MAAM;yBACN,MAAM;8BACN,MAAM;8BACN,MAAM;eACN,MAAM;;;2BAKN,MAAM;mCACN,UAAU;qBACV,UAAU"} \ No newline at end of file diff --git a/types/image/parsers/jpeg.d.ts b/types/image/parsers/jpeg.d.ts new file mode 100644 index 0000000..3488e76 --- /dev/null +++ b/types/image/parsers/jpeg.d.ts @@ -0,0 +1,226 @@ +export type JpegParseEventType = string; +export namespace JpegParseEventType { + const APP0_MARKER: string; + const APP0_EXTENSION: string; + const APP1_EXIF: string; + const DEFINE_QUANTIZATION_TABLE: string; + const DEFINE_HUFFMAN_TABLE: string; + const START_OF_FRAME: string; + const START_OF_SCAN: string; +} +export type JpegSegmentType = number; +export namespace JpegSegmentType { + const SOF0: number; + const SOF1: number; + const SOF2: number; + const DHT: number; + const SOI: number; + const EOI: number; + const SOS: number; + const DQT: number; + const APP0: number; + const APP1: number; +} +export type JpegDensityUnits = number; +export namespace JpegDensityUnits { + const NO_UNITS: number; + const PIXELS_PER_INCH: number; + const PIXELS_PER_CM: number; +} +export type JpegExtensionThumbnailFormat = number; +export namespace JpegExtensionThumbnailFormat { + const JPEG: number; + const ONE_BYTE_PER_PIXEL_PALETTIZED: number; + const THREE_BYTES_PER_PIXEL_RGB: number; +} +export type JpegHuffmanTableType = number; +export namespace JpegHuffmanTableType { + const DC: number; + const AC: number; +} +export type JpegDctType = number; +export namespace JpegDctType { + const BASELINE: number; + const EXTENDED_SEQUENTIAL: number; + const PROGRESSIVE: number; +} +export type JpegComponentType = number; +export namespace JpegComponentType { + const Y: number; + const CB: number; + const CR: number; + const I: number; + const Q: number; +} +/** + * @typedef JpegComponentDetail + * @property {JpegComponentType} componentId + * @property {number} verticalSamplingFactor + * @property {number} horizontalSamplingFactor + * @property {number} quantizationTableNumber + */ +/** + * @typedef JpegStartOfFrame + * @property {JpegDctType} dctType + * @property {number} dataPrecision + * @property {number} imageHeight + * @property {number} imageWidth + * @property {number} numberOfComponents Usually 1, 3, or 4. + * @property {JpegComponentDetail[]} componentDetails + */ +/** + * @typedef JpegStartOfScan + * @property {number} componentsInScan + * @property {number} componentSelectorY + * @property {number} huffmanTableSelectorY + * @property {number} componentSelectorCb + * @property {number} huffmanTableSelectorCb + * @property {number} componentSelectorCr + * @property {number} huffmanTableSelectorCr + * @property {number} scanStartPositionInBlock + * @property {number} scanEndPositionInBlock + * @property {number} successiveApproximationBitPosition + * @property {Uint8Array} rawImageData + */ +export class JpegParser extends EventTarget { + /** @param {ArrayBuffer} ab */ + constructor(ab: ArrayBuffer); + /** + * @type {ByteStream} + * @private + */ + private bstream; + /** + * @type {boolean} + * @private + */ + private hasApp0MarkerSegment; + /** + * Type-safe way to bind a listener for a JpegApp0Marker. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp0Marker(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegApp0Extension. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp0Extension(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegExifProfile. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onApp1Exif(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegDefineQuantizationTable. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onDefineQuantizationTable(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegDefineHuffmanTable. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onDefineHuffmanTable(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegStartOfFrame. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onStartOfFrame(listener: (arg0: CustomEvent) => void): JpegParser; + /** + * Type-safe way to bind a listener for a JpegStartOfScan. + * @param {function(CustomEvent): void} listener + * @returns {JpegParser} for chaining + */ + onStartOfScan(listener: (arg0: CustomEvent) => void): JpegParser; + /** @returns {Promise} A Promise that resolves when the parsing is complete. */ + start(): Promise; +} +export type ExifValue = import('./exif.js').ExifValue; +export type JpegApp0Marker = { + /** + * Like '1.02'. + */ + jfifVersion: string; + densityUnits: JpegDensityUnits; + xDensity: number; + yDensity: number; + xThumbnail: number; + yThumbnail: number; + /** + * RGB data. Size is 3 x thumbnailWidth x thumbnailHeight. + */ + thumbnailData: Uint8Array; +}; +export type JpegApp0Extension = { + thumbnailFormat: JpegExtensionThumbnailFormat; + /** + * Raw thumbnail data + */ + thumbnailData: Uint8Array; +}; +export type JpegExifProfile = Map; +export type JpegDefineQuantizationTable = { + /** + * Table/component number. + */ + tableNumber: number; + /** + * (0=byte, 1=word). + */ + precision: number; + /** + * 64 numbers representing the quantization table. + */ + tableValues: number[]; +}; +export type JpegDefineHuffmanTable = { + /** + * Table/component number (0-3). + */ + tableNumber: number; + /** + * Either DC or AC. + */ + tableType: JpegHuffmanTableType; + /** + * A 16-byte array specifying the # of symbols of each length. + */ + numberOfSymbols: number[]; + symbols: number[]; +}; +export type JpegComponentDetail = { + componentId: JpegComponentType; + verticalSamplingFactor: number; + horizontalSamplingFactor: number; + quantizationTableNumber: number; +}; +export type JpegStartOfFrame = { + dctType: JpegDctType; + dataPrecision: number; + imageHeight: number; + imageWidth: number; + /** + * Usually 1, 3, or 4. + */ + numberOfComponents: number; + componentDetails: JpegComponentDetail[]; +}; +export type JpegStartOfScan = { + componentsInScan: number; + componentSelectorY: number; + huffmanTableSelectorY: number; + componentSelectorCb: number; + huffmanTableSelectorCb: number; + componentSelectorCr: number; + huffmanTableSelectorCr: number; + scanStartPositionInBlock: number; + scanEndPositionInBlock: number; + successiveApproximationBitPosition: number; + rawImageData: Uint8Array; +}; +//# sourceMappingURL=jpeg.d.ts.map \ No newline at end of file diff --git a/types/image/parsers/jpeg.d.ts.map b/types/image/parsers/jpeg.d.ts.map new file mode 100644 index 0000000..9b2431b --- /dev/null +++ b/types/image/parsers/jpeg.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"jpeg.d.ts","sourceRoot":"","sources":["../../../image/parsers/jpeg.js"],"names":[],"mappings":"iCAuBW,MAAM;;;;;;;;;;8BAWN,MAAM;;;;;;;;;;;;;+BAwBN,MAAM;;;;;;2CAkBN,MAAM;;;;;;mCAsBN,MAAM;;;;;0BAcN,MAAM;;;;;;gCAON,MAAM;;;;;;;;AASjB;;;;;;GAMG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;GAaG;AAEH;IAaE,8BAA8B;IAC9B,gBADY,WAAW,EAItB;IAhBD;;;OAGG;IACH,gBAAQ;IAER;;;OAGG;IACH,6BAA6B;IAQ7B;;;;OAIG;IACH,8BAHoB,YAAY,cAAc,CAAC,KAAG,IAAI,GACzC,UAAU,CAKtB;IAED;;;;OAIG;IACH,iCAHoB,YAAY,iBAAiB,CAAC,KAAG,IAAI,GAC5C,UAAU,CAKtB;IAED;;;;OAIG;IACH,4BAHoB,YAAY,eAAe,CAAC,KAAG,IAAI,GAC1C,UAAU,CAKtB;IAED;;;;OAIG;IACH,2CAHoB,YAAY,2BAA2B,CAAC,KAAG,IAAI,GACtD,UAAU,CAKtB;IAED;;;;OAIG;IACH,sCAHoB,YAAY,sBAAsB,CAAC,KAAG,IAAI,GACjD,UAAU,CAKtB;IAED;;;;OAIG;IACH,gCAHoB,YAAY,gBAAgB,CAAC,KAAG,IAAI,GAC3C,UAAU,CAKtB;IAED;;;;OAIG;IACH,+BAHoB,YAAY,eAAe,CAAC,KAAG,IAAI,GAC1C,UAAU,CAKtB;IAED,qFAAqF;IACrF,SADc,QAAQ,IAAI,CAAC,CA8O1B;CACF;wBA3da,OAAO,WAAW,EAAE,SAAS;;;;;iBAqD7B,MAAM;kBACN,gBAAgB;cAChB,MAAM;cACN,MAAM;gBACN,MAAM;gBACN,MAAM;;;;mBACN,UAAU;;;qBAYV,4BAA4B;;;;mBAC5B,UAAU;;;;;;;iBAOV,MAAM;;;;eACN,MAAM;;;;iBACN,MAAM,EAAE;;;;;;iBAWR,MAAM;;;;eACN,oBAAoB;;;;qBACpB,MAAM,EAAE;aACR,MAAM,EAAE;;;iBAqBR,iBAAiB;4BACjB,MAAM;8BACN,MAAM;6BACN,MAAM;;;aAKN,WAAW;mBACX,MAAM;iBACN,MAAM;gBACN,MAAM;;;;wBACN,MAAM;sBACN,mBAAmB,EAAE;;;sBAKrB,MAAM;wBACN,MAAM;2BACN,MAAM;yBACN,MAAM;4BACN,MAAM;yBACN,MAAM;4BACN,MAAM;8BACN,MAAM;4BACN,MAAM;wCACN,MAAM;kBACN,UAAU"} \ No newline at end of file diff --git a/types/image/parsers/parsers.d.ts b/types/image/parsers/parsers.d.ts new file mode 100644 index 0000000..7b51e2d --- /dev/null +++ b/types/image/parsers/parsers.d.ts @@ -0,0 +1,9 @@ +/** + * Creates a new event of the given type with the specified data. + * @template T + * @param {string} type The event type. + * @param {T} data The event data. + * @returns {CustomEvent} The new event. + */ +export function createEvent(type: string, data: T): CustomEvent; +//# sourceMappingURL=parsers.d.ts.map \ No newline at end of file diff --git a/types/image/parsers/parsers.d.ts.map b/types/image/parsers/parsers.d.ts.map new file mode 100644 index 0000000..2cfc3bf --- /dev/null +++ b/types/image/parsers/parsers.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"parsers.d.ts","sourceRoot":"","sources":["../../../image/parsers/parsers.js"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,qCAJW,MAAM,2BAMhB"} \ No newline at end of file diff --git a/types/image/parsers/png.d.ts b/types/image/parsers/png.d.ts new file mode 100644 index 0000000..84d8a76 --- /dev/null +++ b/types/image/parsers/png.d.ts @@ -0,0 +1,381 @@ +export type PngParseEventType = string; +export namespace PngParseEventType { + const IDAT: string; + const IHDR: string; + const PLTE: string; + const bKGD: string; + const cHRM: string; + const eXIf: string; + const gAMA: string; + const hIST: string; + const iTXt: string; + const pHYs: string; + const sBIT: string; + const sPLT: string; + const tEXt: string; + const tIME: string; + const tRNS: string; + const zTXt: string; +} +export type PngColorType = number; +export namespace PngColorType { + const GREYSCALE: number; + const TRUE_COLOR: number; + const INDEXED_COLOR: number; + const GREYSCALE_WITH_ALPHA: number; + const TRUE_COLOR_WITH_ALPHA: number; +} +export type PngInterlaceMethod = number; +export namespace PngInterlaceMethod { + const NO_INTERLACE: number; + const ADAM7_INTERLACE: number; +} +export namespace PngUnitSpecifier { + const UNKNOWN: number; + const METRE: number; +} +/** + * @typedef PngPhysicalPixelDimensions + * @property {number} pixelPerUnitX + * @property {number} pixelPerUnitY + * @property {PngUnitSpecifier} unitSpecifier + */ +/** @typedef {Map} PngExifProfile */ +/** + * @typedef PngHistogram + * @property {number[]} frequencies The # of frequencies matches the # of palette entries. + */ +/** + * @typedef PngSuggestedPaletteEntry + * @property {number} red + * @property {number} green + * @property {number} blue + * @property {number} alpha + * @property {number} frequency + */ +/** + * @typedef PngSuggestedPalette + * @property {string} paletteName + * @property {number} sampleDepth Either 8 or 16. + * @property {PngSuggestedPaletteEntry[]} entries + */ +/** + * @typedef PngChunk Internal use only. + * @property {number} length + * @property {string} chunkType + * @property {ByteStream} chunkStream Do not read more than length! + * @property {number} crc + */ +export class PngParser extends EventTarget { + /** @param {ArrayBuffer} ab */ + constructor(ab: ArrayBuffer); + /** + * @type {ByteStream} + * @private + */ + private bstream; + /** + * @type {PngColorType} + * @private + */ + private colorType; + /** + * @type {PngPalette} + * @private + */ + private palette; + /** + * Type-safe way to bind a listener for a PngBackgroundColor. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onBackgroundColor(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngChromaticities. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onChromaticities(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngCompressedTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onCompressedTextualData(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngExifProfile. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onExifProfile(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngImageGamma. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onGamma(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngHistogram. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onHistogram(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngImageData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onImageData(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngImageHeader. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onImageHeader(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngIntlTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onIntlTextualData(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngLastModTime. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onLastModTime(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngPalette. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onPalette(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngPhysicalPixelDimensions. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onPhysicalPixelDimensions(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngSignificantBits. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onSignificantBits(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngSuggestedPalette. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onSuggestedPalette(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngTextualData. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onTextualData(listener: (arg0: CustomEvent) => void): PngParser; + /** + * Type-safe way to bind a listener for a PngTransparency. + * @param {function(CustomEvent): void} listener + * @returns {PngParser} for chaining + */ + onTransparency(listener: (arg0: CustomEvent) => void): PngParser; + /** @returns {Promise} A Promise that resolves when the parsing is complete. */ + start(): Promise; +} +export type ExifValue = import('./exif.js').ExifValue; +export type PngImageHeader = { + width: number; + height: number; + bitDepth: number; + colorType: PngColorType; + compressionMethod: number; + filterMethod: number; + interlaceMethod: number; +}; +export type PngSignificantBits = { + /** + * Populated for color types 0, 4. + */ + significant_greyscale?: number | undefined; + /** + * Populated for color types 2, 3, 6. + */ + significant_red?: number | undefined; + /** + * Populated for color types 2, 3, 6. + */ + significant_green?: number | undefined; + /** + * Populated for color types 2, 3, 6. + */ + significant_blue?: number | undefined; + /** + * Populated for color types 4, 6. + */ + significant_alpha?: number | undefined; +}; +export type PngChromaticities = { + whitePointX: number; + whitePointY: number; + redX: number; + redY: number; + greenX: number; + greenY: number; + blueX: number; + blueY: number; +}; +export type PngColor = { + red: number; + green: number; + blue: number; +}; +export type PngPalette = { + entries: PngColor[]; +}; +export type PngTransparency = { + /** + * Populated for color type 0. + */ + greySampleValue?: number | undefined; + /** + * Populated for color type 2. + */ + redSampleValue?: number | undefined; + /** + * Populated for color type 2. + */ + blueSampleValue?: number | undefined; + /** + * Populated for color type 2. + */ + greenSampleValue?: number | undefined; + /** + * Populated for color type 3. + */ + alphaPalette?: number[] | undefined; +}; +export type PngImageData = { + rawImageData: Uint8Array; +}; +export type PngTextualData = { + keyword: string; + textString?: string | undefined; +}; +export type PngCompressedTextualData = { + keyword: string; + /** + * Only value supported is 0 for deflate compression. + */ + compressionMethod: number; + compressedText?: Uint8Array | undefined; +}; +export type PngIntlTextualData = { + keyword: string; + /** + * 0 for uncompressed, 1 for compressed. + */ + compressionFlag: number; + /** + * 0 means zlib defalt when compressionFlag is 1. + */ + compressionMethod: number; + languageTag?: string | undefined; + translatedKeyword?: string | undefined; + /** + * The raw UTF-8 text (may be compressed). + */ + text: Uint8Array; +}; +export type PngBackgroundColor = { + /** + * Only for color types 0 and 4. + */ + greyscale?: number | undefined; + /** + * Only for color types 2 and 6. + */ + red?: number | undefined; + /** + * Only for color types 2 and 6. + */ + green?: number | undefined; + /** + * Only for color types 2 and 6. + */ + blue?: number | undefined; + /** + * Only for color type 3. + */ + paletteIndex?: number | undefined; +}; +export type PngLastModTime = { + /** + * Four-digit year. + */ + year: number; + /** + * One-based. Value from 1-12. + */ + month: number; + /** + * One-based. Value from 1-31. + */ + day: number; + /** + * Zero-based. Value from 0-23. + */ + hour: number; + /** + * Zero-based. Value from 0-59. + */ + minute: number; + /** + * Zero-based. Value from 0-60 to allow for leap-seconds. + */ + second: number; +}; +export type PngPhysicalPixelDimensions = { + pixelPerUnitX: number; + pixelPerUnitY: number; + unitSpecifier: { + UNKNOWN: number; + METRE: number; + }; +}; +export type PngExifProfile = Map; +export type PngHistogram = { + /** + * The # of frequencies matches the # of palette entries. + */ + frequencies: number[]; +}; +export type PngSuggestedPaletteEntry = { + red: number; + green: number; + blue: number; + alpha: number; + frequency: number; +}; +export type PngSuggestedPalette = { + paletteName: string; + /** + * Either 8 or 16. + */ + sampleDepth: number; + entries: PngSuggestedPaletteEntry[]; +}; +/** + * Internal use only. + */ +export type PngChunk = { + length: number; + chunkType: string; + /** + * Do not read more than length! + */ + chunkStream: ByteStream; + crc: number; +}; +import { ByteStream } from "../../io/bytestream.js"; +//# sourceMappingURL=png.d.ts.map \ No newline at end of file diff --git a/types/image/parsers/png.d.ts.map b/types/image/parsers/png.d.ts.map new file mode 100644 index 0000000..19fb2c7 --- /dev/null +++ b/types/image/parsers/png.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../../image/parsers/png.js"],"names":[],"mappings":"gCAuBW,MAAM;;;;;;;;;;;;;;;;;;;2BAuBN,MAAM;;;;;;;;iCASN,MAAM;;;;;;;;;AA+GjB;;;;;GAKG;AAEH,uDAAuD;AAEvD;;;GAGG;AAEH;;;;;;;GAOG;AAEH;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;IAmBE,8BAA8B;IAC9B,gBADY,WAAW,EAKtB;IAvBD;;;OAGG;IACH,gBAAQ;IAER;;;OAGG;IACH,kBAAU;IAEV;;;OAGG;IACH,gBAAQ;IASR;;;;OAIG;IACH,mCAHoB,YAAY,kBAAkB,CAAC,KAAG,IAAI,GAC7C,SAAS,CAKrB;IAED;;;;OAIG;IACH,kCAHoB,YAAY,iBAAiB,CAAC,KAAG,IAAI,GAC5C,SAAS,CAKrB;IAED;;;;OAIG;IACH,yCAHoB,YAAY,wBAAwB,CAAC,KAAG,IAAI,GACnD,SAAS,CAKrB;IAED;;;;OAIG;IACH,+BAHoB,YAAY,cAAc,CAAC,KAAG,IAAI,GACzC,SAAS,CAKrB;IAED;;;;OAIG;IACH,yBAHoB,YAAY,MAAM,CAAC,KAAG,IAAI,GACjC,SAAS,CAKrB;IAED;;;;OAIG;IACH,6BAHoB,YAAY,YAAY,CAAC,KAAG,IAAI,GACvC,SAAS,CAKrB;IAED;;;;OAIG;IACH,6BAHoB,YAAY,YAAY,CAAC,KAAG,IAAI,GACvC,SAAS,CAKrB;IAED;;;;OAIG;IACH,+BAHoB,YAAY,cAAc,CAAC,KAAG,IAAI,GACzC,SAAS,CAKrB;IAED;;;;OAIG;IACH,mCAHoB,YAAY,kBAAkB,CAAC,KAAG,IAAI,GAC7C,SAAS,CAKrB;IAED;;;;OAIG;IACH,+BAHoB,YAAY,cAAc,CAAC,KAAG,IAAI,GACzC,SAAS,CAKrB;IAED;;;;OAIG;IACH,2BAHoB,YAAY,UAAU,CAAC,KAAG,IAAI,GACrC,SAAS,CAKrB;IAED;;;;OAIG;IACH,2CAHoB,YAAY,0BAA0B,CAAC,KAAG,IAAI,GACrD,SAAS,CAKrB;IAED;;;;OAIG;IACH,mCAHoB,YAAY,kBAAkB,CAAC,KAAG,IAAI,GAC7C,SAAS,CAKrB;IAED;;;;OAIG;IACH,oCAHoB,YAAY,mBAAmB,CAAC,KAAG,IAAI,GAC9C,SAAS,CAKrB;IAED;;;;OAIG;IACH,+BAHoB,YAAY,cAAc,CAAC,KAAG,IAAI,GACzC,SAAS,CAKrB;IAED;;;;OAIG;IACH,gCAHoB,YAAY,eAAe,CAAC,KAAG,IAAI,GAC1C,SAAS,CAKrB;IAED,qFAAqF;IACrF,SADc,QAAQ,IAAI,CAAC,CAmV1B;CACF;wBA5sBa,OAAO,WAAW,EAAE,SAAS;;WAiD7B,MAAM;YACN,MAAM;cACN,MAAM;eACN,YAAY;uBACZ,MAAM;kBACN,MAAM;qBACN,MAAM;;;;;;4BAKN,MAAM;;;;sBACN,MAAM;;;;wBACN,MAAM;;;;uBACN,MAAM;;;;wBACN,MAAM;;;iBAKN,MAAM;iBACN,MAAM;UACN,MAAM;UACN,MAAM;YACN,MAAM;YACN,MAAM;WACN,MAAM;WACN,MAAM;;;SAKN,MAAM;WACN,MAAM;UACN,MAAM;;;aAKN,QAAQ,EAAE;;;;;;sBAKV,MAAM;;;;qBACN,MAAM;;;;sBACN,MAAM;;;;uBACN,MAAM;;;;mBACN,MAAM,EAAE;;;kBAKR,UAAU;;;aAKV,MAAM;iBACN,MAAM;;;aAKN,MAAM;;;;uBACN,MAAM;qBACN,UAAU;;;aAKV,MAAM;;;;qBACN,MAAM;;;;uBACN,MAAM;kBACN,MAAM;wBACN,MAAM;;;;UACN,UAAU;;;;;;gBAKV,MAAM;;;;UACN,MAAM;;;;YACN,MAAM;;;;WACN,MAAM;;;;mBACN,MAAM;;;;;;UAKN,MAAM;;;;WACN,MAAM;;;;SACN,MAAM;;;;UACN,MAAM;;;;YACN,MAAM;;;;YACN,MAAM;;;mBAUN,MAAM;mBACN,MAAM;;;;;;;;;;;iBAQN,MAAM,EAAE;;;SAKR,MAAM;WACN,MAAM;UACN,MAAM;WACN,MAAM;eACN,MAAM;;;iBAKN,MAAM;;;;iBACN,MAAM;aACN,wBAAwB,EAAE;;;;;;YAK1B,MAAM;eACN,MAAM;;;;iBACN,UAAU;SACV,MAAM"} \ No newline at end of file diff --git a/types/image/webp-shim/webp-shim.d.ts.map b/types/image/webp-shim/webp-shim.d.ts.map index 75b4874..37cf2f0 100644 --- a/types/image/webp-shim/webp-shim.d.ts.map +++ b/types/image/webp-shim/webp-shim.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"webp-shim.d.ts","sourceRoot":"","sources":["../../../image/webp-shim/webp-shim.js"],"names":[],"mappings":"AAgDA;;;GAGG;AACH,6CAHW,WAAW,GAAC,UAAU,GACpB,QAAQ,WAAW,CAAC,CAyBhC;AAED;;;GAGG;AACH,6CAHW,WAAW,GAAC,UAAU,GACpB,QAAQ,WAAW,CAAC,CAyBhC"} \ No newline at end of file +{"version":3,"file":"webp-shim.d.ts","sourceRoot":"","sources":["../../../image/webp-shim/webp-shim.js"],"names":[],"mappings":"AAkDA;;;GAGG;AACH,6CAHW,WAAW,GAAC,UAAU,GACpB,QAAQ,WAAW,CAAC,CAyBhC;AAED;;;GAGG;AACH,6CAHW,WAAW,GAAC,UAAU,GACpB,QAAQ,WAAW,CAAC,CAyBhC"} \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index e6853b8..e401c34 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,11 +1,47 @@ export { findMimeType } from "./file/sniffer.js"; +export { BitBuffer } from "./io/bitbuffer.js"; export { BitStream } from "./io/bitstream.js"; export { ByteBuffer } from "./io/bytebuffer.js"; export { ByteStream } from "./io/bytestream.js"; export type ProbeStream = import('./codecs/codecs.js').ProbeStream; export type ProbeFormat = import('./codecs/codecs.js').ProbeFormat; export type ProbeInfo = import('./codecs/codecs.js').ProbeInfo; -export { UnarchiveEvent, UnarchiveEventType, UnarchiveInfoEvent, UnarchiveErrorEvent, UnarchiveStartEvent, UnarchiveFinishEvent, UnarchiveProgressEvent, UnarchiveExtractEvent, Unarchiver, Unzipper, Unrarrer, Untarrer, getUnarchiver } from "./archive/archive.js"; +export type GifApplicationExtension = import('./image/parsers/gif.js').GifApplicationExtension; +export type GifColor = import('./image/parsers/gif.js').GifColor; +export type GifCommentExtension = import('./image/parsers/gif.js').GifCommentExtension; +export type GifGraphicControlExtension = import('./image/parsers/gif.js').GifGraphicControlExtension; +export type GifHeader = import('./image/parsers/gif.js').GifHeader; +export type GifLogicalScreen = import('./image/parsers/gif.js').GifLogicalScreen; +export type GifPlainTextExtension = import('./image/parsers/gif.js').GifPlainTextExtension; +export type GifTableBasedImage = import('./image/parsers/gif.js').GifTableBasedImage; +export type JpegApp0Extension = import('./image/parsers/jpeg.js').JpegApp0Extension; +export type JpegApp0Marker = import('./image/parsers/jpeg.js').JpegApp0Marker; +export type JpegComponentDetail = import('./image/parsers/jpeg.js').JpegComponentDetail; +export type JpegDefineHuffmanTable = import('./image/parsers/jpeg.js').JpegDefineHuffmanTable; +export type JpegDefineQuantizationTable = import('./image/parsers/jpeg.js').JpegDefineQuantizationTable; +export type JpegStartOfFrame = import('./image/parsers/jpeg.js').JpegStartOfFrame; +export type JpegStartOfScan = import('./image/parsers/jpeg.js').JpegStartOfScan; +export type PngBackgroundColor = import('./image/parsers/png.js').PngBackgroundColor; +export type PngChromaticies = import('./image/parsers/png.js').PngChromaticities; +export type PngColor = import('./image/parsers/png.js').PngColor; +export type PngCompressedTextualData = import('./image/parsers/png.js').PngCompressedTextualData; +export type PngHistogram = import('./image/parsers/png.js').PngHistogram; +export type PngImageData = import('./image/parsers/png.js').PngImageData; +export type PngImageGamma = import('./image/parsers/png.js').PngImageGamma; +export type PngImageHeader = import('./image/parsers/png.js').PngImageHeader; +export type PngIntlTextualData = import('./image/parsers/png.js').PngIntlTextualData; +export type PngLastModTime = import('./image/parsers/png.js').PngLastModTime; +export type PngPalette = import('./image/parsers/png.js').PngPalette; +export type PngPhysicalPixelDimensions = import('./image/parsers/png.js').PngPhysicalPixelDimensions; +export type PngSignificantBits = import('./image/parsers/png.js').PngSignificantBits; +export type PngSuggestedPalette = import('./image/parsers/png.js').PngSuggestedPalette; +export type PngSuggestedPaletteEntry = import('./image/parsers/png.js').PngSuggestedPaletteEntry; +export type PngTextualData = import('./image/parsers/png.js').PngTextualData; +export type PngTransparency = import('./image/parsers/png.js').PngTransparency; +export { UnarchiveEvent, UnarchiveEventType, UnarchiveInfoEvent, UnarchiveErrorEvent, UnarchiveStartEvent, UnarchiveFinishEvent, UnarchiveProgressEvent, UnarchiveExtractEvent, Unarchiver, Unzipper, Unrarrer, Untarrer, getUnarchiver } from "./archive/decompress.js"; export { getFullMIMEString, getShortMIMEString } from "./codecs/codecs.js"; +export { GifParseEventType, GifParser } from "./image/parsers/gif.js"; +export { JpegComponentType, JpegDctType, JpegDensityUnits, JpegExtensionThumbnailFormat, JpegHuffmanTableType, JpegParseEventType, JpegParser, JpegSegmentType } from "./image/parsers/jpeg.js"; +export { PngColorType, PngInterlaceMethod, PngParseEventType, PngParser, PngUnitSpecifier } from "./image/parsers/png.js"; export { convertWebPtoPNG, convertWebPtoJPG } from "./image/webp-shim/webp-shim.js"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/types/index.d.ts.map b/types/index.d.ts.map index eec20b0..5326334 100644 --- a/types/index.d.ts.map +++ b/types/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":";;;;0BASa,OAAO,oBAAoB,EAAE,WAAW;0BAGxC,OAAO,oBAAoB,EAAE,WAAW;wBAGxC,OAAO,oBAAoB,EAAE,SAAS"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":";;;;;0BAQc,OAAO,oBAAoB,EAAE,WAAW;0BACxC,OAAO,oBAAoB,EAAE,WAAW;wBACxC,OAAO,oBAAoB,EAAE,SAAS;sCAEtC,OAAO,wBAAwB,EAAE,uBAAuB;uBACxD,OAAO,wBAAwB,EAAE,QAAQ;kCACzC,OAAO,wBAAwB,EAAE,mBAAmB;yCACpD,OAAO,wBAAwB,EAAE,0BAA0B;wBAC3D,OAAO,wBAAwB,EAAE,SAAS;+BAC1C,OAAO,wBAAwB,EAAE,gBAAgB;oCACjD,OAAO,wBAAwB,EAAE,qBAAqB;iCACtD,OAAO,wBAAwB,EAAE,kBAAkB;gCAEnD,OAAO,yBAAyB,EAAE,iBAAiB;6BACnD,OAAO,yBAAyB,EAAE,cAAc;kCAChD,OAAO,yBAAyB,EAAE,mBAAmB;qCACrD,OAAO,yBAAyB,EAAE,sBAAsB;0CACxD,OAAO,yBAAyB,EAAE,2BAA2B;+BAC7D,OAAO,yBAAyB,EAAE,gBAAgB;8BAClD,OAAO,yBAAyB,EAAE,eAAe;iCAEjD,OAAO,wBAAwB,EAAE,kBAAkB;8BACnD,OAAO,wBAAwB,EAAE,iBAAiB;uBAClD,OAAO,wBAAwB,EAAE,QAAQ;uCACzC,OAAO,wBAAwB,EAAE,wBAAwB;2BACzD,OAAO,wBAAwB,EAAE,YAAY;2BAC7C,OAAO,wBAAwB,EAAE,YAAY;4BAC7C,OAAO,wBAAwB,EAAE,aAAa;6BAC9C,OAAO,wBAAwB,EAAE,cAAc;iCAC/C,OAAO,wBAAwB,EAAE,kBAAkB;6BACnD,OAAO,wBAAwB,EAAE,cAAc;yBAC/C,OAAO,wBAAwB,EAAE,UAAU;yCAC3C,OAAO,wBAAwB,EAAE,0BAA0B;iCAC3D,OAAO,wBAAwB,EAAE,kBAAkB;kCACnD,OAAO,wBAAwB,EAAE,mBAAmB;uCACpD,OAAO,wBAAwB,EAAE,wBAAwB;6BACzD,OAAO,wBAAwB,EAAE,cAAc;8BAC/C,OAAO,wBAAwB,EAAE,eAAe"} \ No newline at end of file diff --git a/types/io/bitbuffer.d.ts b/types/io/bitbuffer.d.ts new file mode 100644 index 0000000..dd99d04 --- /dev/null +++ b/types/io/bitbuffer.d.ts @@ -0,0 +1,55 @@ +/** + * A write-only Bit buffer which uses a Uint8Array as a backing store. + */ +export class BitBuffer { + /** + * @param {number} numBytes The number of bytes to allocate. + * @param {boolean} mtl The bit-packing mode. True means pack bits from most-significant (7) to + * least-significant (0). Defaults false: least-significant (0) to most-significant (8). + */ + constructor(numBytes: number, mtl?: boolean); + /** + * @type {Uint8Array} + * @public + */ + public data: Uint8Array; + /** + * Whether we pack bits from most-significant-bit to least. Defaults false (least-to-most + * significant bit packing). + * @type {boolean} + * @private + */ + private mtl; + /** + * The current byte we are filling with bits. + * @type {number} + * @public + */ + public bytePtr: number; + /** + * Points at the bit within the current byte where the next bit will go. This number ranges + * from 0 to 7 and the direction of packing is indicated by the mtl property. + * @type {number} + * @public + */ + public bitPtr: number; + /** @returns {boolean} */ + getPackingDirection(): boolean; + /** + * Sets the bit-packing direction. Default (false) is least-significant-bit (0) to + * most-significant (7). Changing the bit-packing direction when the bit pointer is in the + * middle of a byte will fill the rest of that byte with 0s using the current bit-packing + * direction and then set the bit pointer to the appropriate bit of the next byte. If there + * are no more bytes left in this buffer, it will throw an error. + */ + setPackingDirection(mtl?: boolean): void; + /** + * writeBits(3, 6) is the same as writeBits(0b000011, 6). + * Will throw an error (without writing) if this would over-flow the buffer. + * @param {number} val The bits to pack into the buffer. Negative values are not allowed. + * @param {number} numBits Must be positive, non-zero and less or equal to than 53, since + * JavaScript can only support 53-bit integers. + */ + writeBits(val: number, numBits: number): void; +} +//# sourceMappingURL=bitbuffer.d.ts.map \ No newline at end of file diff --git a/types/io/bitbuffer.d.ts.map b/types/io/bitbuffer.d.ts.map new file mode 100644 index 0000000..cf8d0d0 --- /dev/null +++ b/types/io/bitbuffer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"bitbuffer.d.ts","sourceRoot":"","sources":["../../io/bitbuffer.js"],"names":[],"mappings":"AAuBA;;GAEG;AACH;IACE;;;;OAIG;IACH,sBAJW,MAAM,QACN,OAAO,EAoCjB;IA5BC;;;OAGG;IACH,aAHU,UAAU,CAGgB;IAEpC;;;;;OAKG;IACH,YAAc;IAEd;;;;OAIG;IACH,gBAHU,MAAM,CAGA;IAEhB;;;;;OAKG;IACH,eAHU,MAAM,CAGc;IAKhC,yBAAyB;IACzB,uBADc,OAAO,CAGpB;IAED;;;;;;OAMG;IACH,yCAkBC;IAED;;;;;;OAMG;IACH,eAJW,MAAM,WACN,MAAM,QA4FhB;CACF"} \ No newline at end of file diff --git a/types/io/bitstream.d.ts b/types/io/bitstream.d.ts index 92186ae..450228d 100644 --- a/types/io/bitstream.d.ts +++ b/types/io/bitstream.d.ts @@ -1,90 +1,132 @@ -export const BitStream: { - new (ab: ArrayBuffer, mtl: boolean, opt_offset: number, opt_length: number): { - /** - * The bytes in the stream. - * @type {Uint8Array} - * @private - */ - bytes: Uint8Array; - /** - * The byte in the stream that we are currently on. - * @type {Number} - * @private - */ - bytePtr: number; - /** - * The bit in the current byte that we will read next (can have values 0 through 7). - * @type {Number} - * @private - */ - bitPtr: number; - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - bitsRead_: number; - peekBits: (n: number, opt_movePointers: any) => number; - /** - * Returns how many bites have been read in the stream since the beginning of time. - * @returns {number} - */ - getNumBitsRead(): number; - /** - * Returns how many bits are currently in the stream left to be read. - * @returns {number} - */ - getNumBitsLeft(): number; - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek, must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_ltm(n: number, opt_movePointers: any): number; - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {number} The peeked bits, as an unsigned number. - */ - peekBits_mtl(n: number, opt_movePointers: any): number; - /** - * Peek at 16 bits from current position in the buffer. - * Bit at (bytePtr,bitPtr) has the highest position in returning data. - * Taken from getbits.hpp in unrar. - * TODO: Move this out of BitStream and into unrar. - * @returns {number} - */ - getBits(): number; - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. Must be a positive integer. - * @returns {number} The read bits, as an unsigned number. - */ - readBits(n: number): number; - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. Must be a positive integer. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n: number, opt_movePointers: any): Uint8Array; - /** - * @param {number} n The number of bytes to read. - * @returns {Uint8Array} The subarray. - */ - readBytes(n: number): Uint8Array; - }; -}; +/** + * This object allows you to peek and consume bits and bytes out of a stream. + * Note that this stream is optimized, and thus, will *NOT* throw an error if + * the end of the stream is reached. Only use this in scenarios where you + * already have all the bits you need. + * + * Bit reading always proceeds from the first byte in the buffer, to the + * second byte, and so on. The MTL flag controls which bit is considered + * first *inside* the byte. The default is least-to-most direction. + * + * An Example for how Most-To-Least vs Least-to-Most mode works: + * + * If you have an ArrayBuffer with the following two Uint8s: + * 185 (0b10111001) and 66 (0b01000010) + * and you perform a series of readBits: 2 bits, then 3, then 5, then 6. + * + * A BitStream in "mtl" mode will yield the following: + * - readBits(2) => 2 ('10') + * - readBits(3) => 7 ('111') + * - readBits(5) => 5 ('00101') + * - readBits(6) => 2 ('000010') + * + * A BitStream in "ltm" mode will yield the following: + * - readBits(2) => 1 ('01') + * - readBits(3) => 6 ('110') + * - readBits(5) => 21 ('10101') + * - readBits(6) => 16 ('010000') + */ +export class BitStream { + /** + * @param {ArrayBuffer} ab An ArrayBuffer object. + * @param {boolean} mtl Whether the stream reads bits from the byte starting with the + * most-significant-bit (bit 7) to least-significant (bit 0). False means the direction is + * from least-significant-bit (bit 0) to most-significant (bit 7). + * @param {Number} opt_offset The offset into the ArrayBuffer + * @param {Number} opt_length The length of this BitStream + */ + constructor(ab: ArrayBuffer, mtl: boolean, opt_offset: number, opt_length: number); + /** + * The bytes in the stream. + * @type {Uint8Array} + * @private + */ + private bytes; + /** + * The byte in the stream that we are currently on. + * @type {Number} + * @private + */ + private bytePtr; + /** + * The bit in the current byte that we will read next (can have values 0 through 7). + * @type {Number} + * @private + */ + private bitPtr; + /** + * An ever-increasing number. + * @type {Number} + * @private + */ + private bitsRead_; + peekBits: (n: number, opt_movePointers: any) => number; + /** + * Returns how many bits have been read in the stream since the beginning of time. + * @returns {number} + */ + getNumBitsRead(): number; + /** + * Returns how many bits are currently in the stream left to be read. + * @returns {number} + */ + getNumBitsLeft(): number; + /** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at least-significant bit (0) of byte0 and moves left until it reaches + * bit7 of byte0, then jumps to bit0 of byte1, etc. + * @param {number} n The number of bits to peek, must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {number} The peeked bits, as an unsigned number. + */ + peekBits_ltm(n: number, opt_movePointers: any): number; + /** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at bit7 of byte0 and moves right until it reaches + * bit0 of byte0, then goes to bit7 of byte1, etc. + * @param {number} n The number of bits to peek. Must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {number} The peeked bits, as an unsigned number. + */ + peekBits_mtl(n: number, opt_movePointers: any): number; + /** + * Peek at 16 bits from current position in the buffer. + * Bit at (bytePtr,bitPtr) has the highest position in returning data. + * Taken from getbits.hpp in unrar. + * TODO: Move this out of BitStream and into unrar. + * @returns {number} + */ + getBits(): number; + /** + * Reads n bits out of the stream, consuming them (moving the bit pointer). + * @param {number} n The number of bits to read. Must be a positive integer. + * @returns {number} The read bits, as an unsigned number. + */ + readBits(n: number): number; + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. Only use this for uncompressed blocks as this throws away remaining + * bits in the current byte. + * @param {number} n The number of bytes to peek. Must be a positive integer. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @returns {Uint8Array} The subarray. + */ + peekBytes(n: number, opt_movePointers: any): Uint8Array; + /** + * @param {number} n The number of bytes to read. + * @returns {Uint8Array} The subarray. + */ + readBytes(n: number): Uint8Array; + /** + * Skips n bits in the stream. Will throw an error if n is < 0 or greater than the number of + * bits left in the stream. + * @param {number} n The number of bits to skip. Must be a positive integer. + * @returns {BitStream} Returns this BitStream for chaining. + */ + skip(n: number): BitStream; +} //# sourceMappingURL=bitstream.d.ts.map \ No newline at end of file diff --git a/types/io/bitstream.d.ts.map b/types/io/bitstream.d.ts.map index 4570782..c8f9e71 100644 --- a/types/io/bitstream.d.ts.map +++ b/types/io/bitstream.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"bitstream.d.ts","sourceRoot":"","sources":["../../io/bitstream.js"],"names":[],"mappings":"AACA;aA8Ce,WAAW,OACX,OAAO;QAchB;;;;WAIG;eAFO,UAAU;QAKpB;;;;WAIG;;QAGH;;;;WAIG;;QAGH;;;;WAIG;;sBAuFM,MAAM,4BAEJ,MAAM;QAnFnB;;;WAGG;0BADU,MAAM;QAMnB;;;WAGG;0BADU,MAAM;QAOnB;;;;;;;;;WASG;wBAHQ,MAAM,0BAEJ,MAAM;QAkDnB;;;;;;;;;WASG;wBAHQ,MAAM,0BAEJ,MAAM;QAgDnB;;;;;;WAMG;mBADU,MAAM;QAQnB;;;;WAIG;oBAFQ,MAAM,GACJ,MAAM;QAMnB;;;;;;;WAOG;qBAHQ,MAAM,0BAEJ,UAAU;QAmDvB;;;WAGG;qBAFQ,MAAM,GACJ,UAAU;;EAQtB"} \ No newline at end of file +{"version":3,"file":"bitstream.d.ts","sourceRoot":"","sources":["../../io/bitstream.js"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;IACE;;;;;;;OAOG;IACH,gBAPW,WAAW,OACX,OAAO,0CA2CjB;IA7BC;;;;OAIG;IACH,cAA+C;IAE/C;;;;OAIG;IACH,gBAAgB;IAEhB;;;;OAIG;IACH,eAAe;IAEf;;;;OAIG;IACH,kBAAkB;IAElB,cAsFS,MAAM,4BAEJ,MAAM,CAxF0C;IAG7D;;;OAGG;IACH,kBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,kBAFa,MAAM,CAKlB;IAED;;;;;;;;;OASG;IACH,gBAJW,MAAM,0BAEJ,MAAM,CAkDlB;IAED;;;;;;;;;OASG;IACH,gBAJW,MAAM,0BAEJ,MAAM,CAgDlB;IAED;;;;;;OAMG;IACH,WAFa,MAAM,CAMlB;IAED;;;;OAIG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;OAOG;IACH,aAJW,MAAM,0BAEJ,UAAU,CAiDtB;IAED;;;OAGG;IACH,aAHW,MAAM,GACJ,UAAU,CAItB;IAED;;;;;OAKG;IACH,QAHW,MAAM,GACJ,SAAS,CAoBrB;CACF"} \ No newline at end of file diff --git a/types/io/bytebuffer.d.ts b/types/io/bytebuffer.d.ts index d815dd0..c2952be 100644 --- a/types/io/bytebuffer.d.ts +++ b/types/io/bytebuffer.d.ts @@ -1,41 +1,52 @@ -export const ByteBuffer: { - new (numBytes: number): { - /** - * @type {Uint8Array} - * @public - */ - data: Uint8Array; - /** - * @type {number} - * @public - */ - ptr: number; - /** - * @param {number} b The byte to insert. - */ - insertByte(b: number): void; - /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. - */ - insertBytes(bytes: Array | Uint8Array | Int8Array): void; - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeNumber(num: number, numBytes: number): void; - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - writeSignedNumber(num: number, numBytes: number): void; - /** - * @param {string} str The ASCII string to write. - */ - writeASCIIString(str: string): void; - }; -}; +/** + * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. + */ +export class ByteBuffer { + /** + * @param {number} numBytes The number of bytes to allocate. + */ + constructor(numBytes: number); + /** + * @type {Uint8Array} + * @public + */ + public data: Uint8Array; + /** + * Points to the byte that will next be written. + * @type {number} + * @public + */ + public ptr: number; + /** + * Returns an exact copy of all the data that has been written to the ByteBuffer. + * @returns {Uint8Array} + */ + getData(): Uint8Array; + /** + * @param {number} b The byte to insert. + */ + insertByte(b: number): void; + /** + * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. + */ + insertBytes(bytes: Array | Uint8Array | Int8Array): void; + /** + * Writes an unsigned number into the next n bytes. If the number is too large + * to fit into n bytes or is negative, an error is thrown. + * @param {number} num The unsigned number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ + writeNumber(num: number, numBytes: number): void; + /** + * Writes a signed number into the next n bytes. If the number is too large + * to fit into n bytes, an error is thrown. + * @param {number} num The signed number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ + writeSignedNumber(num: number, numBytes: number): void; + /** + * @param {string} str The ASCII string to write. + */ + writeASCIIString(str: string): void; +} //# sourceMappingURL=bytebuffer.d.ts.map \ No newline at end of file diff --git a/types/io/bytebuffer.d.ts.map b/types/io/bytebuffer.d.ts.map index c6efdaa..811c7fb 100644 --- a/types/io/bytebuffer.d.ts.map +++ b/types/io/bytebuffer.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"bytebuffer.d.ts","sourceRoot":"","sources":["../../io/bytebuffer.js"],"names":[],"mappings":"AACA;mBAkBe,MAAM;QAOf;;;WAGG;cAFO,UAAU;QAKpB;;;WAGG;aAFO,MAAM;QAOlB;;WAEG;sBADQ,MAAM;QAOjB;;WAEG;2BADQ,MAAO,MAAM,CAAC,GAAC,UAAU,GAAC,SAAS;QAQ9C;;;;;WAKG;yBAFQ,MAAM,YACN,MAAM;QAyBjB;;;;;WAKG;+BAFQ,MAAM,YACN,MAAM;QAuBjB;;WAEG;8BADQ,MAAM;;EAchB"} \ No newline at end of file +{"version":3,"file":"bytebuffer.d.ts","sourceRoot":"","sources":["../../io/bytebuffer.js"],"names":[],"mappings":"AAaA;;GAEG;AACH;IACE;;OAEG;IACH,sBAFW,MAAM,EAmBhB;IAZC;;;OAGG;IACH,aAHU,UAAU,CAGgB;IAEpC;;;;OAIG;IACH,YAHU,MAAM,CAGJ;IAGd;;;OAGG;IACH,WAFa,UAAU,CAMtB;IAED;;OAEG;IACH,cAFW,MAAM,QAShB;IAED;;OAEG;IACH,mBAFW,MAAO,MAAM,CAAC,GAAC,UAAU,GAAC,SAAS,QAU7C;IAED;;;;;OAKG;IACH,iBAHW,MAAM,YACN,MAAM,QAuBhB;IAED;;;;;OAKG;IACH,uBAHW,MAAM,YACN,MAAM,QAqBhB;IAED;;OAEG;IACH,sBAFW,MAAM,QAUhB;CACF"} \ No newline at end of file diff --git a/types/io/bytestream.d.ts b/types/io/bytestream.d.ts index 64fb5d8..cfcf1f2 100644 --- a/types/io/bytestream.d.ts +++ b/types/io/bytestream.d.ts @@ -1,109 +1,150 @@ -export const ByteStream: { - new (ab: ArrayBuffer, opt_offset?: number | undefined, opt_length?: number | undefined): { - /** - * The current page of bytes in the stream. - * @type {Uint8Array} - * @private - */ - bytes: Uint8Array; - /** - * The next pages of bytes in the stream. - * @type {Array} - * @private - */ - pages_: Array; - /** - * The byte in the current page that we will read next. - * @type {Number} - * @private - */ - ptr: number; - /** - * An ever-increasing number. - * @type {Number} - * @private - */ - bytesRead_: number; - /** - * Returns how many bytes have been read in the stream since the beginning of time. - */ - getNumBytesRead(): number; - /** - * Returns how many bytes are currently in the stream left to be read. - */ - getNumBytesLeft(): number; - /** - * Move the pointer ahead n bytes. If the pointer is at the end of the current array - * of bytes and we have another page of bytes, point at the new page. This is a private - * method, no validation is done. - * @param {number} n Number of bytes to increment. - * @private - */ - movePointer_(n: number): void; - /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - peekNumber(n: number): number; - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The n bytes interpreted as an unsigned number. - */ - readNumber(n: number): number; - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - peekSignedNumber(n: number): number; - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {number} The bytes interpreted as a signed number. - */ - readSignedNumber(n: number): number; - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @param {boolean} movePointers Whether to move the pointers. - * @returns {Uint8Array} The subarray. - */ - peekBytes(n: number, movePointers: boolean): Uint8Array; - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {Uint8Array} The subarray. - */ - readBytes(n: number): Uint8Array; - /** - * Peeks at the next n bytes as an ASCII string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - peekString(n: number): string; - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. Must be a positive integer. - * @returns {string} The next n bytes as a string. - */ - readString(n: number): string; - /** - * Feeds more bytes into the back of the stream. - * @param {ArrayBuffer} ab - */ - push(ab: ArrayBuffer): void; - /** - * Creates a new ByteStream from this ByteStream that can be read / peeked. - * @returns {ByteStream} A clone of this ByteStream. - */ - tee(): any; - }; -}; +/** + * This object allows you to peek and consume bytes as numbers and strings out + * of a stream. More bytes can be pushed into the back of the stream via the + * push() method. + * By default, the stream is Little Endian (that is the least significant byte + * is first). To change to Big Endian, use setBigEndian(). + */ +export class ByteStream { + /** + * @param {ArrayBuffer} ab The ArrayBuffer object. + * @param {number=} opt_offset The offset into the ArrayBuffer + * @param {number=} opt_length The length of this ByteStream + */ + constructor(ab: ArrayBuffer, opt_offset?: number | undefined, opt_length?: number | undefined); + /** + * The current page of bytes in the stream. + * @type {Uint8Array} + * @private + */ + private bytes; + /** + * The next pages of bytes in the stream. + * @type {Array} + * @private + */ + private pages_; + /** + * The byte in the current page that we will read next. + * @type {Number} + * @private + */ + private ptr; + /** + * An ever-increasing number. + * @type {Number} + * @private + */ + private bytesRead_; + /** + * Whether the stream is little-endian (true) or big-endian (false). + * @type {boolean} + * @private + */ + private littleEndian_; + /** @returns {boolean} Whether the stream is little-endian (least significant byte is first). */ + isLittleEndian(): boolean; + /** + * Big-Endian means the most significant byte is first. it is sometimes called Motorola-style. + * @param {boolean=} val The value to set. If not present, the stream is set to big-endian. + */ + setBigEndian(val?: boolean | undefined): void; + /** + * Little-Endian means the least significant byte is ifrst. is sometimes called Intel-style. + * @param {boolean=} val The value to set. If not present, the stream is set to little-endian. + */ + setLittleEndian(val?: boolean | undefined): void; + /** + * Returns how many bytes have been consumed (read or skipped) since the beginning of time. + * @returns {number} + */ + getNumBytesRead(): number; + /** + * Returns how many bytes are currently in the stream left to be read. + * @returns {number} + */ + getNumBytesLeft(): number; + /** + * Move the pointer ahead n bytes. If the pointer is at the end of the current array + * of bytes and we have another page of bytes, point at the new page. This is a private + * method, no validation is done. + * @param {number} n Number of bytes to increment. + * @private + */ + private movePointer_; + /** + * Peeks at the next n bytes as an unsigned number but does not advance the + * pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @returns {number} The n bytes interpreted as an unsigned number. + */ + peekNumber(n: number): number; + /** + * Returns the next n bytes as an unsigned number (or -1 on error) + * and advances the stream pointer n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The n bytes interpreted as an unsigned number. + */ + readNumber(n: number): number; + /** + * Returns the next n bytes as a signed number but does not advance the + * pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The bytes interpreted as a signed number. + */ + peekSignedNumber(n: number): number; + /** + * Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {number} The bytes interpreted as a signed number. + */ + readSignedNumber(n: number): number; + /** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @param {boolean} movePointers Whether to move the pointers. + * @returns {Uint8Array} The subarray. + */ + peekBytes(n: number, movePointers: boolean): Uint8Array; + /** + * Reads the next n bytes as a sub-array. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {Uint8Array} The subarray. + */ + readBytes(n: number): Uint8Array; + /** + * Peeks at the next n bytes as an ASCII string but does not advance the pointer. + * @param {number} n The number of bytes to peek at. Must be a positive integer. + * @returns {string} The next n bytes as a string. + */ + peekString(n: number): string; + /** + * Returns the next n bytes as an ASCII string and advances the stream pointer + * n bytes. + * @param {number} n The number of bytes to read. Must be a positive integer. + * @returns {string} The next n bytes as a string. + */ + readString(n: number): string; + /** + * Skips n bytes in the stream. + * @param {number} n The number of bytes to skip. Must be a positive integer. + * @returns {ByteStream} Returns this ByteStream for chaining. + */ + skip(n: number): ByteStream; + /** + * Feeds more bytes into the back of the stream. + * @param {ArrayBuffer} ab + */ + push(ab: ArrayBuffer): void; + /** + * Creates a new ByteStream from this ByteStream that can be read / peeked. + * Note that the teed stream is a disconnected copy. If you push more bytes to the original + * stream, the copy does not get them. + * TODO: Assess whether the above causes more bugs than it avoids. (It would feel weird to me if + * the teed stream shared some state with the original stream.) + * @returns {ByteStream} A clone of this ByteStream. + */ + tee(): ByteStream; +} //# sourceMappingURL=bytestream.d.ts.map \ No newline at end of file diff --git a/types/io/bytestream.d.ts.map b/types/io/bytestream.d.ts.map index b9b05f5..509176c 100644 --- a/types/io/bytestream.d.ts.map +++ b/types/io/bytestream.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"bytestream.d.ts","sourceRoot":"","sources":["../../io/bytestream.js"],"names":[],"mappings":"AACA;aAoBe,WAAW,eACX,MAAM,2BACN,MAAM;QAUf;;;;WAIG;eAFO,UAAU;QAKpB;;;;WAIG;gBAFO,MAAM,UAAU,CAAC;QAK3B;;;;WAIG;;QAGH;;;;WAIG;;QAIL;;WAEG;;QAKH;;WAEG;;QAMH;;;;;;WAMG;wBAFQ,MAAM;QAYjB;;;;;WAKG;sBAFQ,MAAM,GACJ,MAAM;QAqCnB;;;;;WAKG;sBAFQ,MAAM,GACJ,MAAM;QASnB;;;;;WAKG;4BAFQ,MAAM,GACJ,MAAM;QAanB;;;;WAIG;4BAFQ,MAAM,GACJ,MAAM;QASnB;;;;;;WAMG;qBAHQ,MAAM,gBACN,OAAO,GACL,UAAU;QA2CvB;;;;WAIG;qBAFQ,MAAM,GACJ,UAAU;QAMvB;;;;WAIG;sBAFQ,MAAM,GACJ,MAAM;QA+BnB;;;;;WAKG;sBAFQ,MAAM,GACJ,MAAM;QAQnB;;;WAGG;iBADQ,WAAW;QAatB;;;WAGG;;;EAYF"} \ No newline at end of file +{"version":3,"file":"bytestream.d.ts","sourceRoot":"","sources":["../../io/bytestream.js"],"names":[],"mappings":"AAWA;;;;;;GAMG;AACH;IACE;;;;OAIG;IACH,gBAJW,WAAW,eACX,MAAM,2BACN,MAAM,cA4ChB;IAlCC;;;;OAIG;IACH,cAA+C;IAE/C;;;;OAIG;IACH,eAAgB;IAEhB;;;;OAIG;IACH,YAAY;IAEZ;;;;OAIG;IACH,mBAAmB;IAEnB;;;;OAIG;IACH,sBAAyB;IAG3B,gGAAgG;IAChG,kBADc,OAAO,CAGpB;IAED;;;OAGG;IACH,mBAFW,OAAO,oBAIjB;IAED;;;OAGG;IACH,sBAFW,OAAO,oBAIjB;IAED;;;OAGG;IACH,mBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,mBAFa,MAAM,CAKlB;IAED;;;;;;OAMG;IACH,qBAOC;IAED;;;;;OAKG;IACH,cAHW,MAAM,GACJ,MAAM,CAmClB;IAGD;;;;;OAKG;IACH,cAHW,MAAM,GACJ,MAAM,CAMlB;IAGD;;;;;OAKG;IACH,oBAHW,MAAM,GACJ,MAAM,CAUlB;IAGD;;;;OAIG;IACH,oBAHW,MAAM,GACJ,MAAM,CAMlB;IAGD;;;;;;OAMG;IACH,aAJW,MAAM,gBACN,OAAO,GACL,UAAU,CAyCtB;IAED;;;;OAIG;IACH,aAHW,MAAM,GACJ,UAAU,CAItB;IAED;;;;OAIG;IACH,cAHW,MAAM,GACJ,MAAM,CA6BlB;IAED;;;;;OAKG;IACH,cAHW,MAAM,GACJ,MAAM,CAMlB;IAED;;;;OAIG;IACH,QAHW,MAAM,GACJ,UAAU,CAkBtB;IAED;;;OAGG;IACH,SAFW,WAAW,QAWrB;IAED;;;;;;;OAOG;IACH,OAFa,UAAU,CAUtB;CACF"} \ No newline at end of file