UNPKG

node-pkware

Version:

nodejs implementation of StormLib's pkware compressor/de-compressor

321 lines 12.6 kB
import { Buffer } from 'node:buffer'; import { ChBitsAsc, ChCodeAsc, Compression, DictionarySize, DistBits, DistCode, ExLenBits, LenBase, LenBits, LenCode, LITERAL_END_STREAM, } from './constants.js'; import { AbortedError, InvalidCompressionTypeError, InvalidDictionarySizeError } from './errors.js'; import { ExpandingBuffer } from './ExpandingBuffer.js'; import { quotientAndRemainder, getLowestNBitsOf, mergeSparseArrays, nBitsOfOnes, repeat, toHex, unfold, } from './functions.js'; /** * This function assumes there are at least 2 bytes of data in the buffer */ function readHeader(buffer) { const compressionType = buffer.readUint8(0); const dictionarySize = buffer.readUint8(1); if (!(compressionType in Compression) || compressionType === Compression.Unknown) { throw new InvalidCompressionTypeError(); } if (!(dictionarySize in DictionarySize) || dictionarySize === DictionarySize.Unknown) { throw new InvalidDictionarySizeError(); } return { compressionType: compressionType, dictionarySize: dictionarySize, }; } function generateDecodeTables(startIndexes, lengthBits) { const codes = repeat(0, 0x1_00); lengthBits.forEach((lengthBit, i) => { for (let index = startIndexes[i]; index < 0x1_00; index = index + (1 << lengthBit)) { codes[index] = i; } }); return codes; } /** * PAT = populate ascii table */ function createPATIterator(limit, stepper) { return function (n) { if (n >= limit) { return false; } return [n, n + (1 << stepper)]; }; } function populateAsciiTable(value, index, bits, limit) { const iterator = createPATIterator(limit, value - bits); const seed = ChCodeAsc[index] >> bits; const indices = unfold(iterator, seed); const table = []; indices.forEach((idx) => { table[idx] = index; }); return table; } export class Explode { verbose; needMoreInput; isFirstChunk; extraBits; bitBuffer; backupData; lengthCodes; distPosCodes; inputBuffer; outputBuffer; stats; compressionType; dictionarySize; dictionarySizeMask; chBitsAsc; asciiTable2C34; asciiTable2D34; asciiTable2E34; asciiTable2EB4; constructor(config = {}) { this.verbose = config?.verbose ?? false; this.needMoreInput = true; this.isFirstChunk = true; this.extraBits = 0; this.bitBuffer = 0; this.backupData = { extraBits: -1, bitBuffer: -1 }; this.lengthCodes = generateDecodeTables(LenCode, LenBits); this.distPosCodes = generateDecodeTables(DistCode, DistBits); this.inputBuffer = new ExpandingBuffer(0x1_00_00); this.outputBuffer = new ExpandingBuffer(0x4_00_00); this.stats = { chunkCounter: 0 }; this.compressionType = Compression.Unknown; this.dictionarySize = DictionarySize.Unknown; this.dictionarySizeMask = 0; this.chBitsAsc = repeat(0, 0x1_00); this.asciiTable2C34 = repeat(0, 0x1_00); this.asciiTable2D34 = repeat(0, 0x1_00); this.asciiTable2E34 = repeat(0, 0x80); this.asciiTable2EB4 = repeat(0, 0x1_00); } getHandler() { const instance = this; return function (chunk, encoding, callback) { instance.needMoreInput = true; try { instance.inputBuffer.append(chunk); if (instance.isFirstChunk) { instance.isFirstChunk = false; this._flush = instance.onInputFinished.bind(instance); } if (instance.verbose) { instance.stats.chunkCounter = instance.stats.chunkCounter + 1; console.log(`explode: reading ${toHex(chunk.length)} bytes from chunk #${instance.stats.chunkCounter}`); } instance.processChunkData(); const blockSize = 0x10_00; if (instance.outputBuffer.size() <= blockSize) { callback(null, Buffer.from([])); return; } let [numberOfBlocks] = quotientAndRemainder(instance.outputBuffer.size(), blockSize); // making sure to leave one block worth of data for lookback when processing chunk data numberOfBlocks = numberOfBlocks - 1; const numberOfBytes = numberOfBlocks * blockSize; // make sure to create a copy of the output buffer slice as it will get flushed in the next line const output = Buffer.from(instance.outputBuffer.read(0, numberOfBytes)); instance.outputBuffer.flushStart(numberOfBytes); callback(null, output); } catch (error) { callback(error); } }; } generateAsciiTables() { this.chBitsAsc = ChBitsAsc.map((value, index) => { if (value <= 8) { this.asciiTable2C34 = mergeSparseArrays(populateAsciiTable(value, index, 0, 0x1_00), this.asciiTable2C34); return value - 0; } const acc = getLowestNBitsOf(ChCodeAsc[index], 8); if (acc === 0) { this.asciiTable2EB4 = mergeSparseArrays(populateAsciiTable(value, index, 8, 0x1_00), this.asciiTable2EB4); return value - 8; } this.asciiTable2C34[acc] = 0xff; if (getLowestNBitsOf(acc, 6) === 0) { this.asciiTable2E34 = mergeSparseArrays(populateAsciiTable(value, index, 6, 0x80), this.asciiTable2E34); return value - 6; } this.asciiTable2D34 = mergeSparseArrays(populateAsciiTable(value, index, 4, 0x1_00), this.asciiTable2D34); return value - 4; }); } onInputFinished(callback) { if (this.verbose) { console.log('---------------'); console.log('explode: total number of chunks read:', this.stats.chunkCounter); console.log('explode: inputBuffer heap size', toHex(this.inputBuffer.heapSize())); console.log('explode: outputBuffer heap size', toHex(this.outputBuffer.heapSize())); } if (this.needMoreInput) { callback(new AbortedError()); return; } callback(null, this.outputBuffer.read()); } /** * @throws {@link AbortedError} when there isn't enough data to be wasted */ wasteBits(numberOfBits) { if (numberOfBits > this.extraBits && this.inputBuffer.isEmpty()) { throw new AbortedError(); } if (numberOfBits <= this.extraBits) { this.bitBuffer = this.bitBuffer >> numberOfBits; this.extraBits = this.extraBits - numberOfBits; return; } const nextByte = this.inputBuffer.readByte(0); this.inputBuffer.dropStart(1); this.bitBuffer = ((this.bitBuffer >> this.extraBits) | (nextByte << 8)) >> (numberOfBits - this.extraBits); this.extraBits = this.extraBits + 8 - numberOfBits; } /** * @throws {@link AbortedError} */ decodeNextLiteral() { const lastBit = getLowestNBitsOf(this.bitBuffer, 1); this.wasteBits(1); if (lastBit) { let lengthCode = this.lengthCodes[getLowestNBitsOf(this.bitBuffer, 8)]; this.wasteBits(LenBits[lengthCode]); const extraLenghtBits = ExLenBits[lengthCode]; if (extraLenghtBits !== 0) { const extraLength = getLowestNBitsOf(this.bitBuffer, extraLenghtBits); try { this.wasteBits(extraLenghtBits); } catch { if (lengthCode + extraLength !== 0x1_0e) { throw new AbortedError(); } } lengthCode = LenBase[lengthCode] + extraLength; } return lengthCode + 0x1_00; } const lastByte = getLowestNBitsOf(this.bitBuffer, 8); if (this.compressionType === Compression.Binary) { this.wasteBits(8); return lastByte; } let value; if (lastByte > 0) { value = this.asciiTable2C34[lastByte]; if (value === 0xff) { if (getLowestNBitsOf(this.bitBuffer, 6)) { this.wasteBits(4); value = this.asciiTable2D34[getLowestNBitsOf(this.bitBuffer, 8)]; } else { this.wasteBits(6); value = this.asciiTable2E34[getLowestNBitsOf(this.bitBuffer, 7)]; } } } else { this.wasteBits(8); value = this.asciiTable2EB4[getLowestNBitsOf(this.bitBuffer, 8)]; } this.wasteBits(this.chBitsAsc[value]); return value; } /** * @throws {@link AbortedError} */ decodeDistance(repeatLength) { const distPosCode = this.distPosCodes[getLowestNBitsOf(this.bitBuffer, 8)]; const distPosBits = DistBits[distPosCode]; this.wasteBits(distPosBits); let distance; let bitsToWaste; if (repeatLength === 2) { distance = (distPosCode << 2) | getLowestNBitsOf(this.bitBuffer, 2); bitsToWaste = 2; } else { distance = (distPosCode << this.dictionarySize) | (this.bitBuffer & this.dictionarySizeMask); bitsToWaste = this.dictionarySize; } this.wasteBits(bitsToWaste); return distance + 1; } processChunkData() { if (this.inputBuffer.isEmpty()) { return; } if (this.compressionType === Compression.Unknown) { const headerParsedSuccessfully = this.parseInitialData(); if (!headerParsedSuccessfully || this.inputBuffer.isEmpty()) { return; } } this.needMoreInput = false; this.backup(); try { let nextLiteral = this.decodeNextLiteral(); while (nextLiteral !== LITERAL_END_STREAM) { if (nextLiteral >= 0x1_00) { const repeatLength = nextLiteral - 0xfe; const minusDistance = this.decodeDistance(repeatLength); const availableData = this.outputBuffer.read(this.outputBuffer.size() - minusDistance, repeatLength); let addition; if (repeatLength > minusDistance) { const multipliedData = repeat(availableData, Math.ceil(repeatLength / availableData.length)); addition = Buffer.concat(multipliedData).subarray(0, repeatLength); } else { addition = availableData; } this.outputBuffer.append(addition); } else { this.outputBuffer.appendByte(nextLiteral); } this.backup(); nextLiteral = this.decodeNextLiteral(); } } catch { this.needMoreInput = true; } if (this.needMoreInput) { this.restore(); } } parseInitialData() { if (this.inputBuffer.size() < 4) { return false; } const { compressionType, dictionarySize } = readHeader(this.inputBuffer.read(0, 2)); this.compressionType = compressionType; this.dictionarySize = dictionarySize; this.bitBuffer = this.inputBuffer.readByte(2); this.inputBuffer.dropStart(3); this.dictionarySizeMask = nBitsOfOnes(dictionarySize); if (this.compressionType === Compression.Ascii) { this.generateAsciiTables(); } if (this.verbose) { console.log(`explode: compression type: ${Compression[this.compressionType]}`); console.log(`explode: compression level: ${DictionarySize[this.dictionarySize]}`); } return true; } backup() { this.backupData.extraBits = this.extraBits; this.backupData.bitBuffer = this.bitBuffer; this.inputBuffer.saveIndices(); } restore() { this.extraBits = this.backupData.extraBits; this.bitBuffer = this.backupData.bitBuffer; this.inputBuffer.restoreIndices(); } } //# sourceMappingURL=Explode.js.map