add LZNT1 compression operation
This commit is contained in:
parent
d735496641
commit
25347b9f2b
@ -417,6 +417,7 @@
|
||||
"LZMA Compress",
|
||||
"LZ4 Decompress",
|
||||
"LZ4 Compress",
|
||||
"LZNT1 Compress",
|
||||
"LZNT1 Decompress"
|
||||
]
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
*
|
||||
* LZNT1 Decompress.
|
||||
* LZNT1 compression and decompression.
|
||||
*
|
||||
* @author 0xThiebaut [thiebaut.dev]
|
||||
* @copyright Crown Copyright 2023
|
||||
@ -13,6 +13,8 @@ import Utils from "../Utils.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
const COMPRESSED_MASK = 1 << 15,
|
||||
SIGNATURE_MASK = 3 << 12,
|
||||
BLOCK_SIZE = 4096,
|
||||
SIZE_MASK = (1 << 12) - 1;
|
||||
|
||||
/**
|
||||
@ -28,6 +30,116 @@ function getDisplacement(offset) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} output
|
||||
* @param {number} header
|
||||
*/
|
||||
function appendBlockHeader(output, header) {
|
||||
output.push(...Utils.intToByteArray(header, 2, "little"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} block
|
||||
* @param {number} pos
|
||||
* @returns {{offset: number, length: number, lengthBits: number}|null}
|
||||
*/
|
||||
function findBestMatch(block, pos) {
|
||||
const displacement = getDisplacement(pos - 1),
|
||||
lengthBits = 12 - displacement,
|
||||
maxOffset = Math.min(pos, 1 << (4 + displacement)),
|
||||
maxLength = Math.min(block.length - pos, (0xFFF >> displacement) + 3);
|
||||
|
||||
let bestOffset = 0,
|
||||
bestLength = 0;
|
||||
|
||||
for (let offset = 1; offset <= maxOffset; offset++) {
|
||||
let length = 0;
|
||||
while (
|
||||
length < maxLength &&
|
||||
block[pos + length] === block[pos - offset + length]
|
||||
) {
|
||||
length++;
|
||||
}
|
||||
|
||||
if (length > bestLength) {
|
||||
bestOffset = offset;
|
||||
bestLength = length;
|
||||
if (bestLength === maxLength) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLength < 3) return null;
|
||||
return {
|
||||
offset: bestOffset,
|
||||
length: bestLength,
|
||||
lengthBits,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} block
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
function compressBlock(block) {
|
||||
const compressed = [];
|
||||
let pos = 0;
|
||||
|
||||
while (pos < block.length) {
|
||||
const headerIndex = compressed.length;
|
||||
let header = 0,
|
||||
bit = 1,
|
||||
tokens = 0;
|
||||
|
||||
compressed.push(0);
|
||||
|
||||
while (tokens < 8 && pos < block.length) {
|
||||
const match = findBestMatch(block, pos);
|
||||
|
||||
if (match) {
|
||||
const pointer = ((match.offset - 1) << match.lengthBits) | (match.length - 3);
|
||||
compressed.push(...Utils.intToByteArray(pointer, 2, "little"));
|
||||
header |= bit;
|
||||
pos += match.length;
|
||||
} else {
|
||||
compressed.push(block[pos++]);
|
||||
}
|
||||
|
||||
bit <<= 1;
|
||||
tokens++;
|
||||
}
|
||||
|
||||
compressed[headerIndex] = header;
|
||||
}
|
||||
|
||||
return compressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} uncompressed
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
export function compress(uncompressed) {
|
||||
const compressed = [];
|
||||
|
||||
for (let offset = 0; offset < uncompressed.length; offset += BLOCK_SIZE) {
|
||||
const block = uncompressed.slice(offset, offset + BLOCK_SIZE),
|
||||
compressedBlock = compressBlock(block);
|
||||
|
||||
if (block.length === 1 || compressedBlock.length < block.length) {
|
||||
appendBlockHeader(
|
||||
compressed,
|
||||
SIGNATURE_MASK | COMPRESSED_MASK | (compressedBlock.length - 1)
|
||||
);
|
||||
compressed.push(...compressedBlock);
|
||||
} else {
|
||||
appendBlockHeader(compressed, SIGNATURE_MASK | (block.length - 1));
|
||||
compressed.push(...block);
|
||||
}
|
||||
}
|
||||
|
||||
return compressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} compressed
|
||||
* @returns {byteArray}
|
||||
|
||||
41
src/core/operations/LZNT1Compress.mjs
Normal file
41
src/core/operations/LZNT1Compress.mjs
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @author skyswordw
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import {compress} from "../lib/LZNT1.mjs";
|
||||
|
||||
/**
|
||||
* LZNT1 Compress operation
|
||||
*/
|
||||
class LZNT1Compress extends Operation {
|
||||
|
||||
/**
|
||||
* LZNT1 Compress constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "LZNT1 Compress";
|
||||
this.module = "Compression";
|
||||
this.description = "Compresses data using the LZNT1 algorithm.<br><br>Similar to the Windows API <code>RtlCompressBuffer</code>.";
|
||||
this.infoURL = "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/5655f4a3-6ba4-489b-959f-e1f407c52f15";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
run(input, args) {
|
||||
return compress(input);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default LZNT1Compress;
|
||||
@ -681,6 +681,10 @@ WWFkYSBZYWRh\r
|
||||
assert.strictEqual(chef.LZNT1Decompress("\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot").toString(), "compressedtestdatacompressedalot");
|
||||
}),
|
||||
|
||||
it("LZNT1 Compress", () => {
|
||||
assert.strictEqual(chef.LZNT1Decompress(chef.LZNT1Compress("compressedtestdatacompressedalot")).toString(), "compressedtestdatacompressedalot");
|
||||
}),
|
||||
|
||||
it("MD6", () => {
|
||||
assert.strictEqual(chef.MD6("Head Over Heels", {key: "arty"}).toString(), "d8f7fe4931fbaa37316f76283d5f615f50ddd54afdc794b61da522556aee99ad");
|
||||
}),
|
||||
@ -1178,4 +1182,3 @@ ExifImageHeight: 57`);
|
||||
|
||||
|
||||
]);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* LZNT1 Decompress tests.
|
||||
* LZNT1 tests.
|
||||
*
|
||||
* @author 0xThiebaut [thiebaut.dev]
|
||||
* @copyright Crown Copyright 2023
|
||||
@ -8,6 +8,21 @@
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "LZNT1 Compress: repeated input",
|
||||
input: "compressedtestdatacompressedalot",
|
||||
expectedOutput: "1a b0 00 63 6f 6d 70 72 65 73 73 00 65 64 74 65 73 74 64 61 04 74 61 07 88 61 6c 6f 74",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "LZNT1 Compress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "To Hex",
|
||||
args: ["Space", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "LZNT1 Decompress",
|
||||
input: "\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot",
|
||||
@ -18,5 +33,66 @@ TestRegister.addTests([
|
||||
args: []
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "LZNT1 Compress: incompressible input",
|
||||
input: "abcdefghijklmnopqrstuvwxyz",
|
||||
expectedOutput: "19 30 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "LZNT1 Compress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "To Hex",
|
||||
args: ["Space", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "LZNT1 Compress/Decompress: binary",
|
||||
input: "00 01 02 03 00 01 02 03 00 01 02 03 ff ff ff ff 00",
|
||||
expectedOutput: "00 01 02 03 00 01 02 03 00 01 02 03 ff ff ff ff 00",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Hex",
|
||||
args: ["Space"]
|
||||
},
|
||||
{
|
||||
op: "LZNT1 Compress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "LZNT1 Decompress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "To Hex",
|
||||
args: ["Space", 0]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "LZNT1 Compress/Decompress: single byte",
|
||||
input: "41",
|
||||
expectedOutput: "41",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Hex",
|
||||
args: ["None"]
|
||||
},
|
||||
{
|
||||
op: "LZNT1 Compress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "LZNT1 Decompress",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
op: "To Hex",
|
||||
args: ["None", 0]
|
||||
}
|
||||
],
|
||||
}
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user