Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43303502e3 | ||
|
|
011be9c3d5 | ||
|
|
5b16f24310 | ||
|
|
9f8659e053 | ||
|
|
40f73f62bc | ||
|
|
6ae59f3d19 | ||
|
|
9d6651e5b5 | ||
|
|
a90c9eac6a | ||
|
|
cfd3c35a50 | ||
|
|
3776f218e5 | ||
|
|
f7250fe35f | ||
|
|
cd64e24ff2 | ||
|
|
a40ee741c2 | ||
|
|
8a695d1585 |
No files matched your search
+79
-917
File diff suppressed because it is too large.
Load diff
@@ -168,6 +168,9 @@ valid inputs.
|
||||
|
||||
Endpoint nodes keep the exact endpoint in each observation's `properties.value`;
|
||||
semantic request and response nodes keep it in `properties.endpoint`. Endpoint
|
||||
lookup identities are preserved independently of the primitive-value lattice's
|
||||
byte budget. An oversized normalized literal can become unknown while the same
|
||||
string remains an exact endpoint for tracing and comparison. Endpoint
|
||||
identity keys use `value-sha256:<digest>` over the canonical JSON string when
|
||||
that is shorter, or when the original value starts with the reserved digest
|
||||
prefix. Other short endpoint keys remain unchanged. Endpoint labels use the
|
||||
|
||||
@@ -437,7 +437,18 @@ retained candidate strings and temporary canonical-key serialization. This is
|
||||
an in-memory semantic allocation bound independent of CLI or MCP transport
|
||||
budgets. Source literal bytes remain in the parsed source and artifact evidence;
|
||||
when a normalized semantic value exceeds the bound, its value is unknown and
|
||||
coverage is partial. Expression
|
||||
coverage is partial. Direct string literals, cooked templates without
|
||||
substitutions, and derived strings all use this normalization boundary. A
|
||||
single exact primitive is validated without allocating deduplication or sort
|
||||
state or serializing a canonical key. Configuration defaults use the same
|
||||
normalizer; limited defaults retain `value_resource_limit` and an anchored
|
||||
resource-limit unknown. Known null, false, zero, and empty-string defaults stay
|
||||
exact. This primitive-value bound does not truncate lookup identities such as
|
||||
source paths, field names or request endpoints. Full endpoint properties remain
|
||||
available for endpoint queries, tracing and comparison. Each analysis records encountered limit classifications independently of
|
||||
later projections: a selected healthy sibling can remain exact while whole-file
|
||||
coverage reports the encountered bound. Projection preserves limiting values
|
||||
from the shared property-slot owner. Expression
|
||||
evaluation and provenance walks stop after 256 nested levels. Union products
|
||||
and string lengths are checked before allocating combinations or concatenated
|
||||
strings. When a boundary is reached, the value stays unknown, semantic coverage
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
{
|
||||
"base_revision": "e9a71efa31d6c3675b67938b8a79fa4ddf01604c",
|
||||
"scope": "Semantic per-value string-byte normalization; real static shipped-application control, no hosted-model performance claim",
|
||||
"runtime": {
|
||||
"platform": "Linux x64",
|
||||
"node": "24.18.0",
|
||||
"npm": "11.16.0",
|
||||
"tiktoken": "0.12.0"
|
||||
},
|
||||
"baseline_source_sha256": {
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "df3fc30b5ba91901a85fe56f200355930da32d054bb026a747e3414af233b6b5",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "c12b29f0faf4b6511777cba7ed8ac007c6d6170d657f01e43e194feacb3e0601",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1"
|
||||
},
|
||||
"final_source_sha256": {
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
|
||||
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "844d4639401a9f050ce42c80192572a6d0d74fe4f81b37e93a2c20aaca9fb9a6",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
|
||||
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
|
||||
"src/domain/javascript/javascriptSemanticDataEffects.ts": "e69408f2f5910a0107fd5d0cae0ff60ed706448857bc8e535c37cac1a0cd4d37",
|
||||
"src/domain/javascript/javascriptSemanticIr.ts": "68dad360d0a9e6bd72108b2717cae756b84ca1efb48455248ded2ff699d2c113",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
|
||||
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
|
||||
"tests/acceptance/applications/javascriptEndpointPayload.test.ts": "b3cfcde1c5b5e0698f84afd51f4b53e42e56101267707c20c0d038320f5cfbd2",
|
||||
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "070106115b482269794799e448bd20b8e02df1f511f4ceb2b8fa75612c7f78e4",
|
||||
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
|
||||
},
|
||||
"artifact": {
|
||||
"path": "/tmp/rea-codemode-experiment-20261015/artifacts/obsidian-1.14.4.asar",
|
||||
"sha256": "146ef8470d8cbb4a6db209bdc19bf2bad13350444186c0fd93c31087b142b92f"
|
||||
},
|
||||
"selected_source_paths": ["package.json", "main.js"],
|
||||
"where": {
|
||||
"path": "main.js",
|
||||
"kind": "javascript-module"
|
||||
},
|
||||
"identical_parent_evidence_id": "ev_73ef06e3b9b944f54c5257f7960279127d8518d2a73cd8618bd045dbe44e6b5a",
|
||||
"cli_parity": {
|
||||
"evidence_id": "ev_b3586dfdf7d4f01157f8f2978326aaa7103987110f2df4fa5bf3331ff2898749",
|
||||
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
|
||||
"exact_cli_mcp_view_parity": true
|
||||
},
|
||||
"unchanged_query_shapes": {
|
||||
"legacy": {
|
||||
"calls": 24,
|
||||
"structured_reply_bytes": 325201,
|
||||
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
|
||||
"matching_modules": 23,
|
||||
"coverage": {
|
||||
"status": "page",
|
||||
"examined": 55,
|
||||
"total": 55,
|
||||
"next_offset": null,
|
||||
"exhausted": true
|
||||
}
|
||||
},
|
||||
"references": {
|
||||
"calls": 1,
|
||||
"structured_reply_bytes": 22158,
|
||||
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
|
||||
"matching_modules": 23,
|
||||
"coverage": {
|
||||
"status": "page",
|
||||
"examined": 23,
|
||||
"total": 23,
|
||||
"next_offset": null,
|
||||
"exhausted": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"unchanged_token_costs": {
|
||||
"tokenizer": "cl100k_base",
|
||||
"tiktoken_version": "0.12.0",
|
||||
"costs": {
|
||||
"legacy": {
|
||||
"selected_catalog": 7977,
|
||||
"full_catalog": 195522,
|
||||
"common_request": 53,
|
||||
"common_summary": 2675,
|
||||
"requests": 2836,
|
||||
"structured_replies": 71761,
|
||||
"selected_catalog_task_total": 85302,
|
||||
"full_catalog_task_total": 272847
|
||||
},
|
||||
"references": {
|
||||
"selected_catalog": 7977,
|
||||
"full_catalog": 195522,
|
||||
"common_request": 53,
|
||||
"common_summary": 2675,
|
||||
"requests": 102,
|
||||
"structured_replies": 5741,
|
||||
"selected_catalog_task_total": 16548,
|
||||
"full_catalog_task_total": 204093
|
||||
}
|
||||
}
|
||||
},
|
||||
"cohorts": {
|
||||
"baseline": {
|
||||
"preparation_ms": 19089.119622,
|
||||
"query_samples": [
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 383.8260569999984
|
||||
},
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 15.778886999996757
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 13.15173700000014
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 240.44363500000327
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 346.04796499999793
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 13.727306999997381
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"elapsed_seconds": 33.394,
|
||||
"cpu_seconds": 33.016802,
|
||||
"peak_cgroup_bytes": 1979863040,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"final": {
|
||||
"preparation_ms": 19061.292544,
|
||||
"query_samples": [
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 344.6316079999997
|
||||
},
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 14.438351000000694
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 11.38763100000142
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 298.41579600000114
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 211.45682099999976
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 10.275370999999723
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"elapsed_seconds": 32.949,
|
||||
"cpu_seconds": 32.559187,
|
||||
"peak_cgroup_bytes": 1880096768,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conditions": "First query is after shared artifact analysis; subsequent queries share one MCP session. Legacy item reads are serial; a concurrent legacy baseline is unmeasured. Filesystem/build warmness uncontrolled. Token costs count one post-change catalog, preparation request/summary and each request/structured reply once; client rendering, replay, reasoning, final answer and model latency are unmeasured. Preparation and parity/tokenization overhead are outside per-query elapsed times; external family-resource receipts include the complete verifier and children. Before/after used separate fresh MCP sessions. CPU/memory includes analysis, query arms, export, CLI parity and tokenization; cgroup memory includes file cache, not summed RSS. Counters are at payload exit while observer lives, not final scope counters. One before/after control establishes no speedup or equivalence bound. Catalog BPE projection excludes annotations/output schemas.",
|
||||
"resource_policy": {
|
||||
"memory_max_bytes": 8589934592,
|
||||
"early_stop_bytes": 6442450944,
|
||||
"cpu_quota_percent": 100,
|
||||
"swap": false
|
||||
},
|
||||
"observer_sources_sha256": {
|
||||
"resource-run.py": "7728862171415e7dee2c11af2ead2b9a54265fccaf82a7113956d36b29065e33",
|
||||
"resource_payload.py": "6f089e645459eba80c5d73e39c21606c78173a44daf49fabc3b88c4631ce51c6"
|
||||
},
|
||||
"regressions": {
|
||||
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
|
||||
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
|
||||
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
|
||||
"final_focused": "66 passed across five files on the restored 38d263a runtime. Final corrected public selection passed 7/7 in two files: the six literal/default/member cases plus the augmented original large-endpoint acceptance. 67 distinct focused cases across invocations; not a single 67-case invocation.",
|
||||
"final_semantic_suite": "932 passed in 36 files on 38d263a. Final production src tree (including these domain tests) verified byte-identical to that revision; no repeated semantic run claimed.",
|
||||
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
|
||||
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
|
||||
"endpoint_contract_correction": "Restored exact endpoint lookup identities rather than shrinking the existing large-endpoint fixture. Added the identical oversized string as an exported primitive in that acceptance workflow: literal normalization must report its bound while full endpoint lookup/feature trace/semantic trace/version comparison continue to work. Production src tree restored byte-identical to 38d263a; 932 semantic tests from that state remain applicable. Final corrected public tests/gates recorded after execution.",
|
||||
"final_corrected_gates": "Full static gate and docs gate (5 consistency tests) passed after endpoint identity restoration and strengthened unchanged-large-input acceptance. Production src remains byte-identical to 38d263a; final public 7/7 and fresh real control passed."
|
||||
},
|
||||
"prior_attempts": [
|
||||
{
|
||||
"stage": "Initial literal normalization, incomplete for member/default paths identified in review",
|
||||
"revision": "40b9d3c5b9cbeae7661d8e351e1b5ef6e5338931",
|
||||
"source_sha256": {
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "2debbb4f32d7d189819f2413f0f7d05b9bdcf497451a1c578475e3f2ec6c8554",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "747d893d6ddd91cf24d3b62b7eb50e0af9b1715ab355bbfc2e10ed0dc171ec17",
|
||||
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "46ceb5a169de3eec3a335b05ef4aa7b1b341a5ac06ad325bd191e862c5b6be70",
|
||||
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50",
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1"
|
||||
},
|
||||
"real_control_cohort": {
|
||||
"preparation_ms": 18876.764036,
|
||||
"query_samples": [
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 346.7870870000006
|
||||
},
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 18.34765800000241
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 11.56626999999935
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 227.17703500000061
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 251.5148330000011
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 10.888227999999799
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"elapsed_seconds": 31.347,
|
||||
"cpu_seconds": 31.05517,
|
||||
"peak_cgroup_bytes": 1589915648,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"baseline_public": "Quoted and cooked-template cases retained 200000-unit normalized literals and failed; addition and small exact controls passed. Aggregate-capacity test passed.",
|
||||
"baseline_domain_corrected": "2 over-bound failures, 16 passed. Initial combined-run domain assertions had incorrectly required an empty optional resourceLimits array; corrected before using the domain reproduction as evidence.",
|
||||
"fixed_focused": "55 passed across five files.",
|
||||
"semantic_suite": "923 passed across 36 files.",
|
||||
"capacity_after_fixture_cleanup": "1 passed; all 16 bounded values, aggregate size >= original, typed identities and retained tracing preserved.",
|
||||
"interim_static_failure": "Typecheck rejected resume() on the new test SDK stderr Stream declaration. Switched to its declared data event, matching existing typed tests; analyzer source unchanged.",
|
||||
"public_boundary_after_stream_type_fix": "4 passed across actual CLI and stdio MCP; analyzer source unchanged."
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "Projection/default correction; final review subsequently found unnormalized request endpoints",
|
||||
"revision": "38d263a369e7853ccfae1c8e0a3bd387f920c7cf",
|
||||
"source_sha256": {
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
|
||||
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "844d4639401a9f050ce42c80192572a6d0d74fe4f81b37e93a2c20aaca9fb9a6",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
|
||||
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
|
||||
"src/domain/javascript/javascriptSemanticDataEffects.ts": "e69408f2f5910a0107fd5d0cae0ff60ed706448857bc8e535c37cac1a0cd4d37",
|
||||
"src/domain/javascript/javascriptSemanticIr.ts": "68dad360d0a9e6bd72108b2717cae756b84ca1efb48455248ded2ff699d2c113",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
|
||||
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
|
||||
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "070106115b482269794799e448bd20b8e02df1f511f4ceb2b8fa75612c7f78e4",
|
||||
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
|
||||
},
|
||||
"real_control_cohort": {
|
||||
"preparation_ms": 19599.643274,
|
||||
"query_samples": [
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 396.9265450000021
|
||||
},
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 15.658120000000054
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 12.24905100000251
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 237.03392900000108
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 357.78818299999693
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 14.37622100000226
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"elapsed_seconds": 33.969,
|
||||
"cpu_seconds": 33.499223,
|
||||
"peak_cgroup_bytes": 1893015552,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
|
||||
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
|
||||
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
|
||||
"final_focused": "66 passed in five files.",
|
||||
"final_semantic_suite": "932 passed in 36 files.",
|
||||
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
|
||||
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
|
||||
"endpoint_review_baseline": "Production at 38d263a: 7 failed/14 passed across 13 new domain and 8 CLI/MCP cases before endpoint normalization. The later public linked-response case was not in this baseline run. Regression tests were uncommitted; final formatted test hashes are preserved, not claimed as the baseline test bytes.",
|
||||
"endpoint_selection_attempt": "First six-file selection exited before execution because a signed-zero test filename was mistyped; corrected the exact path. No test result claimed for that attempt."
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "Rejected endpoint normalization: violated existing full-endpoint lookup/trace contract; reverted after CI acceptance failure",
|
||||
"revision": "580887209c4fceda432eac4083fac82a0dd78f15",
|
||||
"source_sha256": {
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
|
||||
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "a648f97e631985edcb5259ff4f1fdf0332dca18b8c10b57e050e36a785c1c524",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.requestBudget.test.ts": "8e84ffa70d834f5589d04118f0aaf33b0383b15ea97545ff69c66a74473aca73",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
|
||||
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
|
||||
"src/domain/javascript/javascriptSemanticDataEffects.ts": "eed961e0b1d8a320e76bbba933408e950b4a4a446110401e8cf470f1e74d8315",
|
||||
"src/domain/javascript/javascriptSemanticIr.ts": "3b2ede73dbcfc74c4014e4d1affcbf00ecccc29a422ed495a7a3f2f517056c9b",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
|
||||
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
|
||||
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "61319ea954de7f256841f43ca9355a1706ce8e93d30f2a8ba851d5e717bd19e7",
|
||||
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
|
||||
},
|
||||
"real_control_cohort": {
|
||||
"preparation_ms": 19939.160272,
|
||||
"query_samples": [
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 348.3974309999976
|
||||
},
|
||||
{
|
||||
"round": 0,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 13.97052899999835
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 12.060379000002285
|
||||
},
|
||||
{
|
||||
"round": 1,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 212.01217800000086
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "legacy",
|
||||
"elapsed_ms": 223.73954800000138
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"mode": "references",
|
||||
"elapsed_ms": 10.450989999997546
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"elapsed_seconds": 33.062,
|
||||
"cpu_seconds": 32.71624,
|
||||
"peak_cgroup_bytes": 1897406464,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
|
||||
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
|
||||
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
|
||||
"final_focused": "81 passed in six files; the same CLI/MCP file then passed 9/9 after adding a linked-response case. That follow-up replaces its earlier 8-case coverage; 82 distinct cases across the final six-file selection, not one 82-case invocation.",
|
||||
"final_semantic_suite": "945 passed in 37 files, overlapping focused domain coverage.",
|
||||
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
|
||||
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
|
||||
"endpoint_review_baseline": "Production at 38d263a: 7 failed/14 passed across 13 new domain and 8 CLI/MCP cases before endpoint normalization. The later public linked-response case was not in this baseline run. Regression tests were uncommitted; final formatted test hashes are preserved, not claimed as the baseline test bytes.",
|
||||
"endpoint_selection_attempt": "First six-file selection exited before execution because a signed-zero test filename was mistyped; corrected the exact path. No test result claimed for that attempt.",
|
||||
"endpoint_typecheck_attempt": "Full static gate found the inferred optional dataField in the public test computed key. Replaced the in-property check with an explicit bare === true discriminant; scenario inputs and runtime implementation unchanged. Corrected static/public checks recorded separately.",
|
||||
"final_gates": "Full static gate passed after the explicit test discriminant; final public file passed 9/9 again, then docs gate passed including 5 consistency tests. Runtime implementation unchanged after the 945-test semantic suite and real control."
|
||||
},
|
||||
"ci_failure": {
|
||||
"run_id": 38758262643,
|
||||
"job_id": 116503076738,
|
||||
"test": "tests/acceptance/applications/javascriptEndpointPayload.test.ts",
|
||||
"result": "1 failed/1573 passed/19 skipped; request endpoint no longer matched its complete source value. Other local tests and real Obsidian control passed but did not establish this compatibility contract.",
|
||||
"aggregate_result": "Merged source/built report: 1 failed/15625 passed/138 skipped. Portable tests and CI required failed as aggregates of the same endpoint assertion; no separate additional failed test."
|
||||
}
|
||||
}
|
||||
],
|
||||
"review_baseline_revision": "40b9d3c5b9cbeae7661d8e351e1b5ef6e5338931",
|
||||
"review_baseline_source_sha256": {
|
||||
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
|
||||
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "ee541e634264363822bf4ce05c42c72272029c7e8da72bbeed643ad1341d0756",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a1cf791a61ee4742c351c006b69b64eba6bef3068f7a8450ed8f9366bfe4ff88",
|
||||
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "747d893d6ddd91cf24d3b62b7eb50e0af9b1715ab355bbfc2e10ed0dc171ec17",
|
||||
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "5ee7af18dd847726d8fe1828ea25f386ff9ef317d996a3f7969253bf73973cfa",
|
||||
"src/domain/javascript/javascriptSemanticDataEffects.ts": "4c62462016246bc7198294a74a60e87598fc009c53f1601abc2d4662d41b5bd1",
|
||||
"src/domain/javascript/javascriptSemanticIr.ts": "14955cb4b7912ed6a4f26f804a2480f841dcb6084dbddd15856375fa62cdcfa1",
|
||||
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
|
||||
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
|
||||
"src/domain/javascript/javascriptSemanticState.ts": "d94c5a88bb105284fab192406a8c88860d59f80c93394ed3ee7264969df254a6",
|
||||
"src/domain/javascript/javascriptSemanticValues.ts": "2debbb4f32d7d189819f2413f0f7d05b9bdcf497451a1c578475e3f2ec6c8554",
|
||||
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "46ceb5a169de3eec3a335b05ef4aa7b1b341a5ac06ad325bd191e862c5b6be70",
|
||||
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
|
||||
},
|
||||
"implementation": "Per-analysis state records encountered resource classifications at expression/default normalization; final coverage consumes those facts. Slot projection preserves tagged unknown values. Removed retained-value recursive coverage scan and duplicate raw default reader. Defaults retain optional valueResourceLimit and graph value_resource_limit plus an anchored resource-limit unknown; known null/false/0/empty remain exact. Endpoint identities intentionally remain exact lookup values, outside the normalized primitive lattice; rejected endpoint normalization and its CI failure are preserved separately.",
|
||||
"restored_production_revision": "38d263a369e7853ccfae1c8e0a3bd387f920c7cf",
|
||||
"final_corrected_resource_receipts": {
|
||||
"public_7": {
|
||||
"elapsed_seconds": 43.621,
|
||||
"cpu_seconds": 43.097453,
|
||||
"peak_cgroup_bytes": 1520324608,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"static_and_docs": {
|
||||
"elapsed_seconds": 111.403,
|
||||
"cpu_seconds": 110.830839,
|
||||
"peak_cgroup_bytes": 4268244992,
|
||||
"payload_cleanup_verified": true,
|
||||
"events_at_payload_exit": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# Semantic literal byte-budget verification
|
||||
|
||||
Tracks #3933 under #4027. All normalized semantic literals and configuration
|
||||
defaults use the primitive owner. Its singleton path validates strings/nonfinite
|
||||
numbers/negative zero without a Set, sorting or canonical-key serialization.
|
||||
Each source analysis owns a bounded set of encountered limit classifications;
|
||||
coverage consumes these observations after evaluation. This replaces a recursive
|
||||
scan that only saw retained binding/return values. No persistent cache is added.
|
||||
|
||||
## Correctness and coverage
|
||||
|
||||
Public CLI and actual stdio MCP cases cover quoted strings, cooked templates,
|
||||
equivalent concatenation, member receivers, a configuration-only expression
|
||||
statement and a small exact control. Direct/default oversized values become
|
||||
`resource-limit` unknowns with partial application coverage and
|
||||
`javascript_semantic_primitive_bytes = 1048576`. Defaults carry
|
||||
`value_resource_limit` inline and an unknown anchored to their config node.
|
||||
Known null/false/zero/empty defaults remain exact. Original source/digests and
|
||||
CLI/MCP identity parity are preserved.
|
||||
|
||||
Member and destructuring projections use the shared slot owner's tagged value.
|
||||
An encountered limit stays visible even when a subsequent projection selects a
|
||||
healthy sibling: the selected value remains exactly `7`, while file coverage is
|
||||
partial. This avoids inferring completeness from the final retained values alone.
|
||||
The duplicate raw default reader and superseded retained-value scan are removed.
|
||||
|
||||
Boundary controls admit 174,762 UTF-16 units (1,048,574 estimated JSON bytes)
|
||||
and reject 174,763 (1,048,580 bytes). Actual arithmetic corrected the earlier
|
||||
published 1,048,578 total; the unit-count boundary assertions were already correct.
|
||||
The estimate counts six bytes per unit plus two quotes. It does not establish
|
||||
actual ASCII JSON size above 1 MiB or an observed OOM.
|
||||
|
||||
The initial main baseline reproduced both public literal failures, with addition
|
||||
and small controls passing. A mistaken optional-limit-array control was corrected
|
||||
before the separate domain reproduction (2 failed/16 passed). Review head
|
||||
`40b9d3c5` then reproduced the additional projection/default gaps (8 failed,
|
||||
25 passed). The first review correction passed 65/66 focused tests; a doubling
|
||||
chain now truthfully retains an additional encountered expression-depth limit,
|
||||
so its byte-growth assertion was updated to allow other recorded limits.
|
||||
Prior validation on the restored runtime: **66 focused tests** in five files and **932 semantic tests**
|
||||
in 36 files, with overlapping domain coverage. The large-wire fixture retains
|
||||
16 individually admitted values and at least the former aggregate UTF-16 payload,
|
||||
plus complete CLI/MCP delivery, typed identities, empty strings and retained
|
||||
tracing. Inputs regenerate from the tests.
|
||||
|
||||
## Endpoint identity compatibility
|
||||
|
||||
An attempted endpoint normalization at `5808872` passed its new local cases and real Obsidian control, but failed the existing CI endpoint-payload acceptance workflow (1 failed/1,573 passed/19 skipped in that shard). It violated the established full endpoint lookup/trace contract. This extension was reverted; endpoint values are lookup identities, outside the normalized primitive lattice. The original large endpoint is preserved. Corrected public validation passed **7/7** across two files; this adds one acceptance workflow to the previous 66-case focused selection (**67 distinct cases**, not one 67-case invocation). Its acceptance workflow now also normalizes that identical string as an exported primitive, requiring the typed value bound while endpoint lookup, both trace workflows and version comparison remain usable. Prior measurements and failed attempts remain in the JSON record. Production source is restored byte-identical to `38d263a`; the 932-test semantic run belongs to that state. Final full static and documentation gates (five consistency tests) passed after the compatibility correction.
|
||||
|
||||
## Real application control and costs
|
||||
|
||||
Official Obsidian 1.14.4 ASAR, selected `package.json` and `main.js`. Follow the
|
||||
acquisition/digest and existing verifier command in
|
||||
[module-reference verification](javascript-module-references-2026-10-15.md).
|
||||
Use three alternating-order query pairs with CLI parity on each revision.
|
||||
Application code is not executed.
|
||||
|
||||
The final correction retains the exact baseline full parent/view Evidence IDs,
|
||||
23-row fact digest, source coverage and unknowns. Static `cl100k_base` task costs
|
||||
remain 16,548 with selected name/description/inputSchema contracts, or 204,093
|
||||
with the full catalog projection, for the reference workflow. Serial legacy
|
||||
reads also keep their original costs. The query projection improvement belongs
|
||||
to #4097; these are controls for semantic normalization and coverage.
|
||||
|
||||
| Complete real verifier | Before | Final correction |
|
||||
| ---------------------- | ------------: | ---------------: |
|
||||
| Elapsed seconds | 33.394 | 32.949 |
|
||||
| CPU seconds | 33.016802 | 32.559187 |
|
||||
| Peak cgroup bytes | 1,979,863,040 | 1,880,096,768 |
|
||||
|
||||
Includes initial analysis, both query arms, export, CLI parity and tokenizer.
|
||||
One CPU, 8 GiB hard cap, 6 GiB early stop, no swap; cleanup verified and no OOM.
|
||||
Memory includes charged file cache, not summed RSS. Counters are at payload exit
|
||||
while the observer remains alive. Fresh MCP sessions do not establish OS-cold
|
||||
cache conditions. Shared-host scheduling and filesystem/build warmness were
|
||||
uncontrolled. This one before/after control establishes neither a speedup nor
|
||||
an equivalence bound. Static BPE excludes annotations/output schemas, client
|
||||
rendering/replay, reasoning and final answers; no hosted agent was run. Linux
|
||||
static ASAR analysis was exercised, not live Electron or other platforms.
|
||||
|
||||
[Compact records, samples, source hashes and prior attempts](javascript-literal-byte-budget-2026-10-15.json)
|
||||
preserve the initial incomplete correction separately from the final measurements.
|
||||
No downloadable application or complete bundle is archived. Reproduce boundary
|
||||
outcomes through the listed public tests and reuse the existing real verifier
|
||||
and preserved 8 GiB observer linked above. Observer hashes and result-affecting
|
||||
touched source hashes are pinned in the records.
|
||||
@@ -138,6 +138,7 @@ export const SCOPES = {
|
||||
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
|
||||
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
|
||||
"tests/boundary/process/ProcessEvidence.host.test.ts",
|
||||
"tests/boundary/process/packageNpmInvocation.test.ts",
|
||||
"tests/boundary/process/windowsOutputCapture.test.ts",
|
||||
"tests/acceptance/webModuleCli.test.ts",
|
||||
"tests/acceptance/webSourceLocationCli.test.ts",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
@@ -17,6 +18,25 @@ const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
|
||||
/** Promisified child_process execution helper. */
|
||||
export const exec = promisify(execFile);
|
||||
|
||||
/** Run npm through its JavaScript entry point so Windows command scripts are not required. */
|
||||
export const execNpm = (arguments_, options = {}) => {
|
||||
const environment = options.env ?? process.env;
|
||||
const selectedEntry = environment.npm_execpath;
|
||||
if (
|
||||
selectedEntry !== undefined &&
|
||||
(typeof selectedEntry !== "string" || selectedEntry.length === 0)
|
||||
)
|
||||
throw new Error("npm_execpath must name the selected npm CLI entry point");
|
||||
const entry =
|
||||
selectedEntry ??
|
||||
(process.platform === "win32"
|
||||
? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
|
||||
: undefined);
|
||||
return entry === undefined
|
||||
? exec("npm", arguments_, options)
|
||||
: exec(process.execPath, [entry, ...arguments_], options);
|
||||
};
|
||||
|
||||
/** Run a command and return its stdout. */
|
||||
export const run = async (command, args, env) =>
|
||||
(await exec(command, args, { env })).stdout;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// A single inspection can include native admission plus several WABT commands.
|
||||
// The aggregate verifier covers seven complete fixtures and their failure paths.
|
||||
export const WABT_OPERATION_TIMEOUT_MS = 4 * 60_000;
|
||||
export const WABT_WORKFLOW_TIMEOUT_MS = 15 * 60_000;
|
||||
@@ -2,7 +2,7 @@ import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { exec, json, run } from "./lib/verify-package-core.mjs";
|
||||
import { exec, execNpm, json, run } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
|
||||
export async function verifyPackageInstall({
|
||||
@@ -11,8 +11,7 @@ export async function verifyPackageInstall({
|
||||
workspace,
|
||||
environment,
|
||||
}) {
|
||||
await exec(
|
||||
"npm",
|
||||
await execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -53,8 +52,7 @@ export async function verifyPackageInstall({
|
||||
`packaged PTY backend failed without lifecycle scripts: ${JSON.stringify(processCaptureCapability)}`,
|
||||
);
|
||||
const noOptionalPrefix = join(workspace, "prefix-no-optional");
|
||||
await exec(
|
||||
"npm",
|
||||
await execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -108,7 +106,7 @@ export async function verifyPackageInstall({
|
||||
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
|
||||
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
|
||||
const repackedTarball = (
|
||||
await exec("npm", ["pack", "--silent"], {
|
||||
await execNpm(["pack", "--silent"], {
|
||||
cwd: installedPackageRoot,
|
||||
env: environment,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
||||
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { exec, execNpm } from "./lib/verify-package-core.mjs";
|
||||
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
||||
|
||||
/** Create a tarball and assert production packaging constraints. */
|
||||
@@ -12,13 +12,9 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
const tarball = join(
|
||||
packDirectory,
|
||||
(
|
||||
await exec(
|
||||
"npm",
|
||||
["pack", "--silent", "--pack-destination", packDirectory],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
)
|
||||
await execNpm(["pack", "--silent", "--pack-destination", packDirectory], {
|
||||
cwd: root,
|
||||
})
|
||||
).stdout.trim(),
|
||||
);
|
||||
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
|
||||
|
||||
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { execNpm } from "./lib/verify-package-core.mjs";
|
||||
|
||||
if (process.platform !== "win32" || process.arch !== "x64")
|
||||
throw new Error(
|
||||
@@ -82,15 +83,10 @@ let packageRoot = resolve(packageRootArgument ?? ".");
|
||||
// package roots support an already-installed artifact without a second install.
|
||||
if (packageRootArgument === undefined) {
|
||||
try {
|
||||
const npmEntry =
|
||||
process.env.npm_execpath ??
|
||||
join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js");
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await exec(
|
||||
process.execPath,
|
||||
await execNpm(
|
||||
[
|
||||
npmEntry,
|
||||
"pack",
|
||||
"--json",
|
||||
"--ignore-scripts",
|
||||
@@ -107,10 +103,8 @@ if (packageRootArgument === undefined) {
|
||||
"The package lane must produce one artifact.",
|
||||
);
|
||||
const prefix = join(workspace, "installed");
|
||||
await exec(
|
||||
process.execPath,
|
||||
await execNpm(
|
||||
[
|
||||
npmEntry,
|
||||
"install",
|
||||
"--prefix",
|
||||
prefix,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { join } from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
||||
import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { exec, execNpm } from "./lib/verify-package-core.mjs";
|
||||
import { WABT_WORKFLOW_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
|
||||
import { verifyPackagePack } from "./verify-package-pack.mjs";
|
||||
|
||||
const root = await PrivateRuntimeRoot.create({
|
||||
@@ -23,8 +24,7 @@ try {
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
XDG_CACHE_HOME: join(home, ".cache"),
|
||||
};
|
||||
await exec(
|
||||
"npm",
|
||||
await execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -49,7 +49,11 @@ try {
|
||||
const verified = await exec(
|
||||
process.execPath,
|
||||
["scripts/verify-wasm-artifact.mjs", entrypoint, "--require-tools"],
|
||||
{ env: environment, timeout: 180_000, maxBuffer: 4 * 1024 * 1024 },
|
||||
{
|
||||
env: environment,
|
||||
timeout: WABT_WORKFLOW_TIMEOUT_MS,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
process.stdout.write(verified.stdout);
|
||||
} finally {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import {
|
||||
inspectMipsReadelf,
|
||||
expectedMipsElfProfile,
|
||||
inspectLegacyMipsReadelf,
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
@@ -120,6 +121,17 @@ async function verifySoftFloat(call, symbols, byteOrder) {
|
||||
);
|
||||
return { storage: boundaries };
|
||||
}
|
||||
function assertIncompleteMipsInspection(error) {
|
||||
assert.equal(error.code, "capability_unavailable");
|
||||
assert.equal(error.details.selection_reason, "target_unsupported");
|
||||
const rejection = error.details.rejections.find(
|
||||
(candidate) => candidate.provider_id === "ghidra",
|
||||
);
|
||||
assert.equal(rejection?.code, "architecture_unsupported");
|
||||
assert.match(rejection.reason, /ABI declaration inspection is incomplete/u);
|
||||
assert.match(rejection.reason, /section header table/u);
|
||||
}
|
||||
|
||||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
@@ -475,28 +487,9 @@ try {
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(snapshot.target.architecture, "mips");
|
||||
const abi = independent.mips.abiFlags;
|
||||
assert.deepEqual(
|
||||
snapshot.binding.analysis_profile.parameters.mips_elf,
|
||||
{
|
||||
elf_class: independent.mips.elfClass,
|
||||
byte_order: independent.mips.byteOrder,
|
||||
type: independent.mips.type,
|
||||
flags: independent.mips.flags,
|
||||
abi_flags: {
|
||||
version: abi.version,
|
||||
isa_level: abi.isaLevel,
|
||||
isa_revision: abi.isaRevision,
|
||||
gpr_size: abi.gprSize,
|
||||
cpr1_size: abi.cpr1Size,
|
||||
cpr2_size: abi.cpr2Size,
|
||||
fp_abi: abi.fpAbi,
|
||||
isa_extension: abi.isaExtension,
|
||||
ases: abi.ases,
|
||||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
},
|
||||
expectedMipsElfProfile(independent.mips),
|
||||
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
|
||||
);
|
||||
assert.equal(
|
||||
@@ -617,7 +610,8 @@ try {
|
||||
// PSP keeps its independent Allegrex/A-B-A checks; carved PSP is not verified.
|
||||
let carved;
|
||||
if (!psp) {
|
||||
// Carved firmware keeps all program headers/load bytes but loses sections.
|
||||
// A program-header ABI record survives carving, but GNU declarations in
|
||||
// uncaptured section headers remain unknown: both adapters must refuse.
|
||||
const read16 = (offset) =>
|
||||
byteOrder === "little"
|
||||
? sourceBytes.readUInt16LE(offset)
|
||||
@@ -655,79 +649,50 @@ try {
|
||||
carvedResolved.value.mips.limitations.join(" "),
|
||||
/section header table/u,
|
||||
);
|
||||
const carvedTarget = await call("open_binary", {
|
||||
path: carvedPath,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(carvedTarget.sha256, carvedSha256);
|
||||
const carvedImage = await call("inspect_native_load_image");
|
||||
assertMipsLoadedImage(
|
||||
carvedImage.observations,
|
||||
byteOrder,
|
||||
carvedSha256,
|
||||
);
|
||||
const carvedProcedures = await call("list_procedures");
|
||||
assert.ok(
|
||||
carvedProcedures.some(
|
||||
(p) =>
|
||||
BigInt(p.address) === BigInt(independent.symbols.rea_mips_entry),
|
||||
const carvedError = requireMcpToolError(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: carvedPath, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
);
|
||||
const carvedDossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
procedure: independent.symbols.rea_mips_entry,
|
||||
}),
|
||||
);
|
||||
assert.ok(carvedDossier.pseudocode.trim().length > 0);
|
||||
const carvedSnapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
|
||||
);
|
||||
await call("close_binary", { snapshot_path: carvedSnapshotPath });
|
||||
const carvedSnapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(carvedSnapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(carvedSnapshot.target.sha256, carvedSha256);
|
||||
assertIncompleteMipsInspection(carvedError);
|
||||
let carvedCliError;
|
||||
try {
|
||||
await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
carvedPath,
|
||||
independent.symbols.rea_mips_entry,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 60000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
} catch (cause) {
|
||||
carvedCliError = cause;
|
||||
}
|
||||
assert.ok(
|
||||
carvedSnapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
item.includes("section header table"),
|
||||
),
|
||||
),
|
||||
);
|
||||
const carvedCli = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
carvedPath,
|
||||
independent.symbols.rea_mips_entry,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const carvedEvidence = parseEvidence(JSON.parse(carvedCli.stdout));
|
||||
assert.equal(carvedEvidence.subject?.digest.sha256, carvedSha256);
|
||||
assert.ok(
|
||||
carvedEvidence.limitations.some((item) =>
|
||||
item.includes("section header table"),
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
functionDossierSchema.parse(carvedEvidence.normalized_result)
|
||||
.procedure.address,
|
||||
carvedDossier.procedure.address,
|
||||
carvedCliError &&
|
||||
Number.isInteger(carvedCliError.code) &&
|
||||
carvedCliError.code !== 0,
|
||||
"Expected an incomplete-ABI refusal, not a timeout or success",
|
||||
);
|
||||
assertIncompleteMipsInspection(JSON.parse(carvedCliError.stdout));
|
||||
assert.deepEqual(await readFile(carvedPath), carvedBytes);
|
||||
carved = {
|
||||
sha256: carvedSha256,
|
||||
source_bytes: carvedBytes.length,
|
||||
section_header_offset: sectionOffset,
|
||||
effective_language: carvedImage.observations.language_id,
|
||||
status: "expected-incomplete-abi-refusal",
|
||||
limitations: carvedResolved.value.mips.limitations,
|
||||
procedures: carvedProcedures.length,
|
||||
cli_exit: carvedCliError.code,
|
||||
mcp_error: carvedError,
|
||||
};
|
||||
}
|
||||
reports.push({
|
||||
|
||||
@@ -83,8 +83,11 @@ const FIXTURE_SOURCE = String.raw`
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
static const char rea_kuna_utf8[] __attribute__((used)) = "_φ( °-°)/ REA unicode marker";
|
||||
/* The leading NUL unit ends any UTF-16 run that bytes before this array would
|
||||
start: the UTF-8 marker's last byte and NUL decode as the unit 'r', so on
|
||||
layouts where it precedes this array Kuna reports "rREA wide marker". */
|
||||
static const unsigned short rea_kuna_wide[] __attribute__((used)) = {
|
||||
'R','E','A',' ','w','i','d','e',' ','m','a','r','k','e','r',0
|
||||
0,'R','E','A',' ','w','i','d','e',' ','m','a','r','k','e','r',0
|
||||
};
|
||||
#include <string.h>
|
||||
__attribute__((noinline)) unsigned rea_checksum(const unsigned char *p, size_t n) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cp, mkdir, readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
|
||||
import { exec, pathExists } from "./lib/verify-package-core.mjs";
|
||||
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Exercise Skills' default root discovery without ignored build outputs. */
|
||||
export async function verifyRepositorySkill({ root, workspace }) {
|
||||
@@ -34,8 +34,7 @@ export async function verifyRepositorySkill({ root, workspace }) {
|
||||
await mkdir(consumer, { recursive: true });
|
||||
await mkdir(home, { recursive: true });
|
||||
const skillsVersion = "1.7.2";
|
||||
await exec(
|
||||
"npm",
|
||||
await execNpm(
|
||||
[
|
||||
"exec",
|
||||
"--yes",
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
requireMcpToolError,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { WABT_OPERATION_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
|
||||
|
||||
const directory = process.env.REA_WABT_BIN_DIRECTORY;
|
||||
if (!isAbsolute(directory ?? "")) {
|
||||
@@ -76,6 +77,8 @@ try {
|
||||
throw cause;
|
||||
}
|
||||
const client = new Client({ name: "wabt-artifact-verifier", version: "1" });
|
||||
const callTool = (request) =>
|
||||
client.callTool(request, { timeout: WABT_OPERATION_TIMEOUT_MS });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
@@ -128,7 +131,7 @@ try {
|
||||
TMP: childTempRoot,
|
||||
TEMP: childTempRoot,
|
||||
},
|
||||
timeout: 120_000,
|
||||
timeout: WABT_OPERATION_TIMEOUT_MS,
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
},
|
||||
)
|
||||
@@ -238,7 +241,7 @@ try {
|
||||
const cliValue = await inspectCli(input);
|
||||
assert.equal(cliValue.category, undefined, JSON.stringify(cliValue));
|
||||
const cli = parseEvidence(cliValue);
|
||||
const reply = await client.callTool({
|
||||
const reply = await callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: input,
|
||||
});
|
||||
@@ -332,7 +335,7 @@ try {
|
||||
},
|
||||
);
|
||||
}
|
||||
const bundleReply = await client.callTool({
|
||||
const bundleReply = await callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
@@ -362,7 +365,7 @@ try {
|
||||
JSON.stringify(cliError),
|
||||
);
|
||||
const error = requireMcpToolError(
|
||||
await client.callTool({
|
||||
await callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: invalidInput,
|
||||
}),
|
||||
@@ -370,7 +373,7 @@ try {
|
||||
assert.equal(error.category, "invalid_input");
|
||||
const partial = error.details.partial_observation;
|
||||
assert.equal(partial.kind, "retained-evidence");
|
||||
const failedBundleReply = await client.callTool({
|
||||
const failedBundleReply = await callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
@@ -18,7 +18,10 @@ import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
import {
|
||||
execNpm,
|
||||
verifyCompleteToolCatalog,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { verifyPackageUpdate } from "./verify-package-update.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
@@ -32,9 +35,9 @@ let report;
|
||||
try {
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await npm(
|
||||
await execNpm(
|
||||
["pack", "--json", "--silent", "--pack-destination", workspace],
|
||||
root,
|
||||
{ cwd: root, windowsHide: true },
|
||||
)
|
||||
).stdout,
|
||||
);
|
||||
@@ -63,9 +66,9 @@ try {
|
||||
|
||||
const tarball = join(workspace, packageResult.filename);
|
||||
await access(tarball);
|
||||
await npm(
|
||||
await execNpm(
|
||||
["install", "--no-package-lock", "--no-save", "--prefix", prefix, tarball],
|
||||
workspace,
|
||||
{ cwd: workspace, windowsHide: true },
|
||||
);
|
||||
const entry = join(
|
||||
prefix,
|
||||
@@ -209,9 +212,9 @@ try {
|
||||
throw new Error(`Packaged uninstall did not remove ${id}`);
|
||||
|
||||
if (process.platform !== "win32")
|
||||
await npm(
|
||||
await execNpm(
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
workspace,
|
||||
{ cwd: workspace, windowsHide: true },
|
||||
);
|
||||
const update = await verifyPackageUpdate({
|
||||
prefix,
|
||||
@@ -241,16 +244,3 @@ try {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
|
||||
);
|
||||
|
||||
function npm(arguments_, cwd) {
|
||||
const npmExecPath = process.env.npm_execpath;
|
||||
return npmExecPath === undefined
|
||||
? exec(process.platform === "win32" ? "npm.cmd" : "npm", arguments_, {
|
||||
cwd,
|
||||
windowsHide: true,
|
||||
})
|
||||
: exec(process.execPath, [npmExecPath, ...arguments_], {
|
||||
cwd,
|
||||
windowsHide: true,
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
import { semanticResourceLimitReason } from "../../domain/javascript/javascriptSemanticResourceLimits.js";
|
||||
|
||||
/** Project environment, argv, file, default, and precedence candidates. */
|
||||
export const projectSemanticConfiguration = (
|
||||
@@ -56,7 +57,17 @@ export const projectSemanticConfiguration = (
|
||||
...(operation.kind === "default" ? (["overrides"] as const) : []),
|
||||
],
|
||||
location: operation.location,
|
||||
detail: `Static ${operation.kind} configuration identity is partial.`,
|
||||
...(operation.valueResourceLimit === undefined
|
||||
? {
|
||||
detail:
|
||||
operation.kind === "default"
|
||||
? "Static default configuration value is partial."
|
||||
: `Static ${operation.kind} configuration identity is partial.`,
|
||||
}
|
||||
: {
|
||||
reason: "resource-limit",
|
||||
detail: semanticResourceLimitReason(operation.valueResourceLimit),
|
||||
}),
|
||||
candidateNodeIds: [],
|
||||
});
|
||||
}
|
||||
@@ -80,6 +91,9 @@ const addConfigurationNode = (
|
||||
key: operation.key,
|
||||
source_kind: operation.kind,
|
||||
value: operation.value,
|
||||
...(operation.valueResourceLimit === undefined
|
||||
? {}
|
||||
: { value_resource_limit: operation.valueResourceLimit }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -279,6 +293,7 @@ const bindingOrOwner = (
|
||||
: (context.callableNodes.get(callableId) ?? context.moduleNode));
|
||||
|
||||
interface DataUnknownInput {
|
||||
readonly reason?: "ambiguous-target" | "resource-limit";
|
||||
readonly context: SemanticFlowProjectionContext;
|
||||
readonly node: JavaScriptSemanticGraphNode;
|
||||
readonly family: "boundary" | "configuration" | "request";
|
||||
@@ -317,7 +332,7 @@ const addDataUnknown = (input: DataUnknownInput): void => {
|
||||
node_id: node.node_id,
|
||||
family,
|
||||
relation_kinds: relationKinds,
|
||||
reason: "ambiguous-target",
|
||||
reason: input.reason ?? "ambiguous-target",
|
||||
detail,
|
||||
candidate_node_ids: candidateNodeIds,
|
||||
evidence: unknownSemanticEvidence(context.file, location),
|
||||
|
||||
@@ -84,6 +84,8 @@ export const analyzeInterfaceBuilderBundle = async (input: {
|
||||
recordOmittedArchive(entry.path, "max_documents");
|
||||
unreportedOmittedArchives += selection.unreportedOmitted;
|
||||
for (const entry of selection.selected) {
|
||||
// Plist representation reserved for this document before decoding.
|
||||
let plistReservation = 0;
|
||||
try {
|
||||
const remainingInputBytes =
|
||||
MAX_AGGREGATE_INPUT_BYTES - aggregateInputBytes;
|
||||
@@ -132,6 +134,7 @@ export const analyzeInterfaceBuilderBundle = async (input: {
|
||||
decodedReservation.estimatedBytes,
|
||||
"plist representation exceeds the aggregate Interface Builder decode budget",
|
||||
);
|
||||
plistReservation = decodedReservation.estimatedBytes;
|
||||
binaryPrototypeKeyObserved =
|
||||
decodedReservation.binaryPrototypeKeyObserved;
|
||||
}
|
||||
@@ -165,6 +168,9 @@ export const analyzeInterfaceBuilderBundle = async (input: {
|
||||
} catch (cause: unknown) {
|
||||
if (input.signal?.aborted === true) throw cause;
|
||||
if (cause instanceof InterfaceBuilderDecodeBudgetExceeded) {
|
||||
// A document rejected while decoding, e.g. beyond the shared depth
|
||||
// limit, retains no representation; later documents keep its share.
|
||||
decodeBudget.release(plistReservation);
|
||||
omitted += 1;
|
||||
recordOmittedArchive(entry.path, cause.reason);
|
||||
continue;
|
||||
@@ -700,6 +706,24 @@ const readEntry = async (
|
||||
return Buffer.concat(chunks, length);
|
||||
};
|
||||
|
||||
/**
|
||||
* The shared parser bounds plist value nesting before its recursive decode and
|
||||
* reports the limit as a RangeError; that is an omitted archive, not a
|
||||
* malformed one.
|
||||
*/
|
||||
const parseXmlWithinDecodeBudget = (text: string) => {
|
||||
try {
|
||||
return parseXmlPropertyList(text);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof RangeError)
|
||||
throw new InterfaceBuilderDecodeBudgetExceeded(
|
||||
"aggregate_decode_budget_exhausted",
|
||||
cause.message,
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/** Project decoded plist data and dates while retaining omitted-key coverage. */
|
||||
const decodePlist = (
|
||||
bytes: Buffer,
|
||||
@@ -716,7 +740,7 @@ const decodePlist = (
|
||||
value: parseBinary(bytes),
|
||||
omittedPrototypeKeys: binaryPrototypeKeyObserved ? null : 0,
|
||||
}
|
||||
: parseXmlPropertyList(xmlText ?? decodeXmlPlistText(bytes));
|
||||
: parseXmlWithinDecodeBudget(xmlText ?? decodeXmlPlistText(bytes));
|
||||
const projected = projectPlistValue(value);
|
||||
return {
|
||||
value: projected.value,
|
||||
|
||||
@@ -49,6 +49,14 @@ export class InterfaceBuilderDecodeBudget {
|
||||
this.#remainingBytes -= bytes;
|
||||
}
|
||||
|
||||
/** Return a reservation whose representation was rejected and not retained. */
|
||||
release(bytes: number): void {
|
||||
this.#remainingBytes = Math.min(
|
||||
this.#initialBytes,
|
||||
this.#remainingBytes + bytes,
|
||||
);
|
||||
}
|
||||
|
||||
/** Reserve an upper bound for one JSON-serialized string occurrence. */
|
||||
reserveJsonStringOccurrence(value: string): void {
|
||||
// JSON escaping uses at most six ASCII bytes per UTF-16 code unit, plus
|
||||
@@ -117,7 +125,8 @@ const estimateXmlPlistExpansion = (
|
||||
const xml = xmlText ?? decodeXmlPlistText(bytes);
|
||||
const decodedSourceBytes = xml.length * 2;
|
||||
let elementCount = 0;
|
||||
let xmlDepth = 0;
|
||||
// Value nesting is bounded by the shared plist parser, which counts plist
|
||||
// value depth; raw element depth would also count the envelope and keys.
|
||||
for (const token of xmlPreflightTokens(xml)) {
|
||||
if (token.doctype) {
|
||||
// Only a declaration outside opaque text can introduce an internal
|
||||
@@ -130,15 +139,6 @@ const estimateXmlPlistExpansion = (
|
||||
continue;
|
||||
}
|
||||
elementCount += 1;
|
||||
if (token.closing) xmlDepth -= 1;
|
||||
else if (!token.selfClosing) {
|
||||
xmlDepth += 1;
|
||||
if (xmlDepth > 128)
|
||||
throw new InterfaceBuilderDecodeBudgetExceeded(
|
||||
"aggregate_decode_budget_exhausted",
|
||||
"XML plist nesting exceeds the aggregate Interface Builder decode budget",
|
||||
);
|
||||
}
|
||||
if (decodedSourceBytes * 2 + elementCount * 128 > maxBytes)
|
||||
return maxBytes + 1;
|
||||
}
|
||||
@@ -179,13 +179,7 @@ function* xmlPreflightTokens(xml: string) {
|
||||
}
|
||||
if (end === xml.length) return;
|
||||
cursor = end + 1;
|
||||
if (doctype || element)
|
||||
yield {
|
||||
doctype,
|
||||
internalSubset,
|
||||
closing: xml[start + 1] === "/",
|
||||
selfClosing: xml[end - 1] === "/",
|
||||
};
|
||||
if (doctype || element) yield { doctype, internalSubset };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,43 @@ describe("source-map artifact relationships", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a directory named like a source map contained by its parent", () => {
|
||||
const archive = createOccurrence(entry("app", "directory"), "app", null);
|
||||
archive.artifact_id = contentId("1");
|
||||
const script = createOccurrence(
|
||||
entry("app/main.js", "file"),
|
||||
"app/main.js",
|
||||
archive.occurrence_id,
|
||||
);
|
||||
script.artifact_id = contentId("2");
|
||||
const directory = createOccurrence(
|
||||
entry("app/main.js.map", "directory"),
|
||||
"app/main.js.map",
|
||||
archive.occurrence_id,
|
||||
);
|
||||
directory.artifact_id = contentId("3");
|
||||
|
||||
const { edges, limitations } = createArtifactEdges(contentId("4"), [
|
||||
archive,
|
||||
script,
|
||||
directory,
|
||||
]);
|
||||
|
||||
expect(directory).toMatchObject({
|
||||
artifact_kind: "container",
|
||||
artifact_format: "directory",
|
||||
});
|
||||
expect(
|
||||
edges.find(
|
||||
({ occurrence_id }) => occurrence_id === directory.occurrence_id,
|
||||
),
|
||||
).toMatchObject({
|
||||
relation: "contains",
|
||||
parent_artifact_id: contentId("1"),
|
||||
});
|
||||
expect(limitations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const fat = (wide = false): Buffer =>
|
||||
|
||||
@@ -295,7 +295,9 @@ export const createArtifactEdges = (
|
||||
const edges: ArtifactEdge[] = [];
|
||||
const limitations: string[] = [];
|
||||
for (const occurrence of occurrences) {
|
||||
const isSourceMap = occurrence.logical_path.toLowerCase().endsWith(".map");
|
||||
// Use the occurrence's own classification: a directory named `x.map` is
|
||||
// a container, not a source map of its sibling.
|
||||
const isSourceMap = occurrence.artifact_kind === "source-map";
|
||||
const sourcePath = isSourceMap
|
||||
? occurrence.logical_path.slice(0, -".map".length)
|
||||
: undefined;
|
||||
@@ -337,8 +339,10 @@ export const createArtifactEdges = (
|
||||
relation:
|
||||
occurrence.entry_kind === "slice"
|
||||
? ("slice-of" as const)
|
||||
: isSourceMap && mappedSource === undefined
|
||||
? ("contains" as const)
|
||||
: isSourceMap
|
||||
? mappedSource === undefined
|
||||
? ("contains" as const)
|
||||
: ("maps-source" as const)
|
||||
: relationFor(occurrence.logical_path),
|
||||
occurrence_id: occurrence.occurrence_id,
|
||||
};
|
||||
@@ -587,11 +591,8 @@ const classifyFatArtifact = (
|
||||
return identity.state === "mach-o" ? "mach-o-universal" : "unknown";
|
||||
};
|
||||
|
||||
const relationFor = (path: string): ArtifactEdge["relation"] => {
|
||||
const lower = path.toLowerCase();
|
||||
if (lower.endsWith(".map")) return "maps-source";
|
||||
if (lower.includes(".framework/")) return "embeds";
|
||||
return "contains";
|
||||
};
|
||||
/** Relation of a non-source-map occurrence to its parent. */
|
||||
const relationFor = (path: string): ArtifactEdge["relation"] =>
|
||||
path.toLowerCase().includes(".framework/") ? "embeds" : "contains";
|
||||
|
||||
const depth = (path: string): number => path.split("/").length;
|
||||
@@ -156,6 +156,90 @@ describe("compiled Interface Builder graph projection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyed archive objects with unresolved classes", () => {
|
||||
it("keeps an archived object whose class reference cannot be resolved", () => {
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 99 }, label: "Unresolved object" },
|
||||
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
|
||||
{ $class: { UID: 2 }, "NS.string": "ok" },
|
||||
"plain scalar",
|
||||
],
|
||||
$top: { root: { UID: 3 } },
|
||||
});
|
||||
// The class descriptor and the scalar are not archived objects.
|
||||
expect(parsed.objects).toMatchObject([
|
||||
{
|
||||
id: "1",
|
||||
kind: "other",
|
||||
class_name: null,
|
||||
name: "Unresolved object",
|
||||
attributes: { label: "Unresolved object", $class: { UID: 99 } },
|
||||
},
|
||||
{ id: "3", kind: "view", class_name: "UIView" },
|
||||
]);
|
||||
expect(parsed.objects[1]?.attributes).not.toHaveProperty("$class");
|
||||
expect(parsed.objectCount).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps an unresolved hierarchy root's label and coverage in the graph", () => {
|
||||
const result = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "Main.nib",
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib",
|
||||
raw: {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 99 }, label: "Unresolved object" },
|
||||
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
|
||||
{ $class: { UID: 2 }, "NS.string": "ok" },
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||
});
|
||||
const objectNodes = result.graph.nodes.filter(
|
||||
({ attributes }) => attributes.interface_builder_object_id !== undefined,
|
||||
);
|
||||
expect(objectNodes).toMatchObject([
|
||||
{
|
||||
kind: "unknown",
|
||||
name: "Unresolved object",
|
||||
attributes: { interface_builder_object_id: "1", class_name: null },
|
||||
},
|
||||
{ kind: "view", attributes: { interface_builder_object_id: "3" } },
|
||||
]);
|
||||
expect(result.documents[0]?.object_count).toBe(2);
|
||||
expect(
|
||||
result.graph.edges.some(
|
||||
({ relation, to, resolution }) =>
|
||||
relation === "contains" &&
|
||||
to === objectNodes[0]?.id &&
|
||||
resolution === "observed",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "objects:Main.nib",
|
||||
status: "complete",
|
||||
examined: 2,
|
||||
omitted: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interface Builder parser bounds", () => {
|
||||
it("reports connections discarded by the parser's hard ceiling", () => {
|
||||
const connections = Array.from({ length: 40_001 }, (_, index) => ({
|
||||
|
||||
@@ -327,11 +327,16 @@ const parseKeyedArchive = (
|
||||
if (index === 0) continue;
|
||||
const resolved = record(resolve(raw));
|
||||
const className = firstString(resolved.className);
|
||||
if (className === null) continue;
|
||||
// An archived object names its class with its own `$class` reference;
|
||||
// when that reference cannot be resolved the object is still observed,
|
||||
// with an unknown class. Scalars and class descriptors are not objects.
|
||||
if (className === null && !Object.hasOwn(record(raw), "$class")) continue;
|
||||
objectCount += 1;
|
||||
const fields = Object.fromEntries(
|
||||
Object.entries(resolved).filter(([key]) => key !== "className"),
|
||||
);
|
||||
// Keep the unresolvable class reference itself, e.g. a dangling UID.
|
||||
if (className === null) fields.$class = record(raw).$class;
|
||||
const authoredId = firstString(fields.objectID, fields["object-id"]);
|
||||
const objectId =
|
||||
authoredId ??
|
||||
@@ -357,6 +362,7 @@ const parseKeyedArchive = (
|
||||
}),
|
||||
);
|
||||
if (
|
||||
className !== null &&
|
||||
/(?:Outlet|Connection|Segue|ActionConnection|ControlConnector)/iu.test(
|
||||
className,
|
||||
)
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
} from "./javascriptElectronMemberWrites.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticCoverage } from "./javascriptSemanticCoverage.js";
|
||||
import { semanticResourceLimitsIn } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
parseJavaScriptSource,
|
||||
type ParsedJavaScriptSource,
|
||||
@@ -197,12 +196,7 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
...derived,
|
||||
coverage: semanticCoverage(
|
||||
parserPartial,
|
||||
semanticResourceLimitsIn([
|
||||
...bindings.map(({ value }) => value),
|
||||
...callables.flatMap((callable) =>
|
||||
callable.returnSites.map(({ value }) => value),
|
||||
),
|
||||
]),
|
||||
[...state.valueResourceLimits].sort(),
|
||||
),
|
||||
limitations: [
|
||||
...(parserPartial
|
||||
@@ -698,6 +692,7 @@ const createState = (
|
||||
return {
|
||||
program,
|
||||
staticImportResolutionMode,
|
||||
valueResourceLimits: new Set(),
|
||||
parentsByNode: new WeakMap(),
|
||||
scopes: [root],
|
||||
scopesById: new Map([[root.scopeId, root]]),
|
||||
|
||||
@@ -219,7 +219,120 @@ describe("logical primitive unions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("semantic primitive resource observations", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "member",
|
||||
source: (large: string) =>
|
||||
`const answer = (${JSON.stringify(large)}).field;`,
|
||||
},
|
||||
{
|
||||
name: "destructuring",
|
||||
source: (large: string) =>
|
||||
`const { nested: { answer } } = { nested: ${JSON.stringify(large)} };`,
|
||||
},
|
||||
])("preserves a limiting value through $name projection", ({ source }) => {
|
||||
const ir = analyzeJavaScriptSemantics(source("x".repeat(200_000)));
|
||||
expect(topLevelBinding(ir, "answer").value).toMatchObject({
|
||||
status: "unknown",
|
||||
resourceLimit: "primitive-bytes",
|
||||
});
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
resourceLimits: ["primitive-bytes"],
|
||||
});
|
||||
});
|
||||
|
||||
it("records an encountered bound without weakening a selected healthy sibling", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`const answer = ({ ignored: ${JSON.stringify("x".repeat(200_000))}, kept: 7 }).kept;`,
|
||||
);
|
||||
expect(topLevelBinding(ir, "answer").value).toEqual({
|
||||
status: "literal",
|
||||
value: 7,
|
||||
});
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
resourceLimits: ["primitive-bytes"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("configuration default normalization", () => {
|
||||
it.each(["quoted", "template"])(
|
||||
"normalizes a bare %s configuration default",
|
||||
(syntax) => {
|
||||
const large = "x".repeat(200_000);
|
||||
const expression =
|
||||
syntax === "quoted" ? JSON.stringify(large) : `\`${large}\``;
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`process.env.MODE ?? ${expression};`,
|
||||
);
|
||||
const operation = ir.configurationOperations.find(
|
||||
({ kind }) => kind === "default",
|
||||
);
|
||||
expect(
|
||||
typeof operation?.value === "string" ? operation.value.length : 0,
|
||||
).toBeLessThanOrEqual(174_762);
|
||||
expect(operation).toMatchObject({
|
||||
value: null,
|
||||
resolution: "partial",
|
||||
valueResourceLimit: "primitive-bytes",
|
||||
});
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
resourceLimits: ["primitive-bytes"],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([null, false, 0, ""])(
|
||||
"keeps an exact bounded configuration default %j",
|
||||
(value) => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`process.env.MODE ?? ${JSON.stringify(value)};`,
|
||||
);
|
||||
expect(
|
||||
ir.configurationOperations.find(({ kind }) => kind === "default"),
|
||||
).toMatchObject({ value, resolution: "complete" });
|
||||
expect(ir.coverage.resourceLimits ?? []).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("semantic primitive allocation and binding reuse", () => {
|
||||
it.each(["quoted", "template"])(
|
||||
"applies the normalized byte boundary to %s literals",
|
||||
(syntax) => {
|
||||
// Six estimated JSON bytes per UTF-16 code unit plus two quotes:
|
||||
// 174762 code units fit; one more exceeds the 1 MiB bound.
|
||||
for (const length of [174_762, 174_763]) {
|
||||
const text = "x".repeat(length);
|
||||
const expression =
|
||||
syntax === "quoted" ? JSON.stringify(text) : `\`${text}\``;
|
||||
const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`);
|
||||
const value = topLevelBinding(ir, "answer").value;
|
||||
if (length === 174_762) {
|
||||
expect(value.status).toBe("literal");
|
||||
if (value.status !== "literal")
|
||||
throw new Error("Expected exact value");
|
||||
expect(typeof value.value === "string" && value.value.length).toBe(
|
||||
length,
|
||||
);
|
||||
expect(ir.coverage.resourceLimits ?? []).toEqual([]);
|
||||
} else {
|
||||
expect(value.status).toBe("unknown");
|
||||
expect(value).toMatchObject({ resourceLimit: "primitive-bytes" });
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
omittedCount: null,
|
||||
resourceLimits: ["primitive-bytes"],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("returns an explicit unknown before expanding a large addition product", () => {
|
||||
const term = '(choice ? "a" : "b")';
|
||||
const expression = Array.from({ length: 20 }, () => term).join(" + ");
|
||||
@@ -256,7 +369,7 @@ describe("semantic primitive allocation and binding reuse", () => {
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
omittedCount: null,
|
||||
resourceLimits: ["primitive-bytes"],
|
||||
resourceLimits: expect.arrayContaining(["primitive-bytes"]),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -915,15 +915,6 @@ export const dataEffectArgumentBindingId = (
|
||||
? (resolveSemanticBindingState(state, node, node.name)?.bindingId ?? null)
|
||||
: null;
|
||||
|
||||
/** Read one JSON primitive default without evaluating code. */
|
||||
export const dataEffectPrimitive = (
|
||||
node: t.Node,
|
||||
): string | number | boolean | null => {
|
||||
if (t.isStringLiteral(node) || t.isNumericLiteral(node)) return node.value;
|
||||
if (t.isBooleanLiteral(node)) return node.value;
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read one static string or expression-free template. A template recovered
|
||||
* with an invalid escape has no cooked value; its raw spelling is not a
|
||||
|
||||
@@ -8,11 +8,13 @@ import type {
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyKey,
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
recordSemanticValueLimit,
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
@@ -22,7 +24,6 @@ import {
|
||||
dataEffectArgumentBindingId as argumentBindingId,
|
||||
dataEffectLiteralString as literalString,
|
||||
dataEffectMemberCallee as memberCallee,
|
||||
dataEffectPrimitive as primitiveValue,
|
||||
isSemanticProcessObject as isProcessObject,
|
||||
outerDataEffectBinding as outerAssignedBinding,
|
||||
assignedSemanticResultBindings,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
type DataEffectTraversalContext as TraversalContext,
|
||||
} from "./javascriptSemanticDataEffectHelpers.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js";
|
||||
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
interface DataEffectAnalysis {
|
||||
readonly configurationOperations: readonly JavaScriptSemanticConfigurationOperation[];
|
||||
@@ -175,6 +178,11 @@ const collectDefaults = (
|
||||
const source = unwrapJavaScriptExpression(node.left).node;
|
||||
const candidate = configurationsByNode.get(source);
|
||||
if (candidate === undefined) return;
|
||||
const literal = readExactJavaScriptLiteral(node.right);
|
||||
const value: JavaScriptSemanticValue = literal.found
|
||||
? semanticPrimitiveSet([literal.value])
|
||||
: { status: "unknown", reason: "Default is not an exact primitive." };
|
||||
recordSemanticValueLimit(context.state, value);
|
||||
output.push({
|
||||
configId: `config:default:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
||||
kind: "default",
|
||||
@@ -183,8 +191,11 @@ const collectDefaults = (
|
||||
resultBindingId: outerAssignedBinding(node, context),
|
||||
key: candidate.operation.key,
|
||||
sourceConfigId: candidate.operation.configId,
|
||||
value: primitiveValue(node.right),
|
||||
resolution: primitiveValue(node.right) === null ? "partial" : "complete",
|
||||
value: value.status === "literal" ? value.value : null,
|
||||
...(value.status === "unknown" && value.resourceLimit !== undefined
|
||||
? { valueResourceLimit: value.resourceLimit }
|
||||
: {}),
|
||||
resolution: value.status === "literal" ? "complete" : "partial",
|
||||
});
|
||||
});
|
||||
return output;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { JavaScriptSourceRange } from "./javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
|
||||
import type {
|
||||
JavaScriptSemanticResourceLimit,
|
||||
JavaScriptSemanticValue,
|
||||
} from "./javascriptSemanticValueTypes.js";
|
||||
import type {
|
||||
JavaScriptSemanticCoverage,
|
||||
JavaScriptSemanticReturnCoverage,
|
||||
@@ -302,6 +305,7 @@ export interface JavaScriptSemanticConfigurationOperation {
|
||||
readonly key: string | null;
|
||||
readonly sourceConfigId: string | null;
|
||||
readonly value: string | number | boolean | null;
|
||||
readonly valueResourceLimit?: JavaScriptSemanticResourceLimit;
|
||||
readonly resolution: "complete" | "partial";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export const semanticPrimitiveSet = (
|
||||
values: readonly JavaScriptSemanticPrimitive[],
|
||||
): JavaScriptSemanticValue => {
|
||||
const uniqueValues = new Set<JavaScriptSemanticPrimitive>();
|
||||
let uniqueValues: Set<JavaScriptSemanticPrimitive> | undefined;
|
||||
for (const value of values) {
|
||||
if (typeof value === "number" && !Number.isFinite(value))
|
||||
return {
|
||||
@@ -26,10 +26,19 @@ export const semanticPrimitiveSet = (
|
||||
status: "unknown",
|
||||
reason: "Negative zero cannot be preserved by JSON primitive evidence.",
|
||||
};
|
||||
// The common literal path needs validation, but no deduplication, sorting
|
||||
// or canonical-key allocation. Check strings before retaining the value.
|
||||
if (values.length === 1)
|
||||
return typeof value === "string" &&
|
||||
exceedsSemanticPrimitiveStringByteBudget([value])
|
||||
? semanticResourceLimitUnknown("primitive-bytes")
|
||||
: { status: "literal", value };
|
||||
uniqueValues ??= new Set<JavaScriptSemanticPrimitive>();
|
||||
uniqueValues.add(value);
|
||||
if (uniqueValues.size > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT)
|
||||
return semanticResourceLimitUnknown("primitive-candidates");
|
||||
}
|
||||
if (uniqueValues === undefined) return { status: "union", values: [] };
|
||||
if (
|
||||
exceedsSemanticPrimitiveStringByteBudget(
|
||||
[...uniqueValues].flatMap((value) =>
|
||||
|
||||
@@ -77,25 +77,6 @@ export const semanticResourceLimitUnknown = (
|
||||
resourceLimit,
|
||||
});
|
||||
|
||||
/** Collect resource-limit classifications from nested semantic values. */
|
||||
export const semanticResourceLimitsIn = (
|
||||
values: readonly JavaScriptSemanticValue[],
|
||||
): readonly JavaScriptSemanticResourceLimit[] => {
|
||||
const found = new Set<JavaScriptSemanticResourceLimit>();
|
||||
const pending = [...values];
|
||||
while (pending.length > 0) {
|
||||
const value = pending.pop();
|
||||
if (value === undefined) continue;
|
||||
if (value.status === "unknown" && value.resourceLimit !== undefined)
|
||||
found.add(value.resourceLimit);
|
||||
else if (value.status === "object")
|
||||
for (const property of value.properties) pending.push(property.value);
|
||||
else if (value.status === "array")
|
||||
for (const item of value.items) pending.push(item.value);
|
||||
}
|
||||
return [...found].sort();
|
||||
};
|
||||
|
||||
/** Display label for one stable resource classification. */
|
||||
export const semanticResourceLimitReason = (
|
||||
resourceLimit: JavaScriptSemanticResourceLimit,
|
||||
|
||||
@@ -14,6 +14,10 @@ import type {
|
||||
JavaScriptSemanticReference,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutionMode.js";
|
||||
import type {
|
||||
JavaScriptSemanticResourceLimit,
|
||||
JavaScriptSemanticValue,
|
||||
} from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
/** Internal initializer plus a destructuring/member projection. */
|
||||
interface JavaScriptSemanticInitializer {
|
||||
@@ -75,6 +79,8 @@ export interface JavaScriptSemanticAnalysisState {
|
||||
readonly program: t.Program;
|
||||
/** Effective loader for static imports in this source artifact. */
|
||||
readonly staticImportResolutionMode: JavaScriptModuleResolutionMode;
|
||||
/** Bounds encountered during this analysis, independent of later projections. */
|
||||
readonly valueResourceLimits: Set<JavaScriptSemanticResourceLimit>;
|
||||
readonly parentsByNode: WeakMap<t.Node, t.Node>;
|
||||
readonly scopes: JavaScriptSemanticScopeState[];
|
||||
readonly scopesById: Map<string, JavaScriptSemanticScopeState>;
|
||||
@@ -110,6 +116,15 @@ export interface JavaScriptSemanticAnalysisState {
|
||||
readonly ambientWriteEffects: JavaScriptSemanticMemberEffect[];
|
||||
}
|
||||
|
||||
/** Record one producer outcome without traversing or retaining its value graph. */
|
||||
export const recordSemanticValueLimit = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
value: JavaScriptSemanticValue,
|
||||
): void => {
|
||||
if (value.status === "unknown" && value.resourceLimit !== undefined)
|
||||
state.valueResourceLimits.add(value.resourceLimit);
|
||||
};
|
||||
|
||||
/** One parsed source owns its optional indexes and their per-source lifetime. */
|
||||
export interface JavaScriptBindingIndex {
|
||||
readonly sourceType: t.Program["sourceType"];
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
JavaScriptSemanticValue,
|
||||
} from "./javascriptSemanticValueTypes.js";
|
||||
import {
|
||||
recordSemanticValueLimit,
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
@@ -451,6 +452,15 @@ const evaluateBinding = (
|
||||
const evaluateExpression = (
|
||||
node: t.Node,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptSemanticValue => {
|
||||
const value = evaluateExpressionValue(node, context);
|
||||
recordSemanticValueLimit(context.state, value);
|
||||
return value;
|
||||
};
|
||||
|
||||
const evaluateExpressionValue = (
|
||||
node: t.Node,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptSemanticValue => {
|
||||
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
@@ -471,10 +481,7 @@ const evaluateExpression = (
|
||||
});
|
||||
}
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
if (literal.found)
|
||||
return typeof literal.value === "number"
|
||||
? primitiveSet([literal.value])
|
||||
: { status: "literal", value: literal.value };
|
||||
if (literal.found) return primitiveSet([literal.value]);
|
||||
if (t.isIdentifier(node)) {
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
return binding === undefined
|
||||
@@ -757,11 +764,12 @@ const projectValue = (
|
||||
value: JavaScriptSemanticValue,
|
||||
projection: readonly (string | number | null)[],
|
||||
): JavaScriptSemanticValue => {
|
||||
if (isSemanticResourceLimit(value)) return value;
|
||||
if (projection.includes(null))
|
||||
return { status: "unknown", reason: "Cannot project a dynamic property." };
|
||||
const path = projection.map((key) => String(key));
|
||||
const slot = semanticSlotAtPath(value, path);
|
||||
return slot.presence === "present"
|
||||
return slot.presence === "present" || isSemanticResourceLimit(slot.value)
|
||||
? slot.value
|
||||
: {
|
||||
status: "unknown",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { join } from "node:path";
|
||||
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from "./KunaProjectFiles.js";
|
||||
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
|
||||
import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js";
|
||||
import { kunaUnavailable } from "./KunaUnavailable.js";
|
||||
|
||||
/** The exact producing range for one facet, including a failed C block. */
|
||||
export interface KunaProjectFileSpan {
|
||||
@@ -134,8 +134,7 @@ export class KunaProjectReader {
|
||||
const span = index.value.code.get(address);
|
||||
if (span === undefined)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
kunaUnavailable(
|
||||
operation,
|
||||
`Kuna's export lists no code at ${address}; import slots and data entries have no listing.`,
|
||||
),
|
||||
|
||||
@@ -8,10 +8,7 @@ import type {
|
||||
ProviderAvailability,
|
||||
ProviderTargetSupport,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
createAnalysisProfile,
|
||||
type AnalysisProfileCommitment,
|
||||
@@ -33,6 +30,7 @@ import {
|
||||
import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js";
|
||||
import { KunaRunSlots } from "./KunaRunSlots.js";
|
||||
import { KunaSessionClient } from "./KunaSessionClient.js";
|
||||
import { kunaUnavailable } from "./KunaUnavailable.js";
|
||||
|
||||
/** Architectures REA verified Kuna on: x86-64 and AArch64 ELF and Mach-O, plus ARM Thumb. */
|
||||
const VERIFIED_ARCHITECTURES: ReadonlySet<string> = new Set([
|
||||
@@ -169,14 +167,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
if (!installation.ok)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
"open_binary",
|
||||
installation.error.reason,
|
||||
{ userMessage: installation.error.reason },
|
||||
),
|
||||
);
|
||||
return err(kunaUnavailable("open_binary", installation.error.reason));
|
||||
const { value } = installation;
|
||||
return ok({
|
||||
profile: createAnalysisProfile(
|
||||
@@ -218,13 +209,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
if (!configuration.ok)
|
||||
return {
|
||||
execute: async (operation) =>
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
operation,
|
||||
configuration.error.reason,
|
||||
),
|
||||
),
|
||||
err(kunaUnavailable(operation, configuration.error.reason)),
|
||||
close: async () => ok(null),
|
||||
};
|
||||
return new KunaSessionClient(target, profile, {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
} from "./KunaReferenceFiles.js";
|
||||
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
|
||||
import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js";
|
||||
import { kunaUnavailable } from "./KunaUnavailable.js";
|
||||
|
||||
/** Charge raw JSON bytes conservatively; retained projections are smaller. */
|
||||
const XREF_CACHE_BYTES = 16 * 1024 * 1024;
|
||||
@@ -158,8 +158,7 @@ export class KunaReferenceQueries {
|
||||
const node = graph.nodes.get(address);
|
||||
if (node === undefined)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
kunaUnavailable(
|
||||
operation,
|
||||
`Kuna's graph has no function node at ${address}; its relationships are unknown.`,
|
||||
),
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
KunaProjectReader,
|
||||
type KunaProjectFileSpan,
|
||||
} from "./KunaProjectReader.js";
|
||||
import { kunaUnavailable } from "./KunaUnavailable.js";
|
||||
|
||||
/** How often a request waiting on the project export reports that it still waits. */
|
||||
const WAIT_PROGRESS_INTERVAL_MS = 10_000;
|
||||
@@ -278,13 +279,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
options: ExecutionOptions = {},
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
if (this.#closing)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
operation,
|
||||
"This Kuna session is closed.",
|
||||
),
|
||||
);
|
||||
return err(kunaUnavailable(operation, "This Kuna session is closed."));
|
||||
return this.#own(
|
||||
this.#execute(operation, parameters, {
|
||||
...options,
|
||||
@@ -312,8 +307,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return err(new AnalysisCancelledError(operation));
|
||||
if (operation !== "health" && !isKunaOperation(operation))
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
kunaUnavailable(
|
||||
operation,
|
||||
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
|
||||
),
|
||||
@@ -1229,14 +1223,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
this.#installation ??= inspectKunaInstallation(this.#environment).then(
|
||||
(inspected): Result<KunaInstallation, AnalysisError> => {
|
||||
if (!inspected.ok)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
KUNA_PROVIDER_ID,
|
||||
"health",
|
||||
inspected.error.reason,
|
||||
{ userMessage: inspected.error.reason },
|
||||
),
|
||||
);
|
||||
return err(kunaUnavailable("health", inspected.error.reason));
|
||||
const committed = this.#profile?.parameters["kuna_sha256"];
|
||||
if (committed !== undefined && committed !== inspected.value.sha256)
|
||||
return err(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
|
||||
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
|
||||
|
||||
/**
|
||||
* A Kuna capability gap whose reason is what the caller needs to read. Without
|
||||
* a user message the projection shows only "choose another analysis or
|
||||
* target", which hides facts such as "no code at this address".
|
||||
*/
|
||||
export const kunaUnavailable = (
|
||||
operation: string,
|
||||
reason: string,
|
||||
options: AnalysisErrorOptions = {},
|
||||
): AnalysisCapabilityUnavailableError =>
|
||||
new AnalysisCapabilityUnavailableError(KUNA_PROVIDER_ID, operation, reason, {
|
||||
...options,
|
||||
userMessage: reason,
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -850,18 +851,18 @@ describe("application workflow CLI parity", () => {
|
||||
if (seed === undefined)
|
||||
throw new TypeError("Expected at least one semantic relation");
|
||||
|
||||
const traced = await runCli([
|
||||
const traced = await runCliWithRequestFile(
|
||||
root,
|
||||
"trace-javascript-semantics",
|
||||
JSON.stringify({
|
||||
{
|
||||
application: analyzed.value,
|
||||
query: {
|
||||
seed: { kind: "semantic-node", node_id: seed },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
},
|
||||
}),
|
||||
"--json",
|
||||
]);
|
||||
},
|
||||
);
|
||||
expect(traced).toMatchObject({
|
||||
operation: "trace_javascript_semantics",
|
||||
predicate_type: "rea.javascript-semantic-trace",
|
||||
@@ -1183,7 +1184,9 @@ describe("application workflow CLI copy boundaries", () => {
|
||||
|
||||
describe("application workflow CLI and MCP capture lifetimes", () => {
|
||||
it("compares captured primitives and source values after aliases are replaced", async () => {
|
||||
const root = await createTestTempDirectory("rea-capture-lifetime-cli-");
|
||||
const parent = await createTestTempDirectory("rea-capture-lifetime-cli-");
|
||||
const root = join(parent, "fixtures # capture");
|
||||
await mkdir(root);
|
||||
const fields = [
|
||||
"rebound",
|
||||
"rest",
|
||||
@@ -1247,7 +1250,9 @@ describe("application workflow CLI and MCP capture lifetimes", () => {
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"const module = await import(process.argv[1]); console.log(JSON.stringify(module.default()));",
|
||||
join(root, String(index), "parser.mjs"),
|
||||
// ESM import() takes a URL specifier; a Windows drive path parses as a
|
||||
// `c:` URL scheme and is rejected.
|
||||
pathToFileURL(join(root, String(index), "parser.mjs")).href,
|
||||
]);
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
kind: "capture-boundary",
|
||||
@@ -1300,18 +1305,18 @@ describe("application workflow CLI export Evidence", () => {
|
||||
'export default async function make() { return { kind: "record", extra: 1 }; }',
|
||||
];
|
||||
const [left, right] = await analyzeComparisonSources(root, sources);
|
||||
const compared = await runCli([
|
||||
const compared = await runCliWithRequestFile(
|
||||
root,
|
||||
"compare-javascript-export-shapes",
|
||||
JSON.stringify({
|
||||
{
|
||||
left,
|
||||
right,
|
||||
left_module_path: "parser.mjs",
|
||||
left_export_name: "default",
|
||||
right_module_path: "parser.mjs",
|
||||
right_export_name: "default",
|
||||
}),
|
||||
"--json",
|
||||
]);
|
||||
},
|
||||
);
|
||||
expect(compared).toMatchObject({
|
||||
operation: "compare_javascript_export_shapes",
|
||||
normalized_result: {
|
||||
@@ -1640,3 +1645,19 @@ const runCli = async (
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pass a request carrying complete Evidence through the CLI's file-backed JSON
|
||||
* input. Serialized Evidence exceeds the Windows command-line limit when it is
|
||||
* passed as one argument, so spawn fails with ENAMETOOLONG before the workflow
|
||||
* starts. Inline JSON transport keeps its own coverage in the parity tests.
|
||||
*/
|
||||
const runCliWithRequestFile = async (
|
||||
root: string,
|
||||
command: string,
|
||||
request: unknown,
|
||||
): Promise<unknown> => {
|
||||
const requestPath = join(root, "request.json");
|
||||
await writeFile(requestPath, JSON.stringify(request));
|
||||
return runCli([command, requestPath, "--json"]);
|
||||
};
|
||||
@@ -30,9 +30,12 @@ cliTest(
|
||||
surrogate,
|
||||
replacement,
|
||||
];
|
||||
const source = values
|
||||
const requests = values
|
||||
.map((value) => `fetch(${JSON.stringify(value)});`)
|
||||
.join("\n");
|
||||
// The primitive lattice is bounded; exact endpoint lookup identities stay
|
||||
// available for feature/semantic tracing and version comparison.
|
||||
const source = `${requests}\nexport const oversized = ${JSON.stringify(endpoint)};`;
|
||||
const sourcePath = join(root, "main.js");
|
||||
await writeFile(sourcePath, source);
|
||||
const cliResponse = await cli.run({
|
||||
@@ -79,6 +82,17 @@ cliTest(
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(result.graph.coverage.limits).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "javascript_semantic_primitive_bytes",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
result.semantic_graph.nodes.some(
|
||||
({ kind, properties }) =>
|
||||
kind === "literal" && properties.value === endpoint,
|
||||
),
|
||||
).toBe(false);
|
||||
const endpoints = result.graph.nodes.filter(
|
||||
(node) => node.kind === "endpoint",
|
||||
);
|
||||
@@ -121,18 +135,18 @@ cliTest(
|
||||
available: true,
|
||||
value: { kind: "source-range", source: "main.js" },
|
||||
});
|
||||
const requests = result.semantic_graph.nodes.filter(
|
||||
const requestNodes = result.semantic_graph.nodes.filter(
|
||||
(node) =>
|
||||
node.kind === "request" && node.properties.endpoint === endpoint,
|
||||
);
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requestNodes).toHaveLength(1);
|
||||
|
||||
await verifyEndpointWorkflows({
|
||||
client,
|
||||
evidenceId: evidence.evidence_id,
|
||||
endpoint,
|
||||
endpointNodeId: endpointNode.node_id,
|
||||
requestNodeIds: requests.map(({ node_id }) => node_id),
|
||||
requestNodeIds: requestNodes.map(({ node_id }) => node_id),
|
||||
root,
|
||||
source,
|
||||
sourcePath,
|
||||
|
||||
@@ -4,9 +4,10 @@ import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { buildBinary } from "plist";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js";
|
||||
import { InterfaceBuilderDecodeBudget } from "../../../src/artifacts/apple/InterfaceBuilderDecodeBudget.js";
|
||||
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
|
||||
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
||||
import { xcodeIbtoolAvailable } from "../../fixtures/ibtool.js";
|
||||
@@ -907,6 +908,57 @@ describe("aggregate Interface Builder archive retention budget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interface Builder XML plist depth", () => {
|
||||
it("bounds XML archives by plist value depth, not element depth", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-depth-test-");
|
||||
const bundle = join(root, "Example.app");
|
||||
const resources = join(bundle, "Contents", "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
// The root dict is value depth 0, so the leaf sits at `nested + 1`. The
|
||||
// plist envelope and keys add XML elements but not value depth.
|
||||
const document = (nested: number) =>
|
||||
`<?xml version="1.0"?><plist version="1.0"><dict>${"<key>child</key><dict>".repeat(nested)}<key>leaf</key><integer>1</integer>${"</dict>".repeat(nested)}</dict></plist>`;
|
||||
await writeFile(join(resources, "AtLimit.nib"), document(127));
|
||||
await writeFile(join(resources, "BeyondLimit.nib"), document(128));
|
||||
|
||||
const release = vi.spyOn(InterfaceBuilderDecodeBudget.prototype, "release");
|
||||
const result = await analyzeInterfaceBuilderBundle({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "e".repeat(64),
|
||||
});
|
||||
const released = release.mock.calls.map(([bytes]) => bytes);
|
||||
release.mockRestore();
|
||||
|
||||
// The rejected archive returns its reservation to later documents.
|
||||
expect(released).toHaveLength(1);
|
||||
expect(released[0]).toBeGreaterThan(0);
|
||||
expect(result.documents.map(({ relative_path }) => relative_path)).toEqual([
|
||||
"Contents/Resources/AtLimit.nib",
|
||||
]);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "archive_decode",
|
||||
status: "partial",
|
||||
reason: "aggregate_decode_budget_exhausted",
|
||||
examined: 2,
|
||||
omitted: 1,
|
||||
}),
|
||||
);
|
||||
expect(result.limitations).toContain(
|
||||
"Contents/Resources/BeyondLimit.nib: omitted because aggregate decode budget exhausted.",
|
||||
);
|
||||
});
|
||||
|
||||
it("restores released representation bytes up to the initial budget", () => {
|
||||
const budget = new InterfaceBuilderDecodeBudget(100);
|
||||
budget.reserve(60, "reserve");
|
||||
budget.release(60);
|
||||
expect(budget.remainingBytes).toBe(100);
|
||||
budget.release(10);
|
||||
expect(budget.remainingBytes).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bounded Interface Builder archive decoding", () => {
|
||||
it("counts malformed archives against the document limit", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-test-");
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { afterAll, beforeAll, expect } from "vitest";
|
||||
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const client = new Client({ name: "literal-budget-boundary", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
cwd: process.cwd(),
|
||||
env: { PATH: process.env.PATH ?? "" },
|
||||
stderr: "pipe",
|
||||
});
|
||||
beforeAll(async () => {
|
||||
await client.connect(transport);
|
||||
transport.stderr?.on("data", () => undefined);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await Promise.all([client.close(), transport.close()]);
|
||||
});
|
||||
|
||||
const large = "x".repeat(200_000);
|
||||
for (const scenario of [
|
||||
{ name: "quoted literal", expression: JSON.stringify(large), limited: true },
|
||||
{ name: "cooked template", expression: `\`${large}\``, limited: true },
|
||||
{
|
||||
name: "limited member receiver",
|
||||
expression: `(${JSON.stringify(large)}).field`,
|
||||
limited: true,
|
||||
},
|
||||
{
|
||||
name: "configuration-only default",
|
||||
expression: `process.env.MODE ?? ${JSON.stringify(large)}`,
|
||||
bare: true,
|
||||
limited: true,
|
||||
},
|
||||
{
|
||||
name: "equivalent addition",
|
||||
expression: `${JSON.stringify(large.slice(0, 100_000))} + ${JSON.stringify(large.slice(100_000))}`,
|
||||
limited: true,
|
||||
},
|
||||
{ name: "small exact control", expression: '"small"', limited: false },
|
||||
]) {
|
||||
cliTest(
|
||||
`preserves normalized string-byte coverage for ${scenario.name}`,
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-literal-budget-");
|
||||
const path = join(root, "main.js");
|
||||
const source =
|
||||
"bare" in scenario
|
||||
? `${scenario.expression};\n`
|
||||
: `export const payload = ${scenario.expression};\n`;
|
||||
await writeFile(path, source);
|
||||
const digest = createHash("sha256").update(source).digest("hex");
|
||||
const cliResponse = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(cliResponse.exitCode).toBe(0);
|
||||
const cliEvidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(cliResponse.json);
|
||||
const response = await client.callTool({
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: root },
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
).outputSchema.parse(response.structuredContent);
|
||||
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
|
||||
|
||||
for (const candidate of [cliEvidence, evidence]) {
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
candidate.normalized_result,
|
||||
);
|
||||
const stringLengths = analysis.semantic_graph.nodes.flatMap(
|
||||
({ properties }) =>
|
||||
typeof properties.value === "string"
|
||||
? [properties.value.length]
|
||||
: [],
|
||||
);
|
||||
expect(stringLengths).not.toContain(large.length);
|
||||
if (scenario.limited) {
|
||||
expect(analysis.graph.coverage.status).toBe("partial");
|
||||
expect(analysis.graph.coverage.limits).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "javascript_semantic_primitive_bytes",
|
||||
unit: "bytes",
|
||||
value: 1_048_576,
|
||||
}),
|
||||
);
|
||||
expect(analysis.semantic_graph.unknowns).toContainEqual(
|
||||
expect.objectContaining({
|
||||
reason: "resource-limit",
|
||||
detail: expect.stringMatching(
|
||||
/primitive string-byte budget exceeded/i,
|
||||
),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
expect(stringLengths).toContain(5);
|
||||
expect(analysis.graph.coverage.limits).toEqual([]);
|
||||
}
|
||||
if ("bare" in scenario) {
|
||||
const node = analysis.semantic_graph.nodes.find(
|
||||
({ kind, properties }) =>
|
||||
kind === "config-source" && properties.source_kind === "default",
|
||||
);
|
||||
expect(node?.properties).toMatchObject({
|
||||
value: null,
|
||||
value_resource_limit: "primitive-bytes",
|
||||
});
|
||||
expect(analysis.semantic_graph.unknowns).toContainEqual(
|
||||
expect.objectContaining({
|
||||
node_id: node?.node_id,
|
||||
reason: "resource-limit",
|
||||
}),
|
||||
);
|
||||
}
|
||||
expect(
|
||||
analysis.graph.nodes.find((node) => node.kind === "javascript-asset")
|
||||
?.identity,
|
||||
).toMatchObject({ sha256: digest });
|
||||
}
|
||||
expect(await readFile(path, "utf8")).toBe(source);
|
||||
await client.ping();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -11,13 +11,31 @@ import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascr
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const boundedLargeLiteralPayloads = (): readonly string[] => {
|
||||
// Preserve the former aggregate payload size while each normalized value
|
||||
// stays within its independent semantic byte budget.
|
||||
const values = Array.from({ length: 16 }, (_, index) =>
|
||||
`rea_literal_payload_ğ\\"\0${String(index)}_`.padEnd(24, "x").repeat(4096),
|
||||
);
|
||||
expect(
|
||||
values.reduce((total, value) => total + value.length, 0),
|
||||
).toBeGreaterThanOrEqual(24 * 65_536);
|
||||
expect(values.every((value) => value.length * 6 + 2 <= 1_048_576)).toBe(true);
|
||||
return values;
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"delivers complete large literals through CLI and default stdio MCP without metadata copies",
|
||||
"delivers aggregate large literal payloads through CLI and default stdio MCP without metadata copies",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-literal-payload-");
|
||||
const value = 'rea_literal_payload_ğ\\"\0'.padEnd(24, "x").repeat(65_536);
|
||||
const values = boundedLargeLiteralPayloads();
|
||||
const value = values[0];
|
||||
if (value === undefined) throw new Error("Missing literal payload");
|
||||
const source = [
|
||||
`export const payload = ${JSON.stringify(value)};`,
|
||||
...values.map(
|
||||
(payload, index) =>
|
||||
`export const payload${String(index)} = ${JSON.stringify(payload)};`,
|
||||
),
|
||||
'export const choice = flag ? 1 : "1";',
|
||||
'export const empty = "";',
|
||||
].join("\n");
|
||||
@@ -52,7 +70,7 @@ cliTest(
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: root, format: "directory" },
|
||||
});
|
||||
// The old payload-bearing role key/label exceed the default wire budget.
|
||||
// Default wire delivery must retain every admitted value in the aggregate.
|
||||
expect(response.isError).not.toBe(true);
|
||||
const evidence = toolContract(
|
||||
"analyze_javascript_application",
|
||||
@@ -67,6 +85,15 @@ cliTest(
|
||||
const graph = javascriptApplicationAnalysisResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
).semantic_graph;
|
||||
const retainedPayloads = graph.nodes
|
||||
.filter(
|
||||
(node) =>
|
||||
node.kind === "literal" &&
|
||||
typeof node.properties.value === "string" &&
|
||||
values.includes(node.properties.value),
|
||||
)
|
||||
.map((node) => node.properties.value);
|
||||
expect(retainedPayloads.toSorted()).toEqual(values.toSorted());
|
||||
const literal = graph.nodes.find(
|
||||
(node) => node.kind === "literal" && node.properties.value === value,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
it("runs the caller-selected npm entry through Node without rewriting its inputs", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-npm-invocation-"));
|
||||
try {
|
||||
const npmEntry = join(root, "selected npm entry.mjs");
|
||||
await writeFile(
|
||||
npmEntry,
|
||||
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd(), marker: process.env.REA_NPM_TEST_MARKER }));\n`,
|
||||
);
|
||||
const helper = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-core.mjs"),
|
||||
).href;
|
||||
const program = `
|
||||
const { execNpm } = await import(${JSON.stringify(helper)});
|
||||
const result = await execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: { ...process.env, npm_execpath: ${JSON.stringify(npmEntry)}, REA_NPM_TEST_MARKER: "caller-selected" },
|
||||
});
|
||||
process.stdout.write(result.stdout);
|
||||
`;
|
||||
const result = await execute(
|
||||
process.execPath,
|
||||
["--input-type=module", "--eval", program],
|
||||
{ windowsHide: true },
|
||||
);
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
arguments: ["pack", "value with spaces"],
|
||||
cwd: root,
|
||||
marker: "caller-selected",
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const windowsIt = process.platform === "win32" ? it : it.skip;
|
||||
|
||||
windowsIt(
|
||||
"uses the npm entry adjacent to Node when none is selected",
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-adjacent-npm-"));
|
||||
try {
|
||||
const node = join(root, "node.exe");
|
||||
const npmEntry = join(root, "node_modules/npm/bin/npm-cli.js");
|
||||
await mkdir(dirname(npmEntry), { recursive: true });
|
||||
await Promise.all([
|
||||
copyFile(process.execPath, node),
|
||||
writeFile(
|
||||
npmEntry,
|
||||
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd() }));\n`,
|
||||
),
|
||||
]);
|
||||
const helper = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-core.mjs"),
|
||||
).href;
|
||||
const program = `
|
||||
const { execNpm } = await import(${JSON.stringify(helper)});
|
||||
const environment = { ...process.env };
|
||||
delete environment.npm_execpath;
|
||||
const result = await execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: environment,
|
||||
});
|
||||
process.stdout.write(result.stdout);
|
||||
`;
|
||||
const environment = { ...process.env };
|
||||
delete environment.npm_execpath;
|
||||
const result = await execute(
|
||||
node,
|
||||
["--input-type=module", "--eval", program],
|
||||
{ env: environment, windowsHide: true },
|
||||
);
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
arguments: ["pack", "value with spaces"],
|
||||
cwd: root,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
rizinTestInferiorTty,
|
||||
} from "./RizinTestInferiorTty.js";
|
||||
import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
spawnOwnedProviderProcess,
|
||||
} from "../../../src/process/ProviderProcess.js";
|
||||
import type {
|
||||
ProviderProcessHandle,
|
||||
SpawnedOwnedProviderProcess,
|
||||
@@ -24,10 +28,11 @@ interface InjectedProcessControl {
|
||||
exit(signal?: NodeJS.Signals): void;
|
||||
}
|
||||
|
||||
type RizinTestManagerOptions = NonNullable<
|
||||
ConstructorParameters<typeof RizinDebugSessionManager>[1]
|
||||
>;
|
||||
type InjectedSpawnProcess = NonNullable<
|
||||
NonNullable<
|
||||
ConstructorParameters<typeof RizinDebugSessionManager>[1]
|
||||
>["spawnProcess"]
|
||||
RizinTestManagerOptions["spawnProcess"]
|
||||
>;
|
||||
|
||||
export const incompleteStop: typeof stopOwnedDebuggerProcess = async (
|
||||
@@ -176,17 +181,47 @@ export const createInjectedProcess = (
|
||||
return { spawnProcess, control };
|
||||
};
|
||||
|
||||
export const createRizinCoverageHarness = () => {
|
||||
let directory: string | undefined;
|
||||
export const createRizinCoverageHarness = (
|
||||
controls: {
|
||||
readonly stopOwnedProcess?: (
|
||||
supervisor: ProviderProcessSupervisor,
|
||||
) => ReturnType<ProviderProcessSupervisor["stop"]>;
|
||||
} = {},
|
||||
) => {
|
||||
let latestDirectory: string | undefined;
|
||||
const directories = new Set<string>();
|
||||
const managers: RizinDebugSessionManager[] = [];
|
||||
const track = (manager: RizinDebugSessionManager): void => {
|
||||
const ownedProcesses = new Set<ProviderProcessSupervisor>();
|
||||
const spawnOwnedProcess: InjectedSpawnProcess = async (options) => {
|
||||
const launched = await spawnOwnedProviderProcess(options);
|
||||
// The fixture owns these fake children independently of the manager's
|
||||
// intentionally uncertain debugger/inferior lifecycle.
|
||||
ownedProcesses.add(
|
||||
new ProviderProcessSupervisor(
|
||||
{ ...launched, ownsProcessLifetime: true },
|
||||
{ captureStdout: false, maxDiagnosticBytes: 0 },
|
||||
),
|
||||
);
|
||||
return launched;
|
||||
};
|
||||
const createManager: typeof createRizinTestManager = (
|
||||
environment = process.env,
|
||||
options = {},
|
||||
) => {
|
||||
const manager = createRizinTestManager(environment, {
|
||||
...options,
|
||||
spawnProcess: options.spawnProcess ?? spawnOwnedProcess,
|
||||
});
|
||||
managers.push(manager);
|
||||
return manager;
|
||||
};
|
||||
const makeProvider = async (
|
||||
body: string,
|
||||
shebang = "#!/usr/bin/env node",
|
||||
): Promise<string> => {
|
||||
directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
|
||||
directories.add(directory);
|
||||
latestDirectory = directory;
|
||||
const provider = join(directory, "fake-rizin.mjs");
|
||||
await writeFile(provider, `${shebang}\n${body}\n`);
|
||||
await chmod(provider, 0o700);
|
||||
@@ -194,13 +229,9 @@ export const createRizinCoverageHarness = () => {
|
||||
};
|
||||
const start = async (
|
||||
provider: string,
|
||||
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
|
||||
options: RizinTestManagerOptions = {},
|
||||
): Promise<{ manager: RizinDebugSessionManager; sessionId: string }> => {
|
||||
const manager = createRizinTestManager(
|
||||
{ REA_RIZIN_COMMAND: provider },
|
||||
options,
|
||||
);
|
||||
track(manager);
|
||||
const manager = createManager({ REA_RIZIN_COMMAND: provider }, options);
|
||||
const result = await manager.start({
|
||||
path: "/tmp/coverage-target",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
@@ -210,13 +241,12 @@ export const createRizinCoverageHarness = () => {
|
||||
};
|
||||
const startInjected = async (
|
||||
spawnProcess: InjectedSpawnProcess,
|
||||
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
|
||||
options: RizinTestManagerOptions = {},
|
||||
) => {
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ ...options, spawnProcess },
|
||||
);
|
||||
track(manager);
|
||||
const started = await manager.start({
|
||||
path: "/tmp/injected",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
@@ -229,17 +259,42 @@ export const createRizinCoverageHarness = () => {
|
||||
.splice(0)
|
||||
.map((manager) => manager.closeAll().catch(() => undefined)),
|
||||
);
|
||||
if (directory !== undefined)
|
||||
const failures: Error[] = [];
|
||||
for (const supervisor of ownedProcesses) {
|
||||
const stopped =
|
||||
controls.stopOwnedProcess === undefined
|
||||
? await supervisor.stop()
|
||||
: await controls.stopOwnedProcess(supervisor);
|
||||
if (stopped.status === "incomplete") {
|
||||
failures.push(
|
||||
new Error(`Fake Rizin cleanup incomplete: ${stopped.reason}`),
|
||||
);
|
||||
} else if (supervisor.observedExit() === undefined) {
|
||||
failures.push(
|
||||
new Error("Fake Rizin cleanup did not observe child exit"),
|
||||
);
|
||||
} else {
|
||||
ownedProcesses.delete(supervisor);
|
||||
}
|
||||
}
|
||||
// Keep ownership and fixture files available when cleanup needs a retry.
|
||||
if (failures.length > 0)
|
||||
throw new AggregateError(failures, "Fake Rizin fixture cleanup failed");
|
||||
for (const directory of directories) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
directory = undefined;
|
||||
directories.delete(directory);
|
||||
}
|
||||
latestDirectory = undefined;
|
||||
};
|
||||
const missingProviderPath = (): string => join(directory!, "does-not-exist");
|
||||
const missingProviderPath = (): string =>
|
||||
join(latestDirectory!, "does-not-exist");
|
||||
return {
|
||||
cleanup,
|
||||
makeProvider,
|
||||
missingProviderPath,
|
||||
start,
|
||||
startInjected,
|
||||
track,
|
||||
createManager,
|
||||
spawnOwnedProcess,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { access } from "node:fs/promises";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
stopFakeNoInferior,
|
||||
rizinTestInferiorTty,
|
||||
} from "./RizinTestInferiorTty.js";
|
||||
import { rizinCommand } from "../../../src/rizin/RizinCommand.js";
|
||||
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
||||
import * as providerProcess from "../../../src/process/ProviderProcess.js";
|
||||
import type { SpawnedOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
||||
import {
|
||||
createRizinCoverageHarness,
|
||||
createInjectedProcess,
|
||||
@@ -20,7 +21,8 @@ const {
|
||||
missingProviderPath,
|
||||
start,
|
||||
startInjected,
|
||||
track,
|
||||
createManager,
|
||||
spawnOwnedProcess,
|
||||
} = createRizinCoverageHarness();
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -40,7 +42,7 @@ it.skipIf(process.platform === "win32")(
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects unsupported, pre-cancelled, and failed startup requests",
|
||||
async () => {
|
||||
const windows = createRizinTestManager({}, { platform: "win32" });
|
||||
const windows = createManager({}, { platform: "win32" });
|
||||
expect(
|
||||
(
|
||||
await windows.start({
|
||||
@@ -61,20 +63,20 @@ it.skipIf(process.platform === "win32")(
|
||||
).toBe(false);
|
||||
|
||||
const failed = await makeProvider("process.exit(9);");
|
||||
const manager = createRizinTestManager({
|
||||
const manager = createManager({
|
||||
REA_RIZIN_COMMAND: failed,
|
||||
});
|
||||
track(manager);
|
||||
|
||||
const result = await manager.start({
|
||||
path: "/tmp/a",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
const missing = createRizinTestManager({
|
||||
const missing = createManager({
|
||||
REA_RIZIN_COMMAND: missingProviderPath(),
|
||||
});
|
||||
track(missing);
|
||||
|
||||
const spawnFailure = await missing.start({
|
||||
path: "/tmp/a",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
@@ -87,10 +89,10 @@ it.skipIf(process.platform === "win32")(
|
||||
"returns a startup timeout when Rizin exits before its initial frame",
|
||||
async () => {
|
||||
const provider = await makeProvider("process.exit(0);");
|
||||
const manager = createRizinTestManager({
|
||||
const manager = createManager({
|
||||
REA_RIZIN_COMMAND: provider,
|
||||
});
|
||||
track(manager);
|
||||
|
||||
const result = await manager.start({
|
||||
path: "/tmp/early-exit",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
@@ -110,14 +112,14 @@ it.skipIf(process.platform === "win32")(
|
||||
startup: () => undefined,
|
||||
beforeSpawnReturns: resolveSpawned,
|
||||
});
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{
|
||||
spawnProcess: injected.spawnProcess,
|
||||
stopProcess: async () => ({ status: "stopped" }),
|
||||
},
|
||||
);
|
||||
track(manager);
|
||||
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
try {
|
||||
const starting = manager.start({
|
||||
@@ -139,7 +141,7 @@ it.skipIf(process.platform === "win32")(
|
||||
"does not spawn Rizin when cancellation arrives during TTY validation",
|
||||
async () => {
|
||||
let spawned = false;
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{
|
||||
spawnProcess: async () => {
|
||||
@@ -164,7 +166,7 @@ it.skipIf(process.platform === "win32")(
|
||||
"rejects a non-TTY path before spawning Rizin",
|
||||
async () => {
|
||||
let spawned = false;
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{
|
||||
spawnProcess: async () => {
|
||||
@@ -195,7 +197,7 @@ it.skipIf(process.platform === "win32")(
|
||||
releaseSpawns = resolve;
|
||||
});
|
||||
const injected = createInjectedProcess();
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{
|
||||
spawnProcess: async (options) => {
|
||||
@@ -240,10 +242,9 @@ it.skipIf(process.platform === "win32")(
|
||||
const provider = await makeProvider(
|
||||
'process.stdout.write("x".repeat(16 * 1024 * 1024 + 1) + "\\0"); process.stdin.resume();',
|
||||
);
|
||||
const manager = createRizinTestManager({
|
||||
const manager = createManager({
|
||||
REA_RIZIN_COMMAND: provider,
|
||||
});
|
||||
track(manager);
|
||||
|
||||
const result = await manager.start({
|
||||
path: "/tmp/oversized-startup",
|
||||
@@ -264,19 +265,19 @@ it.skipIf(process.platform === "win32")(
|
||||
const provider = await makeProvider(
|
||||
'setTimeout(() => process.stdout.write("\\0"), 500); process.stdin.resume();',
|
||||
);
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{
|
||||
REA_RIZIN_COMMAND: provider,
|
||||
},
|
||||
{
|
||||
spawnProcess: async (options) => {
|
||||
const launched = await spawnOwnedProviderProcess(options);
|
||||
const launched = await spawnOwnedProcess(options);
|
||||
resolveSpawned();
|
||||
return launched;
|
||||
},
|
||||
},
|
||||
);
|
||||
track(manager);
|
||||
|
||||
const abort = new AbortController();
|
||||
const starting = manager.start(
|
||||
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
|
||||
@@ -288,39 +289,138 @@ it.skipIf(process.platform === "win32")(
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"preserves startup cleanup uncertainty after cancellation",
|
||||
async () => {
|
||||
it
|
||||
.skipIf(process.platform === "win32")
|
||||
.each(["is uncertain", "reports stopped before exit"] as const)(
|
||||
"cleans startup fixtures after cancellation when manager stop %s",
|
||||
async (stopMode) => {
|
||||
const provider = await makeProvider(
|
||||
"process.stdin.resume(); setInterval(() => {}, 1000);",
|
||||
);
|
||||
let resolveSpawned!: () => void;
|
||||
const spawned = new Promise<void>((resolve) => {
|
||||
resolveSpawned = resolve;
|
||||
});
|
||||
const provider = await makeProvider(
|
||||
let ownedLaunch: SpawnedOwnedProviderProcess | undefined;
|
||||
const nativeSpawn = providerProcess.spawnOwnedProviderProcess;
|
||||
const spy = vi
|
||||
.spyOn(providerProcess, "spawnOwnedProviderProcess")
|
||||
.mockImplementation(async (options) => {
|
||||
const launched = await nativeSpawn(options);
|
||||
ownedLaunch = launched;
|
||||
resolveSpawned();
|
||||
return launched;
|
||||
});
|
||||
try {
|
||||
const stopProcess: typeof incompleteStop =
|
||||
stopMode === "is uncertain"
|
||||
? incompleteStop
|
||||
: async () => ({ status: "stopped" });
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: provider },
|
||||
{ stopProcess },
|
||||
);
|
||||
const abort = new AbortController();
|
||||
const starting = manager.start(
|
||||
{
|
||||
path: "/tmp/cancel-start",
|
||||
inferior_tty: await rizinTestInferiorTty(),
|
||||
},
|
||||
abort.signal,
|
||||
);
|
||||
await spawned;
|
||||
abort.abort();
|
||||
const result = await starting;
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok && stopMode === "is uncertain")
|
||||
expect(result.error.message).toContain("injected stop uncertainty");
|
||||
await cleanup();
|
||||
if (ownedLaunch === undefined)
|
||||
throw new Error("Fixture launch was not observed");
|
||||
expect(
|
||||
ownedLaunch.process.exitCode !== null ||
|
||||
ownedLaunch.process.signalCode !== null,
|
||||
"harness returned before fixture child exit",
|
||||
).toBe(true);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
// A failing regression must also reap its observed, directly owned child.
|
||||
if (
|
||||
ownedLaunch !== undefined &&
|
||||
ownedLaunch.process.exitCode === null &&
|
||||
ownedLaunch.process.signalCode === null
|
||||
) {
|
||||
const safety = new providerProcess.ProviderProcessSupervisor(
|
||||
{ ...ownedLaunch, ownsProcessLifetime: true },
|
||||
{ captureStdout: false, maxDiagnosticBytes: 0 },
|
||||
);
|
||||
expect((await safety.stop()).status).not.toBe("incomplete");
|
||||
expect(safety.observedExit()).toBeDefined();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"retains owned fixture processes and files until a failed cleanup can complete",
|
||||
async () => {
|
||||
let failCleanup = true;
|
||||
const fixture = createRizinCoverageHarness({
|
||||
stopOwnedProcess: async (supervisor) =>
|
||||
failCleanup
|
||||
? { status: "incomplete", reason: "injected fixture stop failure" }
|
||||
: supervisor.stop(),
|
||||
});
|
||||
const provider = await fixture.makeProvider(
|
||||
"process.stdin.resume(); setInterval(() => {}, 1000);",
|
||||
);
|
||||
const manager = createRizinTestManager(
|
||||
{ REA_RIZIN_COMMAND: provider },
|
||||
{
|
||||
stopProcess: incompleteStop,
|
||||
spawnProcess: async (options) => {
|
||||
const launched = await spawnOwnedProviderProcess(options);
|
||||
resolveSpawned();
|
||||
return launched;
|
||||
},
|
||||
},
|
||||
);
|
||||
track(manager);
|
||||
const abort = new AbortController();
|
||||
const starting = manager.start(
|
||||
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
|
||||
abort.signal,
|
||||
);
|
||||
await spawned;
|
||||
abort.abort();
|
||||
const result = await starting;
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error.message).toContain("injected stop uncertainty");
|
||||
const launched = await fixture.spawnOwnedProcess({
|
||||
command: provider,
|
||||
arguments: [],
|
||||
runId: randomUUID(),
|
||||
stdin: "pipe",
|
||||
});
|
||||
try {
|
||||
await expect(fixture.cleanup()).rejects.toMatchObject({
|
||||
message: "Fake Rizin fixture cleanup failed",
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("injected fixture stop failure"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(launched.process.exitCode).toBeNull();
|
||||
expect(launched.process.signalCode).toBeNull();
|
||||
await access(provider);
|
||||
const nextProvider = await fixture.makeProvider("process.exit(0);");
|
||||
failCleanup = false;
|
||||
await fixture.cleanup();
|
||||
expect(
|
||||
launched.process.exitCode !== null ||
|
||||
launched.process.signalCode !== null,
|
||||
).toBe(true);
|
||||
await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(access(nextProvider)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
} finally {
|
||||
failCleanup = false;
|
||||
try {
|
||||
await fixture.cleanup();
|
||||
} finally {
|
||||
if (
|
||||
launched.process.exitCode === null &&
|
||||
launched.process.signalCode === null
|
||||
) {
|
||||
const safety = new providerProcess.ProviderProcessSupervisor(
|
||||
{ ...launched, ownsProcessLifetime: true },
|
||||
{ captureStdout: false, maxDiagnosticBytes: 0 },
|
||||
);
|
||||
expect((await safety.stop()).status).not.toBe("incomplete");
|
||||
expect(safety.observedExit()).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
rizinTestInferiorTty,
|
||||
} from "./RizinTestInferiorTty.js";
|
||||
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
|
||||
import {
|
||||
createRizinCoverageHarness,
|
||||
abortOnCheck,
|
||||
@@ -11,7 +7,7 @@ import {
|
||||
incompleteStop,
|
||||
} from "./RizinDebugSessionCoverage.fixtures.js";
|
||||
|
||||
const { cleanup, makeProvider, start, startInjected, track } =
|
||||
const { cleanup, makeProvider, start, startInjected, createManager } =
|
||||
createRizinCoverageHarness();
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -88,7 +84,7 @@ it.skipIf(process.platform === "win32")(
|
||||
async () => {
|
||||
for (const spawnError of [new Error("injected spawn failure"), "opaque"]) {
|
||||
const injected = createInjectedProcess({ spawnError });
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ spawnProcess: injected.spawnProcess },
|
||||
);
|
||||
@@ -102,7 +98,7 @@ it.skipIf(process.platform === "win32")(
|
||||
const injected = createInjectedProcess({
|
||||
startup: (control) => control.closeStdin(),
|
||||
});
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ spawnProcess: injected.spawnProcess },
|
||||
);
|
||||
@@ -124,7 +120,7 @@ it.skipIf(process.platform === "win32")(
|
||||
beforeSpawnReturns: () => abort.abort(),
|
||||
spawnError: new Error("spawn interrupted"),
|
||||
});
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ spawnProcess: injected.spawnProcess },
|
||||
);
|
||||
@@ -147,7 +143,7 @@ it.skipIf(process.platform === "win32")(
|
||||
abort.abort();
|
||||
},
|
||||
});
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ spawnProcess: injected.spawnProcess },
|
||||
);
|
||||
@@ -166,7 +162,7 @@ it.skipIf(process.platform === "win32")(
|
||||
const injected = createInjectedProcess({
|
||||
beforeSpawnReturns: () => abort.abort(),
|
||||
});
|
||||
const manager = createRizinTestManager(
|
||||
const manager = createManager(
|
||||
{ REA_RIZIN_COMMAND: "injected-rizin" },
|
||||
{ spawnProcess: injected.spawnProcess },
|
||||
);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
rizinTestInferiorTty,
|
||||
} from "./RizinTestInferiorTty.js";
|
||||
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
|
||||
import {
|
||||
createRizinCoverageHarness,
|
||||
abortOnCheck,
|
||||
createInjectedProcess,
|
||||
} from "./RizinDebugSessionCoverage.fixtures.js";
|
||||
|
||||
const { cleanup, startInjected, track } = createRizinCoverageHarness();
|
||||
const { cleanup, startInjected, createManager } = createRizinCoverageHarness();
|
||||
afterEach(cleanup);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { evidenceSchema } from "../../../src/domain/evidence.js";
|
||||
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
|
||||
import {
|
||||
createRizinTestManager,
|
||||
rizinTestInferiorTty,
|
||||
} from "./RizinTestInferiorTty.js";
|
||||
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
|
||||
import {
|
||||
createRizinCoverageHarness,
|
||||
createInjectedProcess,
|
||||
incompleteStop,
|
||||
} from "./RizinDebugSessionCoverage.fixtures.js";
|
||||
|
||||
const { cleanup, makeProvider, start, startInjected, track } =
|
||||
const { cleanup, makeProvider, start, startInjected, createManager } =
|
||||
createRizinCoverageHarness();
|
||||
afterEach(cleanup);
|
||||
|
||||
|
||||
@@ -80,3 +80,41 @@ it("reports a graph beyond REA's buffer once per session instead of rebuilding i
|
||||
expect(await client.close()).toEqual({ ok: true, value: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("says why Kuna cannot answer instead of a generic unavailable message", async () => {
|
||||
const { open } = await session("rea-kuna-unavailable-reasons-");
|
||||
const client = open();
|
||||
try {
|
||||
const cases = [
|
||||
{
|
||||
operation: "procedure_assembly",
|
||||
parameters: { procedure: "0x2000" },
|
||||
message:
|
||||
"Kuna's export lists no code at 0x2000; import slots and data entries have no listing.",
|
||||
},
|
||||
{
|
||||
operation: "procedure_callers",
|
||||
parameters: { procedure: "0x1004" },
|
||||
message:
|
||||
"Kuna's graph has no function node at 0x1004; its relationships are unknown.",
|
||||
},
|
||||
{
|
||||
operation: "list_segments",
|
||||
parameters: {},
|
||||
message:
|
||||
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
|
||||
},
|
||||
] as const;
|
||||
for (const { operation, parameters, message } of cases) {
|
||||
const result = await client.execute(operation, parameters);
|
||||
if (result.ok) throw new Error(`${operation} unexpectedly succeeded`);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
message,
|
||||
retryable: false,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
expect(await client.close()).toEqual({ ok: true, value: null });
|
||||
}
|
||||
});
|
||||
@@ -1,10 +1,25 @@
|
||||
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
MipsAbiFlags,
|
||||
MipsElfMetadata,
|
||||
MipsGnuAttributes,
|
||||
} from "../../../src/domain/binaryTargetTypes.js";
|
||||
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
|
||||
export type MipsReadelfMetadata = MipsElfMetadata & {
|
||||
readonly abiFlags: MipsAbiFlags;
|
||||
readonly gnuAttributes: MipsGnuAttributes | null;
|
||||
};
|
||||
export function expectedMipsElfProfile(
|
||||
mips: MipsReadelfMetadata,
|
||||
): Readonly<Record<string, JsonValue>>;
|
||||
|
||||
export type MipsFloatAbi = "hard" | "soft";
|
||||
export interface MipsReadelfFacts {
|
||||
mips: MipsElfMetadata;
|
||||
mips: MipsReadelfMetadata & { readonly gnuAttributes: null };
|
||||
symbols: Record<string, string>;
|
||||
}
|
||||
export function parseMipsReadelfSectionTypes(text: string): readonly string[];
|
||||
export function parseMipsReadelf(
|
||||
text: string,
|
||||
byteOrder: string,
|
||||
|
||||
@@ -23,6 +23,65 @@ const SOFT_FLOAT_SYMBOLS = [
|
||||
"rea_mips_soft_caller",
|
||||
];
|
||||
|
||||
/** Complete indexed ELF32 section types shared by the independent fixture readers. */
|
||||
export function parseMipsReadelfSectionTypes(text) {
|
||||
const counts = [
|
||||
...text.matchAll(/^\s*Number of section headers:\s*(\d+)\s*$/gmu),
|
||||
];
|
||||
assert.equal(counts.length, 1, "Missing or ambiguous readelf section count");
|
||||
const sectionCount = Number(counts[0][1]);
|
||||
assert.ok(sectionCount > 0, "Fixture must have a section header table");
|
||||
const sections = [...text.matchAll(/^[ \t]*\[[ \t]*(\d+)\][ \t]+(.+)$/gmu)];
|
||||
assert.equal(
|
||||
sections.length,
|
||||
sectionCount,
|
||||
"Incomplete readelf section table",
|
||||
);
|
||||
return sections.map((section, index) => {
|
||||
assert.equal(Number(section[1]), index, "Ambiguous readelf section index");
|
||||
const columns =
|
||||
/^(?:\S+[ \t]+)?(\S+)[ \t]+[\da-f]{8}[ \t]+[\da-f]+[ \t]+[\da-f]+[ \t]+[\da-f]{2}[ \t]+(?:[A-Za-z]+[ \t]+)?\d+[ \t]+\d+[ \t]+\d+[ \t]*$/u.exec(
|
||||
section[2],
|
||||
);
|
||||
assert.ok(columns !== null, "Malformed readelf section header");
|
||||
return columns[1];
|
||||
});
|
||||
}
|
||||
|
||||
/** Independent wire-format expectation shared by generic and PSP snapshots. */
|
||||
export function expectedMipsElfProfile(mips) {
|
||||
const abi = mips.abiFlags;
|
||||
assert.ok(abi != null, "Fixture ABI declarations must be inspected");
|
||||
assert.notEqual(
|
||||
mips.gnuAttributes,
|
||||
undefined,
|
||||
"Fixture GNU attributes must be inspected",
|
||||
);
|
||||
return {
|
||||
elf_class: mips.elfClass,
|
||||
byte_order: mips.byteOrder,
|
||||
type: mips.type,
|
||||
flags: mips.flags,
|
||||
gnu_attributes:
|
||||
mips.gnuAttributes === null
|
||||
? null
|
||||
: { fp_abi: mips.gnuAttributes.fpAbi, msa: mips.gnuAttributes.msa },
|
||||
abi_flags: {
|
||||
version: abi.version,
|
||||
isa_level: abi.isaLevel,
|
||||
isa_revision: abi.isaRevision,
|
||||
gpr_size: abi.gprSize,
|
||||
cpr1_size: abi.cpr1Size,
|
||||
cpr2_size: abi.cpr2Size,
|
||||
fp_abi: abi.fpAbi,
|
||||
isa_extension: abi.isaExtension,
|
||||
ases: abi.ases,
|
||||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse only the documented GNU readelf fields needed by our tiny fixtures.
|
||||
* `rel` objects carry their own symbol checks in mips-relocatable-checks.mjs.
|
||||
@@ -51,6 +110,10 @@ export function parseMipsReadelf(
|
||||
objectType === "rel" ? /^REL\b/u : /^EXEC\b/u,
|
||||
);
|
||||
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
|
||||
assert.ok(
|
||||
!parseMipsReadelfSectionTypes(text).includes("GNU_ATTRIBUTES"),
|
||||
"Fixture unexpectedly declares GNU attributes",
|
||||
);
|
||||
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+),.*$/gmu, "flags"));
|
||||
assert.equal(flags, 0x70001001, "Fixture ELF flags differ from build intent");
|
||||
assert.equal(field(/^ISA:\s*(.+)$/gmu, "ISA"), "MIPS32r2");
|
||||
@@ -123,6 +186,7 @@ export function parseMipsReadelf(
|
||||
type: objectType === "rel" ? 1 : 2,
|
||||
flags,
|
||||
abiFlags,
|
||||
gnuAttributes: null,
|
||||
},
|
||||
symbols,
|
||||
};
|
||||
@@ -144,7 +208,7 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
|
||||
);
|
||||
const result = await execute(
|
||||
command,
|
||||
["-h", "-A", "-s", "-W", path],
|
||||
["-h", "-A", "-S", "-s", "-W", path],
|
||||
options,
|
||||
);
|
||||
assert.equal(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseMipsReadelf,
|
||||
} from "./mips-checks.mjs";
|
||||
|
||||
// GNU readelf 2.40 output for the source-owned Clang 17 / LLD fixture.
|
||||
// GNU readelf 2.44 output for the source-owned Clang 19 / LLD 19 fixture.
|
||||
// This tests the reader boundary, not Ghidra; the real lane rebuilds both orders.
|
||||
const readelf = `ELF Header:
|
||||
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
|
||||
@@ -21,7 +21,7 @@ const readelf = `ELF Header:
|
||||
Version: 0x1
|
||||
Entry point address: 0x201b0
|
||||
Start of program headers: 52 (bytes into file)
|
||||
Start of section headers: 2996 (bytes into file)
|
||||
Start of section headers: 2624 (bytes into file)
|
||||
Flags: 0x70001001, noreorder, o32, mips32r2
|
||||
Size of this header: 52 (bytes)
|
||||
Size of program headers: 32 (bytes)
|
||||
@@ -30,6 +30,38 @@ const readelf = `ELF Header:
|
||||
Number of section headers: 24
|
||||
Section header string table index: 22
|
||||
|
||||
Section Headers:
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 00010118 000118 000018 18 A 0 0 8
|
||||
[ 2] .reginfo MIPS_REGINFO 00010130 000130 000018 18 A 0 0 4
|
||||
[ 3] .rodata PROGBITS 00010148 000148 00001e 00 A 0 0 1
|
||||
[ 4] .text PROGBITS 00020170 000170 0000f0 00 AX 0 0 16
|
||||
[ 5] .data PROGBITS 00030260 000260 000000 00 WA 0 0 16
|
||||
[ 6] .got PROGBITS 00030260 000260 000008 00 WAp 0 0 16
|
||||
[ 7] .sdata PROGBITS 00030268 000268 000004 00 WAp 0 0 4
|
||||
[ 8] .bss NOBITS 00030270 00026c 000000 00 WA 0 0 16
|
||||
[ 9] .mdebug.abi32 PROGBITS 00000000 00026c 000000 00 0 0 1
|
||||
[10] .pdr PROGBITS 00000000 00026c 000060 00 0 0 4
|
||||
[11] .debug_abbrev MIPS_DWARF 00000000 0002cc 0000b3 00 0 0 1
|
||||
[12] .debug_info MIPS_DWARF 00000000 00037f 00015e 00 0 0 1
|
||||
[13] .debug_str_offsets MIPS_DWARF 00000000 0004dd 000040 00 0 0 1
|
||||
[14] .debug_str MIPS_DWARF 00000000 00051d 0000ef 01 MS 0 0 1
|
||||
[15] .debug_addr MIPS_DWARF 00000000 00060c 000018 00 0 0 1
|
||||
[16] .comment PROGBITS 00000000 000624 00003e 01 MS 0 0 1
|
||||
[17] .debug_frame MIPS_DWARF 00000000 000664 00004c 00 0 0 4
|
||||
[18] .debug_line MIPS_DWARF 00000000 0006b0 0000f8 00 0 0 1
|
||||
[19] .debug_line_str MIPS_DWARF 00000000 0007a8 000098 01 MS 0 0 1
|
||||
[20] .debug_aranges MIPS_DWARF 00000000 000840 000020 00 0 0 1
|
||||
[21] .symtab SYMTAB 00000000 000860 000090 10 23 4 4
|
||||
[22] .shstrtab STRTAB 00000000 0008f0 0000f0 00 0 0 1
|
||||
[23] .strtab STRTAB 00000000 0009e0 00005f 00 0 0 1
|
||||
Key to Flags:
|
||||
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
|
||||
L (link order), O (extra OS processing required), G (group), T (TLS),
|
||||
C (compressed), x (unknown), o (OS specific), E (exclude),
|
||||
D (mbind), p (processor specific)
|
||||
|
||||
Symbol table '.symtab' contains 9 entries:
|
||||
Num: Value Size Type Bind Vis Ndx Name
|
||||
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
|
||||
@@ -54,6 +86,15 @@ ASEs:
|
||||
None
|
||||
FLAGS 1: 00000000
|
||||
FLAGS 2: 00000000
|
||||
|
||||
Static GOT:
|
||||
Canonical gp value: 00038250
|
||||
|
||||
Reserved entries:
|
||||
Address Access Value
|
||||
00030260 -32752(gp) 00000000
|
||||
00030264 -32748(gp) 80000000
|
||||
|
||||
`;
|
||||
const sha256 = "a".repeat(64);
|
||||
const image = {
|
||||
@@ -94,6 +135,7 @@ describe("MIPS real-verifier independent expectations", () => {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
flags: 0x70001001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: {
|
||||
isaLevel: 32,
|
||||
isaRevision: 2,
|
||||
@@ -229,6 +271,67 @@ describe("MIPS real-verifier independent expectations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS real-verifier GNU attribute coverage", () => {
|
||||
it.each(["exec", "rel"] as const)(
|
||||
"reports verified GNU attribute absence for both byte orders of %s fixtures",
|
||||
(objectType) => {
|
||||
for (const byteOrder of ["little", "big"]) {
|
||||
const text = readelf
|
||||
.replace("little endian", `${byteOrder} endian`)
|
||||
.replace(
|
||||
"EXEC (Executable file)",
|
||||
objectType === "rel"
|
||||
? "REL (Relocatable file)"
|
||||
: "EXEC (Executable file)",
|
||||
);
|
||||
expect(
|
||||
parseMipsReadelf(text, byteOrder, "hard", objectType).mips
|
||||
.gnuAttributes,
|
||||
).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects GNU attribute sections even when their name is unrelated", () => {
|
||||
expect(() =>
|
||||
parseMipsReadelf(
|
||||
readelf.replace(
|
||||
".comment PROGBITS",
|
||||
".comment GNU_ATTRIBUTES",
|
||||
),
|
||||
"little",
|
||||
),
|
||||
).toThrow(/GNU attributes/u);
|
||||
});
|
||||
|
||||
it("uses the section type rather than its name to establish absence", () => {
|
||||
expect(
|
||||
parseMipsReadelf(
|
||||
readelf.replace(
|
||||
".comment PROGBITS",
|
||||
".gnu.attributes PROGBITS",
|
||||
),
|
||||
"little",
|
||||
).mips.gnuAttributes,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"missing table",
|
||||
readelf.replace(
|
||||
/Section Headers:[\s\S]*?Key to Flags:/u,
|
||||
"Key to Flags:",
|
||||
),
|
||||
],
|
||||
["missing entry", readelf.replace(/^.*\[ 5\].*\n/gmu, "")],
|
||||
["duplicate index", readelf.replace("[ 5]", "[ 4]")],
|
||||
["malformed entry", readelf.replace(/^.*\[ 5\].*$/gmu, " [ 5] malformed")],
|
||||
])("refuses GNU attribute absence from a %s", (_name, text) => {
|
||||
expect(() => parseMipsReadelf(text, "little")).toThrow(/section/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS soft-float real-verifier expectations", () => {
|
||||
const softReadelf = readelf
|
||||
.replace("CPR1 size: 32", "CPR1 size: 0")
|
||||
@@ -249,6 +352,7 @@ describe("MIPS soft-float real-verifier expectations", () => {
|
||||
it("reads the soft-float ABI record and its fixture symbols", () => {
|
||||
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
|
||||
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
|
||||
expect(parsed.mips.gnuAttributes).toBeNull();
|
||||
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { it } from "vitest";
|
||||
import { parsePspReadelfAbi } from "./psp-fixture.mjs";
|
||||
import { expectedMipsElfProfile } from "./mips-checks.mjs";
|
||||
import { ghidraMipsProfileParameters } from "../../../src/ghidra/GhidraMipsProfile.js";
|
||||
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { parsePspReadelfMips } from "./psp-fixture.mjs";
|
||||
|
||||
// GNU readelf 2.44 output from the maintained PSPDEV v20261001 fixture.
|
||||
const observed = `MIPS ABI Flags Version: 0
|
||||
@@ -16,19 +19,42 @@ FLAGS 1: 00000001
|
||||
FLAGS 2: 00000000
|
||||
`;
|
||||
|
||||
// Constructed section-table controls combined with the captured ABI declaration;
|
||||
// this does not establish a real PSPSDK or Allegrex-provider run.
|
||||
const metadata = `ELF Header:
|
||||
Class: ELF32
|
||||
Data: 2's complement, little endian
|
||||
Type: EXEC (Executable file)
|
||||
Machine: MIPS R3000
|
||||
Flags: 0x10a23001, noreorder, allegrex, eabi32, mips2
|
||||
Number of section headers: 3
|
||||
Section Headers:
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 08800118 000118 000018 18 A 0 0 8
|
||||
[ 2] .text PROGBITS 08800130 000130 000010 00 AX 0 0 16
|
||||
${observed}`;
|
||||
|
||||
it("reads the independent PSP producer's single-float declaration", () => {
|
||||
assert.deepEqual(parsePspReadelfAbi(observed), {
|
||||
version: 0,
|
||||
isaLevel: 2,
|
||||
isaRevision: 0,
|
||||
gprSize: 1,
|
||||
cpr1Size: 1,
|
||||
cpr2Size: 0,
|
||||
fpAbi: 2,
|
||||
isaExtension: 0,
|
||||
ases: 0,
|
||||
flags1: 1,
|
||||
flags2: 0,
|
||||
assert.deepEqual(parsePspReadelfMips(metadata), {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: {
|
||||
version: 0,
|
||||
isaLevel: 2,
|
||||
isaRevision: 0,
|
||||
gprSize: 1,
|
||||
cpr1Size: 1,
|
||||
cpr2Size: 0,
|
||||
fpAbi: 2,
|
||||
isaExtension: 0,
|
||||
ases: 0,
|
||||
flags1: 1,
|
||||
flags2: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +72,108 @@ it.each([
|
||||
])(
|
||||
"rejects unsupported or ambiguous independent ABI output: %s",
|
||||
(before, after) => {
|
||||
assert.throws(() => parsePspReadelfAbi(observed.replace(before, after)));
|
||||
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"missing table",
|
||||
metadata.replace(
|
||||
/Section Headers:[\s\S]*?MIPS ABI Flags/u,
|
||||
"MIPS ABI Flags",
|
||||
),
|
||||
],
|
||||
["incomplete table", metadata.replace(/^.*\[ 2\].*\n/gmu, "")],
|
||||
["duplicate index", metadata.replace("[ 2]", "[ 1]")],
|
||||
[
|
||||
"GNU section under another name",
|
||||
metadata.replace(
|
||||
".text PROGBITS",
|
||||
".text GNU_ATTRIBUTES",
|
||||
),
|
||||
],
|
||||
])("refuses PSP GNU attribute absence from a %s", (_name, text) => {
|
||||
assert.throws(() => parsePspReadelfMips(text), /section|GNU attributes/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Class: ELF32", "Class: ELF64"],
|
||||
["little endian", "big endian"],
|
||||
["Type: EXEC", "Type: REL"],
|
||||
["Flags: 0x10a23001", "Flags: 0x70001001"],
|
||||
["Class: ELF32", "Class: ELF32\n Class: ELF32"],
|
||||
])("refuses contradictory or ambiguous PSP headers: %s", (before, after) => {
|
||||
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
|
||||
});
|
||||
|
||||
// GNU readelf 2.40 independently decoded this declaration from a constructed
|
||||
// ELF32/PSP-shaped control, not a PSPSDK build or a real Allegrex import.
|
||||
const gnuDeclaration = `Attribute Section: gnu
|
||||
File Attributes
|
||||
Tag_GNU_MIPS_ABI_FP: Hard float (single precision)
|
||||
`;
|
||||
const gnuMetadata = metadata.replace(
|
||||
".text PROGBITS",
|
||||
".text GNU_ATTRIBUTES",
|
||||
);
|
||||
|
||||
it.each(["before", "after"])(
|
||||
"retains the independent single-float GNU declaration %s the ABI dump",
|
||||
(position) => {
|
||||
const text =
|
||||
position === "before"
|
||||
? gnuMetadata.replace(observed, `${gnuDeclaration}\n${observed}`)
|
||||
: `${gnuMetadata}\n${gnuDeclaration}`;
|
||||
assert.deepEqual(parsePspReadelfMips(text), {
|
||||
...parsePspReadelfMips(metadata),
|
||||
gnuAttributes: { fpAbi: 2, msa: null },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["unknown vendor", gnuDeclaration.replace("gnu", "aeabi")],
|
||||
["wrong float ABI", gnuDeclaration.replace("single", "double")],
|
||||
["unknown tag", `${gnuDeclaration} Tag_GNU_Unknown: 1\n`],
|
||||
[
|
||||
"unsupported scope",
|
||||
gnuDeclaration.replace("File Attributes", "Section Attributes: 1"),
|
||||
],
|
||||
["duplicate declaration", `${gnuDeclaration}${gnuDeclaration}`],
|
||||
])("refuses %s rather than inventing PSP GNU facts", (_name, declaration) => {
|
||||
assert.throws(
|
||||
() => parsePspReadelfMips(`${gnuMetadata}\n${declaration}`),
|
||||
/GNU attributes/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses undeclared GNU attributes rather than reporting absence", () => {
|
||||
assert.throws(
|
||||
() => parsePspReadelfMips(`${metadata}\n${gnuDeclaration}`),
|
||||
/GNU attributes/u,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["absent", metadata],
|
||||
["declared", `${gnuMetadata}\n${gnuDeclaration}`],
|
||||
])(
|
||||
"matches production snapshot serialization for %s GNU facts",
|
||||
(_name, text) => {
|
||||
const mips = parsePspReadelfMips(text);
|
||||
const target = {
|
||||
path: "/fixture/psp.elf",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "mips",
|
||||
availableArchitectures: ["mips"],
|
||||
mips,
|
||||
} satisfies BinaryTarget;
|
||||
assert.deepEqual(
|
||||
expectedMipsElfProfile(mips),
|
||||
ghidraMipsProfileParameters(target).mips_elf,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { MipsAbiFlags } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import type { MipsReadelfMetadata } from "./mips-checks.mjs";
|
||||
|
||||
export declare function parsePspReadelfAbi(text: string): MipsAbiFlags;
|
||||
export declare function parsePspReadelfMips(text: string): MipsReadelfMetadata;
|
||||
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
||||
import { copyFile, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { parseMipsReadelfSectionTypes } from "./mips-checks.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
@@ -42,15 +43,11 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
assert.equal(bytes.readUInt32LE(36), 0x10a23001);
|
||||
const readelf = await execute(
|
||||
"psp-readelf",
|
||||
["-h", "-A", "-sW", path],
|
||||
["-h", "-A", "-S", "-sW", path],
|
||||
options,
|
||||
);
|
||||
assert.equal(readelf.stderr.trim(), "");
|
||||
assert.match(readelf.stdout, /Class:\s+ELF32/u);
|
||||
assert.match(readelf.stdout, /Data:\s+2's complement, little endian/u);
|
||||
assert.match(readelf.stdout, /Type:\s+EXEC\b/u);
|
||||
assert.match(readelf.stdout, /Flags:\s+0x10a23001\b/u);
|
||||
const abiFlags = parsePspReadelfAbi(readelf.stdout);
|
||||
const mips = parsePspReadelfMips(readelf.stdout);
|
||||
const abiDump = await execute(
|
||||
"psp-readelf",
|
||||
["-x", ".MIPS.abiflags", path],
|
||||
@@ -99,13 +96,7 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
source_sha256: sourceDigests,
|
||||
symbols,
|
||||
flags: "0x10a23001",
|
||||
mips: {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
abiFlags,
|
||||
},
|
||||
mips,
|
||||
abi_record_hex_dump: abiDump.stdout,
|
||||
global_value: globalValue,
|
||||
versions,
|
||||
@@ -115,8 +106,8 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Independent GNU fields for the pinned PSPSDK fixture, not REA's parser. */
|
||||
export function parsePspReadelfAbi(text) {
|
||||
/** Complete independent fields for the pinned PSPSDK fixture, not REA's parser. */
|
||||
export function parsePspReadelfMips(text) {
|
||||
const field = (expression, label) => {
|
||||
const matches = [...text.matchAll(expression)];
|
||||
assert.equal(
|
||||
@@ -126,6 +117,13 @@ export function parsePspReadelfAbi(text) {
|
||||
);
|
||||
return matches[0][1].trim();
|
||||
};
|
||||
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
|
||||
assert.equal(
|
||||
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
|
||||
"little",
|
||||
);
|
||||
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
|
||||
assert.match(field(/^\s*Flags:\s*(.+)$/gmu, "flags"), /^0x10a23001\b/u);
|
||||
const width = (name) => {
|
||||
const bits = Number(
|
||||
field(new RegExp(`^${name} size:\\s*(\\d+)\\s*$`, "gmu"), name),
|
||||
@@ -178,7 +176,49 @@ export function parsePspReadelfAbi(text) {
|
||||
},
|
||||
"PSPSDK ABI declaration differs from independently inspected build intent",
|
||||
);
|
||||
return abi;
|
||||
return {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
abiFlags: abi,
|
||||
gnuAttributes: parsePspGnuAttributes(text),
|
||||
};
|
||||
}
|
||||
|
||||
// The maintained PSP fixture is single-float. Retain that independently decoded
|
||||
// GNU declaration when present, and refuse other tags/vendors/scopes rather
|
||||
// than confusing an uninspected or unsupported declaration with absence.
|
||||
function parsePspGnuAttributes(text) {
|
||||
const count = parseMipsReadelfSectionTypes(text).filter(
|
||||
(type) => type === "GNU_ATTRIBUTES",
|
||||
).length;
|
||||
const headers = [...text.matchAll(/^Attribute Section: (.+)$/gmu)];
|
||||
if (count === 0) {
|
||||
assert.equal(
|
||||
headers.length,
|
||||
0,
|
||||
"GNU attributes without a declared section",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
assert.equal(count, 1, "Ambiguous GNU attributes sections");
|
||||
assert.equal(headers.length, 1, "Missing or ambiguous GNU attributes output");
|
||||
const header = headers[0];
|
||||
assert.equal(header[1].trim(), "gnu", "Unsupported GNU attributes vendor");
|
||||
const tail = text.slice(header.index + header[0].length);
|
||||
// GNU attributes may precede or follow the architecture-specific ABI dump.
|
||||
const end = tail.search(/^MIPS ABI Flags Version:/mu);
|
||||
const lines = (end < 0 ? tail : tail.slice(0, end))
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
assert.deepEqual(
|
||||
lines,
|
||||
["File Attributes", "Tag_GNU_MIPS_ABI_FP: Hard float (single precision)"],
|
||||
"Unsupported or incomplete PSP GNU attributes declaration",
|
||||
);
|
||||
return { fpAbi: 2, msa: null };
|
||||
}
|
||||
|
||||
/** Observations from Ghidra, not an echo of the requested profile. */
|
||||
|
||||
@@ -69,6 +69,12 @@ describe("curated Windows test lane", () => {
|
||||
"tests/boundary/process/windowsOutputCapture.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the adjacent npm fallback regression in the lane", async () => {
|
||||
expect(await explicitPaths("windows")).toContain(
|
||||
"tests/boundary/process/packageNpmInvocation.test.ts",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps every explicit focused CI test path executable after file moves", async () => {
|
||||
|
||||
Reference in new issue
Block a user