node-pkware
Version:
nodejs implementation of StormLib's pkware compressor/de-compressor
308 lines • 11.6 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;
function getSizeOfMatching(inputBytes, a, b) {
const limit = clamp(2, LONGEST_ALLOWED_REPETITION, b - a);
const view = new Uint8Array(inputBytes);
for (let i = 2; i <= limit; i++) {
if (view[a + i] !== view[b + i]) {
return i;
}
}
return limit;
}
function matchesAt(needle, haystack) {
if (needle.byteLength === 0 || haystack.byteLength === 0) {
return -1;
}
const needleView = new Uint8Array(needle);
const haystackView = new Uint8Array(haystack);
for (let i = 0; i < haystack.byteLength - needle.byteLength; i++) {
let matches = true;
for (let j = 0; j < needle.byteLength; j++) {
if (haystackView[i + j] !== needleView[j]) {
matches = false;
break;
}
}
if (matches) {
return i;
}
}
return -1;
}
/**
* TODO: make sure that we find the most recent one,
* which in turn allows us to store backward length in less amount of bits
* currently the code goes from the furthest point
*/
function findRepetitions(inputBytes, endOfLastMatch, cursor) {
const notEnoughBytes = inputBytes.byteLength - cursor < 2;
const tooClose = cursor === endOfLastMatch || cursor - endOfLastMatch < 2;
if (notEnoughBytes || tooClose) {
return { size: 0, distance: 0 };
}
const haystack = inputBytes.slice(endOfLastMatch, cursor);
const needle = inputBytes.slice(cursor, cursor + 2);
const matchIndex = matchesAt(needle, haystack);
if (matchIndex !== -1) {
const distance = cursor - endOfLastMatch - matchIndex;
let size = 2;
if (distance > 2) {
size = getSizeOfMatching(inputBytes, endOfLastMatch + matchIndex, cursor);
}
return { distance: distance - 1, size };
}
return { size: 0, distance: 0 };
}
export class Implode {
inputBuffer;
inputBufferView;
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.inputBufferStartIndex = 0;
this.outputBuffer = new ArrayBuffer(input.byteLength + 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.inputBuffer.byteLength === 0) {
return;
}
if (this.inputBuffer.byteLength <= 2) {
this.skipFirstTwoBytes();
return;
}
this.skipFirstTwoBytes();
// -------------------------------
// work in progress
const endOfLastMatch = 0; // used when searching for longer repetitions later
while (this.inputBuffer.byteLength - this.inputBufferStartIndex > 0) {
let data;
if (endOfLastMatch > 0) {
data = findRepetitions(this.inputBuffer.slice(endOfLastMatch), endOfLastMatch, this.inputBufferStartIndex);
}
else {
data = findRepetitions(this.inputBuffer, endOfLastMatch, this.inputBufferStartIndex);
}
const { size, distance } = data;
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.inputBufferStartIndex = this.inputBufferStartIndex - blockSize;
}
}
}
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.inputBuffer.byteLength - 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