Files
reverse-engineering-anything/src/domain/analysisSearch.ts
T
Kaoru0822andClaude Opus 5.5 44b6384009 fix(native): find procedures by the secondary names exact lookup accepts (#8520)
* fix(native): find procedures by the secondary names exact lookup accepts

search_procedures matched only each procedure's primary name, while exact
lookup also accepts Kuna inventory aliases (versioned-symbol and generated
sub_ names) and Ghidra symbols at an exact function entry. A search for a
name procedure_address resolves returned nothing, and trace_feature
silently missed the procedure.

Both providers now match the same names their exact lookup accepts. A
procedure still appears once under its primary name; the optional
matched_aliases field lists the secondary names that matched, so aliases
never become procedures of their own and list_procedures is unchanged.
The shared domain helper owns that row rule for Kuna and for the Ghidra
host-side Unicode folding pass. The Ghidra analysis profile records the
new search semantics so older snapshots cannot replay alias-blind results.

Fixes #8330

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ghidra): match leaf spellings of procedure names in search

procedure_address accepts each entry symbol's leaf or namespace-qualified
spelling, but procedure search tested only qualified names, so a regex
such as ^audit_alias$ missed a secondary symbol ns::audit_alias (and ^same$
missed a primary alpha::same) that exact lookup selects.

Lookup and search now share one bridge definition of the names that select
a procedure, so they cannot drift. Matching secondary symbols are still
reported by their qualified names; because a leaf ends its qualified
spelling, the host's Unicode literal pass stays exact over those names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:53:29 -08:00

67 lines
2.5 KiB
TypeScript

import { z } from "zod";
import { nativeSearchInput } from "./native/nativeSearch.js";
import { AnalysisInputError } from "./analysisErrorCore.js";
/** Parsed `search_procedures`/`search_strings` query shared by providers that match in REA. */
export const analysisSearchQuerySchema = z.object(nativeSearchInput);
export type AnalysisSearchQuery = z.infer<typeof analysisSearchQuerySchema>;
/**
* Match a literal substring. With case ignored, ECMAScript Unicode RegExp
* matching applies simple case folding character-by-character
* ([Canonicalize](https://tc39.es/ecma262/2023/multipage/text-processing.html#sec-runtime-semantics-canonicalize));
* it is locale-independent and does not expand characters into sequences.
*/
export const literalSubstringPredicate = (
pattern: string,
caseSensitive: boolean,
): ((value: string) => boolean) => {
if (caseSensitive) return (value) => value.includes(pattern);
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const regex = new RegExp(escaped, "iu");
return (value) => regex.test(value);
};
/**
* Match names the way the search contract describes: a literal substring by
* default, or a Unicode regular expression, case-insensitive unless requested.
* An invalid pattern is the caller's input error.
*/
export const analysisSearchPredicate = (
query: AnalysisSearchQuery,
): ((value: string) => boolean) => {
if (query.mode === "regex") {
let regex: RegExp;
try {
regex = new RegExp(query.pattern, query.case_sensitive ? "u" : "iu");
} catch (cause: unknown) {
throw new AnalysisInputError("search", { cause });
}
return (value) => regex.test(value);
}
return literalSubstringPredicate(query.pattern, query.case_sensitive);
};
/**
* Match one procedure by its primary name and the secondary names its provider
* accepts for exact selection. A procedure remains one row whose value is its
* primary name; matched_aliases keeps each distinct matching secondary
* spelling in provider order, so an alias never becomes a procedure of its own.
*/
export const matchProcedureNames = <Row extends { readonly value: string }>(
row: Row,
aliases: readonly string[],
matches: (value: string) => boolean,
): (Row & { readonly matched_aliases?: string[] }) | undefined => {
const matched =
aliases.length === 0
? []
: [...new Set(aliases)].filter(
(alias) => alias !== row.value && matches(alias),
);
if (matched.length > 0) return { ...row, matched_aliases: matched };
return matches(row.value) ? row : undefined;
};