Added Zstd Compress / Zstd Decompress operations.

This commit is contained in:
Leon Zandman 2026-03-01 01:33:51 +01:00
parent 1eccfa729b
commit 8bb4748c5e
8 changed files with 242 additions and 1 deletions

7
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@astronautlabs/amf": "^0.0.6", "@astronautlabs/amf": "^0.0.6",
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@blu3r4y/lzma": "^2.3.3", "@blu3r4y/lzma": "^2.3.3",
"@bokuweb/zstd-wasm": "^0.0.27",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.11", "@xmldom/xmldom": "^0.8.11",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
@ -1838,6 +1839,12 @@
"lzma.js": "bin/lzma.js" "lzma.js": "bin/lzma.js"
} }
}, },
"node_modules/@bokuweb/zstd-wasm": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/@bokuweb/zstd-wasm/-/zstd-wasm-0.0.27.tgz",
"integrity": "sha512-GDm2uOTK3ESjnYmSeLQifJnBsRCWajKLvN32D2ZcQaaCIJI/Hse9s74f7APXjHit95S10UImsRGkTsbwHmrtmg==",
"license": "MIT"
},
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.10.2", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",

View File

@ -99,6 +99,7 @@
"@astronautlabs/amf": "^0.0.6", "@astronautlabs/amf": "^0.0.6",
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@blu3r4y/lzma": "^2.3.3", "@blu3r4y/lzma": "^2.3.3",
"@bokuweb/zstd-wasm": "^0.0.27",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.11", "@xmldom/xmldom": "^0.8.11",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",

View File

@ -402,7 +402,9 @@
"LZMA Compress", "LZMA Compress",
"LZ4 Decompress", "LZ4 Decompress",
"LZ4 Compress", "LZ4 Compress",
"LZNT1 Decompress" "LZNT1 Decompress",
"Zstd Compress",
"Zstd Decompress"
] ]
}, },
{ {

30
src/core/lib/Zstd.mjs Normal file
View File

@ -0,0 +1,30 @@
/**
* Zstd shared initialisation.
*
* Both ZstdCompress and ZstdDecompress import from here so that
* WebAssembly.instantiate is called exactly once, regardless of how many
* operations use it or in what order they run.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import { init, compress, decompress } from "@bokuweb/zstd-wasm";
let initPromise = null;
/**
* Returns a promise that resolves once the Zstd WASM module is ready.
* Safe to call multiple times the module is only instantiated once.
*
* @returns {Promise<void>}
*/
export function zstdInit() {
if (!initPromise) {
initPromise = init();
}
return initPromise;
}
export { compress, decompress };

View File

@ -0,0 +1,61 @@
/**
* @author Leon Zandman [leon@wirwar.com]
* @copyright Crown Copyright 2027
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
import { zstdInit, compress } from "../lib/Zstd.mjs";
/**
* Zstd Compress operation
*/
class ZstdCompress extends Operation {
/**
* ZstdCompress constructor
*/
constructor() {
super();
this.name = "Zstd Compress";
this.module = "Compression";
this.description = "Compresses data using the Zstandard (Zstd) algorithm. Zstd offers high compression ratios at fast speeds and is widely used in Linux, databases, container images, and network protocols.";
this.infoURL = "https://wikipedia.org/wiki/Zstandard";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.args = [
{
name: "Compression level",
type: "number",
value: 3,
min: 1,
max: 22
}
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {ArrayBuffer}
*/
async run(input, args) {
const [level] = args;
if (input.byteLength === 0) throw new OperationError("Please provide an input.");
if (isWorkerEnvironment()) self.sendStatusMessage("Loading Zstd...");
await zstdInit();
if (isWorkerEnvironment()) self.sendStatusMessage("Compressing data...");
try {
const result = compress(new Uint8Array(input), level);
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
} catch (err) {
throw new OperationError(`Failed to compress: ${err.message}`);
}
}
}
export default ZstdCompress;

View File

@ -0,0 +1,59 @@
/**
* @author Leon Zandman [leon@wirwar.com]
* @copyright Crown Copyright 2027
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
import { zstdInit, decompress } from "../lib/Zstd.mjs";
/**
* Zstd Decompress operation
*/
class ZstdDecompress extends Operation {
/**
* ZstdDecompress constructor
*/
constructor() {
super();
this.name = "Zstd Decompress";
this.module = "Compression";
this.description = "Decompresses data compressed with the Zstandard (Zstd) algorithm.";
this.infoURL = "https://wikipedia.org/wiki/Zstandard";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.args = [];
this.checks = [
{
pattern: "^\\x28\\xb5\\x2f\\xfd",
flags: "",
args: []
}
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {ArrayBuffer}
*/
async run(input, args) {
if (input.byteLength === 0) throw new OperationError("Please provide an input.");
if (isWorkerEnvironment()) self.sendStatusMessage("Loading Zstd...");
await zstdInit();
if (isWorkerEnvironment()) self.sendStatusMessage("Decompressing data...");
try {
const result = decompress(new Uint8Array(input));
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
} catch (err) {
throw new OperationError(`Failed to decompress: ${err.message}`);
}
}
}
export default ZstdDecompress;

View File

@ -182,6 +182,7 @@ import "./tests/JSONtoYAML.mjs";
import "./tests/YARA.mjs"; import "./tests/YARA.mjs";
import "./tests/ParseCSR.mjs"; import "./tests/ParseCSR.mjs";
import "./tests/XXTEA.mjs"; import "./tests/XXTEA.mjs";
import "./tests/Zstd.mjs";
const testStatus = { const testStatus = {
allTestsPassing: true, allTestsPassing: true,

View File

@ -0,0 +1,80 @@
/**
* Zstd tests.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Zstd compress & decompress: string",
input: "The cat sat on the mat.",
expectedOutput: "The cat sat on the mat.",
recipeConfig: [
{
op: "Zstd Compress",
args: [3]
},
{
op: "Zstd Decompress",
args: []
}
]
},
{
// Generated using: node --input-type=module -e "import {init,compress} from '@bokuweb/zstd-wasm'; await init(); const r=compress(new TextEncoder().encode('The cat sat on the mat.'),3); console.log(Buffer.from(r).toString('hex'));"
name: "Zstd compress: level 3",
input: "The cat sat on the mat.",
expectedOutput: "28b52ffd2017b900005468652063617420736174206f6e20746865206d61742e",
recipeConfig: [
{
op: "Zstd Compress",
args: [3]
},
{
op: "To Hex",
args: ["None", 0]
}
]
},
{
// Generated using: node --input-type=module -e "import {init,compress} from '@bokuweb/zstd-wasm'; await init(); const r=compress(new TextEncoder().encode('The cat sat on the mat.'),3); console.log(Buffer.from(r).toString('hex'));"
name: "Zstd decompress: known vector",
input: "28b52ffd2017b900005468652063617420736174206f6e20746865206d61742e",
expectedOutput: "The cat sat on the mat.",
recipeConfig: [
{
op: "From Hex",
args: ["None"]
},
{
op: "Zstd Decompress",
args: []
}
]
},
{
name: "Zstd compress: empty input error",
input: "",
expectedOutput: "Please provide an input.",
recipeConfig: [
{
op: "Zstd Compress",
args: [3]
}
]
},
{
name: "Zstd decompress: empty input error",
input: "",
expectedOutput: "Please provide an input.",
recipeConfig: [
{
op: "Zstd Decompress",
args: []
}
]
}
]);