node-pkware
Version:
node.js implementation of StormLib's pkware compressor/de-compressor
304 lines • 11.2 kB
JavaScript
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