Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@camoto/gamecomp

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@camoto/gamecomp - npm Package Compare versions

Comparing version 3.0.0 to 4.0.0

478

formats/cmp-lzss.js

@@ -30,2 +30,3 @@ /*

import { RecordBuffer, RecordType } from '@camoto/record-io-buffer';
import { BitStream, BitView } from 'bit-buffer';
import Debug from '../util/debug.js';

@@ -48,5 +49,5 @@ const g_debug = Debug.extend(FORMAT_ID);

*
* The length/offset code is read as a single 16-bit integer, which is then
* split into the separate length and offset values. How this split happens is
* controlled by multiple options.
* In byte mode (bitstream = false), the length/offset code is read as a single
* 16-bit integer, which is then split into the separate length and offset
* values. How this split happens is controlled by multiple options.
*

@@ -56,4 +57,4 @@ * Assuming the two bytes are AB followed by CD (i.e. a big-endian value 0xABCD)

*
* Split | lengthHigh | bigEndian | offsetRotate
* ------+------------+-----------+-------------
* Split | lengthHigh | bigEndian | rotateDistance
* ------+------------+-----------+---------------
* ABC+D | false | true | 0

@@ -77,3 +78,4 @@ * A+BCD | true | true | 0

{
static metadata() {
static metadata()
{
return {

@@ -83,14 +85,25 @@ id: FORMAT_ID,

options: {
sizeLength: 'Size of the backreference length field, in bits [4]',
minLen: 'Minimum length of a backreference string, in bytes [3]',
prefillByte: 'Byte used to prefill the sliding window buffer [0x00]',
windowStartAt0: 'true to start at the beginning of the window, false '
+ 'to start at the end of the window (false)',
littleEndian: 'Endian of 16-bit offset+length value: '
+ 'true = little, false = big [false]',
bitstream: 'Embed flag bits in the stream (true) or group them into '
+ 'an initial byte. [false]',
invertFlag: 'If true, flag=0 is codeword, if false flag=1 is codeword '
+ '[false]',
lengthHigh: 'Whether the backreference length field is stored in the '
+ 'upper (most significant) bits of the code (true) or the lower '
+ 'bits. [false]',
offsetRotate: 'How many bits to rotate the backreference offset '
littleEndian: 'Endian of 16-bit offset+length value: '
+ 'true = little, false = big [false]',
minDistance: 'Minimum distance of a backreference string (actual '
+ 'distance when value is 0), in bytes [0]',
minLength: 'Minimum length of a backreference string (actual length when '
+ 'value is 0), in bytes [3]',
rotateDistance: 'How many bits to rotate the backreference distance '
+ 'field [0]',
prefillByte: 'Byte used to prefill the sliding window buffer [0x00]',
relativeDistance: 'Backreference distance is relative to current '
+ 'window position (true) or to the start of the sliding window '
+ '[false]',
sizeDistance: 'Size of the backreference distance field, in bits [12]',
sizeLength: 'Size of the backreference length field, in bits [4]',
windowStartAt0: 'true to start at the beginning of the window, false '
+ 'to start at the end of the window (false)',
},

@@ -100,25 +113,40 @@ };

static reveal(content, options = {}) {
static reveal(content, options = {})
{
const debug = g_debug.extend('reveal');
options.bitstream = parseBool(options.bitstream);
options.invertFlag = parseBool(options.invertFlag);
options.lengthHigh = parseBool(options.lengthHigh);
options.littleEndian = parseBool(options.littleEndian);
options.minDistance = parseInt(options.minDistance || 0);
// minLength can't be 0.
options.minLength = parseInt(options.minLength || 3);
options.rotateDistance = parseInt(options.rotateDistance || 0);
options.prefillByte = parseInt(options.prefillByte || 0x00);
options.relativeDistance = parseBool(options.relativeDistance);
// sizeDistance can't be 0.
options.sizeDistance = parseInt(options.sizeDistance || 12);
// sizeLength can't be 0.
options.sizeLength = parseInt(options.sizeLength || 4);
options.minLen = parseInt(options.minLen || 3);
options.prefillByte = parseInt(options.prefillByte || 0x00);
options.windowStartAt0 = parseBool(options.windowStartAt0);
options.littleEndian = parseBool(options.littleEndian);
options.lengthHigh = parseBool(options.lengthHigh);
options.offsetRotate = parseInt(options.offsetRotate || 0);
if (options.sizeLength > 8) {
throw new Error('Backreference length fields longer than 8 bits are not supported.');
if (!options.bitstream) { // byte mode
if (options.sizeLength > 8) {
throw new Error('Backreference length fields longer than 8 bits are '
+ 'not supported in byte mode.');
}
if (options.sizeLength + options.sizeDistance != 16) {
throw new Error('Backreference length + distance fields must total '
+ '16 bits in byte mode.');
}
}
const sizeOffset = 16 - options.sizeLength;
const windowSize = (1 << sizeOffset);
const maxBackrefSize = (1 << options.sizeLength) + options.minLen - 1;
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefSize;
const windowSize = (1 << options.sizeDistance);
const maxBackrefLength = (1 << options.sizeLength) + options.minLength - 1;
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefLength;
// 2 bytes for each of 8 backrefs, plus flag byte
const minEncodedBytesPer8Chunks = (8 * 2) + 1;
const maxDecodedBytesPer8Chunks = (8 * maxBackrefSize);
const maxDecodedBytesPer8Chunks = (8 * maxBackrefLength);

@@ -128,83 +156,148 @@ const maxTheoreticalCompressionRatio =

let output = new RecordBuffer(content.length * maxTheoreticalCompressionRatio);
// Calculate the shifts and masks needed to extract the length and
// offset values from the word we just read.
let sizeShift, offShift;
let shiftLength, shiftDistance;
if (options.lengthHigh) {
sizeShift = sizeOffset;
offShift = 0;
shiftLength = options.sizeDistance;
shiftDistance = 0;
} else {
sizeShift = 0;
offShift = options.sizeLength;
shiftLength = 0;
shiftDistance = options.sizeLength;
}
const sizeMask = (1 << options.sizeLength) - 1;
const offMask = (1 << sizeOffset) - 1;
const maskLength = (1 << options.sizeLength) - 1;
const maskDistance = (1 << options.sizeDistance) - 1;
let input = new RecordBuffer(content);
let output = new RecordBuffer(content.length * maxTheoreticalCompressionRatio);
let windowPos = windowStartPos;
let slidingWindow = new Array(windowSize).fill(options.prefillByte);
const fnReadWord = input.read.bind(
input,
options.littleEndian ? RecordType.int.u16le : RecordType.int.u16be
);
const invert = options.invertFlag ? 1 : 0;
while (input.distFromEnd() > 0) {
let fnNextFlag, fnReadByte, fnReadLengthDistance, fnBitsRemaining;
if (options.bitstream) {
// Support functions for bit-level LZSS.
let bs = new BitStream(
new BitView(content.buffer, content.byteOffset, content.byteLength)
);
bs.bigEndian = !options.littleEndian;
let flagBitPos = 0;
const flagByte = input.read(RecordType.int.u8);
fnNextFlag = () => bs.readBits(1, false) ^ invert;
while (flagBitPos < 8) {
fnReadByte = () => bs.readBits(8, false);
// if the flag indicates that this chunk is a literal...
if ((flagByte & (1 << flagBitPos)) != 0) {
if (input.distFromEnd() <= 0) break;
fnReadLengthDistance = () => {
let sizeA, sizeB;
if (options.lengthHigh) {
sizeA = options.sizeLength;
sizeB = options.sizeDistance;
} else {
sizeA = options.sizeDistance;
sizeB = options.sizeLength;
}
return [
bs.readBits(sizeA, false),
bs.readBits(sizeB, false),
];
};
const literal = input.read(RecordType.int.u8);
output.write(RecordType.int.u8, literal);
fnBitsRemaining = () => bs.bitsLeft;
slidingWindow[windowPos++] = literal;
windowPos %= windowSize;
} else {
// Support functions for byte-level LZSS.
let input = new RecordBuffer(content);
} else { // otherwise, this chunk is a backreference
let flagBitPos = 8;
let flagByte = 0;
fnNextFlag = () => {
if (flagBitPos >= 8) {
flagByte = input.read(RecordType.int.u8);
flagBitPos = 0;
}
const f = flagByte & 1;
flagByte >>= 1;
flagBitPos++;
return f ^ invert;
};
// verify that there are enough bytes left in the input stream to
// describe the backreference
if (input.distFromEnd() < 2) break;
fnReadByte = () => {
return input.read(RecordType.int.u8);
};
const brWord = fnReadWord();
// Put this outside fnReadLengthDistance() so we avoid a conditional on
// each read operation.
const fnReadWord = input.read.bind(
input,
options.littleEndian ? RecordType.int.u16le : RecordType.int.u16be
);
let backrefOffset = (brWord >> offShift) & offMask;
let backrefSize = (brWord >> sizeShift) & sizeMask;
fnReadLengthDistance = () => {
const brWord = fnReadWord();
backrefSize += options.minLen;
let backrefDistance = (brWord >> shiftDistance) & maskDistance;
let backrefLength = (brWord >> shiftLength) & maskLength;
// Rotate the offset by the given number of bits, e.g. 0xABC rotated
// by 4 = BCA.
if (options.offsetRotate) {
backrefOffset =
(
(backrefOffset << options.offsetRotate)
| (backrefOffset >> (sizeOffset - options.offsetRotate))
) & offMask
;
}
return [
backrefLength,
backrefDistance,
];
};
// copy the bytes from the sliding window to the output, and also
// to the end of the sliding window
for (let brByteIdx = 0; brByteIdx < backrefSize; brByteIdx++) {
fnBitsRemaining = () => input.distFromEnd() * 8;
}
const curByte = slidingWindow[backrefOffset];
output.write(RecordType.int.u8, curByte);
while (fnBitsRemaining() > 0) {
slidingWindow[windowPos] = curByte;
const flag = fnNextFlag();
++backrefOffset; backrefOffset %= windowSize;
++windowPos; windowPos %= windowSize;
}
// if the flag indicates that this chunk is a backreference...
if (flag) {
// verify that there are enough bytes left in the input stream to
// describe the backreference
if (fnBitsRemaining() < 16) break;
let [ backrefLength, backrefDistance ] = fnReadLengthDistance();
// Rotate the offset by the given number of bits, e.g. 0xABC rotated
// by 4 = BCA.
if (options.rotateDistance) {
backrefDistance =
(
(backrefDistance << options.rotateDistance)
| (backrefDistance >> (options.sizeDistance - options.rotateDistance))
) & maskDistance
;
}
flagBitPos++;
backrefLength += options.minLength;
backrefDistance += options.minDistance;
if (options.relativeDistance) {
backrefDistance = windowSize + windowPos - backrefDistance;
}
// copy the bytes from the sliding window to the output, and also
// to the end of the sliding window
for (let brByteIdx = 0; brByteIdx < backrefLength; brByteIdx++) {
backrefDistance %= windowSize;
const curByte = slidingWindow[backrefDistance];
output.write(RecordType.int.u8, curByte);
slidingWindow[windowPos] = curByte;
++backrefDistance;
++windowPos; windowPos %= windowSize;
}
} else { // otherwise, this chunk is a literal
if (fnBitsRemaining() < 8) break;
const literal = fnReadByte();
output.write(RecordType.int.u8, literal);
slidingWindow[windowPos++] = literal;
windowPos %= windowSize;
}
}

@@ -215,3 +308,5 @@

static obscure(content, options = {}) {
static obscure(content, options = {})
{
const debug = g_debug.extend('obscure');

@@ -266,16 +361,21 @@ function findMatch(r_inputPos, buf, bufSize, maxMatchLength) {

const debug = g_debug.extend('obscure');
options.bitstream = parseBool(options.bitstream);
options.invertFlag = parseBool(options.invertFlag);
options.lengthHigh = parseBool(options.lengthHigh);
options.littleEndian = parseBool(options.littleEndian);
options.minDistance = parseInt(options.minDistance || 0);
// minLength can't be 0 (infinite loop).
options.minLength = parseInt(options.minLength || 3);
options.rotateDistance = parseInt(options.rotateDistance || 0);
options.prefillByte = parseInt(options.prefillByte || 0x00);
options.relativeDistance = parseBool(options.relativeDistance);
// sizeDistance can't be 0.
options.sizeDistance = parseInt(options.sizeDistance || 12);
// sizeLength can't be 0.
options.sizeLength = parseInt(options.sizeLength || 4);
options.minLen = parseInt(options.minLen || 3);
options.prefillByte = parseInt(options.prefillByte || 0x00);
options.windowStartAt0 = parseBool(options.windowStartAt0);
options.littleEndian = parseBool(options.littleEndian);
options.lengthHigh = parseBool(options.lengthHigh);
options.offsetRotate = parseInt(options.offsetRotate || 0);
const sizeOffset = 16 - options.sizeLength;
const windowSize = (1 << sizeOffset);
const maxBackrefSize = (1 << options.sizeLength) + options.minLen - 1;
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefSize;
const windowSize = (1 << options.sizeDistance);
const maxBackrefLength = (1 << options.sizeLength) + options.minLength - 1;
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefLength;

@@ -285,36 +385,136 @@ // worst case is an increase in content size by 12.5% (negative compression)

let textBuf = new Uint8Array(windowSize + maxBackrefSize - 1).fill(options.prefillByte);
let textBuf = new Uint8Array(windowSize + maxBackrefLength - 1).fill(options.prefillByte);
let r_windowInputPos = windowStartPos;
let inputPos = 0;
let chunkIndex = 0;
let inputStringLen = 0;
let i = 0;
// s starts maxBackrefSize bytes after windowStartPos, wrapping around to
// s starts maxBackrefLength bytes after windowStartPos, wrapping around to
// the start of the window if needed.
let s = (windowStartPos + maxBackrefSize + windowSize) % windowSize;
let s = (windowStartPos + maxBackrefLength + windowSize) % windowSize;
let sizeShift, offShift;
let shiftLength, shiftDistance;
if (options.lengthHigh) {
sizeShift = sizeOffset;
offShift = 0;
shiftLength = options.sizeDistance;
shiftDistance = 0;
} else {
sizeShift = 0;
offShift = options.sizeLength;
shiftLength = 0;
shiftDistance = options.sizeLength;
}
const offMask = (1 << sizeOffset) - 1;
const maskDistance = (1 << options.sizeDistance) - 1;
// we buffer up 8 chunks at a time (a mixture of literals and backreferences)
// because the flags that indicate the chunk type are packed together into
// a single prefix byte
let chunkBuf = new Array();
const invert = options.invertFlag ? 1 : 0;
// init the flag byte with all the flags clear;
// we'll OR-in individual flags as literals are added
chunkBuf.push(0);
let fnWriteLiteral, fnWriteLengthDistance, fnFlush;
if (options.bitstream) {
// Support functions for bit-level LZSS.
let bs = new BitStream(
new BitView(output.buffer, output.byteOffset, output.byteLength)
);
bs.bigEndian = !options.littleEndian;
fnWriteLiteral = literalByte => {
bs.writeBits(0 ^ invert, 1);
bs.writeBits(literalByte, 8);
};
fnWriteLengthDistance = (backrefLength, backrefDistance) => {
bs.writeBits(1 ^ invert, 1);
if (options.lengthHigh) {
bs.writeBits(backrefLength, options.sizeLength);
bs.writeBits(backrefDistance, options.sizeDistance);
} else {
bs.writeBits(backrefDistance, options.sizeDistance);
bs.writeBits(backrefLength, options.sizeLength);
}
};
fnFlush = () => {
// Write zero bits until the next byte boundary.
const bitsLeft = (8 - (bs.index % 8)) % 8;
if (bitsLeft) bs.writeBits(0, bitsLeft);
return new Uint8Array(output.buffer, 0, bs.byteIndex);
};
} else {
// Support functions for byte-level LZSS.
let chunkIndex = 0;
// we buffer up 8 chunks at a time (a mixture of literals and
// backreferences) because the flags that indicate the chunk type are
// packed together into a single prefix byte
let chunkBuf = new Array();
// init the flag byte with all the flags clear;
// we'll OR-in individual flags as literals are added
chunkBuf.push(0);
function flushChunks() {
// once we've finished 8 chunks, write them to the output
if (++chunkIndex >= 8) {
const binaryChunkBuf = new Uint8Array(chunkBuf);
output.put(binaryChunkBuf);
chunkIndex = 0;
chunkBuf.length = 0;
chunkBuf.push(0);
}
}
fnWriteLiteral = literalByte => {
// set the flag indicating that this chunk is a literal
chunkBuf[0] |= ((0 ^ invert) << chunkIndex);
chunkBuf.push(literalByte);
flushChunks();
};
fnWriteLengthDistance = (backrefLength, backrefDistance) => {
chunkBuf[0] |= ((1 ^ invert) << chunkIndex);
// Rotate the offset by the given number of bits, e.g. 0xABC rotated
// by 4 = CAB. This is the opposite direction to when we read it.
if (options.rotateDistance) {
backrefDistance =
(
(backrefDistance >> options.rotateDistance)
| (backrefDistance << (options.sizeDistance - options.rotateDistance))
) & maskDistance
;
}
backrefLength <<= shiftLength;
backrefDistance <<= shiftDistance;
const word = backrefDistance | backrefLength;
if (options.littleEndian) {
chunkBuf.push(word & 0xFF);
chunkBuf.push(word >> 8);
} else {
chunkBuf.push(word >> 8);
chunkBuf.push(word & 0xFF);
}
flushChunks();
};
fnFlush = () => {
// if there's a partial set of eight chunks, write it to the output
if (chunkIndex > 0) {
const binaryChunkBuf = new Uint8Array(chunkBuf);
output.put(binaryChunkBuf);
}
return output.getU8();
};
}
// Fill the read buffer until it is full or the end of the data is reached.
const lenRead = Math.min(
maxBackrefSize - inputStringLen,
maxBackrefLength - inputStringLen,
content.length - inputPos

@@ -335,3 +535,3 @@ );

let { bestMatchStartPos, bestMatchLen } =
findMatch(r_windowInputPos, textBuf, windowSize, maxBackrefSize);
findMatch(r_windowInputPos, textBuf, windowSize, maxBackrefLength);

@@ -343,47 +543,21 @@ if (bestMatchLen > inputStringLen) {

// if a match was found, produce a backreference in the output stream
if (bestMatchLen >= options.minLen) {
if (
(bestMatchLen >= options.minLength)
&& (bestMatchStartPos >= options.minDistance)
) {
let backrefOffset = bestMatchStartPos;
let backrefSize = (bestMatchLen - options.minLen) << sizeShift;
// Rotate the offset by the given number of bits, e.g. 0xABC rotated
// by 4 = CAB. This is the opposite direction to when we read it.
if (options.offsetRotate) {
backrefOffset =
(
(backrefOffset >> options.offsetRotate)
| (backrefOffset << (sizeOffset - options.offsetRotate))
) & offMask
;
if (options.relativeDistance) {
bestMatchStartPos = (windowSize + r_windowInputPos - bestMatchStartPos) % windowSize;
}
backrefOffset <<= offShift;
let backrefDistance = bestMatchStartPos - options.minDistance;
let backrefLength = bestMatchLen - options.minLength;
const word = backrefOffset | backrefSize;
if (options.littleEndian) {
chunkBuf.push(word & 0xFF);
chunkBuf.push(word >> 8);
} else {
chunkBuf.push(word >> 8);
chunkBuf.push(word & 0xFF);
}
fnWriteLengthDistance(backrefLength, backrefDistance);
} else { // otherwise, produce a literal
bestMatchLen = 1;
// set the flag indicating that this chunk is a literal
chunkBuf[0] |= (1 << chunkIndex);
chunkBuf.push(textBuf[r_windowInputPos]);
fnWriteLiteral(textBuf[r_windowInputPos]);
}
// once we've finished 8 chunks, write them to the output
if (++chunkIndex >= 8) {
const binaryChunkBuf = new Uint8Array(chunkBuf);
output.put(binaryChunkBuf);
chunkIndex = 0;
chunkBuf.length = 0;
chunkBuf.push(0);
}
// replace the matched parts of the string

@@ -394,3 +568,3 @@ i = 0;

textBuf[s] = content[inputPos];
if (s < (maxBackrefSize - 1)) {
if (s < (maxBackrefLength - 1)) {
textBuf[s + windowSize] = content[inputPos];

@@ -413,10 +587,4 @@ }

// if there's a partial set of eight chunks, write it to the output
if (chunkIndex > 0) {
const binaryChunkBuf = new Uint8Array(chunkBuf);
output.put(binaryChunkBuf);
}
return output.getU8();
return fnFlush();
}
}
{
"name": "@camoto/gamecomp",
"version": "3.0.0",
"version": "4.0.0",
"description": "Apply and remove compression and encryption algorithms used by DOS games",

@@ -5,0 +5,0 @@ "bin": {

@@ -14,2 +14,4 @@ # gamecomp.js

* cmp-lzss: Generic LZSS (Lempel-Ziv-Storer-Szymanski)
* Byte mode (8 flag bits stored upfront in a single byte)
* Bit mode (each flag bit stored before each literal or length+distance code)
* cmp-lzw: Generic LZW (Lempel-Ziv-Welch)

@@ -16,0 +18,0 @@ * cmp-rle-bash: Monster Bash *.DAT run-length-encoding

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc