Files
rea/src/kuna/KunaStringQueries.ts
T

186 lines
6.2 KiB
TypeScript

import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import {
analysisSearchPredicate,
analysisSearchQuerySchema,
} from "../domain/analysisSearch.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import { normalizeKunaAddress } from "./KunaAddress.js";
import { kunaProjectDeadline } from "./KunaConfiguration.js";
import type { KunaInstallation } from "./KunaInstallation.js";
import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js";
import { kunaQueryFailureForCaller } from "./KunaQueryErrors.js";
import { KUNA_STRING_MIN_LENGTH } from "./KunaLimits.js";
import {
KunaStringInventoryReader,
kunaStringsAtAddress,
type KunaStringInventory,
type KunaStringItem,
} from "./KunaStringInventory.js";
/** One validated scan shared by literal, regex and exact-address queries. */
export class KunaStringQueries {
#inventory: KunaStringInventory | undefined;
#failure: AnalysisError | undefined;
#pending: KunaPendingQuery<KunaStringInventory> | undefined;
constructor(private readonly work: KunaQueryWork) {}
async execute(
operation: "list_strings" | "search_strings",
parameters: Readonly<Record<string, JsonValue>>,
installation: KunaInstallation,
signal: AbortSignal | undefined,
): Promise<
Result<
{ readonly result: JsonValue; readonly source: JsonValue },
AnalysisError
>
> {
if (signal?.aborted === true)
return err(
new AnalysisCancelledError(operation, { cause: signal?.reason }),
);
const query =
operation === "search_strings"
? analysisSearchQuerySchema.safeParse(parameters)
: undefined;
if (query !== undefined && !query.success)
return err(
analysisInputErrorFromIssues(operation, query.error.issues, parameters),
);
const address =
operation === "list_strings" && parameters.address !== undefined
? normalizeKunaAddress(parameters.address)
: undefined;
if (
operation === "list_strings" &&
parameters.address !== undefined &&
address === undefined
)
return err(
new AnalysisInputError(
operation,
{ userMessage: "Use an explicit hexadecimal u64 string address." },
[{ path: ["address"], reason: "invalid_format" }],
),
);
const inventory = await this.#load(installation, operation, signal);
if (!inventory.ok)
return err(kunaQueryFailureForCaller(inventory.error, operation));
if (this.#cancelled(signal))
return err(
new AnalysisCancelledError(operation, { cause: signal?.reason }),
);
const available = inventory.value.items;
let items: KunaStringItem[] | undefined;
if (address !== undefined)
items = kunaStringsAtAddress(inventory.value, address);
if (query?.success === true) {
if (query.data.mode === "regex") {
const matched = await this.work.match({
operation,
pattern: query.data.pattern,
caseSensitive: query.data.case_sensitive,
values: () => available.map((item) => item.value),
signal,
});
if (!matched.ok) return matched;
items = matched.value.map((index) => available[index]!);
} else {
const predicate = analysisSearchPredicate(query.data);
items = available.filter((item) => predicate(item.value));
}
}
const selected = items ?? [...available];
return ok({
result: selected,
source: {
kind: "kuna-string-inventory",
sha256: inventory.value.sha256,
bytes: inventory.value.bytes,
scanned: inventory.value.scanned,
encoding: "all",
min_length: KUNA_STRING_MIN_LENGTH,
termination: "any",
xrefs: false,
inventory_count: inventory.value.items.length,
selected_count: selected.length,
},
});
}
async #load(
installation: KunaInstallation,
operation: string,
signal: AbortSignal | undefined,
): Promise<Result<KunaStringInventory, AnalysisError>> {
if (this.#inventory !== undefined) return ok(this.#inventory);
if (this.#failure !== undefined) return err(this.#failure);
if (this.#pending?.signal.aborted === true) {
const stopped = await this.work.wait(this.#pending, signal, operation);
if (!stopped.ok && !(stopped.error instanceof AnalysisCancelledError))
return stopped;
if (signal?.aborted === true)
return err(
new AnalysisCancelledError(operation, { cause: signal?.reason }),
);
return this.#load(installation, operation, signal);
}
if (this.#pending === undefined) {
const pending = this.work.start<KunaStringInventory>(
(producerSignal) =>
this.work.scan(
{
installation,
operation: "kuna:strings",
arguments: (input) => [
"strings",
input,
"--json",
"--encoding",
"all",
"--min-length",
String(KUNA_STRING_MIN_LENGTH),
"--termination",
"any",
"--no-xrefs",
],
deadline: kunaProjectDeadline(installation.configuration),
},
new KunaStringInventoryReader(),
producerSignal,
(failure) => {
this.#failure = failure;
},
),
(value) => {
this.#inventory = value;
},
);
this.#pending = pending;
const release = () => {
if (this.#pending === pending) this.#pending = undefined;
};
void pending.promise.then(release, release);
}
return this.work.wait(this.#pending, signal, operation);
}
#cancelled(signal: AbortSignal | undefined): boolean {
return signal?.aborted === true;
}
/** Called only after session-owned work has drained. */
clear(): void {
this.#inventory = undefined;
this.#failure = undefined;
this.#pending = undefined;
}
}