add LZNT1 compression operation
This commit is contained in:
parent
d735496641
commit
25347b9f2b
@ -417,6 +417,7 @@
|
|||||||
"LZMA Compress",
|
"LZMA Compress",
|
||||||
"LZ4 Decompress",
|
"LZ4 Decompress",
|
||||||
"LZ4 Compress",
|
"LZ4 Compress",
|
||||||
|
"LZNT1 Compress",
|
||||||
"LZNT1 Decompress"
|
"LZNT1 Decompress"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* LZNT1 Decompress.
|
* LZNT1 compression and decompression.
|
||||||
*
|
*
|
||||||
* @author 0xThiebaut [thiebaut.dev]
|
* @author 0xThiebaut [thiebaut.dev]
|
||||||
* @copyright Crown Copyright 2023
|
* @copyright Crown Copyright 2023
|
||||||
@ -13,6 +13,8 @@ import Utils from "../Utils.mjs";
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
const COMPRESSED_MASK = 1 << 15,
|
const COMPRESSED_MASK = 1 << 15,
|
||||||
|
SIGNATURE_MASK = 3 << 12,
|
||||||
|
BLOCK_SIZE = 4096,
|
||||||
SIZE_MASK = (1 << 12) - 1;
|
SIZE_MASK = (1 << 12) - 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,6 +30,116 @@ function getDisplacement(offset) {
|
|||||||
return result;
|
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
|
* @param {byteArray} compressed
|
||||||
* @returns {byteArray}
|
* @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");
|
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", () => {
|
it("MD6", () => {
|
||||||
assert.strictEqual(chef.MD6("Head Over Heels", {key: "arty"}).toString(), "d8f7fe4931fbaa37316f76283d5f615f50ddd54afdc794b61da522556aee99ad");
|
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]
|
* @author 0xThiebaut [thiebaut.dev]
|
||||||
* @copyright Crown Copyright 2023
|
* @copyright Crown Copyright 2023
|
||||||
@ -8,6 +8,21 @@
|
|||||||
import TestRegister from "../../lib/TestRegister.mjs";
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
TestRegister.addTests([
|
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",
|
name: "LZNT1 Decompress",
|
||||||
input: "\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot",
|
input: "\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot",
|
||||||
@ -18,5 +33,66 @@ TestRegister.addTests([
|
|||||||
args: []
|
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