add string index lookup operation

This commit is contained in:
skywalker 2026-06-06 19:16:11 +08:00
parent d735496641
commit abd6667a3e
4 changed files with 157 additions and 0 deletions

View File

@ -324,6 +324,7 @@
"Filter",
"Head",
"Tail",
"String index lookup",
"Count occurrences",
"Expand alphabet range",
"Drop bytes",

View File

@ -0,0 +1,89 @@
/**
* @author skyswordw
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import { DELIM_OPTIONS } from "../lib/Delim.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* String index lookup operation
*/
class StringIndexLookup extends Operation {
/**
* StringIndexLookup constructor
*/
constructor() {
super();
this.name = "String index lookup";
this.module = "Default";
this.description = "Looks up characters from a source string using an input list of indexes. This is useful for deobfuscating command lines that build strings by indexing into a lookup string.";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Source string",
"type": "binaryString",
"value": ""
},
{
"name": "Delimiter",
"type": "option",
"value": DELIM_OPTIONS
},
{
"name": "Indexing",
"type": "option",
"value": ["Zero-based", "One-based"]
},
{
"name": "Skip invalid indexes",
"type": "boolean",
"value": true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*
* @throws {OperationError} if an index is invalid and invalid indexes are not skipped
*/
run(input, args) {
const source = [...(args[0] || "")],
delim = Utils.charRep(args[1] || "Space"),
indexOffset = args[2] === "One-based" ? 1 : 0,
skipInvalid = args[3] !== false;
return input.split(delim).reduce((output, rawIndex) => {
const trimmedIndex = rawIndex.trim();
if (trimmedIndex.length === 0) return output;
const parsedIndex = Number(trimmedIndex);
if (!Number.isInteger(parsedIndex)) {
if (skipInvalid) return output;
throw new OperationError(`Invalid index: ${trimmedIndex}`);
}
const sourceIndex = parsedIndex - indexOffset;
if (sourceIndex < 0 || sourceIndex >= source.length) {
if (skipInvalid) return output;
throw new OperationError(`Index out of range: ${parsedIndex}`);
}
return output + source[sourceIndex];
}, "");
}
}
export default StringIndexLookup;

View File

@ -169,6 +169,7 @@ import "./tests/RC6.mjs";
// import "./tests/SplitColourChannels.mjs"; // Cannot test operations that use the File type yet
import "./tests/SQLBeautify.mjs";
import "./tests/StrUtils.mjs";
import "./tests/StringIndexLookup.mjs";
import "./tests/StripIPv4Header.mjs";
import "./tests/StripTCPHeader.mjs";
import "./tests/StripUDPHeader.mjs";

View File

@ -0,0 +1,66 @@
/**
* String index lookup tests.
*
* @author skyswordw
* @copyright Crown Copyright 2026
* @licence Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "String index lookup: zero-based semicolon list",
input: "0;1;2;3;4;5;6;7;8;9",
expectedOutput: "powershell",
recipeConfig: [
{
op: "String index lookup",
args: ["powershell", "Semi-colon", "Zero-based", false]
}
]
},
{
name: "String index lookup: one-based comma list",
input: "1, 2, 3, 4, 5",
expectedOutput: "abcde",
recipeConfig: [
{
op: "String index lookup",
args: ["abcde", "Comma", "One-based", false]
}
]
},
{
name: "String index lookup: skip invalid indexes",
input: "0;99;1;-1;2",
expectedOutput: "abc",
recipeConfig: [
{
op: "String index lookup",
args: ["abc", "Semi-colon", "Zero-based", true]
}
]
},
{
name: "String index lookup: default skips invalid tokens",
input: "0;foo;1;99;2",
expectedOutput: "abc",
recipeConfig: [
{
op: "String index lookup",
args: ["abc", "Semi-colon", "Zero-based"]
}
]
},
{
name: "String index lookup: strict out of range",
input: "3",
expectedOutput: "Index out of range: 3",
recipeConfig: [
{
op: "String index lookup",
args: ["abc", "Semi-colon", "Zero-based", false]
}
]
}
]);