node-pkware
Version:
node.js implementation of StormLib's pkware compressor/de-compressor
381 lines • 15.2 kB
JavaScript
import { ChBitsAsc, ChCodeAsc, DistBits, DistCode, EMPTY_BUFFER, ExLenBits, LenBase, LenBits, LenCode, LITERAL_END_STREAM, } from '../constants.js';
import { AbortedError, InvalidCompressionTypeError, InvalidDictionarySizeError } from '../errors.js';
import { getLowestNBitsOf, mergeSparseArrays, nBitsOfOnes, repeat, unfold, concatArrayBuffersAndLengthedDatas, sliceArrayBufferAt, uint8ArrayToArray, } from '../functions.js';
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 {
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 explode 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;
needMoreInput;
extraBits;
bitBuffer;
lengthCodes;
distPosCodes;
compressionType;
dictionarySize;
dictionarySizeMask;
chBitsAsc;
/**
* the naming comes from stormlib, the 2C34 refers to the table's position in memory
*/
asciiTable2C34;
/**
* the naming comes from stormlib, the 2D34 refers to the table's position in memory
*/
asciiTable2D34;
/**
* the naming comes from stormlib, the 2E34 refers to the table's position in memory
*/
asciiTable2E34;
/**
* the naming comes from stormlib, the 2EB4 refers to the table's position in memory
*/
asciiTable2EB4;
constructor(input) {
this.needMoreInput = true;
this.extraBits = 0;
this.bitBuffer = 0;
this.lengthCodes = generateDecodeTables(LenCode, LenBits);
this.distPosCodes = generateDecodeTables(DistCode, DistBits);
this.inputBuffer = input;
this.inputBufferView = new Uint8Array(this.inputBuffer);
this.inputBufferSize = this.inputBuffer.byteLength;
this.inputBufferStartIndex = 0;
this.outputBuffer = EMPTY_BUFFER;
this.outputBufferView = new Uint8Array(this.outputBuffer);
this.outputBufferSize = 0;
this.compressionType = 'unknown';
this.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);
this.processInput();
if (this.needMoreInput) {
throw new AbortedError();
}
}
/**
* @throws `InvalidCompressionTypeError`
* @throws `InvalidDictionarySizeError`
* @throws `AbortedError`
*/
getResult() {
return this.outputBuffer.slice(0, this.outputBufferSize);
}
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;
});
}
/**
* @throws `AbortedError` when there isn't enough data to be wasted
*/
wasteBits(numberOfBits) {
if (numberOfBits > this.extraBits && this.inputBufferSize - this.inputBufferStartIndex === 0) {
throw new AbortedError();
}
if (numberOfBits <= this.extraBits) {
this.bitBuffer = this.bitBuffer >> numberOfBits;
this.extraBits = this.extraBits - numberOfBits;
return;
}
const nextByte = this.inputBufferView[this.inputBufferStartIndex];
this.inputBufferStartIndex = this.inputBufferStartIndex + 1;
this.bitBuffer = ((this.bitBuffer >> this.extraBits) | (nextByte << 8)) >> (numberOfBits - this.extraBits);
this.extraBits = this.extraBits + 8 - numberOfBits;
}
/**
* @throws `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 === '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 `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 {
switch (this.dictionarySize) {
case 'small': {
distance = (distPosCode << 4) | (this.bitBuffer & this.dictionarySizeMask);
bitsToWaste = 4;
break;
}
case 'medium': {
distance = (distPosCode << 5) | (this.bitBuffer & this.dictionarySizeMask);
bitsToWaste = 5;
break;
}
case 'large': {
distance = (distPosCode << 6) | (this.bitBuffer & this.dictionarySizeMask);
bitsToWaste = 6;
break;
}
}
}
this.wasteBits(bitsToWaste);
return distance + 1;
}
processInput() {
const headerParsedSuccessfully = this.parseInitialData();
if (!headerParsedSuccessfully || this.inputBufferSize - this.inputBufferStartIndex === 0) {
return;
}
this.needMoreInput = false;
const additions = [];
let additionsByteSum = 0;
const finalizedChunks = [];
const blockSize = 0x10_00;
try {
let nextLiteral = this.decodeNextLiteral();
while (nextLiteral !== LITERAL_END_STREAM) {
// we have a character literal here
if (nextLiteral < 0x1_00) {
additions.push({ data: [nextLiteral], byteLength: 1 });
additionsByteSum = additionsByteSum + 1;
nextLiteral = this.decodeNextLiteral();
continue;
}
// we have some bytes to copy from earlier bytes, which is referred to as the "repetition":
// nextLiteral holds information on both how far back the start of the repetition is
// and the info on how long the repetition is
const repeatLength = nextLiteral - 0xfe;
const minusDistance = this.decodeDistance(repeatLength);
// dump the beginning of the output buffer if outputBuffer and the additions exceed 2 blocks
if (this.outputBufferSize + additionsByteSum > blockSize * 2) {
this.outputBufferSize = this.outputBufferSize + additionsByteSum;
this.outputBuffer = concatArrayBuffersAndLengthedDatas([this.outputBuffer, ...additions], this.outputBufferSize);
this.outputBufferView = new Uint8Array(this.outputBuffer);
additions.length = 0;
additionsByteSum = 0;
const [a, b] = sliceArrayBufferAt(this.outputBuffer, blockSize);
finalizedChunks.push(a);
this.outputBuffer = b;
this.outputBufferView = new Uint8Array(this.outputBuffer);
this.outputBufferSize = this.outputBufferSize - blockSize;
}
const start = this.outputBufferSize + additionsByteSum - minusDistance;
// only add the additions if the "repetition" bleeds into the bytes of "additions"
if (this.outputBufferSize < start + repeatLength) {
this.outputBufferSize = this.outputBufferSize + additionsByteSum;
this.outputBuffer = concatArrayBuffersAndLengthedDatas([this.outputBuffer, ...additions], this.outputBufferSize);
this.outputBufferView = new Uint8Array(this.outputBuffer);
additions.length = 0;
additionsByteSum = 0;
}
const availableDataLength = Math.min(start + repeatLength, this.outputBufferSize) - start;
const availableData = {
data: uint8ArrayToArray(this.outputBufferView, start, availableDataLength),
byteLength: availableDataLength,
};
if (repeatLength > minusDistance) {
const repeats = Math.ceil(repeatLength / availableData.byteLength);
const multipliedData = repeat(availableData, repeats);
const addition = concatArrayBuffersAndLengthedDatas(multipliedData, repeatLength * repeats).slice(0, repeatLength);
additions.push(addition);
additionsByteSum = additionsByteSum + repeatLength;
}
else {
additions.push(availableData);
additionsByteSum = additionsByteSum + availableData.byteLength;
}
nextLiteral = this.decodeNextLiteral();
}
}
catch {
this.needMoreInput = true;
}
this.outputBufferSize = finalizedChunks.length * blockSize + this.outputBufferSize + additionsByteSum;
this.outputBuffer = concatArrayBuffersAndLengthedDatas([...finalizedChunks, this.outputBuffer, ...additions], this.outputBufferSize);
this.outputBufferView = new Uint8Array(this.outputBuffer);
}
parseInitialData() {
if (this.inputBufferSize < 4) {
return false;
}
const { compressionType, dictionarySize } = this.readHeader();
this.compressionType = compressionType;
this.dictionarySize = dictionarySize;
this.bitBuffer = this.inputBufferView[this.inputBufferStartIndex + 2];
this.inputBufferStartIndex = this.inputBufferStartIndex + 3;
switch (dictionarySize) {
case 'small': {
this.dictionarySizeMask = nBitsOfOnes(4);
break;
}
case 'medium': {
this.dictionarySizeMask = nBitsOfOnes(5);
break;
}
case 'large': {
this.dictionarySizeMask = nBitsOfOnes(6);
break;
}
}
if (this.compressionType === 'ascii') {
this.generateAsciiTables();
}
return true;
}
/**
* This function assumes there are at least 2 bytes of data in the buffer
*
* @throws `InvalidCompressionTypeError`
* @throws `InvalidDictionarySizeError`
*/
readHeader() {
let compressionType;
switch (this.inputBufferView[0]) {
case 0: {
compressionType = 'binary';
break;
}
case 1: {
compressionType = 'ascii';
break;
}
default: {
throw new InvalidCompressionTypeError();
}
}
let dictionarySize;
switch (this.inputBufferView[1]) {
case 4: {
dictionarySize = 'small';
break;
}
case 5: {
dictionarySize = 'medium';
break;
}
case 6: {
dictionarySize = 'large';
break;
}
default: {
throw new InvalidDictionarySizeError();
}
}
return {
compressionType,
dictionarySize,
};
}
}
//# sourceMappingURL=Explode.js.map