mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
255d906e15
Keep exact wildcard, whitespace and aligned-mask validation in the shared domain refinement. Advertise descriptive string inputs instead of repeating regexes, and remove discarded byte/mask array materialization. Preserve caller spelling and error precedence; cover large public stdio queries, provider-boundary parsing and connection recovery. Fixes #5257
204 lines
6.8 KiB
TypeScript
204 lines
6.8 KiB
TypeScript
import { z } from "zod";
|
|
import { addressCoordinate } from "./addressCoordinate.js";
|
|
import { AnalysisInputError } from "../analysisErrorCore.js";
|
|
import { err, ok, type Result } from "../result.js";
|
|
|
|
/** Exact grammar belongs to the object refinement, not a repeating advertised regex. */
|
|
export const bytePatternFields = {
|
|
pattern: z
|
|
.string()
|
|
.describe(
|
|
"Hexadecimal byte pairs or ?? whole-byte wildcards, with optional whitespace between complete pairs; no leading or trailing whitespace. At least one bit must be constrained after applying the mask.",
|
|
),
|
|
mask: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Hexadecimal mask byte pairs, with optional whitespace between complete pairs; no leading or trailing whitespace. Must contain the same number of bytes as pattern.",
|
|
),
|
|
};
|
|
|
|
const hexDigit = (code: number): number | undefined => {
|
|
if (code >= 48 && code <= 57) return code - 48;
|
|
if (code >= 65 && code <= 70) return code - 55;
|
|
if (code >= 97 && code <= 102) return code - 87;
|
|
return undefined;
|
|
};
|
|
|
|
const readBytePair = (
|
|
value: string,
|
|
offset: number,
|
|
): number | "wildcard" | undefined => {
|
|
if (value[offset] === "?" && value[offset + 1] === "?") return "wildcard";
|
|
const high = hexDigit(value.charCodeAt(offset));
|
|
const low = hexDigit(value.charCodeAt(offset + 1));
|
|
return high === undefined || low === undefined ? undefined : high * 16 + low;
|
|
};
|
|
|
|
const nextPairOffset = (value: string, offset: number): number => {
|
|
const end = offset + 2;
|
|
let next = end;
|
|
while (next < value.length && /\s/u.test(value[next]!)) next += 1;
|
|
return next === value.length && next !== end ? -1 : next;
|
|
};
|
|
|
|
const countBytePairs = (
|
|
value: string,
|
|
field: "pattern" | "mask",
|
|
): number | undefined => {
|
|
let count = 0;
|
|
for (let offset = 0; offset < value.length;) {
|
|
const byte = readBytePair(value, offset);
|
|
if (byte === undefined || (field === "mask" && byte === "wildcard"))
|
|
return undefined;
|
|
count += 1;
|
|
offset = nextPairOffset(value, offset);
|
|
if (offset < 0) return undefined;
|
|
}
|
|
return count === 0 ? undefined : count;
|
|
};
|
|
|
|
/** Validate without materializing byte arrays or using size-dependent regexes. */
|
|
const validateBytePatternValue = (
|
|
pattern: string,
|
|
mask?: string,
|
|
): Result<null, AnalysisInputError> => {
|
|
const invalid = (field: string, message: string) =>
|
|
err(
|
|
new AnalysisInputError("search_bytes", undefined, [
|
|
{ path: [field], reason: "invalid_value", message },
|
|
]),
|
|
);
|
|
const patternCount = countBytePairs(pattern, "pattern");
|
|
if (patternCount === undefined)
|
|
return invalid("pattern", "Use hexadecimal byte pairs or ?? wildcards");
|
|
const maskCount =
|
|
mask === undefined ? undefined : countBytePairs(mask, "mask");
|
|
if (mask !== undefined && maskCount === undefined)
|
|
return invalid("mask", "Use hexadecimal mask byte pairs");
|
|
if (maskCount !== undefined && maskCount !== patternCount)
|
|
return invalid("mask", "Mask length must equal pattern length");
|
|
let patternOffset = 0;
|
|
let maskOffset = 0;
|
|
for (let index = 0; index < patternCount; index += 1) {
|
|
const byte = readBytePair(pattern, patternOffset);
|
|
const selected = mask === undefined ? 255 : readBytePair(mask, maskOffset);
|
|
if (
|
|
typeof byte === "number" &&
|
|
typeof selected === "number" &&
|
|
selected !== 0
|
|
)
|
|
return ok(null);
|
|
patternOffset = nextPairOffset(pattern, patternOffset);
|
|
if (mask !== undefined) maskOffset = nextPairOffset(mask, maskOffset);
|
|
}
|
|
return invalid("pattern", "Pattern must constrain at least one bit");
|
|
};
|
|
|
|
/** Shared semantic validation for public and provider inputs. */
|
|
export const validateBytePattern = (
|
|
value: { pattern: string; mask?: string | undefined },
|
|
context: z.RefinementCtx,
|
|
): void => {
|
|
const parsed = validateBytePatternValue(value.pattern, value.mask);
|
|
if (!parsed.ok)
|
|
for (const issue of parsed.error.issues)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: [...issue.path],
|
|
message: issue.message ?? "Invalid byte pattern",
|
|
});
|
|
};
|
|
|
|
const coverageBlock = z
|
|
.strictObject({
|
|
segment: z.string(),
|
|
start: z.string(),
|
|
end: z.string(),
|
|
bytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
})
|
|
.superRefine((value, context) => {
|
|
const start = addressCoordinate(value.start);
|
|
const end = addressCoordinate(value.end);
|
|
if (
|
|
start === null ||
|
|
end === null ||
|
|
start.space !== end.space ||
|
|
start.offset > end.offset ||
|
|
end.offset - start.offset + 1n !== BigInt(value.bytes)
|
|
)
|
|
context.addIssue({
|
|
code: "custom",
|
|
message:
|
|
"Byte coverage must equal its inclusive address range in one space",
|
|
});
|
|
});
|
|
|
|
/** Inclusive page coverage; matches never span memory blocks. */
|
|
export const byteSearchResultSchema = z
|
|
.strictObject({
|
|
matches: z
|
|
.array(
|
|
z.strictObject({
|
|
address: z.string(),
|
|
segment: z.string(),
|
|
file_offset: z
|
|
.number()
|
|
.int()
|
|
.nonnegative()
|
|
.max(Number.MAX_SAFE_INTEGER)
|
|
.nullable(),
|
|
procedure: z
|
|
.strictObject({ address: z.string(), name: z.string() })
|
|
.nullable(),
|
|
}),
|
|
)
|
|
.max(1000),
|
|
searched: z.strictObject({
|
|
blocks: z.array(coverageBlock),
|
|
bytes: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
skipped_uninitialized: z.array(coverageBlock),
|
|
skipped_not_loaded: z.array(coverageBlock),
|
|
}),
|
|
complete: z.boolean(),
|
|
next_cursor: z.string().min(1).nullable(),
|
|
})
|
|
.superRefine((value, context) => {
|
|
if (
|
|
value.complete !== (value.next_cursor === null) ||
|
|
value.searched.bytes !==
|
|
value.searched.blocks.reduce((sum, block) => sum + block.bytes, 0)
|
|
)
|
|
context.addIssue({
|
|
code: "custom",
|
|
message: "Byte search coverage and continuation disagree",
|
|
});
|
|
const ranges = value.searched.blocks.map((block) => ({
|
|
segment: block.segment,
|
|
start: addressCoordinate(block.start),
|
|
end: addressCoordinate(block.end),
|
|
}));
|
|
for (const [index, match] of value.matches.entries()) {
|
|
const address = addressCoordinate(match.address);
|
|
if (
|
|
address === null ||
|
|
!ranges.some(
|
|
(range) =>
|
|
range.segment === match.segment &&
|
|
range.start !== null &&
|
|
range.end !== null &&
|
|
range.start.space === address.space &&
|
|
range.end.space === address.space &&
|
|
range.start.offset <= address.offset &&
|
|
address.offset <= range.end.offset,
|
|
)
|
|
)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["matches", index, "address"],
|
|
message:
|
|
"Byte match must lie within reported searched coverage in the same segment and address space",
|
|
});
|
|
}
|
|
});
|