fix(verification): distinguish Hopper string recognition from byte fidelity
This commit is contained in:
1 parent
b9d9a474cf
commit
8ea0432ffc
3 files changed
+126
-25
No files matched your search
+11
-1
@@ -369,7 +369,17 @@ subsequent wire recovery and clean shutdown. Caller timeout does not interrupt
|
||||
Hopper's synchronous native operation. Socket boundary tests deterministically
|
||||
cover active and queued expiry, late replies and timer cleanup.
|
||||
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command. Its
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command. Hopper
|
||||
6.4.2's ELF loader did not recognize the original 4,119-byte literal or three
|
||||
non-ASCII byte strings in the measured fixtures. The lane retains complete raw
|
||||
byte checks for those literals through source-owned symbols and reports absent
|
||||
recognition with decoded-object coverage explicitly unverified. Recognized
|
||||
objects must still match their source bytes, encoding, filters and searches.
|
||||
A separate 548-byte ASCII literal requires native fragmentation, shortened
|
||||
provider display, complete decoded fragments and dossier provenance on Linux;
|
||||
the macOS lane retains the original full-length recognition requirement. These
|
||||
are observed fixture/build capabilities, not universal Hopper length limits.
|
||||
Its
|
||||
lifecycle checks attempt competing CLI launches for the active target and a
|
||||
different target, requiring rejection before either can change the owning
|
||||
MCP session's documents or procedures. Lease boundary tests also preserve
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
|
||||
const fixtureSymbolPrefix = process.platform === "darwin" ? "_" : "";
|
||||
|
||||
/** Exercise literal/regex search, Unicode identities and cache invalidation in Hopper. */
|
||||
export async function verifyHopperSearch(call, invalid, procedure) {
|
||||
const address = procedure.address;
|
||||
const original = await call("address_name", { address });
|
||||
let stringCoverage;
|
||||
try {
|
||||
await verifySymbolNames(call, invalid, address);
|
||||
await verifySearchModes(call, address);
|
||||
stringCoverage = await verifySearchModes(call, address);
|
||||
} finally {
|
||||
await call("set_address_name", { address, name: original });
|
||||
}
|
||||
@@ -26,7 +29,7 @@ export async function verifyHopperSearch(call, invalid, procedure) {
|
||||
symbolUtf16LimitValidatedBeforeMutation: true,
|
||||
regexAndCaseModes: true,
|
||||
annotationCacheInvalidation: true,
|
||||
escapedUtf8AndByteStrings: true,
|
||||
stringCoverage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,9 +106,23 @@ async function verifySearchModes(call, address) {
|
||||
[{ address, value: "REA_Search_Upper" }],
|
||||
);
|
||||
const strings = await call("list_strings");
|
||||
const long = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle";
|
||||
const longItem = strings.find((item) =>
|
||||
item.value.startsWith("REA_LONG_LITERAL_"),
|
||||
const linux = process.platform === "linux";
|
||||
const fullLiteral = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle";
|
||||
// Recognition is a provider/loader capability, not an adapter guarantee.
|
||||
// Always retain the original size in raw-byte coverage; Linux additionally
|
||||
// requires an extent observed to produce native fragmented string objects.
|
||||
const fullBytes = Buffer.from(fullLiteral + "\0");
|
||||
const raw = await call("read_bytes", {
|
||||
address: `${fixtureSymbolPrefix}rea_long_literal`,
|
||||
length: fullBytes.length,
|
||||
});
|
||||
assert.equal(raw.complete, true);
|
||||
assert.equal(raw.bytes_hex, fullBytes.toString("hex"));
|
||||
const long = linux
|
||||
? "REA_FRAGMENTED_LITERAL_" + "x".repeat(512) + "fragment_tail"
|
||||
: fullLiteral;
|
||||
const longItem = strings.find(
|
||||
(item) => long.startsWith(item.value) && item.value.startsWith("REA_"),
|
||||
);
|
||||
assert.ok(longItem, "fixture omitted its real long literal");
|
||||
assert.ok(long.startsWith(longItem.value));
|
||||
@@ -158,7 +175,7 @@ async function verifySearchModes(call, address) {
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("search_strings", {
|
||||
pattern: "needle",
|
||||
pattern: linux ? "fragment_tail" : "needle",
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
@@ -176,31 +193,94 @@ async function verifySearchModes(call, address) {
|
||||
}),
|
||||
expected,
|
||||
);
|
||||
await verifyHopperStringObjects(call, [
|
||||
{ value: "REA_UTF8_é_😀", encoding: "utf-8" },
|
||||
const objects = await verifyHopperStringObjects(call, [
|
||||
{
|
||||
value: "REA_UTF8_é_😀",
|
||||
encoding: "utf-8",
|
||||
symbol: `${fixtureSymbolPrefix}rea_utf8_literal`,
|
||||
recognitionOptional: linux,
|
||||
},
|
||||
{ value: 'REA_ESCAPED_"\\line\nend\t\r', encoding: "utf-8" },
|
||||
{ value: "REA_LITERAL_BACKSLASH_\\n", encoding: "utf-8" },
|
||||
{ value: "REA_LITERAL_…", encoding: "utf-8" },
|
||||
{ value: "REA_LATIN1_ÿ", encoding: "latin-1" },
|
||||
{
|
||||
value: "REA_LITERAL_…",
|
||||
encoding: "utf-8",
|
||||
symbol: `${fixtureSymbolPrefix}rea_ellipsis_literal`,
|
||||
recognitionOptional: linux,
|
||||
},
|
||||
{
|
||||
value: "REA_LATIN1_ÿ",
|
||||
encoding: "latin-1",
|
||||
symbol: `${fixtureSymbolPrefix}rea_latin1_literal`,
|
||||
recognitionOptional: linux,
|
||||
},
|
||||
]);
|
||||
return {
|
||||
fullLiteralSourceBytes: fullBytes.length,
|
||||
fragmentedLiteralSourceBytes: Buffer.byteLength(long),
|
||||
fullLiteralRecognition: strings.some((item) =>
|
||||
item.value.startsWith("REA_LONG_LITERAL_"),
|
||||
)
|
||||
? "observed"
|
||||
: "not_observed",
|
||||
...objects,
|
||||
};
|
||||
}
|
||||
|
||||
/** Compare native string identities and values with the source fixture's bytes. */
|
||||
export async function verifyHopperStringObjects(call, expectations) {
|
||||
const strings = await call("list_strings");
|
||||
for (const { value, encoding } of expectations) {
|
||||
const item = strings.find((candidate) => candidate.value === value);
|
||||
assert.ok(
|
||||
item,
|
||||
`Hopper omitted the decoded string ${JSON.stringify(value)}`,
|
||||
);
|
||||
assert.equal(item.string.encoding, encoding);
|
||||
assert.equal(item.string.encoding_status, "inferred");
|
||||
assert.equal(item.string.termination, "present_or_not_required");
|
||||
const recognition = [];
|
||||
for (const {
|
||||
value,
|
||||
encoding,
|
||||
symbol,
|
||||
recognitionOptional = false,
|
||||
} of expectations) {
|
||||
const expectedBytes = Buffer.from(
|
||||
value + "\0",
|
||||
encoding.replaceAll("-", ""),
|
||||
);
|
||||
let sourceAddress;
|
||||
if (symbol !== undefined) {
|
||||
const raw = await call("read_bytes", {
|
||||
address: symbol,
|
||||
length: expectedBytes.length,
|
||||
});
|
||||
sourceAddress = raw.address;
|
||||
assert.equal(raw.complete, true);
|
||||
assert.equal(raw.bytes_hex, expectedBytes.toString("hex"));
|
||||
}
|
||||
const item = strings.find((candidate) =>
|
||||
sourceAddress === undefined
|
||||
? candidate.value === value
|
||||
: candidate.address === sourceAddress,
|
||||
);
|
||||
if (item === undefined && recognitionOptional) {
|
||||
assert.ok(
|
||||
symbol,
|
||||
"optional recognition requires independent source-byte coverage",
|
||||
);
|
||||
recognition.push({
|
||||
value,
|
||||
state: "not_observed",
|
||||
decodedObject: "unverified",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
assert.ok(
|
||||
item,
|
||||
`Hopper omitted the decoded string ${JSON.stringify(value)}`,
|
||||
);
|
||||
assert.equal(
|
||||
item.value,
|
||||
value,
|
||||
"native string decoding changed the source value",
|
||||
);
|
||||
recognition.push({ value, state: "observed", decodedObject: "verified" });
|
||||
assert.equal(item.string.encoding, encoding);
|
||||
assert.equal(item.string.encoding_status, "inferred");
|
||||
assert.equal(item.string.termination, "present_or_not_required");
|
||||
assert.equal(item.string.byte_length, expectedBytes.length);
|
||||
const bytes = await call("read_bytes", {
|
||||
address: item.address,
|
||||
@@ -225,7 +305,7 @@ export async function verifyHopperStringObjects(call, expectations) {
|
||||
);
|
||||
return {
|
||||
sourceBytesVerified: true,
|
||||
inferredEncodingReported: true,
|
||||
recognition,
|
||||
displayEscapesExcludedFromMatching: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,13 +5,24 @@ volatile int rea_c_global = 7;
|
||||
#define REA_LONG_CHUNK "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
#define REA_LONG_256 REA_LONG_CHUNK REA_LONG_CHUNK REA_LONG_CHUNK REA_LONG_CHUNK
|
||||
#define REA_LONG_1024 REA_LONG_256 REA_LONG_256 REA_LONG_256 REA_LONG_256
|
||||
/* Keep the full-size literal even when a loader does not type it as a string. */
|
||||
__attribute__((used)) const char rea_long_literal[] =
|
||||
"REA_LONG_LITERAL_" REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 "needle";
|
||||
/* Linux Hopper 6.4.2 recognizes this extent and splits it into native objects. */
|
||||
__attribute__((used)) const char rea_fragmented_literal[] =
|
||||
"REA_FRAGMENTED_LITERAL_" REA_LONG_256 REA_LONG_256 "fragment_tail";
|
||||
__attribute__((used)) const char rea_utf8_literal[] = "REA_UTF8_é_😀";
|
||||
__attribute__((used)) const char rea_ellipsis_literal[] = "REA_LITERAL_…";
|
||||
__attribute__((used)) const char rea_latin1_literal[] = "REA_LATIN1_\xff";
|
||||
|
||||
__attribute__((noinline, used)) void rea_long_literal_procedure(void) {
|
||||
puts("REA_LONG_LITERAL_" REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 "needle");
|
||||
puts("REA_UTF8_é_😀");
|
||||
puts(rea_long_literal);
|
||||
puts(rea_fragmented_literal);
|
||||
puts(rea_utf8_literal);
|
||||
puts("REA_ESCAPED_\"\\line\nend\t\r");
|
||||
puts("REA_LITERAL_BACKSLASH_\\n");
|
||||
puts("REA_LITERAL_…");
|
||||
puts("REA_LATIN1_\xff");
|
||||
puts(rea_ellipsis_literal);
|
||||
puts(rea_latin1_literal);
|
||||
}
|
||||
|
||||
__attribute__((noinline, used)) int rea_leaf(int value) {
|
||||
|
||||
Reference in new issue
Block a user