Merge 4692abfdaf5a91d307919d8d9977bce0c99f0ec3 into 61deeb81653677b6f04fc01e1e8c4edd2104ca59

This commit is contained in:
Leon Zandman 2026-06-08 15:28:34 +02:00 committed by GitHub
commit f59857d390
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 319 additions and 1 deletions

7
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@alexaltea/capstone-js": "^3.0.5", "@alexaltea/capstone-js": "^3.0.5",
"@astronautlabs/amf": "^0.0.6", "@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3", "@blu3r4y/lzma": "^2.3.3",
"@bokuweb/zstd-wasm": "^0.0.27",
"@noble/hashes": "2.2.0", "@noble/hashes": "2.2.0",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.13", "@xmldom/xmldom": "^0.8.13",
@ -1847,6 +1848,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.3", "version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",

View File

@ -98,6 +98,7 @@
"@astronautlabs/amf": "^0.0.6", "@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3", "@blu3r4y/lzma": "^2.3.3",
"@noble/hashes": "2.2.0", "@noble/hashes": "2.2.0",
"@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.13", "@xmldom/xmldom": "^0.8.13",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",

View File

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

View File

@ -1533,6 +1533,32 @@ export const FILE_SIGNATURES = {
}, },
extractor: extractZlib extractor: extractZlib
}, },
{
name: "Zstandard (zstd)",
extension: "zst",
mime: "application/zstd",
description: "",
signature: {
0: 0x28,
1: 0xb5,
2: 0x2f,
3: 0xfd
},
extractor: null
},
{
name: "Zstandard skippable frame (zstd)",
extension: "zst",
mime: "application/zstd",
description: "",
signature: {
0: [0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f],
1: 0x2a,
2: 0x4d,
3: 0x18
},
extractor: null
},
{ {
name: "xz compression", name: "xz compression",
extension: "xz", extension: "xz",

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

@ -0,0 +1,33 @@
/**
* 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 Leon Zandman [leon@wirwar.com]
* @copyright Crown Copyright 2026
* @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) {
const wasmUrl = typeof self !== "undefined" && self.docURL
? `${self.docURL}/assets/zstd.wasm`
: undefined;
initPromise = init(wasmUrl);
}
return initPromise;
}
export { compress, decompress };

View File

@ -0,0 +1,64 @@
/**
* @author Leon Zandman [leon@wirwar.com]
* @copyright Crown Copyright 2026
* @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. Higher compression levels result in better compression but slower speed.";
this.infoURL = "https://wikipedia.org/wiki/Zstandard";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.args = [
{
name: "Compression level",
type: "option",
value: [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
"21", "22"
],
defaultIndex: 2
}
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {ArrayBuffer}
*/
async run(input, args) {
const level = parseInt(args[0], 10);
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 2026
* @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

@ -199,6 +199,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,110 @@
/**
* Zstd tests.
*
* @author Leon Zandman [leon@wirwar.com]
* @copyright Crown Copyright 2026
* @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 & decompress: level 1",
input: "The cat sat on the mat.",
expectedOutput: "The cat sat on the mat.",
recipeConfig: [
{
op: "Zstd Compress",
args: ["1"]
},
{
op: "Zstd Decompress",
args: []
}
]
},
{
name: "Zstd compress & decompress: level 22",
input: "The cat sat on the mat.",
expectedOutput: "The cat sat on the mat.",
recipeConfig: [
{
op: "Zstd Compress",
args: ["22"]
},
{
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: []
}
]
}
]);

View File

@ -99,6 +99,10 @@ module.exports = {
context: "node_modules/node-forge/dist", context: "node_modules/node-forge/dist",
from: "prime.worker.min.js", from: "prime.worker.min.js",
to: "assets/forge/" to: "assets/forge/"
}, {
context: "node_modules/@bokuweb/zstd-wasm/dist/web/",
from: "zstd.wasm",
to: "assets/"
} }
] ]
}), }),
@ -110,6 +114,17 @@ module.exports = {
operations: [ operations: [
new ReplaceOperation("once", "if (pixelSize < elementMinSize)", "if (false)") new ReplaceOperation("once", "if (pixelSize < elementMinSize)", "if (false)")
] ]
},
{
// @bokuweb/zstd-wasm's init() unconditionally evaluates
// `new URL("./zstd.wasm", import.meta.url)` before checking
// whether a path was passed in. Webpack's `publicPath: ""`
// makes that base URL invalid and the constructor throws.
// Neuter the offending line; we always pass a path from Zstd.mjs.
test: /@bokuweb[\/\\]zstd-wasm[\/\\]dist[\/\\]web[\/\\]index\.web\.js$/,
operations: [
new ReplaceOperation("once", "new URL(`./zstd.wasm`, import.meta.url).href", "''")
]
} }
] ]
}) })