UNPKG

node-pkware

Version:

node.js implementation of StormLib's pkware compressor/de-compressor

304 lines 11.2 kB
import { ChBitsAsc, ChCodeAsc, DistBits, DistCode, ExLenBits, LenBits, LenCode, LONGEST_ALLOWED_REPETITION, } from '../constants.js'; import { clamp, getLowestNBitsOf, repeat, nBitsOfOnes } from '../functions.js'; /** * in bytes */ const SIZE_OF_HEADER = 3; /** * in bytes */ const MAX_SIZE_OF_TERMINATION_LITERAL = 2; /** * lookup table used for finding repetitions easier * * key = 16 bit combination of 2 uint8 bytes * value = latest occurrance within the buffer */ let lastOccurrences = {}; /** * function assumes a < b - 2 */ function getSizeOfMatching(view, a, b) { const limit = clamp(b - a, 2, LONGEST_ALLOWED_REPETITION); for (let i = 2; i <= limit; i++) { if (view[a + i] !== view[b + i]) { return i; } } return limit; } function readUint16(view, at) { const highByte = view[at]; const lowByte = view[at + 1]; return (highByte << 8) | lowByte; } function findRepetitions(view, inputBytesLength, cursor) { const notEnoughBytes = cursor + 2 > inputBytesLength; const tooClose = cursor < 2; if (notEnoughBytes || tooClose) { return { size: 0, distance: 0 }; } const needle = readUint16(view, cursor); const matchedAt = lastOccurrences[needle] ?? -1; lastOccurrences[needle] = cursor; if (matchedAt === -1) { return { size: 0, distance: 0 }; } if (matchedAt === cursor - 2) { return { distance: 1, size: 2 }; } return { distance: cursor - matchedAt - 1, size: getSizeOfMatching(view, matchedAt, cursor), }; } export class Implode { inputBuffer; /** * Used for accessing the data within inputBuffer */ inputBufferView; /** * Used for caching inputBuffer.byteLength as that getter is doing some uncached computation to measure the length of * inputBuffer */ inputBufferSize; /** * The implode algorithm works by trimming off the beginning of inputBuffer byte by byte. Instead of actually * adjusting the inputBuffer every time a byte is handled we store the beginning of the unhandled section and use it * when indexing data that is being read. */ inputBufferStartIndex; outputBuffer; outputBufferView; outputBufferSize; dictionarySizeMask; distCodes; distBits; outBits; nChBits; nChCodes; constructor(input, compressionType, dictionarySize) { this.dictionarySizeMask = 0; this.distCodes = structuredClone(DistCode); this.distBits = structuredClone(DistBits); this.outBits = 0; this.nChBits = repeat(0, 0x3_06); this.nChCodes = repeat(0, 0x3_06); this.setupTables(compressionType, dictionarySize); this.inputBuffer = input; this.inputBufferView = new Uint8Array(this.inputBuffer); this.inputBufferSize = this.inputBuffer.byteLength; this.inputBufferStartIndex = 0; this.outputBuffer = new ArrayBuffer(this.inputBufferSize + SIZE_OF_HEADER + MAX_SIZE_OF_TERMINATION_LITERAL); this.outputBufferView = new Uint8Array(this.outputBuffer); this.outputBufferSize = 0; this.outputHeader(compressionType, dictionarySize); this.processInput(dictionarySize); this.writeTerminationLiteral(); } getResult() { return this.outputBuffer.slice(0, this.outputBufferSize); } setupTables(compressionType, dictionarySize) { switch (compressionType) { case 'ascii': { for (let nCount = 0; nCount < 0x1_00; nCount++) { this.nChBits[nCount] = ChBitsAsc[nCount] + 1; this.nChCodes[nCount] = ChCodeAsc[nCount] * 2; } break; } case 'binary': { let nChCode = 0; for (let nCount = 0; nCount < 0x1_00; nCount++) { this.nChBits[nCount] = 9; this.nChCodes[nCount] = nChCode; nChCode = getLowestNBitsOf(nChCode, 16) + 2; } break; } } switch (dictionarySize) { case 'small': { this.dictionarySizeMask = nBitsOfOnes(4); break; } case 'medium': { this.dictionarySizeMask = nBitsOfOnes(5); break; } case 'large': { this.dictionarySizeMask = nBitsOfOnes(6); break; } } let nCount = 0x1_00; for (let i = 0; i < 0x10; i++) { for (let nCount2 = 0; nCount2 < 1 << ExLenBits[i]; nCount2++) { this.nChBits[nCount] = ExLenBits[i] + LenBits[i] + 1; this.nChCodes[nCount] = (nCount2 << (LenBits[i] + 1)) | (LenCode[i] * 2) | 1; nCount = nCount + 1; } } } outputHeader(compressionType, dictionarySize) { switch (compressionType) { case 'ascii': { this.outputBufferView[0] = 1; break; } case 'binary': { this.outputBufferView[0] = 0; break; } } switch (dictionarySize) { case 'small': { this.outputBufferView[1] = 4; break; } case 'medium': { this.outputBufferView[1] = 5; break; } case 'large': { this.outputBufferView[1] = 6; break; } } this.outputBufferView[2] = 0; this.outputBufferSize = 3; } processInput(dictionarySize) { if (this.inputBufferSize === 0) { return; } if (this.inputBufferSize <= 2) { this.skipFirstTwoBytes(); return; } this.skipFirstTwoBytes(); while (this.inputBufferSize > this.inputBufferStartIndex) { const { size, distance } = findRepetitions(this.inputBufferView, this.inputBufferSize, this.inputBufferStartIndex); const isFlushable = this.isRepetitionFlushable(size, distance); if (isFlushable === false) { const byte = this.inputBufferView[this.inputBufferStartIndex]; this.outputBits(this.nChBits[byte], this.nChCodes[byte]); this.inputBufferStartIndex = this.inputBufferStartIndex + 1; } else { const byte = size + 0xfe; this.outputBits(this.nChBits[byte], this.nChCodes[byte]); if (size === 2) { const byte = distance >> 2; this.outputBits(this.distBits[byte], this.distCodes[byte]); this.outputBits(2, distance & 3); } else { switch (dictionarySize) { case 'small': { const byte = distance >> 4; this.outputBits(this.distBits[byte], this.distCodes[byte]); this.outputBits(4, this.dictionarySizeMask & distance); break; } case 'medium': { const byte = distance >> 5; this.outputBits(this.distBits[byte], this.distCodes[byte]); this.outputBits(5, this.dictionarySizeMask & distance); break; } case 'large': { const byte = distance >> 6; this.outputBits(this.distBits[byte], this.distCodes[byte]); this.outputBits(6, this.dictionarySizeMask & distance); break; } } } this.inputBufferStartIndex = this.inputBufferStartIndex + size; } let blockSize; switch (dictionarySize) { case 'small': { blockSize = 0x4_00; break; } case 'medium': { blockSize = 0x8_00; break; } case 'large': { blockSize = 0x10_00; break; } } if (this.inputBufferStartIndex >= blockSize) { this.inputBuffer = this.inputBuffer.slice(blockSize); this.inputBufferView = new Uint8Array(this.inputBuffer); this.inputBufferSize = this.inputBufferSize - blockSize; this.inputBufferStartIndex = this.inputBufferStartIndex - blockSize; lastOccurrences = {}; } } } writeTerminationLiteral() { this.outputBits(this.nChBits.at(-1), this.nChCodes.at(-1)); } /** * @returns false - non flushable * @returns true - flushable * @returns null - flushable, but there might be a better repetition */ isRepetitionFlushable(size, distance) { if (size === 0) { return false; } // If we found repetition of 2 bytes, that is 0x1_00 or further back, // don't bother. Storing the distance of 0x1_00 bytes would actually // take more space than storing the 2 bytes as-is. if (size === 2 && distance >= 0x1_00) { return false; } if (size >= 8 || this.inputBufferSize - this.inputBufferStartIndex < 2) { return true; } return null; } /** * repetitions are at least 2 bytes long, * so the initial 2 bytes can be moved to the output as is */ skipFirstTwoBytes() { const [byte1, byte2] = this.inputBufferView; this.outputBits(this.nChBits[byte1], this.nChCodes[byte1]); this.outputBits(this.nChBits[byte2], this.nChCodes[byte2]); this.inputBufferStartIndex = this.inputBufferStartIndex + 2; } outputBits(numberOfBits, bitBuffer) { if (numberOfBits > 8) { this.outputBits(8, bitBuffer); bitBuffer = bitBuffer >> 8; numberOfBits = numberOfBits - 8; } const oldOutBits = this.outBits; this.outputBufferView[this.outputBufferSize - 1] = this.outputBufferView[this.outputBufferSize - 1] | getLowestNBitsOf(bitBuffer << oldOutBits, 8); this.outBits = this.outBits + numberOfBits; if (this.outBits > 8) { this.outBits = getLowestNBitsOf(this.outBits, 3); bitBuffer = bitBuffer >> (8 - oldOutBits); this.outputBufferView[this.outputBufferSize] = getLowestNBitsOf(bitBuffer, 8); this.outputBufferSize = this.outputBufferSize + 1; } else { this.outBits = getLowestNBitsOf(this.outBits, 3); if (this.outBits === 0) { this.outputBufferView[this.outputBufferSize] = 0; this.outputBufferSize = this.outputBufferSize + 1; } } } } //# sourceMappingURL=Implode.js.map