Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
246 lines
7.5 KiB
JavaScript
246 lines
7.5 KiB
JavaScript
// Parse one bounded native architecture catalog; views never reinterpret names.
|
|
import { z } from "zod";
|
|
import { NATIVE_OUTPUT_BYTES } from "./limits.mjs";
|
|
import {
|
|
nativeLocationKey,
|
|
hex,
|
|
ordinal,
|
|
rowParser,
|
|
requireFact,
|
|
formatFailure,
|
|
} from "./native-contract.mjs";
|
|
import { freezeFacts } from "./fact-reads.mjs";
|
|
import { nativePage } from "./native-reads.mjs";
|
|
|
|
const u32 = z.number().int().nonnegative().max(0xffffffff);
|
|
const space = z.strictObject({
|
|
native_index: ordinal,
|
|
name: z.string().min(1),
|
|
kind: z.string().min(1),
|
|
address_size_bytes: u32,
|
|
addressable_unit_bytes: u32.positive(),
|
|
endianness: z.enum(["big", "little"]),
|
|
highest_offset: hex,
|
|
});
|
|
const register = z.strictObject({
|
|
native_row: ordinal,
|
|
space: z.string().min(1).nullable(),
|
|
offset: hex,
|
|
size_bytes: u32.positive(),
|
|
name: z.string().nullable(),
|
|
name_bytes_hex: z
|
|
.string()
|
|
.regex(/^(?:[0-9a-f]{2})*$/u)
|
|
.nullable(),
|
|
});
|
|
const schema = z.strictObject({
|
|
schema_version: z.literal(1),
|
|
binary: z.string(),
|
|
architecture: z.strictObject({
|
|
native_id: z.string().min(1),
|
|
default_code_space: z.string().min(1).nullable(),
|
|
}),
|
|
space_index_capacity: ordinal,
|
|
register_count: ordinal,
|
|
spaces: z.array(space),
|
|
registers: z.array(register),
|
|
coverage: z.strictObject({
|
|
scope: z.string(),
|
|
native_exact_register_map: z.literal("complete"),
|
|
alias_relationships: z.literal("unknown"),
|
|
dynamic_join_records: z.literal("not-collected"),
|
|
memory_values: z.literal("not-returned"),
|
|
listing_discovery: z.literal(false),
|
|
functions_decompiled: z.literal(0),
|
|
}),
|
|
});
|
|
// A valid record necessarily occupies at least this many wire bytes. Admit row
|
|
// counts before mapping malformed arrays or allocating their parsed rows.
|
|
const minSpaceBytes = Buffer.byteLength(
|
|
JSON.stringify({
|
|
native_index: 0,
|
|
name: "x",
|
|
kind: "x",
|
|
address_size_bytes: 0,
|
|
addressable_unit_bytes: 1,
|
|
endianness: "big",
|
|
highest_offset: "0x0",
|
|
}),
|
|
);
|
|
const minRegisterBytes = Buffer.byteLength(
|
|
JSON.stringify({
|
|
native_row: 0,
|
|
space: "x",
|
|
offset: "0x0",
|
|
size_bytes: 1,
|
|
name: "",
|
|
name_bytes_hex: null,
|
|
}),
|
|
);
|
|
const parse = rowParser(schema, {
|
|
spaces: (r) => space.parse(r),
|
|
registers: (r) => register.parse(r),
|
|
});
|
|
export function parseNativeStorageMap(text, expectedInput) {
|
|
try {
|
|
requireFact(
|
|
Buffer.byteLength(text) <= NATIVE_OUTPUT_BYTES,
|
|
"Native storage map exceeds wire budget",
|
|
);
|
|
const raw = JSON.parse(text);
|
|
for (const [name, minimum] of [
|
|
["spaces", minSpaceBytes],
|
|
["registers", minRegisterBytes],
|
|
])
|
|
requireFact(
|
|
Array.isArray(raw?.[name]) &&
|
|
raw[name].length <= Math.floor(NATIVE_OUTPUT_BYTES / minimum),
|
|
`Native storage ${name} exceeds format-derived row capacity`,
|
|
);
|
|
const facts = parse(raw);
|
|
requireFact(
|
|
facts.binary === expectedInput,
|
|
"Native storage map input differs from requested subject",
|
|
);
|
|
requireFact(
|
|
facts.register_count === facts.registers.length,
|
|
"Native register count mismatch",
|
|
);
|
|
const spacesByName = new Map(),
|
|
spacesByIndex = new Map(),
|
|
byLocation = new Map(),
|
|
byName = new Map();
|
|
for (const row of facts.spaces) {
|
|
requireFact(
|
|
row.native_index < facts.space_index_capacity &&
|
|
!spacesByName.has(row.name) &&
|
|
!spacesByIndex.has(row.native_index),
|
|
"Duplicate or invalid native space identity",
|
|
);
|
|
spacesByName.set(row.name, row);
|
|
spacesByIndex.set(row.native_index, row);
|
|
}
|
|
requireFact(
|
|
facts.architecture.default_code_space === null ||
|
|
spacesByName.has(facts.architecture.default_code_space),
|
|
"Unretained native default code space",
|
|
);
|
|
for (const [index, row] of facts.registers.entries()) {
|
|
requireFact(
|
|
row.native_row === index,
|
|
"Native register row identity mismatch",
|
|
);
|
|
requireFact(
|
|
(row.name === null) === (row.name_bytes_hex !== null),
|
|
"Native register name/byte knowledge mismatch",
|
|
);
|
|
requireFact(
|
|
row.space === null || spacesByName.has(row.space),
|
|
"Register references unretained native space",
|
|
);
|
|
if (row.space !== null) {
|
|
const key = nativeLocationKey(row);
|
|
requireFact(!byLocation.has(key), "Duplicate native register location");
|
|
byLocation.set(key, row);
|
|
}
|
|
if (row.name !== null) {
|
|
if (!byName.has(row.name)) byName.set(row.name, []);
|
|
byName.get(row.name).push(row);
|
|
}
|
|
}
|
|
freezeFacts(facts);
|
|
return createViews(facts, {
|
|
spacesByName,
|
|
spacesByIndex,
|
|
byLocation,
|
|
byName,
|
|
});
|
|
} catch (error) {
|
|
throw formatFailure(error);
|
|
}
|
|
}
|
|
function createViews(
|
|
facts,
|
|
{ spacesByName, spacesByIndex, byLocation, byName },
|
|
) {
|
|
const metadata = freezeFacts({
|
|
architecture: facts.architecture,
|
|
totals: {
|
|
spaces: facts.spaces.length,
|
|
space_index_capacity: facts.space_index_capacity,
|
|
registers: facts.register_count,
|
|
},
|
|
coverage: {
|
|
...facts.coverage,
|
|
unmatched_selectors:
|
|
"no retained exact map match; not absence of hardware storage, aliases or runtime values",
|
|
names: "native varnode_xref names; not every translator symbol/alias",
|
|
unknown_space_register_count: facts.registers.filter(
|
|
(r) => r.space === null,
|
|
).length,
|
|
undecodable_name_count: facts.registers.filter((r) => r.name === null)
|
|
.length,
|
|
},
|
|
});
|
|
return Object.freeze({
|
|
spaces(input) {
|
|
const names = input.names && [...new Set(input.names)],
|
|
indices = input.indices && [...new Set(input.indices)];
|
|
const rows = (
|
|
indices
|
|
? indices.flatMap((i) => spacesByIndex.get(i) ?? [])
|
|
: names
|
|
? names.flatMap((n) => spacesByName.get(n) ?? [])
|
|
: facts.spaces
|
|
).filter(
|
|
(r) =>
|
|
(!names || names.includes(r.name)) &&
|
|
(!indices || indices.includes(r.native_index)),
|
|
);
|
|
return {
|
|
...metadata,
|
|
...nativePage(rows, input, ["native_index", "name"]),
|
|
missing_names: names?.filter((n) => !spacesByName.has(n)) ?? [],
|
|
missing_indices: indices?.filter((i) => !spacesByIndex.has(i)) ?? [],
|
|
};
|
|
},
|
|
registers(input) {
|
|
const names = input.names && new Set(input.names),
|
|
locations =
|
|
input.locations &&
|
|
new Map(input.locations.map((r) => [nativeLocationKey(r), r]));
|
|
const normalize = (s) => (input.case_sensitive ? s : s.toLowerCase()),
|
|
query = normalize(input.query ?? "");
|
|
const rows = locations
|
|
? [...locations.keys()].flatMap((k) => byLocation.get(k) ?? [])
|
|
: names
|
|
? [...names].flatMap((n) => byName.get(n) ?? [])
|
|
: facts.registers;
|
|
return {
|
|
...metadata,
|
|
...nativePage(
|
|
rows.filter(
|
|
(r) =>
|
|
(!names || names.has(r.name)) &&
|
|
(!query ||
|
|
(r.name !== null && normalize(r.name).includes(query))),
|
|
),
|
|
input,
|
|
["native_row", "space", "offset", "size_bytes"],
|
|
),
|
|
unretained_names: names ? [...names].filter((n) => !byName.has(n)) : [],
|
|
...(locations
|
|
? {
|
|
unretained_locations: [...locations]
|
|
.filter(([k]) => !byLocation.has(k))
|
|
.map(([, r]) => ({ ...r })),
|
|
unretained_space_names: [
|
|
...new Set([...locations.values()].map((r) => r.space)),
|
|
].filter((n) => !spacesByName.has(n)),
|
|
}
|
|
: {}),
|
|
};
|
|
},
|
|
});
|
|
}
|