diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 64b0bdad23..9da1eed639 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -2,7 +2,7 @@ name: reverse-engineer-anything description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis. metadata: - version: "53" + version: "55" --- # REA @@ -94,6 +94,9 @@ explicit path or endpoint and do not need it. than extra calls and schemas. Runtime options are metadata, not files. Compact delivery still inventories and hashes members. See the [Python packaged applications guide](https://github.com/morluto/rea/blob/main/docs/python-packaged-applications.md). +- Bun `--compile` executable: `open_binary`, then `inspect_artifact`; the Bun + module graph is the application layer. Extract stored modules and analyze the + JavaScript. See [Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md). - Apple app plist, Interface Builder, asset catalog, or keyed archive resources: `open_bundle_resources`, then the selected resource tool. This does not require `CFBundleExecutable` or a deep provider. Resource sessions have no diff --git a/.agents/skills/reverse-engineer-anything/references/environments.md b/.agents/skills/reverse-engineer-anything/references/environments.md index 990c7be9ee..0be64fbab2 100644 --- a/.agents/skills/reverse-engineer-anything/references/environments.md +++ b/.agents/skills/reverse-engineer-anything/references/environments.md @@ -124,9 +124,11 @@ shape, with the application's own locators, paths and headers: } ``` -Give every step a `timeout_ms`. A step without one waits until the scenario -is cancelled, so a locator that never matches hangs the capture, and the -client with it. Match `wait_for` locators to exactly one element. +Give each step that accepts `timeout_ms` a deadline. Without one, a step waits +until it completes or the scenario is cancelled, so a locator that never +matches hangs the capture and the client with it. The `wait_for_timeout` pause +instead uses `duration_ms` and does not accept `timeout_ms`. Match `wait_for` +locators to exactly one element. 1. Sign in through the UI: `fill` the user and password fields (the password as a secret reference), submit, then `wait_for` an element only a signed-in @@ -153,13 +155,17 @@ client with it. Match `wait_for` locators to exactly one element. with varied free text. After each write, and before the next one, read the object back by id and then its list: create 1, read 1, list, create 2, read 2, list; never several writes before their reads. -4. Leave more than a second between a create and its first edit - (`wait_for_timeout` with `duration_ms: 1100`). +4. Leave more than a second between two creates and between a create and its + first edit (`wait_for_timeout` with `duration_ms: 1100`), so lists sorted + by creation time show them in order. 5. Update both objects and read each back, changing the fields a person would change (a name, an email address, a status, a due date) rather than only a description: a served update accepts only the members a recorded - update stored. Delete both and read each back (expect 404, or the object - marked deleted) and the list. + update stored. Run an action that turns something on or off (star, + bookmark, archive) twice in a row on one object, reading it back after + each: the second call shows whether it sets or flips the field, and the + environment serves the action only once a call has shown it. Delete both + and read each back (expect 404, or the object marked deleted) and the list. 6. Vary what the two creates send, however you write them: the first every field the application takes, the second only what a person must give (its title or name), with the other fields left out or empty. A served create diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index bff8a3625d..b739157f82 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -695,3 +695,20 @@ refusals. ESP8266 images are refused, and flash-encrypted images are not recognized. Segments are not disassembled, and RISC-V or Xtensa segments cannot be opened as raw-binary. See [ESP images](https://github.com/morluto/rea/blob/main/docs/esp-image.md). + +## Bun standalone executables + +A `bun build --compile` executable stores its module graph in Mach-O +`__BUN,__bun`, a PE or ELF `.bun` section, or an appended trailer. +`open_binary` then `inspect_artifact` reports root format `bun-standalone` with +the carrier, structural layout, module count, entry point, raw flags and +compile exec-argv; each module keeps its `/$bunfs/...` source name, loader, +encoding, sizes and SHA-256, with sourcemap, bytecode and module-info regions as +separate children. Use summary root metadata `entry_point_logical_path` as the +exact inventory `logical_paths` selector, then read the selected content node by +its artifact ID; the reader owns virtual-name normalization and indexed fallback. `extract-artifact` writes stored contents through the safe +output tree; route extracted JavaScript to `analyze_javascript_application`. +Layout labels describe record shapes, not a detected compiler version; unknown +or ambiguous layouts are unsupported. Bytecode is retained, not decoded, and the +executable is never run. See +[Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d02bf6bd2d..cdec07f83d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,6 +144,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc + - name: Validate source architecture before dependency installation + if: needs.changes.outputs.code == 'true' + run: node scripts/verify/architecture-guards.mjs - name: Cache npm package downloads id: npm-cache uses: ./.github/actions/npm-cache @@ -773,6 +776,7 @@ jobs: tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts tests/boundary/cli/nativeSignatureCli.test.ts tests/boundary/cli/nativeMachoArm64_32Cli.test.ts + tests/boundary/cli/nativeMachoEncryptionCli.test.ts tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts src/process/capture/FilesystemSnapshot.test.ts tests/process-global/filesystemSnapshotFallbackCancellation.test.ts diff --git a/bridge/hopper_bridge.py b/bridge/hopper_bridge.py index 42aa59fcac..56b16d4ed8 100644 --- a/bridge/hopper_bridge.py +++ b/bridge/hopper_bridge.py @@ -22,6 +22,9 @@ HOPPER_SYMBOL_NAME_UTF16_UNITS = 1024 # that does not fit is null, not an older descriptor path. # 536870887 is Node's 536870888-character string ceiling minus the framing LF. # The ceiling is the encoded payload; JSON expansion is measured before it. +# Encoded JSON allowance, in bytes, shared by one instruction list, one +# native-caller list, and one caller-scan page. This release has no runtime override. +CALLER_SCAN_ALLOWANCE_BYTES = 512 * 1024 _BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES" DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024 MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464 @@ -232,6 +235,9 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset( "procedure_assembly", "procedure_callees", "procedure_callers", + "rea_caller_scan_page", + "rea_caller_scan_target", + "rea_caller_base", "procedure_info", "procedure_pseudo_code", "procedure_references", @@ -485,19 +491,30 @@ def _instruction_addresses(procedure): return result -def _native_calls(procedure, direction): +def _iter_native_calls(procedure, direction): references = procedure.getAllCallees() if direction == "outgoing" else procedure.getAllCallers() kinds = {0: "none", 1: "unknown", 2: "direct", 3: "objective_c"} - calls = {} - unresolved = [] for reference in references: source, target, kind = reference.fromAddress(), reference.toAddress(), reference.type() if source in BAD_ADDRESSES: + yield None, None, None continue if target in BAD_ADDRESSES: - unresolved.append({"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind}) + yield source, None, {"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind} continue - calls[(source, target)] = {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"} + yield source, target, {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"} + + +def _native_calls(procedure, direction): + calls = {} + unresolved = [] + for source, target, detail in _iter_native_calls(procedure, direction): + if detail is None: + continue + if target is None: + unresolved.append(detail) + else: + calls[(source, target)] = detail return calls, sorted(unresolved, key=lambda item: int(item["address"], 16)) @@ -547,6 +564,136 @@ def _procedure_map(document): return result +def _caller_target_data(procedure): + addresses = [] + seen = set() + size = 0 + for block in procedure.basicBlockIterator(): + for address in _block_instruction_addresses(procedure, block): + if address in seen: + continue + encoded = _hex(address) + size += len(encoded) + 4 + if size > CALLER_SCAN_ALLOWANCE_BYTES: + return {"target": _procedure_identity(procedure), "instructions": addresses, "instructions_complete": False} + addresses.append(encoded) + seen.add(address) + return {"target": _procedure_identity(procedure), "instructions": addresses, "instructions_complete": True} + + +def _caller_scan_target(document, params): + return _caller_target_data(_procedure(document, params.get("procedure"))) + + +def _caller_base(document, params): + procedure = _procedure(document, params.get("procedure")) + callers = [] + size = 0 + complete = True + for item in procedure.getAllCallerProcedures(): + address = _hex(item.getEntryPoint()) + size += len(address) + 4 + if size > CALLER_SCAN_ALLOWANCE_BYTES: + complete = False + break + callers.append(address) + return { + **_caller_target_data(procedure), + "callers": sorted(callers, key=lambda value: int(value, 16)), + "callers_complete": complete, + } + + +def _caller_scan_page(document, params): + """Observe forward relationships in one bounded, stateless bridge request.""" + start = params.get("start") + if isinstance(start, bool) or not isinstance(start, int) or start < 0: + raise InvalidRequestError("Caller scan start must be a non-negative integer") + segments = document.getSegmentsList() + counts = [segment.getProcedureCount() for segment in segments] + total = sum(counts) + end = min(start + 64, total) + if start > total: + raise InvalidRequestError("Caller scan start exceeds the procedure inventory") + items = [] + item_bytes = 0 + page_limit = CALLER_SCAN_ALLOWANCE_BYTES + oversized = False + identities = {} + containing = {} + + def identity(procedure): + entry = procedure.getEntryPoint() + if entry not in identities: + identities[entry] = _procedure_identity(procedure) + return identities[entry] + + def owner(address): + if address not in containing: + procedure, _ = _containing_procedure(document, address) + containing[address] = identity(procedure) if procedure is not None else None + return containing[address] + + segment_start = 0 + for segment, count in zip(segments, counts): + begin, stop = max(start - segment_start, 0), min(end - segment_start, count) + segment_start += count + for index in range(begin, max(begin, stop)): + candidate = segment.getProcedureAtIndex(index) + item = {"caller": identity(candidate), "callees": [], "references": []} + size = len(json.dumps(item, ensure_ascii=True).encode("utf-8")) + 1 + remaining = page_limit - item_bytes + too_large = size > remaining + if not too_large: + for callee in candidate.getAllCalleeProcedures(): + address = _hex(callee.getEntryPoint()) + size += len(address) + 4 + if size > remaining: + too_large = True + break + item["callees"].append(address) + if not too_large: + callee_entries = set(item["callees"]) + for source, destination, call in _iter_native_calls(candidate, "outgoing"): + # Account for every native record, even when its target cannot + # be associated with an observed procedure. + size += 32 + if size > remaining: + too_large = True + break + if destination is None: + continue + source_procedure = item["caller"] if _hex(source) == item["caller"]["address"] else owner(source) + target_address = _hex(destination) + if target_address in callee_entries: + target_entry = target_address + else: + target_procedure = owner(destination) + if target_procedure is None: + continue + target_entry = target_procedure["address"] + edge = { + "source_address": _hex(source), "target_address": _hex(destination), + "source_procedure": "caller" if source_procedure is not None and source_procedure["address"] == item["caller"]["address"] else source_procedure, + "target_procedure": target_entry, + "call": call, + } + size += len(json.dumps(edge, ensure_ascii=True).encode("utf-8")) + 1 + if size > remaining: + too_large = True + break + item["references"].append(edge) + if too_large: + oversized = not items + break + items.append(item) + item_bytes += size + else: + continue + break + return {"start": start, "next": start + len(items), "total": total, "items": items, "oversized": oversized} + + def _string_record(segment, address, display): """Read one native typed object, retaining its shortened display separately.""" length = segment.getObjectLength(address) @@ -887,6 +1034,15 @@ def _dispatch(method, params): if method in EXHAUSTIVE_ANALYSIS_METHODS: _api().require_analysis_complete(document, method) + if method == "rea_caller_scan_target": + return _caller_scan_target(document, params) + + if method == "rea_caller_base": + return _caller_base(document, params) + + if method == "rea_caller_scan_page": + return _caller_scan_page(document, params) + if method == "analyze_function": return _analyze_function(document, params) if method == "read_function_instructions": diff --git a/docs/adb-device-analysis.md b/docs/adb-device-analysis.md index 1df083c5c7..634a004b0a 100644 --- a/docs/adb-device-analysis.md +++ b/docs/adb-device-analysis.md @@ -103,6 +103,10 @@ existing analysis families directly, for example | `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict | | `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run | +Screen capture validates the PNG signature, IHDR framing, dimensions, and IHDR +checksum before publishing. It does not validate encoding fields or decode and +validate image contents; that limitation is retained in Evidence. + When `push_adb_file` receives an existing directory, adb places the local file inside it under the local basename; Evidence reports that effective file path while retaining the caller's directory path in the input parameters. The diff --git a/docs/agent-environments.md b/docs/agent-environments.md index 62dbb3d76e..8bcc62be86 100644 --- a/docs/agent-environments.md +++ b/docs/agent-environments.md @@ -739,14 +739,14 @@ tools are not served, so no call claims a change it does not make; each one's plan states why in its `reason`. A served write changes the live state, which every MCP session and `data/get` then see: -| Effect | Call | -| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `create` | Adds an entity to the collection the path names, built from the observed entity with each ruled field taken from the call or generated, at the plan's `insert` position. A plan tied to its exemplar's collection writes only there. | -| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, composed, reference, unique and renewed changes the plan models. | -| `update` (action) | A `POST`, `PUT` or `PATCH` to an item path plus an action segment (`/{type}/{id}/archive`) sets, on the entity before the segment, each scalar the recorded calls showed the action set (`set`), with any sent members; it is served only when a recorded call found those fields already set, so a toggle never is. | -| `delete` | Removes the entity the path names. | -| bulk `update` or `delete` | A `DELETE`, `PUT` or `PATCH` at a collection, or a `POST` to it plus `delete`, `remove` or `destroy`, applies its effect to every entity the body names, as recorded: an array of ids or of objects carrying the id field, at the root or as the body's one array member (`{"IDs": […], "Read": true}`). | -| `document` | Merges the body into the plan's document object, or replaces it for `PUT`. | +| Effect | Call | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `create` | Adds an entity to the collection the path names, built from the observed entity with each ruled field taken from the call or generated, at the plan's `insert` position. A plan tied to its exemplar's collection writes only there. | +| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, composed, reference, unique and renewed changes the plan models. | +| `update` (action) | A `POST`, `PUT` or `PATCH` to an item path plus an action segment (`/{type}/{id}/archive`) sets, on the entity before the segment, each scalar the recorded calls showed the action set (`set`), or flips each boolean they showed it flipping both ways (`toggle`), with any sent members; it is served only when a recorded call found what it sets already set, or flipped back what it toggles. | +| `delete` | Removes the entity the path names. | +| bulk `update` or `delete` | A `DELETE`, `PUT` or `PATCH` at a collection, or a `POST` to it plus `delete`, `remove` or `destroy`, applies its effect to every entity the body names, as recorded: an array of ids or of objects carrying the id field, at the root or as the body's one array member (`{"IDs": […], "Read": true}`). | +| `document` | Merges the body into the plan's document object, or replaces it for `PUT`. | An update's unsent changes follow the plan's rules. A `held-clock` is a clock the client sends back as it read it and the application stamps anew (Ghost's @@ -757,7 +757,9 @@ the entity already names under its `created…by` counterpart into a `modified…by` or `updated…by` field. A `derived` field takes a value showing the one sent member's text that the entity stores nowhere (a field Ghost derives from `html`), only with that member's recorded value. An action's -`set` rule sets a scalar to the value the recorded calls left it at. +`set` rule sets a scalar to the value the recorded calls left it at, and its +`toggle` rule flips a boolean, refusing a call on an entity that holds none +there (Miniflux's `PUT /v1/entries/{id}/bookmark` stars and unstars). Its answer has the status and content type of the plan's recorded response, with the written entity or its identity rendered in, no body, or the recorded diff --git a/docs/apk-signature.md b/docs/apk-signature.md index ac9ecca957..d6150d25c6 100644 --- a/docs/apk-signature.md +++ b/docs/apk-signature.md @@ -179,22 +179,26 @@ share the application workflow, projection, retention and publication owners. - `schemes_agree_on_signer` (observed): every present block scheme presents the same _set_ of signer certificates. An extra signer in one scheme is disagreement; a signer without a certificate makes it `null` (unknown). - Build-tools 33+ rotate through v3.1 by default, so a rotated app usually - reads `false` here and `true` below. + A single present block scheme with known signers agrees with itself; + v1 is compared separately below. - `signer_change_declared_by_rotation` (declared): the signers differ, but - each appears in a lineage the APK declares. Read the two together: a - rotated APK shows `false` / `true`; signers that differ with no declared - lineage containing them show `false` / `false`. It is `null` when a - signer's identity is unknown, or when the signers differ and a declared - lineage could not be decoded. + each appears in a lineage the APK declares. Read the two together: block + schemes with different signers covered by a declared lineage show `false` + / `true`; differing signers without such a lineage show `false` / `false`. + A lineage alone does not establish a change between block schemes. It is + `null` when a signer's identity is unknown, or when the signers differ and + a declared lineage could not be decoded. - `v1_signers_match_block_signers` (observed): the v1 signer certificates equal what the lowest block scheme that has signers (v2, else v3, else v3.1) expects v1 to carry. That is its signer set, except that a v3/v3.1 signer declaring a rotation lineage expects the lineage's first (original) certificate, as apksig requires. `false` is notable: the JAR - signature names a different key than the block signature. A key rotation - keeps v1 (and v2) on the original key, so it reads `true` while - `schemes_agree_on_signer` reads `false`, with or without v2. `null` when + signature names a different key than the block signature. When v1 uses + the lineage's original key and v3 its newer key, this comparison reads + `true`. If v3 is the only present block scheme, agreement is `true` and + declared inter-block signer change is `false` even though rotation is + present. If v2 also uses the original key, agreement is `false` and the + declared inter-block change is `true`. `null` when v1 or a populated block scheme is absent, any signer's identity is unknown (a `decode_error`, a `null` certificate or a `subjectKeyIdentifier` `SignerInfo`), or the lineage's original certificate cannot be diff --git a/docs/bun-standalone.md b/docs/bun-standalone.md new file mode 100644 index 0000000000..5b6b915ab6 --- /dev/null +++ b/docs/bun-standalone.md @@ -0,0 +1,62 @@ +# Bun standalone executables + +`inspect-artifact` inventories embedded Bun module graphs in Mach-O `__BUN,__bun`, +PE `.bun`, ELF `.bun`, and appended trailers. `extract-artifact` writes their stored +module contents through the existing safe output tree. Neither operation executes +the executable or decompiles bytecode. + +```sh +rea inspect-artifact ./hello --json +rea extract-artifact ./hello --json +``` + +In MCP, open the executable with `open_binary`, then call `inspect_artifact`. +Use `detail: "summary"` for large graphs and `inspect_analysis_view` to page retained +Evidence. The root metadata's `entry_point_logical_path` is the reader-owned +location of its entry module. Use it as an inventory `logical_paths` filter, +then read selected node fields with the returned content identity. This also +works for names mapped to indexed extraction paths. The complete Evidence stays +retained; no source-name normalization is needed in the caller. + +The root reports the carrier, layout, module count, entry point, raw flags and +compile exec-argv. Each module retains its source name, loader and encoding values, +entry-point status, sizes and SHA-256. Sourcemaps, bytecode and module-info regions +are separate children. Source names such as `/$bunfs/root/main.js` remain in +metadata; extraction uses a relative path. Unsafe or non-UTF-8 names receive an +indexed path and retain their source spelling or bytes. Virtual names deeper +than 256 logical path parts also use an indexed path with an explicit limitation, +preventing unbounded path-tree work while retaining the source fact. Duplicate names remain +separate occurrences; ordinary extraction collision rules apply. + +The recognized record shapes are the 36-byte Bun 1.3.3 layout and the 52-byte +Bun 1.3.9 layout. These labels identify structural layouts, not a detected compiler +version. Bun does not store an explicit revision identifier: unknown table sizes +and ambiguous known layouts are unsupported. A future layout that is byte-for-byte +compatible cannot be distinguished. Invalid pointers and truncated claimed sections +are malformed. An absent trailer without a named section does not establish a Bun +graph. Unknown enum values remain raw, with a null name. + +Sourcemaps are stored Bun regions, not necessarily standalone JSON source maps. +Extracted contents retain their original encoding. Use JavaScript analysis only on +compatible source files; bytecode is retained as evidence, not decoded. + +Module parsing reads exact metadata ranges rather than loading stored asset +bodies or whole pages around small pointers. A cache retains at most 256 KiB +and 1,024 ranges for one reader preparation. Cumulative uncached metadata reads +are limited to 256 MiB, checked before allocation or I/O. The module table, +decoded strings and combined logical path tree have separate admission checks; +at most 250,000 distinct path prefixes are admitted before inventory construction. +Stored bodies are range-streamed for hashing and extraction. Header/suffix observations are reused +only for the same admitted file state, with selected-path checks before publication. + +See [limits and recovery](design/limits-and-recovery.md) for admission ceilings. +Synthetic tests cover the carriers and malformed declarations. Run +`node scripts/verify-bun-standalone.mjs` after `npm run build:cached` for local Bun +verification, and add `--docker` to compile a Linux fixture with `oven/bun:1.4.0`. +Bun and Docker are fixture producers only and are not installed by REA. + +Layout references: [Bun 1.3.3 serializer](https://github.com/oven-sh/bun/blob/bun-v1.3.3/src/StandaloneModuleGraph.zig) +and [Bun 1.3.9 serializer](https://github.com/oven-sh/bun/blob/bun-v1.3.9/src/StandaloneModuleGraph.zig). + +The current 52-byte record order can also be checked in the +[Bun 1.4.4 Rust serializer](https://github.com/oven-sh/bun/blob/bun-v1.4.4/src/standalone_graph/StandaloneModuleGraph.rs). diff --git a/docs/cli.md b/docs/cli.md index d2b23e10bc..620ef19d92 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1256,3 +1256,8 @@ rea inspect-pe-load-config /absolute/path/to/application.exe --json Reports TLS callbacks and size-covered load-config fields without execution. Use `--max-callbacks` and `--max-file-bytes` to set read budgets. See [PE TLS and load configuration](pe-load-config.md). + +Bun standalone executables use the existing `inspect-artifact` and +`extract-artifact` commands to inventory and materialize embedded source, +sourcemaps and bytecode without running the executable. See +[Bun standalone executables](bun-standalone.md) for layout coverage and limits. diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 5123a4efac..3461bd6855 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -283,8 +283,9 @@ patterns. A provider being unavailable does not discard its selected paths; doctor reports its readiness separately. Explicit selections override discovered defaults, including when setup runs again with a changed selection. Unrelated environment variables, process ownership tokens and one-shot target paths are not -copied. Existing client environment overrides remain intact unless setup supplies -the same key. +copied. For an explicitly configured startup target, see the +[initial-target settings](mcp-contracts.md#initial-target-at-startup). Existing +client environment overrides remain intact unless setup supplies the same key. You can edit `startupTimeoutMs` and `toolTimeoutMs` there; doctor keeps positive caller-selected values aligned and reports a non-blocking advisory when a tool timeout is shorter than the known provider allowance. diff --git a/docs/design/limits-and-recovery.md b/docs/design/limits-and-recovery.md index 8b94489dc7..94d9642010 100644 --- a/docs/design/limits-and-recovery.md +++ b/docs/design/limits-and-recovery.md @@ -378,3 +378,17 @@ The [verification report](../verification/runtime-analysis-settings.md) separate public MCP checks, simulated provider coverage, real Ghidra execution, and measured task costs. It does not establish real-provider behavior for every editable setting or active worker telemetry. + +## Bun standalone module graphs + +`MAX_BUN_MODULE_COUNT` admits at most 100,000 module records. +`MAX_BUN_RETAINED_BYTES` admits at most 256 MiB of serialized graph payload and +separately bounds retained entry metadata. Physical metadata read work is also +limited to 256 MiB per preparation, including repeated uncached ranges. A +256 KiB / 1,024-entry exact-range cache avoids full-page amplification. The +combined logical path tree admits at most 250,000 distinct prefixes; this is +checked before adding nodes, with shared prefixes counted once. The shared +artifact expansion budget also bounds the sum of referenced member lengths. Refusals name the boundary. +There is no runtime override; additional host memory or `NODE_OPTIONS` does not +raise these ceilings. Use a smaller producer fixture or a separately reviewed +reader for larger graphs. No partial extraction is claimed on admission failure. diff --git a/docs/dotnet-bundles.md b/docs/dotnet-bundles.md new file mode 100644 index 0000000000..121d030f0e --- /dev/null +++ b/docs/dotnet-bundles.md @@ -0,0 +1,110 @@ +# .NET single-file bundles + +Use `rea inspect-artifact /absolute/path/host --json` to inventory an ordinary +.NET single-file host and `rea extract-artifact /absolute/path/host --json` to +materialize its members into a fresh, REA-owned output directory. With a host +selected through `open_binary`, the corresponding MCP tools are +`inspect_artifact` and `extract_artifact`. No .NET installation is needed for +inspection or extraction. + +For a compact inventory, call `inspect_artifact` with `detail: "summary"`, +then use `inspect_analysis_view` with the returned `parent_evidence_id`. +Inventory occurrence pages accept `logical_paths`, `occurrence_ids`, selected +`fields`, `offset`, and `limit`; node pages accept batches of `artifact_ids` +to retrieve expanded-byte digests. Include `format_metadata` and +`source_location` when member declarations and stored extents matter. Identity +fields survive projection, and the complete inspection Evidence stays retained. + +REA detects the apphost marker and its preceding header offset from PE, ELF, +and thin Mach-O bytes, regardless of filename. An unbundled apphost's zero +offset remains an ordinary native artifact. Universal Mach-O slice inventory +keeps its existing behavior; bundled contents inside slices are not expanded. +When a native file has a recognized PyInstaller CArchive, apphost-marker +scanning stops before its payload. A .NET helper stored inside that archive +does not replace the outer container's format; an outer .NET host can still +contain a PyInstaller helper as a member. + +## Supported layout + +| Header | Producer generation | Member encoding | +| ------ | ---------------------------------- | --------------------------- | +| 1.0 | .NET Core 3.x | Uncompressed | +| 2.0 | .NET 5 | Uncompressed | +| 6.0 | .NET 6 and later using this layout | Uncompressed or raw DEFLATE | + +Versions, minor versions, and flags outside this set are refused explicitly; +they do not silently become an ordinary PE or a successful empty inventory. +Flags 0 and the v2+ compatibility flag 1 are admitted. Unknown file-type bytes +are retained with a limitation, rather than treated as executable authority. +Encrypted/protected wrappers and recovering their payloads are outside this +reader's scope. + +The host keeps its whole-file SHA-256. Each embedded file gets its expanded-byte +SHA-256, path, declared size, compressed size, and `format_metadata` declaration +(`major_version`, `minor_version`, `file_type`). Its `source_location` describes +the stored host extent, including the compressed length when applicable. +The root manifest's `root_format` is `dotnet-bundle`; the root content node +still records its native PE/ELF/Mach-O format. A file-type declaration or `.dll` +suffix does not establish a valid assembly. Inspect an extracted component +explicitly with the managed tools to establish PE/CLI facts. + +## Admission and containment + +Metadata is range-read with a 64 KiB cache. Policy budgets are 16 MiB of +manifest metadata, 100,000 files, 64 KiB per UTF-8 string, 512 MiB of stored or +expanded bytes per file, 256 path parts per member, 250,000 distinct in-memory +path-tree nodes, and 2 GiB of aggregate expanded bytes. The node budget counts +shared directory prefixes only once. These are REA +resource policies, not limits imposed by Microsoft's format. Marker scanning +streams 64 KiB chunks and reuses completed probe facts during extraction; +ordinary native files without a marker incur one extra +read pass before host hashing. + +Extraction hashes the whole host again through the same descriptor used to read +members and compares it with the inventory's root digest before creating an +output directory. This adds a full-host read pass to extraction, including when +the embedded members themselves are small. + +Malformed/truncated headers, unsafe or conflicting paths, overlapping member +ranges, members crossing the manifest, contradictory configuration extents, +invalid UTF-8, corrupt/trailing compressed data, and incorrect expanded sizes +fail before output publication. Source descriptor/path identity is checked +during reading; extraction verifies the host digest and member bytes against +the inventory. Existing output roots are never overwritten. Cancellation and +failures release owned streams/descriptors through the shared extraction +rollback scope. + +## Reproduce producer fixtures + +The source-owned generator uses the installed .NET 8 SDK's **Microsoft +HostModel** producer to emit x64 v1, v2, plain v6, and compressed v6 bundles +for Windows, Linux, or macOS. It does not run generated hosts or download runtime packs. Supply an +SDK apphost template from +`packs/Microsoft.NETCore.App.Host.//runtimes//native/apphost` +(`apphost.exe` on Windows). Pass `windows` (the default), `linux`, or `osx` +as the optional third generator argument to match that template. + +```bash +dotnet build scripts/fixtures/dotnet-bundle/BundleFixture.csproj -c Release --configfile scripts/fixtures/dotnet-bundle/NuGet.Config -o /absolute/path/producer -p:BaseIntermediateOutputPath=/absolute/path/producer-obj/ +dotnet /absolute/path/producer/BundleFixture.dll /absolute/path/apphost.exe /absolute/path/absent-bundles-directory +rea inspect-artifact /absolute/path/absent-bundles-directory/net8-compressed/Probe.exe --json +rea extract-artifact /absolute/path/absent-bundles-directory/net8-compressed/Probe.exe --json +``` + +The generator writes `oracle.json` beside each host. An independent C# +`BinaryReader`/`DeflateStream` pass records version, member types and stored +extents, then compares every expanded member digest with its source file. +Compare those records with inspection Evidence and the extracted bytes. +This verifies Microsoft's compression writer separately from synthetic parser +fixtures; it is not evidence that the bundled host executes on a target runtime. + +Deterministic boundary and public-entrypoint coverage: + +```bash +npm run test:focused -- tests/boundary/filesystem/dotnetBundleArtifacts.test.ts tests/acceptance/dotnetBundleJourney.test.ts +``` + +Layout references: Microsoft's [manifest writer](https://github.com/dotnet/runtime/blob/v8.0.22/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs), +[file entry writer](https://github.com/dotnet/runtime/blob/v8.0.22/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs), +[apphost marker handling](https://github.com/dotnet/runtime/blob/v8.0.22/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs), +and [compression producer](https://github.com/dotnet/runtime/blob/v8.0.22/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs). diff --git a/docs/firmware-analysis.md b/docs/firmware-analysis.md index e5a7a8f4cc..ac5d109614 100644 --- a/docs/firmware-analysis.md +++ b/docs/firmware-analysis.md @@ -82,6 +82,11 @@ Published file `relative_path` preserves producer spelling for lineage; `path` uses the writer's actual normalized filename. An extracted filename that cannot be represented losslessly as UTF-8 fails publication explicitly with its raw name bytes in hexadecimal instead of silently omitting the entry. +Unpublished symlinks retain the extracted tree's target bytes in lowercase +`link_target_hex`; `link_target` is null when those bytes are not valid UTF-8. +Older retained Evidence may omit the hexadecimal field. Unblob may have rewritten +the target before REA observed it, so these bytes are not necessarily those stored +in the firmware image. ## CLI and MCP diff --git a/docs/installation.md b/docs/installation.md index fcf661da4c..2b0ab76286 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -928,7 +928,12 @@ selected only when it is the arm64 family's only candidate. REA checks the entire source's SHA-256 while copying the slice; source identity and reported offsets still refer to the original container. Any other ambiguous architecture subtype requires an explicitly extracted thin image; REA does not guess. -Configured loader arguments remain explicit overrides. +Set `HOPPER_LOADER_ARGS_JSON` in REA's launch environment to a JSON array of +strings to override the derived Hopper loader arguments, for example +`["-l","Mach-O"]`. Empty string elements are preserved; NUL bytes are not +accepted. An omitted setting or empty array uses the derived arguments. `rea +setup` forwards an explicitly selected setting to supported registrations; +restart the MCP server to apply a change. These arguments are Hopper-specific. FAT64 is distinct from the CPU architecture: FAT32 containers can contain 64-bit executable images. This preparation avoids the Raw Binary loader dialog observed with native FAT64 loading on Hopper 6.1.0-demo. diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 6f9fb270c7..2f89295c45 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -224,6 +224,25 @@ resulting value. Deferred callback bodies do not become inputs to the enclosing assignment. Computed method keys belong to the enclosing callable; parameter defaults and method bodies belong to the method. Uncertain later property reads remain opt-in through `include_ambiguous_dynamic_edges`. + +Binary, logical, unary, template, conditional, and sequence expressions retain +`influences` edges from supported immediate inputs to the result, then to a +direct declaration, assignment, call argument, or return. These are static +dependencies, not aliases or proof that the expression executed; conditional +operands and logical-assignment writes carry `conditional: true`. Unsupported +operand dependencies, unmodeled receiving containers, dynamic call results, +and sequence side effects remain unknown. An applicable +unknown makes the trace status `partial` even when a seed was found. Literal +operator operands, direct call arguments and directly returned scalar literals +can also be seeds. Older graphs that only recorded a call-site literal remain +searchable. The offered per-file node capacity also bounds the producer's +expression and operand collections before graph projection. +Expression nodes and unknowns share the existing per-file graph admission +budgets; omitted facts make graph coverage truncated and the trace partial. +Consumers that previously treated `found` as compatible with a relevant +unknown should handle `partial` and inspect the returned unknowns. Retained +graphs without `influences` edges remain readable but do not gain edges until +the application is analyzed again. Global timer, `fetch`, `WebSocket`, `JSON.parse`, and coercion calls receive these built-in effects only when the file has no visible possible write to that global, including a computed write through a global object. The check does not diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 772e97bd6e..36e64bf205 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -243,13 +243,17 @@ authenticates the saved bundle; the full source is retained on disk. The canonical parser admits PE/CLI bytes and their metadata/CIL without loading an assembly. It reports observed implementation markers and unavailable facts; -it does not unpack .NET single-file hosts or infer a NativeAOT identity from an -ordinary native PE. It does not decode IL2CPP; use +it does not itself unpack .NET single-file hosts or infer a NativeAOT identity +from an ordinary native PE. It does not decode IL2CPP; use [IL2CPP metadata](il2cpp-metadata.md) for the image, type and method names in a Unity `global-metadata.dat`. Inputs without admitted CLI metadata do not become managed assemblies through naming or routing guesses. Inspect separately obtained components explicitly and preserve their identities. +For ordinary .NET single-file hosts, use the existing artifact tools to +[inventory and extract bundle members](dotnet-bundles.md), then inspect the +extracted assembly explicitly with the managed tools. + A valid implementation marker establishes a candidate native boundary, not recovered native semantics or runtime behavior. diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 431107ea8a..4b5dd9e09b 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -177,8 +177,13 @@ if retention is unavailable or fails, the diagnostic reports that reason. SDK-generated input validation failures are normalized into the same canonical `invalid_request` text when the bounded diagnostic fits the response budget. The complete field/type diagnostic is preserved when normalization fits the -response budget, including request-specific rejected fields and reasons. Oversized -SDK diagnostics keep the existing retained-Evidence recovery path. An unknown +response budget, including request-specific rejected fields and reasons. Zod +input issues retain their field paths and available type and range constraints. +SDK diagnostic parts may be distributed between field issues and the main +message so each retained explanation appears once. +If their conservative projection allowance is exhausted, the response states +that field-level detail was omitted and retains the original SDK diagnostic. +Oversized SDK diagnostics keep the existing retained-Evidence recovery path. An unknown tool remains an MCP JSON-RPC `-32602` error, but its message directs the caller to `tools/list` and an advertised operation instead of repeating the unknown name. @@ -435,10 +440,12 @@ result's `_meta`, whether the call succeeded or failed: ``` The marker is declared once per tool contract (`returnsTargetContent`) and is -on by default: only `export_evidence_bundle` and `record_unknown`, whose -results and errors contain only REA session state or caller-supplied values, -omit it. Tools whose results or errors can include provider, launcher, or -device output stay marked, including `binary_session` (provider failure +on by default: `export_evidence_bundle`, `record_unknown`, `search_guidance`, +and `read_guidance_section` omit it. The first two contain only REA session +state or caller-supplied values; the guidance tools return REA's own installed +documentation, not text from an analysis target. Tools whose results or errors +can include provider, launcher, or device output stay marked, including +`binary_session` (provider failure reasons and availability diagnostics), `close_binary` (provider cleanup diagnostics with captured launcher output), `inspect_adb_client` and `inspect_apktool_client` (version output and stderr excerpts), and @@ -590,10 +597,12 @@ reach WABT or change the original source Evidence ID. Completed `inspect_macho` Evidence supports a summary and `view: {kind: "macho", facet: "exports", offset: 0, limit: 64}`. Facets are `entrypoints`, `architectures`, `build_metadata`, `load_commands`, -`dependencies`, `imports`, `exports`, `segments`, and `provenance`; pages return -the original rows. The summary keeps the selected slice, header fields and UUID, -and replaces each collection with its returned count, reported total and -`exhaustive` flag. Each page repeats the paged collection's `source_total` and +`dependencies`, `imports`, `exports`, `segments`, `encryption`, and +`provenance`; pages return the original rows. Evidence retained before +`encryption` was reported pages no encryption rows and reports that collection +as non-exhaustive with an unknown status. The summary keeps the selected +slice, header fields and UUID, and replaces each collection with its returned +count, reported total and `exhaustive` flag. Each page repeats the paged collection's `source_total` and `source_exhaustive`; exhausting a non-exhaustive collection does not establish that unreturned symbols are absent. Large frameworks return megabytes of symbols (Electron Framework: 2.5 MB of imports and exports, 3.3 KB as a @@ -791,6 +800,22 @@ inspection fails or is cancelled before a provider client exists. If no client remains, no provider run or health observation is available; follow the cleanup error's reported resources. +## Initial target at startup + +MCP normally starts without a target; use `open_binary` to select one during the +session. To open a target before serving MCP, set `HOPPER_TARGET_PATH` to the +selected path in REA's launch environment. `HOPPER_TARGET_KIND` accepts +`executable` (the default) or `database`. Despite the names, these are initial +session settings; the configured provider still determines how the target is +opened. If the initial open fails, REA reports the failure and exits with code 1 +without serving MCP. Shutdown during startup cancels and cleans up the open as +described below. + +The source-checkout helper `node scripts/print-mcp-config.mjs /path/to/target` +prints a registration containing these settings; `--kind database` selects the +database kind. The helper is not part of the installed npm package. `rea setup` +does not copy these one-shot target settings into generated registrations. + ## Transports The eager stdio runtime owns SIGINT, SIGTERM and stdin EOF before opening an diff --git a/docs/mcp-prompts.md b/docs/mcp-prompts.md index 55b66da374..ef630404bf 100644 --- a/docs/mcp-prompts.md +++ b/docs/mcp-prompts.md @@ -18,6 +18,26 @@ non-string values are rejected with field paths. `action: "list"` accepts no workflow identifier or arguments. The MCP adapter respects the same `REA_MCP_TOOL_FAMILIES` scope as native prompts, including render requests. +To find a section in the installed guidance bundle by analyst question, use +`search_guidance` and pass a returned locator to `read_guidance_section`. The +CLI equivalent is `rea guidance --query "exact JavaScript module"`, followed +by `rea guidance --action read --locator LOCATOR`. Search is limited to the +packaged managed skill files, works without providers or a target, and returns +deterministically paged matches. Every non-generic query word must occur in the +section; generic-only queries return no matches. Results are documentation; locators +are bound to the installed bundle and report stale or unavailable guidance without +returning cached section content. A no-match result says nothing about analysis +capability availability. + +Search returns at most 20 matches per page with 500-code-unit context excerpts. +Read retrieves the complete selected section. Admission permits 1 MiB per file, +4 MiB per bundle, 4,096 sections, and 25,165,824 expanded UTF-16 code units before +section text is materialized. Unreadable or oversized bundles return +`bundle_unavailable`, with no partial index or inferred absence. Cleanup failures +remain errors with their retained retry owner. The bundle and complete section +are bound by fixed-size hashes in locators, so a long heading does not make its own +locator unreadable. + ## Prompt inventory | Prompt | Purpose | diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 15350f894e..89d4bf811c 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -343,6 +343,27 @@ Hopper regex mode. ## Bounded static traces +Hopper caller queries, incoming procedure references and function dossiers +reconcile reported reverse relationships with forward call observations. Each +standalone query or composed workflow scans the current document in pages of at +most 64 procedures and retains at most 4 MiB of encoded scan results. A composed +workflow reuses its own index and clears it when the workflow finishes. The +default cooperative scan budget is +2,000 ms; set `REA_HOPPER_CALLER_SCAN_BUDGET_MS` to a positive integer number of +milliseconds before starting REA to change it. The default limits additional +work on the serial Hopper bridge; larger documents may need a higher setting. +Results report the examined procedure count and any budget interruption. +An individual page, native caller list, and target instruction list each have +a 512 KiB encoded allowance. Hitting one reports the incomplete observation. +Cancellation is checked between pages, but cannot interrupt a Hopper API call +already executing; Hopper's native API itself materializes its reference arrays +before REA can bound their copies. Hopper exposes no document revision token, so scans are not +reused across workflows and coverage and consistency during a live scan remain +unknown even when every procedure was examined. +Hopper observations are re-observed rather than replayed from snapshots because +the provider has no document revision token. Safe observations may still be +retained as Evidence. + `trace_native_ui_action` / `rea trace-native-ui-action ` accepts one authored selector/object ID, native symbol or exact function address. It joins UI wiring to uniquely matched class/selector implementations and follows @@ -568,6 +589,40 @@ segments do not produce file ranges. When the selected slice's container offset is unknown, those mapped locations are omitted with an explicit limitation; architecture inventory locations remain available. +`inspect_macho.encryption` reports the `LC_ENCRYPTION_INFO(_64)` state of every +slice, not only the selected one. It decodes only the FAT table, slice headers +and load commands. Like every Mach-O container read, a `0xcafebabe` input of at +most 16 MiB is first read whole to rule out a JVM class file, so that read +includes code bytes; larger inputs and thin images read only the structures +above. A slice is `encrypted` when its single encryption command has a nonzero +`cryptid`, `not-encrypted` when `cryptid` is zero, and `absent` only when every +load command was read and none was an encryption command. A command whose width +or `cmdsize` contradicts the image, more than one such command, or a range past +the slice is `malformed`; a slice whose commands could not be fully read is +`malformed` or `unknown` with a reason, keeping any commands seen before the +failure. A slice with more than two encryption commands keeps the first two +and states the count in its reason. Range mapping in all slices shares one +budget of segment and section records (16,384, a sixteenth of the native +output budget); a slice past it keeps its status and reports its mapping as +`unknown` with that reason. Framing caps retained segment commands before decoding. +Only a successful mapping consumes the shared allowance; failed mappings leave +it available to later slices. Each slice's `range` is end-exclusive in slice and input-file +coordinates and maps to file-backed segment fragments, VM addresses and the +overlapping nonempty sections; bytes outside every segment remain unmapped +fragments. A segment with no VM size keeps its file bytes as fragments with +null VM addresses, and a segment whose file size exceeds its VM size or whose +VM range leaves the address space makes the mapping `unknown` with a reason +while the slice keeps its encryption status. `architecture` is the lipo name of the record at the same offset, +size and CPU identity, and is null when no single record matches. A ciphertext +range adds a limitation naming its input-file bytes, because disassembly, +decompilation, strings and references derived from them do not describe +program code. Encryption is read through the same regular-file descriptor +that `inspect_macho` binds for its load-command reads, whose SHA-256 must match +the session target before and after the native tools run, or the call reports +`artifact_changed`. A target that cannot be opened, or a symlinked, non-regular +or changing target, fails the call instead of reporting tool facts without an +encryption status. + Hopper's `address_to_file_offset` combines the native image-relative mapping with original-file coordinates only after checking the source against the session's SHA-256. FAT tables and embedded Mach-O headers come from that same hashing pass. @@ -604,6 +659,9 @@ complete observation. undecodable filenames and skipped symlinks, as partial coverage. Known executable roots remain available; unexamined names and symlink targets cannot establish an empty root set or the absence of other roots. + A dependency symlink whose target is not valid UTF-8 is undetermined, with + its stored target bytes in the limitation and partial coverage; a selected + root crossing such a symlink is refused with the same bytes in its diagnostic. - `inspect_asset_catalog` / `rea inspect-asset-catalog ` reads compiled `Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw rendition fields, pagination and exact UI resource-name matches are returned. @@ -757,6 +815,11 @@ complete observation. files and universal slices before metadata decoding. CPU subtypes and capability bits remain source facts; an `arm64` request may select an `arm64e` image under the existing slice preference policy. + Objective-C class, protocol and category lists require eight-byte entries; + Swift type and conformance sections require four-byte entries. Misaligned + section lengths and unsupported non-PowerPC big-endian CPU declarations return + `unsupported_target` with the selected path and precise reason. Recovery + selects a well-formed or supported artifact and reopens that target. The byte reader holds the whole selected file in memory, so it admits files up to 64 MiB / 67108864 bytes by default. Select a larger budget with `max_input_bytes` (CLI `--max-input-bytes`), at most 1 GiB / 1073741824 diff --git a/docs/specs/agent-environments/write-sorts-references-toggles/plan.md b/docs/specs/agent-environments/write-sorts-references-toggles/plan.md new file mode 100644 index 0000000000..80cfd70c25 --- /dev/null +++ b/docs/specs/agent-environments/write-sorts-references-toggles/plan.md @@ -0,0 +1,11 @@ +# Implementation plan + +1. **Sorted creates.** Placed creates whose confirming read was a whole listing in collection order may be placed + again; narrowing keeps the sorts a later listing follows and adds those its items show, the type's id field last. +2. **Listed references.** `referenceRule` reads a sent list of ids; `many` on the reference rule and its schema; + `referencedValue` maps each id; the create merge lets an empty reference agree and frees the members it fixed. +3. **Hidden bodies.** Scenario records give a request with a body Playwright did not expose, and no `Content-Type`, a + `bodySize` of 0. +4. **Toggles.** `toggledRules` turns a plan's `set` and a later call's opposite `set` into `toggle`; the rule's schema, + serving, later-call checks and the action verdict. +5. **Skill, tests and docs.** diff --git a/docs/specs/agent-environments/write-sorts-references-toggles/spec.md b/docs/specs/agent-environments/write-sorts-references-toggles/spec.md new file mode 100644 index 0000000000..75e1fa64b3 --- /dev/null +++ b/docs/specs/agent-environments/write-sorts-references-toggles/spec.md @@ -0,0 +1,41 @@ +# Serve sorted creates, listed references and toggles + +Issue: https://github.com/morluto/rea/issues/4933 + +## Outcome + +Over 148 recorded captures (the 130 measured before and 18 from a fresh recording round), #7539 withholds 302 of 694 +write plans. A short diagnosis of the largest clusters found three gaps REA could close: + +- **A create's sort learned too late.** Grafana lists folders by title. A recording's first scratch folder sorted + after the one seeded folder, so the plan placed creates last; the second sorted before the first, the list after it + contradicted the plan, and the update and delete after that went unconfirmed. +- **Lists of references.** Homebox creates an item from `parentId` and `tagIds` and answers with the parent and tag + objects. REA modelled a parent but not a list of tags, and a create that sent `null` and `[]` disagreed with one that + named both. +- **Toggles.** Miniflux stars and unstars an entry with a body-less `PUT /v1/entries/{id}/bookmark`. Chromium exposes + no body for it, which REA read as unusable, and an action flipping a flag back was taken to contradict the call that + set it. + +## Behaviour + +- **Sorted creates.** A create a whole listing in collection order placed last is placed again by a field a later such + listing shows it sorted by, the type's id field last. A sort an earlier such listing did not follow stays ruled out, + though the entity that showed it is gone. A create another read placed keeps its place. +- **Listed references.** A field holding one object per id a sent list names, in that order, is a `reference` with + `many`. A create that sent the member as `null` or `[]` and stored the field so agrees with one that named entities, + whichever create first showed the reference; + serving stores the entities a call names, or the empty value as that create did. A `null` id stays refused where + no recorded create sent one, and a call leaving the member out stores no field where the observed create showed + none. +- **Hidden bodies.** A scenario request whose body Playwright did not expose and that names no `Content-Type` sent no + body. +- **Toggles.** An item action a later call shows flipping back a boolean an earlier call set, when no call found it + already set, has a `toggle` rule: serving flips the field, refusing an entity that holds no boolean there. +- **Skill.** Recordings run an action twice in a row on one object, reading it back after each (skill 55). + +Plans and views that need none of this are unchanged. + +## Acceptance + +- No write of the 148 captures goes from served to withheld, and no view changes. diff --git a/docs/specs/agent-environments/write-sorts-references-toggles/tasks.md b/docs/specs/agent-environments/write-sorts-references-toggles/tasks.md new file mode 100644 index 0000000000..3fb6efc9ea --- /dev/null +++ b/docs/specs/agent-environments/write-sorts-references-toggles/tasks.md @@ -0,0 +1,11 @@ +# Tasks + +- [x] Place a create again by the sort a later whole listing shows. +- [x] Learn and serve references a sent list of ids names, and let an empty reference agree. +- [x] Treat a hidden request body without a `Content-Type` as absent. +- [x] Learn, check and serve toggles. +- [x] Ask recordings to run an action twice on one object. +- [x] Add view, serving, record and unit tests. +- [x] Document the rules. +- [x] Measure the recorded captures. +- [x] Run static, formatting, documentation and test gates. diff --git a/docs/specs/agent-environments/write-sorts-references-toggles/verification.md b/docs/specs/agent-environments/write-sorts-references-toggles/verification.md new file mode 100644 index 0000000000..c499b2beca --- /dev/null +++ b/docs/specs/agent-environments/write-sorts-references-toggles/verification.md @@ -0,0 +1,88 @@ +# Verification + +Executed on macOS with Node v24.22.1 on this branch and on #7539 at `30e2a582`; the census and the base reproduction +were repeated after merging #7539 at `2817665d`, `174a0ff2`, `5691f97f` (which merges main with #7332) +and `3b800a10`. + +## Diagnosis + +Over 148 recorded captures, #7539 withholds 302 of 694 write plans. The largest clusters: Homebox item writes (36), +unconfirmed deletes (23), Grafana's feature-flag evaluation calls (15, which change no state), Linkding renumbering +bundles after a delete (7) and Miniflux's bookmark toggle (5). + +- **Grafana.** A fresh recording created "Scratch Pier 3", which the list after it showed after the one seeded folder, + then "Scratch Meadow Notes 4", which the list after it showed before the first, by title. The create plan had settled + on `last` when the first list confirmed it, so the second list contradicted it, and the folder update and delete + after it went unconfirmed. +- **Homebox.** Item creates send `parentId` and `tagIds` and answer with `parent` and `tags` objects. The recording + that created one item with neither (`null` and `[]`) and one with both was withheld: `tags` had no rule, and the + first create fixed both members to their empty values. +- **Miniflux.** `PUT /v1/entries/{id}/bookmark` sends no body and answers `204`. Chromium exposes no body for it and + the request names no `Content-Type`; REA read the body as unusable. Across the captures, every `POST`, `PUT` or + `PATCH` whose body was captured names `content-type` among its request header names (835 of 835). + +## Gates + +- `npm run -s check:fast`, `format:check`, `knip`, `scan:todos`, `docs:check`, `verify:test-discovery`, + `mcp-catalog:check` and `jscpd`: passed. +- `npx vitest run --project domain --project services`: 11,886 passed, 6 skipped, merged with `3b800a10`. An earlier run of the suite failed a + `SetupRegistrationPrewarm` runner test whose child process outran its 5-second timeout; alone it passes on this + branch and on #7539 (3.0 to 4.7 seconds). +- `npm run -s test:focused --` the environment build, export and serve CLI boundaries, the analysis-view CLI and MCP + boundaries and the environment serve MCP boundary: 7 files, 81 passed. `outputSchemaProfile`, `toolSchemaValidity`, + `mcpSchemaValidation`, `outputSchemaSemanticConstraints` and `analysisViewMcp`: 5 files, 54 passed; the compact + catalog stays under its frame. + +## Tests + +- View: a create placed last, then a create a later whole listing shows before it by title, learns `title` ascending + and its delete is served; an item create naming its parent and tags by id, after one naming none, learns + `reference` rules for `parent` and for `tags` (`many`) and is served; a body-less action two calls show starring + then unstarring an entry learns `toggle` and is served, and is not when its calls flip the flag one way only or a + later call leaves it unchanged. +- Serving: a create stores the parent and the tags its ids name, in the order sent; a create leaving out both, or + sending them as `null` and `[]`, stores `null` and `[]`; a tag the environment does not hold is refused, and so is a + `null` shelf where every recorded create named one; an entry the recording never starred flips on each call. +- Records: a hidden request body is absent when the request names no `Content-Type`, and stays unusable when it names + one. +- The nine behaviour tests fail on #7539 at `30e2a582`, `2817665d`, `174a0ff2`, `5691f97f` and `3b800a10`; the three that guard toggles and `null` ids + pass there too. + +## Write plans and views over recorded captures + +The 148 captures, built by #7539 and by this branch: 694 plans, 392 served and 399, both at `30e2a582` and after +merging `2817665d`, `174a0ff2`, `5691f97f` and +`3b800a10`. + +- No write goes from served to withheld, and no served plan changes. Newly served: Grafana's folder create, update and + delete in a fresh recording; Homebox's item create in another; Miniflux's bookmark toggle in three recordings. +- Five withheld plans explain more. Two Miniflux recordings learn the toggle but stay withheld: a list of starred + entries with no `order` parameter lists them by `published_at`, which REA answers in state order. Linkding's + unarchive learns its body-less call and stays `unconfirmed`, seen once. Two Homebox item writes learn their tag + references and stay withheld for other members. +- Views are unchanged: 3,188 views across the captures. + +## Review round + +Greptile's review of `4e777d28` raised five findings. Two were in this branch's code and are fixed: + +- A sort an earlier whole listing ruled out could come back from a later listing once the entity that ruled it out was + deleted, and a create's retry could adopt it. Each create now keeps the sorts its whole listings did not follow. +- A create sending a reference's member as `null` after a later create added the reference, the first having shown no + such field, was withheld: the empty-reference agreement now applies there too. + +Both tests fail on `4e777d28` and on #7539 at `174a0ff2`; the census is unchanged. The other three were in #7332's and +#7539's code: `308aebf0` (#7332), already here, covers the made-object count, and #7539's `174a0ff2`, merged here, +fixes the update default and the read clock spelling. + +Greptile's review of `f7c1bfe9` raised one more, fixed: once a later create sent a reference's member as `null`, a +served create leaving the member out stored `null`, though the create the plan observes left it out and stored no such +field. Serving now leaves the field out there, and still stores `null` for a call that sends it; the serving test fails +on `f7c1bfe9`. The census is unchanged. + +## Not verified + +- No environment was rebuilt and run with agents for this branch, and no recording was made with skill 55. +- The order an application gives a list read with no sort parameter (Miniflux's `published_at`) when it is not state + order. +- The rest of the Homebox cluster: `assetId` numbering and item updates; Linkding renumbering bundles after a delete. diff --git a/docs/specs/agent-environments/write-varied-calls/plan.md b/docs/specs/agent-environments/write-varied-calls/plan.md new file mode 100644 index 0000000000..93e9255730 --- /dev/null +++ b/docs/specs/agent-environments/write-varied-calls/plan.md @@ -0,0 +1,9 @@ +# Implementation plan + +1. **Update members.** `refinedBody` keeps only the required members a later update also sent. +2. **Numbered unless sent.** `sent` on sequence rules and their schema; the create merge adopts it for an echoed member + and a sequence from a call that left the member out; serving and replay take the sent value when there is one. +3. **Cleared by a PUT.** `explainUpdate` records a `PUT`'s left-out member cleared to an empty value as a default; + update refinement keeps defaults from later calls; `updatedFields` applies them to members a call leaves out. +4. **Update clocks.** `restampedFields` respells clocks from the first later read as it does nested clocks. +5. **Skill, tests and docs.** diff --git a/docs/specs/agent-environments/write-varied-calls/spec.md b/docs/specs/agent-environments/write-varied-calls/spec.md new file mode 100644 index 0000000000..4458a29f52 --- /dev/null +++ b/docs/specs/agent-environments/write-varied-calls/spec.md @@ -0,0 +1,56 @@ +# Serve writes whose calls send different members + +Issue: https://github.com/morluto/rea/issues/4933 + +## Outcome + +A fresh recording round of seven applications, main against #7332, showed the recording skill now varies what the two +creates and two updates of a type send, as it asks. Where the second call left out a member the first sent, REA +withheld the write: + +- **Updates.** An update plan required every member its first call sent and never narrowed that, so EspoCRM's second + contact update, which changed only an email address, was refused in both recordings of the branch. +- **Members numbered when left out.** Linkding stores a bundle's `order` as sent, or one past the others when a call + leaves it out. One create showed the first and another the second, which the create merge took as a disagreement. + +The same recordings and the 130 earlier captures showed two more update gaps: + +- **Whole-object PUTs.** Paperless clears a tag's, correspondent's or document type's `owner` when a `PUT` leaves it + out, and Homebox an item's `assetId`, so those updates were `unexplained-change`. +- **Update clocks read at another precision.** Vikunja answers a task update with `done_at` to the nanosecond while its + reads spell it to the second, so the read after the update contradicted it. + +The round also found two creates of EspoCRM contacts in one second, which a list sorted newest first shows in an order +the application breaks ties in. The skill now spaces creates. + +## Behaviour + +- **Update members.** A later update narrows the plan's required members to those every call sent. +- **Numbered unless sent.** A member one create sent and stored as sent, and another left out and the application + numbered one past the others, is a `sequence` with `sent`, in either order: serving stores the value a call sends, + else the next number, and a later call that sends it must have stored what it sent. The next number follows the + largest value any call took, a deleted entity's included, in replay as in serving. +- **Cleared by a PUT.** A field a `PUT` left out that the application cleared to an empty value (`""`, `null`, `false`, + `[]` or `{}`) is that member's default (`body.defaults`) when every call that left it out stored that value, as its + answer shows it or, where the answer leaves the member out, the read after it: serving stores it for an update that + leaves the member out. Calls that left it out and stored it otherwise withhold the plan. +- **Update clocks.** The state holds an update's clock as the first read of the entity after it spells the same instant + at a coarser precision in the same zone and separator, as it already does for nested clocks: replay and serving store + every update's clocks in the spelling of the read the plan keeps as `observation.read`, so a later update whose read + spells them otherwise contradicts the plan, while answers keep the update's own. A create's clocks stay as answered, + as serving makes them. +- **Skill.** Recordings leave more than a second between two creates, as between a create and its first edit. + +Plans and views that need none of this are unchanged. + +## Not covered + +- Vikunja numbers tasks per project. Creates into projects the recording did not list cannot be numbered, so its task + creates stay withheld. +- Linkding renumbers bundles' `order` after a delete, which no rule models. +- Homebox item creates expand sent tag ids into tag objects and pin `parentId`; they stay withheld. + +## Acceptance + +- No write tool of the recorded captures goes from served to withheld. +- The writes the diagnosis named are served where their recordings show nothing else against them. diff --git a/docs/specs/agent-environments/write-varied-calls/tasks.md b/docs/specs/agent-environments/write-varied-calls/tasks.md new file mode 100644 index 0000000000..446d670a0d --- /dev/null +++ b/docs/specs/agent-environments/write-varied-calls/tasks.md @@ -0,0 +1,11 @@ +# Tasks + +- [x] Narrow an update plan's required members to those every call sent. +- [x] Serve a member the value a call sends, else the next number, when calls showed both. +- [x] Learn and serve the value a `PUT` stores for a member it leaves out. +- [x] Hold an update's clock as the read after it spells it. +- [x] Ask recordings to space creates. +- [x] Add view, serving and unit tests. +- [x] Document the rules. +- [x] Measure the recorded captures. +- [x] Run static, formatting, documentation and test gates. diff --git a/docs/specs/agent-environments/write-varied-calls/verification.md b/docs/specs/agent-environments/write-varied-calls/verification.md new file mode 100644 index 0000000000..506b581e1f --- /dev/null +++ b/docs/specs/agent-environments/write-varied-calls/verification.md @@ -0,0 +1,95 @@ +# Verification + +Executed on macOS with Node v24.22.1 on this branch, on #7332 at `7d01a5d5` (main `5aa23c3d`), and on Ubuntu 24.04 VMs +with snap Chromium for the recording round. + +## Diagnosis + +Claude Code (`claude-sonnet-5-5`) built an environment of each of seven applications from the same prompt, once with +REA main `5aa23c3d` (skill 50) and once with #7332 at `7d01a5d5` (skill 53). Each bundle's write tasks ran with +`fireworks_ai/gpt-oss-120b` and `fireworks_ai/glm-5p3-flash`, which scored alike: + +| Application | Write tasks | Main | #7332 | +| ----------- | ----------- | ------- | ------- | +| Miniflux | 3 | 3/3 | 3/3 | +| Grafana | 2 | 2/2 | 2/2 | +| Vikunja | 3 + 2 | 0/3 0/2 | 1/3 2/2 | +| Paperless | 3 | 1/3 | 2/3 | +| Linkding | 2 | 0/2 | 2/2 | +| Homebox | 2 | 2/2 | 0/2 | +| EspoCRM | 3 | 1/3 | 0/3 | +| Total | 20 | 9/20 | 12/20 | + +Served writes were 14 and 25. Recorded again, Homebox and EspoCRM gave main 2/2 and 1/3 and #7332 2/2 and 3/3: the +first losses were the agents' runs, not REA. Built with both versions, none of the 18 recordings served fewer writes +with #7332 (44 and 53 in all). + +Replaying #7332's recordings with failing checks and refusals logged gave the causes this branch takes: EspoCRM's +second contact update left out `addressCity` and was refused for lacking a required member (both recordings); Linkding's +second bundle create left `order` to the application, which the merge took as disagreeing with the first create's +echo; Vikunja's task update stamped `done_at` to the nanosecond where the read after it showed seconds. Over the +earlier captures, 14 withheld updates were `PUT`s whose application cleared a member they left out (Paperless `owner`, +Homebox `assetId`). + +## Gates + +- `npm run -s check:fast`, `format:check`, `scan:todos`, `docs:check`, `verify:test-discovery`, `mcp-catalog:check` and + `jscpd`: passed. `npx knip` reports `staticFileUrlPath`, `ObjcClassImportBindingCoverage` and + `AndroidManifestComponentInventory`, as main `5aa23c3d` does. +- `npx vitest run --project domain --project services`: 11,694 passed, 8 failed (`appleApplication.test.ts`), which fail + the same way on main `5aa23c3d`. +- `npm run -s test:focused --` the environment build, export and serve CLI boundaries, the analysis-view CLI and MCP + boundaries and the environment serve MCP boundary: 80 passed. `outputSchemaProfile`, `toolSchemaValidity`, + `mcpSchemaValidation`, `outputSchemaSemanticConstraints` and `analysisViewMcp`: 54 passed; the compact catalog stays + under its frame. +- The seven new view and serving tests fail on #7332 at `7d01a5d5` and pass here. + +## Write plans over recorded captures + +The 130 captures #7332 measured and the round's 18 recordings, built by #7332 at `7d01a5d5` and by this branch: 694 +plans, 386 served and 398. No write goes from served to withheld and every view is unchanged. Newly served: + +- Paperless correspondent, document type and tag updates in four recordings, whose `PUT` clears `owner`. +- EspoCRM's contact update and Linkding's bundle create in #7332's recordings of the round. +- Grafana's folder update and delete in a fresh recording whose second update sent fewer members. +- Homebox's item update in the published capture, whose `PUT` clears `assetId`. + +Vikunja's task update reads back confirmed, but stays withheld: the task creates before it are not modelled (per-project +numbers), so their entities carry the creates' answer spellings into later reads. + +## Review round + +- Greptile's first review: a `sequence` with `sent` now accepts a value a later create sent, whichever create came + first; a default must agree with every update that left its member out, else the plan is withheld; and serving + stores an update's clocks as the update plan's read-back spells them (`observation.read`). Their four tests fail on + `6300ec80`. #7332's made-object clock check, merged in, has its test there. +- Gates as above: `domain` + `services` 11,699 passed, 8 failed (main's `appleApplication.test.ts`); focused boundaries + 80 passed; the five MCP schema files 54 passed. The census stays at 694 plans and 398 served, with no plan or view + changing. + +Greptile's review after main `21b19270` was merged in (`467ac78f`) raised three more, fixed: + +- A member a call left out and the application stored a fixed value for (`body.defaults`) is no longer turned into a + `sequence` with `sent` by a later call that left it out and got the next number. +- Serving stores an update's clock at a coarser read's precision from the answer's own digits, so an answer to the + nanosecond read back to the microsecond keeps its microseconds. +- A `PUT` that names a `reference` field's entity by its member supplies that field: it is not checked against, or + given, the default a `PUT` that left the field out showed, whichever of the two calls came first. + +Their tests fail on `467ac78f`, and the reference case with the clearing call first on `7030ca4d`. Gates pass but +`knip`, whose one finding main `21b19270` reports too; `domain` + `services` 12,046 passed; focused boundaries 82 passed; the five MCP schema files 55 passed. Over the 148 captures, main +serves 380 writes and this branch 392, with no plan or view changing from `467ac78f`. + +Greptile's review of `b56f8924` raised two more on numbered members, fixed: a member a call sent empty (`""`) is no +longer filled in for a call that leaves it out when a `sequence` with `sent` numbers it; and a later create the +application numbered at or below a value an earlier create sent (9 sent and deleted, then 2) no longer turns the echo +into a numbered `sequence`, since serving counts past every value a call sent. Their tests fail on `b56f8924`; gates +pass but `knip` (main's finding), `domain` + `services` 12,048 passed, and the census is unchanged. Greptile's next +review found that check counting values sent in other collections, which withheld a create numbered within its own +collection; the check now counts them in the sequence's scope, and its test fails on `d4922b3d`. + +## Not verified + +- No recording round ran with this branch, so its write tasks are not measured. +- The skill's spacing of creates. +- Vikunja per-project task numbers, Linkding's renumbering after a delete and Homebox item creates, which stay withheld. diff --git a/docs/testing.md b/docs/testing.md index 57390a6564..4609628192 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1741,6 +1741,10 @@ The Apple artifact CLI/MCP lane uses one `macos-15` job to avoid adding demand for scarce macOS runner capacity. It runs the artifact-format and metadata verifiers, followed by one consolidated focused invocation containing all existing native and process-boundary regression paths. +The real native provenance verifier checks CLI/MCP agreement on complete +per-slice encryption absence. The encryption CLI regression builds a universal +image, marks its owned arm64 command with `cryptid=1`, and compares the reported +range with `otool`; this verifies declared metadata, not actual FairPlay data. The Linux source-test shards download a shared runtime and generated test metadata snapshot produced by the build job with `npm run test:prepare`. The diff --git a/docs/web-network-captures.md b/docs/web-network-captures.md index 2b75fedcb7..cc8f7d9aa8 100644 --- a/docs/web-network-captures.md +++ b/docs/web-network-captures.md @@ -740,7 +740,9 @@ one-element array member) that a later item read of a type first shows (served only with the path values its recorded call sent, unless it creates into the type's own collection, whose path has no parameters, and a `path` rule stores each of its path values), and otherwise `none` with reason `no-state-target`. A plan is built from the tool's first successful call: its -request body must be a usable JSON object (as response bodies are usable), and +request body must be a usable JSON object (as response bodies are usable), or +absent for an item action (a scenario request is absent when Playwright exposed +no body and it names no `Content-Type`, which the bodies browsers send carry), and the written entity must show in that call's response or in a later read before the next write. Each field of a created entity gets the first rule that explains it: generated `identity` (a sequence, UUID or token, by the recorded @@ -780,15 +782,19 @@ with the entity leaves out a field only later reads showed, which no initial entity holds, when the entity does not hold it, as Vikunja's reads of tasks it did not create show no `subscription`. A `reference` is an object holding fields of the -entity a sent member names, as the state holds it (a parent beside `parentId`): +entity a sent member names, as the state holds it (a parent beside `parentId`), +or a list of them, one per id a sent list names, in its order (`many`, tags +beside `tagIds`): the member is accepted with any value, serving looks the entity up and refuses a call naming one the environment does not hold or holds in more than one collection, and the field is absent, from the entity and the create's answer, when the call does not send the member. An id naming entities of one type in several collections teaches no reference. Each later create widens its plan when, explained on its own, it agrees with it: both calls show the same fields -but for a reference whose member a call without it did not send (one an earlier -create added stays), every field both rule they rule alike (an echo one showed +but for a reference whose member a call without it did not send, or sent as +`null` or `[]` and stored so (one an earlier create added stays, and a call may +send the member empty as that create did; one leaving it out stores no such field +when the create the plan observes showed none), every field both rule they rule alike (an echo one showed sorted stays sorted, unless a call stored its strings out of order as sent), and a field neither rules holds one value, unless the later call's rule for it spells the earlier calls' value too: a `composed` or `template` string, or a @@ -798,7 +804,14 @@ clocks and own members, and a count only the later call shows starts at the first object's value; a later create's made object must hold clocks its own request stamped, in the rule's format. Members either call sent are accepted, only those every call sent are required, and an echoed member a call left out takes the value that call stored -(`body.defaults`). A member a create sent empty (`""`, `null`, `false`, `[]` or +(`body.defaults`). A member one call sent and stored as sent, and another left +out and the application numbered one past the others, takes the value a call +sends, else that number (a `sequence` with `sent`), whichever call came first, +unless a call that left it out stored a fixed value (its `body.defaults`) or +the number a later call was given is not above every value an earlier create +sent in the sequence's scope (the type, or the later call's collection); an empty value a call sent is not applied to a call that leaves it out; +a later call that sends it must have stored what it sent, and the next number +follows the largest value any call took, a deleted entity's included. A member a create sent empty (`""`, `null`, `false`, `[]` or `{}`), as a form sends a field left as it was, is not required: a call that leaves it out is applied as sending that value (`body.blank`), unless a call that left it out showed a default, which it then keeps. A call may send it as any @@ -861,13 +874,28 @@ is unchanged) must be a clock, a nested clock, an increment, a slug, a fresh `un value, a `stamp` of the acting user, a value `derived` from the one member it stores nowhere, a `reference` to the entity a sent member names (kept by a call that names the entity the field already repeats), a `composed` string whose parts read members it stored as sent or fields it left as they were and spell the value before the call too, or for -an item action a `set` of a scalar to the value it showed, or the plan is +an item action a `set` of a scalar to the value it showed (a `toggle` once a +later call flips back the boolean an earlier one set, when no call found it set +already), or, for a `PUT`, a +member it left out that the application cleared to an empty value, which calls +that leave it out then store (`body.defaults`) as long as every call that left +it out stored that value (a call naming a `reference` field's entity by its +member does not leave the field out), or the plan is `unexplained-change`; an increment, slug, stamp, derived -value or set must hold for every call of the tool, and an action is served -only once a call found what it sets already set. A write is served only +value, set or toggle must hold for every call of the tool, and an action is +served only once a call found what it sets already set, or flipped back what it +toggles. A write is served only with the query its first call sent, which serving applies to a call that -leaves a parameter out and the catalog advertises as required. Every member the first call sent is required, except by `PATCH` and -`POST` updates and members a create sent empty. +leaves a parameter out and the catalog advertises as required. Every member every call sent is required, except by `PATCH` and +`POST` updates and members a create sent empty. The state holds an update's +clock as the first read of the entity after it spells the same instant at a +coarser precision in the same zone and separator: replay and serving store every +update's clocks in the spelling of the read the plan keeps as +`observation.read`, so a later update whose read spells them otherwise +contradicts the plan, while answers keep the update's own; a create's clocks +stay as answered. A `PUT` default must agree with what every call that left the +member out stored, as its answer shows it or, where the answer leaves it out, +the read after the call. A read on a line `query` answers is checked against the query evaluated over the replayed state. It covers the collection only when every parameter sorts or @@ -900,7 +928,10 @@ contradicted it; otherwise its `reason` is the first of its build reason (`no-call`, `body-unusable`, `no-observation`, `unexplained-change`), `contradicted` (with `contradicted_by`), `side-effect`, `unconfirmed` and `incomplete-entities` (an update or delete that answers with the entity, when no initial entity holds every field its recorded answer shows but those only later reads showed, counting for an update the fields its rules write, so it could answer only for entities the environment created). An otherwise served plan whose learned rules generate clock values is withheld with `unknown-clock` when no global clock start is established; its learned rules and confirming reads remain retained. `checked_record_ordinals` lists the confirming reads, `insert` places created -entities (sorted by a field the collection's recorded order shows, else last), +entities (sorted by a field the collection's recorded order shows, else last; +a create a whole listing in collection order placed last is placed again by a +field a later such listing shows it sorted by and no earlier one ruled out, the +type's id field last), and `write_clock` is the end of the last timed record, to the next second, when all request completions are known; otherwise it is null. An update that writes the field its type's creates are sorted by moves its entity diff --git a/package.json b/package.json index 051fad65cd..177a2a62f8 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "typecheck": "node scripts/run-typescript.mjs --noEmit", "verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs", "verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs", + "verify:hopper:callers": "npm run build:cached && node scripts/verify-real-hopper-callers.mjs", "verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs", "verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs", "verify:hopper:fat64": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs --fat64", @@ -309,6 +310,7 @@ "verify:agentenv:environment": "npm run build:cached && node scripts/verify-agentenv-environment.mjs", "verify:web:http3": "npm run build:cached && node scripts/verify-http3-network-captures.mjs", "verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs", + "verify:bun:standalone": "npm run build:cached && node scripts/verify-bun-standalone.mjs", "verify:go:binary": "npm run build:cached && node scripts/verify-go-binary.mjs", "verify:il2cpp:metadata": "npm run build:cached && node scripts/verify-il2cpp-metadata.mjs", "verify:wasm:package": "npm run build:cached && node scripts/verify-packaged-wasm-artifact.mjs", diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index 19bce50582..6991b2c78a 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -107,6 +107,7 @@ export const SCOPES = { "tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts", "tests/boundary/cli/nativeSignatureCli.test.ts", "tests/boundary/cli/nativeMachoArm64_32Cli.test.ts", + "tests/boundary/cli/nativeMachoEncryptionCli.test.ts", "tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts", "src/process/capture/FilesystemSnapshot.test.ts", "tests/process-global/filesystemSnapshotFallbackCancellation.test.ts", diff --git a/scripts/fixtures/dotnet-bundle/BundleFixture.csproj b/scripts/fixtures/dotnet-bundle/BundleFixture.csproj new file mode 100644 index 0000000000..31194c6360 --- /dev/null +++ b/scripts/fixtures/dotnet-bundle/BundleFixture.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0 + enable + none + + + + $(MSBuildSDKsPath)/../Microsoft.NET.HostModel.dll + + + diff --git a/scripts/fixtures/dotnet-bundle/NuGet.Config b/scripts/fixtures/dotnet-bundle/NuGet.Config new file mode 100644 index 0000000000..9a611e303c --- /dev/null +++ b/scripts/fixtures/dotnet-bundle/NuGet.Config @@ -0,0 +1,2 @@ + + diff --git a/scripts/fixtures/dotnet-bundle/Program.cs b/scripts/fixtures/dotnet-bundle/Program.cs new file mode 100644 index 0000000000..2e7e5686a9 --- /dev/null +++ b/scripts/fixtures/dotnet-bundle/Program.cs @@ -0,0 +1,92 @@ +using System.Runtime.InteropServices; +using System.Buffers.Binary; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.NET.HostModel.AppHost; +using Microsoft.NET.HostModel.Bundle; + +// Trusted fixture generator only. Generated hosts are inspected, never executed. +if (args.Length is < 2 or > 3) + throw new ArgumentException("Expected apphost template, absent output directory, and optional windows/linux/osx target"); +var targetOS = (args.ElementAtOrDefault(2) ?? "windows") switch +{ + "windows" => OSPlatform.Windows, + "linux" => OSPlatform.Linux, + "osx" => OSPlatform.OSX, + _ => throw new ArgumentException("Target OS must be windows, linux or osx"), +}; +var hostName = targetOS == OSPlatform.Windows ? "Probe.exe" : "Probe"; +var template = Path.GetFullPath(args[0]); +var output = Path.GetFullPath(args[1]); +if (Directory.Exists(output)) throw new IOException("Output directory must be absent"); +Directory.CreateDirectory(output); +var assembly = typeof(Program).Assembly.Location; +var resource = Path.Combine(output, "resource.txt"); +File.WriteAllText(resource, string.Concat(Enumerable.Repeat("owned compressed fixture content\n", 4096))); +foreach (var (version, compress) in new[] { (new Version(3, 1), false), (new Version(5, 0), false), (new Version(8, 0), false), (new Version(8, 0), true) }) +{ + var label = $"net{version.Major}-{(compress ? "compressed" : "plain")}"; + var directory = Path.Combine(output, label); + Directory.CreateDirectory(directory); + var host = Path.Combine(directory, targetOS == OSPlatform.Windows ? "template.exe" : "template"); + HostWriter.CreateAppHost(template, host, "BundleFixture.dll"); + var bundler = new Bundler(hostName, directory, + BundleOptions.BundleOtherFiles | (compress ? BundleOptions.EnableCompression : BundleOptions.None), + targetOS, Architecture.X64, version, appAssemblyName: "BundleFixture"); + var inputs = new Dictionary + { + ["BundleFixture.dll"] = assembly, + ["BundleFixture.deps.json"] = Path.ChangeExtension(assembly, ".deps.json"), + ["BundleFixture.runtimeconfig.json"] = Path.ChangeExtension(assembly, ".runtimeconfig.json"), + ["data/resource.txt"] = resource, + }; + var generated = bundler.GenerateBundle(new[] { + new FileSpec(host, hostName), + new FileSpec(assembly, "BundleFixture.dll"), + new FileSpec(Path.ChangeExtension(assembly, ".deps.json"), "BundleFixture.deps.json"), + new FileSpec(Path.ChangeExtension(assembly, ".runtimeconfig.json"), "BundleFixture.runtimeconfig.json"), + new FileSpec(resource, "data/resource.txt"), + }); + File.WriteAllText(Path.Combine(directory, "oracle.json"), JsonSerializer.Serialize(Observe(generated, inputs), new JsonSerializerOptions { WriteIndented = true })); + Console.WriteLine(generated); +} + +// Independent positive-fixture oracle: .NET's BinaryReader owns its string/number framing. +static object Observe(string path, Dictionary inputs) +{ + var bytes = File.ReadAllBytes(path); + var signature = Convert.FromHexString("8b1202b96a612038727b930214d7a03213f5b9e6efae3318ee3b2dce24b36aae"); + var marker = bytes.AsSpan().IndexOf(signature); + if (marker < 8) throw new InvalidDataException("Generated host has no bundle marker"); + var header = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(marker - 8, 8)); + using var stream = new MemoryStream(bytes); + stream.Position = header; + using var reader = new BinaryReader(stream); + var major = reader.ReadUInt32(); + var minor = reader.ReadUInt32(); + var count = reader.ReadInt32(); + var bundleId = reader.ReadString(); + if (major >= 2) stream.Position += 40; + var rows = new List(); + var found = new HashSet(); + for (var index = 0; index < count; index++) + { + var offset = reader.ReadInt64(); + var size = reader.ReadInt64(); + var compressed = major == 6 ? reader.ReadInt64() : 0; + var kind = reader.ReadByte(); + var name = reader.ReadString(); + if (!inputs.TryGetValue(name, out var input) || !found.Add(name)) + throw new InvalidDataException("Unexpected generated member: " + name); + using var stored = new MemoryStream(bytes, checked((int)offset), checked((int)(compressed == 0 ? size : compressed))); + using var expanded = compressed == 0 ? null : new DeflateStream(stored, CompressionMode.Decompress); + var digest = Convert.ToHexString(SHA256.HashData((Stream)expanded ?? stored)).ToLowerInvariant(); + var expected = File.ReadAllBytes(input); + if (expected.LongLength != size || digest != Convert.ToHexString(SHA256.HashData(expected)).ToLowerInvariant()) + throw new InvalidDataException("Generated member bytes differ: " + name); + rows.Add(new { name, offset, size, compressed_size = compressed, file_type = kind, sha256 = digest }); + } + if (!found.SetEquals(inputs.Keys)) throw new InvalidDataException("Generated bundle omitted an input"); + return new { source = "Microsoft.NET.HostModel + independent BinaryReader/DeflateStream", host = path, major, minor, header_offset = header, bundle_id = bundleId, sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), files = rows }; +} diff --git a/scripts/fixtures/hopper-tail-call.c b/scripts/fixtures/hopper-tail-call.c new file mode 100644 index 0000000000..1daaa760d3 --- /dev/null +++ b/scripts/fixtures/hopper-tail-call.c @@ -0,0 +1,5 @@ +#include +__attribute__((noinline)) int target_fn(int x) { return x * 5; } +__attribute__((noinline)) int call_through(int (*fp)(int), int x) { return fp(x); } +__attribute__((noinline)) int call_direct(int x) { return target_fn(x); } +int main(void) { printf("%d %d\n", call_through(target_fn, 3), call_direct(4)); return 0; } diff --git a/scripts/lib/development-tests.mjs b/scripts/lib/development-tests.mjs index dacd818201..84d094043b 100644 --- a/scripts/lib/development-tests.mjs +++ b/scripts/lib/development-tests.mjs @@ -38,6 +38,11 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => { // Test-only outputs are declared beside the test planner rather than becoming // runtime or typecheck prerequisites. Add a consumer here when it reads one. const artifactConsumers = { + "artifacts:skills": [ + "tests/boundary/cli/guidanceCli.test.ts", + "tests/boundary/guidanceSearch.test.ts", + "tests/boundary/mcp/registerGuidanceTools.test.ts", + ], "artifacts:mcp-catalog": [ "tests/boundary/mcp/deferredRuntime.test.ts", "tests/boundary/mcp/contractPresentation.test.ts", diff --git a/scripts/lib/import-specifiers.d.mts b/scripts/lib/import-specifiers.d.mts new file mode 100644 index 0000000000..ae29fe371b --- /dev/null +++ b/scripts/lib/import-specifiers.d.mts @@ -0,0 +1,18 @@ +/** One static import or re-export specifier and its source line. */ +export interface ImportSpecifierFact { + readonly specifier: string; + readonly line: number; +} + +/** Static string specifier on one import or re-export node, when it has one. */ +export function importSpecifier(node: { + readonly type: string; +}): string | undefined; + +/** + * Static import and re-export specifiers in source order. + * Comments and string literals are not imports. + */ +export function readImportSpecifiers( + source: string, +): readonly ImportSpecifierFact[]; diff --git a/scripts/lib/import-specifiers.mjs b/scripts/lib/import-specifiers.mjs new file mode 100644 index 0000000000..776b0440da --- /dev/null +++ b/scripts/lib/import-specifiers.mjs @@ -0,0 +1,43 @@ +import { parse } from "@babel/parser"; +import { traverseFast } from "@babel/types"; + +const literalValue = (node) => { + if (node?.type === "StringLiteral") return node.value; + if (node?.type === "TemplateLiteral" && node.expressions.length === 0) + return node.quasis[0]?.value.cooked ?? undefined; + return undefined; +}; + +/** Static string specifier on one import or re-export node, when it has one. */ +export const importSpecifier = (node) => { + switch (node.type) { + case "ImportDeclaration": + case "ExportNamedDeclaration": + case "ExportAllDeclaration": + case "ImportExpression": + case "TSImportType": + return literalValue(node.source); + case "TSImportEqualsDeclaration": + return node.moduleReference.type === "TSExternalModuleReference" + ? literalValue(node.moduleReference.expression) + : undefined; + default: + return undefined; + } +}; + +/** + * Static import and re-export specifiers in source order. + * Comments and string literals are not imports. Non-string dynamic imports + * are omitted. Callers decide which specifiers are in-repo edges. + */ +export const readImportSpecifiers = (source) => { + const specifiers = []; + const tree = parse(source, { sourceType: "module", plugins: ["typescript"] }); + traverseFast(tree, (node) => { + const specifier = importSpecifier(node); + if (specifier === undefined) return; + specifiers.push({ specifier, line: node.loc?.start.line ?? 1 }); + }); + return specifiers; +}; diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index 315362873b..a5dbc26615 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -1,8 +1,8 @@ -import { parse } from "@babel/parser"; -import { traverseFast } from "@babel/types"; import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { readImportSpecifiers } from "./import-specifiers.mjs"; + const PURE_LAYERS = new Set(["domain", "contracts"]); const MIGRATED_PROVIDER_ROOTS = new Set([ "sqlite", @@ -27,30 +27,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([ const repositoryPath = (root, path) => relative(root, path).replaceAll("\\", "/"); -const literalValue = (node) => { - if (node?.type === "StringLiteral") return node.value; - if (node?.type === "TemplateLiteral" && node.expressions.length === 0) - return node.quasis[0]?.value.cooked ?? undefined; - return undefined; -}; - -const importSpecifier = (node) => { - switch (node.type) { - case "ImportDeclaration": - case "ExportNamedDeclaration": - case "ExportAllDeclaration": - case "ImportExpression": - case "TSImportType": - return literalValue(node.source); - case "TSImportEqualsDeclaration": - return node.moduleReference.type === "TSExternalModuleReference" - ? literalValue(node.moduleReference.expression) - : undefined; - default: - return undefined; - } -}; - const sourceTarget = (file, specifier) => { if (specifier.startsWith("file:")) return fileURLToPath(specifier); if (isAbsolute(specifier)) return resolve(specifier); @@ -115,26 +91,14 @@ const failedBoundary = (file, target) => { export const inspectModuleBoundaries = (file, source, root) => { const absoluteFile = resolve(root, file); const importer = repositoryPath(root, absoluteFile); - const tree = parse(source, { - sourceType: "module", - plugins: ["typescript"], - }); const violations = []; - traverseFast(tree, (node) => { - const specifier = importSpecifier(node); - if (specifier === undefined) return; + for (const { specifier, line } of readImportSpecifiers(source)) { const resolved = sourceTarget(absoluteFile, specifier); - if (resolved === undefined) return; + if (resolved === undefined) continue; const target = repositoryPath(root, resolved); const boundary = failedBoundary(importer, target); if (boundary !== undefined) - violations.push({ - file: importer, - line: node.loc?.start.line ?? 1, - specifier, - target, - boundary, - }); - }); + violations.push({ file: importer, line, specifier, target, boundary }); + } return violations; }; diff --git a/scripts/rea.mjs b/scripts/rea.mjs index 71006980a7..57e2b02485 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -128,7 +128,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { ); const configurationError = cliConfigurationError(process.env); let dispatchFailure; - let closeCliJavaScriptResources; + let closeCliResources; // Incur can exit from the callback below before this function returns. // That callback owns the status, including a closed-pipe exit of 141. let exiting = false; @@ -150,7 +150,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { writeStdout(versionOutput); } else { const cliRuntime = await import("../dist/cli-runtime/cli.js"); - closeCliJavaScriptResources = cliRuntime.closeCliJavaScriptResources; + closeCliResources = cliRuntime.closeCliResources; const { createCli } = cliRuntime; const { createCliResultOutput } = await import("../dist/cli-runtime/cliResultOutput.js"); @@ -176,10 +176,10 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { void (async () => { let status = code; try { - await closeCliJavaScriptResources(); + await closeCliResources(); } catch (cleanup) { process.stderr.write( - `Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`, + `Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`, ); if (status === 0) status = 1; } @@ -225,7 +225,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { // Serve and output rendering can throw after a worker is parked. Stop it // on that path too, and keep the dispatch failure when both fail. try { - await closeCliJavaScriptResources?.(); + await closeCliResources?.(); } catch (cleanup) { // The exit callback reports this same shared failure and then exits. if (!exiting) { diff --git a/scripts/run-development-tests.mjs b/scripts/run-development-tests.mjs index 8237f8ad58..499f73c479 100644 --- a/scripts/run-development-tests.mjs +++ b/scripts/run-development-tests.mjs @@ -61,8 +61,10 @@ try { process.execPath, resolve("scripts/run-turbo.mjs"), "run", - ...(plan.needsBuild ? ["artifacts:skills"] : []), - ...plan.artifactTasks, + ...new Set([ + ...(plan.needsBuild ? ["artifacts:skills"] : []), + ...plan.artifactTasks, + ]), ]); if (prepared.code !== 0) process.exitCode = prepared.code; } diff --git a/scripts/verify-bun-standalone.mjs b/scripts/verify-bun-standalone.mjs new file mode 100644 index 0000000000..19d8e0024c --- /dev/null +++ b/scripts/verify-bun-standalone.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js"; +import { parseEvidence } from "../dist/domain/evidence.js"; +import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; +import { createVerifierCommands } from "./lib/verify-commands.mjs"; + +const root = await PrivateRuntimeRoot.create({ prefix: "rea-bun-verifier-" }); +const commands = await createVerifierCommands(); +const run = (command, args, options = {}) => + commands.exec(command, args, { + timeout: 120_000, + maxBuffer: 16 * 1024 * 1024, + ...options, + }); +// The root holds the compiled target and child temporary files. Release it +// only after every owned command family has settled. +let commandsClosed = false; +await withVerifierCleanup(async () => { + const source = join(root.path, "hello.ts"); + await writeFile(source, 'console.log("REA_BUN_VERIFICATION");\n'); + const docker = process.argv.includes("--docker"); + const binary = join(root.path, "hello"); + const compiler = docker + ? "oven/bun:1.4.0" + : (await run("bun", ["--version"])).stdout.trim(); + if (docker) { + await run("docker", [ + "run", + "--rm", + "--network=none", + "-v", + `${root.path}:/work`, + "-w", + "/work", + "oven/bun:1.4.0", + "build", + "--compile", + "hello.ts", + "--outfile", + "hello", + ]); + } else { + await run("bun", ["build", "--compile", source, "--outfile", binary]); + } + const cli = async (command) => + JSON.parse( + ( + await run( + process.execPath, + ["scripts/rea.mjs", command, binary, "--json"], + { + env: { + ...process.env, + TMPDIR: root.path, + TMP: root.path, + TEMP: root.path, + }, + }, + ) + ).stdout, + ); + const inspected = parseEvidence( + await cli("inspect-artifact"), + ).normalized_result; + const graph = inspected.substeps.find( + (step) => step.operation === "inventory_artifact", + )?.evidence.normalized_result; + assert.equal(graph.manifest.root_format, "bun-standalone"); + const module = graph.occurrences.find( + (row) => + row.format_metadata?.kind === "bun-standalone-entry" && + row.format_metadata.region === "contents" && + row.format_metadata.entry_point, + ); + assert.ok(module); + const extracted = parseEvidence( + await cli("extract-artifact"), + ).normalized_result; + assert.equal(extracted.containment_verified, true); + assert.match( + await readFile(join(extracted.output_root, module.logical_path), "utf8"), + /REA_BUN_VERIFICATION/u, + ); + console.log( + JSON.stringify( + { + compiler, + host: `${process.platform}-${process.arch}`, + docker, + root_format: graph.manifest.root_format, + entry_point: module.logical_path, + sha256: graph.nodes.find( + (node) => node.artifact_id === module.artifact_id, + )?.sha256, + extraction_verified: true, + target_executed: false, + }, + null, + 2, + ), + ); +}, [ + async () => { + await commands.close(); + commandsClosed = true; + }, + async () => { + if (commandsClosed) await root.close(); + }, +]); diff --git a/scripts/verify-real-hopper-callers.mjs b/scripts/verify-real-hopper-callers.mjs new file mode 100644 index 0000000000..7407114e6c --- /dev/null +++ b/scripts/verify-real-hopper-callers.mjs @@ -0,0 +1,309 @@ +// Run from the repository root after `npm run build:cached` on macOS arm64, +// with Hopper and the Xcode command-line tools installed. No target is executed. +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { constants } from "node:fs"; +import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { artifactMcpCall } from "./lib/artifact-e2e.mjs"; +import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs"; +import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; + +assert.equal(process.platform, "darwin", "requires macOS"); +assert.equal( + process.arch, + "arm64", + "requires macOS arm64 (fixture tail branch is ARM64)", +); +const root = process.cwd(); +const require = createRequire(join(root, "package.json")); +const { Client } = await import( + pathToFileURL(require.resolve("@modelcontextprotocol/client")) +); +const { StdioClientTransport } = await import( + pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio")) +); +const { parseConfig } = await import( + pathToFileURL(join(root, "dist/config/parseConfig.js")) +); +const config = parseConfig(process.env); +if (!config.ok) throw config.error; +await access(config.value.hopperLauncherPath, constants.X_OK); +const run = promisify(execFile); +const clang = (await run("xcrun", ["--find", "clang"])).stdout.trim(); +const sdk = ( + await run("xcrun", ["--sdk", "macosx", "--show-sdk-path"]) +).stdout.trim(); +const directory = await mkdtemp(join("/tmp", "rea-hopper-callers-verifier-")); +const fixture = join(directory, "tail-call"); +const large = join(directory, "long-scan"); +const source = fileURLToPath( + new URL("./fixtures/hopper-tail-call.c", import.meta.url), +); +const digest = async (path) => + createHash("sha256") + .update(await readFile(path)) + .digest("hex"); +const sessionsBefore = await snapshotHopperRuntime( + "/tmp", + HOPPER_TARGET_LEASE_DIRECTORY, +); +const ownedProcessIds = new Set(); +const client = new Client({ name: "hopper-callers-verifier", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: ["dist/main.js"], + cwd: root, + env: { + ...process.env, + REA_ANALYSIS_PROVIDER: "hopper", + REA_LOG_LEVEL: "silent", + REA_HOPPER_CALLER_SCAN_BUDGET_MS: "5000", + }, + stderr: "pipe", +}); +let stderr = ""; +transport.stderr?.on("data", (chunk) => { + stderr += chunk.toString(); +}); +const call = async (name, args, options = {}) => { + process.stderr.write(`calling ${name}\n`); + const result = await artifactMcpCall( + client, + { name, arguments: args }, + { timeout: 120000, ...options }, + ); + assert.notEqual(result.isError, true, `${name}: ${JSON.stringify(result)}`); + return ( + result.structuredContent ?? + JSON.parse(result.content.find((x) => x.type === "text").text) + ); +}; +const address = (result) => { + const match = result.normalized_result.match(/^(0x[0-9a-f]+):/mu); + assert(match, "Hopper assembly has no instruction address"); + return match[1]; +}; +const matchingRef = (ref, site, from, to) => + ref.source_address === site && + ref.source_procedure?.address === from && + ref.target_procedure?.address === to && + ref.call?.classification === "direct"; +try { + await run(clang, [ + "-isysroot", + sdk, + "-arch", + "arm64", + "-O1", + source, + "-o", + fixture, + ]); + const lines = ["#include "]; + for (let i = 0; i < 4000; i++) { + const id = String(i).padStart(4, "0"); + lines.push( + `__attribute__((noinline, used)) int rea_progress_${id}(void) { puts("REA_PROGRESS_${id}"); return ${i}; }`, + ); + } + lines.push("int main(void) { return rea_progress_0000(); }"); + const largeSource = join(directory, "long-scan.c"); + await writeFile(largeSource, lines.join("\n")); + await run(clang, [ + "-isysroot", + sdk, + "-arch", + "arm64", + "-O1", + largeSource, + "-o", + large, + ]); + const before = await digest(fixture), + beforeLarge = await digest(large); + await client.connect(transport); + if (transport.pid === null) + throw new Error("MCP process identity unavailable"); + ownedProcessIds.add(transport.pid); + let notifications = 0; + const original = transport.onmessage; + transport.onmessage = (message) => { + if (message.method === "notifications/progress") notifications++; + original?.(message); + }; + await call("open_binary", { path: fixture }); + const assembly = await call("procedure_assembly", { + procedure: "_call_direct", + }); + const branch = assembly.normalized_result.match( + /^(0x[0-9a-f]+):\s+b\s+_target_fn\s*$/mu, + ); + assert( + branch, + `compiler did not produce expected tail branch: ${assembly.normalized_result}`, + ); + const site = branch[1], + from = address(assembly); + const to = address( + await call("procedure_assembly", { procedure: "_target_fn" }), + ); + assert( + ( + await call("procedure_callees", { procedure: from }) + ).normalized_result.includes(to), + ); + const callers = await call("procedure_callers", { + procedure: to, + detail: "complete", + }); + assert(callers.normalized_result.includes(from), "tail caller missing"); + assert.equal(callers.relationship_coverage.status, "unknown"); + assert( + callers.raw_result.forward_observations.callee_inventory_callers.some( + (x) => x.address === from, + ), + "forward caller provenance missing", + ); + const refs = ( + await call("procedure_references", { procedure: to, direction: "incoming" }) + ).normalized_result.references; + assert( + refs.some((x) => matchingRef(x, site, from, to)), + "exact incoming tail reference missing", + ); + const dossier = ( + await call("analyze_function", { procedure: to, detail: "complete" }) + ).normalized_result; + assert( + dossier.callers.some((x) => x.address === from), + "dossier caller missing", + ); + assert( + dossier.incoming_references.some((x) => matchingRef(x, site, from, to)), + "dossier reference missing", + ); + const graph = ( + await call("get_call_graph", { + address: to, + direction: "backward", + detail: "complete", + }) + ).normalized_result.graph; + assert( + Object.values(graph) + .flat() + .some((x) => x.address === to && x.calls.includes(from)), + "backward graph edge missing", + ); + const path = ( + await call("trace_call_path", { + start: to, + goal: from, + direction: "backward", + }) + ).normalized_result; + assert.equal(path.goal_status, "reached"); + assert( + path.edges.some( + (x) => x.source_address === from && x.target_address === to, + ), + ); + await call("close_binary", {}); + assert.equal(await digest(fixture), before); + console.log( + JSON.stringify({ + event: "tail_call_verified", + from, + site, + to, + sha256: before, + }), + ); + await call("open_binary", { path: large }); + await call("procedure_assembly", { procedure: "_rea_progress_0000" }); + const query = { + address: "_rea_progress_0000", + direction: "backward", + detail: "complete", + }; + const count = notifications; + await call("get_call_graph", query); + assert.equal(notifications, count, "unsolicited progress without token"); + const progress = []; + await call("get_call_graph", query, { onprogress: (x) => progress.push(x) }); + assert( + progress.every((x, i) => i === 0 || x.progress > progress[i - 1].progress), + "progress not monotonic", + ); + assert( + progress.some((x) => + x.message?.includes("hopper_caller_reconciliation: Examined"), + ), + "missing examined-count progress", + ); + const controller = new AbortController(); + let partial = false; + let resolved = false; + try { + await artifactMcpCall( + client, + { name: "get_call_graph", arguments: query }, + { + timeout: 120000, + signal: controller.signal, + onprogress: (x) => { + const match = x.message?.match( + /hopper_caller_reconciliation: Examined (\d+) of (\d+) procedures/u, + ); + if (match && Number(match[1]) < Number(match[2])) { + partial = true; + controller.abort("cancel during real scan"); + } + }, + }, + ); + resolved = true; + } catch (error) { + if (!controller.signal.aborted) throw error; + } + assert( + partial && !resolved, + "did not observe cancellation after partial scan progress; cancellation coverage unavailable on this host", + ); + await client.ping(); + const retry = await call("get_call_graph", query); + assert(retry.normalized_result.graph, "retry did not return graph"); + await call("close_binary", {}); + assert.equal(await digest(large), beforeLarge); + console.log( + JSON.stringify({ + event: "progress_cancel_retry_verified", + progress, + sha256: beforeLarge, + }), + ); +} finally { + await client.close(); + await transport.close(); + await new Promise((resolve) => setTimeout(resolve, 500)); + const leaked = [ + ...(await snapshotHopperRuntime( + "/tmp", + HOPPER_TARGET_LEASE_DIRECTORY, + ownedProcessIds, + )), + ].filter((path) => !sessionsBefore.has(path)); + if (leaked.length === 0) + await rm(directory, { recursive: true, force: true }); + if (stderr) process.stderr.write(stderr); + assert.deepEqual( + leaked, + [], + `Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`, + ); +} diff --git a/scripts/verify/apple/dispatch.mjs b/scripts/verify/apple/dispatch.mjs index 5f419e89ad..9d7c4a3054 100644 --- a/scripts/verify/apple/dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -10,10 +10,14 @@ import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceS import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js"; import { artifactCliEvidence, + artifactMcpCall, artifactMcpResult, withArtifactMcp, } from "../../lib/artifact-e2e.mjs"; import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js"; +import { analysisErrorProjectionSchema } from "../../../dist/contracts/errorSchemas.js"; +import { cliOperationFailurePayload } from "../../../dist/cliLogging.js"; +import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs"; if (process.platform !== "darwin") throw new Error( @@ -344,6 +348,74 @@ try { commands, target_sha256: cliResult.target_sha256, }); + for (const { section, size, reason } of [ + { + section: "__objc_classlist", + size: 12, + reason: "Misaligned Objective-C class-list size", + }, + { + section: "__swift5_types", + size: 6, + reason: "Misaligned Swift type section", + }, + ]) { + const bytes = Buffer.alloc(4096); + bytes.writeUInt32LE(0xfeedfacf); + bytes.writeUInt32LE(0x0100000c, 4); + bytes.writeUInt32LE(2, 12); + bytes.writeUInt32LE(1, 16); + bytes.writeUInt32LE(152, 20); + bytes.writeUInt32LE(0x19, 32); // LC_SEGMENT_64 + bytes.writeUInt32LE(152, 36); + bytes.write("__DATA", 40); + bytes.writeBigUInt64LE(0x100000000n, 56); + bytes.writeBigUInt64LE(4096n, 64); + bytes.writeBigUInt64LE(4096n, 80); + bytes.writeUInt32LE(7, 88); + bytes.writeUInt32LE(5, 92); + bytes.writeUInt32LE(1, 96); + bytes.write(section, 104); + bytes.write("__DATA", 120); + bytes.writeBigUInt64LE(0x100000200n, 136); + bytes.writeBigUInt64LE(BigInt(size), 144); + bytes.writeUInt32LE(512, 152); + const path = join(root, `malformed-${section}.macho`); + await writeFile(path, bytes); + const target = await parseBinaryTarget(path); + assert.ok( + target.ok, + "Malformed section fixture must pass target admission", + ); + let cliFailure; + try { + await artifactCliEvidence("inspect-native-dispatch-metadata", path); + assert.fail("Malformed metadata unexpectedly succeeded"); + } catch (cause) { + assert.equal(cause.code, 1); + cliFailure = analysisErrorProjectionSchema.parse( + cliOperationFailurePayload(JSON.parse(cause.stdout)), + ); + } + assert.equal(cliFailure.code, "unsupported_target"); + assert.equal(cliFailure.category, "unsupported_target"); + assert.deepEqual(cliFailure.details, { + operation: "inspect_native_dispatch_metadata", + path: target.value.path, + reason, + }); + await withArtifactMcp(path, async (client) => { + const failure = requireMcpToolError( + await artifactMcpCall(client, { + name: "inspect_native_dispatch_metadata", + arguments: {}, + }), + ); + assert.equal(failure.code, cliFailure.code); + assert.deepEqual(failure.details, cliFailure.details); + }); + results.push({ variant: `malformed-${section}`, reason }); + } for (const mode of LINK_MODES) { const objcPath = join(root, `fixture-${mode.mode}`); await exec("/usr/bin/xcrun", [ diff --git a/scripts/verify/apple/native-provenance.mjs b/scripts/verify/apple/native-provenance.mjs index a18a2d497f..524ed93e39 100644 --- a/scripts/verify/apple/native-provenance.mjs +++ b/scripts/verify/apple/native-provenance.mjs @@ -134,6 +134,15 @@ export async function verifyNativeProvenance(app, executable) { ]); if (operation === "inspect_macho") { + const encryption = cli.normalized_result.encryption; + assert.equal(encryption.exhaustive, true); + assert.ok(encryption.total > 0); + assert.equal(encryption.items.length, encryption.total); + for (const slice of encryption.items) { + assert.equal(slice.status, "absent"); + assert.deepEqual(slice.commands, []); + assert.equal(slice.range, null); + } const delayed = cli.normalized_result.load_commands.items.find( ({ fields }) => fields.name === "@rpath/libdelay.dylib (offset 28)", ); diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index 1aad503f91..07a51ce615 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -156,6 +156,9 @@ check( files, /`[^`\n]*\\0/, [ + // Bundle fingerprint: fixed packaged paths (no NUL/newline) and 64 hex + // digest characters per record, never guide contents. Preserve v1 locators. + "src/application/GuidanceAccess.ts", "src/application/javascript/ElectronBoundaryAnalysis.ts", "src/application/javascript/ElectronBoundaryGraphContext.ts", "src/application/javascript/JavaScriptArtifactGraphContext.ts", diff --git a/src/adb/AdbDeviceOperations.ts b/src/adb/AdbDeviceOperations.ts new file mode 100644 index 0000000000..87664314f3 --- /dev/null +++ b/src/adb/AdbDeviceOperations.ts @@ -0,0 +1,135 @@ +import { + createAnalysisExecution, + type AnalysisExecution, +} from "../application/AnalysisProvider.js"; +import { AnalysisProtocolError } from "../domain/analysisErrorCore.js"; +import { err, ok, type Result } from "../domain/result.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { adbArguments, runAdb, type AdbRunLimits } from "./AdbCommand.js"; +import type { AdbBinarySelection } from "./AdbConfiguration.js"; +import { + parseAdbDevicesOutput, + parseGetpropOutput, +} from "./AdbDeviceOutput.js"; +import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js"; +import type { + AdbClientResult, + AdbProviderIdentity, +} from "./AdbOperationResults.js"; + +const DEVICES_TIMEOUT_MS = 20_000; +const DEVICES_MAX_OUTPUT_BYTES = 1024 * 1024; +const SHELL_TIMEOUT_MS = 30_000; +const GETPROP_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; + +export interface AdbCommandContext { + readonly operation: AdbRequest["operation"]; + readonly request: AdbRequest; + readonly selection: AdbBinarySelection; + readonly client: AdbClientResult; + readonly provider: AdbProviderIdentity; + readonly signal: AbortSignal | undefined; + readonly shell: ( + arguments_: readonly string[], + limits: AdbRunLimits, + ) => ReturnType; +} + +export const inspectAdbClientOperation = ( + context: AdbCommandContext, +): Result => + ok( + createAnalysisExecution( + { client: context.client, contacted_adb_server: false }, + context.provider, + ), + ); + +export const listAdbDevicesOperation = async ( + context: AdbCommandContext, +): Promise> => { + const ran = await runAdb( + context.operation, + { device: "adb-devices" }, + context.selection, + ["devices", "-l"], + { + timeoutMs: DEVICES_TIMEOUT_MS, + maxOutputBytes: DEVICES_MAX_OUTPUT_BYTES, + volatileOutput: true, + }, + context.signal, + ); + if (!ran.ok) return ran; + const { devices, unparsedLines, daemonStarted } = parseAdbDevicesOutput( + ran.value.stdout, + ran.value.stderr, + ); + if (unparsedLines.total > 0) + return err( + new AnalysisProtocolError( + `adb devices reported an unrecognized line: ${unparsedLines.sample[0]!}`, + ), + ); + return ok( + createAnalysisExecution( + { + client: context.client, + devices, + may_have_started_adb_server: daemonStarted, + }, + context.provider, + { + limitations: [ + "Device kind is inferred from the emulator serial prefix and USB transport metadata, not from device attestation", + "Kind stays unknown for TCP transports unless the serial is an emulator port", + ], + }, + ), + ); +}; + +export const inspectAdbDeviceOperation = async ( + context: AdbCommandContext, +): Promise> => { + const { request } = context; + if (request.operation !== "inspect_adb_device") + return err( + new AnalysisProtocolError( + `adb device inspection does not accept ${request.operation}`, + ), + ); + const serial = request.input.serial; + const ran = await runAdb( + context.operation, + { device: serial }, + context.selection, + adbArguments(serial, ["shell", "getprop"]), + { + timeoutMs: SHELL_TIMEOUT_MS, + maxOutputBytes: GETPROP_MAX_OUTPUT_BYTES, + volatileOutput: true, + }, + context.signal, + ); + if (!ran.ok) return ran; + const { properties, emulatorObserved } = parseGetpropOutput(ran.value.stdout); + return ok( + createAnalysisExecution( + { + client: context.client, + serial, + properties, + property_count: properties.length, + emulator_observed: emulatorObserved, + }, + context.provider, + { + limitations: [ + "Properties are a fixed whitelist of build identity facts, not the full getprop dump", + "emulator_observed is null when the build does not report ro.kernel.qemu", + ], + }, + ), + ); +}; diff --git a/src/adb/AdbOperationResults.ts b/src/adb/AdbOperationResults.ts new file mode 100644 index 0000000000..5dd7002d02 --- /dev/null +++ b/src/adb/AdbOperationResults.ts @@ -0,0 +1,53 @@ +import { createAnalysisExecution } from "../application/AnalysisProvider.js"; +import type { AdbUnparsedLines } from "./AdbUnparsedLines.js"; +import type { + AdbBinarySelection, + AdbClientIdentity, +} from "./AdbConfiguration.js"; + +export const clientIdentityResult = ( + selection: AdbBinarySelection, + identity: AdbClientIdentity, +) => ({ + binary_path: selection.binary, + path_source: selection.pathSource, + version: identity.version, + installed_path: identity.installedPath, +}); + +export type AdbClientResult = ReturnType; +export type AdbProviderIdentity = { + id: string; + name: string; + version: string | null; +}; + +/** + * Build a parsed-listing execution whose `unparsed_lines`, coverage, and + * truncation limitation all derive from the parser's unrecognized-line facts, + * so the limitation can never describe a field the result does not carry. + */ +export const parsedListingExecution = ( + result: Readonly>, + unparsed: AdbUnparsedLines, + provider: AdbProviderIdentity, + limitations: readonly string[] = [], +) => + createAnalysisExecution( + { + ...result, + unparsed_lines: unparsed.sample, + coverage: unparsed.total > 0 ? "partial" : "complete", + }, + provider, + { + limitations: [ + ...limitations, + ...(unparsed.total > unparsed.sample.length + ? [ + `unparsed_lines reports the first ${String(unparsed.sample.length)} of ${String(unparsed.total)} unrecognized lines`, + ] + : []), + ], + }, + ); diff --git a/src/adb/AdbProvider.test.ts b/src/adb/AdbProvider.test.ts index 1bccde00bc..d81ade095a 100644 --- a/src/adb/AdbProvider.test.ts +++ b/src/adb/AdbProvider.test.ts @@ -38,6 +38,11 @@ import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js"; const it = test.skipIf(process.platform === "win32"); +const SCREEN_PNG = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC", + "base64", +); + const VERSION_BLOCK = "printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' \"$0\""; @@ -1340,6 +1345,36 @@ exit 1`, expect(closeAttempts).toBe(2); }); +it("verifies the device-side digest for a long device path", async () => { + const { binary, root } = await writeAdbStub( + () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi +if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi +if [ "$1" = "-s" ] && [ "$3" = "push" ]; then exit 0; fi +if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then printf '%s %s\\n' "8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043" "$5"; exit 0; fi +exit 1`, + ); + const localFile = join(root, "payload.bin"); + await writeFile(localFile, "push-payload"); + const digest = createHash("sha256").update("push-payload").digest("hex"); + const pushed = await evidenceResult(providerOver(binary), { + operation: "push_adb_file", + input: { + serial: "emulator-5554", + local_path: localFile, + // Each component stays within the usual 255-byte limit; the whole + // path still makes the digest line exceed the former 1 KiB budget. + device_path: `/data/local/tmp/${Array.from({ length: 5 }, (_, index) => + String(index).padEnd(220, "d"), + ).join("/")}/payload.bin`, + overwrite: false, + }, + }); + expect(pushed).toMatchObject({ + device_sha256: digest, + device_digest_source: "device_sha256sum", + }); +}); + it("reports a digest mismatch after push as a changed artifact", async () => { const { binary, root } = await writeAdbStub( () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi @@ -1453,19 +1488,20 @@ exit 1`, it("captures the screen as a digested PNG with its dimensions", async () => { const { binary, root } = await writeAdbStub( - () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi + (root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi if [ "$1" = "-s" ] && [ "$3" = "exec-out" ]; then - printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003' + cat "${join(root, "screen-fixture.png")}" exit 0 fi exit 1`, ); + await writeFile(join(root, "screen-fixture.png"), SCREEN_PNG); const output = join(root, "captures"); const result = await evidenceResult(providerOver(binary), { operation: "capture_adb_screen", input: { serial: "emulator-5554", output_directory: output }, }); - expect(result).toMatchObject({ width: 4, height: 3 }); + expect(result).toMatchObject({ width: 1, height: 1 }); expect( await readFile(join(output, "screen.png")).then( (bytes) => bytes.subarray(1, 4).toString("latin1") === "PNG", @@ -1476,13 +1512,14 @@ exit 1`, it("stops a screen capture aborted after adb returns but before publication", async () => { const { binary, root } = await writeAdbStub( - () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi + (root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi if [ "$1" = "-s" ] && [ "$3" = "exec-out" ]; then - printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003' + cat "${join(root, "screen-fixture.png")}" exit 0 fi exit 1`, ); + await writeFile(join(root, "screen-fixture.png"), SCREEN_PNG); const output = join(root, "cancelled-capture"); const controller = new AbortController(); adbTestHooks.afterScreencap = () => controller.abort(); diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts index 37eaac339c..904f78db07 100644 --- a/src/adb/AdbProvider.ts +++ b/src/adb/AdbProvider.ts @@ -66,6 +66,7 @@ import { } from "../domain/analysisErrorCore.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; +import { readPngHeaderDimensions } from "../domain/pngHeader.js"; import { err, ok, type Result } from "../domain/result.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { jsonObjectSchema } from "../domain/jsonValue.js"; @@ -111,8 +112,6 @@ import { import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { displayDevicePath, - parseAdbDevicesOutput, - parseGetpropOutput, parsePackageListOutput, parsePmPathOutput, } from "./AdbDeviceOutput.js"; @@ -132,7 +131,6 @@ import { parseFeatureListOutput, parsePackageChangeOutput, parseProcessListOutput, - parsePngDimensions, parseResolveActivityOutput, parseServiceListOutput, parseSettingsGetOutput, @@ -141,7 +139,18 @@ import { parseWmDensityOutput, parseWmSizeOutput, } from "./AdbShellOutput.js"; -import type { AdbUnparsedLines } from "./AdbUnparsedLines.js"; +import { + clientIdentityResult, + parsedListingExecution, + type AdbClientResult, + type AdbProviderIdentity, +} from "./AdbOperationResults.js"; +import { + inspectAdbClientOperation, + inspectAdbDeviceOperation, + listAdbDevicesOperation, + type AdbCommandContext, +} from "./AdbDeviceOperations.js"; /** Stable identity for the ADB device-acquisition provider. */ export const ADB_PROVIDER_IDENTITY = { @@ -150,10 +159,7 @@ export const ADB_PROVIDER_IDENTITY = { version: null, } as const; -const DEVICES_TIMEOUT_MS = 20_000; -const DEVICES_MAX_OUTPUT_BYTES = 1024 * 1024; const SHELL_TIMEOUT_MS = 30_000; -const GETPROP_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; const PACKAGE_LIST_TIMEOUT_MS = 60_000; const PACKAGE_LIST_MAX_OUTPUT_BYTES = 8 * 1024 * 1024; const PM_PATH_TIMEOUT_MS = 30_000; @@ -403,49 +409,6 @@ const syncStagedAdbFile = async ( if (primary !== undefined) throw primary; }; -const clientIdentityResult = ( - selection: AdbBinarySelection, - identity: AdbClientIdentity, -) => ({ - binary_path: selection.binary, - path_source: selection.pathSource, - version: identity.version, - installed_path: identity.installedPath, -}); - -type ClientIdentity = ReturnType; -type ProviderIdentity = { id: string; name: string; version: string | null }; - -/** - * Build a parsed-listing execution whose `unparsed_lines`, coverage, and - * truncation limitation all derive from the parser's unrecognized-line facts, - * so the limitation can never describe a field the result does not carry. - */ -const parsedListingExecution = ( - result: Readonly>, - unparsed: AdbUnparsedLines, - provider: ProviderIdentity, - limitations: readonly string[] = [], -) => - createAnalysisExecution( - { - ...result, - unparsed_lines: unparsed.sample, - coverage: unparsed.total > 0 ? "partial" : "complete", - }, - provider, - { - limitations: [ - ...limitations, - ...(unparsed.total > unparsed.sample.length - ? [ - `unparsed_lines reports the first ${String(unparsed.sample.length)} of ${String(unparsed.total)} unrecognized lines`, - ] - : []), - ], - }, - ); - /** Materialize the caller's output directory, refusing existing files inside. */ const prepareOutputDirectory = async ( operation: string, @@ -1376,7 +1339,7 @@ export class AdbProvider { ); } const client = clientIdentityResult(selection, identity); - const provider: ProviderIdentity = { + const provider: AdbProviderIdentity = { ...ADB_PROVIDER_IDENTITY, version: identity.version, }; @@ -1395,87 +1358,22 @@ export class AdbProvider { { ...limits, volatileOutput: true }, signal, ); + const context: AdbCommandContext = { + operation, + request, + selection, + client, + provider, + signal, + shell, + }; switch (operation) { case "inspect_adb_client": - return ok( - createAnalysisExecution( - { client, contacted_adb_server: false }, - provider, - ), - ); - case "list_adb_devices": { - const ran = await runAdb( - operation, - { device: "adb-devices" }, - selection, - ["devices", "-l"], - { - timeoutMs: DEVICES_TIMEOUT_MS, - maxOutputBytes: DEVICES_MAX_OUTPUT_BYTES, - volatileOutput: true, - }, - signal, - ); - if (!ran.ok) return ran; - const { devices, unparsedLines, daemonStarted } = parseAdbDevicesOutput( - ran.value.stdout, - ran.value.stderr, - ); - if (unparsedLines.total > 0) - return err( - new AnalysisProtocolError( - `adb devices reported an unrecognized line: ${unparsedLines.sample[0]!}`, - ), - ); - return ok( - createAnalysisExecution( - { client, devices, may_have_started_adb_server: daemonStarted }, - provider, - { - limitations: [ - "Device kind is inferred from the emulator serial prefix and USB transport metadata, not from device attestation", - "Kind stays unknown for TCP transports unless the serial is an emulator port", - ], - }, - ), - ); - } - case "inspect_adb_device": { - const ran = await runAdb( - operation, - { device: request.input.serial }, - selection, - adbArguments(request.input.serial, ["shell", "getprop"]), - { - timeoutMs: SHELL_TIMEOUT_MS, - maxOutputBytes: GETPROP_MAX_OUTPUT_BYTES, - volatileOutput: true, - }, - signal, - ); - if (!ran.ok) return ran; - const { properties, emulatorObserved } = parseGetpropOutput( - ran.value.stdout, - ); - return ok( - createAnalysisExecution( - { - client, - serial: request.input.serial, - properties, - property_count: properties.length, - emulator_observed: emulatorObserved, - }, - provider, - { - limitations: [ - "Properties are a fixed whitelist of build identity facts, not the full getprop dump", - "emulator_observed is null when the build does not report ro.kernel.qemu", - ], - }, - ), - ); - } + return inspectAdbClientOperation(context); + case "list_adb_devices": + return listAdbDevicesOperation(context); + case "inspect_adb_device": + return inspectAdbDeviceOperation(context); case "list_adb_packages": { const scopeFlag = request.input.scope === "third_party" @@ -1976,8 +1874,8 @@ export class AdbProvider { private async pullPackage( operation: "pull_adb_package", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly package: string; @@ -2373,8 +2271,8 @@ export class AdbProvider { private async pullFile( operation: "pull_adb_file", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly device_path: string; @@ -2470,8 +2368,8 @@ export class AdbProvider { private async pushFile( operation: "push_adb_file", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly local_path: string; @@ -2585,7 +2483,12 @@ export class AdbProvider { { device: input.serial, path: devicePath }, selection, adbArguments(input.serial, ["shell", "sha256sum", devicePath]), - { timeoutMs: SHELL_TIMEOUT_MS, maxOutputBytes: 1024 }, + { + timeoutMs: SHELL_TIMEOUT_MS, + // sha256sum echoes the path, escaping special characters, so a + // long path must not exhaust the budget for the digest line. + maxOutputBytes: 1024 + 2 * Buffer.byteLength(devicePath), + }, signal, ); if (!deviceDigest.ok) { @@ -2674,8 +2577,8 @@ export class AdbProvider { private async installPackage( operation: "install_adb_package", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly apk_path: string; @@ -2775,8 +2678,8 @@ export class AdbProvider { private async uninstallPackage( operation: "uninstall_adb_package", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly package: string }, signal: AbortSignal | undefined, ): Promise> { @@ -2842,8 +2745,8 @@ export class AdbProvider { private async startApp( operation: "start_adb_app", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly package: string }, signal: AbortSignal | undefined, ): Promise> { @@ -2898,8 +2801,8 @@ export class AdbProvider { private async startActivity( operation: "start_adb_activity", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly action: string; @@ -2960,8 +2863,8 @@ export class AdbProvider { private async runAmStart( operation: "start_adb_app" | "start_adb_activity", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, serial: string, arguments_: readonly string[], resultFields: Record, @@ -3050,8 +2953,8 @@ export class AdbProvider { private async stopApp( operation: "stop_adb_app", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly package: string }, signal: AbortSignal | undefined, ): Promise> { @@ -3084,8 +2987,8 @@ export class AdbProvider { private async captureScreen( operation: "capture_adb_screen", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly output_directory: string }, signal: AbortSignal | undefined, ): Promise> { @@ -3114,11 +3017,11 @@ export class AdbProvider { return err( new AnalysisCancelledError(operation, { cause: signal.reason }), ); - const dimensions = parsePngDimensions(captured.value.stdout); - if (dimensions.width === null) + const dimensions = readPngHeaderDimensions(captured.value.stdout); + if (dimensions === undefined) return err( new AnalysisProtocolError( - "screencap did not return a PNG payload; the device may not expose a display", + "screencap returned an invalid PNG header; the device may not expose a display", ), ); return this.#withRuntimeRoot( @@ -3182,6 +3085,7 @@ export class AdbProvider { { limitations: [ "The capture is one frame at read time; no recording or interaction occurred", + "PNG signature, IHDR framing, checksum, and dimensions are validated; encoding and image contents are not validated", ...(usedExclusiveFallback ? [ "The exclusive-create fallback makes the output path visible while its bytes are being written", @@ -3209,8 +3113,8 @@ export class AdbProvider { private async collectBugreport( operation: "collect_adb_bugreport", selection: AdbBinarySelection, - client: ClientIdentity, - provider: ProviderIdentity, + client: AdbClientResult, + provider: AdbProviderIdentity, input: { readonly serial: string; readonly output_directory: string }, signal: AbortSignal | undefined, ): Promise> { diff --git a/src/adb/AdbShellOutput.ts b/src/adb/AdbShellOutput.ts index ed35d975a9..bca37332b2 100644 --- a/src/adb/AdbShellOutput.ts +++ b/src/adb/AdbShellOutput.ts @@ -609,26 +609,3 @@ export const parseAmStartOutput = ( (text === "" ? "am start returned no activity" : text.slice(0, 512)), }; }; - -const PNG_SIGNATURE = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); - -/** Read image dimensions from a PNG header, or null for other payloads. */ -export const parsePngDimensions = ( - bytes: Uint8Array, -): { - width: number | null; - height: number | null; -} => { - if ( - bytes.length < 24 || - !Buffer.from(bytes.subarray(0, 8)).equals(PNG_SIGNATURE) - ) - return { width: null, height: null }; - const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - return { - width: view.getUint32(16), - height: view.getUint32(20), - }; -}; diff --git a/src/application/AnalysisContextQueries.ts b/src/application/AnalysisContextQueries.ts index 436cea3ad3..769bd11f74 100644 --- a/src/application/AnalysisContextQueries.ts +++ b/src/application/AnalysisContextQueries.ts @@ -8,6 +8,7 @@ import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { analysisBookmarkSchema } from "../domain/native/nativeInventory.js"; import { parseContainingProcedureResolution } from "../domain/native/nativeObservation.js"; +import { nativeAddressCoordinateInputSchema } from "../domain/native/nativeAddress.js"; import { resolveAnalysisDocument } from "./AnalysisDocument.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -176,8 +177,34 @@ const matchingBookmarks = (value: JsonValue, address: string): Facet => { remediation: "Retry list_bookmarks directly and report the provider output shape if it remains malformed.", }; - return { - state: "available", - value: parsed.data.filter((bookmark) => bookmark.address === address), - }; + const target = nativeAddressCoordinateInputSchema.safeParse(address); + const matching = []; + for (const bookmark of parsed.data) { + if (bookmark.address === address) { + matching.push(bookmark); + continue; + } + const candidate = nativeAddressCoordinateInputSchema.safeParse( + bookmark.address, + ); + if ( + !target.success || + !candidate.success || + (target.data.offset === candidate.data.offset && + target.data.space !== candidate.data.space) + ) + return { + state: "unavailable", + reason: + "Bookmark address identity could not be compared with the selected address.", + remediation: + "Call list_bookmarks directly to inspect the provider addresses.", + }; + if ( + target.data.offset === candidate.data.offset && + target.data.space === candidate.data.space + ) + matching.push(bookmark); + } + return { state: "available", value: matching }; }; diff --git a/src/application/AnalysisProvider.ts b/src/application/AnalysisProvider.ts index caafeae4b0..ed926de452 100644 --- a/src/application/AnalysisProvider.ts +++ b/src/application/AnalysisProvider.ts @@ -1,3 +1,4 @@ +import type { AnalysisWorkScope } from "./AnalysisWorkScope.js"; import type { AnalysisObservation } from "../domain/analysisObservation.js"; import type { ProviderInspectionScope } from "./ProviderInspectionScope.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; @@ -26,6 +27,8 @@ import { } from "../domain/relationshipCoverage.js"; export interface ExecutionOptions { + /** Ephemeral state shared only by one composed workflow. */ + readonly workScope?: AnalysisWorkScope; readonly signal?: AbortSignal; readonly progress?: ProgressReporter; /** diff --git a/src/application/AnalysisWorkScope.ts b/src/application/AnalysisWorkScope.ts new file mode 100644 index 0000000000..40a200c188 --- /dev/null +++ b/src/application/AnalysisWorkScope.ts @@ -0,0 +1,18 @@ +/** In-memory, provider-owned intermediate facts confined to one composed task. */ +export class AnalysisWorkScope { + readonly #entries = new Map(); + #closed = false; + + get(owner: object): T | undefined { + return this.#entries.get(owner) as T | undefined; + } + + set(owner: object, value: T): void { + if (!this.#closed) this.#entries.set(owner, value); + } + + clear(): void { + this.#closed = true; + this.#entries.clear(); + } +} diff --git a/src/application/CapabilityInventory.test.ts b/src/application/CapabilityInventory.test.ts index 1af42fe6b5..d70e4e479f 100644 --- a/src/application/CapabilityInventory.test.ts +++ b/src/application/CapabilityInventory.test.ts @@ -1,3 +1,4 @@ +import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js"; import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js"; import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; import { describe, expect, it } from "vitest"; @@ -100,6 +101,77 @@ const entry = ( return found; }; +it("reports the active resource session's dispatch policy without limiting independent tools", () => { + const resourceStatus = { + ...status({ open: true }), + session_kind: "bundle_resources", + capabilities: [ + { + provider: { id: "bundle-resources", name: "Resources", version: "1" }, + shared_limitations: [], + groups: [ + { + operations: [ + "inspect_plist", + "decode_interface_builder", + "inspect_asset_catalog", + "inspect_keyed_archive", + ], + effects: [], + limitations: [], + available: true, + reason: null, + availability_code: null, + }, + ], + }, + ], + }; + const entries = buildCapabilityInventory(resourceStatus, enabledPolicy); + for (const name of [ + "inspect_artifact", + "extract_artifact", + "trace_dylib_resolution", + "get_navigation_context", + ]) + expect(entries.find((item) => item.name === name)).toMatchObject({ + available: false, + reason: "target_unsupported", + remediation: expect.stringContaining("open_binary"), + }); + for (const name of [ + "inspect_plist", + "decode_interface_builder", + "inspect_asset_catalog", + "inspect_keyed_archive", + "inspect_pe_imports", + "inspect_desktop_application", + "close_binary", + ]) + expect(entries.find((item) => item.name === name)).toMatchObject({ + available: true, + }); + expect( + entries.find((item) => item.name === "inspect_apk_signature"), + ).toMatchObject({ + available: true, + reason: "available", + remediation: null, + }); + const sqlite = sqliteDatabaseCapability(process.versions.node); + expect( + entries.find((item) => item.name === "inspect_sqlite_database"), + ).toMatchObject( + sqlite.available + ? { available: true } + : { + available: false, + reason: "provider_unavailable", + remediation: sqlite.reason, + }, + ); +}); + describe("ADB and Apktool host availability", () => { it("uses only their selected owner probes", () => { const ready = { diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index 649361ba62..193459d3ef 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -42,6 +42,7 @@ import type { JebAnalysisAvailability } from "./jeb/JebAnalysisPort.js"; import { FRIDA_TOOL_CONTRACTS } from "../contracts/fridaToolContracts.js"; import { TOOL_CONTRACTS } from "../contracts/toolContracts.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; +import { isBundleResourceOperation } from "./binary/BundleResourceSession.js"; const FRIDA_OPERATION_NAMES: ReadonlySet = new Set( FRIDA_TOOL_CONTRACTS.map(({ name }) => name), @@ -49,6 +50,7 @@ const FRIDA_OPERATION_NAMES: ReadonlySet = new Set( const statusSchema = z.object({ open: z.boolean(), + session_kind: z.literal("bundle_resources").optional(), kind: z.enum(["executable", "database", "archive", "artifact"]).optional(), format: z.string().optional(), capabilities: z @@ -128,6 +130,7 @@ type AvailabilityContext = { readonly name: string; readonly kind: ToolKind; readonly targetOpen: boolean; + readonly resourceSession: boolean; readonly requiresActiveTarget: boolean; readonly targetKind: | "executable" @@ -260,6 +263,7 @@ export const buildCapabilityInventory = ( name: contract.name, kind: contract.kind, targetOpen: status.open, + resourceSession: status.session_kind === "bundle_resources", requiresActiveTarget: "requiresActiveTarget" in contract && contract.requiresActiveTarget === true, @@ -380,6 +384,19 @@ export function compactToolAvailability( } const availabilityFor = (context: AvailabilityContext): Availability => { + const workflowAvailability = workflowAvailabilityFor(context); + if ( + context.targetOpen && + context.resourceSession && + context.requiresActiveTarget && + !isBundleResourceOperation(context.name) && + (workflowAvailability === null || context.kind === "session") + ) + return { + reason: "target_unsupported", + remediation: + "This resource-only session cannot execute this operation. Call open_binary with an executable analysis target.", + }; // These built-in workflows accept a caller-selected endpoint; they do not load the optional passive provider. if ( context.name === "inspect_byte_entropy" || @@ -403,7 +420,6 @@ const availabilityFor = (context: AvailabilityContext): Availability => { reason: "provider_unavailable", remediation: `Adapter ${failure.providerId} could not load: ${failure.reason}. Repair the adapter installation and restart REA; other capabilities remain available.`, }; - const workflowAvailability = workflowAvailabilityFor(context); if (workflowAvailability !== null) return workflowAvailability; const hostDecision = hostAvailability(context); if (hostDecision !== null) return hostDecision; @@ -453,6 +469,7 @@ const workflowAvailabilityFor = ({ "Run persistent debugger sessions on Linux with /proc descriptor readback.", ); if ( + name === "inspect_apk_signature" || name === "inspect_desktop_application" || name === "inspect_go_binary" || name === "inspect_il2cpp_metadata" || diff --git a/src/application/GuidanceAccess.boundaries.test.ts b/src/application/GuidanceAccess.boundaries.test.ts new file mode 100644 index 0000000000..2602c6986e --- /dev/null +++ b/src/application/GuidanceAccess.boundaries.test.ts @@ -0,0 +1,139 @@ +import { expect, it } from "vitest"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { readGuidanceSectionInputSchema } from "../contracts/guidanceContracts.js"; +import { MANAGED_SKILL_FILES } from "./SetupSkill.js"; +import { + readGuidanceSectionWithReader, + searchGuidanceWithReader, +} from "./GuidanceAccess.js"; + +const reader = (text: string) => async (path: string) => + path === MANAGED_SKILL_FILES[0] ? text : "# Other\n"; + +it("does not substitute a common domain word for an unknown query subject", async () => { + const resources = new ArtifactResourceScope(); + expect( + await searchGuidanceWithReader( + { query: "how do I inspect module frobnicate qzxv" }, + reader("# Modules\nInspect a module with retained facts.\n"), + resources, + ), + ).toMatchObject({ ok: true, value: { status: "no_matches" } }); + await resources.close(); +}); + +it.each([ + ["## C#", "C#"], + ["## C# ###", "C#"], + ["## C\\#", "C\\#"], + [" ## C#", "C#"], + ["## C# \t ### \t", "C#"], + ["## ###", ""], +])("preserves the source title in %s", async (line, heading) => { + const resources = new ArtifactResourceScope(); + const source = reader(`${line}\nneedle instructions\n`); + const result = await searchGuidanceWithReader( + { query: "needle" }, + source, + resources, + ); + if (!result.ok || result.value.status !== "matches") + throw new Error("Expected matching documentation"); + expect(result.value.matches[0]?.heading).toBe(heading); + const value = await readGuidanceSectionWithReader( + { locator: result.value.matches[0]?.locator }, + source, + resources, + ); + expect(value).toMatchObject({ + ok: true, + value: { status: "section", heading }, + }); + await resources.close(); +}); + +it("issues readable locators independently of heading length", async () => { + const resources = new ArtifactResourceScope(); + const heading = `needle ${"x".repeat(3000)}`; + const source = reader(`## ${heading}\nInstructions\n`); + const result = await searchGuidanceWithReader( + { query: "needle" }, + source, + resources, + ); + if (!result.ok || result.value.status !== "matches") + throw new Error("Expected match"); + const locator = result.value.matches[0]?.locator; + expect(readGuidanceSectionInputSchema.safeParse({ locator }).success).toBe( + true, + ); + expect( + await readGuidanceSectionWithReader({ locator }, source, resources), + ).toMatchObject({ + ok: true, + value: { status: "section", heading }, + }); + await resources.close(); +}); + +it.each([2048, 2049])( + "bounds the combined index across guides with %i sections each", + async (count) => { + const resources = new ArtifactResourceScope(); + const result = await searchGuidanceWithReader( + { query: "needle", limit: 1 }, + async (path) => + path === MANAGED_SKILL_FILES[0] || path === MANAGED_SKILL_FILES[1] + ? "# needle\n".repeat(count) + : "", + resources, + ); + expect(result).toMatchObject({ + ok: true, + value: { status: count === 2048 ? "matches" : "bundle_unavailable" }, + }); + await resources.close(); + }, +); + +it("preserves retry cleanup errors and their live owner in both operations", async () => { + const resources = new ArtifactResourceScope(); + const source = reader("# needle\nInstructions\n"); + const initial = await searchGuidanceWithReader( + { query: "needle" }, + source, + resources, + ); + if (!initial.ok || initial.value.status !== "matches") + throw new Error("Expected match"); + const locator = initial.value.matches[0]?.locator; + let blocked = true; + resources.retain({ + kind: "lease", + resource: "guidance descriptor", + lease: { + release: async () => { + if (blocked) throw new Error("still owned"); + }, + }, + }); + const retryingReader = (path: string) => resources.run(() => source(path)); + const errors = []; + for (const result of [ + await searchGuidanceWithReader( + { query: "needle" }, + retryingReader, + resources, + ), + await readGuidanceSectionWithReader({ locator }, retryingReader, resources), + ]) { + if (result.ok) throw new Error("Cleanup failures must remain errors"); + expect(result.error).toMatchObject({ cleanupIncomplete: true }); + expect(result.error.cleanupResources).toContain("guidance descriptor"); + errors.push(result.error); + } + blocked = false; + await resources.close(); + expect(resources.hasPending).toBe(false); + for (const error of errors) expect(error.cleanupIncomplete).toBe(false); +}); diff --git a/src/application/GuidanceAccess.lifecycle.test.ts b/src/application/GuidanceAccess.lifecycle.test.ts new file mode 100644 index 0000000000..f60bed2fd9 --- /dev/null +++ b/src/application/GuidanceAccess.lifecycle.test.ts @@ -0,0 +1,183 @@ +import { expect, it } from "vitest"; +import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js"; +import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js"; +import { MANAGED_SKILL_FILES } from "./SetupSkill.js"; +import { + readGuidanceSectionWithReader, + searchGuidanceWithReader, +} from "./GuidanceAccess.js"; + +const gate = () => { + let release = () => {}; + const promise = new Promise((resolve) => { + release = resolve; + }); + return { promise, release }; +}; +const content = (path: string) => + path === MANAGED_SKILL_FILES[0] ? "# needle\nInstructions\n" : "# Other\n"; + +it.each(["search", "read"] as const)( + "finishes every guide in an admitted %s while shutdown waits", + async (action) => { + const resources = new ArtifactResourceScope(); + + const reached = gate(); + const resume = gate(); + const paths: string[] = []; + const reader = (path: string, admitted = resources) => + admitted.run(async () => { + paths.push(path); + if (path === MANAGED_SKILL_FILES[1]) { + reached.release(); + await resume.promise; + } + return content(path); + }); + const launched: Promise[] = []; + try { + const initial = await searchGuidanceWithReader( + { query: "needle" }, + async (path) => content(path), + resources, + ); + if (!initial.ok || initial.value.status !== "matches") + throw new Error("Expected match"); + const operation = + action === "search" + ? searchGuidanceWithReader({ query: "needle" }, reader, resources) + : readGuidanceSectionWithReader( + { locator: initial.value.matches[0]?.locator }, + reader, + resources, + ); + launched.push(operation); + await awaitAcknowledgement( + reached.promise, + operation, + "second guidance read", + ); + let closed = false; + const closing = resources.close().then(() => { + closed = true; + }); + launched.push(closing); + await Promise.resolve(); + expect(closed).toBe(false); + resume.release(); + expect(await operation).toMatchObject({ + ok: true, + value: { status: action === "search" ? "matches" : "section" }, + }); + await closing; + expect(paths).toEqual(MANAGED_SKILL_FILES); + expect(closed).toBe(true); + expect(resources.hasPending).toBe(false); + } finally { + resume.release(); + await Promise.allSettled(launched); + await resources.close(); + } + }, +); + +it("retains a failed child while another admitted request reaches later reads", async () => { + const resources = new ArtifactResourceScope(); + const reached = gate(); + const resume = gate(); + const paths: string[] = []; + let blocked = true; + const failure = new RetainedRegularFileCleanupFailure( + new RegularFileCleanupFailure({ + path: "owned guidance descriptor", + owner: { + close: async () => { + if (blocked) throw new Error("close denied"); + }, + } as never, + outcome: { kind: "failed", cause: new Error("read failed") }, + cleanupCause: new Error("close denied"), + }), + ); + const launched: Promise[] = []; + try { + const active = searchGuidanceWithReader( + { query: "needle" }, + (path, child) => + child.run(async () => { + paths.push(path); + if (path === MANAGED_SKILL_FILES[1]) { + reached.release(); + await resume.promise; + } + return content(path); + }), + resources, + ); + launched.push(active); + await awaitAcknowledgement( + reached.promise, + active, + "overlapping guidance read", + ); + const failing = searchGuidanceWithReader( + { query: "needle" }, + async (_path, child) => { + child.retain(failure.cleanupOwner); + throw failure; + }, + resources, + ); + launched.push(failing); + const failed = await failing; + if (failed.ok) throw new Error("Expected cleanup failure"); + expect(failed.error.cleanupResources).toEqual([ + "owned guidance descriptor", + ]); + expect(failed.error.cleanupIncomplete).toBe(true); + const closing = resources.close().then( + () => undefined, + (cause: unknown) => cause, + ); + launched.push(closing); + resume.release(); + expect(await active).toMatchObject({ + ok: true, + value: { status: "matches" }, + }); + expect(paths).toEqual(MANAGED_SKILL_FILES); + expect(await closing).toMatchObject({ + cleanup: { resources: ["owned guidance descriptor"] }, + }); + expect(resources.hasPending).toBe(true); + expect(failed.error.cleanupIncomplete).toBe(true); + blocked = false; + await resources.close(); + expect(resources.hasPending).toBe(false); + expect(failed.error.cleanupIncomplete).toBe(false); + } finally { + resume.release(); + blocked = false; + await Promise.allSettled(launched); + await resources.close(); + } +}); + +it("refuses new work after shutdown without reporting a missing bundle", async () => { + const resources = new ArtifactResourceScope(); + await resources.close(); + let reads = 0; + await expect( + searchGuidanceWithReader( + { query: "needle" }, + async (path) => { + reads += 1; + return content(path); + }, + resources, + ), + ).rejects.toMatchObject({ reason: "unavailable" }); + expect(reads).toBe(0); +}); diff --git a/src/application/GuidanceAccess.test.ts b/src/application/GuidanceAccess.test.ts new file mode 100644 index 0000000000..3ceca83f02 --- /dev/null +++ b/src/application/GuidanceAccess.test.ts @@ -0,0 +1,320 @@ +import { expect, it } from "vitest"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { MANAGED_SKILL_FILES } from "./SetupSkill.js"; +import { searchGuidanceOutputSchema } from "../contracts/guidanceContracts.js"; +import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js"; +import { + readGuidanceSectionWithReader, + searchGuidanceWithReader, +} from "./GuidanceAccess.js"; + +const moduleGuide = `# JavaScript applications\n\n## Read a module without repeating analysis\n\nFor an exact JavaScript module (javascript-module), select its source path and module identity:\n\n\`\`\`json\n{"node_id":"module:main.js","kind":"javascript-module"}\n\`\`\`\n`; +const guideFixture = async (path: string): Promise => + path === "references/javascript-applications.md" + ? moduleGuide + : `# ${path}\n\nGeneral module guidance for pagination.\n`; + +const valueOf = async ( + request: ReturnType, +) => { + const result = await request; + if (!result.ok) throw result.error; + return searchGuidanceOutputSchema.parse(result.value); +}; + +it("ranks a focused JavaScript module section and retrieves it with its locator", async () => { + const resources = new ArtifactResourceScope(); + const result = await valueOf( + searchGuidanceWithReader( + { query: "exact JavaScript module" }, + guideFixture, + resources, + ), + ); + expect(result.status).toBe("matches"); + if (result.status !== "matches") throw new Error("Expected guide matches"); + expect(result.matches[0]).toMatchObject({ + guide: "references/javascript-applications.md", + heading: "Read a module without repeating analysis", + matched_terms: ["exact", "javascript", "module"], + context: expect.stringContaining("javascript-module"), + }); + const selected = result.matches[0]; + if (selected === undefined) throw new Error("Expected selected guidance"); + const retrieved = await readGuidanceSectionWithReader( + { locator: selected.locator }, + guideFixture, + resources, + ); + if (!retrieved.ok) throw retrieved.error; + expect(retrieved.value).toMatchObject({ + status: "section", + source: "installed_rea_guidance", + guide: selected.guide, + heading: selected.heading, + locator: selected.locator, + content: expect.stringContaining('"node_id":"module:main.js"'), + }); +}); + +it("returns an honest no-match result and rejects empty queries and invalid bounds", async () => { + const resources = new ArtifactResourceScope(); + expect( + await valueOf( + searchGuidanceWithReader( + { query: "unlikely qzxv term" }, + guideFixture, + resources, + ), + ), + ).toMatchObject({ + status: "no_matches", + total_matches: 0, + message: expect.stringContaining("does not indicate"), + }); + expect( + await valueOf( + searchGuidanceWithReader( + { query: "how do I frobnicate qzxv" }, + guideFixture, + resources, + ), + ), + ).toMatchObject({ + status: "no_matches", + total_matches: 0, + }); + expect( + await searchGuidanceWithReader({ query: " " }, guideFixture, resources), + ).toMatchObject({ ok: false }); + expect( + await searchGuidanceWithReader( + { query: "x".repeat(2049) }, + guideFixture, + resources, + ), + ).toMatchObject({ ok: false }); + expect( + await searchGuidanceWithReader( + { query: "module", limit: 21 }, + guideFixture, + resources, + ), + ).toMatchObject({ ok: false }); +}); + +it("does not treat Markdown headings inside fenced code as section boundaries", async () => { + const result = await valueOf( + searchGuidanceWithReader( + { query: "phantom-heading-token" }, + async (path) => + `${await guideFixture(path)}\n\n\`\`\`markdown\n# phantom-heading-token\n\`\`\`\n`, + new ArtifactResourceScope(), + ), + ); + expect(result.status).toBe("matches"); + if (result.status !== "matches") throw new Error("Expected guide matches"); + expect(result.matches.map(({ heading }) => heading)).not.toContain( + "phantom-heading-token", + ); +}); + +it("refuses an oversized guide bundle instead of indexing partial content", async () => { + const result = await searchGuidanceWithReader( + { query: "module" }, + async (path) => + path === "SKILL.md" ? "x".repeat(4_194_305) : guideFixture(path), + new ArtifactResourceScope(), + ); + expect(result).toMatchObject({ + ok: true, + value: { status: "bundle_unavailable" }, + }); +}); + +it("paginates deterministically without losing, duplicating, or reordering matches", async () => { + const readGuide = guideFixture; + const first = await valueOf( + searchGuidanceWithReader( + { query: "module", limit: 2 }, + readGuide, + new ArtifactResourceScope(), + ), + ); + const repeat = await valueOf( + searchGuidanceWithReader( + { query: "module", limit: 2 }, + readGuide, + new ArtifactResourceScope(), + ), + ); + expect(first).toEqual(repeat); + if (first.status !== "matches") throw new Error("Expected guide matches"); + expect(first.has_more).toBe(true); + const all = first.matches.map(({ locator }) => locator); + let cursor = first.next_cursor; + while (cursor !== undefined) { + const page = await valueOf( + searchGuidanceWithReader( + { query: "module", limit: 2, cursor }, + readGuide, + new ArtifactResourceScope(), + ), + ); + if (page.status !== "matches") throw new Error("Expected guide matches"); + all.push(...page.matches.map(({ locator }) => locator)); + cursor = page.next_cursor; + } + const complete = await valueOf( + searchGuidanceWithReader( + { query: "module", limit: 20 }, + readGuide, + new ArtifactResourceScope(), + ), + ); + if (complete.status !== "matches") throw new Error("Expected guide matches"); + expect(new Set(all).size).toBe(all.length); + expect(all).toEqual(complete.matches.map(({ locator }) => locator)); +}); + +it("rejects malformed locators and reports changed or missing installed guides", async () => { + const resources = new ArtifactResourceScope(); + const search = await valueOf( + searchGuidanceWithReader( + { query: "exact JavaScript module" }, + guideFixture, + resources, + ), + ); + if (search.status !== "matches") throw new Error("Expected guide matches"); + expect( + await readGuidanceSectionWithReader( + { locator: "../../outside.md" }, + guideFixture, + resources, + ), + ).toMatchObject({ ok: false }); + const changedReader = async (path: string) => + `${await guideFixture(path)}\nChanged after search.\n`; + const stale = await readGuidanceSectionWithReader( + { locator: search.matches[0]?.locator }, + changedReader, + resources, + ); + expect(stale).toMatchObject({ ok: true, value: { status: "stale_locator" } }); + const firstPage = await valueOf( + searchGuidanceWithReader( + { query: "module", limit: 1 }, + guideFixture, + resources, + ), + ); + if (firstPage.status !== "matches" || firstPage.next_cursor === undefined) + throw new Error("Expected a continuation token"); + expect( + await searchGuidanceWithReader( + { query: "module", limit: 1, cursor: firstPage.next_cursor }, + changedReader, + resources, + ), + ).toMatchObject({ ok: true, value: { status: "stale_continuation" } }); + expect( + await searchGuidanceWithReader( + { query: "different query", cursor: firstPage.next_cursor }, + guideFixture, + resources, + ), + ).toMatchObject({ ok: false }); + expect( + await searchGuidanceWithReader( + { query: "module", cursor: "garbage" }, + guideFixture, + resources, + ), + ).toMatchObject({ ok: false }); + const missingReader = async (): Promise => { + throw Object.assign(new Error("missing"), { code: "ENOENT" }); + }; + expect( + await searchGuidanceWithReader( + { query: "module" }, + missingReader, + resources, + ), + ).toMatchObject({ ok: true, value: { status: "bundle_unavailable" } }); + expect( + await readGuidanceSectionWithReader( + { locator: search.matches[0]?.locator }, + missingReader, + resources, + ), + ).toMatchObject({ ok: true, value: { status: "bundle_unavailable" } }); +}); + +it("returns cleanup failures separately and retains their owner until retry succeeds", async () => { + const resources = new ArtifactResourceScope(); + let failRelease = true; + const failure = new RegularFileCleanupFailure({ + path: "guidance file handle", + owner: { + close: async () => { + if (failRelease) throw new Error("descriptor close failed"); + }, + } as never, + outcome: { kind: "failed", cause: new Error("guide read failed") }, + cleanupCause: new Error("descriptor close failed"), + }); + const retainedFailure = new RetainedRegularFileCleanupFailure(failure); + resources.retain(retainedFailure.cleanupOwner); + const result = await searchGuidanceWithReader( + { query: "module" }, + async () => { + throw retainedFailure; + }, + resources, + ); + if (result.ok) throw new Error("Expected a retained cleanup failure"); + expect(result.error).toMatchObject({ cleanupIncomplete: true }); + expect(result.error.cleanupResources).toEqual(["guidance file handle"]); + expect(resources.hasPending).toBe(true); + failRelease = false; + await resources.close(); + expect(resources.hasPending).toBe(false); + expect(result.error.cleanupIncomplete).toBe(false); + expect(result.error.cleanupResources).toEqual([]); + + const freshResources = new ArtifactResourceScope(); + freshResources.retain({ + kind: "lease", + resource: "unrelated pending resource", + lease: { release: async () => undefined }, + }); + expect( + await searchGuidanceWithReader( + { query: "module" }, + async () => { + throw new Error("guide is unreadable"); + }, + freshResources, + ), + ).toMatchObject({ + ok: true, + value: { status: "bundle_unavailable" }, + }); + await freshResources.close(); +}); + +it("uses only the fixed managed guide inventory for injected reads", async () => { + const requested: string[] = []; + await searchGuidanceWithReader( + { query: "module" }, + async (path) => { + requested.push(path); + return guideFixture(path); + }, + new ArtifactResourceScope(), + ); + expect(requested).toEqual([...MANAGED_SKILL_FILES]); +}); diff --git a/src/application/GuidanceAccess.ts b/src/application/GuidanceAccess.ts new file mode 100644 index 0000000000..3087102d7d --- /dev/null +++ b/src/application/GuidanceAccess.ts @@ -0,0 +1,602 @@ +import { createHash } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import type { z } from "zod"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; +import { MANAGED_SKILL_FILES, readSkillFile } from "./SetupSkill.js"; +import { PRODUCT_IDENTITY } from "../identity.js"; +import { + readGuidanceSectionInputSchema, + readGuidanceSectionOutputSchema, + type ReadGuidanceSectionOutput, + type SearchGuidanceOutput, + searchGuidanceInputSchema, + searchGuidanceOutputSchema, +} from "../contracts/guidanceContracts.js"; +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { + bindRegularFileCleanup, + RetainedRegularFileCleanupFailure, +} from "./RegularFileRead.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { + parseGuidanceSections, + type GuideSection, +} from "./GuidanceSections.js"; + +const SKILL_ROOT = `../../skills/${PRODUCT_IDENTITY.skillName}/`; +const SOURCE = "installed_rea_guidance" as const; +const MAX_GUIDE_FILE_BYTES = 1_048_576; +const MAX_GUIDE_BUNDLE_BYTES = 4_194_304; +const sha256 = (value: string | Uint8Array): string => + createHash("sha256").update(value).digest("hex"); + +interface GuidanceBundle { + readonly digest: string; + readonly sections: readonly GuideSection[]; +} + +const readPackagedGuide = ( + relativePath: string, + resources: ArtifactResourceScope, +): Promise => + readSkillFile( + fileURLToPath(new URL(`${SKILL_ROOT}${relativePath}`, import.meta.url)), + resources, + MAX_GUIDE_FILE_BYTES, + ); + +type GuidanceBundleRead = + | { readonly kind: "available"; readonly bundle: GuidanceBundle } + | { readonly kind: "unavailable"; readonly message: string } + | { readonly kind: "cleanup"; readonly error: ProviderCleanupError }; + +const guidanceCleanupFailure = ( + cause: unknown, + resources: ArtifactResourceScope, + operation: string, +): ProviderCleanupError => { + const observation = ArtifactReaderFailure.cleanupObservation( + cause, + resources.cleanupResources, + ); + const error = new ProviderCleanupError( + "rea", + resources.cleanupResources, + { phase: "managed_guidance_read", reason: observation.reason }, + { + operation, + cause, + cleanup: observation, + remediationAction: + "Close REA so it can retry releasing the retained guidance file handle. Do not repeat the request until cleanup completes.", + }, + ); + bindRegularFileCleanup(cause, error); + return error; +}; + +const readBundle = async ( + readGuide: (relativePath: string) => Promise, + resources: ArtifactResourceScope, + operation: string, +): Promise => { + const files = new Map(); + let bundleBytes = 0; + try { + for (const relativePath of MANAGED_SKILL_FILES) { + const content = await readGuide(relativePath); + bundleBytes += Buffer.byteLength(content, "utf8"); + if (bundleBytes > MAX_GUIDE_BUNDLE_BYTES) + return { + kind: "unavailable", + message: + "Installed REA guidance exceeds the 4 MiB bundle allowance; no partial bundle was indexed.", + }; + files.set(relativePath, content.replaceAll("\r\n", "\n")); + } + } catch (cause: unknown) { + return cause instanceof RetainedRegularFileCleanupFailure || + (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) + ? { + kind: "cleanup", + error: guidanceCleanupFailure(cause, resources, operation), + } + : { + kind: "unavailable", + message: + "Installed REA guidance is missing or unreadable; no complete bundle could be indexed.", + }; + } + const digest = sha256( + [...files] + .map(([path, content]) => `${path}\0${sha256(content)}\n`) + .join(""), + ); + const sections = parseGuidanceSections(files); + if (sections === undefined) + return { + kind: "unavailable", + message: + "Installed REA guidance exceeds the 4,096-section or 25,165,824 expanded UTF-16-code-unit index allowance; no partial index was used.", + }; + return { kind: "available", bundle: { digest, sections } }; +}; + +const words = (text: string): string[] => [ + ...new Set(text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []), +]; + +const GENERIC_QUERY_WORDS: ReadonlySet = new Set([ + "a", + "about", + "an", + "and", + "are", + "as", + "at", + "be", + "been", + "by", + "can", + "could", + "did", + "do", + "does", + "for", + "from", + "how", + "i", + "in", + "is", + "it", + "my", + "of", + "on", + "or", + "should", + "so", + "that", + "the", + "their", + "there", + "they", + "this", + "to", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "who", + "why", + "will", + "with", + "would", + "you", + "your", +]); + +const sectionLocator = ( + bundle: GuidanceBundle, + section: GuideSection, +): string => { + const identity = JSON.stringify({ + v: 1, + bundle: bundle.digest, + guide: section.guide, + ordinal: section.ordinal, + section: sha256(section.content), + }); + return `rea-guide-v1.${Buffer.from(identity).toString("base64url")}`; +}; + +type GuidanceReader = ( + relativePath: string, + resources: ArtifactResourceScope, +) => Promise; +type GuidanceResult = Result< + Value, + AnalysisInputError | ProviderCleanupError +>; + +/** Admit the complete request once; file reads belong to its independent child. */ +const runGuidanceOperation = async ( + resources: ArtifactResourceScope, + operationName: string, + operation: (child: ArtifactResourceScope) => Promise>, +): Promise> => { + try { + return await resources.run(async () => { + const child = new ArtifactResourceScope(); + try { + return await operation(child); + } finally { + if (child.hasPending) + resources.retain({ + kind: "lease", + resource: "managed guidance operation", + lease: new AnalysisCleanupLease(() => child.close(), { + get cleanupResources() { + return child.cleanupResources; + }, + }), + }); + else await child.close(); + } + }); + } catch (cause: unknown) { + if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) + return err(guidanceCleanupFailure(cause, resources, operationName)); + throw cause; + } +}; + +/** Reader seams receive the operation child rather than re-entering the root. */ +export const searchGuidanceWithReader = ( + input: unknown, + readGuide: GuidanceReader, + resources: ArtifactResourceScope, +): Promise> => + runGuidanceOperation(resources, "search_guidance", (child) => + searchGuidanceInScope(input, (path) => readGuide(path, child), child), + ); + +export const readGuidanceSectionWithReader = ( + input: unknown, + readGuide: GuidanceReader, + resources: ArtifactResourceScope, +): Promise> => + runGuidanceOperation(resources, "read_guidance_section", (child) => + readGuidanceSectionInScope(input, (path) => readGuide(path, child), child), + ); + +const invalidInput = ( + operation: string, + input: unknown, + issues: readonly z.core.$ZodIssue[], +): Result => + err(analysisInputErrorFromIssues(operation, issues, input)); + +export const searchGuidance = async ( + input: unknown, + resources: ArtifactResourceScope, +): Promise< + Result +> => searchGuidanceWithReader(input, readPackagedGuide, resources); + +/** Reader seam keeps packaged-guide drift and missing-file behavior testable. */ +const searchGuidanceInScope = async ( + input: unknown, + readGuide: (relativePath: string) => Promise, + resources: ArtifactResourceScope, +): Promise< + Result +> => { + const parsed = searchGuidanceInputSchema.safeParse(input); + if (!parsed.success) + return invalidInput("search_guidance", input, parsed.error.issues); + const cursor = + parsed.data.cursor === undefined + ? undefined + : decodeGuidanceCursor(parsed.data.cursor); + if (parsed.data.cursor !== undefined && cursor === undefined) + return invalidInput("search_guidance", input, [ + { + code: "custom", + path: ["cursor"], + message: "Invalid guidance continuation token", + }, + ]); + const queryDigest = sha256(parsed.data.query); + if (cursor !== undefined && cursor.query !== queryDigest) + return invalidInput("search_guidance", input, [ + { + code: "custom", + path: ["cursor"], + message: "The continuation token belongs to a different query", + }, + ]); + const loaded = await readBundle(readGuide, resources, "search_guidance"); + if (loaded.kind === "cleanup") return err(loaded.error); + if (loaded.kind === "unavailable") + return ok( + searchGuidanceOutputSchema.parse({ + status: "bundle_unavailable", + source: SOURCE, + content_kind: "documentation", + message: loaded.message, + }), + ); + const bundle = loaded.bundle; + + if (cursor !== undefined && cursor.bundle !== bundle.digest) + return ok( + searchGuidanceOutputSchema.parse({ + status: "stale_continuation", + source: SOURCE, + content_kind: "documentation", + message: + "The installed guidance changed since this search page was issued. Restart the search to continue with current content.", + }), + ); + + const queryWords = words(parsed.data.query); + const specificQueryWords = queryWords.filter( + (word) => !GENERIC_QUERY_WORDS.has(word), + ); + const queryWordsToMatch = specificQueryWords; + const ranked = bundle.sections + .flatMap((section) => { + const sectionWords = new Set(words(section.content)); + const matchedSpecific = queryWordsToMatch.filter((word) => + sectionWords.has(word), + ); + if ( + queryWordsToMatch.length === 0 || + matchedSpecific.length !== queryWordsToMatch.length + ) + return []; + const matched = queryWords.filter((word) => sectionWords.has(word)); + const headingWords = new Set(words(section.heading)); + const score = + matched.length + + matched.filter((word) => headingWords.has(word)).length * 2; + return { + section, + matched, + score, + wordCount: sectionWords.size, + }; + }) + .sort( + (left, right) => + right.score - left.score || + // Prefer a focused recipe over a long overview when term scores tie. + left.wordCount - right.wordCount || + compareText(left.section.guide, right.section.guide) || + left.section.ordinal - right.section.ordinal, + ); + + if (ranked.length === 0) + return ok( + searchGuidanceOutputSchema.parse({ + status: "no_matches", + source: SOURCE, + content_kind: "documentation", + query: parsed.data.query, + message: + "No bundled guidance sections matched this query. This does not indicate whether an analysis capability is available.", + total_matches: 0, + }), + ); + + const offset = cursor?.offset ?? 0; + if (cursor !== undefined && offset >= ranked.length) + return invalidInput("search_guidance", input, [ + { + code: "custom", + path: ["cursor"], + message: + "The continuation token does not select a remaining result page", + }, + ]); + const page = ranked.slice(offset, offset + parsed.data.limit); + const hasMore = offset + page.length < ranked.length; + return ok( + searchGuidanceOutputSchema.parse({ + status: "matches", + source: SOURCE, + content_kind: "documentation", + package: `${PRODUCT_IDENTITY.packageName}@${PRODUCT_IDENTITY.packageVersion}`, + bundle_sha256: bundle.digest, + query: parsed.data.query, + matches: page.map(({ section, matched }) => ({ + guide: section.guide, + heading: section.heading, + locator: sectionLocator(bundle, section), + matched_terms: matched, + relevance: `Matched ${matched.join(", ")} in this section.`, + context: contextFor(section.content, matched), + })), + total_matches: ranked.length, + offset, + limit: parsed.data.limit, + has_more: hasMore, + ...(hasMore + ? { + next_cursor: encodeGuidanceCursor({ + v: 1, + bundle: bundle.digest, + query: queryDigest, + offset: offset + page.length, + }), + } + : {}), + }), + ); +}; + +export const readGuidanceSection = async ( + input: unknown, + resources: ArtifactResourceScope, +): Promise< + Result +> => readGuidanceSectionWithReader(input, readPackagedGuide, resources); + +/** Reader seam keeps packaged-guide drift and missing-file behavior testable. */ +const readGuidanceSectionInScope = async ( + input: unknown, + readGuide: (relativePath: string) => Promise, + resources: ArtifactResourceScope, +): Promise< + Result +> => { + const parsed = readGuidanceSectionInputSchema.safeParse(input); + if (!parsed.success) + return invalidInput("read_guidance_section", input, parsed.error.issues); + const identity = decodeLocator(parsed.data.locator); + if (identity === undefined) + return invalidInput("read_guidance_section", input, [ + { + code: "custom", + path: ["locator"], + message: "Malformed or unsupported guidance locator", + }, + ]); + const loaded = await readBundle( + readGuide, + resources, + "read_guidance_section", + ); + if (loaded.kind === "cleanup") return err(loaded.error); + if (loaded.kind === "unavailable") + return ok( + readGuidanceSectionOutputSchema.parse({ + status: "bundle_unavailable", + source: SOURCE, + content_kind: "documentation", + message: loaded.message, + }), + ); + const bundle = loaded.bundle; + if (identity.bundle !== bundle.digest) + return ok( + readGuidanceSectionOutputSchema.parse({ + status: "stale_locator", + source: SOURCE, + content_kind: "documentation", + message: + "The installed guidance changed since this locator was issued. Search again for a current locator.", + }), + ); + const section = bundle.sections.find( + (candidate) => + candidate.guide === identity.guide && + candidate.ordinal === identity.ordinal && + sha256(candidate.content) === identity.section, + ); + if (section === undefined) + return ok( + readGuidanceSectionOutputSchema.parse({ + status: "stale_locator", + source: SOURCE, + content_kind: "documentation", + message: + "The selected section is no longer present in the installed guidance. Search again.", + }), + ); + return ok( + readGuidanceSectionOutputSchema.parse({ + status: "section", + source: SOURCE, + content_kind: "documentation", + package: `${PRODUCT_IDENTITY.packageName}@${PRODUCT_IDENTITY.packageVersion}`, + bundle_sha256: bundle.digest, + guide: section.guide, + heading: section.heading, + locator: parsed.data.locator, + content: section.content, + }), + ); +}; + +interface LocatorIdentity { + readonly v: 1; + readonly bundle: string; + readonly guide: string; + readonly ordinal: number; + readonly section: string; +} + +interface GuidanceCursor { + readonly v: 1; + readonly bundle: string; + readonly query: string; + readonly offset: number; +} + +const encodeGuidanceCursor = (cursor: GuidanceCursor): string => + `rea-guidance-page-v1.${Buffer.from(JSON.stringify(cursor)).toString("base64url")}`; + +const decodeGuidanceCursor = (cursor: string): GuidanceCursor | undefined => { + const prefix = "rea-guidance-page-v1."; + if (!cursor.startsWith(prefix)) return undefined; + try { + const value: unknown = JSON.parse( + Buffer.from(cursor.slice(prefix.length), "base64url").toString("utf8"), + ); + if (typeof value !== "object" || value === null) return undefined; + const item = value as Partial; + if ( + item.v !== 1 || + typeof item.bundle !== "string" || + !/^[a-f0-9]{64}$/u.test(item.bundle) || + typeof item.query !== "string" || + !/^[a-f0-9]{64}$/u.test(item.query) || + typeof item.offset !== "number" || + !Number.isInteger(item.offset) || + item.offset < 1 + ) + return undefined; + return item as GuidanceCursor; + } catch { + return undefined; + } +}; + +const decodeLocator = (locator: string): LocatorIdentity | undefined => { + if (!locator.startsWith("rea-guide-v1.")) return undefined; + try { + const value: unknown = JSON.parse( + Buffer.from(locator.slice("rea-guide-v1.".length), "base64url").toString( + "utf8", + ), + ); + if (typeof value !== "object" || value === null) return undefined; + const item = value as Partial; + if ( + item.v !== 1 || + typeof item.bundle !== "string" || + !/^[a-f0-9]{64}$/u.test(item.bundle) || + typeof item.guide !== "string" || + !(MANAGED_SKILL_FILES as readonly string[]).includes(item.guide) || + !Number.isInteger(item.ordinal) || + typeof item.section !== "string" || + !/^[a-f0-9]{64}$/u.test(item.section) + ) + return undefined; + return item as LocatorIdentity; + } catch { + return undefined; + } +}; + +const contextFor = (content: string, matched: readonly string[]): string => { + const lines = content.split("\n"); + const heading = lines[0] ?? ""; + const terms = new Set(matched); + let bestIndex = -1; + let bestScore = 0; + lines.slice(1).forEach((line, index) => { + const score = words(line).filter((word) => terms.has(word)).length; + if (score > bestScore) { + bestScore = score; + bestIndex = index + 1; + } + }); + const excerptStart = bestIndex < 0 ? 1 : Math.max(1, bestIndex - 1); + const excerptEnd = bestIndex < 0 ? 4 : bestIndex + 2; + return [heading, ...lines.slice(excerptStart, excerptEnd)] + .join(" ") + .replaceAll(/\s+/gu, " ") + .slice(0, 500); +}; + +const compareText = (left: string, right: string): number => + left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/GuidanceSections.ts b/src/application/GuidanceSections.ts new file mode 100644 index 0000000000..629bdc60f2 --- /dev/null +++ b/src/application/GuidanceSections.ts @@ -0,0 +1,123 @@ +/** Parsed documentation facts; callers never reinterpret Markdown boundaries. */ +export interface GuideSection { + readonly guide: string; + readonly heading: string; + readonly content: string; + readonly ordinal: number; +} + +// The shipped bundle has tens of headings. These ceilings bound object count +// and UTF-16 text retained by a complete index before allocating section text. +const MAX_SECTIONS = 4096; +// ATX has at most six enclosing levels over the admitted 4 MiB bundle. +const MAX_EXPANDED_CHARACTERS = 6 * 4_194_304; + +const blank = (value: string | undefined): boolean => + value === " " || value === "\t"; + +/** ATX closing hashes and ASCII whitespace, scanned once from each end. */ +const headingTitle = (raw: string): string => { + let start = 0; + let end = raw.length; + while (start < end && blank(raw[start])) start += 1; + while (end > start && blank(raw[end - 1])) end -= 1; + let hashes = end; + while (hashes > start && raw[hashes - 1] === "#") hashes -= 1; + if (hashes < end && (hashes === start || blank(raw[hashes - 1]))) { + end = hashes; + while (end > start && blank(raw[end - 1])) end -= 1; + } + return raw.slice(start, end); +}; + +interface Heading { + readonly level: number; + readonly title: string; + readonly start: number; + end: number; +} + +const atxHeading = ( + line: string, +): Pick | undefined => { + const match = /^ {0,3}(#{1,6})(?:[ \t]+(.*)|$)/u.exec(line); + if (match?.[1] === undefined) return undefined; + const raw = match[2] ?? ""; + // Closing hashes need whitespace; literal C# and escaped hashes survive. + const title = headingTitle(raw); + return { level: match[1].length, title }; +}; + +const readHeadings = ( + content: string, + remaining: number, +): Heading[] | undefined => { + const headings: Heading[] = []; + const enclosing: Heading[] = []; + let fence: { marker: string; length: number } | undefined; + let start = 0; + while (start < content.length) { + const newline = content.indexOf("\n", start); + const end = newline < 0 ? content.length : newline; + const line = content.slice(start, end); + const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line); + const marker = fenceMatch?.[1]; + if (fence !== undefined) { + if ( + marker?.[0] === fence.marker && + marker.length >= fence.length && + /^[ \t]*$/u.test(fenceMatch?.[2] ?? "") + ) + fence = undefined; + } else if ( + marker !== undefined && + !(marker[0] === "`" && fenceMatch?.[2]?.includes("`")) + ) { + fence = { marker: marker[0] ?? "", length: marker.length }; + } else { + const parsed = atxHeading(line); + if (parsed !== undefined) { + if (headings.length >= remaining) return undefined; + while ((enclosing.at(-1)?.level ?? 0) >= parsed.level) { + const parent = enclosing.pop(); + if (parent !== undefined) parent.end = start; + } + const heading = { ...parsed, start, end: content.length }; + headings.push(heading); + enclosing.push(heading); + } + } + start = newline < 0 ? content.length : newline + 1; + } + return headings; +}; + +/** Bound the whole bundle before materializing an expanded section index. */ +export const parseGuidanceSections = ( + files: ReadonlyMap, +): readonly GuideSection[] | undefined => { + const documents: { guide: string; content: string; headings: Heading[] }[] = + []; + let count = 0; + let expanded = 0; + for (const [guide, content] of files) { + const headings = readHeadings(content, MAX_SECTIONS - count); + if (headings === undefined) return undefined; + count += headings.length; + for (const heading of headings) { + expanded += heading.end - heading.start; + if (expanded > MAX_EXPANDED_CHARACTERS) return undefined; + } + documents.push({ guide, content, headings }); + } + const sections: GuideSection[] = []; + for (const { guide, content, headings } of documents) + for (const [ordinal, heading] of headings.entries()) + sections.push({ + guide, + ordinal, + heading: heading.title, + content: content.slice(heading.start, heading.end).trim(), + }); + return sections; +}; diff --git a/src/application/JsonFiles.ts b/src/application/JsonFiles.ts index f104a7e57d..1218671e41 100644 --- a/src/application/JsonFiles.ts +++ b/src/application/JsonFiles.ts @@ -44,6 +44,7 @@ import { } from "../artifacts/AnalysisCleanupLease.js"; import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js"; import { OwnedStagedOutput } from "../filesystem/OwnedStagedOutput.js"; +import { StagedOutputError } from "../filesystem/StagedOutputError.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; /** Request control and its owning operation for an interruptible atomic write. */ @@ -399,6 +400,7 @@ const writeTextPartsOwned = async ( path: requestedPath, cleanup, published: { ...cause.outcome.value, path: requestedPath }, + ...stagedOutputDiagnostics(cause.cleanupCause), }), cause.lease, ); @@ -411,16 +413,22 @@ const writeTextPartsOwned = async ( }), cause.lease, ); + const failure = classifyWriteFailure(cause.outcome.error); return stagedCleanupFailure( - new EvidenceFileError( - "write", - fileAccessFailureReason(cause.outcome.error), - { - cause, - path: requestedPath, - cleanup, - }, - ), + new EvidenceFileError("write", failure.reason, { + cause, + path: requestedPath, + cleanup, + ...(failure.systemCode === undefined + ? {} + : { systemCode: failure.systemCode }), + ...stagedOutputDiagnostics( + !cause.outcome.ok && + cause.outcome.error instanceof StagedOutputError + ? cause.outcome.error + : cause.cleanupCause, + ), + }), cause.lease, ); } @@ -437,15 +445,33 @@ const writeTextPartsOwned = async ( path: requestedPath, }), ); + const failure = classifyWriteFailure(cause); return err( - new EvidenceFileError("write", fileAccessFailureReason(cause), { + new EvidenceFileError("write", failure.reason, { cause, path: requestedPath, + ...(failure.systemCode === undefined + ? {} + : { systemCode: failure.systemCode }), + ...stagedOutputDiagnostics(cause), }), ); } }; +/** Recognize only host codes that establish a publication resource refusal. */ +const classifyWriteFailure = ( + cause: unknown, +): { + readonly reason: EvidenceFileError["reason"]; + readonly systemCode?: "EFBIG" | "ENOSPC" | "EDQUOT"; +} => { + const code = fileErrorCode(cause); + return code === "EFBIG" || code === "ENOSPC" || code === "EDQUOT" + ? { reason: "storage-limit", systemCode: code } + : { reason: fileAccessFailureReason(cause) }; +}; + /** The retained staging lease clears this error only when a retry confirms release. */ const stagedCleanupFailure = < Failure extends EvidenceFileError | AnalysisCancelledError, @@ -595,6 +621,15 @@ class StagedOutputCleanupFailure extends Error { } } +const stagedOutputDiagnostics = ( + cause: unknown, +): { + readonly diagnostics?: Readonly>; +} => { + if (!(cause instanceof StagedOutputError)) return {}; + return { diagnostics: { code: cause.code, ...cause.detail } }; +}; + const failureMessage = (cause: unknown): string => cause instanceof Error ? cause.message || cause.name : String(cause); diff --git a/src/application/SetupRegistrationEnvironment.ts b/src/application/SetupRegistrationEnvironment.ts index ad629fb595..401e18806d 100644 --- a/src/application/SetupRegistrationEnvironment.ts +++ b/src/application/SetupRegistrationEnvironment.ts @@ -65,6 +65,7 @@ const SELECTED_REGISTRATION_SETTINGS = [ "REA_GHIDRA_PROJECT_PATH", "REA_GHIDRA_PROGRAM_NAME", "REA_GHIDRA_NATIVEAOT_JAR", + "REA_HOPPER_CALLER_SCAN_BUDGET_MS", "REA_HOPPER_STARTUP_TIMEOUT_MS", "REA_JEB_MCP_URL", "REA_JEB_MCP_TIMEOUT_MS", diff --git a/src/application/SetupSkill.ts b/src/application/SetupSkill.ts index bc4c83b339..14883e8105 100644 --- a/src/application/SetupSkill.ts +++ b/src/application/SetupSkill.ts @@ -43,21 +43,27 @@ export const isManagedSkillManifest = (content: string): boolean => export const readSkillFile = ( path: string, resources?: ArtifactResourceScope, + maxBytes = Number.MAX_SAFE_INTEGER, ): Promise => - readSkillFileSnapshot(path, resources).then(({ bytes }) => + readSkillFileSnapshot(path, resources, maxBytes).then(({ bytes }) => bytes.toString("utf8"), ); const readSkillFileSnapshot = async ( path: string, resources?: ArtifactResourceScope, + maxBytes = Number.MAX_SAFE_INTEGER, ): Promise => withRegularFile( path, async (handle, stats) => { - const bytes = await readBoundedFileBytes(handle, stats.size, { - expectedSize: stats.size, - }); + const bytes = await readBoundedFileBytes( + handle, + Math.min(stats.size, maxBytes), + { + expectedSize: stats.size, + }, + ); if (bytes === undefined || bytes.length !== stats.size) throw new RegularFileChangedError(path); return { bytes, mode: stats.mode & 0o7777, state: stats }; diff --git a/src/application/WorkflowEvidence.test.ts b/src/application/WorkflowEvidence.test.ts index 0cb64d0cbf..deed631640 100644 --- a/src/application/WorkflowEvidence.test.ts +++ b/src/application/WorkflowEvidence.test.ts @@ -188,7 +188,8 @@ it("retains unique upstream limits in canonical order across concurrent observat const analysis: AnalysisOperationPort = { execute: async (operation, parameters, forwarded) => { expect(parameters).toEqual({ marker: true }); - expect(forwarded).toBe(options); + expect(forwarded).toMatchObject(options); + expect(forwarded?.workScope).toBeDefined(); const call = calls++; if (call === 0) await gate; else release(); @@ -621,3 +622,60 @@ it.each([ }); }, ); + +it("shares temporary provider state only within a single completed workflow", async () => { + const owner = {}; + const scopes: Array< + NonNullable + > = []; + const provider = { id: "fixture", name: "Fixture", version: "1" }; + const analysis: AnalysisOperationPort = { + execute: async (_operation, _parameters, options) => { + const scope = options?.workScope; + if (scope === undefined) throw new Error("workflow scope missing"); + scopes.push(scope); + scope.set(owner, (scope.get(owner) ?? 0) + 1); + return ok(createAnalysisExecution(null, provider)); + }, + }; + const run = () => + executeComposedWorkflow(analysis, async (observed) => { + await observed.execute("health", {}); + await observed.execute("health", {}); + expect(scopes.at(-1)?.get(owner)).toBe(2); + return ok(null); + }); + await run(); + expect(scopes[0]?.get(owner)).toBeUndefined(); + scopes[0]?.set(owner, "late completion"); + expect(scopes[0]?.get(owner)).toBeUndefined(); + await run(); + expect(scopes[0]).toBe(scopes[1]); + expect(scopes[0]).not.toBe(scopes[2]); + expect(scopes[2]).toBe(scopes[3]); +}); + +it("clears workflow state when its callback throws", async () => { + const owner = {}; + let captured: import("./AnalysisProvider.js").ExecutionOptions["workScope"]; + const analysis: AnalysisOperationPort = { + execute: async (_operation, _parameters, options) => { + captured = options?.workScope; + captured?.set(owner, "in flight"); + return ok( + createAnalysisExecution(null, { + id: "fixture", + name: "Fixture", + version: "1", + }), + ); + }, + }; + await expect( + executeComposedWorkflow(analysis, async (observed) => { + await observed.execute("health", {}); + throw new Error("failed workflow"); + }), + ).rejects.toThrow("failed workflow"); + expect(captured?.get(owner)).toBeUndefined(); +}); diff --git a/src/application/WorkflowEvidence.ts b/src/application/WorkflowEvidence.ts index a68f8ba9fa..1b59ed7fdd 100644 --- a/src/application/WorkflowEvidence.ts +++ b/src/application/WorkflowEvidence.ts @@ -1,3 +1,4 @@ +import { AnalysisWorkScope } from "./AnalysisWorkScope.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { jsonStringByteLength } from "../domain/jsonStringByteLength.js"; import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js"; @@ -91,10 +92,14 @@ export const executeComposedWorkflow = async ( let perItemBytes = 0; let omittedObservations = 0; let cachePolicy: "live" | undefined; - const result = await execute({ + const workScope = new AnalysisWorkScope(); + const observed: AnalysisOperationPort = { resourceScope: analysis.resourceScope, execute: async (operation, parameters, options) => { - const execution = await analysis.execute(operation, parameters, options); + const execution = await analysis.execute(operation, parameters, { + ...options, + workScope, + }); if (execution.ok && execution.value.evidenceId !== undefined) evidenceLinks.add(execution.value.evidenceId); if (execution.ok && execution.value.sourceEvidence !== undefined) { @@ -139,7 +144,10 @@ export const executeComposedWorkflow = async ( } return execution; }, - }); + }; + const result = await Promise.resolve() + .then(() => execute(observed)) + .finally(() => workScope.clear()); const sources = sourceLedger.recordsForAnalysis(); if (!result.ok) return withEvidenceSources(result, sources); for (const source of sources) evidenceLinks.add(source.evidence_id); diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 17fab89578..9fe7d51c6d 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -32,6 +32,7 @@ import { AnalysisSnapshotCache, SNAPSHOT_CACHE_ENTRY_CEILING, isSnapshotCacheable, + isSourceEvidenceRetainable, } from "./AnalysisSnapshotCache.js"; import { EvidenceLedger } from "../investigation/EvidenceLedger.js"; import { enhancedOutputSchemas } from "../../contracts/toolOutputSchemaGroups.js"; @@ -776,6 +777,59 @@ describe("analysis snapshot cache policy", () => { ); }, ); + + it("retains caller observations without a document while refusing to replay them", () => { + const descriptor: CapabilityDescriptor = { + operation: "procedure_callers", + provider: ANALYSIS_SNAPSHOT_PROVIDER, + available: true, + reason: null, + effects: { + mutatesArtifact: false, + launchesProcess: false, + mayShowUi: false, + mayAccessNetwork: false, + mayWriteFilesystem: false, + changesPermissions: false, + requiresRoot: false, + }, + limitations: [], + }; + const parameters = { procedure: "0x1000" }; + expect( + isSourceEvidenceRetainable("procedure_callers", descriptor, parameters), + ).toBe(true); + expect( + isSnapshotCacheable("procedure_callers", descriptor, parameters), + ).toBe(false); + expect( + isSnapshotCacheable("procedure_callers", descriptor, { + ...parameters, + document: "fixture", + }), + ).toBe(true); + const callees = { ...descriptor, operation: "procedure_callees" as const }; + expect( + isSourceEvidenceRetainable("procedure_callees", callees, parameters), + ).toBe(true); + const pseudocode = { + ...descriptor, + operation: "procedure_pseudo_code" as const, + }; + expect( + isSourceEvidenceRetainable( + "procedure_pseudo_code", + pseudocode, + parameters, + ), + ).toBe(false); + expect( + isSourceEvidenceRetainable("procedure_pseudo_code", pseudocode, { + ...parameters, + document: "fixture", + }), + ).toBe(true); + }); }); describe("analysis snapshot cache partitioning", () => { diff --git a/src/application/binary/AnalysisSnapshotCache.ts b/src/application/binary/AnalysisSnapshotCache.ts index 159139d72e..e5917209dd 100644 --- a/src/application/binary/AnalysisSnapshotCache.ts +++ b/src/application/binary/AnalysisSnapshotCache.ts @@ -87,8 +87,28 @@ const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet = new Set( /** Maximum duplicated result bindings retained across direct and workflow queries. */ export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000; -/** Whether an operation is immutable and independent of provider UI state. */ -export const isSnapshotCacheable = ( +const replayDocumentSelected = ( + operation: AnalysisOperation, + parameters: Readonly>, +): boolean => + !DOCUMENT_SCOPED_OPERATIONS.has(operation) || + typeof parameters.document === "string"; + +/** + * Caller and callee walks omit `document`. Their observations are still + * Evidence. Other document-scoped reads, including `procedure_pseudo_code` + * from `decompile_all`, already keep each body in the file-backed export. + * Copying those bodies into the in-memory ledger would retain the export. + */ +const DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS: ReadonlySet = + new Set(["procedure_callers", "procedure_callees"]); + +/** + * Whether a source observation is safe to retain as Evidence. + * A missing document selector blocks snapshot replay. Only caller and callee + * walks stay in the ledger without one. + */ +export const isSourceEvidenceRetainable = ( operation: AnalysisOperation, descriptor: CapabilityDescriptor | undefined, parameters: Readonly>, @@ -96,15 +116,25 @@ export const isSnapshotCacheable = ( !STATEFUL_OPERATIONS.has(operation) && !RESOURCE_SCOPED_OPERATIONS.has(operation) && (!DOCUMENT_SCOPED_OPERATIONS.has(operation) || - typeof parameters.document === "string") && + typeof parameters.document === "string" || + DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS.has(operation)) && (!CURSOR_DEFAULT_OPERATIONS.has(operation) || typeof parameters.address === "string") && descriptor?.effects.mutatesArtifact === false && - descriptor.cachePolicy !== "live" && (descriptor.cachePolicy === "snapshot" || descriptor.effects.mayWriteFilesystem === false) && descriptor.effects.changesPermissions === false; +/** Whether an observation can additionally be replayed as a snapshot. */ +export const isSnapshotCacheable = ( + operation: AnalysisOperation, + descriptor: CapabilityDescriptor | undefined, + parameters: Readonly>, +): descriptor is CapabilityDescriptor => + isSourceEvidenceRetainable(operation, descriptor, parameters) && + replayDocumentSelected(operation, parameters) && + descriptor.cachePolicy !== "live"; + /** Windows Ghidra must admit the selected source through its native snapshot owner. */ export const requiresNativeSourceAdmissionBeforeReplay = ( providerId: string | undefined, diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 494f4f660b..9753cd4ac3 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -44,6 +44,7 @@ import { InvestigationRecords } from "../investigation/InvestigationRecords.js"; import { recordEvidenceSources } from "../investigation/recordEvidenceSources.js"; import { BinarySessionRecords } from "./BinarySessionRecords.js"; import { binarySessionStatus } from "./BinarySessionStatus.js"; +import { isSourceEvidenceRetainable } from "./AnalysisSnapshotCache.js"; import type { BinarySessionDetail } from "../../contracts/sessionStatusContract.js"; import { resolveSessionOpen, @@ -59,10 +60,11 @@ import { } from "./BinarySessionExecution.js"; import { closeAnalysisClient } from "../AnalysisClientCleanup.js"; import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; +import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { profilelessNativeDispatchProfile } from "./ProfilelessNativeWorkflowSnapshot.js"; import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js"; -import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js"; +import { ResourceSnapshotMismatchError } from "../../domain/evidenceErrors.js"; import { parseBinaryTarget, type BinaryTargetResolverOptions, @@ -175,6 +177,7 @@ export class BinarySession | { readonly run: SessionRun; readonly retainDocument: boolean; + readonly failures: Set; } | undefined; #transition: Promise = Promise.resolve(); @@ -715,7 +718,7 @@ export class BinarySession _path: string | undefined, signal: AbortSignal | undefined, operation: () => Promise>, - retirementFailure: ( + completionFailure: ( value: Result, error: AnalysisError, ) => AnalysisError, @@ -749,14 +752,47 @@ export class BinarySession const recovered = await cleanupRecovery( this.#externalMutationCleanupFailure, ); - if (!recovered.ok) return recovered; + if (!recovered.ok) + return err( + isAborted(signal) + ? analysisErrorWithCancellation( + recovered.error, + new AnalysisCancelledError(operationName, { + cause: signal?.reason, + }), + ) + : recovered.error, + ); if (this.#externalMutationCleanupFailure.cleanupIncomplete) - return err(this.#externalMutationCleanupFailure); + return err( + isAborted(signal) + ? analysisErrorWithCancellation( + this.#externalMutationCleanupFailure, + new AnalysisCancelledError(operationName, { + cause: signal?.reason, + }), + ) + : this.#externalMutationCleanupFailure, + ); this.#externalMutationCleanupFailure = undefined; } const pendingCleanup = await this.#retryPendingCleanup(); - if (!pendingCleanup.ok) return pendingCleanup; + if (!pendingCleanup.ok) + return err( + isAborted(signal) + ? analysisErrorWithCancellation( + pendingCleanup.error, + new AnalysisCancelledError(operationName, { + cause: signal?.reason, + }), + ) + : pendingCleanup.error, + ); + if (isAborted(signal)) + return err( + new AnalysisCancelledError(operationName, { cause: signal?.reason }), + ); const activeAtDispatch = this.#active; if (activeAtDispatch?.route.binding?.identity.id === "ghidra") @@ -774,10 +810,39 @@ export class BinarySession const value = await operation(); this.#externalMutationCleanupFailure = !value.ok && value.error.cleanupIncomplete ? value.error : undefined; - if (!retireTarget) return ok(value); - - const retired = await this.#closeActive({}, false); - if (!retired.ok) return err(retirementFailure(value, retired.error)); + if (retireTarget) { + const retired = await this.#closeActive({}, false); + if (!retired.ok) { + const primary = completionFailure(value, retired.error); + const failure = + retired.error.cleanupIncomplete && primary !== retired.error + ? analysisErrorWithCleanupFailure( + primary, + retired.error, + operationName, + ).bindCleanupDependencies([primary, retired.error]) + : primary; + return err( + isAborted(signal) + ? analysisErrorWithCancellation( + failure, + new AnalysisCancelledError(operationName, { + cause: signal?.reason, + }), + ) + : failure, + ); + } + } + if (isAborted(signal)) + return err( + completionFailure( + value, + new AnalysisCancelledError(operationName, { + cause: signal?.reason, + }), + ), + ); return ok(value); }); } @@ -958,6 +1023,7 @@ export class BinarySession this.#pendingCleanup = { run, retainDocument: options.retainDocument === true, + failures: new Set(), }; return this.#retryPendingCleanup( options.progress, @@ -983,10 +1049,19 @@ export class BinarySession { ...(progress === undefined ? {} : { progress }), ...(pending.retainDocument ? { retainDocument: true } : {}), - ...(onCleanupFailure === undefined ? {} : { onCleanupFailure }), + onCleanupFailure: (failure) => { + if (failure.cleanupIncomplete) pending.failures.add(failure); + onCleanupFailure?.(failure); + }, }, ); - if (closed.ok) this.#pendingCleanup = undefined; + if (closed.ok) { + for (const failure of pending.failures) + if (!failure.hasCleanupOwner) failure.resolveCleanup(); + this.#pendingCleanup = undefined; + } else if (closed.error.cleanupIncomplete) { + pending.failures.add(closed.error); + } return closed; } @@ -1074,21 +1149,13 @@ export class BinarySession override exportAnalysisSnapshot() { if (this.#activeBundleResources !== undefined) - return err( - new AnalysisSnapshotMismatchError( - "Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.", - ), - ); + return err(new ResourceSnapshotMismatchError("export")); return super.exportAnalysisSnapshot(); } override importAnalysisSnapshot(snapshot: AnalysisSnapshot) { if (this.#activeBundleResources !== undefined) - return err( - new AnalysisSnapshotMismatchError( - "Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.", - ), - ); + return err(new ResourceSnapshotMismatchError("import")); return super.importAnalysisSnapshot(snapshot); } @@ -1104,7 +1171,7 @@ export class BinarySession execute( name: Parameters[0], arguments_: Readonly>, - options?: Pick, + options?: ExecutionOptions, ): Promise> { const generation = this.#transitionGeneration; const call = this.#execute(name, arguments_, options, this.#transition); @@ -1116,7 +1183,7 @@ export class BinarySession executeIfProviderReady( name: Parameters[0], arguments_: Readonly>, - options?: Pick, + options?: Pick, ): Promise | undefined> { const active = this.#active; if ( @@ -1175,7 +1242,7 @@ export class BinarySession async #execute( name: Parameters[0], arguments_: Readonly>, - options: Pick | undefined, + options: ExecutionOptions | undefined, transition: Promise, ): Promise> { const transitioned = await this.#waitForTransition( @@ -1255,7 +1322,11 @@ export class BinarySession ); if (!sourcesRecorded.ok) return sourcesRecorded; } - if (execution.ok && cacheable && profile !== undefined) { + if ( + execution.ok && + isSourceEvidenceRetainable(name, capability, arguments_) && + profile !== undefined + ) { const evidence = createAnalysisExecutionEvidence( name, arguments_, @@ -1272,7 +1343,7 @@ export class BinarySession const recorded = this.recordEvidence(evidence); if (!recorded.ok) return recorded; const retained = { ...execution.value, evidenceId: evidence.evidence_id }; - if (execution.value.cachePolicy !== "live") + if (cacheable && execution.value.cachePolicy !== "live") this.recordSnapshot({ target: active.target, profile, diff --git a/src/application/binary/BinarySessionBundleResources.test.ts b/src/application/binary/BinarySessionBundleResources.test.ts index 506ec45635..8c07ef83ca 100644 --- a/src/application/binary/BinarySessionBundleResources.test.ts +++ b/src/application/binary/BinarySessionBundleResources.test.ts @@ -14,7 +14,10 @@ import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js"; import { BinarySession } from "./BinarySession.js"; import { SessionProviderRouter } from "./SessionProviderRouter.js"; import { bundleResourceRootSchema } from "../../domain/apple/bundleResourceRoot.js"; -import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { analysisErrorRemediationAction } from "../../domain/analysisErrorPresentation.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; +import { ResourceSnapshotMismatchError } from "../../domain/evidenceErrors.js"; import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js"; import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; @@ -36,6 +39,24 @@ const binaryIdentity: ProviderIdentity = { }; const temporaryDirectories: string[] = []; +const expectResourceSnapshotRefusal = ( + error: AnalysisError, + operation: "export" | "import", +): void => { + expect(error).toBeInstanceOf(ResourceSnapshotMismatchError); + expect(error).toMatchObject({ + _tag: "EvidenceIntegrityError", + operation, + }); + const projected = projectAnalysisError(error); + expect(projected.code).toBe("evidence_integrity_mismatch"); + expect(projected.category).toBe("integrity_mismatch"); + expect(projected.retryable).toBe(false); + expect(projected.remediation).toEqual({ + action: analysisErrorRemediationAction(error), + }); +}; + afterEach(async () => { await Promise.all( temporaryDirectories @@ -137,13 +158,17 @@ it("opens a resource-only bundle without executable identity and keeps snapshot resource_root: { canonical_path: opened.value.canonicalPath }, }); const snapshot = session.exportAnalysisSnapshot(); - expect(snapshot.ok).toBe(false); - if (!snapshot.ok) - expect(snapshot.error).toBeInstanceOf(AnalysisSnapshotMismatchError); + if (snapshot.ok) + throw new Error("resource session exported a binary snapshot"); + expectResourceSnapshotRefusal(snapshot.error, "export"); const imported = session.importAnalysisSnapshot({} as AnalysisSnapshot); - expect(imported.ok).toBe(false); - if (!imported.ok) - expect(imported.error).toBeInstanceOf(AnalysisSnapshotMismatchError); + if (imported.ok) + throw new Error("resource session imported a binary snapshot"); + expectResourceSnapshotRefusal(imported.error, "import"); + expect(await session.status()).toMatchObject({ + open: true, + session_kind: "bundle_resources", + }); const closedResult = await session.close(); expect(closedResult.ok).toBe(true); expect(closed).toBe(true); diff --git a/src/application/binary/BinarySessionLiveness.test.ts b/src/application/binary/BinarySessionLiveness.test.ts index 72c1c56f28..a39c4ebec1 100644 --- a/src/application/binary/BinarySessionLiveness.test.ts +++ b/src/application/binary/BinarySessionLiveness.test.ts @@ -73,6 +73,47 @@ it("preserves an explicitly unbound provider observation", async () => { } }); +it("retains live source Evidence without replaying a prior observation", async () => { + const { session, deep } = openSession({ live: true }); + try { + expect((await session.open(process.execPath)).ok).toBe(true); + expect( + ( + await session.execute("address_name", { + document: "0", + address: "0x9999", + }) + ).ok, + ).toBe(true); + const child = deep.created[0]; + if (child === undefined) throw new Error("expected provider child"); + let reads = 0; + child.execute = () => + Promise.resolve( + ok(createAnalysisExecution(`live ${++reads}`, deepIdentity)), + ); + const parameters = { document: "0", address: "0x1000" }; + const first = await session.execute("address_name", parameters); + const second = await session.execute("address_name", parameters); + expect(first.ok && first.value.result).toBe("live 1"); + expect(second.ok && second.value.result).toBe("live 2"); + if (!first.ok || !second.ok) return; + expect(first.value.evidenceId).toBeDefined(); + expect(second.value.evidenceId).toBeDefined(); + expect(first.value.evidenceId).not.toBe(second.value.evidenceId); + expect(session.exportEvidenceBundle().records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ normalized_result: "live 1" }), + expect.objectContaining({ normalized_result: "live 2" }), + ]), + ); + const snapshot = session.exportAnalysisSnapshot(); + if (snapshot.ok) expect(snapshot.value.entries).toHaveLength(0); + } finally { + await session.close(); + } +}); + it("keeps unbound observations out of persisted target snapshots", async () => { const { session, deep } = openSession(); const directory = await createTestTempDirectory("rea-unbound-snapshot-"); @@ -458,6 +499,7 @@ const openSession = ( failOperationInitially?: boolean; beforeFirstClose?: () => Promise; explicitNullSubject?: boolean; + live?: boolean; } = {}, ): { session: BinarySession; @@ -493,6 +535,7 @@ const killableDeepProvider = (options: { failOperationInitially?: boolean; beforeFirstClose?: () => Promise; explicitNullSubject?: boolean; + live?: boolean; }): { provider: AnalysisProviderCandidate; created: KillableChild[]; @@ -503,7 +546,12 @@ const killableDeepProvider = (options: { const withOperationHealth = options.operationHealth !== false; const provider: AnalysisProviderCandidate = { identity: () => deepIdentity, - capabilities: () => [capability(deepIdentity, "address_name")], + capabilities: () => [ + { + ...capability(deepIdentity, "address_name"), + ...(options.live ? { cachePolicy: "live" as const } : {}), + }, + ], inspectAvailability: (): ProviderAvailability => ({ status: "available", code: null, diff --git a/src/application/binary/BinarySessionPort.ts b/src/application/binary/BinarySessionPort.ts index 12e46f26a6..6796ece3d7 100644 --- a/src/application/binary/BinarySessionPort.ts +++ b/src/application/binary/BinarySessionPort.ts @@ -47,7 +47,7 @@ export interface BinarySessionPort _path: string | undefined, signal: AbortSignal | undefined, operation: () => Promise>, - retirementFailure: ( + completionFailure: ( value: Result, error: AnalysisError, ) => AnalysisError, diff --git a/src/application/binary/BundleResourceSession.ts b/src/application/binary/BundleResourceSession.ts index 2e11804af9..cecca90a94 100644 --- a/src/application/binary/BundleResourceSession.ts +++ b/src/application/binary/BundleResourceSession.ts @@ -24,6 +24,13 @@ export const BUNDLE_RESOURCE_OPERATIONS = new Set([ ...BUNDLE_ARTIFACT_RESOURCE_OPERATIONS, ]); +/** Share the dispatch owner's resource-session operation policy with discovery. */ +export const isBundleResourceOperation = (name: string): boolean => { + for (const operation of BUNDLE_RESOURCE_OPERATIONS) + if (operation === name) return true; + return false; +}; + /** Provider-specific owner of the four read-only app-resource operations. */ export interface BundleResourceClientFactory { createClient( diff --git a/src/application/environment/environmentStateBulkAnswer.ts b/src/application/environment/environmentStateBulkAnswer.ts index 434cc9fdd9..b0d0894a6b 100644 --- a/src/application/environment/environmentStateBulkAnswer.ts +++ b/src/application/environment/environmentStateBulkAnswer.ts @@ -37,6 +37,7 @@ import { observedEntity, pathValues, sortedOrder, + storedUpdate, unloadable, heldEntity, updateTarget, @@ -210,12 +211,16 @@ export const answerBulk = ( ); if (!answer.ok) return answer; if (update) { + const stored = written.value.map((entity) => ({ + ...entity, + fields: storedUpdate(serving, plan, entity.fields), + })); const updated = serving.store.updateAll( - written.value, + stored, sortedOrder( serving, plan, - written.value.map((after, index) => ({ + stored.map((after, index) => ({ before: targets.value[index]!.entity, after, })), diff --git a/src/application/environment/environmentStateItemWritesAnswer.test.ts b/src/application/environment/environmentStateItemWritesAnswer.test.ts new file mode 100644 index 0000000000..fdc21025c5 --- /dev/null +++ b/src/application/environment/environmentStateItemWritesAnswer.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; + +import { + ADDED_EMPTY_REFERENCE_ROWS, + BOOKMARK_TOGGLE_ROWS, + ITEM_LOCATIONS, + ITEM_REFERENCES_ROWS, + ITEM_TAGS, +} from "../../../tests/fixtures/environmentStateItemWrites.js"; +import { FILED_ROWS } from "../../../tests/fixtures/environmentStateDerivedValues.js"; +import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js"; + +describe("served item writes", () => { + it("answers a create with the parent and tags its ids name, in the order sent", () => { + const items = servedFrom(ITEM_REFERENCES_ROWS); + const body = items.answer("post_items", { + body: { + name: "Snow shovel", + parentId: ITEM_LOCATIONS[0]!["id"]!, + tagIds: [ITEM_TAGS[1]!["id"]!, ITEM_TAGS[0]!["id"]!], + }, + }).body; + expect(body).toMatchObject({ + name: "Snow shovel", + parent: ITEM_LOCATIONS[0], + tags: [ITEM_TAGS[1], ITEM_TAGS[0]], + }); + }); + + it("answers a create naming no parent and no tags with none", () => { + const items = servedFrom(ITEM_REFERENCES_ROWS); + expect( + items.answer("post_items", { body: { name: "Spare fuse" } }).body, + ).toMatchObject({ + name: "Spare fuse", + parent: null, + tags: [], + }); + }); + + it("answers a create sending no parent and no tags as a recorded create sent them", () => { + const items = servedFrom(ITEM_REFERENCES_ROWS); + expect( + items.answer("post_items", { + body: { name: "Spare fuse", parentId: null, tagIds: [] }, + }).body, + ).toMatchObject({ name: "Spare fuse", parent: null, tags: [] }); + }); + + it("answers a create leaving out a parent the first create left out with none, and one sending null with null", () => { + const items = servedFrom(ADDED_EMPTY_REFERENCE_ROWS); + const left = items.answer("post_items", { body: { name: "Spare fuse" } }) + .body as Record; + expect(left).toMatchObject({ name: "Spare fuse" }); + expect(Object.hasOwn(left, "parent")).toBe(false); + expect( + items.answer("post_items", { + body: { name: "Spare bulb", parentId: null }, + }).body, + ).toMatchObject({ name: "Spare bulb", parent: null }); + expect( + items.answer("post_items", { + body: { name: "Spare cord", parentId: ITEM_LOCATIONS[0]!["id"]! }, + }).body, + ).toMatchObject({ name: "Spare cord", parent: ITEM_LOCATIONS[0] }); + }); + + it("refuses a create naming no shelf when every recorded create named one", () => { + const books = servedFrom(FILED_ROWS); + expect( + books.refusal("post_books", { body: { name: "Loose", shelf_id: null } }), + ).toMatchObject({ message: expect.stringContaining("shelf_id") }); + }); + + it("refuses a create naming a tag the environment does not hold", () => { + const items = servedFrom(ITEM_REFERENCES_ROWS); + expect( + items.refusal("post_items", { + body: { + name: "Mystery", + tagIds: ["3b9f2a10-0009-4000-8000-000000000009"], + }, + }), + ).toMatchObject({ message: expect.stringContaining("tagIds") }); + }); + + it("flips an entry's flag on each body-less call", () => { + const entries = servedFrom(BOOKMARK_TOGGLE_ROWS); + const starred = () => + ( + entries.answer("get_entries_by_id", { id: "2" }).body as { + starred: boolean; + } + ).starred; + expect(starred()).toBe(false); + entries.answer("put_entries_bookmark_by_id", { id: "2" }); + expect(starred()).toBe(true); + entries.answer("put_entries_bookmark_by_id", { id: "2" }); + expect(starred()).toBe(false); + }); +}); diff --git a/src/application/environment/environmentStateVariedCallsAnswer.test.ts b/src/application/environment/environmentStateVariedCallsAnswer.test.ts new file mode 100644 index 0000000000..5137265a18 --- /dev/null +++ b/src/application/environment/environmentStateVariedCallsAnswer.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; + +import { + DONE_CLOCK_ROWS, + FEWER_MEMBERS_ROWS, + NUMBERED_THEN_BLANK_ROWS, + NUMBERED_UNLESS_SENT_ROWS, + OWNER_CLEARED_THEN_NAMED_ROWS, + OWNER_NAMED_THEN_CLEARED_ROWS, + PUT_CLEARS_ROWS, +} from "../../../tests/fixtures/environmentStateVariedCalls.js"; +import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js"; + +describe("served writes whose calls send different members", () => { + it("numbers a created bundle that leaves its order out, and keeps one a call sends", () => { + const bundles = servedFrom(NUMBERED_UNLESS_SENT_ROWS); + expect( + bundles.answer("post_bundles", { body: { name: "Echo" } }).body, + ).toMatchObject({ name: "Echo", order: 2 }); + expect( + bundles.answer("post_bundles", { body: { name: "Foxtrot", order: 9 } }) + .body, + ).toMatchObject({ + name: "Foxtrot", + order: 9, + }); + }); + + it("numbers past an order a call sent, even once that bundle is gone", () => { + const bundles = servedFrom(NUMBERED_UNLESS_SENT_ROWS); + bundles.answer("post_bundles", { body: { name: "Echo" } }); + bundles.answer("post_bundles", { body: { name: "Foxtrot", order: 9 } }); + bundles.store.removeAll( + bundles.store + .ofType("/api/bundles") + .filter((entity) => entity.fields["order"] === 9), + ); + expect( + bundles.answer("post_bundles", { body: { name: "Golf" } }).body, + ).toMatchObject({ name: "Golf", order: 10 }); + }); + + it("clears a member a PUT leaves out, as the application did, and keeps one it sends", () => { + const tags = servedFrom(PUT_CLEARS_ROWS); + expect( + tags.answer("put_tags_by_id", { id: "2", body: { name: "Receipts" } }) + .body, + ).toMatchObject({ + name: "Receipts", + owner: null, + }); + expect(tags.answer("get_tags_by_id", { id: "2" }).body).toMatchObject({ + owner: null, + }); + expect( + tags.answer("put_tags_by_id", { + id: "1", + body: { name: "Mail", owner: 3 }, + }).body, + ).toMatchObject({ + name: "Mail", + owner: 3, + }); + }); + + it.each([ + ["named first", OWNER_NAMED_THEN_CLEARED_ROWS], + ["cleared first", OWNER_CLEARED_THEN_NAMED_ROWS], + ])( + "clears a referenced owner a PUT leaves out, and stores the one a PUT names by its id: %s", + (_order, rows) => { + const tags = servedFrom(rows); + expect( + tags.answer("put_tags_by_id", { id: "1", body: { name: "Receipts" } }) + .body, + ).toMatchObject({ name: "Receipts", owner: null }); + expect( + tags.answer("put_tags_by_id", { + id: "2", + body: { name: "Mail", ownerId: 4 }, + }).body, + ).toMatchObject({ name: "Mail", owner: { id: 4, name: "Bo" } }); + }, + ); + + it("answers an update with its clock spelled as the recorded answer and stores it as reads spell it", () => { + const tasks = servedFrom(DONE_CLOCK_ROWS); + const answered = tasks.answer("patch_tasks_by_id", { + id: "1", + body: { done: true }, + }).body as { done_at: string }; + expect(answered.done_at).toMatch(/\.\d{9}Z$/u); + const read = tasks.answer("get_tasks_by_id", { id: "1" }).body as { + done_at: string; + }; + expect(read.done_at).toMatch(/:\d{2}Z$/u); + }); + + it("serves an update that sends only the member every recorded update sent", () => { + const contacts = servedFrom(FEWER_MEMBERS_ROWS); + expect( + contacts.answer("put_contacts_by_id", { + id: "1", + body: { email: "ada.f@example.test" }, + }).body, + ).toMatchObject({ name: "Ada Finch", email: "ada.f@example.test" }); + expect( + contacts.refusal("put_contacts_by_id", { + id: "2", + body: { city: "Kumasi" }, + }), + ).toMatchObject({ + message: expect.stringContaining("email"), + }); + }); + + it("numbers a created bundle that leaves its order out, though a recorded create sent it empty", () => { + const bundles = servedFrom(NUMBERED_THEN_BLANK_ROWS); + expect( + bundles.answer("post_bundles", { body: { name: "Echo" } }).body, + ).toMatchObject({ name: "Echo", order: "2" }); + }); +}); diff --git a/src/application/environment/environmentStateWriteAnswer.ts b/src/application/environment/environmentStateWriteAnswer.ts index fb997d84f0..895512e342 100644 --- a/src/application/environment/environmentStateWriteAnswer.ts +++ b/src/application/environment/environmentStateWriteAnswer.ts @@ -23,6 +23,7 @@ import { nestedClockValue, nextMadeCount, unenveloped, + readSpelledClocks, } from "../../domain/analysisView/environmentStateWriteStamps.js"; import { anyLinkedSource, @@ -41,6 +42,7 @@ import { generatedIntegerDigits, seededInteger, seededLike, + sequenceNumber, spelledLike, startedSequence, } from "../../domain/analysisView/environmentStateWriteValues.js"; @@ -230,6 +232,12 @@ export const generator = ( ): { readonly forTarget: (target: GeneratedTarget) => GenerateValue; readonly taken: Map; + /** Count a number the call sent for a sequence, as replay does, so later generated ones follow it. */ + readonly sentSequence: ( + target: string, + rule: Extract, + value: JsonValue | undefined, + ) => void; } => { const { store } = serving; const write = store.pendingWrite(); @@ -239,6 +247,11 @@ export const generator = ( taken.set(key, value); return value; }; + const sequenceKey = ( + rule: Extract, + target: string, + ) => + `${request.tool.name} ${rule.field} ${rule.scope === "collection" ? target : ""}`; const ofType = store.ofType(request.plan.target ?? ""); // A sequence never reuses a value the state started with, even after its entity was deleted. const started = store.startedWith(request.plan.target ?? ""); @@ -291,7 +304,7 @@ export const generator = ( spelledLike( exemplar, sequence( - `${request.tool.name} ${rule.field} ${rule.scope === "collection" ? target.name : ""}`, + sequenceKey(rule, target.name), startedSequence( nextHeld( rule.scope === "collection" @@ -361,7 +374,17 @@ export const generator = ( ); } }; - return { forTarget: (target) => (rule) => generate(target, rule), taken }; + return { + forTarget: (target) => (rule) => generate(target, rule), + taken, + sentSequence: (target, rule, value) => { + const number = sequenceNumber(value); + if (number === undefined) return; + const key = sequenceKey(rule, target); + // commitWrite keeps the larger of this and the values taken before. + taken.set(key, Math.max(taken.get(key) ?? number, number)); + }, + }; }; /** A write's answer on its recorded response's status and content type: a rendered body, none, or the recorded one. */ @@ -556,6 +579,17 @@ const answerCreate = ( idField, ); if (!fields.ok) return err(refused(tool, fields.error)); + for (const rule of plan.rules) + if ( + rule.rule === "sequence" && + rule.sent === true && + Object.hasOwn(call.body ?? {}, rule.field) + ) + values.sentSequence( + collection, + rule, + ownMember(fields.value, rule.field), + ); const field = unloadableMember(fields.value); if (field !== undefined) return err(unloadable(tool, `Field ${field}`)); const id = scalarText(ownMember(fields.value, idField)); @@ -665,7 +699,10 @@ const answerUpdate = ( }, ); if (!answer.ok) return answer; - const after = updatedEntity(entity, written.value); + const after = updatedEntity( + entity, + storedUpdate(serving, plan, written.value), + ); const updated = serving.store.updateAll( [after], sortedOrder(serving, plan, [{ before: entity, after }]), @@ -675,6 +712,20 @@ const answerUpdate = ( return answer; }; +/** An update's fields as the state stores them: its clocks spelled as the plan's read-back spelled them. */ +export const storedUpdate = ( + serving: StateServing, + plan: StateWritePlan, + written: JsonObject, +): JsonObject => + readSpelledClocks( + plan.rules, + written, + plan.observation?.read === undefined + ? undefined + : recordedObject(serving, plan.observation.read), + ); + /** The collection's members in the order a sorted collection holds them after these writes; empty when the plan keeps them in place. */ export const sortedOrder = ( serving: StateServing, diff --git a/src/application/javascript/JavaScriptSemanticGraphAssignmentProjection.ts b/src/application/javascript/JavaScriptSemanticGraphAssignmentProjection.ts index 8c7d48ff47..748b6ec490 100644 --- a/src/application/javascript/JavaScriptSemanticGraphAssignmentProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphAssignmentProjection.ts @@ -1,6 +1,7 @@ import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js"; import { createSemanticNodeRangeLookup } from "./JavaScriptSemanticGraphProjection.js"; import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import { addSemanticGraphRelation, addSemanticGraphUnknown, @@ -13,6 +14,11 @@ export function* projectSemanticAssignments( ): Generator { const readsWithin = createSemanticNodeRangeLookup(context.readNodes); const writesWithin = createSemanticNodeRangeLookup(context.writeNodes); + const expressions = new Set( + context.ir.expressionFlows.map((flow) => + javascriptSourceRangeKey(flow.location), + ), + ); for (const [index, flow] of context.ir.assignmentFlows.entries()) { if (index % 64 === 0) yield; const owner = @@ -22,9 +28,11 @@ export function* projectSemanticAssignments( const writes = writesWithin(flow.target).filter((node) => sourceRangesEqual(node.identity.source_range, flow.target), ); - const reads = readsWithin(flow.source).filter( - (node) => node.function_node_id === owner, - ); + const reads = expressions.has(javascriptSourceRangeKey(flow.source)) + ? [] + : readsWithin(flow.source).filter( + (node) => node.function_node_id === owner, + ); // Compound assignments consume the previous value as well as the RHS. if (flow.operator !== "=") reads.push( diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts index 51f724ebeb..d9819c750b 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts @@ -31,7 +31,7 @@ import { getterExpansionSource } from "../../../tests/fixtures/javascriptGetterE const SHA256 = "a".repeat(64); -it("retains direct call literals and resolves literal queries to their call site", () => { +it("retains direct call literals and resolves literal queries to their argument", () => { const source = 'api.rpc("proc_0", 7, false, null, dynamic, ...values, `fixed`, -3);'; const graph = graphFor(source); @@ -57,7 +57,10 @@ it("retains direct call literals and resolves literal queries to their call site direction: "forward-influence", }), ); - expect(trace.seed_node_ids).toContain(call.node_id); + expect( + trace.nodes.find((node) => node.node_id === trace.seed_node_ids[0])?.label, + ).toBe("argument 0"); + expect(trace.nodes.some((node) => node.node_id === call.node_id)).toBe(true); }); it("reports the primitive byte bound when a direct argument is too large", () => { diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index fb9d4830d4..bb14d397f3 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -14,9 +14,11 @@ import { } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import type { JavaScriptSemanticCallArgument, + JavaScriptSemanticCallReturnFlow, JavaScriptSemanticIr, } from "../../domain/javascript/javascriptSemanticIr.js"; import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { @@ -31,6 +33,7 @@ import { } from "./JavaScriptSemanticGraphDataProjection.js"; import { projectSemanticResources } from "./JavaScriptSemanticGraphResourceProjection.js"; import { projectSemanticAssignments } from "./JavaScriptSemanticGraphAssignmentProjection.js"; +import { projectSemanticExpressionInfluence } from "./JavaScriptSemanticGraphExpressionProjection.js"; import { projectSemanticObjects } from "./JavaScriptSemanticGraphObjectProjection.js"; import { projectSemanticValues } from "./JavaScriptSemanticGraphValueProjection.js"; import { @@ -935,6 +938,11 @@ function* projectDefinitionsAndReferences( } const projectCalls = (context: FileContext): void => { + const expressions = new Set( + context.ir.expressionFlows.map((flow) => + javascriptSourceRangeKey(flow.location), + ), + ); for (const call of context.ir.callSites) { const callNode = context.callSiteNodes.get(call.callSiteId); if (callNode === undefined) continue; @@ -959,7 +967,40 @@ const projectCalls = (context: FileContext): void => { `${call.callSiteId}\u0000${String(argument.index)}`, argumentNode, ); - const references = context.referenceNodesWithin(argument.location); + if (!argument.spread && Object.hasOwn(argument, "literal")) + addRelation(context.state, { + source: argumentNode, + target: callNode, + relation: "influences", + resolution: "resolved", + }); + if ( + call.resolution !== "exact" && + call.resultUsed && + argumentNode !== null + ) + addSemanticGraphUnknown(context.state, { + node_id: argumentNode.node_id, + family: "data-flow", + relation_kinds: ["influences"], + reason: "dynamic-call", + detail: + "The dynamic call's return dependency on this argument is unknown.", + candidate_node_ids: [], + evidence: unknownSemanticEvidence(context.file, argument.location), + }); + const references = + argument.valueExpressionLocation !== null && + expressions.has( + javascriptSourceRangeKey(argument.valueExpressionLocation), + ) + ? [] + : context + .referenceNodesWithin(argument.location) + .filter( + (reference) => + reference.function_node_id === callNode.function_node_id, + ); for (const reference of references) addRelation(context.state, { source: reference, @@ -994,20 +1035,46 @@ const projectCalls = (context: FileContext): void => { resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate", }); } - for (const flow of context.ir.callReturnFlows) + const returnFlowsByCallSite = new Map< + string, + JavaScriptSemanticCallReturnFlow[] + >(); + for (const flow of context.ir.callReturnFlows) { + const grouped = returnFlowsByCallSite.get(flow.callSiteId); + if (grouped === undefined) + returnFlowsByCallSite.set(flow.callSiteId, [flow]); + else grouped.push(flow); addRelation(context.state, { source: context.returnSiteNodes.get(flow.returnSiteId), target: context.callSiteNodes.get(flow.callSiteId), relation: "returns-to-call", resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate", }); - for (const flow of context.ir.callResultFlows) - addRelation(context.state, { - source: context.callSiteNodes.get(flow.callSiteId), - target: context.bindingNodes.get(flow.bindingId), - relation: "aliases", - resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate", - }); + } + for (const flow of context.ir.callResultFlows) { + const resolution = + context.callResolutions.get(flow.callSiteId) ?? "candidate"; + const target = context.bindingNodes.get(flow.bindingId); + const returns = returnFlowsByCallSite.get(flow.callSiteId) ?? []; + // A literal argument may influence the invocation. The assigned value + // comes from modeled returns, so an ignored parameter does not reach it. + if (returns.length === 0) { + addRelation(context.state, { + source: context.callSiteNodes.get(flow.callSiteId), + target, + relation: "aliases", + resolution, + }); + continue; + } + for (const item of returns) + addRelation(context.state, { + source: context.returnSiteNodes.get(item.returnSiteId), + target, + relation: "aliases", + resolution, + }); + } projectSemanticReturnValues(context); }; @@ -1025,6 +1092,14 @@ const createArgumentNode = ( ? "spread argument" : `argument ${String(argument.index)}`, functionNodeId: callNode.function_node_id, + ...(!argument.spread && Object.hasOwn(argument, "literal") + ? { + properties: { + literal_argument_value: argument.literal ?? null, + literal_argument_call_site_node_id: callNode.node_id, + }, + } + : {}), }); /** Per-file projection passes, run in this order with a yield between each. */ @@ -1035,6 +1110,7 @@ const FILE_PROJECTION_PASSES: readonly (( projectSemanticObjects, projectSemanticAssignments, projectCalls, + projectSemanticExpressionInfluence, projectSemanticPromises, projectSemanticEvents, projectSemanticTimers, diff --git a/src/application/javascript/JavaScriptSemanticGraphExpressionInfluence.test.ts b/src/application/javascript/JavaScriptSemanticGraphExpressionInfluence.test.ts new file mode 100644 index 0000000000..11c36243c2 --- /dev/null +++ b/src/application/javascript/JavaScriptSemanticGraphExpressionInfluence.test.ts @@ -0,0 +1,356 @@ +import { expect, it } from "vitest"; +import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js"; +import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; +import { resolveJavaScriptSemanticQuerySeeds } from "../../domain/javascript/javascriptSemanticQueryIdentity.js"; +import { javaScriptSemanticQueryInputSchema } from "../../domain/javascript/javascriptSemanticQuerySchemas.js"; +import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js"; +import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js"; +import { completeSemanticSteps } from "../../domain/javascript/javascriptSemanticTraversal.js"; + +const trace = (body: string) => { + const graph = graphForJavaScript( + `const base = "https://example.test"; ${body}`, + ); + return queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "https://example.test" }, + direction: "forward-influence", + }), + ); +}; + +it.each([ + ["declaration", 'const url = base + "/x"; fetch(url);'], + ["assignment", 'let url; url = base + "/x"; fetch(url);'], + ["nested operator", 'const url = (base + "/x") + "/y"; fetch(url);'], + ["call argument", 'fetch(base + "/x");'], + ["template", "fetch(`${base}/x`);"], +])("follows transformed input to a %s without opt-in", (_name, body) => { + const result = trace(body); + expect(result.nodes.map((node) => node.label)).toContain("argument 0"); + expect(result.relations.some((edge) => edge.relation === "influences")).toBe( + true, + ); +}); + +it("explains an unmodeled dynamic call result at the reached argument", () => { + const result = trace('const url = wrap(base + "/x"); fetch(url);'); + expect(result.unknowns.map((unknown) => unknown.detail)).toContain( + "The dynamic call's return dependency on this argument is unknown.", + ); +}); + +it.each([ + 'const obj = { url: wrap(base + "/x") }; fetch(obj.url);', + 'function address() { return wrap(base + "/x"); }', + 'consume(wrap(base + "/x"));', +])("keeps nested dynamic call return influence unknown: %s", (body) => { + const result = trace(body); + expect(result.status).not.toBe("found"); + expect(result.unknowns.map((unknown) => unknown.detail)).toContain( + "The dynamic call's return dependency on this argument is unknown.", + ); +}); + +it("does not report an unused dynamic call result as a missing value", () => { + const result = trace('consume(base + "/x");'); + expect( + result.unknowns.some( + (unknown) => + unknown.detail === + "The dynamic call's return dependency on this argument is unknown.", + ), + ).toBe(false); +}); + +it("bounds operator production before graph admission and reports truncation", () => { + const expressions = Array.from({ length: 500 }, () => "${base}").join(""); + const source = `const base = "value"; const value = \`${expressions}\`;`; + const parsed = parseJavaScriptSource(source, "app.js"); + if (parsed === null) throw new Error("Expected parsed source"); + const ir = completeSemanticSteps( + analyzeParsedJavaScriptSemanticsSteps(parsed, "import", 4), + ); + expect(ir.expressionFlowsTruncated).toBe(true); + expect(ir.expressionFlows.length).toBeLessThanOrEqual(4); + expect( + ir.expressionFlows.reduce((count, flow) => count + flow.operands.length, 0), + ).toBeLessThanOrEqual(4); + expect(ir.coverage).toMatchObject({ status: "partial", omittedCount: null }); + expect(graphForJavaScript(source, ir).coverage.truncated).toBe(true); +}); + +it("follows a transformed return expression", () => { + const result = trace('function address() { return base + "/x"; }'); + expect(result.nodes.map((node) => node.label)).toContain("return"); +}); + +it("does not treat a deferred callback body as an enclosing call argument value", () => { + const graph = graphForJavaScript( + 'const base = "https://example.test"; consume(() => base + "/x");', + ); + const base = graph.nodes.find( + (node) => node.kind === "binding" && node.label === "base", + ); + const argument = graph.nodes.find( + (node) => node.kind === "expression" && node.label === "argument 0", + ); + expect(base).toBeDefined(); + expect(argument).toBeDefined(); + const result = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "https://example.test" }, + direction: "forward-influence", + include_ambiguous_dynamic_edges: true, + }), + ); + expect(result.nodes.map((node) => node.node_id)).not.toContain( + argument?.node_id, + ); +}); + +it.each(['"/unique"', "`/unique`"])( + "seeds an exact literal operand %s and reaches the receiving argument", + (operand) => { + const graph = graphForJavaScript( + `const prefix = "root"; const path = prefix + ${operand}; fetch(path);`, + ); + const result = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "/unique" }, + direction: "forward-influence", + }), + ); + expect(result.nodes.map((node) => node.label)).toContain("argument 0"); + }, +); + +it("marks a short-circuited operand as conditional without claiming alias identity", () => { + const graph = graphForJavaScript( + 'const base = "unique"; const maybe = ready && base; fetch(maybe);', + ); + const conditional = graph.relations.filter( + (edge) => + edge.relation === "influences" && edge.properties.conditional === true, + ); + expect(conditional).toHaveLength(1); + expect( + graph.nodes.find((node) => node.node_id === conditional[0]?.source_node_id) + ?.label, + ).toBe("base"); +}); + +it("reports uncertainty on a read inside an unsupported operand", () => { + const result = trace('const url = format(base) + "/x"; fetch(url);'); + expect(result.status).toBe("partial"); + expect( + result.unknowns.some((unknown) => + unknown.detail.includes("unmodeled operator operand"), + ), + ).toBe(true); +}); + +it.each([ + 'function answer() { return "return-only"; }', + 'const answer = () => "return-only";', +])("seeds a literal returned directly: %s", (source) => { + const graph = graphForJavaScript(source); + const result = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "return-only" }, + direction: "forward-influence", + }), + ); + expect(result.summary.total_seed_matches).toBeGreaterThan(0); + expect(result.nodes.map((node) => node.label)).toContain("return"); +}); + +it("marks a logical assignment's transformed input conditional", () => { + const graph = graphForJavaScript( + 'const base = "unique"; let value; value ||= base + "/x";', + ); + const conditional = graph.relations.find( + (edge) => + edge.relation === "influences" && + edge.properties.conditional === true && + graph.nodes.find((node) => node.node_id === edge.target_node_id) + ?.label === "value", + ); + expect(conditional).toBeDefined(); +}); + +it("keeps sequence side effects unknown instead of attributing earlier values directly", () => { + const result = trace('const url = (touch(base), "fixed"); fetch(url);'); + expect(result.status).toBe("partial"); + expect( + result.unknowns.some((unknown) => + unknown.detail.includes("unmodeled operator operand"), + ), + ).toBe(true); +}); + +it("seeds a direct argument at its occurrence and reaches the call", () => { + const graph = graphForJavaScript('consume("only-direct");'); + const result = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "only-direct" }, + direction: "forward-influence", + }), + ); + expect(result.summary.total_seed_matches).toBe(1); + expect( + result.nodes.find((node) => node.node_id === result.seed_node_ids[0]) + ?.label, + ).toBe("argument 0"); + expect(result.nodes.some((node) => node.kind === "call-site")).toBe(true); +}); + +it("keeps older call-site literal evidence searchable when argument literals are absent", () => { + const graph = graphForJavaScript('consume("only-direct");'); + const legacy = { + ...graph, + nodes: graph.nodes.filter( + (node) => + node.properties.literal_argument_call_site_node_id === undefined, + ), + }; + const seeds = resolveJavaScriptSemanticQuerySeeds( + legacy, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "only-direct" }, + direction: "forward-influence", + }), + ); + const call = graph.nodes.find((node) => node.kind === "call-site"); + expect(seeds).toEqual([call?.node_id]); +}); + +it("does not treat literals inside a spread or deferred callback as direct arguments", () => { + const graph = graphForJavaScript( + 'consume(...["spread-only"], () => "deferred-only");', + ); + for (const value of ["spread-only", "deferred-only"]) { + const result = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value }, + direction: "forward-influence", + }), + ); + expect( + result.nodes.filter( + (node) => + node.properties.literal_argument_call_site_node_id !== undefined, + ), + ).toEqual([]); + } +}); + +it.each([ + 'const obj = { url: base + "/x" }; fetch(obj.url);', + 'const values = [base + "/x"]; fetch(values[0]);', + 'const url = wrap(base + "/x"); fetch(url);', +])("reports unknown when a consumer boundary is unmodeled: %s", (body) => { + const result = trace(body); + expect(result.status).not.toBe("found"); + expect(result.unknowns.length).toBeGreaterThan(0); +}); + +it("does not reuse an outer literal for the same text in a nested function", () => { + const graph = graphForJavaScript( + 'const suffix = "end"; const value = true ? "token" : () => "token" + suffix;', + ); + const tokens = graph.nodes.filter( + (node) => node.kind === "literal" && node.properties.value === "token", + ); + expect(tokens.length).toBeGreaterThan(1); + const inner = tokens.find((node) => node.function_node_id !== null); + const outer = tokens.find((node) => node.function_node_id === null); + if (inner === undefined || outer === undefined) + throw new Error("Expected separate outer and nested token literals"); + expect(inner.identity.source_range).not.toEqual(outer.identity.source_range); + const traced = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "semantic-node", node_id: outer.node_id }, + direction: "forward-influence", + }), + ); + expect(traced.nodes.map((node) => node.node_id)).not.toContain(inner.node_id); +}); + +it("does not send an ignored literal argument to the call result", () => { + const graph = graphForJavaScript( + 'function ignore(x) { return "safe"; } const result = ignore("token");', + ); + const resultBinding = graph.nodes.find( + (node) => node.kind === "binding" && node.label === "result", + ); + if (resultBinding === undefined) throw new Error("Expected result binding"); + const forward = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "token" }, + direction: "forward-influence", + }), + ); + expect(forward.nodes.some((node) => node.kind === "call-site")).toBe(true); + expect(forward.nodes.map((node) => node.node_id)).not.toContain( + resultBinding.node_id, + ); + const backward = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "semantic-node", node_id: resultBinding.node_id }, + direction: "backward-provenance", + }), + ); + expect( + backward.nodes.some( + (node) => + node.properties.value === "token" || + node.properties.literal_argument_value === "token", + ), + ).toBe(false); + expect(backward.nodes.some((node) => node.properties.value === "safe")).toBe( + true, + ); +}); + +it("keeps conditional binding candidates distinct from operand occurrences", () => { + const graph = graphForJavaScript('export const choice = flag ? 1 : "1";'); + const choices = graph.nodes.filter( + (node) => + node.kind === "literal" && + (node.properties.value === 1 || node.properties.value === "1"), + ); + expect(choices).toHaveLength(4); + expect( + new Set( + choices.map((node) => + JSON.stringify([node.properties.value, node.identity.source_range]), + ), + ).size, + ).toBe(4); +}); + +it("projects repeated operator literals with indexed lookup", () => { + const count = 1_200; + const declarations = Array.from( + { length: count }, + (_, index) => `const item${String(index)} = input + "/item";`, + ); + const graph = graphForJavaScript( + `const input = "root";\n${declarations.join("\n")}`, + ); + expect( + graph.nodes.filter( + (node) => node.kind === "literal" && node.properties.value === "/item", + ), + ).toHaveLength(count); +}); diff --git a/src/application/javascript/JavaScriptSemanticGraphExpressionProjection.ts b/src/application/javascript/JavaScriptSemanticGraphExpressionProjection.ts new file mode 100644 index 0000000000..fd5fb24321 --- /dev/null +++ b/src/application/javascript/JavaScriptSemanticGraphExpressionProjection.ts @@ -0,0 +1,398 @@ +import { + compareJavaScriptSourcePoints, + javascriptSourceRangeContains, + javascriptSourceRangeKey, +} from "../../domain/javascript/javascriptSourceCoordinates.js"; +import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; +import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; +import { compositeKey } from "../../domain/unicodeCodePointOrder.js"; +import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js"; +import { + addSemanticGraphRelation, + addSemanticGraphUnknown, + retainSemanticGraphNode, +} from "./JavaScriptSemanticGraphConstruction.js"; +import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; + +const sameArtifact = ( + node: JavaScriptSemanticGraphNode, + context: SemanticFlowProjectionContext, +): boolean => + node.identity.module_path === context.file.path && + node.identity.artifact_sha256 === context.file.sha256; + +const literalValueKey = (value: string | number | boolean | null): string => + `${typeof value}:${JSON.stringify(value)}`; + +type IndexedOperatorLiteral = { + readonly node: JavaScriptSemanticGraphNode; + readonly range: JavaScriptSourceRange; + readonly order: number; +}; + +/** + * Literals are remembered by their own source range and value. + * An initializer that merely contains another occurrence, including a + * nested function body, does not reuse the declaration's literal. + */ +const operatorLiteralIndex = ( + context: SemanticFlowProjectionContext, +): { + remember( + node: JavaScriptSemanticGraphNode | null, + ): JavaScriptSemanticGraphNode | null; + find( + value: string | number | boolean | null, + location: JavaScriptSourceRange, + ): JavaScriptSemanticGraphNode | undefined; +} => { + const exact = new Map(); + const byValue = new Map(); + const seen = new Set(); + let order = 0; + const remember = ( + node: JavaScriptSemanticGraphNode | null, + ): JavaScriptSemanticGraphNode | null => { + if ( + node === null || + node.kind !== "literal" || + !sameArtifact(node, context) || + seen.has(node.node_id) + ) + return node; + const range = node.identity.source_range; + const value = node.properties.value; + if ( + range === null || + (typeof value !== "string" && + typeof value !== "number" && + typeof value !== "boolean" && + value !== null) + ) + return node; + seen.add(node.node_id); + const entry = { node, range, order: order++ }; + const valueKey = literalValueKey(value); + const id = compositeKey([valueKey, javascriptSourceRangeKey(range)]); + const group = byValue.get(valueKey) ?? []; + group.push(entry); + byValue.set(valueKey, group); + if (!exact.has(id)) exact.set(id, entry); + return node; + }; + for (const node of context.state.nodes.values()) remember(node); + return { + remember, + find(value, location) { + const valueKey = literalValueKey(value); + const operandKey = javascriptSourceRangeKey(location); + let best: IndexedOperatorLiteral | undefined; + const consider = (entry: IndexedOperatorLiteral | undefined): void => { + if ( + entry !== undefined && + (best === undefined || entry.order < best.order) + ) + best = entry; + }; + consider(exact.get(compositeKey([valueKey, operandKey]))); + const group = byValue.get(valueKey) ?? []; + let low = 0; + let high = group.length; + while (low < high) { + const middle = (low + high) >> 1; + const entry = group[middle]; + if ( + entry !== undefined && + compareJavaScriptSourcePoints(entry.range.start, location.start) <= 0 + ) + low = middle + 1; + else high = middle; + } + const candidate = group[low - 1]; + if ( + candidate !== undefined && + javascriptSourceRangeContains(candidate.range, location) + ) + consider(candidate); + return best?.node; + }, + }; +}; + +/** Project immediate operator dependencies without treating them as aliases. */ +export function* projectSemanticExpressionInfluence( + context: SemanticFlowProjectionContext & { + readonly argumentNodes: ReadonlyMap; + }, +): Generator { + if (context.ir.expressionFlowsTruncated) + context.state.fileNodesDropped = true; + const key = javascriptSourceRangeKey; + const expressions = new Map(); + const usedExpressions = new Set(); + const reads = new Map( + context.readNodes.flatMap((node) => + node.identity.source_range === null + ? [] + : [[key(node.identity.source_range), node] as const], + ), + ); + const writes = new Map( + context.writeNodes.flatMap((node) => + node.identity.source_range === null + ? [] + : [[key(node.identity.source_range), node] as const], + ), + ); + const bindings = new Map( + context.ir.bindings.flatMap((binding) => { + const node = context.bindingNodes.get(binding.bindingId); + return node === undefined + ? [] + : binding.definitions.map( + (definition) => [key(definition.location), node] as const, + ); + }), + ); + for (const [index, flow] of context.ir.expressionFlows.entries()) { + if (index % 64 === 0) yield; + const owner = + flow.ownerCallableId === null + ? null + : (context.callableNodes.get(flow.ownerCallableId)?.node_id ?? null); + const node = retainSemanticGraphNode(context.state, context.file, { + kind: "expression", + roleKey: `operator:${index}`, + location: flow.location, + label: flow.operator, + functionNodeId: owner, + properties: { operator: flow.operator }, + }); + if (node !== null) expressions.set(key(flow.location), node); + } + const remainingUnknowns = + context.state.unknownCapacity === null + ? context.readNodes.length + : Math.max( + 0, + context.state.unknownCapacity - context.state.unknowns.size, + ); + let unsupportedReads = 0; + let unsupportedOverflow = false; + const orderedReads = context.readNodes + .flatMap((node) => + node.identity.source_range === null + ? [] + : [{ node, location: node.identity.source_range }], + ) + .sort((left, right) => + compareJavaScriptSourcePoints(left.location.start, right.location.start), + ); + let processedOperands = 0; + let processedReferences = 0; + const literals = operatorLiteralIndex(context); + for (const [index, flow] of context.ir.expressionFlows.entries()) { + if (index % 64 === 0) yield; + const result = expressions.get(key(flow.location)); + for (const operand of flow.operands) { + if (processedOperands % 64 === 0) yield; + processedOperands += 1; + if (operand.kind === "unsupported") { + if (result !== undefined) + addSemanticGraphUnknown(context.state, { + node_id: result.node_id, + family: "data-flow", + relation_kinds: ["influences"], + reason: "unknown-value", + detail: "An operator operand has unmodeled value dependencies.", + candidate_node_ids: [], + evidence: unknownSemanticEvidence(context.file, operand.location), + }); + if (!unsupportedOverflow) { + let low = 0, + high = orderedReads.length; + while (low < high) { + const middle = Math.floor((low + high) / 2); + if ( + compareJavaScriptSourcePoints( + orderedReads[middle]!.location.start, + operand.location.start, + ) < 0 + ) + low = middle + 1; + else high = middle; + } + for (let offset = low; offset < orderedReads.length; offset++) { + const { node: reference, location } = orderedReads[offset]!; + if ( + compareJavaScriptSourcePoints( + location.start, + operand.location.end, + ) > 0 + ) + break; + if (processedReferences % 64 === 0) yield; + processedReferences += 1; + if (processedReferences > remainingUnknowns) { + unsupportedOverflow = true; + break; + } + if ( + !javascriptSourceRangeContains(operand.location, location) || + reference.function_node_id !== result?.function_node_id + ) + continue; + if (unsupportedReads >= remainingUnknowns) { + unsupportedOverflow = true; + break; + } + unsupportedReads += 1; + addSemanticGraphUnknown(context.state, { + node_id: reference.node_id, + family: "data-flow", + relation_kinds: ["influences"], + reason: "unknown-value", + detail: + "This read occurs inside an unmodeled operator operand; its influence on the result is unknown.", + candidate_node_ids: [], + evidence: unknownSemanticEvidence(context.file, operand.location), + }); + } + } + continue; + } + const source = + operand.kind === "reference" + ? reads.get(key(operand.location)) + : operand.kind === "expression" + ? expressions.get(key(operand.location)) + : (literals.find(operand.literal ?? null, operand.location) ?? + literals.remember( + retainSemanticGraphNode(context.state, context.file, { + kind: "literal", + roleKey: `operator-literal:${key(operand.location)}:${JSON.stringify(operand.literal ?? null)}`, + location: operand.location, + label: "operator literal", + functionNodeId: result?.function_node_id ?? null, + properties: { value: operand.literal ?? null }, + }) ?? null, + )); + if ( + operand.kind === "expression" && + source !== undefined && + result !== undefined + ) + usedExpressions.add(key(operand.location)); + addSemanticGraphRelation(context.state, { + source, + target: result, + relation: "influences", + resolution: "resolved", + properties: { conditional: operand.conditional }, + }); + } + } + // Some operands are calls, members or other constructs whose value owner is + // not yet modeled here. Do not claim complete influence for any query that + // may cross one of these unmodeled operator inputs. + if (unsupportedOverflow) + addSemanticGraphUnknown(context.state, { + node_id: null, + family: "data-flow", + relation_kinds: ["influences"], + reason: "resource-limit", + detail: + "The bounded operator-dependency audit omitted some unmodeled operand reads; influence through them remains unknown.", + candidate_node_ids: [], + evidence: unknownSemanticEvidence(context.file, null), + }); + const connect = ( + source: JavaScriptSemanticGraphNode | undefined, + target: JavaScriptSemanticGraphNode | undefined, + conditional = false, + ) => { + if ( + source !== undefined && + target !== undefined && + source.identity.source_range !== null && + expressions.get(key(source.identity.source_range)) === source + ) + usedExpressions.add(key(source.identity.source_range)); + addSemanticGraphRelation(context.state, { + source, + target, + relation: "influences", + resolution: "resolved", + properties: { conditional }, + }); + }; + for (const flow of context.ir.initializerFlows) + connect(expressions.get(key(flow.source)), bindings.get(key(flow.target))); + for (const flow of context.ir.assignmentFlows) { + const source = expressions.get(key(flow.source)); + const target = writes.get(key(flow.target)); + if (source !== undefined && target !== undefined) + usedExpressions.add(key(flow.source)); + addSemanticGraphRelation(context.state, { + source, + target, + relation: "influences", + resolution: "resolved", + properties: { + conditional: ["&&=", "||=", "??="].includes(flow.operator), + assignment_operator: flow.operator, + }, + }); + } + for (const call of context.ir.callSites) + for (const argument of call.arguments) { + if (argument.valueExpressionLocation === null) continue; + connect( + expressions.get(key(argument.valueExpressionLocation)), + context.argumentNodes.get( + `${call.callSiteId}\u0000${String(argument.index)}`, + ), + ); + } + const literalReturns = new Map( + context.ir.literalReturns.map((item) => [key(item.location), item]), + ); + for (const callable of context.ir.callables) + for (const site of callable.returnSites) { + if (site.valueExpressionLocation !== null) { + const target = context.returnSiteNodes.get(site.returnSiteId); + connect(expressions.get(key(site.valueExpressionLocation)), target); + const literal = literalReturns.get(key(site.valueExpressionLocation)); + if (literal !== undefined) { + const source = retainSemanticGraphNode(context.state, context.file, { + kind: "literal", + roleKey: `return-literal:${site.returnSiteId}`, + location: literal.location, + label: "return literal", + functionNodeId: target?.function_node_id ?? null, + properties: { value: literal.value }, + }); + addSemanticGraphRelation(context.state, { + source, + target, + relation: "aliases", + resolution: "resolved", + }); + } + } + } + for (const flow of context.ir.expressionFlows) { + const node = expressions.get(key(flow.location)); + if (node === undefined || usedExpressions.has(key(flow.location))) continue; + addSemanticGraphUnknown(context.state, { + node_id: node.node_id, + family: "data-flow", + relation_kinds: ["influences"], + reason: "unknown-value", + detail: + "The receiving expression or container for this operator result is not modeled; further influence is unknown.", + candidate_node_ids: [], + evidence: unknownSemanticEvidence(context.file, flow.location), + }); + } +} diff --git a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts index 619670ac9b..06fb86bdf1 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts @@ -269,10 +269,24 @@ export const semanticObjectReadOccurrence = ( export const projectSemanticReturnValues = ( context: SemanticFlowProjectionContext, ): void => { + const expressions = new Set( + context.ir.expressionFlows.map((flow) => + javascriptSourceRangeKey(flow.location), + ), + ); for (const callable of context.ir.callables) for (const site of callable.returnSites) { const returnNode = context.returnSiteNodes.get(site.returnSiteId); - const references = context.referenceNodesWithin(site.location); + const references = + site.valueExpressionLocation !== null && + expressions.has(javascriptSourceRangeKey(site.valueExpressionLocation)) + ? [] + : context + .referenceNodesWithin(site.location) + .filter( + (reference) => + reference.function_node_id === returnNode?.function_node_id, + ); for (const reference of references) addSemanticGraphRelation(context.state, { source: reference, diff --git a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts index e8c1aa4270..dd77388c12 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts @@ -360,7 +360,13 @@ it("does not promote transformed return expressions to resolved aliases", () => ({ relation, target_node_id }) => relation === "aliases" && target_node_id === returnNode.node_id, ), - ).toEqual([expect.objectContaining({ resolution: "candidate" })]); + ).toEqual([]); + expect( + graph.relations.filter( + ({ relation, target_node_id }) => + relation === "influences" && target_node_id === returnNode.node_id, + ), + ).toEqual([expect.objectContaining({ resolution: "resolved" })]); const output = graph.nodes.find( ({ kind, label }) => kind === "binding" && label === "output", ); @@ -376,10 +382,8 @@ it("does not promote transformed return expressions to resolved aliases", () => direction: "backward-provenance", }), ); - expect(strict.nodes.map(({ node_id }) => node_id)).not.toContain( - input.node_id, - ); - expect(strict.status).toBe("ambiguous"); + expect(strict.nodes.map(({ node_id }) => node_id)).toContain(input.node_id); + expect(strict.status).toBe("partial"); }); it("uses unavailable evidence when no source produced semantic IR", () => { diff --git a/src/application/javascript/JavaScriptSemanticUnknownCapacity.test.ts b/src/application/javascript/JavaScriptSemanticUnknownCapacity.test.ts index 6dec214d3b..6a95a43dfb 100644 --- a/src/application/javascript/JavaScriptSemanticUnknownCapacity.test.ts +++ b/src/application/javascript/JavaScriptSemanticUnknownCapacity.test.ts @@ -318,3 +318,25 @@ it("applies the same unknown admission in direct and transferred construction", transferred.finish(digest, application), ); }); + +it("bounds reads within unmodeled nested operator operands", () => { + const body = `const base = "token"; const value = ${"wrap(".repeat(30)}base${")".repeat(30)} + "/x";`; + const bounded = fileProjection(source("app.js", body), 200, 1); + expect(bounded.unknowns.length).toBeLessThanOrEqual(1); + expect(bounded.omittedUnknownOccurrences?.length).toBeGreaterThan(0); + const projection = createJavaScriptSemanticGraphProjection({ + totalUnknowns: 1, + fileUnknowns: 1, + }); + projection.adoptFileProjection(bounded); + const graph = projection.finish(digest, application); + expect(graph.coverage.truncated).toBe(true); + const trace = queryJavaScriptSemanticGraph( + graph, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "token" }, + direction: "forward-influence", + }), + ); + expect(trace.status).not.toBe("found"); +}); diff --git a/src/artifacts/ArtifactEmbeddedFormatProbes.test.ts b/src/artifacts/ArtifactEmbeddedFormatProbes.test.ts index dbd56673b6..78c14054f9 100644 --- a/src/artifacts/ArtifactEmbeddedFormatProbes.test.ts +++ b/src/artifacts/ArtifactEmbeddedFormatProbes.test.ts @@ -1,4 +1,4 @@ -import { writeFile } from "node:fs/promises"; +import { appendFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, vi } from "vitest"; @@ -6,6 +6,8 @@ import { expect, it, vi } from "vitest"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { withOwnedRegularFile } from "../filesystem/OwnedRegularFile.js"; import { ArtifactEmbeddedFormatProbes } from "./ArtifactEmbeddedFormatProbes.js"; +import * as dotnetLayout from "./dotnet/DotnetBundle.js"; +import { dotnetBundleFixture } from "./dotnet/DotnetBundle.fixture.js"; import * as pckLocation from "./GodotPckLocation.js"; it("distinguishes examined absence from unexamined formats and honors cancellation on a cached result", async () => { @@ -70,3 +72,74 @@ it("leaves a failed probe unexamined so a subsequent successful observation can locate.mockRestore(); } }); + +it("reuses a located .NET header only for the same source and honors cached cancellation", async () => { + const root = await createTestTempDirectory("rea-dotnet-probe-facts-"); + const path = join(root, "host"); + const fixture = dotnetBundleFixture(); + await writeFile(path, fixture.bytes); + const locate = vi.spyOn(dotnetLayout, "findDotnetBundleHeader"); + try { + const facts = await withOwnedRegularFile(path, {}, async (source) => { + const controller = new AbortController(); + const first = new ArtifactEmbeddedFormatProbes( + path, + source, + controller.signal, + ); + expect( + Object.hasOwn(first.completedFacts(), "dotnetBundleHeaderOffset"), + ).toBe(false); + await expect(first.dotnetBundle()).resolves.toBe(fixture.headerOffset); + const completed = first.completedFacts(); + const resumed = new ArtifactEmbeddedFormatProbes( + path, + source, + controller.signal, + completed, + ); + await expect(resumed.dotnetBundle()).resolves.toBe(fixture.headerOffset); + expect(locate).toHaveBeenCalledTimes(1); + const reason = new Error("cancel cached .NET observation"); + controller.abort(reason); + await expect(resumed.dotnetBundle()).rejects.toMatchObject({ + reason: "cancelled", + cause: reason, + }); + return completed; + }); + await appendFile(path, "changed"); + await withOwnedRegularFile(path, {}, async (source) => { + expect( + () => new ArtifactEmbeddedFormatProbes(path, source, undefined, facts), + ).toThrow("Root artifact changed before inventory"); + }); + expect(locate).toHaveBeenCalledTimes(1); + } finally { + locate.mockRestore(); + } +}); + +it("keeps a failed .NET probe unexamined before a successful absence observation", async () => { + const root = await createTestTempDirectory("rea-dotnet-probe-retry-"); + const path = join(root, "plain"); + await writeFile(path, "plain bytes"); + const failure = new Error("injected marker read failure"); + const locate = vi + .spyOn(dotnetLayout, "findDotnetBundleHeader") + .mockRejectedValueOnce(failure); + try { + await withOwnedRegularFile(path, {}, async (source) => { + const probes = new ArtifactEmbeddedFormatProbes(path, source); + await expect(probes.dotnetBundle()).rejects.toBe(failure); + expect( + Object.hasOwn(probes.completedFacts(), "dotnetBundleHeaderOffset"), + ).toBe(false); + await expect(probes.dotnetBundle()).resolves.toBeUndefined(); + expect(probes.completedFacts().dotnetBundleHeaderOffset).toBeNull(); + expect(locate).toHaveBeenCalledTimes(2); + }); + } finally { + locate.mockRestore(); + } +}); diff --git a/src/artifacts/ArtifactEmbeddedFormatProbes.ts b/src/artifacts/ArtifactEmbeddedFormatProbes.ts index 1acb291535..9882d130d4 100644 --- a/src/artifacts/ArtifactEmbeddedFormatProbes.ts +++ b/src/artifacts/ArtifactEmbeddedFormatProbes.ts @@ -1,3 +1,7 @@ +import { + probeBunStandaloneGraph, + type BunStandaloneProbe, +} from "./bun/BunModuleGraphLayout.js"; import type { StableRegularFileDescriptor, RegularFileState, @@ -6,6 +10,7 @@ import { regularFileState, sameRegularFileState, } from "../filesystem/RegularFile.js"; +import { findDotnetBundleHeader } from "./dotnet/DotnetBundle.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; import { abortIfNeeded } from "./ArtifactHash.js"; import { locateGodotPck, type GodotPckLocation } from "./GodotPckLocation.js"; @@ -22,8 +27,10 @@ import { /** Completed observations only; omission means unexamined, and null is examined absence. */ export interface ArtifactEmbeddedFormatProbeFacts { readonly sourceState: RegularFileState; + readonly bunStandalone?: BunStandaloneProbe; readonly godotPck?: GodotPckLocation | null; readonly nodeSea?: NodeSeaLocation | null; + readonly dotnetBundleHeaderOffset?: number | null; readonly pyInstaller?: PyInstallerCookieObservation; } @@ -94,6 +101,37 @@ export class ArtifactEmbeddedFormatProbes { return found.value.state === "located" ? found.value.cookie : undefined; } + async dotnetBundle(): Promise { + abortIfNeeded(this.signal); + if (this.#facts.dotnetBundleHeaderOffset !== undefined) + return this.#facts.dotnetBundleHeaderOffset ?? undefined; + // An apphost inside a recognized CArchive cannot claim its outer carrier. + const packaged = await this.pyInstaller(); + const header = await findDotnetBundleHeader( + this.source.handle, + this.source.initial.size, + this.signal, + packaged?.archiveStart ?? this.source.initial.size, + ); + abortIfNeeded(this.signal); + this.#facts = { ...this.#facts, dotnetBundleHeaderOffset: header ?? null }; + return header; + } + + async bunStandalone(): Promise { + abortIfNeeded(this.signal); + if (this.#facts.bunStandalone !== undefined) + return this.#facts.bunStandalone; + const observed = await probeBunStandaloneGraph( + this.source.handle, + this.source.initial.size, + this.signal, + ); + abortIfNeeded(this.signal); + this.#facts = { ...this.#facts, bunStandalone: observed }; + return observed; + } + completedFacts(): ArtifactEmbeddedFormatProbeFacts { return this.#facts; } diff --git a/src/artifacts/ArtifactPaths.test.ts b/src/artifacts/ArtifactPaths.test.ts index 8f7a8de17b..26c4814d01 100644 --- a/src/artifacts/ArtifactPaths.test.ts +++ b/src/artifacts/ArtifactPaths.test.ts @@ -187,3 +187,13 @@ it.each([["app.js"], ["app.js", "App.js"], ["other.js"]])( expect(destinationCaseCollision("res/app.js", names)).toBeUndefined(); }, ); + +it("reserves prefix capacity atomically without declaring destination kinds", () => { + const registry = new ArtifactPathRegistry({ maxParts: 4, maxNodes: 3 }); + registry.reservePath("a/b"); + expect(() => registry.reservePath("a/c/d")).toThrow(); + expect(() => registry.reservePath("z")).not.toThrow(); + expect(() => registry.reservePath("a/b")).not.toThrow(); + registry.add("a/b", "file"); + expect(() => registry.add("a/b", "directory")).toThrow(); +}); diff --git a/src/artifacts/ArtifactPaths.ts b/src/artifacts/ArtifactPaths.ts index 30e87ef5a5..3af175bfb9 100644 --- a/src/artifacts/ArtifactPaths.ts +++ b/src/artifacts/ArtifactPaths.ts @@ -2,7 +2,11 @@ import { posix } from "node:path"; import { logicalPathNfc } from "../domain/artifactIdentity.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; -import { ArtifactReaderFailure, type ArtifactEntry } from "./ArtifactReader.js"; +import { + ArtifactReaderFailure, + artifactFixedLimitFailure, + type ArtifactEntry, +} from "./ArtifactReader.js"; const ABSOLUTE_DRIVE_PATH = /^[A-Za-z]:[\\/]/u; @@ -87,16 +91,59 @@ export const normalizeArtifactPath = (input: string): string => { return normalized; }; -/** Reject exact and same-spelling prefix collisions. Case-distinct names stay distinct. */ +/** Reject prefix collisions and optionally bound the allocated path tree. */ export class ArtifactPathRegistry { readonly #root: PathTrieNode = { kind: undefined, children: new Map() }; + #nodeCount = 0; + + constructor( + private readonly budget?: { + readonly maxParts: number; + readonly maxNodes: number; + }, + ) {} add(path: string, kind: ArtifactEntry["kind"]): void { if (!hasDestinationPath(kind)) return; + this.#visit(path, kind); + } + + /** Account for distinct prefixes without claiming file/directory validity. */ + reservePath(path: string): void { + this.#visit(path); + } + + #visit(path: string, kind?: DestinationEntryKind): void { const parts = path.split("/"); + if (this.budget !== undefined && parts.length > this.budget.maxParts) + throw artifactFixedLimitFailure( + "Artifact path depth", + this.budget.maxParts, + "path parts", + ); + if (this.budget !== undefined) { + let observed = this.#root; + for (const [index, part] of parts.entries()) { + const existing = observed.children.get(part); + if (existing === undefined) { + if (parts.length - index > this.budget.maxNodes - this.#nodeCount) + throw artifactFixedLimitFailure( + "Artifact path tree", + this.budget.maxNodes, + "path nodes", + ); + break; + } + observed = existing; + } + } let node = this.#root; for (const [index, part] of parts.entries()) { - if (node.kind !== undefined && node.kind !== "directory") + if ( + kind !== undefined && + node.kind !== undefined && + node.kind !== "directory" + ) throw new ArtifactReaderFailure( "path", `Artifact prefix conflict: ${pathDiagnostic(path)}`, @@ -105,9 +152,10 @@ export class ArtifactPathRegistry { if (child === undefined) { child = { kind: undefined, children: new Map() }; node.children.set(part, child); + if (this.budget !== undefined) this.#nodeCount++; } node = child; - if (index === parts.length - 1) { + if (kind !== undefined && index === parts.length - 1) { if (node.kind !== undefined && node.kind !== kind) throw new ArtifactReaderFailure( "path", @@ -120,7 +168,7 @@ export class ArtifactPathRegistry { ); } } - node.kind = kind; + if (kind !== undefined) node.kind = kind; } } diff --git a/src/artifacts/ArtifactReader.ts b/src/artifacts/ArtifactReader.ts index b9e73401c6..c0f8b087b9 100644 --- a/src/artifacts/ArtifactReader.ts +++ b/src/artifacts/ArtifactReader.ts @@ -1,3 +1,4 @@ +import { fixedAllowanceRemediation } from "../domain/fixedAllowanceRemediation.js"; import type { ArtifactAdmissionLimit, ArtifactLimitBoundary, @@ -46,6 +47,8 @@ export interface ArtifactEntry { readonly executable: boolean; readonly encrypted: boolean; readonly byteOffset: number | null; + /** Stored source extent; expanded size may differ for compressed members. */ + readonly byteLength?: number; readonly declaredIntegrity: ArtifactIntegrityClaim | null; readonly formatMetadata?: ArtifactFormatMetadata; readonly unpacked: boolean; @@ -109,6 +112,7 @@ export const sameArtifactEntry = ( entry.executable === produced.executable && entry.encrypted === produced.encrypted && entry.byteOffset === produced.byteOffset && + entry.byteLength === produced.byteLength && sameDeclaredIntegrity(entry.declaredIntegrity, produced.declaredIntegrity) && sameFormatMetadata(entry.formatMetadata, produced.formatMetadata) && entry.unpacked === produced.unpacked && @@ -193,6 +197,8 @@ export interface ArtifactReader { | "unityfs" | "uimage" | "pyinstaller" + | "dotnet-bundle" + | "bun-standalone" | "node-sea" | "intel-hex" | "srec" @@ -333,6 +339,19 @@ export class ArtifactReaderFailure extends Error { } } +/** Fixed reader input ceilings share producer-owned recovery across CLI and MCP. */ +export const artifactFixedLimitFailure = ( + name: string, + maximum: number, + unit: string, +): ArtifactReaderFailure => { + const message = `${fixedAllowanceRemediation(`${name} of ${String(maximum)} ${unit}`, "parser")} This release has no runtime override. Summary delivery and pagination do not raise this input limit.`; + return new ArtifactReaderFailure("limit", message, { + boundary: "capacity", + userMessage: message, + }); +}; + const combineCleanup = ( primary: AnalysisCleanupObservation | undefined, cleanup: AnalysisCleanupObservation, diff --git a/src/artifacts/ArtifactReaderAdmission.ts b/src/artifacts/ArtifactReaderAdmission.ts new file mode 100644 index 0000000000..1f53fd06fe --- /dev/null +++ b/src/artifacts/ArtifactReaderAdmission.ts @@ -0,0 +1,18 @@ +import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js"; +import type { GodotPckAdmission } from "./GodotPckLocation.js"; +import type { NodeSeaAdmission } from "./nodeSea/NodeSeaAdmission.js"; +import type { PyInstallerAdmission } from "./pyinstaller/PyInstallerAdmission.js"; +import type { BunStandaloneAdmission } from "./bun/BunStandaloneAdmission.js"; +import type { DotnetBundleAdmission } from "./dotnet/DotnetBundleAdmission.js"; + +/** Parsed format facts carried with the exact source that established them. */ +export type ArtifactFormatAdmission = + | GodotPckAdmission + | NodeSeaAdmission + | PyInstallerAdmission + | DotnetBundleAdmission + | BunStandaloneAdmission; + +export type ArtifactReaderAdmission = + | StableRegularFileDescriptor + | ArtifactFormatAdmission; diff --git a/src/artifacts/BunStandaloneArtifactReader.ts b/src/artifacts/BunStandaloneArtifactReader.ts new file mode 100644 index 0000000000..8eb1d90c9e --- /dev/null +++ b/src/artifacts/BunStandaloneArtifactReader.ts @@ -0,0 +1,408 @@ +import type { BunStandaloneAdmission } from "./bun/BunStandaloneAdmission.js"; +import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js"; +import { setImmediate } from "node:timers/promises"; +import { Readable, Transform, pipeline } from "node:stream"; +import { finished } from "node:stream/promises"; +import { artifactExpansionBudgetBytes } from "../domain/artifactExpansionBudget.js"; +import type { + BunStandaloneEntryMetadata, + BunStandaloneMetadata, +} from "../domain/artifactFormatMetadata.js"; +import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; +import type { JsonValue } from "../domain/jsonValue.js"; +import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js"; +import { assertArtifactReadActive } from "./ArtifactFileOperation.js"; +import { + normalizeArtifactPath, + ArtifactPathRegistry, +} from "./ArtifactPaths.js"; +import { + ArtifactReaderFailure, + copyArtifactEntry, + sameArtifactEntry, + type ArtifactEntry, + type ArtifactReader, + type ArtifactTraversalOptions, +} from "./ArtifactReader.js"; +import { ArtifactFileSource } from "./ArtifactFileSource.js"; +import { ArtifactResourceScope } from "./ArtifactResourceScope.js"; +import { + MAX_BUN_RETAINED_BYTES, + bunFormatFailure, + bunMetadataBytesLimit, + probeBunStandaloneGraph, + loadBunStandaloneGraph, + type LocatedBunGraph, +} from "./bun/BunModuleGraphLayout.js"; +import { + decodeBunModuleGraph, + type BunDecodedModule, +} from "./bun/BunModuleGraphMembers.js"; + +const REGIONS = ["contents", "sourcemap", "bytecode", "module-info"] as const; +type BunRegion = (typeof REGIONS)[number]; +const MAX_LOGICAL_PATH_PARTS = 256; +const MAX_LOGICAL_PATH_NODES = 250_000; +interface BunModuleDestination { + readonly path: string; + readonly sourcePath?: string; + readonly limitations?: readonly string[]; +} + +/** One descriptor and bounded reads of an embedded Bun standalone module graph. */ +export class BunStandaloneArtifactReader implements ArtifactReader { + readonly format = "bun-standalone" as const; + readonly #scope = new ArtifactResourceScope(); + readonly #entries = new Map(); + readonly #streams = new Set(); + readonly #source: ArtifactFileSource; + #admission: BunStandaloneAdmission | undefined; + #prepared: Promise | undefined; + #header: BunStandaloneMetadata | undefined; + #closing = false; + + constructor( + path: string, + admitted?: StableRegularFileDescriptor | BunStandaloneAdmission, + ) { + if (admitted !== undefined && "kind" in admitted) { + this.#admission = admitted; + this.#source = new ArtifactFileSource( + path, + "Bun standalone", + admitted.source, + ); + } else + this.#source = new ArtifactFileSource(path, "Bun standalone", admitted); + } + + formatMetadata(): BunStandaloneMetadata | undefined { + return this.#header === undefined ? undefined : { ...this.#header }; + } + + async prepareContainer( + expectedSha256: string, + signal?: AbortSignal, + ): Promise { + await this.#scope.run(async () => { + this.#assertOpen(signal); + await this.#source.bindSelectedPathSha256(expectedSha256, signal); + }); + } + + async *entries( + signal?: AbortSignal, + _options?: ArtifactTraversalOptions, + ): AsyncIterable { + await this.#scope.run(async () => { + this.#assertOpen(signal); + this.#prepared ??= this.#prepare(signal); + await this.#prepared; + await this.#source.readSelectedPath(signal); + }); + let index = 0; + for (const entry of this.#entries.values()) { + if (index++ % 256 === 0) await setImmediate(); + this.#assertOpen(signal); + yield copyArtifactEntry(entry); + } + } + + async open(entry: ArtifactEntry, signal?: AbortSignal): Promise { + return this.#scope.run(async () => { + this.#assertOpen(signal); + const stored = this.#entries.get(entry.adapterKey); + if (stored === undefined || !sameArtifactEntry(entry, stored)) + throw new ArtifactReaderFailure( + "integrity", + "Bun standalone entry does not match its owning inventory", + ); + const { handle } = await this.#source.readSelectedPath(signal); + this.#assertOpen(signal); + if (stored.byteOffset === null || stored.declaredSize === null) + throw bunFormatFailure("member has no physical byte range"); + const source = + stored.declaredSize === 0 + ? Readable.from([]) + : readFileHandleChunks(handle, { + start: stored.byteOffset, + end: stored.byteOffset + stored.declaredSize - 1, + signal, + cancellationFailure: (reason) => + new ArtifactReaderFailure( + "cancelled", + "Bun standalone read cancelled", + { cause: reason }, + ), + }); + const output = memberStream(stored); + output.on("error", () => undefined); + this.#streams.add(output); + output.once("close", () => this.#streams.delete(output)); + void this.#scope + .run( + () => + new Promise((resolve) => { + pipeline(source, output, () => { + resolve(); + }); + }), + ) + .catch((cause: unknown) => + output.destroy( + cause instanceof Error + ? cause + : new Error("Bun standalone stream ownership failed", { cause }), + ), + ); + return output; + }); + } + + async close(): Promise { + this.#closing = true; + for (const stream of this.#streams) + stream.destroy( + new ArtifactReaderFailure( + "unavailable", + "Bun standalone reader closed", + ), + ); + await this.#scope.close(); + await Promise.all( + [...this.#streams].map((stream) => + finished(stream).catch(() => undefined), + ), + ); + await this.#source.close(); + this.#entries.clear(); + this.#header = undefined; + this.#admission = undefined; + this.#prepared = undefined; + } + + provenance(): readonly [] { + return []; + } + + #assertOpen(signal?: AbortSignal): void { + assertArtifactReadActive(signal); + if (this.#closing) + throw new ArtifactReaderFailure( + "unavailable", + "Bun standalone reader is closed", + ); + } + + async #prepare(signal?: AbortSignal): Promise { + const source = await this.#source.readSelectedPath(signal); + const probe = + this.#admission?.probe ?? + (await probeBunStandaloneGraph( + source.handle, + source.initial.size, + signal, + )); + if (probe.status === "absent") + throw bunFormatFailure("no Bun section or trailer"); + if (probe.failure !== undefined) throw probe.failure; + if (probe.claim === undefined) + throw bunFormatFailure("graph claim was not admitted"); + const located = await loadBunStandaloneGraph( + source.handle, + probe.claim, + signal, + ); + if (located.status === "absent") + throw bunFormatFailure("no Bun section or trailer"); + if (located.failure !== undefined) throw located.failure; + if (located.graph === undefined) + throw bunFormatFailure("graph data was not admitted"); + const decoded = await decodeBunModuleGraph(located.graph); + const entryModule = decoded.modules[decoded.entryPointId]; + const entryDestination = + entryModule === undefined ? null : logicalModulePath(entryModule); + const header: BunStandaloneMetadata = { + kind: "bun-standalone", + layout_revision: decoded.revision, + carrier: located.graph.carrier, + payload_offset: located.graph.payloadOffset, + payload_length: located.graph.payloadLength, + module_count: decoded.modules.length, + entry_point_id: decoded.entryPointId, + entry_point_source_name: entryModule?.nameText.text ?? null, + entry_point_logical_path: entryDestination?.path ?? null, + compile_exec_argv: decoded.compileExecArgv.text, + compile_exec_argv_hex: decoded.compileExecArgv.hex, + flags: decoded.flags, + }; + let retainedBytes = jsonValueByteLength(header, MAX_BUN_RETAINED_BYTES); + if (retainedBytes > MAX_BUN_RETAINED_BYTES) + throw bunMetadataBytesLimit(retainedBytes); + let declaredPlaintext = 0n; + const expansionBudget = BigInt( + artifactExpansionBudgetBytes(located.graph.payloadLength), + ); + const paths = new ArtifactPathRegistry({ + maxParts: MAX_LOGICAL_PATH_PARTS, + maxNodes: MAX_LOGICAL_PATH_NODES, + }); + for (const module of decoded.modules) { + assertArtifactReadActive(signal); + const destination = + module.index === decoded.entryPointId && entryDestination !== null + ? entryDestination + : logicalModulePath(module); + for (const region of REGIONS) { + const range = regionRange(module, region); + if (range === null) continue; + declaredPlaintext += BigInt(range.length); + if (declaredPlaintext > expansionBudget) + throw new ArtifactReaderFailure( + "limit", + `Bun standalone readable entries declare ${String(declaredPlaintext)} plaintext bytes, exceeding this graph's ${String(expansionBudget)}-byte expansion budget. This release has no runtime override. Host memory and NODE_OPTIONS do not raise it.`, + { boundary: "capacity" }, + ); + const entry = moduleEntry( + located.graph, + module, + region, + range, + destination, + ); + paths.reservePath(entry.path); + retainedBytes += jsonValueByteLength( + entryMetadataJson(entry), + Math.max(0, MAX_BUN_RETAINED_BYTES - retainedBytes), + ); + if (retainedBytes > MAX_BUN_RETAINED_BYTES) + throw bunMetadataBytesLimit(retainedBytes); + this.#entries.set(entry.adapterKey, entry); + } + } + await this.#source.readSelectedPath(signal); + this.#assertOpen(signal); + this.#header = header; + this.#admission = undefined; + } +} + +const regionRange = ( + module: BunDecodedModule, + region: BunRegion, +): { readonly offset: number; readonly length: number } | null => { + if (region === "contents") return module.contents; + if (region === "sourcemap") return module.sourcemap; + if (region === "bytecode") return module.bytecode; + return module.moduleInfo; +}; + +const moduleEntry = ( + graph: LocatedBunGraph, + module: BunDecodedModule, + region: BunRegion, + range: { readonly offset: number; readonly length: number }, + located: BunModuleDestination, +): ArtifactEntry => { + const suffix = + region === "contents" + ? "" + : region === "sourcemap" + ? ".bunmap" + : region === "bytecode" + ? ".bytecode" + : ".module-info"; + const metadata: BunStandaloneEntryMetadata = { + kind: "bun-standalone-entry", + index: module.index, + region, + source_name: module.nameText.text, + source_name_hex: module.nameText.hex, + encoding: module.encoding, + encoding_name: module.encodingName, + loader: module.loader, + loader_name: module.loaderName, + module_format: module.moduleFormat, + module_format_name: module.moduleFormatName, + side: module.side, + side_name: module.sideName, + entry_point: module.index === graph.entryPointId, + sourcemap_bytes: module.sourcemap?.length ?? 0, + bytecode_bytes: module.bytecode?.length ?? 0, + module_info_bytes: module.moduleInfo?.length ?? 0, + bytecode_origin: module.bytecodeOrigin?.text ?? null, + bytecode_origin_hex: module.bytecodeOrigin?.hex ?? null, + }; + return { + path: `${located.path}${suffix}`, + ...(located.sourcePath === undefined + ? {} + : { sourcePath: located.sourcePath }), + kind: "file", + declaredSize: range.length, + compressedSize: null, + executable: false, + encrypted: false, + byteOffset: graph.payloadOffset + range.offset, + declaredIntegrity: null, + unpacked: false, + limitations: [...(located.limitations ?? [])], + adapterKey: `bun:${String(module.index)}:${region}`, + formatMetadata: metadata, + }; +}; + +/** + * Bun virtual names keep a leading slash (`/$bunfs/...`). Strip one slash + * when the remainder is a safe relative path, and keep the raw spelling on + * `sourcePath`. Unsafe, drive, and invalid UTF-8 names fall back to + * `bun-modules/` so inventory can still retain the module. + */ +const logicalModulePath = (module: BunDecodedModule): BunModuleDestination => { + const fallback = `bun-modules/${String(module.index)}`; + const text = module.nameText.text; + if (text === null || text.length === 0) return { path: fallback }; + const candidate = text.startsWith("/") ? text.slice(1) : text; + // Preserve virtual source facts without admitting an unbounded logical path tree. + let parts = 1; + for (let index = 0; index < candidate.length; index++) + if (candidate.charCodeAt(index) === 47 && ++parts > MAX_LOGICAL_PATH_PARTS) + return { + path: fallback, + sourcePath: text, + limitations: [ + `The virtual name exceeds the fixed ${String(MAX_LOGICAL_PATH_PARTS)}-part logical path allowance; an indexed path is used and the original name is retained.`, + ], + }; + + try { + return { + path: normalizeArtifactPath(candidate), + sourcePath: text, + }; + } catch (cause: unknown) { + if (!(cause instanceof ArtifactReaderFailure) || cause.reason !== "path") + throw cause; + return { path: fallback, sourcePath: text }; + } +}; + +const memberStream = (entry: ArtifactEntry): Transform => { + let bytes = 0; + return new Transform({ + transform(chunk: Buffer, _encoding, callback) { + bytes += chunk.length; + callback(null, chunk); + }, + flush(callback) { + callback( + bytes === entry.declaredSize + ? null + : bunFormatFailure(`member ended early: ${entry.path}`), + ); + }, + }); +}; + +const entryMetadataJson = (entry: ArtifactEntry): JsonValue => + entry.formatMetadata === undefined ? null : { ...entry.formatMetadata }; diff --git a/src/artifacts/DotnetBundleArtifactReader.ts b/src/artifacts/DotnetBundleArtifactReader.ts new file mode 100644 index 0000000000..c81cf8f6aa --- /dev/null +++ b/src/artifacts/DotnetBundleArtifactReader.ts @@ -0,0 +1,242 @@ +import { Readable } from "node:stream"; +import { finished, pipeline } from "node:stream/promises"; +import { createInflateRaw } from "node:zlib"; + +import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js"; +import { ArtifactFileSource } from "./ArtifactFileSource.js"; +import { ArtifactResourceScope } from "./ArtifactResourceScope.js"; +import type { DotnetBundleAdmission } from "./dotnet/DotnetBundleAdmission.js"; +import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js"; +import { abortIfNeeded } from "./ArtifactHash.js"; +import { + ArtifactReaderFailure, + copyArtifactEntry, + sameArtifactEntry, + type ArtifactEntry, + type ArtifactReader, +} from "./ArtifactReader.js"; +import { + findDotnetBundleHeader, + readDotnetBundle, + type DotnetBundle, + type DotnetBundleFile, +} from "./dotnet/DotnetBundle.js"; + +/** Stable, range-streamed .NET bundle reader; extraction paths remain owned by SafeOutputTree. */ +export class DotnetBundleArtifactReader implements ArtifactReader { + readonly format = "dotnet-bundle" as const; + readonly #source: ArtifactFileSource; + readonly #scope = new ArtifactResourceScope(); + #admission: DotnetBundleAdmission | undefined; + #bundle: DotnetBundle | undefined; + #closed = false; + readonly #entries = new Map< + string, + { entry: ArtifactEntry; file: DotnetBundleFile } + >(); + readonly #streams = new Set(); + + constructor( + path: string, + admitted?: StableRegularFileDescriptor | DotnetBundleAdmission, + ) { + if (admitted !== undefined && "kind" in admitted) { + this.#admission = admitted; + this.#source = new ArtifactFileSource( + path, + ".NET bundle", + admitted.source, + ); + } else this.#source = new ArtifactFileSource(path, ".NET bundle", admitted); + } + + /** Bind extraction to the whole host that produced its inventory snapshot. */ + async prepareContainer( + expectedSha256: string, + signal?: AbortSignal, + ): Promise { + await this.#scope.run(async () => { + this.#assertOpen(signal); + await this.#source.bindSelectedPathSha256(expectedSha256, signal); + }); + } + + async *entries(signal?: AbortSignal): AsyncIterable { + const bundle = await this.#scope.run(async () => { + this.#assertOpen(signal); + const source = await this.#source.readSelectedPath(signal); + if (this.#bundle === undefined) { + const header = + this.#admission?.headerOffset ?? + (await findDotnetBundleHeader( + source.handle, + source.initial.size, + signal, + )); + if (header === undefined) + throw new ArtifactReaderFailure( + "format", + "Selected file has no .NET bundle header", + ); + this.#bundle = await readDotnetBundle( + source.handle, + source.initial.size, + header, + signal, + ); + this.#admission = undefined; + } + await this.#source.readSelectedPath(signal); + return this.#bundle; + }); + this.#entries.clear(); + for (const [index, file] of bundle.files.entries()) { + this.#assertOpen(signal); + const entry: ArtifactEntry = { + path: file.path, + kind: "file", + declaredSize: file.size, + compressedSize: file.compressedSize || null, + byteOffset: file.offset, + byteLength: file.compressedSize || file.size, + executable: false, + encrypted: false, + declaredIntegrity: null, + unpacked: false, + adapterKey: String(index), + formatMetadata: { + kind: "dotnet-bundle-entry", + major_version: bundle.majorVersion, + minor_version: bundle.minorVersion, + file_type: file.fileType, + }, + limitations: [ + ".NET bundle file types are declarations, not proof of assembly validity or executable authority.", + ...(file.fileType > 5 + ? [ + `Unknown .NET bundle file type ${file.fileType}; bytes are retained without interpreting the type.`, + ] + : []), + ], + }; + this.#entries.set(entry.adapterKey, { + entry: copyArtifactEntry(entry), + file, + }); + yield entry; + } + await this.#source.readSelectedPath(signal); + } + + async open(entry: ArtifactEntry, signal?: AbortSignal): Promise { + return this.#scope.run(async () => { + this.#assertOpen(signal); + const source = await this.#source.readSelectedPath(signal); + this.#assertOpen(signal); + const selected = this.#entries.get(entry.adapterKey); + if (selected === undefined || !sameArtifactEntry(entry, selected.entry)) + throw new ArtifactReaderFailure( + "integrity", + "Selected .NET bundle entry differs from its observed manifest", + ); + const stream = Readable.from( + this.#readMember(source, selected.file, signal), + { objectMode: false }, + ); + this.#streams.add(stream); + stream.once("close", () => this.#streams.delete(stream)); + return stream; + }); + } + + provenance() { + return []; + } + + async close(): Promise { + this.#closed = true; + for (const stream of this.#streams) stream.destroy(); + await this.#scope.close(); + await Promise.all( + [...this.#streams].map((stream) => + finished(stream).catch(() => undefined), + ), + ); + await this.#source.close(); + this.#entries.clear(); + this.#admission = undefined; + this.#bundle = undefined; + } + + #assertOpen(signal?: AbortSignal): void { + abortIfNeeded(signal); + if (this.#closed) + throw new ArtifactReaderFailure( + "unavailable", + ".NET bundle reader is closed", + ); + } + + async *#readMember( + source: StableRegularFileDescriptor, + file: DotnetBundleFile, + signal?: AbortSignal, + ): AsyncGenerator { + const storedBytes = file.compressedSize || file.size; + const input = + storedBytes === 0 + ? Readable.from([]) + : readFileHandleChunks(source.handle, { + start: file.offset, + end: file.offset + storedBytes - 1, + signal, + }); + const inflater = file.compressedSize === 0 ? undefined : createInflateRaw(); + const decoded = inflater ?? input; + const pumping = + inflater === undefined ? undefined : pipeline(input, inflater); + // Observe early pipeline rejections while the output iterator is still reading. + void pumping?.catch(() => undefined); + let bytes = 0; + try { + for await (const raw of decoded) { + abortIfNeeded(signal); + const chunk: Buffer = raw; + bytes += chunk.length; + if (bytes > file.size) + throw new ArtifactReaderFailure( + "limit", + `.NET bundle entry expands beyond its declared size: ${file.path}`, + ); + yield chunk; + } + await pumping; + if ( + bytes !== file.size || + (inflater !== undefined && inflater.bytesWritten !== storedBytes) + ) + throw new ArtifactReaderFailure( + "format", + `.NET bundle member length or compressed extent does not match its declaration: ${file.path}`, + ); + await this.#source.readSelectedPath(signal); + } catch (cause) { + abortIfNeeded(signal); + if (cause instanceof ArtifactReaderFailure) throw cause; + const malformedCompression = + inflater !== undefined && + cause instanceof Error && + "code" in cause && + ["Z_DATA_ERROR", "Z_BUF_ERROR"].includes(String(cause.code)); + throw new ArtifactReaderFailure( + malformedCompression ? "format" : "io", + `Could not read .NET bundle member: ${file.path}`, + { cause }, + ); + } finally { + input.destroy(); + decoded.destroy(); + await pumping?.catch(() => undefined); + } + } +} diff --git a/src/artifacts/apple/DylibResolutionReader.ts b/src/artifacts/apple/DylibResolutionReader.ts index 3a6ccba6c8..0f13c0faf9 100644 --- a/src/artifacts/apple/DylibResolutionReader.ts +++ b/src/artifacts/apple/DylibResolutionReader.ts @@ -4,6 +4,7 @@ import { isAbsolute, join, relative, sep } from "node:path"; import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import { resolveTreePath } from "../../domain/apple/dyldPaths.js"; +import { observedSymlinkTarget } from "../../domain/symlinkTarget.js"; import { dylibResolutionInputSchema, dylibResolutionResultSchema, @@ -113,7 +114,12 @@ class FilesystemTreeView implements DylibTreeView { try { const metadata = await lstat(absolute); if (metadata.isSymbolicLink()) { - const target = await readlink(absolute); + const observed = observedSymlinkTarget( + await readlink(absolute, { encoding: "buffer" }), + ); + if (observed.text === null) + return { kind: "symlink", target: null, targetHex: observed.hex }; + const target = observed.text; // Windows link text separates components with `\`. return { kind: "symlink", @@ -333,6 +339,11 @@ const requireMachoRoots = async ( const lookup = await resolveTreePath(view, path); if (lookup.kind === "escapes") throw reject("path", `Root ${path} resolves outside the analyzed root`); + if (lookup.kind === "undetermined") + throw reject( + "path", + `Root ${path} crosses symlink ${lookup.path} with non-UTF-8 target bytes ${lookup.targetHex}`, + ); if (lookup.kind !== "file") throw reject( "path", diff --git a/src/artifacts/apple/MachoEncryptionReader.test.ts b/src/artifacts/apple/MachoEncryptionReader.test.ts new file mode 100644 index 0000000000..e550f55482 --- /dev/null +++ b/src/artifacts/apple/MachoEncryptionReader.test.ts @@ -0,0 +1,916 @@ +import { describe, expect, it } from "vitest"; + +import { CPU, fatImage, machoImage, readerOf } from "./MachoImage.fixture.js"; +import { readMachoEncryption } from "./MachoEncryptionReader.js"; +import { + MAX_LOAD_COMMAND_BYTES, + readMachoImage, +} from "./MachoLoadCommandReader.js"; + +const LC_SEGMENT = 0x1; +const LC_SEGMENT_64 = 0x19; +const LC_ENCRYPTION_INFO = 0x21; +const LC_ENCRYPTION_INFO_64 = 0x2c; +const S_ZEROFILL = 0x1; + +const read = (bytes: Uint8Array, maximumMappingRecords = 4096) => + readMachoEncryption(readerOf(bytes), bytes.length, maximumMappingRecords); + +const encryption = ( + fields: { + readonly cryptoff: number; + readonly cryptsize: number; + readonly cryptid: number; + }, + wide = true, + size = wide ? 24 : 20, +): Uint8Array => { + const bytes = new Uint8Array(size); + const view = new DataView(bytes.buffer); + view.setUint32(0, wide ? LC_ENCRYPTION_INFO_64 : LC_ENCRYPTION_INFO, true); + view.setUint32(4, size, true); + if (size >= 20) { + view.setUint32(8, fields.cryptoff, true); + view.setUint32(12, fields.cryptsize, true); + view.setUint32(16, fields.cryptid, true); + } + return bytes; +}; + +const segment = ( + name: string, + fields: { + readonly vmAddress: bigint; + /** Defaults to the file size, as linkers emit for file-backed segments. */ + readonly vmSize?: bigint; + readonly fileOffset: number; + readonly fileSize: number; + readonly sections?: readonly { + readonly name: string; + readonly offset: number; + readonly size: number; + readonly flags?: number; + }[]; + }, + wide = true, +): Uint8Array => { + const sections = fields.sections ?? []; + const header = wide ? 72 : 56; + const record = wide ? 80 : 68; + const bytes = new Uint8Array(header + sections.length * record); + const view = new DataView(bytes.buffer); + const encoder = new TextEncoder(); + view.setUint32(0, wide ? LC_SEGMENT_64 : LC_SEGMENT, true); + view.setUint32(4, bytes.length, true); + bytes.set(encoder.encode(name), 8); + if (wide) { + view.setBigUint64(24, fields.vmAddress, true); + view.setBigUint64(32, fields.vmSize ?? BigInt(fields.fileSize), true); + view.setBigUint64(40, BigInt(fields.fileOffset), true); + view.setBigUint64(48, BigInt(fields.fileSize), true); + view.setUint32(64, sections.length, true); + } else { + view.setUint32(24, Number(fields.vmAddress), true); + view.setUint32(28, Number(fields.vmSize ?? BigInt(fields.fileSize)), true); + view.setUint32(32, fields.fileOffset, true); + view.setUint32(36, fields.fileSize, true); + view.setUint32(48, sections.length, true); + } + sections.forEach((section, index) => { + const at = header + index * record; + bytes.set(encoder.encode(section.name), at); + bytes.set(encoder.encode(name), at + 16); + if (wide) { + view.setBigUint64(at + 40, BigInt(section.size), true); + view.setUint32(at + 48, section.offset, true); + view.setUint32(at + 64, section.flags ?? 0, true); + } else { + view.setUint32(at + 36, section.size, true); + view.setUint32(at + 40, section.offset, true); + view.setUint32(at + 56, section.flags ?? 0, true); + } + }); + return bytes; +}; + +/** A thin image padded so that commands can name ranges past its header. */ +const padded = (image: Uint8Array, size = 0x10000): Uint8Array => { + const bytes = new Uint8Array(size); + bytes.set(image); + return bytes; +}; + +const text = (wide = true) => + segment( + "__TEXT", + { + vmAddress: wide ? 0x100000000n : 0x1000n, + fileOffset: 0, + fileSize: 0x8000, + sections: [ + { name: "__text", offset: 0x4000, size: 0x100 }, + { name: "__cstring", offset: 0x7f00, size: 0x100 }, + { name: "__bss", offset: 0, size: 0x100, flags: S_ZEROFILL }, + ], + }, + wide, + ); + +const onlySlice = async (bytes: Uint8Array) => { + const facts = await read(bytes); + if (facts.status !== "parsed" || facts.slices.length !== 1) + throw new Error(`expected one parsed slice, got ${JSON.stringify(facts)}`); + return facts.slices[0]!; +}; + +describe("Mach-O encryption reader: thin images", () => { + it("reports absent only after reading every load command", async () => { + expect(await onlySlice(machoImage({ commands: [text()] }))).toEqual({ + architecture: null, + cpu_type: CPU.arm64.type, + cpu_subtype: CPU.arm64.subtype, + container_file_offset: 0, + size: expect.any(Number), + status: "absent", + reason: null, + commands: [], + range: null, + }); + }); + + it("maps a plaintext range to segments, sections and VM addresses", async () => { + const slice = await onlySlice( + padded( + machoImage({ + commands: [ + text(), + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 0 }), + ], + }), + ), + ); + expect(slice).toMatchObject({ + status: "not-encrypted", + reason: null, + commands: [ + { + kind: "LC_ENCRYPTION_INFO_64", + index: 1, + cmdsize: 24, + cryptoff: 0x4000, + cryptsize: 0x4000, + cryptid: 0, + pad: 0, + }, + ], + range: { + content: "plaintext", + slice_start: 0x4000, + slice_end: 0x8000, + container_start: 0x4000, + container_end: 0x8000, + mapping: { + status: "mapped", + fragments: [ + { + slice_start: 0x4000, + slice_end: 0x8000, + segment: "__TEXT", + vm_start: "0x100004000", + vm_end: "0x100008000", + }, + ], + // Zero-fill sections occupy no file bytes, whatever their offset. + sections: [ + { segment: "__TEXT", section: "__text" }, + { segment: "__TEXT", section: "__cstring" }, + ], + }, + }, + }); + }); + + it("marks a nonzero cryptid as ciphertext and keeps unmapped bytes explicit", async () => { + const slice = await onlySlice( + padded( + machoImage({ + commands: [ + text(), + encryption({ cryptoff: 0x7000, cryptsize: 0x2000, cryptid: 1 }), + ], + }), + ), + ); + expect(slice.status).toBe("encrypted"); + expect(slice.range).toMatchObject({ + content: "ciphertext", + mapping: { + fragments: [ + { slice_start: 0x7000, slice_end: 0x8000, segment: "__TEXT" }, + { + slice_start: 0x8000, + slice_end: 0x9000, + segment: null, + vm_start: null, + vm_end: null, + }, + ], + sections: [{ segment: "__TEXT", section: "__cstring" }], + }, + }); + }); +}); + +describe("Mach-O encryption reader: thin image edge cases", () => { + it("keeps an encrypted empty range without claiming any bytes", async () => { + const slice = await onlySlice( + padded( + machoImage({ + commands: [ + text(), + encryption({ cryptoff: 0x4000, cryptsize: 0, cryptid: 1 }), + ], + }), + ), + ); + expect(slice).toMatchObject({ + status: "encrypted", + range: { + slice_start: 0x4000, + slice_end: 0x4000, + mapping: { status: "mapped", fragments: [], sections: [] }, + }, + }); + }); + + it("reads the 32-bit command form in a 32-bit image", async () => { + const slice = await onlySlice( + padded( + machoImage({ + cpu: CPU.i386, + wide: false, + commands: [ + text(false), + encryption( + { cryptoff: 0x1000, cryptsize: 0x1000, cryptid: 1 }, + false, + ), + ], + }), + ), + ); + expect(slice).toMatchObject({ + status: "encrypted", + commands: [{ kind: "LC_ENCRYPTION_INFO", cmdsize: 20, pad: null }], + range: { + mapping: { + fragments: [{ vm_start: "0x2000", vm_end: "0x3000" }], + }, + }, + }); + }); + + it("reports an unreadable segment mapping as unknown, not unmapped", async () => { + const broken = text(); + new DataView(broken.buffer).setUint32(64, 9, true); + const slice = await onlySlice( + padded( + machoImage({ + commands: [ + broken, + encryption({ cryptoff: 0x4000, cryptsize: 0x100, cryptid: 1 }), + ], + }), + ), + ); + expect(slice).toMatchObject({ + status: "encrypted", + range: { + mapping: { + status: "unknown", + reason: "load command 0 section records exceed cmdsize", + }, + }, + }); + }); +}); + +describe("Mach-O encryption reader: segment mapping", () => { + const ciphertext = encryption({ + cryptoff: 0x4000, + cryptsize: 0x2000, + cryptid: 1, + }); + + it("never reports a byte twice when segment file ranges overlap", async () => { + const first = segment("__A", { + vmAddress: 0x1000n, + fileOffset: 0x4000, + fileSize: 0x1800, + }); + const second = segment("__B", { + vmAddress: 0x9000n, + fileOffset: 0x5000, + fileSize: 0x1000, + }); + const slice = await onlySlice( + padded(machoImage({ commands: [first, second, ciphertext] })), + ); + expect(slice.range?.mapping).toMatchObject({ + status: "mapped", + fragments: [ + { slice_start: 0x4000, slice_end: 0x5800, segment: "__A" }, + { + slice_start: 0x5800, + slice_end: 0x6000, + segment: "__B", + vm_start: "0x9800", + vm_end: "0xa000", + }, + ], + }); + }); + + it("reports a segment command of the other width as an unknown mapping", async () => { + const slice = await onlySlice( + padded( + machoImage({ + cpu: CPU.i386, + wide: false, + commands: [ + text(true), + encryption( + { cryptoff: 0x4000, cryptsize: 0x100, cryptid: 1 }, + false, + ), + ], + }), + ), + ); + expect(slice).toMatchObject({ + status: "encrypted", + range: { + mapping: { + status: "unknown", + reason: "load command 0 LC_SEGMENT_64 appears in a 32-bit image", + }, + }, + }); + }); + + it("names only sections whose bytes the range and their segment both cover", async () => { + const spanning = segment("__TEXT", { + vmAddress: 0x1000n, + fileOffset: 0x4000, + fileSize: 0x2000, + sections: [ + { name: "__text", offset: 0x3000, size: 0x2000 }, + // Declared past the segment's file-backed bytes. + { name: "__stray", offset: 0x7000, size: 0x1000 }, + ], + }); + const covered = await onlySlice( + padded( + machoImage({ + commands: [ + spanning, + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 1 }), + ], + }), + ), + ); + expect(covered.range?.mapping).toMatchObject({ + sections: [{ segment: "__TEXT", section: "__text" }], + }); + const empty = await onlySlice( + padded( + machoImage({ + commands: [ + spanning, + encryption({ cryptoff: 0x4000, cryptsize: 0, cryptid: 1 }), + ], + }), + ), + ); + expect(empty.range?.mapping).toMatchObject({ fragments: [], sections: [] }); + }); + + it("keeps segment name bytes 0x80-0x9f as their own code points", async () => { + const named = segment("__X", { + vmAddress: 0x1000n, + fileOffset: 0x4000, + fileSize: 0x2000, + }); + named[11] = 0x80; + const slice = await onlySlice( + padded(machoImage({ commands: [named, ciphertext] })), + ); + expect(slice.range?.mapping).toMatchObject({ + fragments: [{ segment: "__X\u0080" }], + }); + }); +}); + +describe("Mach-O encryption reader: segment VM extent", () => { + const ciphertext = encryption({ + cryptoff: 0x4000, + cryptsize: 0x2000, + cryptid: 1, + }); + + it("keeps file-only segment bytes without VM addresses", async () => { + const fileOnly = segment("__FILE", { + vmAddress: 0x4000n, + vmSize: 0n, + fileOffset: 0x4000, + fileSize: 0x2000, + }); + const slice = await onlySlice( + padded(machoImage({ commands: [fileOnly, ciphertext] })), + ); + expect(slice.status).toBe("encrypted"); + expect(slice.range?.mapping).toEqual({ + status: "mapped", + fragments: [ + { + slice_start: 0x4000, + slice_end: 0x6000, + segment: "__FILE", + vm_start: null, + vm_end: null, + }, + ], + sections: [], + }); + }); + + it.each([ + [ + "a file size beyond its VM size", + { vmAddress: 0x1000n, vmSize: 0x1000n }, + "segment __TEXT file size 8192 exceeds its VM size 4096", + ], + [ + "a VM range beyond the address space", + { vmAddress: 2n ** 64n - 0x1000n, vmSize: 0x2000n }, + "segment __TEXT VM range exceeds the 64-bit address space", + ], + ])( + "reports %s as an unknown mapping without losing the status", + async (_case, vm, reason) => { + const invalid = segment("__TEXT", { + ...vm, + fileOffset: 0x4000, + fileSize: 0x2000, + }); + const slice = await onlySlice( + padded(machoImage({ commands: [invalid, ciphertext] })), + ); + expect(slice.status).toBe("encrypted"); + expect(slice.range?.content).toBe("ciphertext"); + expect(slice.range?.mapping).toEqual({ + status: "unknown", + reason: `load command 0 ${reason}`, + }); + }, + ); +}); + +describe("Mach-O encryption reader: defects are never absent", () => { + it.each([ + { + name: "a 64-bit command in a 32-bit image", + image: () => + machoImage({ + cpu: CPU.i386, + wide: false, + commands: [encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 })], + }), + reason: "load command 0 LC_ENCRYPTION_INFO_64 appears in a 32-bit image", + }, + { + name: "an unexpected cmdsize", + image: () => + machoImage({ + commands: [ + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 }, true, 32), + ], + }), + reason: + "load command 0 LC_ENCRYPTION_INFO_64 has cmdsize 32, expected 24", + }, + { + name: "a range beyond the slice", + image: () => + machoImage({ + commands: [ + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 1 }), + ], + }), + reason: expect.stringMatching( + /^load command 0 encrypted range ends at 32768, beyond the \d+-byte slice$/u, + ), + }, + { + name: "two encryption commands", + image: () => + machoImage({ + commands: [ + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 0 }), + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 1 }), + ], + }), + reason: "slice has 2 encryption load commands", + }, + ])("reports $name as malformed", async ({ image, reason }) => { + const slice = await onlySlice(image()); + expect(slice).toMatchObject({ status: "malformed", reason, range: null }); + expect(slice.commands.length).toBeGreaterThan(0); + }); + + it("retains two of many encryption commands and counts the rest", async () => { + const slice = await onlySlice( + machoImage({ + commands: Array.from({ length: 1000 }, () => + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 1 }), + ), + }), + ); + expect(slice).toMatchObject({ + status: "malformed", + reason: "slice has 1000 encryption load commands", + range: null, + }); + expect(slice.commands.map(({ index }) => index)).toEqual([0, 1]); + }); + + it("keeps commands read before a framing defect as partial facts", async () => { + const unsized = new Uint8Array(16); + new DataView(unsized.buffer).setUint32(0, 0x7f000001, true); + const slice = await onlySlice( + machoImage({ + commands: [ + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 1 }), + unsized, + ], + }), + ); + expect(slice).toMatchObject({ + status: "malformed", + reason: "load command 1 at offset 24 has invalid cmdsize 0", + commands: [{ kind: "LC_ENCRYPTION_INFO_64", cryptid: 1 }], + range: null, + }); + }); + + it("reports the 20-byte form in a 64-bit image as a framing defect", async () => { + const slice = await onlySlice( + machoImage({ + commands: [ + encryption({ cryptoff: 0, cryptsize: 0, cryptid: 1 }, false), + ], + }), + ); + expect(slice).toMatchObject({ + status: "malformed", + reason: "load command 0 at offset 0 has invalid cmdsize 20", + commands: [], + }); + }); + + it("reports unread load commands as unknown", async () => { + const oversized = machoImage({}); + new DataView(oversized.buffer).setUint32( + 20, + MAX_LOAD_COMMAND_BYTES + 8, + true, + ); + expect(await onlySlice(oversized)).toMatchObject({ + status: "unknown", + reason: expect.stringContaining("above the"), + }); + }); + + it("keeps file-level failures distinct from slice status", async () => { + expect(await read(Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]))).toEqual({ + status: "not-mach-o", + }); + expect( + await read(Uint8Array.from([0xfe, 0xed, 0xfa, 0xcf, 0, 0, 0, 0])), + ).toEqual({ + status: "unsupported", + reason: "big-endian Mach-O images are not supported", + }); + }); +}); + +/** A padded image whose __TEXT range 0x4000-0x8000 is ciphertext. */ +const encrypted = ( + cpu: { readonly type: number; readonly subtype: number } = CPU.arm64, +) => + padded( + machoImage({ + cpu, + commands: [ + text(), + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 1 }), + ], + }), + ); + +describe("Mach-O encryption reader: universal files", () => { + it("reports every slice in container coordinates", async () => { + const facts = await read( + fatImage([ + { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) }, + { cpu: CPU.arm64, bytes: encrypted() }, + ]), + ); + expect(facts).toMatchObject({ + status: "parsed", + slices: [ + { + cpu_type: CPU.x86_64.type, + container_file_offset: 4096, + status: "absent", + }, + { + cpu_type: CPU.arm64.type, + container_file_offset: 8192, + status: "encrypted", + range: { + container_start: 8192 + 0x4000, + container_end: 8192 + 0x8000, + }, + }, + ], + }); + }); + + it("does not let one defective slice hide the others", async () => { + const bigEndian = new Uint8Array(32); + new DataView(bigEndian.buffer).setUint32(0, 0xfeedfacf, false); + const fat = fatImage([ + { cpu: CPU.arm64, bytes: bigEndian }, + { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.arm64 }) }, + { cpu: CPU.arm64, bytes: machoImage({}) }, + { cpu: CPU.arm64e, bytes: encrypted(CPU.arm64e) }, + ]); + // The third declaration's range runs past the end of the file. + new DataView(fat.buffer).setUint32(8 + 2 * 20 + 12, fat.length, false); + const facts = await read(fat); + if (facts.status !== "parsed") throw new Error(facts.status); + expect( + facts.slices.map(({ status, reason }) => ({ status, reason })), + ).toEqual([ + { + status: "unknown", + reason: "big-endian Mach-O slices are not supported", + }, + { + status: "malformed", + reason: + "FAT architecture 1 CPU type/subtype disagrees with its Mach-O header", + }, + { + status: "malformed", + reason: "FAT architecture 2 slice range extends beyond the file", + }, + { status: "encrypted", reason: null }, + ]); + expect(facts.slices[2]).toMatchObject({ + cpu_type: CPU.arm64.type, + cpu_subtype: CPU.arm64.subtype, + commands: [], + }); + }); + + it("keeps arm64e variants apart by CPU subtype", async () => { + const facts = await read( + fatImage([ + { cpu: CPU.arm64e, bytes: machoImage({ cpu: CPU.arm64e }) }, + { cpu: CPU.arm64eX1, bytes: machoImage({ cpu: CPU.arm64eX1 }) }, + ]), + ); + expect(facts).toMatchObject({ + status: "parsed", + slices: [ + { cpu_subtype: CPU.arm64e.subtype, status: "absent" }, + { cpu_subtype: CPU.arm64eX1.subtype, status: "absent" }, + ], + }); + }); + + it("covers Apple's hidden ARM64 slice after the declared rows", async () => { + const armv7 = { type: 12, subtype: 9 }; + const bytes = fatImage([ + { cpu: armv7, bytes: machoImage({ cpu: armv7 }) }, + { + cpu: CPU.arm64, + bytes: padded( + machoImage({ + commands: [ + text(), + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 1 }), + ], + }), + ), + }, + ]); + // nfat_arch 1 leaves the ARM64 row hidden in the table-to-slice gap. + new DataView(bytes.buffer, bytes.byteOffset).setUint32(4, 1); + expect(await read(bytes)).toMatchObject({ + status: "parsed", + slices: [ + { cpu_type: armv7.type, status: "absent" }, + { cpu_type: CPU.arm64.type, status: "encrypted" }, + ], + }); + }); + + it("keeps the declared slice when the hidden ARM64 row's range is invalid", async () => { + const armv7 = { type: 12, subtype: 9 }; + const bytes = fatImage([ + { cpu: armv7, bytes: machoImage({ cpu: armv7 }) }, + { cpu: CPU.arm64, bytes: padded(machoImage({ commands: [text()] })) }, + ]); + const view = new DataView(bytes.buffer, bytes.byteOffset); + view.setUint32(4, 1); + // The hidden row's size runs past the end of the file. + view.setUint32(8 + 20 + 12, bytes.byteLength); + expect(await read(bytes)).toMatchObject({ + status: "parsed", + slices: [ + { cpu_type: armv7.type, status: "absent" }, + { + cpu_type: CPU.arm64.type, + status: "malformed", + reason: "FAT architecture 1 slice range extends beyond the file", + commands: [], + }, + ], + }); + }); +}); + +describe("Mach-O encryption reader: large universal files", () => { + it("keeps a healthy slice when a sibling's range is invalid past the 16 MiB scan limit", async () => { + const slice = padded( + machoImage({ + commands: [ + text(), + encryption({ cryptoff: 0x4000, cryptsize: 0x4000, cryptid: 1 }), + ], + }), + ); + const size = MAX_LOAD_COMMAND_BYTES + 0x100000; + const table = new DataView(new ArrayBuffer(8 + 2 * 20)); + table.setUint32(0, 0xcafebabe); + table.setUint32(4, 2); + const entry = (index: number, fields: readonly number[]) => + fields.forEach((value, field) => + table.setUint32(8 + index * 20 + field * 4, value), + ); + entry(0, [CPU.arm64.type, CPU.arm64.subtype, 0x1000, slice.length, 12]); + // Declared past the end of the file. + entry(1, [CPU.x86_64.type, CPU.x86_64.subtype, size, 0x1000, 12]); + const prefix = new Uint8Array(0x1000 + slice.length); + prefix.set(new Uint8Array(table.buffer)); + prefix.set(slice, 0x1000); + // The bytes past the slice are zero, so the reader never needs them. + const sparseReadAt = async (offset: number, length: number) => { + const result = new Uint8Array( + Math.max(0, Math.min(length, size - offset)), + ); + if (offset < prefix.length) + result.set(prefix.subarray(offset, offset + result.length)); + return result; + }; + + expect(await readMachoEncryption(sparseReadAt, size, 4096)).toMatchObject({ + status: "parsed", + slices: [ + { container_file_offset: 0x1000, status: "encrypted" }, + { + container_file_offset: size, + status: "malformed", + reason: "FAT architecture 1 slice range extends beyond the file", + }, + ], + }); + // The confirmed container reports the sibling's real fault, not an + // unresolved JVM identity. + expect(await readMachoImage(sparseReadAt, size)).toEqual({ + status: "malformed", + reason: "FAT architecture 1 slice range extends beyond the file", + }); + }); +}); + +describe("Mach-O encryption reader: mapping budget", () => { + it.each([ + "section bytes", + "VM size", + "VM overflow", + "command width", + ] as const)( + "releases failed %s mappings before admitting the next slice", + async (failure) => { + const invalid = segment( + "__BROKEN", + { + vmAddress: + failure === "VM overflow" ? 0xffffffffffffffffn : 0x100000000n, + vmSize: failure === "VM size" ? 0xffffn : 0x10000n, + fileOffset: 0, + fileSize: 0x10000, + }, + failure !== "command width", + ); + if (failure === "section bytes") + new DataView(invalid.buffer).setUint32(64, 1, true); + // Two records for the bad segment and five for text(): the first + // slice fills admission but discards its mapping on validation failure. + const facts = await read( + fatImage([ + { + cpu: CPU.x86_64, + bytes: padded( + machoImage({ + cpu: CPU.x86_64, + commands: [ + invalid, + text(), + encryption({ + cryptoff: 0x4000, + cryptsize: 0x4000, + cryptid: 1, + }), + ], + }), + ), + }, + { cpu: CPU.arm64, bytes: encrypted() }, + ]), + 7, + ); + if (facts.status !== "parsed") throw new Error(facts.status); + expect(facts.slices[0]).toMatchObject({ + status: "encrypted", + range: { mapping: { status: "unknown", reason: expect.any(String) } }, + }); + const healthy = await read(encrypted()); + if (healthy.status !== "parsed") throw new Error(healthy.status); + expect(facts.slices[1]?.range?.mapping).toEqual( + healthy.slices[0]?.range?.mapping, + ); + expect(facts.slices[1]?.range?.mapping.status).toBe("mapped"); + }, + ); + + it("leaves the budget to later slices when a slice does not map its range", async () => { + const facts = await read( + fatImage([ + { + cpu: CPU.x86_64, + bytes: padded(machoImage({ cpu: CPU.x86_64, commands: [text()] })), + }, + { cpu: CPU.arm64, bytes: encrypted() }, + ]), + 5, + ); + if (facts.status !== "parsed") throw new Error(facts.status); + expect( + facts.slices.map(({ status, range }) => [status, range?.mapping.status]), + ).toEqual([ + ["absent", undefined], + ["encrypted", "mapped"], + ]); + }); + + it("shares one mapping budget across slices and keeps each status", async () => { + // text() is one segment with three section records: five mapping records. + const facts = await read( + fatImage([ + { cpu: CPU.arm64, bytes: encrypted() }, + { cpu: CPU.arm64e, bytes: encrypted(CPU.arm64e) }, + ]), + 5, + ); + if (facts.status !== "parsed") throw new Error(facts.status); + expect( + facts.slices.map(({ status, range }) => ({ + status, + mapping: range?.mapping.status, + })), + ).toEqual([ + { status: "encrypted", mapping: "mapped" }, + { status: "encrypted", mapping: "unknown" }, + ]); + expect(facts.slices[1]?.range?.mapping).toEqual({ + status: "unknown", + reason: + "mapping needs more than the 5 segment and section records shared by all slices", + }); + }); +}); diff --git a/src/artifacts/apple/MachoEncryptionReader.ts b/src/artifacts/apple/MachoEncryptionReader.ts new file mode 100644 index 0000000000..2159ec4b29 --- /dev/null +++ b/src/artifacts/apple/MachoEncryptionReader.ts @@ -0,0 +1,503 @@ +import type { + MachoEncryptedRange, + MachoEncryptionCommand, + MachoEncryptionFacts, + MachoSliceEncryption, +} from "../../domain/apple/machoEncryption.js"; +import { validateFatSliceIdentity } from "../../domain/apple/machoContainer.js"; +import { readMachoSectionHeader } from "../../domain/apple/machoSectionHeader.js"; +import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js"; +import { + frameLoadCommands, + readMachoContainer, + readSliceCommandTable, + type MachoLoadCommand, + type MachoReadIssue, + type MachoSliceHeader, + type ReadAt, +} from "./MachoLoadCommandReader.js"; + +const LC_SEGMENT = 0x1; +const LC_SEGMENT_64 = 0x19; +const LC_ENCRYPTION_INFO = 0x21; +const LC_ENCRYPTION_INFO_64 = 0x2c; +// loader.h: encryption_info_command is five 32-bit words; the 64-bit form adds `pad`. +const ENCRYPTION_COMMAND_SIZE = { 32: 20, 64: 24 } as const; +// loader.h section types whose bytes occupy no file range. +const ZEROFILL_SECTION_TYPES = new Set([0x1, 0xc, 0x12]); +// A second encryption command already makes a slice malformed; later ones +// are counted, not retained. +const RETAINED_ENCRYPTION_COMMANDS = 2; + +/** Segment and section records left for range mapping across every slice. */ +interface MappingBudget { + readonly maximum: number; + remaining: number; +} + +/** + * Read each slice's `LC_ENCRYPTION_INFO(_64)` status from structural metadata. + * The shared container recognizer owns the reads needed for JVM disambiguation. + * One slice's defect never hides another slice's status. Range mapping in all + * slices shares `maximumMappingRecords` segment and section records; a slice + * past it keeps its status with an unknown mapping. + */ +export const readMachoEncryption = async ( + readAt: ReadAt, + size: number, + maximumMappingRecords: number, +): Promise => { + const budget: MappingBudget = { + maximum: maximumMappingRecords, + remaining: maximumMappingRecords, + }; + const container = await readMachoContainer(readAt, size); + if (container.kind === "not-mach-o") return { status: "not-mach-o" }; + if (container.kind === "issue") + return { status: container.status, reason: container.reason }; + if (container.kind === "thin") + return { + status: "parsed", + slices: [await readSlice(readAt, { offset: 0, size, fat: null }, budget)], + }; + // Apple's hidden ARM64 slice is part of the file's coverage, so an + // undecodable hidden row (a short read) leaves the slice set itself + // unknown before any declared slice is read. + const { hidden } = container; + if (hidden !== null && hidden.status !== "declared") + return { status: hidden.status, reason: hidden.reason }; + const slices: MachoSliceEncryption[] = []; + for (const [index, declaration] of container.header.slices.entries()) { + const rangeIssue = container.header.rangeIssues[index] ?? null; + slices.push( + rangeIssue === null + ? await readSlice( + readAt, + { + offset: declaration.offset, + size: declaration.size, + fat: declaration, + }, + budget, + ) + : unread( + declaration, + rangeIssue.category === "unsupported" ? "unknown" : "malformed", + rangeIssue.reason, + ), + ); + } + if (hidden !== null) + slices.push( + hidden.rangeIssue === null + ? await readSlice( + readAt, + { + offset: hidden.declaration.offset, + size: hidden.declaration.size, + fat: hidden.declaration, + }, + budget, + ) + : unread(hidden.declaration, "malformed", hidden.rangeIssue), + ); + return { status: "parsed", slices }; +}; + +type FatDeclaration = Parameters[0]; + +const unread = ( + declaration: FatDeclaration, + status: "malformed" | "unknown", + reason: string, +): MachoSliceEncryption => ({ + architecture: null, + cpu_type: declaration.cpuType, + cpu_subtype: declaration.cpuSubtype, + container_file_offset: declaration.offset, + size: declaration.size, + status, + reason, + commands: [], + range: null, +}); + +const readSlice = async ( + readAt: ReadAt, + slice: { + readonly offset: number; + readonly size: number; + readonly fat: FatDeclaration | null; + }, + budget: MappingBudget, +): Promise => { + const table = await readSliceCommandTable(readAt, slice.offset, slice.size); + const base = { + architecture: null, + cpu_type: table.header?.cpuType ?? slice.fat?.cpuType ?? null, + cpu_subtype: table.header?.cpuSubtype ?? slice.fat?.cpuSubtype ?? null, + container_file_offset: slice.offset, + size: slice.size, + }; + // Only encryption and segment commands are retained; the rest are framed + // and dropped so a large table of small commands stays one buffer. + const commands: MachoEncryptionCommand[] = []; + let encryptionCommands = 0; + // Null once this slice's segments need more records than the shared + // mapping budget has left; none are retained past that point. + let segments: MachoLoadCommand[] | null = []; + let segmentRecords = 0; + // Commands seen before a failure stay as partial facts; an incomplete walk + // can never prove the command is absent. + const incomplete = ({ + status, + reason, + }: MachoReadIssue): MachoSliceEncryption => ({ + ...base, + status: status === "unsupported" ? "unknown" : "malformed", + reason, + commands, + range: null, + }); + if (table.status !== "read") return incomplete(table); + const { header } = table; + const framed = frameLoadCommands(table, (command) => { + if (isEncryptionCommand(command.command)) { + encryptionCommands += 1; + if (commands.length < RETAINED_ENCRYPTION_COMMANDS) + commands.push(decodeEncryptionCommand(command)); + } else if ( + segments !== null && + (command.command === LC_SEGMENT || command.command === LC_SEGMENT_64) + ) { + segmentRecords += mappingRecords(command); + if (segmentRecords > budget.remaining) segments = null; + else segments.push(command); + } + }); + if (framed.status !== "framed") return incomplete(framed); + const identityIssue = + slice.fat === null + ? null + : validateFatSliceIdentity(slice.fat, { + cpuType: header.cpuType, + cpuSubtype: header.cpuSubtype, + }); + if (identityIssue !== null) + return { + ...base, + status: "malformed", + reason: identityIssue, + commands, + range: null, + }; + const outcome = classify(commands, encryptionCommands, header, slice.size); + if (outcome.status !== "valid") + return { ...base, ...outcome, commands, range: null }; + const range = encryptedRange( + outcome.command, + header, + segments, + slice.offset, + budget.maximum, + ); + // Framing caps transient decoding; only a completed mapping retains its + // reservation. Failed mappings discard those records before the next slice. + if (range.mapping.status === "mapped") budget.remaining -= segmentRecords; + return { + ...base, + status: outcome.command.cryptid === 0 ? "not-encrypted" : "encrypted", + reason: null, + commands, + range, + }; +}; + +const isEncryptionCommand = (command: number): boolean => + command === LC_ENCRYPTION_INFO || command === LC_ENCRYPTION_INFO_64; + +const decodeEncryptionCommand = ({ + command, + body, + index, +}: MachoLoadCommand): MachoEncryptionCommand => { + const view = viewOf(body); + const word = (at: number): number | null => + body.byteLength >= at + 4 ? view.getUint32(at, true) : null; + const wide = command === LC_ENCRYPTION_INFO_64; + return { + kind: wide ? "LC_ENCRYPTION_INFO_64" : "LC_ENCRYPTION_INFO", + index, + cmdsize: body.byteLength, + cryptoff: word(8), + cryptsize: word(12), + cryptid: word(16), + pad: wide ? word(20) : null, + }; +}; + +type Classification = + | { + readonly status: "valid"; + readonly command: MachoEncryptionCommand & { + readonly cryptoff: number; + readonly cryptsize: number; + readonly cryptid: number; + }; + } + | { readonly status: "absent"; readonly reason: null } + | { readonly status: "malformed"; readonly reason: string }; + +const classify = ( + commands: readonly MachoEncryptionCommand[], + count: number, + header: MachoSliceHeader, + sliceSize: number, +): Classification => { + if (count === 0) return { status: "absent", reason: null }; + const [command] = commands; + if (command === undefined || count > 1) + return { + status: "malformed", + reason: `slice has ${count} encryption load commands`, + }; + const width = header.wide ? 64 : 32; + const expectedKind = header.wide + ? "LC_ENCRYPTION_INFO_64" + : "LC_ENCRYPTION_INFO"; + if (command.kind !== expectedKind) + return { + status: "malformed", + reason: `load command ${command.index} ${command.kind} appears in a ${width}-bit image`, + }; + const expectedSize = ENCRYPTION_COMMAND_SIZE[width]; + const { cryptoff, cryptsize, cryptid } = command; + if ( + command.cmdsize !== expectedSize || + cryptoff === null || + cryptsize === null || + cryptid === null + ) + return { + status: "malformed", + reason: `load command ${command.index} ${command.kind} has cmdsize ${command.cmdsize}, expected ${expectedSize}`, + }; + // Both fields are 32-bit, so their sum is a safe integer. + if (cryptoff + cryptsize > sliceSize) + return { + status: "malformed", + reason: `load command ${command.index} encrypted range ends at ${cryptoff + cryptsize}, beyond the ${sliceSize}-byte slice`, + }; + return { + status: "valid", + command: { ...command, cryptoff, cryptsize, cryptid }, + }; +}; + +const encryptedRange = ( + command: { + readonly cryptoff: number; + readonly cryptsize: number; + readonly cryptid: number; + }, + header: MachoSliceHeader, + segmentCommands: readonly MachoLoadCommand[] | null, + containerOffset: number, + maximumMappingRecords: number, +): MachoEncryptedRange => { + const start = command.cryptoff; + const end = command.cryptoff + command.cryptsize; + return { + content: command.cryptid === 0 ? "plaintext" : "ciphertext", + slice_start: start, + slice_end: end, + container_start: containerOffset + start, + container_end: containerOffset + end, + mapping: + segmentCommands === null + ? { + status: "unknown", + reason: `mapping needs more than the ${maximumMappingRecords} segment and section records shared by all slices`, + } + : mapRange(start, end, header, segmentCommands), + }; +}; + +interface SegmentFacts { + readonly index: number; + readonly name: string; + readonly vmAddress: bigint; + readonly vmSize: bigint; + readonly fileOffset: number; + readonly fileSize: number; + readonly sections: readonly { + readonly name: string; + readonly offset: number; + readonly size: number; + readonly zerofill: boolean; + }[]; +} + +const mapRange = ( + start: number, + end: number, + header: MachoSliceHeader, + segmentCommands: readonly MachoLoadCommand[], +): MachoEncryptedRange["mapping"] => { + const segments: SegmentFacts[] = []; + for (const command of segmentCommands) { + if ((command.command === LC_SEGMENT_64) !== header.wide) + return { + status: "unknown", + reason: `load command ${command.index} ${command.command === LC_SEGMENT_64 ? "LC_SEGMENT_64" : "LC_SEGMENT"} appears in a ${header.wide ? 64 : 32}-bit image`, + }; + const segment = decodeSegment(command); + if (typeof segment === "string") + return { status: "unknown", reason: segment }; + // dyld maps only `vmsize` bytes, so file bytes past it have no address. + if (segment.vmSize > 0n && BigInt(segment.fileSize) > segment.vmSize) + return { + status: "unknown", + reason: `load command ${segment.index} segment ${segment.name} file size ${segment.fileSize} exceeds its VM size ${segment.vmSize}`, + }; + if ( + segment.vmAddress + segment.vmSize > + (header.wide ? 2n ** 64n : 2n ** 32n) + ) + return { + status: "unknown", + reason: `load command ${segment.index} segment ${segment.name} VM range exceeds the ${header.wide ? 64 : 32}-bit address space`, + }; + segments.push(segment); + } + const fileBacked = segments + .filter(({ fileSize }) => fileSize > 0) + .sort((left, right) => left.fileOffset - right.fileOffset); + const fragments: Extract< + MachoEncryptedRange["mapping"], + { status: "mapped" } + >["fragments"] = []; + let cursor = start; + for (const segment of fileBacked) { + // Overlapping segments never report the same byte twice. + const from = Math.max(cursor, segment.fileOffset); + const to = Math.min(end, segment.fileOffset + segment.fileSize); + if (from >= to) continue; + if (from > cursor) fragments.push(unmapped(cursor, from)); + // A segment with no VM size keeps file-only bytes that are never mapped. + const mapped = segment.vmSize > 0n; + fragments.push({ + slice_start: from, + slice_end: to, + segment: segment.name, + vm_start: mapped + ? hex(segment.vmAddress + BigInt(from - segment.fileOffset)) + : null, + vm_end: mapped + ? hex(segment.vmAddress + BigInt(to - segment.fileOffset)) + : null, + }); + cursor = Math.max(cursor, to); + } + if (cursor < end) fragments.push(unmapped(cursor, end)); + // A section counts only where the range, the section and its segment's + // file-backed bytes all overlap, so names agree with the fragments. + const sections = fileBacked.flatMap((segment) => + segment.sections + .filter( + ({ zerofill, size, offset }) => + !zerofill && + Math.max(start, offset, segment.fileOffset) < + Math.min(end, offset + size, segment.fileOffset + segment.fileSize), + ) + .map(({ name }) => ({ segment: segment.name, section: name })), + ); + return { status: "mapped", fragments, sections }; +}; + +const unmapped = (from: number, to: number) => ({ + slice_start: from, + slice_end: to, + segment: null, + vm_start: null, + vm_end: null, +}); + +/** + * Mapping records a segment command can produce: at most two fragments, and + * each section record that fits in its `cmdsize`, decoded and reported once. + */ +const mappingRecords = ({ command, body }: MachoLoadCommand): number => { + const wide = command === LC_SEGMENT_64; + return ( + 2 + + Math.max( + 0, + Math.floor((body.byteLength - (wide ? 72 : 56)) / (wide ? 80 : 68)), + ) + ); +}; + +/** Decode one segment command, or say why its file mapping is unknown. */ +const decodeSegment = ({ + command, + body, + index, +}: MachoLoadCommand): SegmentFacts | string => { + const wide = command === LC_SEGMENT_64; + const parsed = readMachoSegmentHeader(body, wide, true); + if (!parsed.ok) + return `load command ${index} is too short for a segment command`; + const segment = parsed.value; + const fileOffset = Number(segment.fileOffset); + const fileSize = Number(segment.fileSize); + if (!Number.isSafeInteger(fileOffset + fileSize)) + return `load command ${index} segment file range exceeds the safe range`; + const sectionSize = wide ? 80 : 68; + if ( + segment.sectionCount > + (body.byteLength - segment.headerSize) / sectionSize + ) + return `load command ${index} section records exceed cmdsize`; + const sections: SegmentFacts["sections"][number][] = []; + for (let section = 0; section < segment.sectionCount; section += 1) { + const at = segment.headerSize + section * sectionSize; + const header = readMachoSectionHeader( + body.subarray(at, at + sectionSize), + wide, + true, + ); + if (!header.ok) + return `load command ${index} section records exceed cmdsize`; + sections.push({ + name: fixedName(header.value.nameBytes), + offset: header.value.offset, + size: Number(header.value.size), + zerofill: ZEROFILL_SECTION_TYPES.has(header.value.flags & 0xff), + }); + } + return { + index, + name: fixedName(segment.nameBytes), + vmAddress: segment.address, + vmSize: segment.memorySize, + fileOffset, + fileSize, + sections, + }; +}; + +/** A 16-byte NUL-padded loader.h name, decoded byte for byte. */ +const fixedName = (bytes: Uint8Array): string => { + const end = bytes.indexOf(0); + // Node's latin1 maps every byte to one code point; WHATWG "latin1" is + // windows-1252 and would rewrite 0x80-0x9f. + return Buffer.from(end < 0 ? bytes : bytes.subarray(0, end)).toString( + "latin1", + ); +}; + +const hex = (value: bigint): string => `0x${value.toString(16)}`; + +const viewOf = (bytes: Uint8Array): DataView => + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts index 3fa89fd1a1..912a6dfa90 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.test.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts @@ -38,6 +38,66 @@ import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js"; const read = (bytes: Uint8Array) => readMachoImage(readerOf(bytes), bytes.length); +it.skipIf(process.platform === "win32")( + "keeps non-UTF-8 symlink targets distinct from a real replacement character", + async () => { + const root = await createTestTempDirectory("rea-dylib-raw-symlink-"); + const executable = machoImage({ + commands: [ + buildVersionCommand(1), + dylibCommand(LC.LOAD_DYLIB, "@loader_path/raw-link"), + dylibCommand(LC.LOAD_DYLIB, "@loader_path/unicode-link"), + ], + }); + await writeFile(join(root, "App"), executable); + await writeFile( + join(root, "lib\ufffd"), + machoImage({ + fileType: FILE_TYPE.dylib, + commands: [buildVersionCommand(1)], + }), + ); + await symlink( + Buffer.from([0x6c, 0x69, 0x62, 0xff]), + join(root, "raw-link"), + ); + await symlink("lib\ufffd", join(root, "unicode-link")); + const trace = await traceDylibResolution({ + rootPath: root, + targetPath: join(root, "App"), + targetSha256: createHash("sha256").update(executable).digest("hex"), + enumerateRoots: false, + parameters: { roots: ["App"] }, + }); + expect(trace.edges.map((edge) => edge.resolution)).toEqual([ + { status: "undetermined", image: null }, + { status: "resolved", image: "lib\ufffd" }, + ]); + expect(trace.coverage.status).toBe("partial"); + expect(trace.limitations).toEqual( + expect.arrayContaining([ + expect.stringContaining('"raw-link":"6c6962ff"'), + ]), + ); + await expect( + traceDylibResolution({ + rootPath: root, + targetPath: join(root, "App"), + targetSha256: createHash("sha256").update(executable).digest("hex"), + enumerateRoots: false, + parameters: { roots: ["raw-link"] }, + }), + ).rejects.toMatchObject({ + issues: [ + { + path: ["roots", 0], + message: expect.stringContaining("non-UTF-8 target bytes 6c6962ff"), + }, + ], + }); + }, +); + it("retains unknown executable-root traversal after a complete callback and keeps known image facts", async () => { const root = await createTestTempDirectory("rea-dylib-traversal-callback-"); const bytes = machoImage({ commands: [buildVersionCommand(1)] }); @@ -1097,6 +1157,22 @@ describe("LC_LAZY_LOAD_DYLIB_INFO image offsets", () => { expect(linkedit.status).toBe("malformed"); }); + it("does not alias a BOM-prefixed segment name to __LINKEDIT", async () => { + const result = await read( + machoImageWithLazyLoadDylibInfo({ + path: "@rpath/libl.dylib", + flagImageOffset: 0x4001, + commandsBefore: [ + segmentCommand({ + name: "\uFEFF__LINKEDIT", + vmaddr: 0n, + vmsize: 0x100000n, + }), + ], + }), + ); + expect(result.status).toBe("parsed"); + }); it("keeps a short segment parseable until a lazy-info command needs its VM range", async () => { const short = new Uint8Array(16); const view = new DataView(short.buffer); @@ -1882,6 +1958,72 @@ describe("Mach-O load command reader failures", () => { }); }); +describe("Mach-O load command reader defect precedence", () => { + it("keeps defect precedence when one image has several defects", async () => { + const shortRpath = new Uint8Array(8); + const shortView = new DataView(shortRpath.buffer); + shortView.setUint32(0, LC.RPATH, true); + shortView.setUint32(4, 8, true); + const unsized = new Uint8Array(16); + new DataView(unsized.buffer).setUint32(0, LC.RPATH, true); + const decodeFirst = { + status: "malformed", + reason: "load command 0 is too short to hold a string offset", + }; + // An earlier command's decode defect wins over a later framing defect, + expect(await read(machoImage({ commands: [shortRpath, unsized] }))).toEqual( + decodeFirst, + ); + // over a missing later command, + const missing = machoImage({ commands: [shortRpath] }); + new DataView(missing.buffer).setUint32(16, 2, true); + expect(await read(missing)).toEqual(decodeFirst); + // and over unconsumed command bytes. + const unconsumed = machoImage({ + commands: [shortRpath, rpathCommand("x")], + }); + new DataView(unconsumed.buffer).setUint32(16, 1, true); + expect(await read(unconsumed)).toEqual(decodeFirst); + + // The command-size limit is checked before the slice bounds. + const oversized = machoImage({}); + new DataView(oversized.buffer).setUint32( + 20, + MAX_LOAD_COMMAND_BYTES + 8, + true, + ); + expect(await read(oversized)).toMatchObject({ status: "unsupported" }); + + // A malformed FAT slice wins over an earlier or later unsupported one. + const bigEndian = new Uint8Array(32); + new DataView(bigEndian.buffer).setUint32(0, 0xfeedfacf, false); + const overlong = machoImage({}); + new DataView(overlong.buffer).setUint32(20, overlong.byteLength, true); + for (const slices of [ + [bigEndian, overlong], + [overlong, bigEndian], + ]) { + const fat = fatImage(slices.map((bytes) => ({ cpu: CPU.arm64, bytes }))); + expect(await read(fat)).toEqual({ + status: "malformed", + reason: "load commands extend beyond the Mach-O slice", + }); + } + + // The first FAT range issue is reported, whatever its category. + const ranged = fatImage([ + { cpu: CPU.arm64, bytes: machoImage({}) }, + { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) }, + ]); + new DataView(ranged.buffer).setUint32(8 + 16, 53, false); + new DataView(ranged.buffer).setUint32(8 + 20 + 12, ranged.length, false); + expect(await read(ranged)).toEqual({ + status: "unsupported", + reason: "FAT architecture 0 alignment exponent 53 is unsupported", + }); + }); +}); + it.each([true, false])( "rejects unconsumed command bytes in wide=%s headers", async (wide) => { diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts index 172b64488b..a661865a98 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -36,7 +36,9 @@ import { readMachoHeaderPrefixFacts, validateFatSlice, validateFatSliceIdentity, + type FatContainerHeader, type FatSliceDeclaration, + type FatSliceRangeIssue, } from "../../domain/apple/machoContainer.js"; /** Read `length` bytes at `offset`; shorter results mean end of file. */ @@ -100,88 +102,190 @@ class MachoFormatIssue extends Error { } } +/** A structural defect, and whether it is malformed or merely unsupported. */ +export interface MachoReadIssue { + readonly status: "malformed" | "unsupported"; + readonly reason: string; +} + +/** + * File-level identity. FAT range issues stay per slice so a tolerant reader + * can still report the slices whose ranges are valid. + */ +export type MachoContainer = + | { readonly kind: "not-mach-o" } + | ({ readonly kind: "issue" } & MachoReadIssue) + | { readonly kind: "thin"; readonly size: number } + | { + readonly kind: "fat"; + readonly header: ParsedFatContainerHeader; + readonly hidden: HiddenFatRow | null; + }; + +/** + * Apple's hidden ARM64 row with its own range defect, or why its slot could + * not be decoded. It stays outside the raw `nfat_arch` declarations. + */ +export type HiddenFatRow = + | { + readonly status: "declared"; + readonly declaration: FatSliceDeclaration; + readonly rangeIssue: string | null; + } + | MachoReadIssue; + +type ParsedFatContainerHeader = Extract< + FatContainerHeader, + { readonly state: "parsed" } +>; + +/** Classify a file as thin, FAT or not Mach-O without decoding any slice. */ +export const readMachoContainer = async ( + readAt: ReadAt, + size: number, +): Promise => { + const head = await readAt(0, Math.min(size, 8)); + if (head.byteLength < 8) + // A recognized magic with no room for a header is a truncated image. + return hasMachoMagic(head) + ? { + kind: "issue", + status: "malformed", + reason: "Mach-O header is truncated", + } + : { kind: "not-mach-o" }; + const fatPrefix = readFatHeaderPrefix(head); + const mayBeJvmClass = + fatPrefix.state === "parsed" && !fatPrefix.wide && !fatPrefix.littleEndian; + let header: FatContainerHeader; + if (mayBeJvmClass && size <= MAX_JVM_CLASS_IDENTITY_BYTES) { + const complete = await readAt(0, size); + header = + complete.byteLength === size + ? readFatContainerHeader(complete, size) + : readFatContainerHeader(head, size); + } else header = readFatContainerHeader(head, size); + if (header.state !== "not-fat") { + if (header.state === "needs-data") { + header = readFatContainerHeader( + await readAt(0, header.requiredBytes), + size, + ); + } + if (header.state === "not-fat") return { kind: "not-mach-o" }; + if (header.state === "needs-data") + return { + kind: "issue", + status: "malformed", + reason: "FAT table is truncated", + }; + if (header.state === "malformed") + return { kind: "issue", status: "malformed", reason: header.reason }; + let identity = classifyFatContainerIdentity(header, () => undefined); + if (identity.state === "needs-slice-header") { + const sliceHeaders = new Map(); + for (const offset of identity.offsets) + sliceHeaders.set(offset, await readSliceMagic(readAt, offset)); + identity = classifyFatContainerIdentity(header, (offset) => + sliceHeaders.get(offset), + ); + } + if ( + identity.state === "not-mach-o" || + identity.state === "needs-slice-header" + ) + return { kind: "not-mach-o" }; + // A large CAFEBABE input whose bounded prefix resembles an invalid + // known-CPU FAT table remains unknown until its JVM identity can be + // checked; do not turn the resource limit into a malformed claim. A slice + // with a valid range that starts with a Mach-O header settles identity, + // and its siblings' range faults then stay their own. + if ( + identity.state === "malformed" && + mayBeJvmClass && + size > MAX_JVM_CLASS_IDENTITY_BYTES && + !(await observesMachoSlice(readAt, header)) + ) + return { + kind: "issue", + status: "unsupported", + reason: JVM_CLASS_IDENTITY_LIMIT_REASON, + }; + // Aliased declarations are rejected at the declaration owner before any + // slice payload is parsed. A declaration whose own range is already + // defective keeps that fault; it does not turn its neighbours into aliases. + const overlap = findOverlappingFatSlices(rangeValidSlices(header)); + if (overlap !== null) + return { + kind: "issue", + status: "malformed", + reason: overlapReason(overlap), + }; + // Identity is only malformed through a slice range issue, which the + // returned rangeIssues retain. + return { + kind: "fat", + header, + hidden: await readAppleHiddenArm64Row(readAt, size, header), + }; + } + const thin = readMachoHeaderPrefixFacts(head).format; + if (thin?.littleEndian === true) return { kind: "thin", size }; + if (thin !== null) + return { + kind: "issue", + status: "unsupported", + reason: "big-endian Mach-O images are not supported", + }; + return { kind: "not-mach-o" }; +}; + +/** A slice's first word read big-endian, or null when it is truncated. */ +const readSliceMagic = async ( + readAt: ReadAt, + offset: number, +): Promise => { + const bytes = await readAt(offset, 4); + return bytes.byteLength < 4 ? null : viewOf(bytes).getUint32(0, false); +}; + +const observesMachoSlice = async ( + readAt: ReadAt, + header: ParsedFatContainerHeader, +): Promise => { + for (const [index, slice] of header.slices.entries()) + if ( + header.rangeIssues[index] === null && + readMachoHeaderPrefixFacts(await readAt(slice.offset, 4)).format !== null + ) + return true; + return false; +}; + /** Decode every slice's dylib-loading load commands without reading code. */ export const readMachoImage = async ( readAt: ReadAt, size: number, ): Promise => { try { - const head = await readAt(0, Math.min(size, 8)); - if (head.byteLength < 8) - // A recognized magic with no room for a header is a truncated image. - return hasMachoMagic(head) - ? { status: "malformed", reason: "Mach-O header is truncated" } - : { status: "not-mach-o" }; - const fatPrefix = readFatHeaderPrefix(head); - const mayBeJvmClass = - fatPrefix.state === "parsed" && - !fatPrefix.wide && - !fatPrefix.littleEndian; - let header: ReturnType; - if (mayBeJvmClass && size <= MAX_JVM_CLASS_IDENTITY_BYTES) { - const complete = await readAt(0, size); - header = - complete.byteLength === size - ? readFatContainerHeader(complete, size) - : readFatContainerHeader(head, size); - } else header = readFatContainerHeader(head, size); - if (header.state !== "not-fat") { - if (header.state === "needs-data") { - header = readFatContainerHeader( - await readAt(0, header.requiredBytes), - size, - ); - } - if (header.state === "not-fat") return { status: "not-mach-o" }; - if (header.state === "needs-data") - throw new MachoFormatIssue("malformed", "FAT table is truncated"); - if (header.state === "malformed") - throw new MachoFormatIssue("malformed", header.reason); - let identity = classifyFatContainerIdentity(header, () => undefined); - if (identity.state === "needs-slice-header") { - const sliceHeaders = new Map(); - for (const offset of identity.offsets) { - const bytes = await readAt(offset, 4); - sliceHeaders.set( - offset, - bytes.byteLength < 4 ? null : viewOf(bytes).getUint32(0, false), - ); - } - identity = classifyFatContainerIdentity(header, (offset) => - sliceHeaders.get(offset), - ); - } - if (identity.state === "not-mach-o") return { status: "not-mach-o" }; - if (identity.state === "malformed") { - // A large CAFEBABE input whose bounded prefix resembles an invalid - // known-CPU FAT table remains unknown until its JVM identity can be - // checked; do not turn the resource limit into a malformed claim. - if (mayBeJvmClass && size > MAX_JVM_CLASS_IDENTITY_BYTES) - throw new MachoFormatIssue( - "unsupported", - JVM_CLASS_IDENTITY_LIMIT_REASON, - ); - throw new MachoFormatIssue(identity.category, identity.reason); - } - if (identity.state === "needs-slice-header") - return { status: "not-mach-o" }; - assertDistinctFatSlices(header.slices); - const hiddenSlice = await readAppleHiddenArm64Slice(readAt, size, header); - return { - status: "parsed", - slices: await readFatSlices(readAt, header.slices), - ...(hiddenSlice === null ? {} : { hiddenSlices: [hiddenSlice] }), - }; - } - const thin = readMachoHeaderPrefixFacts(head).format; - if (thin?.littleEndian === true) + const container = await readMachoContainer(readAt, size); + if (container.kind === "not-mach-o") return { status: "not-mach-o" }; + if (container.kind === "issue") + return { status: container.status, reason: container.reason }; + if (container.kind === "thin") return { status: "parsed", slices: [await readSlice(readAt, 0, size)] }; - if (thin !== null) - throw new MachoFormatIssue( - "unsupported", - "big-endian Mach-O images are not supported", - ); - return { status: "not-mach-o" }; + const { header } = container; + const rangeIssue = header.rangeIssues.find( + (issue): issue is FatSliceRangeIssue => issue !== null, + ); + if (rangeIssue !== undefined) + return { status: rangeIssue.category, reason: rangeIssue.reason }; + const hiddenSlice = await readHiddenSlice(readAt, container.hidden); + return { + status: "parsed", + slices: await readFatSlices(readAt, header.slices), + ...(hiddenSlice === null ? {} : { hiddenSlices: [hiddenSlice] }), + }; } catch (cause: unknown) { if (cause instanceof MachoFormatIssue) return { status: cause.status, reason: cause.message }; @@ -189,53 +293,65 @@ export const readMachoImage = async ( } }; -/** Reject aliasing at the declaration owner before parsing any slice payload. */ -const assertDistinctFatSlices = ( - declarations: readonly FatSliceDeclaration[], -): void => { - const overlap = findOverlappingFatSlices(declarations); - if (overlap !== null) - throw new MachoFormatIssue( - "malformed", - `Mach-O slices ${overlap[0].index} and ${overlap[1].index} overlap.`, - ); -}; +const rangeValidSlices = ( + header: ParsedFatContainerHeader, +): FatSliceDeclaration[] => + header.slices.filter((_, index) => header.rangeIssues[index] === null); + +const overlapReason = ( + overlap: NonNullable>, +): string => + `Mach-O slices ${overlap[0].index} and ${overlap[1].index} overlap.`; /** * Apple lipo stores one hidden ARM64 row after the declared 32-bit FAT table. - * It remains separate from the raw nfat_arch declarations and is only admitted - * from the single table-to-first-slice slot cctools recognizes. + * It is only admitted from the single table-to-first-slice slot cctools + * recognizes. */ -const readAppleHiddenArm64Slice = async ( +const readAppleHiddenArm64Row = async ( readAt: ReadAt, fileSize: number, - header: Extract< - ReturnType, - { state: "parsed" } - >, -): Promise => { + header: ParsedFatContainerHeader, +): Promise => { if (header.wide) return null; const firstOffset = Math.min(...header.slices.map(({ offset }) => offset)); const end = header.tableEnd + 20; if (end > fileSize || end > firstOffset) return null; const bytes = await readAt(header.tableEnd, 20); if (bytes.byteLength < 20) - throw new MachoFormatIssue( - "malformed", - "Hidden ARM64 FAT row is truncated", - ); + return { status: "malformed", reason: "Hidden ARM64 FAT row is truncated" }; const row = readFatSliceDeclarations(bytes, { count: 1, wide: false, littleEndian: header.littleEndian, }); if (row.status !== "parsed") - throw new MachoFormatIssue("malformed", row.reason); + return { status: "malformed", reason: row.reason }; const declaration = { ...row.slices[0]!, index: header.count }; if (declaration.cpuType !== CPU_TYPE_ARM64) return null; - const rangeIssue = validateFatSlice(declaration, fileSize, end); + const overlap = findOverlappingFatSlices([ + ...rangeValidSlices(header), + declaration, + ]); + return { + status: "declared", + declaration, + rangeIssue: + overlap === null + ? validateFatSlice(declaration, fileSize, end) + : overlapReason(overlap), + }; +}; + +const readHiddenSlice = async ( + readAt: ReadAt, + row: HiddenFatRow | null, +): Promise => { + if (row === null) return null; + if (row.status !== "declared") + throw new MachoFormatIssue(row.status, row.reason); + const { declaration, rangeIssue } = row; if (rangeIssue !== null) throw new MachoFormatIssue("malformed", rangeIssue); - assertDistinctFatSlices([...header.slices, declaration]); const slice = await readSlice(readAt, declaration.offset, declaration.size, { fatCpuType: declaration.cpuType, fatCpuSubtype: declaration.cpuSubtype, @@ -288,6 +404,127 @@ const readFatSlices = async ( return slices; }; +/** Little-endian `mach_header(_64)` fields that frame one slice's commands. */ +export interface MachoSliceHeader { + /** True for `MH_MAGIC_64`; the magic, not the CPU type, sets the width. */ + readonly wide: boolean; + readonly headerSize: number; + readonly cpuType: number; + readonly cpuSubtype: number; + readonly fileType: number; + readonly commandCount: number; + readonly commandBytes: number; +} + +/** One framed load command; `offset` is relative to the end of the header. */ +export interface MachoLoadCommand { + readonly index: number; + readonly command: number; + readonly offset: number; + readonly body: Uint8Array; +} + +/** One slice's header and raw load-command table, or why it was not read. */ +export type MachoSliceCommandTable = + | { + readonly status: "read"; + readonly header: MachoSliceHeader; + readonly bytes: Uint8Array; + } + | (MachoReadIssue & { readonly header: MachoSliceHeader | null }); + +/** Read one slice's header and load-command bytes without framing them. */ +export const readSliceCommandTable = async ( + readAt: ReadAt, + base: number, + length: number, +): Promise => { + const headerBytes = await readAt(base, 28); + const parsed = readMachoHeaderDeclarations(headerBytes, length); + if (!parsed.ok) + return failed("malformed", "Mach-O header is truncated", null); + if (parsed.value === null) + return failed( + "malformed", + `slice at offset ${base} has no Mach-O header`, + null, + ); + if (!parsed.value.littleEndian) + return failed( + "unsupported", + "big-endian Mach-O slices are not supported", + null, + ); + const { wide, headerSize, commandCount, commandBytes, fileType, identity } = + parsed.value; + const header: MachoSliceHeader = { + wide, + headerSize, + cpuType: identity.cpuType, + cpuSubtype: identity.cpuSubtype, + fileType, + commandCount, + commandBytes, + }; + if (header.commandBytes > MAX_LOAD_COMMAND_BYTES) + return failed( + "unsupported", + `load commands occupy ${header.commandBytes} bytes, above the ${MAX_LOAD_COMMAND_BYTES}-byte limit`, + header, + ); + if (header.headerSize + header.commandBytes > length) + return failed( + "malformed", + "load commands extend beyond the Mach-O slice", + header, + ); + const bytes = await readAt(base + header.headerSize, header.commandBytes); + if (bytes.byteLength < header.commandBytes) + return failed("malformed", "load commands is truncated", header); + return { status: "read", header, bytes }; +}; + +const failed = ( + status: MachoReadIssue["status"], + reason: string, + header: MachoSliceHeader | null, +): MachoSliceCommandTable => ({ status, reason, header }); + +/** + * Hand each load command to `visit` in order without retaining it. A framing + * failure is returned after every command before the failing one was visited. + */ +export const frameLoadCommands = ( + { + header, + bytes, + }: { readonly header: MachoSliceHeader; readonly bytes: Uint8Array }, + visit: (command: MachoLoadCommand) => void, +): { readonly status: "framed" } | MachoReadIssue => { + const cursor = new MachoLoadCommandCursor({ + wide: header.wide, + littleEndian: true, + commandCount: header.commandCount, + commandBytes: bytes.byteLength, + }); + while (cursor.nextOffset !== null) { + const frame = cursor.read( + bytes.subarray(cursor.nextOffset, cursor.nextOffset + 8), + ); + if (!frame.ok) return { status: "malformed", reason: frame.error }; + const { index, command, offset, size } = frame.value; + visit({ + index, + command, + offset, + body: bytes.subarray(offset, offset + size), + }); + } + const complete = cursor.finish(); + if (!complete.ok) return { status: "malformed", reason: complete.error }; + return { status: "framed" }; +}; + const readSlice = async ( readAt: ReadAt, base: number, @@ -298,96 +535,19 @@ const readSlice = async ( readonly alignmentExponent: number; } | null = null, ): Promise => { - const header = await readExact(readAt, base, 28, "Mach-O header"); - const parsed = readMachoHeaderDeclarations(header, length); - if (!parsed.ok) - throw new MachoFormatIssue("malformed", "Mach-O header is truncated"); - if (parsed.value !== null && parsed.value.littleEndian) { - const { wide, headerSize, commandCount, commandBytes, fileType, identity } = - parsed.value; - if (commandBytes > MAX_LOAD_COMMAND_BYTES) - throw new MachoFormatIssue( - "unsupported", - `load commands occupy ${commandBytes} bytes, above the ${MAX_LOAD_COMMAND_BYTES}-byte limit`, - ); - if (headerSize + commandBytes > length) - throw new MachoFormatIssue( - "malformed", - "load commands extend beyond the Mach-O slice", - ); - const commands = await readExact( - readAt, - base + headerSize, - commandBytes, - "load commands", - ); - const decoded = decodeCommands(commands, { - wide, - cpuType: identity.cpuType, - cpuSubtype: identity.cpuSubtype, - fileType, - commandCount, - sliceOffset: base, - sliceSize: length, - fatCpuType: fat?.fatCpuType ?? null, - fatCpuSubtype: fat?.fatCpuSubtype ?? null, - fatAlignmentExponent: fat?.alignmentExponent ?? null, - }); - const slice = decoded.slice; - await appendDependencies( - slice, - decoded.dependencies, - readAt, - base, - length, - decoded.maxVmOffset, - ); - const distinctPlatforms = [...new Set(slice.platforms)]; - slice.platform = - distinctPlatforms.length === 1 ? (distinctPlatforms[0] ?? null) : null; - return slice; - } - throw new MachoFormatIssue( - parsed.value !== null && !parsed.value.littleEndian - ? "unsupported" - : "malformed", - parsed.value !== null && !parsed.value.littleEndian - ? "big-endian Mach-O slices are not supported" - : `slice at offset ${base} has no Mach-O header`, - ); -}; - -const decodeCommands = ( - bytes: Uint8Array, - header: { - readonly wide: boolean; - readonly cpuType: number; - readonly cpuSubtype: number; - readonly fileType: number; - readonly commandCount: number; - readonly sliceOffset: number; - readonly sliceSize: number; - readonly fatCpuType: number | null; - readonly fatCpuSubtype: number | null; - readonly fatAlignmentExponent: number | null; - }, -): { - readonly slice: MachoSlice; - readonly dependencies: readonly ( - | { readonly kind: "parsed"; readonly value: MachoDependency } - | LazyLoadDylibInfoCommand - )[]; - readonly maxVmOffset: bigint; -} => { + const table = await readSliceCommandTable(readAt, base, length); + if (table.status !== "read") + throw new MachoFormatIssue(table.status, table.reason); + const { header } = table; const slice: MachoSlice = { architecture: machoArchitectureName(header.cpuType, header.cpuSubtype), - slice_offset: header.sliceOffset, - slice_size: header.sliceSize, + slice_offset: base, + slice_size: length, cpu_type: header.cpuType, cpu_subtype: header.cpuSubtype, - fat_cpu_type: header.fatCpuType, - fat_cpu_subtype: header.fatCpuSubtype, - fat_alignment_exponent: header.fatAlignmentExponent, + fat_cpu_type: fat?.fatCpuType ?? null, + fat_cpu_subtype: fat?.fatCpuSubtype ?? null, + fat_alignment_exponent: fat?.alignmentExponent ?? null, file_type: fileTypeName(header.fileType), file_type_code: header.fileType, platform: null, @@ -404,20 +564,10 @@ const decodeCommands = ( > = []; const segments: SegmentSpan[] = []; let unreadableSegment: number | undefined; - let lazyInfo = false; let lazyInfoCount = 0; - const commands = new MachoLoadCommandCursor({ - wide: header.wide, - littleEndian: true, - commandCount: header.commandCount, - commandBytes: bytes.byteLength, - }); - while (commands.nextOffset !== null) { - const offset = commands.nextOffset; - const frame = commands.read(bytes.subarray(offset, offset + 8)); - if (!frame.ok) throw new MachoFormatIssue("malformed", frame.error); - const { index, command, size } = frame.value; - const body = bytes.subarray(offset, offset + size); + // Each command decodes as it is framed, so a defect in an earlier command + // keeps precedence over a later framing defect. + const framed = frameLoadCommands(table, ({ command, body, index }) => { if (command === LC_SEGMENT || command === LC_SEGMENT_64) { const span = readSegmentSpan(body, command === LC_SEGMENT_64); if (span === undefined) unreadableSegment ??= index; @@ -430,19 +580,29 @@ const decodeCommands = ( "unsupported", `LC_LAZY_LOAD_DYLIB_INFO commands exceed the ${MAX_LAZY_INFO_DEPENDENCIES}-dependency limit`, ); - lazyInfo = true; } const decoded = decodeCommand(command, body, index, slice); if (decoded !== undefined) dependencies.push(decoded); - } - const complete = commands.finish(); - if (!complete.ok) throw new MachoFormatIssue("malformed", complete.error); - if (lazyInfo && unreadableSegment !== undefined) + }); + if (framed.status !== "framed") + throw new MachoFormatIssue(framed.status, framed.reason); + if (lazyInfoCount > 0 && unreadableSegment !== undefined) throw new MachoFormatIssue( "malformed", `load command ${unreadableSegment} is too short to establish the slice VM range for LC_LAZY_LOAD_DYLIB_INFO`, ); - return { slice, dependencies, maxVmOffset: maximumVmOffset(segments) }; + await appendDependencies( + slice, + dependencies, + readAt, + base, + length, + maximumVmOffset(segments), + ); + const distinctPlatforms = [...new Set(slice.platforms)]; + slice.platform = + distinctPlatforms.length === 1 ? (distinctPlatforms[0] ?? null) : null; + return slice; }; interface LazyLoadDylibInfoCommand { @@ -485,7 +645,8 @@ const readSegmentSpan = ( const { nameBytes, address: vmaddr, memorySize: vmsize } = parsed.value; const end = nameBytes.indexOf(0); return { - name: new TextDecoder("utf-8", { fatal: false }).decode( + // Segment names are fixed-width Mach-O data; a leading BOM is literal name bytes. + name: new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode( end < 0 ? nameBytes : nameBytes.subarray(0, end), ), vmaddr, diff --git a/src/artifacts/artifactFormats.test.ts b/src/artifacts/artifactFormats.test.ts new file mode 100644 index 0000000000..3d3adb831c --- /dev/null +++ b/src/artifacts/artifactFormats.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; + +import { ARTIFACT_FORMAT_IDS } from "../domain/artifactGraph.js"; +import { + ZIP_PACKAGE_FORMATS, + type ZipPackageFormat, +} from "../domain/zipPackageFormat.js"; +import { + ARTIFACT_FORMATS, + ZIP_FAMILY_CAPABILITIES, +} from "./artifactFormats.js"; +import { + createArtifactReader, + type ArtifactReaderFormat, +} from "./createArtifactReader.js"; + +type Equal = + (() => Value extends Left ? 1 : 2) extends < + Value, + >() => Value extends Right ? 1 : 2 + ? true + : false; + +type FormatsWithReaders = + | ZipPackageFormat + | "dotnet-bundle" + | "bun-standalone" + | "dmg" + | "asar" + | "godot-pck" + | "unityfs" + | "uimage" + | "mach-o-universal" + | "directory" + | "pyinstaller" + | "node-sea" + | "intel-hex" + | "srec" + | "esp-image" + | "fit"; + +const readerFormatsMatch: Equal = + true; +const unknownHasNoReader: "unknown" extends ArtifactReaderFormat + ? false + : true = true; + +describe("artifact format descriptors", () => { + it("derives the reader-format union from descriptors that construct a reader", () => { + expect(readerFormatsMatch).toBe(true); + expect(unknownHasNoReader).toBe(true); + }); + + it("covers every domain format id and no other id", () => { + expect(Object.keys(ARTIFACT_FORMATS).sort()).toEqual( + [...ARTIFACT_FORMAT_IDS].sort(), + ); + }); + + it("leaves unknown without a reader", () => { + expect("createReader" in ARTIFACT_FORMATS.unknown).toBe(false); + expect( + createArtifactReader("/unused", "unknown", { + environment: {}, + unreadableDirectories: "omit", + }), + ).toBeUndefined(); + }); + + it("shares one capability row across zip package families", () => { + for (const id of ZIP_PACKAGE_FORMATS) + expect(ARTIFACT_FORMATS[id]).toMatchObject(ZIP_FAMILY_CAPABILITIES); + const zipIds = new Set(ZIP_PACKAGE_FORMATS); + for (const id of ARTIFACT_FORMAT_IDS) + if (!zipIds.has(id as ZipPackageFormat)) + expect(ARTIFACT_FORMATS[id].zipFamily).toBe(false); + }); +}); diff --git a/src/artifacts/artifactFormats.ts b/src/artifacts/artifactFormats.ts new file mode 100644 index 0000000000..018e2a90d6 --- /dev/null +++ b/src/artifacts/artifactFormats.ts @@ -0,0 +1,384 @@ +import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js"; +import { + ARTIFACT_FORMAT_IDS, + type ArtifactFormatId, +} from "../domain/artifactGraph.js"; +import { ESP_IMAGE_LIMITATIONS } from "../domain/firmware/espImageFacts.js"; +import { + ZIP_PACKAGE_FORMATS, + type ZipPackageFormat, +} from "../domain/zipPackageFormat.js"; +import type { ArtifactReader } from "./ArtifactReader.js"; +import type { ArtifactReaderAdmission } from "./ArtifactReaderAdmission.js"; +import { AsarArtifactReader } from "./AsarArtifactReader.js"; +import type { BunStandaloneAdmission } from "./bun/BunStandaloneAdmission.js"; +import { BunStandaloneArtifactReader } from "./BunStandaloneArtifactReader.js"; +import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; +import type { DotnetBundleAdmission } from "./dotnet/DotnetBundleAdmission.js"; +import { DotnetBundleArtifactReader } from "./DotnetBundleArtifactReader.js"; +import { EspImageArtifactReader } from "./EspImageArtifactReader.js"; +import { FitArtifactReader } from "./FitArtifactReader.js"; +import type { GodotPckAdmission } from "./GodotPckLocation.js"; +import { GodotPckArtifactReader } from "./GodotPckArtifactReader.js"; +import { HexRecordArtifactReader } from "./HexRecordArtifactReader.js"; +import { MachOSliceArtifactReader } from "./MachOSliceArtifactReader.js"; +import { NativeDmgArtifactReader } from "./NativeDmgArtifactReader.js"; +import type { NodeSeaAdmission } from "./nodeSea/NodeSeaAdmission.js"; +import { NodeSeaArtifactReader } from "./NodeSeaArtifactReader.js"; +import type { PyInstallerAdmission } from "./pyinstaller/PyInstallerAdmission.js"; +import { PyInstallerArtifactReader } from "./PyInstallerArtifactReader.js"; +import { UImageArtifactReader } from "./UImageArtifactReader.js"; +import { UnityFsArtifactReader } from "./UnityFsArtifactReader.js"; +import { ZipArtifactReader } from "./ZipArtifactReader.js"; + +/** How inventory describes a format when no reader was constructed. */ +export type AbsentInventory = "platform" | "universal" | "unavailable" | "root"; + +export interface ArtifactReaderConstruction { + readonly environment: Readonly; + readonly unreadableDirectories: "fail" | "omit"; + readonly admission?: ArtifactReaderAdmission | undefined; + readonly asarHeaderAdmission?: AsarHeaderAdmission | undefined; + /** Caller-selected root spelling; only ASAR companion lookup consumes this. */ + readonly selectedPath?: string | undefined; +} + +export interface FormatDescriptor { + readonly id: ArtifactFormatId; + /** Every ZIP package family shares this row of capabilities. */ + readonly zipFamily: boolean; + /** Root occurrence kind is container, including directories and zip packages. */ + readonly occurrenceContainer: boolean; + /** Path-role classification treats the format as a container of its own. */ + readonly roleContainer: boolean; + /** Classification keeps the admitted source for a later reader. */ + readonly retainClassifiedSource: boolean; + /** Extraction can open this format without an embedded-pack probe. */ + readonly extractable: boolean; + /** Reader construction is refused off macOS. */ + readonly requiresDarwin: boolean; + readonly absentInventory: AbsentInventory; + /** Extra limitation when a reader was constructed. */ + readonly readerLimitation?: string; + /** Absent for formats with no entry reader, including `unknown`. */ + readonly createReader?: + | ((path: string, options: ArtifactReaderConstruction) => ArtifactReader) + | undefined; +} + +const sourceOf = (options: ArtifactReaderConstruction) => { + const admission = options.admission; + return admission !== undefined && "kind" in admission + ? admission.source + : admission; +}; + +function admittedAs( + kind: "dotnet-bundle", + options: ArtifactReaderConstruction, +): DotnetBundleAdmission | ReturnType; +function admittedAs( + kind: "bun-standalone", + options: ArtifactReaderConstruction, +): BunStandaloneAdmission | ReturnType; +function admittedAs( + kind: "godot-pck", + options: ArtifactReaderConstruction, +): GodotPckAdmission | ReturnType; +function admittedAs( + kind: "pyinstaller", + options: ArtifactReaderConstruction, +): PyInstallerAdmission | ReturnType; +function admittedAs( + kind: "node-sea", + options: ArtifactReaderConstruction, +): NodeSeaAdmission | ReturnType; +function admittedAs( + kind: string, + options: ArtifactReaderConstruction, +): ArtifactReaderAdmission | ReturnType { + const admission = options.admission; + return admission !== undefined && + "kind" in admission && + admission.kind === kind + ? admission + : sourceOf(options); +} + +/** One capability row for every ZIP-backed package family. */ +export const ZIP_FAMILY_CAPABILITIES = { + zipFamily: true, + occurrenceContainer: true, + roleContainer: false, + retainClassifiedSource: true, + extractable: true, + requiresDarwin: false, + absentInventory: "unavailable", +} as const satisfies Omit; + +type FormatCapabilities = Omit; +type ReaderFn = ( + path: string, + options: ArtifactReaderConstruction, +) => ArtifactReader; + +const withReader = ( + id: Id, + capabilities: FormatCapabilities, + createReader: ReaderFn, +) => ({ id, ...capabilities, createReader }); + +const withoutReader = ( + id: Id, + capabilities: FormatCapabilities, +) => ({ id, ...capabilities }); + +const zipDescriptor = (id: ZipPackageFormat) => + withReader( + id, + ZIP_FAMILY_CAPABILITIES, + (path, options) => + new ZipArtifactReader(path, id, undefined, sourceOf(options)), + ); + +function described( + id: Id, + descriptor: FormatCapabilities & { readonly createReader: ReaderFn }, +): ReturnType>; +function described( + id: Id, + descriptor: FormatCapabilities, +): ReturnType>; +function described( + id: Id, + descriptor: FormatCapabilities & { readonly createReader?: ReaderFn }, +) { + const { createReader, ...capabilities } = descriptor; + return createReader === undefined + ? withoutReader(id, capabilities) + : withReader(id, capabilities, createReader); +} + +const plain = { + zipFamily: false, + occurrenceContainer: false, + roleContainer: false, + retainClassifiedSource: false, + extractable: false, + requiresDarwin: false, + absentInventory: "root", +} as const satisfies Omit; + +function container( + id: Id, + descriptor: Partial & { readonly createReader: ReaderFn }, +): ReturnType>; +function container( + id: Id, + descriptor?: Partial, +): ReturnType>; +function container( + id: Id, + descriptor: Partial & { + readonly createReader?: ReaderFn; + } = {}, +) { + const { createReader, ...rest } = descriptor; + const capabilities = { + ...plain, + occurrenceContainer: true as const, + ...rest, + }; + return createReader === undefined + ? withoutReader(id, capabilities) + : withReader(id, capabilities, createReader); +} + +const NON_ZIP_FORMATS = { + "dotnet-bundle": container("dotnet-bundle", { + retainClassifiedSource: true, + absentInventory: "root", + createReader: (path, options) => + new DotnetBundleArtifactReader( + path, + admittedAs("dotnet-bundle", options), + ), + }), + "bun-standalone": container("bun-standalone", { + retainClassifiedSource: true, + absentInventory: "unavailable", + createReader: (path, options) => + new BunStandaloneArtifactReader( + path, + admittedAs("bun-standalone", options), + ), + }), + dmg: container("dmg", { + requiresDarwin: true, + absentInventory: "platform", + createReader: (path, options) => + new NativeDmgArtifactReader(path, options.environment), + }), + pkg: container("pkg", { absentInventory: "platform" }), + asar: container("asar", { + extractable: true, + absentInventory: "unavailable", + createReader: (path, options) => + new AsarArtifactReader(path, { + ...(options.asarHeaderAdmission === undefined + ? {} + : { headerAdmission: options.asarHeaderAdmission }), + ...(options.selectedPath === undefined + ? {} + : { selectedPath: options.selectedPath }), + }), + }), + "godot-pck": container("godot-pck", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "PCK inventory establishes packaged bytes and metadata; scenes, binary resources, GDScript bytecode and runtime signal behavior were not decoded.", + createReader: (path, options) => + new GodotPckArtifactReader(path, admittedAs("godot-pck", options)), + }), + unityfs: container("unityfs", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "UnityFS inventory establishes packaged bytes and metadata; serialized objects, textures, meshes and runtime asset behavior were not decoded. LZMA-compressed members are unsupported.", + createReader: (path, options) => + new UnityFsArtifactReader(path, sourceOf(options)), + }), + uimage: container("uimage", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "uImage inventory establishes the 64-byte legacy header facts and stored payload bytes. Compressed payloads are not decompressed, and the image is not executed.", + createReader: (path, options) => + new UImageArtifactReader(path, sourceOf(options)), + }), + "javascript-bundle": described("javascript-bundle", plain), + "source-map": described("source-map", plain), + "mach-o-universal": described("mach-o-universal", { + ...plain, + retainClassifiedSource: true, + extractable: true, + requiresDarwin: true, + absentInventory: "universal", + createReader: (path, options) => + new MachOSliceArtifactReader( + path, + options.environment, + undefined, + sourceOf(options), + ), + }), + "mach-o": described("mach-o", plain), + elf: described("elf", plain), + pe: described("pe", plain), + "dos-mz": described("dos-mz", plain), + "dos-com": described("dos-com", plain), + "raw-binary": described("raw-binary", plain), + "gamecube-dol": described("gamecube-dol", plain), + hopper: described("hopper", plain), + "analysis-database": described("analysis-database", plain), + plist: described("plist", plain), + entitlements: described("entitlements", plain), + directory: container("directory", { + absentInventory: "unavailable", + createReader: (path, options) => + new DirectoryArtifactReader(path, options.unreadableDirectories), + }), + file: described("file", plain), + pyinstaller: container("pyinstaller", { + retainClassifiedSource: true, + createReader: (path, options) => + new PyInstallerArtifactReader(path, admittedAs("pyinstaller", options)), + }), + "node-sea": container("node-sea", { + retainClassifiedSource: true, + absentInventory: "unavailable", + readerLimitation: + "Node SEA inventory establishes the injected blob, flags, fuse sentinel, main script, optional code cache, snapshot, and assets. V8 snapshots and code cache are not deserialized, and the application is not executed.", + createReader: (path, options) => + new NodeSeaArtifactReader(path, admittedAs("node-sea", options)), + }), + "intel-hex": container("intel-hex", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "Intel HEX/S-record inventory decodes data records into address-contiguous regions. It does not establish CPU, byte order, memory map, or that a declared start address is executable; gaps are unwritten, not zero.", + createReader: (path, options) => + new HexRecordArtifactReader(path, "intel-hex", sourceOf(options)), + }), + srec: container("srec", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "Intel HEX/S-record inventory decodes data records into address-contiguous regions. It does not establish CPU, byte order, memory map, or that a declared start address is executable; gaps are unwritten, not zero.", + createReader: (path, options) => + new HexRecordArtifactReader(path, "srec", sourceOf(options)), + }), + "esp-image": container("esp-image", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: ESP_IMAGE_LIMITATIONS.inventoryScope, + createReader: (path, options) => + new EspImageArtifactReader(path, sourceOf(options)), + }), + fit: container("fit", { + roleContainer: true, + retainClassifiedSource: true, + extractable: true, + absentInventory: "unavailable", + readerLimitation: + "FIT inventory reports each /images node's stored bytes and its declared type, architecture, OS, compression, load and entry. Image data is not decompressed or decrypted, signatures are not verified, nothing is executed, and load and entry addresses are declarations.", + createReader: (path, options) => + new FitArtifactReader(path, sourceOf(options)), + }), + unknown: described("unknown", plain), +} satisfies Record< + Exclude, + FormatDescriptor +>; + +const ZIP_FORMATS = Object.fromEntries( + ZIP_PACKAGE_FORMATS.map((id) => [id, zipDescriptor(id)]), +) as { + readonly [Id in ZipPackageFormat]: ReturnType; +}; + +/** + * One descriptor per domain format id. `unknown` has no reader. ZIP package + * families share {@link ZIP_FAMILY_CAPABILITIES}. + */ +const ARTIFACT_FORMAT_REGISTRY = { + ...NON_ZIP_FORMATS, + ...ZIP_FORMATS, +}; + +export const ARTIFACT_FORMATS: { + readonly [Id in ArtifactFormatId]: (typeof ARTIFACT_FORMAT_REGISTRY)[Id]; +} = ARTIFACT_FORMAT_REGISTRY; + +const listedFormatIds: readonly ArtifactFormatId[] = ARTIFACT_FORMAT_IDS; + +/** Descriptor for a recognized format id, when the value is one. */ +export const artifactFormatDescriptor = ( + format: string, +): FormatDescriptor | undefined => + listedFormatIds.some((id) => id === format) + ? ARTIFACT_FORMATS[format as ArtifactFormatId] + : undefined; diff --git a/src/artifacts/bun/BunModuleGraph.test.ts b/src/artifacts/bun/BunModuleGraph.test.ts new file mode 100644 index 0000000000..55e2aa8282 --- /dev/null +++ b/src/artifacts/bun/BunModuleGraph.test.ts @@ -0,0 +1,355 @@ +import { open, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, vi } from "vitest"; +import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; +import { + locateBunStandaloneGraph, + probeBunStandaloneGraph, + bunModuleCountLimit, + BUN_GRAPH_SUFFIX_BYTES, +} from "./BunModuleGraphLayout.js"; +import { decodeBunModuleGraph } from "./BunModuleGraphMembers.js"; +import { + bunModuleGraphPayload, + bunPrefixedPayload, + machoBunSection, + machoBunSections, + peBunSection, + elfBunSection, + elfAppendedBunPayload, + elfPrefix, +} from "./BunStandalone.fixture.js"; + +async function locate(bytes: Buffer) { + const path = join(await createTestTempDirectory("rea-bun-parser-"), "app"); + await writeFile(path, bytes); + const handle = await open(path, "r"); + try { + const result = await locateBunStandaloneGraph(handle, bytes.length); + if (result.status === "present" && result.graph !== undefined) { + const graph = result.graph; + return { + ...result, + graph: { + ...graph, + data: { + length: graph.data.length, + read: async (offset: number, length: number) => + Buffer.from( + bytes.subarray( + graph.payloadOffset + offset, + graph.payloadOffset + offset + length, + ), + ), + }, + }, + }; + } + return result; + } finally { + await handle.close(); + } +} + +/** Probe while counting bytes read through the handle. */ +async function probe(bytes: Buffer) { + const path = join(await createTestTempDirectory("rea-bun-parser-"), "app"); + await writeFile(path, bytes); + const handle = await open(path, "r"); + let bytesRead = 0; + const read = handle.read.bind(handle); + const counted = new Proxy(handle, { + get: (target, key) => + key === "read" + ? async (...args: Parameters) => { + const result = await read(...args); + bytesRead += result.bytesRead; + return result; + } + : Reflect.get(target, key), + }); + try { + return { + result: await probeBunStandaloneGraph(counted, bytes.length), + bytesRead, + }; + } finally { + await handle.close(); + } +} + +it.each([ + ["Mach-O", machoBunSection], + ["PE", peBunSection], + ["ELF", elfBunSection], +] as const)( + "decodes section carrier %s with preserved names and auxiliary regions", + async (_name, carrier) => { + const result = await locate( + carrier( + bunPrefixedPayload( + bunModuleGraphPayload({ + modules: [ + { + name: "/$bunfs/root/main.js", + contents: Buffer.from("hello"), + sourcemap: Buffer.from("map"), + bytecode: Buffer.from([1]), + }, + ], + compileExecArgv: "--smol", + }), + ), + ), + ); + expect(result.status).toBe("present"); + if (result.status !== "present" || !result.graph) + throw new Error("missing graph"); + const decoded = await decodeBunModuleGraph(result.graph); + expect(decoded.compileExecArgv.text).toBe("--smol"); + expect(decoded.modules[0]).toMatchObject({ + nameText: { text: "/$bunfs/root/main.js" }, + contents: { length: 5 }, + sourcemap: { length: 3 }, + bytecode: { length: 1 }, + }); + }, +); + +it.each([0, 16])( + "decodes legacy Mach-O four-byte prefixes with %s padding bytes", + async (padding) => { + const payload = bunModuleGraphPayload({ + revision: "bun-1.3.3", + modules: [ + { name: "/$bunfs/root/main.js", contents: Buffer.from("hello") }, + ], + }); + const prefix = Buffer.alloc(4); + prefix.writeUInt32LE(payload.length); + const bytes = machoBunSection( + Buffer.concat([prefix, payload, Buffer.alloc(padding)]), + ); + const result = await locate(bytes); + if (result.status !== "present" || !result.graph) + throw new Error("missing graph"); + expect(result.graph.carrier).toBe("mach-o-section"); + expect(result.graph.payloadOffset).toBe( + bytes.length - padding - payload.length, + ); + expect(result.graph.payloadLength).toBe(payload.length); + const module = (await decodeBunModuleGraph(result.graph)).modules[0]!; + expect(module.nameText.text).toBe("/$bunfs/root/main.js"); + expect( + ( + await result.graph.data.read( + module.contents.offset, + module.contents.length, + ) + ).toString(), + ).toBe("hello"); + }, +); + +it.each(["trailer", "byte_count"])( + "rejects legacy Mach-O prefixes with invalid %s", + async (field) => { + const payload = bunModuleGraphPayload({ + revision: "bun-1.3.3", + modules: [{ name: "/$bunfs/root/main.js" }], + }); + if (field === "trailer") payload[payload.length - 1] = 0; + else payload.writeBigUInt64LE(0n, payload.length - BUN_GRAPH_SUFFIX_BYTES); + const prefix = Buffer.alloc(4); + prefix.writeUInt32LE(payload.length); + expect( + await locate(machoBunSection(Buffer.concat([prefix, payload]))), + ).toMatchObject({ status: "present", failure: expect.anything() }); + }, +); + +it.each(["bun-1.3.3", "bun-1.3.9"] as const)( + "decodes appended %s and retains duplicate names", + async (revision) => { + const result = await locate( + elfAppendedBunPayload( + bunModuleGraphPayload({ + revision, + modules: [{ name: "same" }, { name: "same" }], + }), + ), + ); + if (result.status !== "present" || !result.graph) + throw new Error("missing graph"); + expect( + (await decodeBunModuleGraph(result.graph)).modules.map( + (m) => m.nameText.text, + ), + ).toEqual(["same", "same"]); + }, +); + +it.each([ + [ + { modules: [{ contentsPointer: { offset: 99999, length: 1 } }] }, + "pointer exceeds", + ], + [{ modules: [{}], tableLength: 53 }, "unsupported"], + [ + { modules: [{ loader: 0, moduleFormat: 0 }], tableLength: 468 }, + "more than one known revision", + ], + [{ modules: [{}], entryPointId: 8 }, "entry_point_id"], +])("fails closed for invalid module declarations", async (input, reason) => { + const result = await locate( + elfAppendedBunPayload(bunModuleGraphPayload(input)), + ); + if (result.status !== "present" || !result.graph) + throw new Error("missing graph"); + const graph = result.graph; + await expect(decodeBunModuleGraph(graph)).rejects.toThrow(reason); +}); + +it("recognizes a truncated section as malformed", async () => { + const section = bunPrefixedPayload(bunModuleGraphPayload({ modules: [{}] })); + const result = await locate( + machoBunSection(section.subarray(0, section.length - 1)), + ); + expect(result).toMatchObject({ + status: "present", + failure: { reason: "format" }, + }); +}); + +it("locates the legacy trailer before its final carrier length", async () => { + const bytes = elfAppendedBunPayload( + bunModuleGraphPayload({ modules: [{ name: "main.js" }] }), + ); + const length = Buffer.alloc(8); + length.writeBigUInt64LE(BigInt(bytes.length + 8)); + const result = await locate(Buffer.concat([bytes, length])); + if (result.status !== "present" || !result.graph) + throw new Error("missing graph"); + expect((await decodeBunModuleGraph(result.graph)).modules).toHaveLength(1); +}); + +it("does not admit oversized payloads or truncated length prefixes", async () => { + const prefix = Buffer.alloc(8); + prefix.writeBigUInt64LE(300n * 1024n * 1024n); + expect(await locate(machoBunSection(prefix))).toMatchObject({ + status: "present", + failure: { reason: "limit" }, + }); + expect(await locate(machoBunSection(Buffer.from([1])))).toMatchObject({ + status: "present", + failure: { reason: "format" }, + }); + expect(await locate(machoBunSection(Buffer.alloc(8)))).toEqual({ + status: "absent", + }); +}); + +it("refuses multiple embedded graphs instead of selecting one", async () => { + const section = bunPrefixedPayload(bunModuleGraphPayload({ modules: [{}] })); + expect(await locate(machoBunSections([section, section]))).toMatchObject({ + status: "present", + failure: { reason: "unavailable" }, + }); +}); + +it("probes a section graph without reading its payload data", async () => { + const contents = Buffer.alloc(1024 * 1024, 0x61); + const bytes = machoBunSection( + bunPrefixedPayload( + bunModuleGraphPayload({ modules: [{ name: "main.js", contents }] }), + ), + ); + const { result, bytesRead } = await probe(bytes); + expect(result).toMatchObject({ + status: "present", + claim: { carrier: "mach-o-section" }, + }); + expect(bytesRead).toBeLessThan(4096); +}); + +it.each([ + [ + "Mach-O", + (section: Buffer) => { + const bytes = machoBunSection(Buffer.concat([section, Buffer.alloc(16)])); + return bytes.subarray(0, bytes.length - 8); + }, + ], + [ + "ELF", + (section: Buffer) => { + const bytes = elfBunSection(section); + const sectionOffset = 64 + "\0.shstrtab\0.bun\0".length; + bytes.writeBigUInt64LE( + BigInt(bytes.length - sectionOffset + 1), + bytes.length - 64 + 32, + ); + return bytes; + }, + ], +] as const)( + "rejects %s sections whose declared tail is missing", + async (_name, carrier) => { + const section = bunPrefixedPayload( + bunModuleGraphPayload({ modules: [{ name: "main.js" }] }), + ); + expect(await locate(carrier(section))).toMatchObject({ + status: "present", + failure: { + reason: "format", + message: expect.stringContaining("past the end of the file"), + }, + }); + }, +); + +it("matches ELF section names in bounded time when headers share one long name", async () => { + const shnum = 16_383; + const stringOffset = 64; + const strings = Buffer.alloc(1024 * 1024, 0x61); + const tableOffset = stringOffset + strings.length; + const bytes = Buffer.alloc(tableOffset + shnum * 64); + elfPrefix().copy(bytes, 0); + bytes.writeBigUInt64LE(BigInt(tableOffset), 40); + bytes.writeUInt16LE(64, 58); + bytes.writeUInt16LE(shnum, 60); + bytes.writeUInt16LE(0, 62); + strings.copy(bytes, stringOffset); + for (let index = 0; index < shnum; index += 1) { + const header = tableOffset + index * 64; + bytes.writeBigUInt64LE(BigInt(stringOffset), header + 24); + bytes.writeBigUInt64LE(BigInt(strings.length), header + 32); + } + const started = performance.now(); + expect((await probe(bytes)).result).toEqual({ status: "absent" }); + expect(performance.now() - started).toBeLessThan(2_000); +}); + +it("refuses an excessive table count before reading or allocating the module table", async () => { + const count = 100_001; + const result = await locate( + elfAppendedBunPayload( + bunModuleGraphPayload({ modules: [{}], tableLength: count * 52 }), + ), + ); + if (result.status !== "present" || result.graph === undefined) + throw new Error("Missing graph"); + const read = vi.fn(result.graph.data.read); + const expected = bunModuleCountLimit(count); + await expect( + decodeBunModuleGraph({ + ...result.graph, + data: { ...result.graph.data, read }, + }), + ).rejects.toMatchObject({ + reason: "limit", + boundary: "capacity", + message: expected.message, + }); + expect(read).not.toHaveBeenCalled(); +}); diff --git a/src/artifacts/bun/BunModuleGraphLayout.ts b/src/artifacts/bun/BunModuleGraphLayout.ts new file mode 100644 index 0000000000..51e20e68e5 --- /dev/null +++ b/src/artifacts/bun/BunModuleGraphLayout.ts @@ -0,0 +1,835 @@ +import { + readFatContainerHeader, + readMachoHeaderPrefixFacts, +} from "../../domain/apple/machoContainer.js"; +import { + readMachoLoadCommandTableHeader, + MachoLoadCommandCursor, +} from "../../domain/apple/machoLoadCommands.js"; +import type { FileHandle } from "node:fs/promises"; + +import { fixedAllowanceRemediation } from "../../domain/fixedAllowanceRemediation.js"; +import { abortIfNeeded } from "../ArtifactHash.js"; +import { + ArtifactReaderFailure, + artifactFixedLimitFailure, +} from "../ArtifactReader.js"; + +/** Exact trailer written by Bun's standalone module graph serializer. */ +export const BUN_TRAILER = Buffer.from("\n---- Bun! ----\n"); +export const BUN_TRAILER_BYTES = BUN_TRAILER.length; +export const BUN_OFFSETS_BYTES = 32; +export const BUN_GRAPH_SUFFIX_BYTES = BUN_OFFSETS_BYTES + BUN_TRAILER_BYTES; +/** Current compilers prefix a section with this little-endian payload length. */ +export const BUN_LENGTH_PREFIX_BYTES = 8; +/** Bun 1.3.3 through 1.3.8 module records. */ +export const BUN_RECORD_BYTES_1_3_3 = 36; +/** Bun 1.3.9 through at least 1.4.4 module records. */ +export const BUN_RECORD_BYTES_1_3_9 = 52; + +/** + * Module-table admission. This release has no runtime override; host memory + * and NODE_OPTIONS do not raise it. + */ +export const MAX_BUN_MODULE_COUNT = 100_000; +/** + * Serialized graph payload admission, in bytes. A claimed length above this + * ceiling is refused before the payload is read. This release has no runtime + * override; host memory and NODE_OPTIONS do not raise it. + */ +export const MAX_BUN_RETAINED_BYTES = 256 * 1024 * 1024; + +const MAX_LOAD_COMMAND_BYTES = 16 * 1024 * 1024; +const MAX_SECTION_TABLE_BYTES = 1024 * 1024; +const MAX_PE_SECTIONS = 96; +const LC_SEGMENT = 0x1; +const LC_SEGMENT_64 = 0x19; +const SEGMENT_64_HEADER_BYTES = 72; +const SEGMENT_32_HEADER_BYTES = 56; +const SECTION_64_BYTES = 80; +const SECTION_32_BYTES = 68; + +export const BUN_LAYOUT_REVISIONS = ["bun-1.3.3", "bun-1.3.9"] as const; +export type BunLayoutRevision = (typeof BUN_LAYOUT_REVISIONS)[number]; +export const BUN_CARRIERS = [ + "mach-o-section", + "pe-section", + "elf-section", + "appended-trailer", +] as const; +export type BunCarrier = (typeof BUN_CARRIERS)[number]; + +export interface BunStringPointer { + readonly offset: number; + readonly length: number; +} + +/** Bounded metadata windows; the artifact reader streams stored member bodies. */ +export interface BunGraphData { + readonly length: number; + read(offset: number, length: number): Promise; +} + +/** One located graph payload. Pointers are relative to `data`. */ +export interface LocatedBunGraph { + readonly carrier: BunCarrier; + readonly payloadOffset: number; + readonly payloadLength: number; + readonly data: BunGraphData; + readonly modules: BunStringPointer; + readonly entryPointId: number; + readonly compileExecArgv: BunStringPointer; + readonly flags: number; +} + +/** A graph whose extent and suffix were validated without reading its data. */ +export interface BunGraphClaim { + readonly carrier: BunCarrier; + readonly payloadOffset: number; + readonly payloadLength: number; +} + +export type BunStandaloneProbe = + | { readonly status: "absent" } + | { + readonly status: "present"; + readonly claim: BunGraphClaim; + readonly failure?: never; + } + | { + readonly status: "present"; + readonly failure: ArtifactReaderFailure; + readonly claim?: never; + }; + +export type BunStandaloneRecognition = + | { readonly status: "absent" } + | { + readonly status: "present"; + readonly graph: LocatedBunGraph; + readonly failure?: never; + } + | { + readonly status: "present"; + readonly failure: ArtifactReaderFailure; + readonly graph?: never; + }; + +interface SectionWindow { + readonly carrier: Exclude; + readonly fileOffset: number; + readonly declaredSize: number; + readonly available: number; + /** The declared section extent ends past EOF. */ + readonly truncated: boolean; +} + +/** Probe, then read the one claimed payload. */ +export const locateBunStandaloneGraph = async ( + handle: FileHandle, + fileSize: number, + signal?: AbortSignal, +): Promise => { + const probed = await probeBunStandaloneGraph(handle, fileSize, signal); + if (probed.status === "absent" || probed.claim === undefined) return probed; + return loadBunStandaloneGraph(handle, probed.claim, signal); +}; + +/** + * Locate at most one graph claim with bounded reads of headers, length + * prefixes, and suffixes. Payload data is not read. Multiple real claims are + * an unsupported ambiguity. + */ +export const probeBunStandaloneGraph = async ( + handle: FileHandle, + fileSize: number, + signal?: AbortSignal, +): Promise => { + abortIfNeeded(signal); + const claims = [ + ...(await machOClaims(handle, fileSize, 0, fileSize, signal)), + ...(await peClaims(handle, fileSize, signal)), + ...(await elfClaims(handle, fileSize, signal)), + ]; + const real: BunStandaloneProbe[] = []; + for (const claim of claims) { + abortIfNeeded(signal); + const evaluated = await evaluateSection(handle, fileSize, claim, signal); + if (evaluated.status === "present") { + if (real.length > 0) + return presentFailure( + new ArtifactReaderFailure( + "unavailable", + "Bun standalone module graph layout is unsupported: multiple embedded graphs were located", + ), + ); + real.push(evaluated); + } + } + const only = real[0]; + if (only !== undefined) return only; + return evaluateAppendedTrailer(handle, fileSize, signal); +}; + +/** Read a probed payload once and validate its suffix against the claim. */ +export const loadBunStandaloneGraph = async ( + handle: FileHandle, + claim: BunGraphClaim, + signal?: AbortSignal, +): Promise => { + const suffix = await readExact( + handle, + claim.payloadOffset + claim.payloadLength - BUN_GRAPH_SUFFIX_BYTES, + BUN_GRAPH_SUFFIX_BYTES, + signal, + ); + const failure = suffixFailure(suffix, claim.payloadLength); + if (failure !== undefined) return { status: "present", failure }; + const offsets = suffix.subarray(0, BUN_OFFSETS_BYTES); + return { + status: "present", + graph: { + ...claim, + data: new BunGraphRangeReader( + handle, + claim.payloadOffset, + claim.payloadLength - BUN_GRAPH_SUFFIX_BYTES, + signal, + ), + modules: readPointer(offsets, 8), + entryPointId: offsets.readUInt32LE(16), + compileExecArgv: readPointer(offsets, 20), + flags: offsets.readUInt32LE(28), + }, + }; +}; + +const MAX_METADATA_READ_BYTES = 256 * 1024 * 1024; +const METADATA_CACHE_BYTES = 256 * 1024; +const METADATA_CACHE_ENTRIES = 1024; + +/** Exact-range caching avoids page amplification from file-controlled pointers. */ +class BunGraphRangeReader implements BunGraphData { + readonly #ranges = new Map(); + #retainedBytes = 0; + #readBytes = 0; + constructor( + private readonly handle: FileHandle, + private readonly start: number, + readonly length: number, + private readonly signal?: AbortSignal, + ) {} + + async read(offset: number, length: number): Promise { + abortIfNeeded(this.signal); + if ( + !Number.isSafeInteger(offset) || + !Number.isSafeInteger(length) || + offset < 0 || + length < 0 || + offset > this.length || + length > this.length - offset + ) + throw bunFormatFailure("metadata range exceeds the Bun module graph"); + if (length === 0) return Buffer.alloc(0); + const key = `${String(offset)}:${String(length)}`; + const existing = this.#ranges.get(key); + if (existing !== undefined) { + this.#ranges.delete(key); + this.#ranges.set(key, existing); + return Buffer.from(existing); + } + if (length > MAX_METADATA_READ_BYTES - this.#readBytes) + throw artifactFixedLimitFailure( + "Bun metadata read work", + MAX_METADATA_READ_BYTES, + "bytes", + ); + // Charge physical work before allocation or I/O, including uncached large names. + this.#readBytes += length; + const bytes = await readExact( + this.handle, + this.start + offset, + length, + this.signal, + ); + if (length > METADATA_CACHE_BYTES) return bytes; + while ( + this.#ranges.size >= METADATA_CACHE_ENTRIES || + this.#retainedBytes + length > METADATA_CACHE_BYTES + ) { + const oldest = this.#ranges.keys().next().value!; + this.#retainedBytes -= this.#ranges.get(oldest)!.length; + this.#ranges.delete(oldest); + } + this.#ranges.set(key, bytes); + this.#retainedBytes += length; + return Buffer.from(bytes); + } +} + +const bunByteLimit = ( + claimed: number | bigint, + scope: "payload" | "metadata", +): ArtifactReaderFailure => { + const allowance = `Bun standalone ${scope} allowance of ${String(MAX_BUN_RETAINED_BYTES)} bytes`; + const message = `The ${scope} claims ${String(claimed)} bytes. ${fixedAllowanceRemediation(allowance, "parser")} This release has no runtime override.`; + return new ArtifactReaderFailure("limit", message, { + boundary: scope === "payload" ? "file-size" : "memory", + userMessage: message, + }); +}; +export const bunPayloadBytesLimit = ( + claimed: number | bigint, +): ArtifactReaderFailure => bunByteLimit(claimed, "payload"); +export const bunMetadataBytesLimit = ( + claimed: number | bigint, +): ArtifactReaderFailure => bunByteLimit(claimed, "metadata"); + +export const bunModuleCountLimit = (count: number): ArtifactReaderFailure => + new ArtifactReaderFailure( + "limit", + `Bun standalone module graph claims ${String(count)} modules, exceeding MAX_BUN_MODULE_COUNT (${String(MAX_BUN_MODULE_COUNT)} modules). This release has no runtime override. Host memory and NODE_OPTIONS do not raise it.`, + { boundary: "capacity" }, + ); + +export const bunFormatFailure = (detail: string): ArtifactReaderFailure => + new ArtifactReaderFailure( + "format", + `Bun standalone module graph is malformed: ${detail}`, + ); + +const presentFailure = ( + failure: ArtifactReaderFailure, +): BunStandaloneProbe => ({ status: "present", failure }); + +const evaluateSection = async ( + handle: FileHandle, + fileSize: number, + claim: SectionWindow, + signal?: AbortSignal, +): Promise => { + if (claim.truncated) + return presentFailure( + bunFormatFailure("section extends past the end of the file"), + ); + if (claim.available < BUN_LENGTH_PREFIX_BYTES) + return presentFailure(bunFormatFailure("trailer is truncated")); + const prefix = await readExact( + handle, + claim.fileOffset, + BUN_LENGTH_PREFIX_BYTES, + signal, + ); + let prefixBytes = BUN_LENGTH_PREFIX_BYTES; + let payloadLength = prefix.readBigUInt64LE(0); + // Legacy Mach-O writers used a u32 length. Validate its suffix before + // interpreting payload bytes as the upper half of a current u64 prefix. + if (claim.carrier === "mach-o-section") { + const legacyLength = prefix.readUInt32LE(0); + if ( + legacyLength >= BUN_GRAPH_SUFFIX_BYTES && + legacyLength + 4 <= claim.available + ) { + const suffix = await readExact( + handle, + claim.fileOffset + 4 + legacyLength - BUN_GRAPH_SUFFIX_BYTES, + BUN_GRAPH_SUFFIX_BYTES, + signal, + ); + if ( + suffix.subarray(BUN_OFFSETS_BYTES).equals(BUN_TRAILER) && + suffix.readBigUInt64LE(0) + BigInt(BUN_GRAPH_SUFFIX_BYTES) === + BigInt(legacyLength) + ) { + prefixBytes = 4; + payloadLength = BigInt(legacyLength); + } + } + } + if (payloadLength === 0n) return { status: "placeholder" }; + if (payloadLength > BigInt(MAX_BUN_RETAINED_BYTES)) + return presentFailure(bunPayloadBytesLimit(payloadLength)); + if (payloadLength < BigInt(BUN_GRAPH_SUFFIX_BYTES)) + return presentFailure(bunFormatFailure("trailer is truncated")); + const length = Number(payloadLength); + if (claim.available < prefixBytes + length) + return presentFailure(bunFormatFailure("trailer is truncated")); + const payloadOffset = claim.fileOffset + prefixBytes; + const suffix = await readExact( + handle, + payloadOffset + length - BUN_GRAPH_SUFFIX_BYTES, + BUN_GRAPH_SUFFIX_BYTES, + signal, + ); + const failure = suffixFailure(suffix, length); + if (failure !== undefined) return presentFailure(failure); + return { + status: "present", + claim: { carrier: claim.carrier, payloadOffset, payloadLength: length }, + }; +}; + +/** + * Older ELF builds append the graph at EOF without a named section. The + * trailer has to be the exact final bytes; this does not scan backward. + */ +const evaluateAppendedTrailer = async ( + handle: FileHandle, + fileSize: number, + signal?: AbortSignal, +): Promise => { + if (fileSize < BUN_GRAPH_SUFFIX_BYTES) return { status: "absent" }; + let graphEnd = fileSize; + let trailer = await readExact( + handle, + graphEnd - BUN_TRAILER_BYTES, + BUN_TRAILER_BYTES, + signal, + ); + if (!trailer.equals(BUN_TRAILER)) { + if (fileSize < BUN_GRAPH_SUFFIX_BYTES + 8) return { status: "absent" }; + graphEnd -= 8; + trailer = await readExact( + handle, + graphEnd - BUN_TRAILER_BYTES, + BUN_TRAILER_BYTES, + signal, + ); + if (!trailer.equals(BUN_TRAILER)) return { status: "absent" }; + const length = await readExact(handle, graphEnd, 8, signal); + if (length.readBigUInt64LE() !== BigInt(fileSize)) + return presentFailure( + bunFormatFailure("appended carrier length does not match file size"), + ); + } + const offsets = await readExact( + handle, + graphEnd - BUN_GRAPH_SUFFIX_BYTES, + BUN_OFFSETS_BYTES, + signal, + ); + const byteCount = offsets.readBigUInt64LE(0); + if ( + byteCount + BigInt(BUN_GRAPH_SUFFIX_BYTES) > + BigInt(MAX_BUN_RETAINED_BYTES) + ) + return presentFailure( + bunPayloadBytesLimit(byteCount + BigInt(BUN_GRAPH_SUFFIX_BYTES)), + ); + const payloadLength = byteCount + BigInt(BUN_GRAPH_SUFFIX_BYTES); + if (payloadLength > BigInt(graphEnd)) + return presentFailure(bunFormatFailure("trailer is truncated")); + const length = Number(payloadLength); + return { + status: "present", + claim: { + carrier: "appended-trailer", + payloadOffset: graphEnd - length, + payloadLength: length, + }, + }; +}; + +/** Validate the Offsets and trailer that end a payload of `payloadLength` bytes. */ +const suffixFailure = ( + suffix: Buffer, + payloadLength: number, +): ArtifactReaderFailure | undefined => { + if (suffix.length < BUN_GRAPH_SUFFIX_BYTES) + return bunFormatFailure("trailer is truncated"); + if (!suffix.subarray(BUN_OFFSETS_BYTES).equals(BUN_TRAILER)) + return bunFormatFailure("trailer does not match the Bun sentinel"); + const byteCount = suffix.readBigUInt64LE(0); + if (byteCount > BigInt(MAX_BUN_RETAINED_BYTES)) + return bunPayloadBytesLimit(byteCount); + if (byteCount + BigInt(BUN_GRAPH_SUFFIX_BYTES) !== BigInt(payloadLength)) + return bunFormatFailure("byte_count does not match the located payload"); + return undefined; +}; + +export const readPointer = ( + bytes: Buffer, + offset: number, +): BunStringPointer => ({ + offset: bytes.readUInt32LE(offset), + length: bytes.readUInt32LE(offset + 4), +}); + +const machOClaims = async ( + handle: FileHandle, + fileSize: number, + imageOffset: number, + imageLimit: number, + signal?: AbortSignal, +): Promise => { + if (imageOffset < 0 || imageOffset >= fileSize) return []; + const header = await tryRead(handle, imageOffset, 12, fileSize, signal); + if (header === null || header.length < 4) return []; + const facts = readMachoHeaderPrefixFacts(header); + if (facts.identity === null) + return imageOffset === 0 ? fatClaims(handle, fileSize, header, signal) : []; + const full = await tryRead( + handle, + imageOffset, + Math.min(32, imageLimit), + fileSize, + signal, + ); + if (full === null) return []; + const parsed = readMachoLoadCommandTableHeader(full, imageLimit); + if (!parsed.ok || parsed.value === null) return []; + const table = parsed.value; + const { + littleEndian, + headerSize: headerBytes, + commandBytes: sizeofcmds, + } = table; + if (sizeofcmds > MAX_LOAD_COMMAND_BYTES) return []; + const commands = await tryRead( + handle, + imageOffset + headerBytes, + sizeofcmds, + Math.min(fileSize, imageOffset + imageLimit), + signal, + ); + if (commands === null || commands.length !== sizeofcmds) return []; + const claims: SectionWindow[] = []; + const cursor = new MachoLoadCommandCursor(table); + while (cursor.nextOffset !== null) { + abortIfNeeded(signal); + const read = cursor.read(commands.subarray(cursor.nextOffset)); + if (!read.ok) break; + const { command: cmd, offset, size } = read.value; + if (cmd === LC_SEGMENT_64 || cmd === LC_SEGMENT) + claims.push( + ...segmentClaims( + commands.subarray(offset, offset + size), + littleEndian, + cmd === LC_SEGMENT_64, + imageOffset, + Math.min(fileSize, imageOffset + imageLimit), + ), + ); + } + return claims; +}; + +const segmentClaims = ( + command: Buffer, + littleEndian: boolean, + wide: boolean, + imageOffset: number, + fileSize: number, +): readonly SectionWindow[] => { + const headerBytes = wide ? SEGMENT_64_HEADER_BYTES : SEGMENT_32_HEADER_BYTES; + const sectionBytes = wide ? SECTION_64_BYTES : SECTION_32_BYTES; + if (command.length < headerBytes) return []; + const nsects = readU32(command, wide ? 64 : 48, littleEndian); + if (headerBytes + nsects * sectionBytes > command.length) return []; + const claims: SectionWindow[] = []; + for (let index = 0; index < nsects; index += 1) { + const start = headerBytes + index * sectionBytes; + const section = command.subarray(start, start + sectionBytes); + if (!exactName(section.subarray(0, 16), "__bun")) continue; + if (!exactName(section.subarray(16, 32), "__BUN")) continue; + const size = wide + ? readBoundedU64(section, 40, littleEndian) + : readU32(section, 36, littleEndian); + const offset = readU32(section, wide ? 48 : 40, littleEndian); + if (size === undefined) { + claims.push({ + carrier: "mach-o-section", + fileOffset: imageOffset + offset, + declaredSize: MAX_BUN_RETAINED_BYTES + 1, + available: 0, + truncated: false, + }); + continue; + } + claims.push( + windowFor("mach-o-section", imageOffset + offset, size, fileSize), + ); + } + return claims; +}; + +const fatClaims = async ( + handle: FileHandle, + fileSize: number, + prefix: Buffer, + signal?: AbortSignal, +): Promise => { + let parsed = readFatContainerHeader(prefix, fileSize); + if (parsed.state === "needs-data") { + const table = await tryRead( + handle, + 0, + parsed.requiredBytes, + fileSize, + signal, + ); + if (table === null) return []; + parsed = readFatContainerHeader(table, fileSize); + } + if (parsed.state !== "parsed") return []; + const claims: SectionWindow[] = []; + for (const slice of parsed.slices) { + abortIfNeeded(signal); + if (parsed.rangeIssues[slice.index] !== null) continue; + claims.push( + ...(await machOClaims( + handle, + fileSize, + slice.offset, + slice.size, + signal, + )), + ); + } + return claims; +}; + +const peClaims = async ( + handle: FileHandle, + fileSize: number, + signal?: AbortSignal, +): Promise => { + const dos = await tryRead(handle, 0, 64, fileSize, signal); + if (dos === null || dos.length < 64 || dos[0] !== 0x4d || dos[1] !== 0x5a) + return []; + const eLfanew = dos.readUInt32LE(0x3c); + if (eLfanew < 64 || eLfanew > fileSize - 24) return []; + const pe = await tryRead(handle, eLfanew, 24, fileSize, signal); + if (pe === null || !pe.subarray(0, 4).equals(Buffer.from("PE\0\0"))) + return []; + const sectionCount = Math.min(pe.readUInt16LE(6), MAX_PE_SECTIONS); + const optionalSize = pe.readUInt16LE(20); + const tableOffset = eLfanew + 24 + optionalSize; + const table = await tryRead( + handle, + tableOffset, + sectionCount * 40, + fileSize, + signal, + ); + if (table === null) return []; + const claims: SectionWindow[] = []; + for (let index = 0; index < sectionCount; index += 1) { + const start = index * 40; + if (start + 40 > table.length) break; + if (!exactName(table.subarray(start, start + 8), ".bun")) continue; + const virtualSize = table.readUInt32LE(start + 8); + const rawSize = table.readUInt32LE(start + 16); + const rawPointer = table.readUInt32LE(start + 20); + const declared = virtualSize === 0 ? rawSize : virtualSize; + const readable = Math.min(declared, rawSize); + claims.push(windowFor("pe-section", rawPointer, readable, fileSize)); + } + return claims; +}; + +const elfClaims = async ( + handle: FileHandle, + fileSize: number, + signal?: AbortSignal, +): Promise => { + const header = await tryRead(handle, 0, 64, fileSize, signal); + if ( + header === null || + header.length < 16 || + !header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) + ) + return []; + const wide = header[4] === 2; + if (header[4] !== 1 && header[4] !== 2) return []; + if (header[5] !== 1 && header[5] !== 2) return []; + const littleEndian = header[5] === 1; + const shentsize = wide ? 64 : 40; + const shoffOffset = wide ? 40 : 32; + const shnumOffset = wide ? 60 : 48; + const shstrndxOffset = wide ? 62 : 50; + if (header.length < (wide ? 64 : 52)) return []; + const observedShentsize = readU16(header, wide ? 58 : 46, littleEndian); + if (observedShentsize !== 0 && observedShentsize !== shentsize) return []; + const shoff = wide + ? readBoundedU64(header, shoffOffset, littleEndian) + : readU32(header, shoffOffset, littleEndian); + const shnum = readU16(header, shnumOffset, littleEndian); + const shstrndx = readU16(header, shstrndxOffset, littleEndian); + // Extended numbering and unreadable tables stay absent so a native ELF is + // not reclassified just because its section table could not be walked. + if ( + shoff === undefined || + shoff === 0 || + shnum === 0 || + shnum === 0xffff || + shstrndx >= shnum || + shnum * shentsize > MAX_SECTION_TABLE_BYTES + ) + return []; + const table = await tryRead( + handle, + shoff, + shnum * shentsize, + fileSize, + signal, + ); + if (table === null) return []; + const strings = elfSectionBounds(table, shstrndx, wide, littleEndian); + if (strings === null) return []; + const stringTable = await tryRead( + handle, + strings.offset, + Math.min(strings.size, MAX_SECTION_TABLE_BYTES), + fileSize, + signal, + ); + if (stringTable === null) return []; + const claims: SectionWindow[] = []; + for (let index = 0; index < shnum; index += 1) { + const bounds = elfSectionBounds(table, index, wide, littleEndian); + if (bounds === null) continue; + const nameOffset = readU32(table, index * shentsize, littleEndian); + if (!elfNameIsBun(stringTable, nameOffset)) continue; + claims.push(windowFor("elf-section", bounds.offset, bounds.size, fileSize)); + } + return claims; +}; + +const elfSectionBounds = ( + table: Buffer, + index: number, + wide: boolean, + littleEndian: boolean, +): { readonly offset: number; readonly size: number } | null => { + const shentsize = wide ? 64 : 40; + const start = index * shentsize; + if (start + shentsize > table.length) return null; + const offset = wide + ? readBoundedU64(table, start + 24, littleEndian) + : readU32(table, start + 16, littleEndian); + const size = wide + ? readBoundedU64(table, start + 32, littleEndian) + : readU32(table, start + 20, littleEndian); + if (offset === undefined || size === undefined) return null; + return { offset, size }; +}; + +const ELF_BUN_NAME = Buffer.from(".bun"); + +/** + * Compare only `.bun` and its terminator. Many headers may share one long + * name, so scanning each name to its NUL would repeat unbounded work. + */ +const elfNameIsBun = (table: Buffer, offset: number): boolean => { + const end = offset + ELF_BUN_NAME.length; + if (end > table.length) return false; + if (end < table.length && table[end] !== 0) return false; + return table.subarray(offset, end).equals(ELF_BUN_NAME); +}; + +const windowFor = ( + carrier: SectionWindow["carrier"], + fileOffset: number, + declaredSize: number, + fileSize: number, +): SectionWindow => { + if ( + !Number.isSafeInteger(fileOffset) || + fileOffset < 0 || + fileOffset >= fileSize + ) + return { + carrier, + fileOffset, + declaredSize, + available: 0, + truncated: false, + }; + const room = fileSize - fileOffset; + return { + carrier, + fileOffset, + declaredSize, + available: Math.min(declaredSize, room), + truncated: declaredSize > room, + }; +}; + +const exactName = (bytes: Buffer, text: string): boolean => { + const encoded = Buffer.alloc(bytes.length); + encoded.write(text, "utf8"); + return encoded.equals(bytes); +}; + +const readBoundedU64 = ( + bytes: Buffer, + offset: number, + littleEndian: boolean, +): number | undefined => { + if (offset < 0 || offset + 8 > bytes.length) return undefined; + const value = littleEndian + ? bytes.readBigUInt64LE(offset) + : bytes.readBigUInt64BE(offset); + if (value > BigInt(Number.MAX_SAFE_INTEGER)) return undefined; + return Number(value); +}; + +const readU32 = ( + bytes: Buffer, + offset: number, + littleEndian: boolean, +): number => + littleEndian ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); + +const readU16 = ( + bytes: Buffer, + offset: number, + littleEndian: boolean, +): number => + littleEndian ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset); + +const tryRead = async ( + handle: FileHandle, + position: number, + length: number, + fileSize: number, + signal?: AbortSignal, +): Promise => { + abortIfNeeded(signal); + if ( + !Number.isSafeInteger(position) || + !Number.isSafeInteger(length) || + length < 0 || + position < 0 || + length === 0 || + position > fileSize || + fileSize - position < length + ) + return length === 0 ? Buffer.alloc(0) : null; + return readExact(handle, position, length, signal); +}; + +const readExact = async ( + handle: FileHandle, + position: number, + length: number, + signal?: AbortSignal, +): Promise => { + const bytes = Buffer.alloc(length); + let offset = 0; + while (offset < length) { + abortIfNeeded(signal); + const read = await handle.read( + bytes, + offset, + length - offset, + position + offset, + ); + abortIfNeeded(signal); + if (read.bytesRead === 0) throw bunFormatFailure("trailer is truncated"); + offset += read.bytesRead; + } + return bytes; +}; diff --git a/src/artifacts/bun/BunModuleGraphMembers.ts b/src/artifacts/bun/BunModuleGraphMembers.ts new file mode 100644 index 0000000000..88b6dfa785 --- /dev/null +++ b/src/artifacts/bun/BunModuleGraphMembers.ts @@ -0,0 +1,365 @@ +import { setImmediate } from "node:timers/promises"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; +import { + BUN_RECORD_BYTES_1_3_3, + BUN_RECORD_BYTES_1_3_9, + MAX_BUN_MODULE_COUNT, + MAX_BUN_RETAINED_BYTES, + bunMetadataBytesLimit, + bunFormatFailure, + bunModuleCountLimit, + readPointer, + type BunLayoutRevision, + type BunStringPointer, + type LocatedBunGraph, + type BunGraphData, +} from "./BunModuleGraphLayout.js"; + +/** + * Loader discriminants are append-only in Bun 1.4.4 `src/ast/loader.rs`. + * Unknown ids stay raw. The stale C header value for TOML is not used. + */ +const LOADERS = [ + "jsx", + "js", + "ts", + "tsx", + "css", + "file", + "json", + "jsonc", + "toml", + "wasm", + "napi", + "base64", + "dataurl", + "text", + "bunsh", + "sqlite", + "sqlite-embedded", + "html", + "yaml", + "json5", + "md", + "xml", +] as const; + +const MODULE_FORMATS = ["none", "esm", "cjs"] as const; +const SIDES = ["server", "client"] as const; + +export interface BunTextFact { + readonly text: string | null; + readonly hex: string | null; +} + +export interface BunByteRange { + readonly offset: number; + readonly length: number; +} + +export interface BunDecodedModule { + readonly index: number; + readonly nameText: BunTextFact; + readonly contents: BunByteRange; + readonly sourcemap: BunByteRange | null; + readonly bytecode: BunByteRange | null; + readonly moduleInfo: BunByteRange | null; + readonly bytecodeOrigin: BunTextFact | null; + readonly encoding: number; + readonly encodingName: "binary" | "latin1" | "utf8" | null; + readonly loader: number; + readonly loaderName: string | null; + readonly moduleFormat: number; + readonly moduleFormatName: (typeof MODULE_FORMATS)[number] | null; + readonly side: number; + readonly sideName: (typeof SIDES)[number] | null; +} + +export interface BunDecodedGraph { + readonly revision: BunLayoutRevision; + readonly entryPointId: number; + readonly flags: number; + readonly compileExecArgv: BunTextFact; + readonly modules: readonly BunDecodedModule[]; +} + +/** + * Decode one located payload. Record size is selected by which known layout + * is structurally valid. An unknown or ambiguous size is unsupported, not a + * malformed graph. Flag bits stay raw: Bun 1.3 and 1.4 reuse the low bits. + */ +export const decodeBunModuleGraph = async ( + graph: LocatedBunGraph, +): Promise => { + const { data } = graph; + const table = graph.modules; + if (!pointerFits(data, table)) + throw bunFormatFailure("modules pointer exceeds the Bun module graph"); + if (table.length === 0) throw unsupported("the module table is empty"); + const candidates = recordSizes(table.length); + if (candidates.length === 0) + throw unsupported( + `record table length ${String(table.length)} matches no known revision`, + ); + // Count admission precedes table allocation, independently of payload size. + if (candidates.every((size) => table.length / size > MAX_BUN_MODULE_COUNT)) + throw bunModuleCountLimit(table.length / Math.max(...candidates)); + const records = await data.read(table.offset, table.length); + let recordBytes = candidates[0]!; + if (candidates.length > 1) { + const assessments = []; + for (const recordBytes of candidates) + assessments.push({ + recordBytes, + failure: await assessLayout( + data, + records, + recordBytes, + graph.entryPointId, + ), + }); + const valid = assessments.filter((item) => item.failure === null); + if (valid.length > 1) + throw unsupported( + `record table length ${String(table.length)} matches more than one known revision`, + ); + const selected = valid[0] ?? assessments[0]; + if (selected === undefined || selected.failure !== null) + throw ( + selected?.failure ?? + unsupported( + `record table length ${String(table.length)} matches no known revision`, + ) + ); + recordBytes = selected.recordBytes; + } + const count = table.length / recordBytes; + if (graph.entryPointId >= count) + throw bunFormatFailure( + `entry_point_id ${String(graph.entryPointId)} is outside the module table`, + ); + const revision: BunLayoutRevision = + recordBytes === BUN_RECORD_BYTES_1_3_9 ? "bun-1.3.9" : "bun-1.3.3"; + if (!pointerFits(data, graph.compileExecArgv)) + throw bunFormatFailure( + "compile_exec_argv pointer exceeds the Bun module graph", + ); + const modules: BunDecodedModule[] = []; + // Bound repeated pointer expansion before decoding strings. JSON escaping and + // UTF-16 strings can expand each source byte; aliased names count per record. + let retained = graph.compileExecArgv.length * 6; + if (retained > MAX_BUN_RETAINED_BYTES) throw bunMetadataBytesLimit(retained); + for (let index = 0; index < count; index += 1) { + const record = records.subarray( + index * recordBytes, + (index + 1) * recordBytes, + ); + retained += 512 + readPointer(record, 0).length * 6; + if (recordBytes === BUN_RECORD_BYTES_1_3_9) + retained += readPointer(record, 40).length * 6; + if (retained > MAX_BUN_RETAINED_BYTES) + throw bunMetadataBytesLimit(retained); + if (index % 256 === 0) await setImmediate(); + if (candidates.length === 1) { + const failure = await recordPointerFailure( + data, + record, + recordBytes, + false, + ); + if (failure !== null) throw failure; + } + modules.push(await decodeRecord(data, record, index, revision)); + } + return { + revision, + entryPointId: graph.entryPointId, + flags: graph.flags, + compileExecArgv: textFact( + await readNul(data, graph.compileExecArgv, "compile_exec_argv"), + true, + ), + modules, + }; +}; + +const recordSizes = (length: number): readonly number[] => { + const sizes: number[] = []; + if (length % BUN_RECORD_BYTES_1_3_3 === 0) sizes.push(BUN_RECORD_BYTES_1_3_3); + if (length % BUN_RECORD_BYTES_1_3_9 === 0) sizes.push(BUN_RECORD_BYTES_1_3_9); + return sizes; +}; + +const assessLayout = async ( + data: BunGraphData, + records: Buffer, + recordBytes: number, + entryPointId: number, +): Promise => { + const count = records.length / recordBytes; + if (count > MAX_BUN_MODULE_COUNT) return bunModuleCountLimit(count); + for (let index = 0; index < count; index += 1) { + const record = records.subarray( + index * recordBytes, + (index + 1) * recordBytes, + ); + if (index % 256 === 0) await setImmediate(); + const failure = await recordPointerFailure(data, record, recordBytes); + if (failure !== null) return failure; + } + if (entryPointId >= count) + return bunFormatFailure( + `entry_point_id ${String(entryPointId)} is outside the module table`, + ); + return null; +}; + +const recordPointerFailure = async ( + data: BunGraphData, + record: Buffer, + recordBytes: number, + verifyTextNuls = true, +): Promise => { + const fields: readonly (readonly [number, boolean, string])[] = + recordBytes === BUN_RECORD_BYTES_1_3_9 + ? [ + [0, true, "name"], + [8, true, "contents"], + [16, false, "sourcemap"], + [24, false, "bytecode"], + [32, false, "module_info"], + [40, true, "bytecode_origin"], + ] + : [ + [0, true, "name"], + [8, true, "contents"], + [16, false, "sourcemap"], + [24, false, "bytecode"], + ]; + for (const [offset, nul, label] of fields) { + const failure = await pointerFailure( + data, + readPointer(record, offset), + nul && (verifyTextNuls || offset === 8), + label, + ); + if (failure !== null) return failure; + } + return null; +}; + +const decodeRecord = async ( + data: BunGraphData, + record: Buffer, + index: number, + revision: BunLayoutRevision, +): Promise => { + const wide = revision === "bun-1.3.9"; + const metadataAt = wide ? 48 : 32; + const encoding = record[metadataAt] ?? 0; + const loader = record[metadataAt + 1] ?? 0; + const moduleFormat = record[metadataAt + 2] ?? 0; + const side = record[metadataAt + 3] ?? 0; + const name = await readNul(data, readPointer(record, 0), "name"); + const contents = readPointer(record, 8); + const sourcemap = optionalRange(readPointer(record, 16)); + const bytecode = optionalRange(readPointer(record, 24)); + const moduleInfo = wide ? optionalRange(readPointer(record, 32)) : null; + const origin = wide + ? textFact( + await readNul(data, readPointer(record, 40), "bytecode_origin"), + false, + ) + : null; + // Discriminant 2 was an unused Utf8 slot. Bun 1.4.4 reuses it, on the + // 52-byte record only, for little-endian UTF-16. No version discriminator + // distinguishes these meanings, so keep the wide layout value unnamed. + const encodingName = + encoding === 2 + ? wide + ? null + : "utf8" + : named(["binary", "latin1"] as const, encoding); + return { + index, + nameText: textFact(name, true), + contents: { offset: contents.offset, length: contents.length }, + sourcemap, + bytecode, + moduleInfo, + bytecodeOrigin: + origin === null || (origin.text === null && origin.hex === null) + ? null + : origin, + encoding, + encodingName, + loader, + loaderName: named(LOADERS, loader), + moduleFormat, + moduleFormatName: named(MODULE_FORMATS, moduleFormat), + side, + sideName: named(SIDES, side), + }; +}; + +const optionalRange = (pointer: BunStringPointer): BunByteRange | null => + pointer.length === 0 + ? null + : { offset: pointer.offset, length: pointer.length }; + +const named = ( + table: Names, + value: number, +): Names[number] | null => table[value] ?? null; + +const textFact = (bytes: Buffer, emptyIsText: boolean): BunTextFact => { + if (bytes.length === 0) + return emptyIsText ? { text: "", hex: null } : { text: null, hex: null }; + const text = bytes.toString("utf8"); + if (Buffer.from(text, "utf8").equals(bytes)) return { text, hex: null }; + return { text: null, hex: bytes.toString("hex") }; +}; + +const readNul = async ( + data: BunGraphData, + pointer: BunStringPointer, + label: string, +): Promise => { + const end = pointer.offset + pointer.length; + if (!pointerFits(data, pointer) || (pointer.length > 0 && end >= data.length)) + throw bunFormatFailure(`${label} pointer exceeds the Bun module graph`); + const bytes = await data.read( + pointer.offset, + pointer.length + (pointer.length > 0 ? 1 : 0), + ); + if (pointer.length > 0 && bytes[pointer.length] !== 0) + throw bunFormatFailure(`${label} is missing its terminating NUL`); + return bytes.subarray(0, pointer.length); +}; + +const pointerFits = (data: BunGraphData, pointer: BunStringPointer): boolean => + pointer.offset <= data.length && + pointer.offset + pointer.length <= data.length; + +const pointerFailure = async ( + data: BunGraphData, + pointer: BunStringPointer, + nul: boolean, + label: string, +): Promise => { + const end = pointer.offset + pointer.length; + if (pointer.offset > data.length || end > data.length) + return bunFormatFailure(`${label} pointer exceeds the Bun module graph`); + if (!nul || pointer.length === 0) return null; + if (end >= data.length) + return bunFormatFailure(`${label} pointer exceeds the Bun module graph`); + if ((await data.read(end, 1))[0] !== 0) + return bunFormatFailure(`${label} is missing its terminating NUL`); + return null; +}; + +const unsupported = (detail: string): ArtifactReaderFailure => + new ArtifactReaderFailure( + "unavailable", + `Bun standalone module graph layout is unsupported: ${detail}`, + ); diff --git a/src/artifacts/bun/BunStandalone.fixture.ts b/src/artifacts/bun/BunStandalone.fixture.ts new file mode 100644 index 0000000000..d770de6e9d --- /dev/null +++ b/src/artifacts/bun/BunStandalone.fixture.ts @@ -0,0 +1,246 @@ +import { + BUN_OFFSETS_BYTES, + BUN_RECORD_BYTES_1_3_3, + BUN_RECORD_BYTES_1_3_9, + BUN_TRAILER, + type BunLayoutRevision, + type BunStringPointer, +} from "./BunModuleGraphLayout.js"; + +export interface BunFixtureModule { + readonly name?: string | Buffer; + readonly contents?: Buffer; + readonly sourcemap?: Buffer; + readonly bytecode?: Buffer; + readonly moduleInfo?: Buffer; + readonly bytecodeOrigin?: string | Buffer; + readonly encoding?: number; + readonly loader?: number; + readonly moduleFormat?: number; + readonly side?: number; + readonly contentsPointer?: BunStringPointer; +} + +/** Build the graph payload: data, Offsets, and the Bun trailer. No length prefix. */ +export const bunModuleGraphPayload = (input: { + readonly revision?: BunLayoutRevision; + readonly modules?: readonly BunFixtureModule[]; + readonly entryPointId?: number; + readonly flags?: number; + readonly compileExecArgv?: string | Buffer; + readonly tableLength?: number; +}): Buffer => { + const revision = input.revision ?? "bun-1.3.9"; + const recordBytes = + revision === "bun-1.3.3" ? BUN_RECORD_BYTES_1_3_3 : BUN_RECORD_BYTES_1_3_9; + const modules = input.modules ?? []; + const chunks: Buffer[] = []; + let cursor = 0; + const place = (bytes: Buffer, nul: boolean): BunStringPointer => { + const offset = cursor; + if (bytes.length > 0) { + chunks.push(bytes); + cursor += bytes.length; + if (nul) { + chunks.push(Buffer.from([0])); + cursor += 1; + } + } + return { offset, length: bytes.length }; + }; + const placed = modules.map((module) => ({ + module, + name: place(asBuffer(module.name ?? ""), true), + contents: place(module.contents ?? Buffer.alloc(0), true), + sourcemap: place(module.sourcemap ?? Buffer.alloc(0), false), + bytecode: place(module.bytecode ?? Buffer.alloc(0), false), + moduleInfo: place(module.moduleInfo ?? Buffer.alloc(0), false), + origin: place(asBuffer(module.bytecodeOrigin ?? ""), true), + })); + const argv = place(asBuffer(input.compileExecArgv ?? ""), true); + const tableOffset = cursor; + const tableLength = input.tableLength ?? modules.length * recordBytes; + const records = Buffer.alloc( + Math.max(tableLength, modules.length * recordBytes), + ); + for (const [index, row] of placed.entries()) { + const record = records.subarray( + index * recordBytes, + (index + 1) * recordBytes, + ); + const contents = row.module.contentsPointer ?? row.contents; + writePointer(record, 0, row.name); + writePointer(record, 8, contents); + writePointer(record, 16, row.sourcemap); + writePointer(record, 24, row.bytecode); + const metadataAt = recordBytes === BUN_RECORD_BYTES_1_3_9 ? 48 : 32; + if (recordBytes === BUN_RECORD_BYTES_1_3_9) { + writePointer(record, 32, row.moduleInfo); + writePointer(record, 40, row.origin); + } + record[metadataAt] = row.module.encoding ?? 0; + record[metadataAt + 1] = row.module.loader ?? 1; + record[metadataAt + 2] = row.module.moduleFormat ?? 1; + record[metadataAt + 3] = row.module.side ?? 0; + } + chunks.push(records.subarray(0, tableLength)); + cursor += tableLength; + const data = Buffer.concat(chunks); + const offsets = Buffer.alloc(BUN_OFFSETS_BYTES); + offsets.writeBigUInt64LE(BigInt(data.length), 0); + writePointer(offsets, 8, { offset: tableOffset, length: tableLength }); + offsets.writeUInt32LE(input.entryPointId ?? 0, 16); + writePointer(offsets, 20, argv); + offsets.writeUInt32LE(input.flags ?? 0, 28); + return Buffer.concat([data, offsets, BUN_TRAILER]); +}; + +/** Length prefix used by current Mach-O, PE, and ELF section writers. */ +export const bunPrefixedPayload = (payload: Buffer): Buffer => { + const prefix = Buffer.alloc(8); + prefix.writeBigUInt64LE(BigInt(payload.length), 0); + return Buffer.concat([prefix, payload]); +}; + +export const machoBunSection = (section: Buffer): Buffer => { + const headerBytes = 32; + const commandBytes = 72 + 80; + const contentOffset = headerBytes + commandBytes; + const bytes = Buffer.alloc(contentOffset + section.length); + bytes.writeUInt32LE(0xfeedfacf, 0); + bytes.writeUInt32LE(0x0100000c, 4); + bytes.writeUInt32LE(2, 12); + bytes.writeUInt32LE(1, 16); + bytes.writeUInt32LE(commandBytes, 20); + bytes.writeUInt32LE(0x19, 32); + bytes.writeUInt32LE(commandBytes, 36); + bytes.write("__BUN", 40, "utf8"); + bytes.writeBigUInt64LE(BigInt(section.length), 64); + bytes.writeBigUInt64LE(BigInt(contentOffset), 72); + bytes.writeBigUInt64LE(BigInt(section.length), 80); + bytes.writeUInt32LE(1, 96); + bytes.write("__bun", 104, "utf8"); + bytes.write("__BUN", 120, "utf8"); + bytes.writeBigUInt64LE(BigInt(section.length), 144); + bytes.writeUInt32LE(contentOffset, 152); + section.copy(bytes, contentOffset); + return bytes; +}; + +/** Two real `__bun` sections. Inventory must not guess which graph to read. */ +export const machoBunSections = (sections: readonly Buffer[]): Buffer => { + const headerBytes = 32; + const sectionCount = sections.length; + const commandBytes = 72 + 80 * sectionCount; + let contentOffset = headerBytes + commandBytes; + const contents: Buffer[] = []; + const offsets: number[] = []; + for (const section of sections) { + offsets.push(contentOffset); + contents.push(section); + contentOffset += section.length; + } + const bytes = Buffer.alloc(contentOffset); + bytes.writeUInt32LE(0xfeedfacf, 0); + bytes.writeUInt32LE(0x0100000c, 4); + bytes.writeUInt32LE(2, 12); + bytes.writeUInt32LE(1, 16); + bytes.writeUInt32LE(commandBytes, 20); + bytes.writeUInt32LE(0x19, 32); + bytes.writeUInt32LE(commandBytes, 36); + bytes.write("__BUN", 40, "utf8"); + bytes.writeUInt32LE(sectionCount, 96); + sections.forEach((section, index) => { + const sectionHeader = 32 + 72 + index * 80; + const offset = offsets[index] ?? 0; + bytes.write("__bun", sectionHeader, "utf8"); + bytes.write("__BUN", sectionHeader + 16, "utf8"); + bytes.writeBigUInt64LE(BigInt(section.length), sectionHeader + 40); + bytes.writeUInt32LE(offset, sectionHeader + 48); + section.copy(bytes, offset); + }); + return bytes; +}; + +export const peBunSection = (section: Buffer): Buffer => { + const eLfanew = 64; + const optionalHeaderSize = 240; + const sectionTable = eLfanew + 24 + optionalHeaderSize; + const rawPointer = 512; + const rawSize = Math.max(512, Math.ceil(section.length / 512) * 512); + const bytes = Buffer.alloc(rawPointer + rawSize); + bytes.write("MZ", 0, "ascii"); + bytes.writeUInt32LE(eLfanew, 0x3c); + bytes.write("PE\0\0", eLfanew, "binary"); + bytes.writeUInt16LE(0x8664, eLfanew + 4); + bytes.writeUInt16LE(1, eLfanew + 6); + bytes.writeUInt16LE(optionalHeaderSize, eLfanew + 20); + bytes.writeUInt16LE(0x20b, eLfanew + 24); + bytes.write(".bun", sectionTable, "utf8"); + bytes.writeUInt32LE(section.length, sectionTable + 8); + bytes.writeUInt32LE(rawSize, sectionTable + 16); + bytes.writeUInt32LE(rawPointer, sectionTable + 20); + section.copy(bytes, rawPointer); + return bytes; +}; + +export const elfPrefix = (): Buffer => { + const bytes = Buffer.alloc(64); + bytes.writeUInt32LE(0x464c457f, 0); + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + bytes.writeUInt16LE(2, 16); + bytes.writeUInt16LE(0x3e, 18); + bytes.writeUInt32LE(1, 20); + bytes.writeUInt16LE(64, 52); + return bytes; +}; + +/** ELF64 with e_shoff 0 so only an exact EOF trailer can carry the graph. */ +export const elfAppendedBunPayload = (payload: Buffer): Buffer => + Buffer.concat([elfPrefix(), payload]); + +/** ELF64 whose `.bun` section holds the length-prefixed graph. */ +export const elfBunSection = (section: Buffer): Buffer => { + const shstr = Buffer.from("\0.shstrtab\0.bun\0"); + const shstrOffset = 64; + const sectionOffset = shstrOffset + shstr.length; + const tableOffset = sectionOffset + section.length; + const bytes = Buffer.alloc(tableOffset + 64 * 3); + elfPrefix().copy(bytes, 0); + bytes.writeBigUInt64LE(BigInt(tableOffset), 40); + bytes.writeUInt16LE(64, 52); + bytes.writeUInt16LE(64, 58); + bytes.writeUInt16LE(3, 60); + bytes.writeUInt16LE(1, 62); + shstr.copy(bytes, shstrOffset); + section.copy(bytes, sectionOffset); + writeElfSection(bytes, tableOffset + 64, 1, shstrOffset, shstr.length); + writeElfSection(bytes, tableOffset + 128, 11, sectionOffset, section.length); + return bytes; +}; + +const writeElfSection = ( + bytes: Buffer, + header: number, + name: number, + offset: number, + size: number, +): void => { + bytes.writeUInt32LE(name, header); + bytes.writeBigUInt64LE(BigInt(offset), header + 24); + bytes.writeBigUInt64LE(BigInt(size), header + 32); +}; + +const writePointer = ( + bytes: Buffer, + offset: number, + pointer: BunStringPointer, +): void => { + bytes.writeUInt32LE(pointer.offset, offset); + bytes.writeUInt32LE(pointer.length, offset + 4); +}; + +const asBuffer = (value: string | Buffer): Buffer => + typeof value === "string" ? Buffer.from(value) : value; diff --git a/src/artifacts/bun/BunStandaloneAdmission.ts b/src/artifacts/bun/BunStandaloneAdmission.ts new file mode 100644 index 0000000000..3a1a86a8b7 --- /dev/null +++ b/src/artifacts/bun/BunStandaloneAdmission.ts @@ -0,0 +1,9 @@ +import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js"; +import type { BunStandaloneProbe } from "./BunModuleGraphLayout.js"; + +/** Header and suffix facts belong to this exact admitted carrier. */ +export interface BunStandaloneAdmission { + readonly kind: "bun-standalone"; + readonly source: StableRegularFileDescriptor; + readonly probe: Extract; +} diff --git a/src/artifacts/bun/BunStandaloneArtifactReader.test.ts b/src/artifacts/bun/BunStandaloneArtifactReader.test.ts new file mode 100644 index 0000000000..af5efb26e6 --- /dev/null +++ b/src/artifacts/bun/BunStandaloneArtifactReader.test.ts @@ -0,0 +1,64 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { buffer } from "node:stream/consumers"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; +import { BunStandaloneArtifactReader } from "../BunStandaloneArtifactReader.js"; +import { + bunModuleGraphPayload, + elfAppendedBunPayload, +} from "./BunStandalone.fixture.js"; + +it("keeps duplicate and unsafe names distinct while extracting exact bytes", async () => { + const path = join(await createTestTempDirectory("rea-bun-reader-"), "app"); + await writeFile( + path, + elfAppendedBunPayload( + bunModuleGraphPayload({ + modules: [ + { name: "/$bunfs/a.js", contents: Buffer.from("first") }, + { name: "/$bunfs/a.js", contents: Buffer.from("second") }, + { name: "../../escape", contents: Buffer.from("third") }, + ], + }), + ), + ); + const reader = new BunStandaloneArtifactReader(path); + try { + const entries = await collect(reader.entries()); + expect(entries.map((entry) => entry.path)).toEqual([ + "$bunfs/a.js", + "$bunfs/a.js", + "bun-modules/2", + ]); + expect(new Set(entries.map((entry) => entry.adapterKey)).size).toBe(3); + for (const [index, entry] of entries.entries()) + expect((await buffer(await reader.open(entry))).toString()).toBe( + ["first", "second", "third"][index], + ); + const entry = entries[0]!; + await expect( + reader.open({ ...entry, byteOffset: 0 }), + ).rejects.toMatchObject({ reason: "integrity" }); + const controller = new AbortController(); + controller.abort(); + await expect(reader.open(entry, controller.signal)).rejects.toMatchObject({ + reason: "cancelled", + }); + await writeFile(path, "changed"); + await expect(reader.open(entry)).rejects.toMatchObject({ + reason: "integrity", + }); + } finally { + await reader.close(); + } + await expect(collect(reader.entries())).rejects.toMatchObject({ + reason: "unavailable", + }); +}); + +async function collect(values: AsyncIterable): Promise { + const result: T[] = []; + for await (const value of values) result.push(value); + return result; +} diff --git a/src/artifacts/createArtifactReader.ts b/src/artifacts/createArtifactReader.ts index 590580bc20..3cb90b7602 100644 --- a/src/artifacts/createArtifactReader.ts +++ b/src/artifacts/createArtifactReader.ts @@ -1,58 +1,23 @@ -import type { GodotPckAdmission } from "./GodotPckLocation.js"; -import type { PyInstallerAdmission } from "./pyinstaller/PyInstallerAdmission.js"; -import type { NodeSeaAdmission } from "./nodeSea/NodeSeaAdmission.js"; -import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js"; -import { AsarArtifactReader } from "./AsarArtifactReader.js"; -import type { ArtifactReader } from "./ArtifactReader.js"; -import { HexRecordArtifactReader } from "./HexRecordArtifactReader.js"; -import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; -import { EspImageArtifactReader } from "./EspImageArtifactReader.js"; -import { FitArtifactReader } from "./FitArtifactReader.js"; -import { GodotPckArtifactReader } from "./GodotPckArtifactReader.js"; -import { MachOSliceArtifactReader } from "./MachOSliceArtifactReader.js"; -import { NativeDmgArtifactReader } from "./NativeDmgArtifactReader.js"; -import { NodeSeaArtifactReader } from "./NodeSeaArtifactReader.js"; -import { PyInstallerArtifactReader } from "./PyInstallerArtifactReader.js"; -import { UImageArtifactReader } from "./UImageArtifactReader.js"; -import { UnityFsArtifactReader } from "./UnityFsArtifactReader.js"; -import { ZipArtifactReader } from "./ZipArtifactReader.js"; import type { ArtifactOccurrence } from "../domain/artifactGraph.js"; +import type { ArtifactReader } from "./ArtifactReader.js"; import { - isZipPackageFormat, - type ZipPackageFormat, -} from "../domain/zipPackageFormat.js"; -import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js"; + ARTIFACT_FORMATS, + type ArtifactReaderConstruction, +} from "./artifactFormats.js"; + +export type { ArtifactReaderConstruction } from "./artifactFormats.js"; /** Formats with an artifact entry reader; availability policy belongs to the caller. */ -export type ArtifactReaderFormat = - | "directory" - | "asar" - | "pyinstaller" - | "node-sea" - | "godot-pck" - | "unityfs" - | "uimage" - | "intel-hex" - | "srec" - | "fit" - | "mach-o-universal" - | "dmg" - | "esp-image" - | ZipPackageFormat; - -interface ArtifactReaderConstruction { - readonly environment: Readonly; - readonly unreadableDirectories: "fail" | "omit"; - readonly admission?: - | StableRegularFileDescriptor - | NodeSeaAdmission - | PyInstallerAdmission - | GodotPckAdmission - | undefined; - readonly asarHeaderAdmission?: AsarHeaderAdmission | undefined; - /** Caller-selected root spelling; only ASAR companion lookup consumes this. */ - readonly selectedPath?: string | undefined; -} +export type ArtifactReaderFormat = { + [Id in keyof typeof ARTIFACT_FORMATS]: (typeof ARTIFACT_FORMATS)[Id] extends { + readonly createReader: ( + path: string, + options: ArtifactReaderConstruction, + ) => ArtifactReader; + } + ? Id + : never; +}[keyof typeof ARTIFACT_FORMATS]; export function createArtifactReader( path: string, @@ -70,73 +35,8 @@ export function createArtifactReader( format: ArtifactOccurrence["artifact_format"], options: ArtifactReaderConstruction, ): ArtifactReader | undefined { - const admission = options.admission; - const rootSource = - admission !== undefined && "kind" in admission - ? admission.source - : admission; - if (isZipPackageFormat(format)) - return new ZipArtifactReader(path, format, undefined, rootSource); - switch (format) { - case "directory": - return new DirectoryArtifactReader(path, options.unreadableDirectories); - case "asar": - return new AsarArtifactReader(path, { - ...(options.asarHeaderAdmission === undefined - ? {} - : { headerAdmission: options.asarHeaderAdmission }), - ...(options.selectedPath === undefined - ? {} - : { selectedPath: options.selectedPath }), - }); - case "pyinstaller": - return new PyInstallerArtifactReader( - path, - admission !== undefined && - "kind" in admission && - admission.kind === "pyinstaller" - ? admission - : rootSource, - ); - case "node-sea": - return new NodeSeaArtifactReader( - path, - admission !== undefined && - "kind" in admission && - admission.kind === "node-sea" - ? admission - : rootSource, - ); - case "godot-pck": - return new GodotPckArtifactReader( - path, - admission !== undefined && - "kind" in admission && - admission.kind === "godot-pck" - ? admission - : rootSource, - ); - case "uimage": - return new UImageArtifactReader(path, rootSource); - case "unityfs": - return new UnityFsArtifactReader(path, rootSource); - case "intel-hex": - case "srec": - return new HexRecordArtifactReader(path, format, rootSource); - case "fit": - return new FitArtifactReader(path, rootSource); - case "mach-o-universal": - return new MachOSliceArtifactReader( - path, - options.environment, - undefined, - rootSource, - ); - case "dmg": - return new NativeDmgArtifactReader(path, options.environment); - case "esp-image": - return new EspImageArtifactReader(path, rootSource); - default: - return undefined; - } + const descriptor = ARTIFACT_FORMATS[format]; + return "createReader" in descriptor + ? descriptor.createReader(path, options) + : undefined; } diff --git a/src/artifacts/dotnet/DotnetBundle.fixture.ts b/src/artifacts/dotnet/DotnetBundle.fixture.ts new file mode 100644 index 0000000000..7904122202 --- /dev/null +++ b/src/artifacts/dotnet/DotnetBundle.fixture.ts @@ -0,0 +1,107 @@ +import { deflateRawSync } from "node:zlib"; + +/** Source-owned Microsoft bundle-layout fixture; no target executable is run. */ +export const dotnetBundleFixture = ( + files: readonly { + path: string; + bytes: Buffer; + compressed?: boolean; + storedBytes?: Buffer; + fileType?: number; + declaredSize?: number; + declaredOffset?: number; + }[] = [ + { + path: "App.dll", + bytes: Buffer.from("owned assembly bytes"), + fileType: 1, + }, + ], + majorVersion = 6, +): { bytes: Buffer; headerOffset: number; offsets: readonly number[] } => { + const host = Buffer.alloc(1024); + host.write("MZ"); + host.writeUInt32LE(128, 60); + host.write("PE\0\0", 128); + host.writeUInt16LE(0x8664, 132); + host.writeUInt16LE(1, 134); + host.writeUInt16LE(240, 148); + host.writeUInt16LE(0x22, 150); + const optional = 152; + host.writeUInt16LE(0x20b, optional); + host.writeUInt32LE(0x1000, optional + 16); + host.writeBigUInt64LE(0x140000000n, optional + 24); + host.writeUInt32LE(4096, optional + 32); + host.writeUInt32LE(512, optional + 36); + host.writeUInt32LE(8192, optional + 56); + host.writeUInt32LE(512, optional + 60); + host.writeUInt16LE(3, optional + 68); + host.writeUInt32LE(16, optional + 108); + host.write(".text", 392); + host.writeUInt32LE(512, 400); + host.writeUInt32LE(4096, 404); + host.writeUInt32LE(512, 408); + host.writeUInt32LE(512, 412); + host.writeUInt32LE(0x60000020, 428); + // Kept literal independently of the reader to pin Microsoft's apphost marker. + Buffer.from( + "8b1202b96a612038727b930214d7a03213f5b9e6efae3318ee3b2dce24b36aae", + "hex", + ).copy(host, 544); + const payloads = files.map( + (file) => + file.storedBytes ?? + (file.compressed ? deflateRawSync(file.bytes) : file.bytes), + ); + let offset = host.length; + const offsets = payloads.map((bytes) => { + const at = offset; + offset += bytes.length; + return at; + }); + const headerOffset = offset; + host.writeBigUInt64LE(BigInt(headerOffset), 536); + const header = [ + u32(majorVersion), + u32(0), + u32(files.length), + text("owned-fixture"), + ]; + if (majorVersion >= 2) header.push(Buffer.alloc(40)); + const table = files.flatMap((file, index) => [ + u64(file.declaredOffset ?? offsets[index]!), + u64(file.declaredSize ?? file.bytes.length), + ...(majorVersion === 6 + ? [u64(file.compressed ? payloads[index]!.length : 0)] + : []), + Buffer.from([file.fileType ?? 1]), + text(file.path), + ]); + return { + bytes: Buffer.concat([host, ...payloads, ...header, ...table]), + headerOffset, + offsets, + }; +}; + +const u32 = (value: number): Buffer => { + const out = Buffer.alloc(4); + out.writeUInt32LE(value); + return out; +}; +const u64 = (value: number): Buffer => { + const out = Buffer.alloc(8); + out.writeBigUInt64LE(BigInt(value)); + return out; +}; +const text = (value: string): Buffer => { + const bytes = Buffer.from(value); + const length: number[] = []; + let remaining = bytes.length; + do { + const part = remaining & 127; + remaining >>>= 7; + length.push(remaining ? part | 128 : part); + } while (remaining); + return Buffer.concat([Buffer.from(length), bytes]); +}; diff --git a/src/artifacts/dotnet/DotnetBundle.ts b/src/artifacts/dotnet/DotnetBundle.ts new file mode 100644 index 0000000000..36cfb66086 --- /dev/null +++ b/src/artifacts/dotnet/DotnetBundle.ts @@ -0,0 +1,292 @@ +import type { FileHandle } from "node:fs/promises"; + +import { readFileHandleChunks } from "../../filesystem/readFileHandleChunks.js"; +import { + ArtifactPathRegistry, + normalizeArtifactPath, +} from "../ArtifactPaths.js"; +import { abortIfNeeded } from "../ArtifactHash.js"; +import { + ArtifactReaderFailure, + artifactFixedLimitFailure, +} from "../ArtifactReader.js"; + +/** SHA-256 marker embedded by Microsoft's apphost; the preceding UInt64 is the header offset. */ +export const DOTNET_BUNDLE_SIGNATURE = Buffer.from( + "8b1202b96a612038727b930214d7a03213f5b9e6efae3318ee3b2dce24b36aae", + "hex", +); + +// These are resource budgets, not format limits. Metadata stays bounded even +// when malicious manifests alias the same bytes or declare many empty files. +const MAX_METADATA_BYTES = 16 * 1024 * 1024; +const MAX_ENTRIES = 100_000; +const MAX_PATH_BYTES = 64 * 1024; +const MAX_PATH_PARTS = 256; +const MAX_PATH_NODES = 250_000; +const MAX_BUNDLE_ENTRY_BYTES = 512 * 1024 * 1024; +const MAX_EXPANDED_BYTES = 2 * 1024 * 1024 * 1024; + +export interface DotnetBundleFile { + readonly path: string; + readonly offset: number; + readonly size: number; + readonly compressedSize: number; + readonly fileType: number; +} + +export interface DotnetBundle { + readonly majorVersion: number; + readonly minorVersion: number; + readonly headerOffset: number; + readonly files: readonly DotnetBundleFile[]; +} + +/** Scan [0, markerSearchEnd) for the first marker; header offsets use whole-file coordinates. */ +export const findDotnetBundleHeader = async ( + handle: FileHandle, + size: number, + signal?: AbortSignal, + markerSearchEnd = size, +): Promise => { + abortIfNeeded(signal); + let tail = Buffer.alloc(0); + if (markerSearchEnd === 0) return undefined; + try { + for await (const value of readFileHandleChunks(handle, { + end: markerSearchEnd - 1, + signal, + })) { + abortIfNeeded(signal); + const chunk: Buffer = value; + const bytes = Buffer.concat([tail, chunk]); + for ( + let index = bytes.indexOf(DOTNET_BUNDLE_SIGNATURE); + index >= 0; + index = bytes.indexOf(DOTNET_BUNDLE_SIGNATURE, index + 1) + ) { + if (index < 8) continue; + const offset = bytes.readBigUInt64LE(index - 8); + // HostWriter.IsBundle uses the first apphost marker. Later markers may + // belong to embedded native apphosts, whose coordinates are not this host's. + if (offset === 0n) return undefined; + if (offset > BigInt(size - 12)) + fail(".NET bundle header is outside the host file"); + return Number(offset); + } + // A complete marker already examined must not be examined again. + tail = bytes.subarray(Math.max(0, bytes.length - 39)); + } + } catch (cause) { + abortIfNeeded(signal); + throw cause; + } + return undefined; +}; + +/** Read Microsoft's v1/v2/v6 manifest without loading an assembly or executing the host. */ +export const readDotnetBundle = async ( + handle: FileHandle, + size: number, + headerOffset: number, + signal?: AbortSignal, +): Promise => { + const cursor = new BundleCursor(handle, size, headerOffset, signal); + const majorVersion = await cursor.u32(); + const minorVersion = await cursor.u32(); + if (![1, 2, 6].includes(majorVersion) || minorVersion !== 0) + throw new ArtifactReaderFailure( + "unavailable", + `Unsupported .NET bundle header ${majorVersion}.${minorVersion}; supported versions are 1.0, 2.0 and 6.0`, + ); + const count = await cursor.u32(); + if (count > MAX_ENTRIES) + throw artifactFixedLimitFailure( + ".NET bundle entry count", + MAX_ENTRIES, + "entries", + ); + await cursor.string(); // Bundle ID is not an authentication or content digest. + const configuration: { offset: number; size: number; type: number }[] = []; + if (majorVersion >= 2) { + configuration.push({ + offset: await cursor.u64(), + size: await cursor.u64(), + type: 3, + }); + configuration.push({ + offset: await cursor.u64(), + size: await cursor.u64(), + type: 4, + }); + const flags = await cursor.u64(); + if (flags > 1) + throw new ArtifactReaderFailure( + "unavailable", + `Unsupported .NET bundle flags: ${flags}`, + ); + } + const files: DotnetBundleFile[] = []; + const paths = new ArtifactPathRegistry({ + maxParts: MAX_PATH_PARTS, + maxNodes: MAX_PATH_NODES, + }); + let expandedBytes = 0; + for (let index = 0; index < count; index++) { + abortIfNeeded(signal); + const offset = await cursor.u64(); + const fileSize = await cursor.u64(); + const compressedSize = majorVersion === 6 ? await cursor.u64() : 0; + const fileType = (await cursor.take(1))[0]!; + const path = await cursor.string(); + const normalized = normalizeArtifactPath(path); + paths.add(normalized, "file"); + if ( + fileSize > MAX_BUNDLE_ENTRY_BYTES || + compressedSize > MAX_BUNDLE_ENTRY_BYTES + ) + throw artifactFixedLimitFailure( + ".NET bundle member stored or expanded size", + MAX_BUNDLE_ENTRY_BYTES, + "bytes", + ); + expandedBytes += fileSize; + if (expandedBytes > MAX_EXPANDED_BYTES) + throw artifactFixedLimitFailure( + ".NET bundle aggregate expanded size", + MAX_EXPANDED_BYTES, + "bytes", + ); + const storedSize = compressedSize || fileSize; + if (offset > headerOffset || storedSize > headerOffset - offset) + fail(`.NET bundle entry crosses the manifest or file boundary: ${path}`); + files.push({ path, offset, size: fileSize, compressedSize, fileType }); + } + const ranges = files + .filter((file) => (file.compressedSize || file.size) > 0) + .toSorted((a, b) => a.offset - b.offset); + let end = 0; + for (const file of ranges) { + if (file.offset < end) + fail(`Overlapping .NET bundle entries: ${file.path}`); + end = file.offset + (file.compressedSize || file.size); + } + for (const item of configuration) { + if (item.offset === 0 && item.size === 0) continue; + if ( + !files.some( + (file) => + file.fileType === item.type && + file.offset === item.offset && + file.size === item.size && + file.compressedSize === 0, + ) + ) + fail(".NET bundle configuration location disagrees with its file table"); + } + return { majorVersion, minorVersion, headerOffset, files }; +}; + +/** Cached positional reads bound allocation and correctly handle short reads. */ +class BundleCursor { + #position: number; + #cache = Buffer.alloc(0); + #cacheOffset = 0; + + constructor( + private readonly handle: FileHandle, + private readonly size: number, + private readonly start: number, + private readonly signal?: AbortSignal, + ) { + this.#position = start; + } + + async take(length: number): Promise { + abortIfNeeded(this.signal); + if (this.#position - this.start + length > MAX_METADATA_BYTES) + throw artifactFixedLimitFailure( + ".NET bundle metadata", + MAX_METADATA_BYTES, + "bytes", + ); + if (length > this.size - this.#position) + fail("Truncated .NET bundle manifest"); + const cached = this.#position - this.#cacheOffset; + if (cached < 0 || cached + length > this.#cache.length) { + this.#cacheOffset = this.#position; + this.#cache = Buffer.alloc( + Math.min(Math.max(length, 64 * 1024), this.size - this.#position), + ); + let read = 0; + while (read < this.#cache.length) { + abortIfNeeded(this.signal); + const reply = await this.handle + .read( + this.#cache, + read, + this.#cache.length - read, + this.#cacheOffset + read, + ) + .catch((cause: unknown) => { + abortIfNeeded(this.signal); + throw cause; + }); + if (reply.bytesRead === 0) fail("Truncated .NET bundle manifest"); + read += reply.bytesRead; + } + } + const at = this.#position - this.#cacheOffset; + this.#position += length; + return this.#cache.subarray(at, at + length); + } + + async u32(): Promise { + return (await this.take(4)).readUInt32LE(); + } + async u64(): Promise { + const value = (await this.take(8)).readBigUInt64LE(); + if (value > BigInt(Number.MAX_SAFE_INTEGER)) + throw artifactFixedLimitFailure( + ".NET bundle exact integer range", + Number.MAX_SAFE_INTEGER, + "integer units", + ); + return Number(value); + } + async string(): Promise { + let length = 0; + for (let shift = 0; shift <= 28; shift += 7) { + const byte = (await this.take(1))[0]!; + if (shift === 28 && byte > 15) fail("Invalid .NET bundle string length"); + length += (byte & 127) * 2 ** shift; + if ((byte & 128) !== 0) continue; + if (length > MAX_PATH_BYTES) + throw artifactFixedLimitFailure( + ".NET bundle decoded string", + MAX_PATH_BYTES, + "UTF-8 bytes", + ); + // Source failures are not encoding failures. Acquire bytes outside the + // decoder boundary so I/O and cancellation retain their original cause. + const bytes = await this.take(length); + try { + return new TextDecoder("utf-8", { + fatal: true, + ignoreBOM: true, + }).decode(bytes); + } catch (cause) { + throw new ArtifactReaderFailure( + "format", + "Invalid UTF-8 in .NET bundle manifest", + { cause }, + ); + } + } + return fail("Invalid .NET bundle string length"); + } +} + +const fail = (message: string): never => { + throw new ArtifactReaderFailure("format", message); +}; diff --git a/src/artifacts/dotnet/DotnetBundleAdmission.ts b/src/artifacts/dotnet/DotnetBundleAdmission.ts new file mode 100644 index 0000000000..a53c337377 --- /dev/null +++ b/src/artifacts/dotnet/DotnetBundleAdmission.ts @@ -0,0 +1,8 @@ +import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js"; + +/** The marker observation belongs to this unchanged, borrowed descriptor. */ +export interface DotnetBundleAdmission { + readonly kind: "dotnet-bundle"; + readonly source: StableRegularFileDescriptor; + readonly headerOffset: number; +} diff --git a/src/artifacts/extraction/ArtifactExtraction.ts b/src/artifacts/extraction/ArtifactExtraction.ts index 6e5b780060..62c96e4b20 100644 --- a/src/artifacts/extraction/ArtifactExtraction.ts +++ b/src/artifacts/extraction/ArtifactExtraction.ts @@ -7,6 +7,7 @@ import { import { hashStableRootArtifactHandle } from "../inventory/hashStableRootArtifact.js"; import { absoluteCallerPath } from "../../filesystem/callerPath.js"; import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js"; +import { artifactFormatDescriptor } from "../artifactFormats.js"; import { createArtifactReader } from "../createArtifactReader.js"; import { resolveAsarHeaderAdmission } from "../AsarHeader.js"; import { @@ -40,7 +41,6 @@ import { ArtifactTraversalObservation } from "../ArtifactTraversalObservation.js import { abortIfNeeded } from "../ArtifactHash.js"; import { SafeOutputTree } from "../SafeOutputTree.js"; import { SafeOutputTreeCreationFailure } from "../SafeOutputTreeCreationFailure.js"; -import { isZipPackageFormat } from "../../domain/zipPackageFormat.js"; import { artifactExtractionResultSchema, type ArtifactExtractionResult, @@ -582,19 +582,7 @@ const requireExtractableFormat = async ( // A thin Mach-O has no child entries of its own: inventory lists only its // root unless it carries an embedded pack, so it takes the same pack probe // as ELF and PE while only the universal container has a slice reader. - if ( - !directory && - format !== "asar" && - format !== "godot-pck" && - format !== "unityfs" && - format !== "uimage" && - format !== "intel-hex" && - format !== "srec" && - format !== "esp-image" && - format !== "fit" && - format !== "mach-o-universal" && - !isZipPackageFormat(format) - ) { + if (!directory && artifactFormatDescriptor(format)?.extractable !== true) { const detected = await withArtifactRegularFile( path, input.resourceScope, @@ -605,7 +593,9 @@ const requireExtractableFormat = async ( (await probes.godotPck()) !== undefined || ((format === "elf" || format === "pe" || format === "mach-o") && ((await probes.nodeSea()) !== undefined || - (await probes.pyInstaller()) !== undefined)); + (await probes.pyInstaller()) !== undefined || + (await probes.bunStandalone()).status === "present" || + (await probes.dotnetBundle()) !== undefined)); if (pack && input.expectedRootSha256 !== undefined) { const digest = await hashStableRootArtifactHandle( path, diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts index 3fbc1f6d48..97f4ab5df5 100644 --- a/src/artifacts/inventory/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -1,3 +1,4 @@ +import { artifactFormatDescriptor } from "../artifactFormats.js"; import { hasGodotPckHeader } from "../../domain/godotPckFormat.js"; import { hasUImageMagic } from "../../domain/uimageFormat.js"; import { hasUnityFsHeader } from "../../domain/unityfsFormat.js"; @@ -29,7 +30,6 @@ import type { } from "../../domain/artifactGraph.js"; import { hasZipSignature, - isZipPackageFormat, zipPackageFormatForPath, type ZipPackageFormat, } from "../../domain/zipPackageFormat.js"; @@ -136,7 +136,10 @@ export const createOccurrence = ( entry.byteOffset === null || entry.declaredSize === null ? null - : { offset: entry.byteOffset, length: entry.declaredSize }, + : { + offset: entry.byteOffset, + length: entry.byteLength ?? entry.declaredSize, + }, limitations: [...entry.limitations], ...(entry.declaredIntegrity === null ? {} @@ -278,22 +281,7 @@ export const rootOccurrenceFor = ( logical_path: ".", entry_kind: metadata.format === "directory" ? "directory" : "file", artifact_kind: - metadata.format === "directory" || - isZipPackageFormat(metadata.format) || - [ - "asar", - "godot-pck", - "unityfs", - "uimage", - "dmg", - "pkg", - "pyinstaller", - "node-sea", - "intel-hex", - "srec", - "esp-image", - "fit", - ].includes(metadata.format) + artifactFormatDescriptor(metadata.format)?.occurrenceContainer === true ? "container" : artifactRoleForFormat(metadata.path, metadata.format).kind, artifact_format: metadata.format, @@ -584,15 +572,7 @@ const artifactRoleForFormat = ( format: zipPackageFormatForPath(path) ?? format, }; if (format === "plist") return { kind: "plist", format }; - if ( - format === "godot-pck" || - format === "unityfs" || - format === "uimage" || - format === "intel-hex" || - format === "srec" || - format === "esp-image" || - format === "fit" - ) + if (artifactFormatDescriptor(format)?.roleContainer === true) return { kind: "container", format }; return { kind: ["native-addon", "dynamic-library"].includes(byPath.kind) diff --git a/src/artifacts/inventory/classify.ts b/src/artifacts/inventory/classify.ts index ac93f0f4b8..fffddcbf92 100644 --- a/src/artifacts/inventory/classify.ts +++ b/src/artifacts/inventory/classify.ts @@ -1,8 +1,9 @@ -import type { GodotPckAdmission } from "../GodotPckLocation.js"; -import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js"; +import type { + ArtifactReaderAdmission, + ArtifactFormatAdmission, +} from "../ArtifactReaderAdmission.js"; import { assertArtifactSelectedSource } from "../ArtifactSelectedSource.js"; import { artifactNameForClassification } from "../../domain/artifactNameSelection.js"; -import type { NodeSeaAdmission } from "../nodeSea/NodeSeaAdmission.js"; import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js"; import { ArtifactEmbeddedFormatProbes, @@ -24,9 +25,9 @@ import type { ArtifactOccurrence } from "../../domain/artifactGraph.js"; import { hasZipSignature, locateTrailingZipArchive, - isZipPackageFormat, zipRootPackageFormat, } from "../../domain/zipPackageFormat.js"; +import { artifactFormatDescriptor } from "../artifactFormats.js"; import { ArtifactReaderFailure } from "../ArtifactReader.js"; import type { HashResult } from "../ArtifactHash.js"; import { @@ -50,12 +51,7 @@ interface RootClassification { type RootInventoryClassification = RootClassification & { readonly rootSource: RootInventorySource | undefined; - readonly readerAdmission: - | StableRegularFileDescriptor - | NodeSeaAdmission - | PyInstallerAdmission - | GodotPckAdmission - | undefined; + readonly readerAdmission: ArtifactReaderAdmission | undefined; }; /** The admitted descriptor and the single owner that can confirm its close. */ @@ -162,17 +158,7 @@ export const classifyAndHashRootForInventory = async ( options.signal, )); const retainSource = - isZipPackageFormat(format) || - format === "mach-o-universal" || - format === "godot-pck" || - format === "unityfs" || - format === "uimage" || - format === "pyinstaller" || - format === "node-sea" || - format === "intel-hex" || - format === "srec" || - format === "esp-image" || - format === "fit"; + artifactFormatDescriptor(format)?.retainClassifiedSource === true; outcome = { kind: "completed", value: { @@ -212,12 +198,7 @@ const classifyRootFormat = async ( signal?: AbortSignal, asarHeaderAdmission?: AsarHeaderAdmission, selectedPath?: string, -): Promise< - | ArtifactOccurrence["artifact_format"] - | NodeSeaAdmission - | PyInstallerAdmission - | GodotPckAdmission -> => { +): Promise => { const { handle } = source; const probe = createArtifactClassificationProbe(asarHeaderAdmission); const chunk = Buffer.alloc(64 * 1024); @@ -306,8 +287,16 @@ const classifyRootFormat = async ( ); return { kind: "node-sea", source, location: sea, decoded }; } + const bun = await embeddedProbes.bunStandalone(); + if (bun.status === "present") + return { kind: "bun-standalone", source, probe: bun }; const packaged = await embeddedProbes.pyInstaller(); abortIfNeeded(signal); + if (format !== "mach-o-universal") { + const headerOffset = await embeddedProbes.dotnetBundle(); + if (headerOffset !== undefined) + return { kind: "dotnet-bundle", source, headerOffset }; + } // A valid cookie also proves enough bytes for a thin Mach-O identity; // PE and universal Mach-O classification already require stronger proof. if (packaged !== undefined) diff --git a/src/artifacts/inventory/reader.ts b/src/artifacts/inventory/reader.ts index 11f9514a97..b2bda7feed 100644 --- a/src/artifacts/inventory/reader.ts +++ b/src/artifacts/inventory/reader.ts @@ -1,11 +1,10 @@ -import type { GodotPckAdmission } from "../GodotPckLocation.js"; -import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js"; -import type { NodeSeaAdmission } from "../nodeSea/NodeSeaAdmission.js"; +import type { ArtifactReaderAdmission } from "../ArtifactReaderAdmission.js"; import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js"; import type { ArtifactReader, ArtifactReaderTraversal, } from "../ArtifactReader.js"; +import { artifactFormatDescriptor } from "../artifactFormats.js"; import { createArtifactReader } from "../createArtifactReader.js"; import type { ArtifactInventoryCoverage, @@ -15,23 +14,16 @@ import { isZipPackageFormat, ZIP_NON_ENTRY_LIMITATION, } from "../../domain/zipPackageFormat.js"; -import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js"; -import { ESP_IMAGE_LIMITATIONS } from "../../domain/firmware/espImageFacts.js"; - export const createReader = ( path: string, format: ArtifactOccurrence["artifact_format"], environment: Readonly, - readerAdmission?: - | StableRegularFileDescriptor - | NodeSeaAdmission - | PyInstallerAdmission - | GodotPckAdmission, + readerAdmission?: ArtifactReaderAdmission, asarHeaderAdmission?: AsarHeaderAdmission, selectedPath?: string, ): ArtifactReader | undefined => { if ( - (format === "mach-o-universal" || format === "dmg") && + artifactFormatDescriptor(format)?.requiresDarwin === true && process.platform !== "darwin" ) return undefined; @@ -60,70 +52,30 @@ export const inventoryCoverage = ( ...(isZipPackageFormat(reader.format) ? [ZIP_NON_ENTRY_LIMITATION] : []), - ...(reader.format === "godot-pck" - ? [ - "PCK inventory establishes packaged bytes and metadata; scenes, binary resources, GDScript bytecode and runtime signal behavior were not decoded.", - ] - : []), - ...(reader.format === "unityfs" - ? [ - "UnityFS inventory establishes packaged bytes and metadata; serialized objects, textures, meshes and runtime asset behavior were not decoded. LZMA-compressed members are unsupported.", - ] - : []), - ...(reader.format === "node-sea" - ? [ - "Node SEA inventory establishes the injected blob, flags, fuse sentinel, main script, optional code cache, snapshot, and assets. V8 snapshots and code cache are not deserialized, and the application is not executed.", - ] - : []), - ...(reader.format === "intel-hex" || reader.format === "srec" - ? [ - "Intel HEX/S-record inventory decodes data records into address-contiguous regions. It does not establish CPU, byte order, memory map, or that a declared start address is executable; gaps are unwritten, not zero.", - ] - : []), - ...(reader.format === "esp-image" - ? [ESP_IMAGE_LIMITATIONS.inventoryScope] - : []), - ...(reader.format === "uimage" - ? [ - "uImage inventory establishes the 64-byte legacy header facts and stored payload bytes. Compressed payloads are not decompressed, and the image is not executed.", - ] - : []), - ...(reader.format === "fit" - ? [ - "FIT inventory reports each /images node's stored bytes and its declared type, architecture, OS, compression, load and entry. Image data is not decompressed or decrypted, signatures are not verified, nothing is executed, and load and entry addresses are declarations.", - ] - : []), + ...(artifactFormatDescriptor(reader.format)?.readerLimitation === + undefined + ? [] + : [artifactFormatDescriptor(reader.format)!.readerLimitation!]), ...traversal.limitations, ], }; + const absent = artifactFormatDescriptor(format)?.absentInventory ?? "root"; // dmg and pkg reach here only after classifyRootFormat matched their bytes. - if (format === "dmg" || format === "pkg") + if (absent === "platform") return { coverage: { scope: "container-entries", status: "unknown" }, limitations: [ `${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`, ], }; - if (format === "mach-o-universal") + if (absent === "universal") return { coverage: { scope: "container-entries", status: "unknown" }, limitations: [ "Universal Mach-O slices require the native macOS lipo adapter.", ], }; - if ( - isZipPackageFormat(format) || - format === "directory" || - format === "asar" || - format === "godot-pck" || - format === "unityfs" || - format === "uimage" || - format === "node-sea" || - format === "intel-hex" || - format === "srec" || - format === "esp-image" || - format === "fit" - ) + if (absent === "unavailable") return { coverage: { scope: "container-entries", status: "unknown" }, limitations: [`${format} child inventory reader is unavailable.`], diff --git a/src/artifacts/inventory/scanReader.ts b/src/artifacts/inventory/scanReader.ts index 5258f80bc7..9873bbaa1e 100644 --- a/src/artifacts/inventory/scanReader.ts +++ b/src/artifacts/inventory/scanReader.ts @@ -177,10 +177,13 @@ const visitArtifactEntries = async ( : `${frame.prefix}/${entry.path}`, ); const expandableAsar = isExpandableAsar(entry, logicalPath); - // PyInstaller TOC rows are source-distinct even when their logical + // PyInstaller rows and Bun modules are source-distinct even when their logical // destinations collide. Inventory retains those observations; the // extraction path registry remains the authority for materialization. - if (frame.reader.format !== "pyinstaller") + if ( + frame.reader.format !== "pyinstaller" && + frame.reader.format !== "bun-standalone" + ) context.registry.add( logicalPath, expandableAsar ? "directory" : entry.kind, diff --git a/src/artifacts/javascript/JavaScriptSourceAnalysis.ts b/src/artifacts/javascript/JavaScriptSourceAnalysis.ts index e30049e412..e34916f626 100644 --- a/src/artifacts/javascript/JavaScriptSourceAnalysis.ts +++ b/src/artifacts/javascript/JavaScriptSourceAnalysis.ts @@ -4,6 +4,7 @@ import type { } from "../../domain/javascript/javascriptSourceAnalysis.js"; import { JavaScriptAnalysisWorker } from "../../javascript/analysis/JavaScriptAnalysisWorker.js"; import type { spawnOwnedProviderProcess } from "../../process/ProviderProcess.js"; +import { WorkerCleanupRegistry } from "../../process/OwnedWorkerRuntime.js"; import { ArtifactResourceScope } from "../ArtifactResourceScope.js"; interface RetainedJavaScriptSourceAnalysis { @@ -17,6 +18,21 @@ const retained = new WeakMap< RetainedJavaScriptSourceAnalysis >(); const closeHookRegistered = new WeakSet(); +const workerCleanups = new WeakMap< + ArtifactResourceScope, + WorkerCleanupRegistry +>(); + +const workerCleanupFor = ( + scope: ArtifactResourceScope, +): WorkerCleanupRegistry => { + let cleanup = workerCleanups.get(scope); + if (cleanup === undefined) { + cleanup = new WorkerCleanupRegistry(); + workerCleanups.set(scope, cleanup); + } + return cleanup; +}; export interface JavaScriptSourceAnalysisAcquisition { /** Keep a healthy worker until this scope closes. */ @@ -82,7 +98,12 @@ const checkoutJavaScriptSourceAnalysis = async ( throw cause; } } - const worker = new JavaScriptAnalysisWorker(limits, launcher, sourceCount); + const worker = new JavaScriptAnalysisWorker( + limits, + launcher, + sourceCount, + workerCleanupFor(scope), + ); // A concurrent analysis may have parked another healthy worker while the // replaced process was closing. Claim the slot only when it is still empty. // This extra process is closed through the port returned below. diff --git a/src/browser/CdpBrowserProvider.admission.test.ts b/src/browser/CdpBrowserProvider.admission.test.ts new file mode 100644 index 0000000000..4a698b92b6 --- /dev/null +++ b/src/browser/CdpBrowserProvider.admission.test.ts @@ -0,0 +1,56 @@ +import { expect, it } from "vitest"; + +import { createBrowserObservationProvider } from "../composition/browserObservation.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; +import { WebBundleAnalysisAdmission } from "./IsolatedWebBundleAnalyzer.js"; + +const input = analyzeWebBundleInputSchema.parse({ + cdp_endpoint: "http://127.0.0.1:9", + target_id: "page-1", + include_script_sources: true, +}); + +it("shares a capture limit when browser providers use one admission", async () => { + const admission = new WebBundleAnalysisAdmission(); + const first = createBrowserObservationProvider(admission); + const second = createBrowserObservationProvider(admission); + const pending = first.analyzeBundle(input); + const blocked = await second.analyzeBundle(input); + try { + expect(blocked.ok).toBe(false); + if (blocked.ok) throw new Error("expected the shared admission to refuse"); + expect(projectAnalysisError(blocked.error)).toMatchObject({ + code: "resource_constraint", + details: { + resource: "capacity", + reported_limits: { concurrent_web_bundle_analyses: 1 }, + }, + }); + } finally { + await pending; + await Promise.allSettled([first.close?.(), second.close?.()]); + } +}); + +it("does not share a capture limit across browser providers", async () => { + const first = createBrowserObservationProvider(); + const second = createBrowserObservationProvider(); + const pending = first.analyzeBundle(input); + const other = second.analyzeBundle(input); + try { + const blocked = await other; + expect(blocked.ok).toBe(false); + if (blocked.ok) throw new Error("expected capture to fail closed"); + expect(projectAnalysisError(blocked.error).code).not.toBe( + "resource_constraint", + ); + } finally { + await Promise.allSettled([ + pending, + other, + first.close?.(), + second.close?.(), + ]); + } +}); diff --git a/src/browser/CdpBrowserProvider.ts b/src/browser/CdpBrowserProvider.ts index 00316328a4..2fe78ceb5e 100644 --- a/src/browser/CdpBrowserProvider.ts +++ b/src/browser/CdpBrowserProvider.ts @@ -14,7 +14,10 @@ import { type BrowserTargetList, type WebPageInspection, } from "../domain/browserObservationSchemas.js"; -import { IsolatedWebBundleAnalyzer } from "./IsolatedWebBundleAnalyzer.js"; +import { + IsolatedWebBundleAnalyzer, + type WebBundleAnalysisAdmission, +} from "./IsolatedWebBundleAnalyzer.js"; import { sourceMapsWithCaptureExclusions } from "../domain/webSourceMapCoverage.js"; import type { AnalyzeWebBundleInput, @@ -97,7 +100,11 @@ const CLEANUP_DOMAINS = [ /** Passive browser observation through a selected user-owned CDP endpoint. */ export class CdpBrowserProvider implements BrowserObservationPort { readonly #owner = new CdpTargetSessionOwner(); - readonly #bundleAnalyzer = new IsolatedWebBundleAnalyzer(); + readonly #bundleAnalyzer: IsolatedWebBundleAnalyzer; + + constructor(admission?: WebBundleAnalysisAdmission) { + this.#bundleAnalyzer = new IsolatedWebBundleAnalyzer(undefined, admission); + } async close(): Promise { const outcomes = await Promise.allSettled([ diff --git a/src/browser/CdpCaptureEventHandlers.ts b/src/browser/CdpCaptureEventHandlers.ts index 6b65dba2cb..2a281df2d3 100644 --- a/src/browser/CdpCaptureEventHandlers.ts +++ b/src/browser/CdpCaptureEventHandlers.ts @@ -187,6 +187,8 @@ const sourceMapForScript = ( readonly budgetExhausted: boolean; } => { const declaredUrl = cdpStringValue(value); + if (value !== undefined && declaredUrl === undefined) + state.completeness.exclude("source_maps", "invalid_protocol_value"); if (declaredUrl === undefined || declaredUrl === "") return { sourceMapUrl: null, diff --git a/src/browser/ElectronFileScope.ts b/src/browser/ElectronFileScope.ts index facec35c8d..29ba753b41 100644 --- a/src/browser/ElectronFileScope.ts +++ b/src/browser/ElectronFileScope.ts @@ -38,34 +38,60 @@ type ElectronFileOperation = | "observe_javascript_runtime" | "list_javascript_runtime_targets"; -// ASAR descriptor ownership outlives an individual provider call; every later -// authorization retries retained close leases before admitting new work. -const resources = new ArtifactResourceScope(); +/** + * ASAR descriptor ownership for one host. Authorization and shutdown share + * this scope so another host's file work does not block or fail this one. + */ +export class ElectronFileScope { + readonly #resources = new ArtifactResourceScope(); + + async authorize( + value: string, + signal?: AbortSignal, + operation: ElectronFileOperation = "inspect_electron_page", + asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION, + ): Promise { + try { + return await this.#resources.run( + async () => + await resolveAuthorizedElectronFile( + value, + signal, + asarHeaderAdmission, + this.#resources, + ), + ); + } catch (cause: unknown) { + if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) + throw cause; + if (signal?.aborted === true) + throw new AnalysisCancelledError(operation, { cause: signal.reason }); + throw cause; + } + } + + retryCleanup(): Promise { + return this.#resources.retryCleanup(); + } +} + +/** Electron providers that do not pass a host scope. */ +const processElectronFiles = new ElectronFileScope(); /** Resolve one local file URL while rejecting remote hosts and encoded separators. */ -export const authorizedElectronFile = async ( +export const authorizedElectronFile = ( value: string, signal?: AbortSignal, operation: ElectronFileOperation = "inspect_electron_page", asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION, -): Promise => { - try { - return await resources.run( - async () => - await resolveAuthorizedElectronFile(value, signal, asarHeaderAdmission), - ); - } catch (cause: unknown) { - if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) - throw cause; - if (signal?.aborted === true) - throw new AnalysisCancelledError(operation, { cause: signal.reason }); - throw cause; - } -}; + files: ElectronFileScope = processElectronFiles, +): Promise => + files.authorize(value, signal, operation, asarHeaderAdmission); /** Retry retained ASAR descriptor cleanup during provider shutdown. */ -export const retryElectronFileCleanup = async (): Promise => - await resources.retryCleanup(); +export const retryElectronFileCleanup = ( + files: ElectronFileScope = processElectronFiles, +): Promise => files.retryCleanup(); /** Translate a retained ASAR descriptor failure at its provider boundary. */ export const mapElectronFileAuthorizationFailure = ( @@ -129,6 +155,7 @@ const resolveAuthorizedElectronFile = async ( value: string, signal: AbortSignal | undefined, asarHeaderAdmission: AsarHeaderAdmission, + resources: ArtifactResourceScope, ): Promise => { throwIfAborted(signal); let url: URL; @@ -171,7 +198,12 @@ const resolveAuthorizedElectronFile = async ( void cause; } throwIfAborted(signal); - return await authorizedAsarMember(path, signal, asarHeaderAdmission); + return await authorizedAsarMember( + path, + signal, + asarHeaderAdmission, + resources, + ); }; /** @@ -183,6 +215,7 @@ const authorizedAsarMember = async ( path: string, signal: AbortSignal | undefined, asarHeaderAdmission: AsarHeaderAdmission, + resources: ArtifactResourceScope, ): Promise => { const segments = path.split(sep); for (let index = 1; index < segments.length - 1; index += 1) { diff --git a/src/browser/IsolatedWebBundleAnalyzer.test.ts b/src/browser/IsolatedWebBundleAnalyzer.test.ts index d4d5df8176..560f2b6c91 100644 --- a/src/browser/IsolatedWebBundleAnalyzer.test.ts +++ b/src/browser/IsolatedWebBundleAnalyzer.test.ts @@ -14,9 +14,12 @@ import type { import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; import { analyzeCapturedWebScript } from "../domain/webBundleAnalyzer.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { OwnedWorkerRuntime } from "../process/OwnedWorkerRuntime.js"; import { err, ok } from "../domain/result.js"; import { IsolatedWebBundleAnalyzer, + WebBundleAnalysisAdmission, type WebBundleAnalysisWorkerPort, } from "./IsolatedWebBundleAnalyzer.js"; @@ -297,15 +300,19 @@ it("retains cleanup ownership after mixed worker/close failure and resolves it o { operation: "analyze_web_bundle" }, ); let closeAttempts = 0; - const analyzer = new IsolatedWebBundleAnalyzer(() => ({ - analyze: async () => err({ error: primary, completed: null }), - close: async () => { - closeAttempts += 1; - if (closeAttempts <= 2) throw closeFailure; - closeFailure.resolveCleanup(); - primary.resolveCleanup(); - }, - })); + const admission = new WebBundleAnalysisAdmission(); + const analyzer = new IsolatedWebBundleAnalyzer( + () => ({ + analyze: async () => err({ error: primary, completed: null }), + close: async () => { + closeAttempts += 1; + if (closeAttempts <= 2) throw closeFailure; + closeFailure.resolveCleanup(); + primary.resolveCleanup(); + }, + }), + admission, + ); expect((await analyzer.reserve()).ok).toBe(true); const result = await analyzer.analyze(captured, noSourceMaps, input); @@ -318,7 +325,10 @@ it("retains cleanup ownership after mixed worker/close failure and resolves it o completeness: { status: "partial", parsed_scripts: 0 }, }); - const replacement = new IsolatedWebBundleAnalyzer(() => inertWorker()); + const replacement = new IsolatedWebBundleAnalyzer( + () => inertWorker(), + admission, + ); analyzer.releaseReservation(); const blocked = await replacement.reserve(); expect(blocked.ok).toBe(false); @@ -331,19 +341,57 @@ it("retains cleanup ownership after mixed worker/close failure and resolves it o replacement.releaseReservation(); }); -it("enforces one process-wide capture reservation across controller instances", async () => { - const first = new IsolatedWebBundleAnalyzer(() => inertWorker()); - const second = new IsolatedWebBundleAnalyzer(() => inertWorker()); +it("shares one capture reservation when analyzers use one admission", async () => { + const admission = new WebBundleAnalysisAdmission(); + const first = new IsolatedWebBundleAnalyzer(() => inertWorker(), admission); + const second = new IsolatedWebBundleAnalyzer(() => inertWorker(), admission); expect((await first.reserve()).ok).toBe(true); const blocked = await second.reserve(); expect(blocked.ok).toBe(false); - if (blocked.ok) throw new Error("Expected process-wide admission refusal"); - expect(blocked.error._tag).toBe("AnalysisResourceConstraintError"); + if (blocked.ok) throw new Error("Expected shared admission refusal"); + expect(projectAnalysisError(blocked.error)).toMatchObject({ + code: "resource_constraint", + details: { + resource: "capacity", + reported_limits: { concurrent_web_bundle_analyses: 1 }, + }, + }); first.releaseReservation(); expect((await second.reserve()).ok).toBe(true); second.releaseReservation(); }); +it("does not fail one admission when another host's worker runtime is retained", async () => { + let fail = true; + const stuck = new OwnedWorkerRuntime({ + rootPrefix: "rea-web-host-isolation-", + retryCleanup: async () => { + if (fail) throw new Error("other host still owns its worker"); + }, + }); + stuck.retainForCleanup(); + try { + const analyzer = new IsolatedWebBundleAnalyzer(() => inertWorker()); + expect((await analyzer.reserve()).ok).toBe(true); + analyzer.releaseReservation(); + await expect(OwnedWorkerRuntime.retryPending()).rejects.toThrow( + "other host still owns its worker", + ); + } finally { + fail = false; + await stuck.close(); + } +}); + +it("does not share a capture reservation across independent admissions", async () => { + const first = new IsolatedWebBundleAnalyzer(() => inertWorker()); + const second = new IsolatedWebBundleAnalyzer(() => inertWorker()); + expect((await first.reserve()).ok).toBe(true); + expect((await second.reserve()).ok).toBe(true); + first.releaseReservation(); + second.releaseReservation(); +}); + const inertWorker = (): WebBundleAnalysisWorkerPort => ({ analyze: async () => err({ diff --git a/src/browser/IsolatedWebBundleAnalyzer.ts b/src/browser/IsolatedWebBundleAnalyzer.ts index 86fcae1c21..c34abe2132 100644 --- a/src/browser/IsolatedWebBundleAnalyzer.ts +++ b/src/browser/IsolatedWebBundleAnalyzer.ts @@ -22,51 +22,93 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup. import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { err, ok, type Result } from "../domain/result.js"; import { ProviderStartupDeadline } from "../process/ProviderDeadline.js"; -import { WebBundleAnalysisWorker } from "./WebBundleAnalysisWorker.js"; +import { + WebBundleAnalysisWorker, + WebBundleWorkerCleanup, +} from "./WebBundleAnalysisWorker.js"; -// A process-wide admission, rather than one permit per provider instance, bounds -// simultaneous web analyzer children and their source/result transfer reservations. export type WebBundleAnalysisWorkerPort = Pick< WebBundleAnalysisWorker, "analyze" | "close" >; -let activeAnalyzer: IsolatedWebBundleAnalyzer | undefined; +/** + * One host's web-analyzer permit and failed-cleanup set. Independent + * admissions do not share a reservation. + */ +export class WebBundleAnalysisAdmission { + readonly workers = new WebBundleWorkerCleanup(); + #active: IsolatedWebBundleAnalyzer | undefined; + readonly #pendingCleanup = new Set(); + + active(): IsolatedWebBundleAnalyzer | undefined { + return this.#active; + } + + hold(analyzer: IsolatedWebBundleAnalyzer): void { + this.#active = analyzer; + } + + release(analyzer: IsolatedWebBundleAnalyzer): void { + if (this.#active === analyzer) this.#active = undefined; + } + + pending(): readonly IsolatedWebBundleAnalyzer[] { + return [...this.#pendingCleanup]; + } + + retain(analyzer: IsolatedWebBundleAnalyzer): void { + this.#pendingCleanup.add(analyzer); + } + + forget(analyzer: IsolatedWebBundleAnalyzer): void { + this.#pendingCleanup.delete(analyzer); + } +} /** Own one bundle's sequential analysis and finalization through provider close. */ export class IsolatedWebBundleAnalyzer { - static readonly #pendingCleanup = new Set(); + readonly #admission: WebBundleAnalysisAdmission; #closed = false; #running: Promise> | undefined; #controller: AbortController | undefined; #worker: WebBundleAnalysisWorkerPort | undefined; + readonly createWorker: (heapMb: number) => WebBundleAnalysisWorkerPort; + constructor( - readonly createWorker: (heapMb: number) => WebBundleAnalysisWorkerPort = ( - heapMb, - ) => new WebBundleAnalysisWorker(heapMb), - ) {} + createWorker?: (heapMb: number) => WebBundleAnalysisWorkerPort, + admission: WebBundleAnalysisAdmission = new WebBundleAnalysisAdmission(), + ) { + this.#admission = admission; + this.createWorker = + createWorker ?? + ((heapMb) => + new WebBundleAnalysisWorker(heapMb, undefined, admission.workers)); + } /** Reserve capacity before capture, so concurrent captures cannot retain source inventories. */ async reserve(): Promise> { if (this.#closed) return err(new AnalysisCancelledError("analyze_web_bundle")); - if (activeAnalyzer !== undefined) + if (this.#admission.active() !== undefined) return err( new AnalysisResourceConstraintError( "analyze_web_bundle", "capacity", - "Another web-bundle capture or analysis owns the process-wide source and result reservation", + "Another web-bundle capture or analysis owns this admission's source and result reservation", { concurrent_web_bundle_analyses: 1 }, ), ); - // Retry exact earlier owners before a replacement capture can retain sources. + // Claim the slot before awaiting cleanup so another host cannot pass the check. + this.#admission.hold(this); try { - for (const owner of [...IsolatedWebBundleAnalyzer.#pendingCleanup]) + for (const owner of this.#admission.pending()) await owner.#releaseWorker(); - await WebBundleAnalysisWorker.retryPending(); + await this.#admission.workers.retry(); await this.#releaseWorker(); } catch (cause) { + this.#admission.release(this); return err( cause instanceof AnalysisError ? cause @@ -78,23 +120,15 @@ export class IsolatedWebBundleAnalyzer { ), ); } - if (this.#closed) + if (this.#closed) { + this.#admission.release(this); return err(new AnalysisCancelledError("analyze_web_bundle")); - if (activeAnalyzer !== undefined) - return err( - new AnalysisResourceConstraintError( - "analyze_web_bundle", - "capacity", - "Another web-bundle capture or analysis owns the process-wide source and result reservation", - { concurrent_web_bundle_analyses: 1 }, - ), - ); - activeAnalyzer = this; + } return ok(undefined); } releaseReservation(): void { - if (activeAnalyzer === this) activeAnalyzer = undefined; + this.#admission.release(this); } analyze( @@ -331,9 +365,9 @@ export class IsolatedWebBundleAnalyzer { try { await this.#worker?.close(); this.#worker = undefined; - IsolatedWebBundleAnalyzer.#pendingCleanup.delete(this); + this.#admission.forget(this); } catch (cause) { - IsolatedWebBundleAnalyzer.#pendingCleanup.add(this); + this.#admission.retain(this); throw cause; } } diff --git a/src/browser/WebBundleAnalysisWorker.ts b/src/browser/WebBundleAnalysisWorker.ts index b88d0af5c1..9bf2696a83 100644 --- a/src/browser/WebBundleAnalysisWorker.ts +++ b/src/browser/WebBundleAnalysisWorker.ts @@ -40,7 +40,11 @@ import { webBundleScriptAnalysisSchema, type WebBundleScriptAnalysis, } from "../domain/webBundleScriptAnalysis.js"; -import { OwnedWorkerRuntime } from "../process/OwnedWorkerRuntime.js"; +import { + OwnedWorkerRuntime, + WorkerCleanupRegistry, + processWorkerCleanup, +} from "../process/OwnedWorkerRuntime.js"; import { RuntimeControlUnavailableError, runtimeControlCapabilityError, @@ -120,7 +124,7 @@ export interface WebBundleScriptAnalysisFailure { /** Sequential inert-source analysis; only bounded completed facts cross into the host. */ export class WebBundleAnalysisWorker { - static readonly #pendingCleanup = new Set(); + readonly #workerCleanup: WebBundleWorkerCleanup; readonly #runtime: OwnedWorkerRuntime; readonly #files = new ArtifactResourceScope(); readonly #cleanup: AnalysisCleanupLease; @@ -177,7 +181,9 @@ export class WebBundleAnalysisWorker { constructor( readonly heapMb = DEFAULT_WEB_BUNDLE_HEAP_MB, launcher: typeof spawnOwnedProviderProcess = spawnOwnedProviderProcess, + workerCleanup: WebBundleWorkerCleanup = processWebBundleCleanup, ) { + this.#workerCleanup = workerCleanup; if ( !Number.isSafeInteger(heapMb) || heapMb < MIN_WEB_BUNDLE_HEAP_MB || @@ -189,6 +195,7 @@ export class WebBundleAnalysisWorker { this.#runtime = new OwnedWorkerRuntime({ rootPrefix: "rea-web-bundle-analysis-", launcher, + cleanupRegistry: workerCleanup.runtimes, retryCleanup: () => this.close(), onInputFailure: (cause) => { if (this.#activeDeadline?.interruption !== undefined) return; @@ -259,8 +266,7 @@ export class WebBundleAnalysisWorker { } static async retryPending(): Promise { - for (const worker of [...this.#pendingCleanup]) await worker.close(); - await OwnedWorkerRuntime.retryPending(); + await processWebBundleCleanup.retry(); } async analyze( @@ -826,7 +832,7 @@ export class WebBundleAnalysisWorker { } #cleanupFailure(cause: unknown, primary?: AnalysisError): AnalysisError { - WebBundleAnalysisWorker.#pendingCleanup.add(this); + this.#workerCleanup.retain(this); this.#runtime.retainForCleanup(); const cleanup = new ProviderCleanupError( PROVIDER, @@ -859,11 +865,39 @@ export class WebBundleAnalysisWorker { await this.#files.close(); await this.#runtime.close(); this.#reset(); - WebBundleAnalysisWorker.#pendingCleanup.delete(this); + this.#workerCleanup.release(this); } catch (cause) { - WebBundleAnalysisWorker.#pendingCleanup.add(this); + this.#workerCleanup.retain(this); this.#runtime.retainForCleanup(); throw cause; } } } + +/** Web-analyzer workers and their runtimes retained by one host. */ +export class WebBundleWorkerCleanup { + readonly #workers = new Set(); + readonly runtimes: WorkerCleanupRegistry; + + constructor(runtimes?: WorkerCleanupRegistry) { + this.runtimes = runtimes ?? new WorkerCleanupRegistry(); + } + + retain(worker: WebBundleAnalysisWorker): void { + this.#workers.add(worker); + } + + release(worker: WebBundleAnalysisWorker): void { + this.#workers.delete(worker); + } + + async retry(): Promise { + for (const worker of [...this.#workers]) await worker.close(); + await this.runtimes.retry(); + } +} + +/** Workers constructed without a host. Their runtimes stay on the process registry. */ +export const processWebBundleCleanup = new WebBundleWorkerCleanup( + processWorkerCleanup, +); diff --git a/src/cli.ts b/src/cli.ts index 6d6b70286c..b9829e68dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { writeAmpRegistrationBackup } from "./cli/ampRegistrationBackup.js"; +import { registerToolFamilies } from "./contracts/toolFamilyIds.js"; import { registerGpuCommands } from "./cli/gpuCommands.js"; import { registerNativeFunctionMatchingCommands } from "./cli/nativeFunctionMatchingCommands.js"; import { registerCaptureCommands } from "./cli/captureCommands.js"; @@ -79,9 +80,26 @@ import { excludeDeclaredSensitiveArguments } from "./cli/explicitSensitiveArgume import { rejectSurplusCliArguments } from "./cli/strictPositionals.js"; import { presentCliArgumentErrors } from "./cli/argumentErrors.js"; import { completionHelpNote, createCliCompletions } from "./cli/completions.js"; +import { + closeCliGuidanceResources, + registerGuidanceCommands, +} from "./cli/guidanceCommands.js"; +import { closeCliJavaScriptResources } from "./cli/javascriptApplicationAnalysis.js"; import { registerGuidedWorkflowCommands } from "./cli/guidedWorkflowCommands.js"; -export { closeCliJavaScriptResources } from "./cli/javascriptApplicationAnalysis.js"; +/** Close every process-owned resource used by CLI commands. */ +export const closeCliResources = async (): Promise => { + const results = await Promise.allSettled([ + closeCliJavaScriptResources(), + closeCliGuidanceResources(), + ]); + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length === 1) throw failures[0]; + if (failures.length > 1) + throw new AggregateError(failures, "CLI resource cleanup failed"); +}; /** * Build the one-shot Incur CLI without starting Hopper at import time. @@ -153,58 +171,75 @@ export const createCli = ( const cli = timedCli as unknown as CliInstance; registerSetupCommands(cli, logger, environment); - registerGuidedWorkflowCommands(cli, logger); - registerReverseEngineeringCommands(cli, logger, environment); - registerCutterCommands(cli, logger, environment); - registerCoreAnalysisCommands( - cli, - logger, - analysis.runDirectAnalysis, - environment, - ); - registerUtilityCommands(cli, logger, environment, analysis); - registerWasmCommands(cli, logger, environment); - registerApkSignatureCommands(cli, logger, environment); - registerCaptureCommands(cli, logger, environment); - registerSignalCommands(cli, logger, environment); - registerEvmCommands(cli, logger, environment); - registerDesktopApplicationCommands(cli, logger); - registerGoCommands(cli, logger); - registerIl2cppCommands(cli, logger); - registerModelCommands(cli, logger); - registerGpuCommands(cli, logger); - registerRustCommands(cli, logger); - registerPythonPycCommands(cli, logger); - registerErlangCommands(cli, logger); - registerSqliteCommands(cli, logger, environment); - registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); - registerManagedCommands(cli, logger, analysis.runProviderAnalysis); - registerAndroidCommands(cli, logger, environment); - registerApktoolCommands(cli, logger, environment); - registerPcapCommands(cli, logger, environment); - registerFlutterCommands(cli, logger); - registerJebCommands(cli, logger, environment); - registerAdbCommands(cli, logger, environment); - registerFirmwareCommands(cli, logger, environment); - registerBinaryDiagnosticsCommands(cli, logger, environment); - registerAnalysisViewCommands(cli, logger); - registerNativeFunctionMatchingCommands(cli, logger, environment); - registerFridaCommands(cli, logger, fridaInstrumentation); - registerEvidenceCommands(cli, logger); - registerEnvironmentCommands(cli, logger, environment); - registerProcessCommands(cli, logger, environment); - registerBrowserCommands(cli, logger); - registerWebScriptCommands(cli, logger); - registerWebModuleCommands(cli, logger, environment); - registerWebSourceLocationCommands(cli, logger, environment); - registerWebRuntimeCommands(cli, logger); - registerWebNetworkCaptureCommands(cli, logger, environment); - registerJavaScriptRecoveryCommands(cli, logger, environment); - registerAdvancedBrowserCommands(cli, logger); - registerBrowserScenarioCommands(cli, logger, environment); - registerElectronCommands(cli, logger, environment); - registerJavaScriptRuntimeObservationCommands(cli, logger, environment); - registerApplicationCommands(cli, logger); + registerToolFamilies({ + direct: () => + registerCoreAnalysisCommands( + cli, + logger, + analysis.runDirectAnalysis, + environment, + ), + // Enhanced workflows are MCP tools. Their CLI entry points live with core analysis. + enhanced: () => undefined, + native: () => { + registerReverseEngineeringCommands(cli, logger, environment); + registerCutterCommands(cli, logger, environment); + registerUtilityCommands(cli, logger, environment, analysis); + registerBinaryDiagnosticsCommands(cli, logger, environment); + }, + artifact: () => { + registerWasmCommands(cli, logger, environment); + registerApkSignatureCommands(cli, logger, environment); + registerCaptureCommands(cli, logger, environment); + registerSignalCommands(cli, logger, environment); + registerEvmCommands(cli, logger, environment); + registerDesktopApplicationCommands(cli, logger); + registerGoCommands(cli, logger); + registerIl2cppCommands(cli, logger); + registerModelCommands(cli, logger); + registerGpuCommands(cli, logger); + registerRustCommands(cli, logger); + registerPythonPycCommands(cli, logger); + registerErlangCommands(cli, logger); + registerSqliteCommands(cli, logger, environment); + registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); + }, + managed: () => + registerManagedCommands(cli, logger, analysis.runProviderAnalysis), + firmware: () => registerFirmwareCommands(cli, logger, environment), + android: () => registerAndroidCommands(cli, logger, environment), + pcap: () => registerPcapCommands(cli, logger, environment), + flutter: () => registerFlutterCommands(cli, logger), + frida: () => registerFridaCommands(cli, logger, fridaInstrumentation), + apktool: () => registerApktoolCommands(cli, logger, environment), + jeb: () => registerJebCommands(cli, logger, environment), + adb: () => registerAdbCommands(cli, logger, environment), + browser: () => { + registerBrowserCommands(cli, logger); + registerWebRuntimeCommands(cli, logger); + registerWebNetworkCaptureCommands(cli, logger, environment); + registerAdvancedBrowserCommands(cli, logger); + registerBrowserScenarioCommands(cli, logger, environment); + }, + electron: () => registerElectronCommands(cli, logger, environment), + "javascript-runtime": () => + registerJavaScriptRuntimeObservationCommands(cli, logger, environment), + application: () => { + registerGuidanceCommands(cli, logger); + registerGuidedWorkflowCommands(cli, logger); + registerAnalysisViewCommands(cli, logger); + registerEvidenceCommands(cli, logger); + registerEnvironmentCommands(cli, logger, environment); + registerProcessCommands(cli, logger, environment); + registerJavaScriptRecoveryCommands(cli, logger, environment); + registerWebScriptCommands(cli, logger); + registerWebModuleCommands(cli, logger, environment); + registerWebSourceLocationCommands(cli, logger, environment); + registerApplicationCommands(cli, logger); + }, + session: () => + registerNativeFunctionMatchingCommands(cli, logger, environment), + }); presentOmittableDefaults(cli); if (resultOutput !== undefined) streamCliCommandResults(cli, resultOutput); const serveStrict = rejectSurplusCliArguments(cli); diff --git a/src/cli/cliOutputFormatter.ts b/src/cli/cliOutputFormatter.ts index 049600d4ed..ed4cd5910b 100644 --- a/src/cli/cliOutputFormatter.ts +++ b/src/cli/cliOutputFormatter.ts @@ -323,21 +323,36 @@ const escapeTableCell = (value: string): string => (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`, ) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax) + .replace(/[\\|`*[\]~{}#]|_+/g, escapeInlineSyntax) + // Encode renderer-extension introducers at the same boundary as HTML + // characters. Generated entities must not themselves be re-escaped. + .replace(/[&<>:]|;(?=-?[)(])/g, (character) => { + switch (character) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case ":": + return ":"; + default: + return ";"; + } + }) + .replace(/([xXB8]-)\)/g, "$1)") .replace(/\r\n|\r|\n/g, "
"); +const encodeMarkdownWhitespace = (whitespace: string): string => + whitespace.replace(/ /g, " ").replace(/\t/g, " "); + /** Heading and paragraph facts are literal data, including block syntax. */ const escapeMarkdownText = (value: string): string => escapeTableCell(value) - .replace(/#/g, "\\#") .replace(/^(\s*)([-+=])/, "$1\\$2") .replace(/^(\s*\d{1,9})([.)])(?=\s)/, "$1\\$2") - .replace(/^[ \t]+/, (indent) => - indent.replace(/ /g, " ").replace(/\t/g, " "), - ); + .replace(/^[ \t]+/, encodeMarkdownWhitespace) + .replace(/[ \t]+$/u, encodeMarkdownWhitespace); const markdownHeading = (path: readonly string[]): string => `## ${escapeMarkdownText(path.join("."))}\n\n`; @@ -348,8 +363,8 @@ const renderTable = ( ): string => { if (headers.length === 0) return ""; const encodeTableCell = (value: string): string => - escapeTableCell(value).replace(/^ +| +$/g, (spaces) => - " ".repeat(spaces.length), + escapeTableCell(value).replace(/^[ \t]+|[ \t]+$/g, (spaces) => + encodeMarkdownWhitespace(spaces), ); const escapedHeaders = headers.map(encodeTableCell); const widths = escapedHeaders.map((header) => header.length); diff --git a/src/cli/guidanceCommands.ts b/src/cli/guidanceCommands.ts new file mode 100644 index 0000000000..34486f309b --- /dev/null +++ b/src/cli/guidanceCommands.ts @@ -0,0 +1,131 @@ +import { z } from "incur"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { + readGuidanceSection, + searchGuidance, +} from "../application/GuidanceAccess.js"; +import { + readGuidanceSectionInputSchema, + searchGuidanceLimitSchema, + searchGuidanceInputSchema, +} from "../contracts/guidanceContracts.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { Logger } from "pino"; +import type { CliInstance } from "./types.js"; + +/** Guidance descriptors owned by one CLI host. */ +export class CliGuidanceResources { + #retained: ArtifactResourceScope | undefined; + + resources(): ArtifactResourceScope { + this.#retained ??= new ArtifactResourceScope(); + return this.#retained; + } + + /** Release this host's descriptors, retaining failed cleanup for retry. */ + async close(): Promise { + const resources = this.#retained; + if (resources === undefined) return; + await resources.close(); + if (this.#retained === resources) this.#retained = undefined; + } +} + +/** Process CLI host used when a caller does not pass its own resources. */ +const processGuidanceResources = new CliGuidanceResources(); + +/** Release CLI-owned guidance descriptors, retaining failed cleanup for retry. */ +export const closeCliGuidanceResources = ( + resources: CliGuidanceResources = processGuidanceResources, +): Promise => resources.close(); + +/** Register offline search and retrieval for the installed REA guide bundle. */ +export const registerGuidanceCommands = ( + cli: CliInstance, + logger: Logger, + resources: CliGuidanceResources = processGuidanceResources, +): void => { + cli.command(CLI_COMMANDS.guidance, { + description: "Search or read the installed REA workflow guidance", + args: z.object({}), + options: z + .object({ + action: z + .enum(["search", "read"]) + .default("search") + .describe("Guidance action to perform: search or read"), + query: searchGuidanceInputSchema.shape.query + .optional() + .describe( + "Required with search; all non-generic query words must match", + ), + locator: readGuidanceSectionInputSchema.shape.locator + .optional() + .describe("Required with --action read; locator returned by search"), + limit: searchGuidanceLimitSchema + .optional() + .describe("Maximum search matches; search only, defaults to 8"), + cursor: searchGuidanceInputSchema.shape.cursor + .optional() + .describe("Continue a search page; search only"), + }) + .superRefine((options, context) => { + if (options.action === "search") { + if (options.query === undefined) + context.addIssue({ + code: "custom", + path: ["query"], + message: "--query is required with --action search", + }); + if (options.locator !== undefined) + context.addIssue({ + code: "custom", + path: ["locator"], + message: "--locator can only be used with --action read", + }); + } else { + if (options.locator === undefined) + context.addIssue({ + code: "custom", + path: ["locator"], + message: "--locator is required with --action read", + }); + for (const option of ["query", "limit", "cursor"] as const) + if (options[option] !== undefined) + context.addIssue({ + code: "custom", + path: [option], + message: `--${option} can only be used with --action search`, + }); + } + }), + run: ({ options }) => + logCliCommand(logger, CLI_COMMANDS.guidance, async () => { + const result = + options.action === "search" + ? await searchGuidance( + { + ...(options.query === undefined + ? {} + : { query: options.query }), + ...(options.limit === undefined + ? {} + : { limit: options.limit }), + ...(options.cursor === undefined + ? {} + : { cursor: options.cursor }), + }, + resources.resources(), + ) + : await readGuidanceSection( + { + locator: options.locator, + }, + resources.resources(), + ); + return result.ok ? result.value : projectAnalysisError(result.error); + }), + }); +}; diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index 88f482fe80..06a6be6612 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -50,40 +50,48 @@ export const cliJavaScriptSourcePaths = async ( }; /** - * One scope for every JavaScript analysis in this process. A healthy worker - * stays parked between commands; {@link closeCliJavaScriptResources} stops it. - * Pending cleanup is still retried before each command returns. + * One parked JavaScript worker for one CLI host. A healthy worker stays + * parked between that host's commands. Independent hosts do not share it. */ -let retainedScope: ArtifactResourceScope | undefined; -/** One shutdown shared by the exit callback and the dispatch `finally`. */ -let closing: Promise | undefined; +export class CliJavaScriptAnalysisHost { + #retainedScope: ArtifactResourceScope | undefined; + /** One shutdown shared by the exit callback and the dispatch `finally`. */ + #closing: Promise | undefined; -const cliAnalysisScope = (): ArtifactResourceScope => { - retainedScope ??= new ArtifactResourceScope(); - return retainedScope; -}; + scope(): ArtifactResourceScope { + this.#retainedScope ??= new ArtifactResourceScope(); + return this.#retainedScope; + } -/** - * Stop the process-owned JavaScript worker. Callers that overlap share the - * attempt in flight. The scope stays owned when shutdown fails, so the next - * call retries instead of treating the worker as already released. - */ -export const closeCliJavaScriptResources = (): Promise => { - if (closing !== undefined) return closing; - const scope = retainedScope; - if (scope === undefined) return Promise.resolve(); - let published: Promise; - published = scope - .close() - .then(() => { - if (retainedScope === scope) retainedScope = undefined; - }) - .finally(() => { - if (closing === published) closing = undefined; - }); - closing = published; - return published; -}; + /** + * Stop this host's worker. Overlapping callers share the attempt in flight. + * The scope stays owned when shutdown fails, so the next call retries. + */ + close(): Promise { + if (this.#closing !== undefined) return this.#closing; + const scope = this.#retainedScope; + if (scope === undefined) return Promise.resolve(); + let published: Promise; + published = scope + .close() + .then(() => { + if (this.#retainedScope === scope) this.#retainedScope = undefined; + }) + .finally(() => { + if (this.#closing === published) this.#closing = undefined; + }); + this.#closing = published; + return published; + } +} + +/** Process CLI host used when a caller does not pass its own host. */ +const processJavaScriptAnalysis = new CliJavaScriptAnalysisHost(); + +/** Stop the process-owned JavaScript worker. */ +export const closeCliJavaScriptResources = ( + host: CliJavaScriptAnalysisHost = processJavaScriptAnalysis, +): Promise => host.close(); /** Execute the shared CLI boundary for static JavaScript analysis. */ export const runCliJavaScriptApplicationAnalysis = async ( @@ -95,6 +103,7 @@ export const runCliJavaScriptApplicationAnalysis = async ( readonly evidenceOutput: string | undefined; readonly view?: AnalysisViewRequest; }, + host: CliJavaScriptAnalysisHost = processJavaScriptAnalysis, ): Promise => { const { analyzeJavaScriptApplication } = await import("../application/javascript/JavaScriptApplicationService.js"); @@ -104,7 +113,7 @@ export const runCliJavaScriptApplicationAnalysis = async ( }, { minimumIntervalMs: 0 }, ); - const resources = cliAnalysisScope(); + const resources = host.scope(); const result = await analyzeJavaScriptApplication(input, resources, { environment, progress, diff --git a/src/cli/javascriptRuntimeCommands.ts b/src/cli/javascriptRuntimeCommands.ts index 0e542057d3..a27a36c850 100644 --- a/src/cli/javascriptRuntimeCommands.ts +++ b/src/cli/javascriptRuntimeCommands.ts @@ -6,6 +6,7 @@ import { observeJavaScriptRuntime, } from "../application/javascript/JavaScriptRuntimeObservationService.js"; import { createJavaScriptRuntimeObservationProvider } from "../composition/javascriptRuntimeObservation.js"; +import { V8InspectorAdmission } from "../inspector/V8InspectorProvider.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import { @@ -40,6 +41,13 @@ export const registerJavaScriptRuntimeObservationCommands = ( logger: Logger, environment: Readonly>, ): void => { + const admission = new V8InspectorAdmission(); + const providerForCommand = () => + createJavaScriptRuntimeObservationProvider( + environment, + undefined, + admission, + ); cli.command(CLI_COMMANDS.listJavaScriptRuntimeTargets, { description: "List Node/Electron V8 Inspector targets", args: z.object({ @@ -51,8 +59,7 @@ export const registerJavaScriptRuntimeObservationCommands = ( logger, CLI_COMMANDS.listJavaScriptRuntimeTargets, async () => { - const provider = - createJavaScriptRuntimeObservationProvider(environment); + const provider = providerForCommand(); const input = { inspector_endpoint: args.endpoint, }; @@ -97,8 +104,7 @@ export const registerJavaScriptRuntimeObservationCommands = ( logger, CLI_COMMANDS.observeJavaScriptRuntime, async () => { - const provider = - createJavaScriptRuntimeObservationProvider(environment); + const provider = providerForCommand(); const input = { inspector_endpoint: args.endpoint, target_id: args.targetId, diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts index a645bb3285..87d9ff95c1 100644 --- a/src/cli/reverseEngineeringCommands.ts +++ b/src/cli/reverseEngineeringCommands.ts @@ -9,8 +9,9 @@ import { AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import { evidenceSchema, type Evidence } from "../domain/evidence.js"; +import type { JsonValue } from "../domain/jsonValue.js"; import type { Result } from "../domain/result.js"; -import { err } from "../domain/result.js"; +import { err, ok } from "../domain/result.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { OBJDUMP_ARTIFACT_OPERATIONS, @@ -26,11 +27,15 @@ import { gdbOneShotCommandMayInvalidateDetachProof, } from "../gdb/GdbSessionManager.js"; import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; +import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { analysisEvidenceDeliveryOptions, cliEvidenceDeliveryInputError, + deliverCliAnalysisEvidence, publishCliAnalysisEvidence, } from "./analysisEvidenceDelivery.js"; +import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; +import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js"; const executeWithCleanup = async ( service: ReverseEngineeringService, @@ -117,6 +122,48 @@ const asCleanupError = (cause: unknown): AnalysisError => { cause }, ); +/** One-shot debugger cleanup: the retained owner's closeAll decides whether cleanup remains incomplete. */ +const closeDebuggerCommandSession = async ( + manager: { + close(sessionId: string): Promise>; + closeAll(): Promise; + }, + sessionId: string | undefined, + providerId: string, + operation: string, + log: (cleanup: JsonValue | string) => void, +): Promise => { + if (sessionId !== undefined) { + try { + const closed = await manager.close(sessionId); + if (!closed.ok) log(projectAnalysisError(closed.error)); + } catch (cause) { + log(cause instanceof Error ? cause.message : "Unknown cleanup failure"); + } + } + try { + await manager.closeAll(); + } catch (cause) { + throw new ProviderCleanupError( + providerId, + sessionId === undefined ? [] : [sessionId], + { + reason: + cause instanceof AggregateError + ? cause.errors + .map((error: unknown) => + error instanceof Error ? error.message : String(error), + ) + .join("; ") + : cause instanceof Error + ? cause.message + : String(cause), + }, + { cause, operation }, + ); + } +}; + /** Add one-shot reverse-engineering commands using the shared service layer. */ export const registerReverseEngineeringCommands = ( cli: CliInstance, @@ -310,16 +357,13 @@ export const registerReverseEngineeringCommands = ( ); const manager = createGdbManager(environment); let sessionId: string | undefined; - const completed = await (async () => { - try { + return deliverCliAnalysisEvidence({ + ...delivery, + run: async () => { const started = await manager.start(signal, { inferiorTty: options.inferiorTty, }); - if (!started.ok) - return { - ok: false as const, - error: projectAnalysisError(started.error), - }; + if (!started.ok) return err(started.error); sessionId = started.value.session_id; const executed = await manager.execute( sessionId, @@ -327,59 +371,24 @@ export const registerReverseEngineeringCommands = ( signal, ); return executed.ok - ? { ok: true as const, value: executed.value.evidence } - : { - ok: false as const, - error: projectAnalysisError(executed.error), - }; - } finally { - let cleanupFailed = false; - if (sessionId !== undefined) { - try { - const closed = await manager.close(sessionId); - cleanupFailed = !closed.ok; - if (!closed.ok) + ? ok(executed.value.evidence) + : err(executed.error); + }, + service: { + close: () => + closeDebuggerCommandSession( + manager, + sessionId, + "gdb", + "gdb_console", + (cleanup) => logger.error( - { - command: CLI_COMMANDS.debugWithGdb, - cleanup: projectAnalysisError(closed.error), - }, + { command: CLI_COMMANDS.debugWithGdb, cleanup }, "GDB cleanup failed after collecting command output", - ); - } catch (cause) { - cleanupFailed = true; - logger.error( - { - command: CLI_COMMANDS.debugWithGdb, - cleanup: - cause instanceof Error - ? cause.message - : "Unknown cleanup failure", - }, - "GDB cleanup failed after collecting command output", - ); - } - } - try { - await manager.closeAll(); - cleanupFailed = false; - } catch (cause) { - cleanupFailed = true; - logger.error( - { - command: CLI_COMMANDS.debugWithGdb, - cleanup: - cause instanceof Error - ? cause.message - : "Unknown cleanup failure", - }, - "GDB fallback cleanup failed", - ); - } - if (cleanupFailed) process.exitCode = 1; - } - })(); - return publishCliAnalysisEvidence(completed, delivery); + ), + ), + }, + }); }), ), }); @@ -410,68 +419,47 @@ export const registerReverseEngineeringCommands = ( logCliCommand(logger, CLI_COMMANDS.debugWithRizin, async () => { const manager = new RizinDebugSessionManager(environment); let sessionId: string | undefined; - try { - const started = await manager.start( - { - ...args, - inferior_tty: options.inferiorTty, - ...(options.backend === undefined - ? {} - : { backend: options.backend }), - }, - signal, - ); - if (!started.ok) return projectAnalysisError(started.error); - sessionId = started.value.session_id; - const executed = await manager.execute( - started.value.session_id, - args.command, - signal, - ); - return executed.ok - ? executed.value.evidence - : projectAnalysisError(executed.error); - } finally { - try { - if (sessionId !== undefined) { - const closed = await manager.close(sessionId); - if (!closed.ok) - logger.error( - { - command: CLI_COMMANDS.debugWithRizin, - cleanup: projectAnalysisError(closed.error), - }, - "Rizin cleanup failed after collecting command output", - ); - } - } catch (cause) { - logger.error( + return finishAnalysisCommand( + async () => { + const started = await manager.start( { - command: CLI_COMMANDS.debugWithRizin, - cleanup: - cause instanceof Error - ? cause.message - : "Unknown cleanup failure", + ...args, + inferior_tty: options.inferiorTty, + ...(options.backend === undefined + ? {} + : { backend: options.backend }), }, - "Rizin cleanup failed after collecting command output", + signal, ); - } - // The retained owner retries all sessions. Only its final outcome - // establishes whether command cleanup remains incomplete. - await manager.closeAll().catch((cause: unknown) => { - process.exitCode = 1; - logger.error( - { - command: CLI_COMMANDS.debugWithRizin, - cleanup: - cause instanceof Error - ? cause.message - : "Unknown cleanup failure", - }, - "Rizin fallback cleanup failed", + if (!started.ok) return err(started.error); + sessionId = started.value.session_id; + const executed = await manager.execute( + sessionId, + args.command, + signal, ); - }); - } + return executed.ok + ? ok(executed.value.evidence) + : err(executed.error); + }, + { + close: () => + closeDebuggerCommandSession( + manager, + sessionId, + "rizin", + "rizin_debug_command", + (cleanup) => + logger.error( + { command: CLI_COMMANDS.debugWithRizin, cleanup }, + "Rizin cleanup failed after collecting command output", + ), + ), + }, + "rizin", + "rizin_debug_command", + signal, + ).then(projectCliAnalysisResult); }), ), }); diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index 383731647b..63d91892b4 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -29,6 +29,7 @@ export const CLI_COMMANDS = Object.freeze({ inspectNativeDispatchMetadata: "inspect-native-dispatch-metadata", capabilities: "capabilities", providers: "providers", + guidance: "guidance", guidedWorkflows: "guided-workflows", function: "function", annotateNativeFunction: "annotate-native-function", diff --git a/src/composition/browserObservation.ts b/src/composition/browserObservation.ts index 5d4e2e934c..9fa6d09e53 100644 --- a/src/composition/browserObservation.ts +++ b/src/composition/browserObservation.ts @@ -1,6 +1,8 @@ import type { BrowserObservationPort } from "../application/BrowserObservationPort.js"; import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js"; +import type { WebBundleAnalysisAdmission } from "../browser/IsolatedWebBundleAnalyzer.js"; /** Construct a fresh provider without opening a target or acquiring an engine. */ -export const createBrowserObservationProvider = (): BrowserObservationPort => - new CdpBrowserProvider(); +export const createBrowserObservationProvider = ( + admission?: WebBundleAnalysisAdmission, +): BrowserObservationPort => new CdpBrowserProvider(admission); diff --git a/src/composition/javascriptAnalysisWorkerProcess.ts b/src/composition/javascriptAnalysisWorkerProcess.ts index 2ae016b52e..540cbadd6b 100644 --- a/src/composition/javascriptAnalysisWorkerProcess.ts +++ b/src/composition/javascriptAnalysisWorkerProcess.ts @@ -47,6 +47,7 @@ const analyze: JavaScriptSourceWorkerAnalyzer = async ( analyzeParsedJavaScriptSemanticsSteps( parsed, moduleResolutionModeForJavaScriptReference(file.path, "static-import"), + capacity.nodes, ), ); parsed = null; diff --git a/src/composition/javascriptRuntimeObservation.ts b/src/composition/javascriptRuntimeObservation.ts index ffc67a8dbe..3255b56108 100644 --- a/src/composition/javascriptRuntimeObservation.ts +++ b/src/composition/javascriptRuntimeObservation.ts @@ -1,11 +1,20 @@ import type { AnalysisSettingsReader } from "../domain/analysisSettings.js"; import { runtimeAsarHeaderAdmission } from "../config/runtimeAnalysisSettings.js"; import type { JavaScriptRuntimeObservationPort } from "../application/javascript/JavaScriptRuntimeObservationPort.js"; -import { V8InspectorProvider } from "../inspector/V8InspectorProvider.js"; +import { + V8InspectorAdmission, + V8InspectorProvider, +} from "../inspector/V8InspectorProvider.js"; + +export { V8InspectorAdmission }; /** Construct a fresh provider without opening a target or acquiring an engine. */ export const createJavaScriptRuntimeObservationProvider = ( environment: Readonly>, settings?: AnalysisSettingsReader, + admission?: V8InspectorAdmission, ): JavaScriptRuntimeObservationPort => - new V8InspectorProvider(runtimeAsarHeaderAdmission(environment, settings)); + new V8InspectorProvider( + runtimeAsarHeaderAdmission(environment, settings), + admission, + ); diff --git a/src/composition/optionalObservationProviders.ts b/src/composition/optionalObservationProviders.ts index 08e60e4fd0..36b25ace12 100644 --- a/src/composition/optionalObservationProviders.ts +++ b/src/composition/optionalObservationProviders.ts @@ -12,6 +12,8 @@ import { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY, } from "../browser/providerIdentities.js"; import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../inspector/providerIdentity.js"; +import type { V8InspectorAdmission } from "../inspector/V8InspectorProvider.js"; +import type { WebBundleAnalysisAdmission } from "../browser/IsolatedWebBundleAnalyzer.js"; import { err, ok, type Result } from "../domain/result.js"; /** Precisely typed startup factories for the five existing observation ports. */ @@ -27,11 +29,16 @@ export const createOptionalObservationFactories = ( settings?: AnalysisSettingsReader, ): OptionalObservationFactories => { const environment = snapshotEnvironment(selectedEnvironment); + let runtimeAdmission: V8InspectorAdmission | undefined; + let webBundleAdmission: WebBundleAnalysisAdmission | undefined; return { - browserObservation: async () => - ( - await import("./browserObservation.js") - ).createBrowserObservationProvider(), + browserObservation: async () => { + const browser = await import("./browserObservation.js"); + const { WebBundleAnalysisAdmission: Admission } = + await import("../browser/IsolatedWebBundleAnalyzer.js"); + webBundleAdmission ??= new Admission(); + return browser.createBrowserObservationProvider(webBundleAdmission); + }, browserScenarioCapture: async () => (await import("./browserScenario.js")).createBrowserScenarioProvider( environment, @@ -44,10 +51,15 @@ export const createOptionalObservationFactories = ( (await import("./electronScenario.js")).createElectronScenarioProvider( environment, ), - javascriptRuntimeObservation: async () => - ( - await import("./javascriptRuntimeObservation.js") - ).createJavaScriptRuntimeObservationProvider(environment, settings), + javascriptRuntimeObservation: async () => { + const runtime = await import("./javascriptRuntimeObservation.js"); + runtimeAdmission ??= new runtime.V8InspectorAdmission(); + return runtime.createJavaScriptRuntimeObservationProvider( + environment, + settings, + runtimeAdmission, + ); + }, }; }; diff --git a/src/config/debuggerSessionLimits.test.ts b/src/config/debuggerSessionLimits.test.ts new file mode 100644 index 0000000000..6dad453fd6 --- /dev/null +++ b/src/config/debuggerSessionLimits.test.ts @@ -0,0 +1,255 @@ +import { expect, it } from "vitest"; + +import { ConfigurationError } from "../domain/configurationErrors.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MAXIMUM_DEBUGGER_MAX_SESSIONS, + MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MINIMUM_DEBUGGER_MAX_SESSIONS, +} from "../domain/analysisResourceLimits.js"; +import { GdbSessionManager } from "../gdb/GdbSessionManager.js"; +import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; +import { parseEnvironment } from "./environment.js"; +import { + DEBUGGER_COMMAND_DEADLINE_SETTING, + DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, + DEBUGGER_MAX_PENDING_COMMANDS_SETTING, + DEBUGGER_MAX_SESSIONS_SETTING, + debuggerCommandDeadlineConstraint, + debuggerMaxCommandOutputBytesConstraint, + debuggerMaxPendingCommandsConstraint, + debuggerMaxSessionsConstraint, + parseDebuggerSessionLimits, + resolveDebuggerSessionLimits, +} from "./debuggerSessionLimits.js"; + +it("uses the shared defaults when the settings are unset", () => { + const parsed = parseDebuggerSessionLimits({}); + if (!parsed.ok) throw parsed.error; + expect(parsed.value.commandDeadline).toMatchObject({ + setting: DEBUGGER_COMMAND_DEADLINE_SETTING, + value: 120_000, + source: "default", + minimum: MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + maximum: MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + unit: "ms", + }); + expect(parsed.value.maxSessions).toMatchObject({ + setting: DEBUGGER_MAX_SESSIONS_SETTING, + value: 8, + source: "default", + minimum: MINIMUM_DEBUGGER_MAX_SESSIONS, + maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS, + unit: "sessions", + }); + expect(parsed.value.maxPendingCommands).toMatchObject({ + setting: DEBUGGER_MAX_PENDING_COMMANDS_SETTING, + value: 16, + source: "default", + minimum: MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + maximum: MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + unit: "commands", + }); + expect(parsed.value.maxCommandOutputBytes).toMatchObject({ + setting: DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, + value: 16 * 1024 * 1024, + source: "default", + minimum: MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + maximum: MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + unit: "bytes", + }); +}); + +it("accepts one in-range override for each shared debugger cap", () => { + const parsed = parseDebuggerSessionLimits({ + [DEBUGGER_COMMAND_DEADLINE_SETTING]: "1000", + [DEBUGGER_MAX_SESSIONS_SETTING]: "2", + [DEBUGGER_MAX_PENDING_COMMANDS_SETTING]: "3", + [DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]: "4096", + }); + if (!parsed.ok) throw parsed.error; + expect(parsed.value.commandDeadline).toMatchObject({ + value: 1000, + source: "setting", + }); + expect(parsed.value.maxSessions).toMatchObject({ + value: 2, + source: "setting", + }); + expect(parsed.value.maxPendingCommands).toMatchObject({ + value: 3, + source: "setting", + }); + expect(parsed.value.maxCommandOutputBytes).toMatchObject({ + value: 4096, + source: "setting", + }); +}); + +it.each([ + [DEBUGGER_COMMAND_DEADLINE_SETTING, "0", debuggerCommandDeadlineConstraint], + [DEBUGGER_MAX_SESSIONS_SETTING, "65", debuggerMaxSessionsConstraint], + [ + DEBUGGER_MAX_PENDING_COMMANDS_SETTING, + "00", + debuggerMaxPendingCommandsConstraint, + ], + [ + DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, + String(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES + 1), + debuggerMaxCommandOutputBytesConstraint, + ], +])( + "rejects invalid %s at the setting boundary", + (setting, value, constraint) => { + const parsed = parseDebuggerSessionLimits({ [setting]: value }); + if (parsed.ok) throw new Error(`${setting} was accepted`); + expect(parsed.error).toBeInstanceOf(ConfigurationError); + expect(parsed.error.settings).toEqual([{ setting, constraint }]); + const startup = parseEnvironment({ [setting]: value }); + if (startup.ok) throw new Error(`${setting} was accepted at startup`); + expect(projectAnalysisError(startup.error)).toMatchObject({ + code: "configuration_invalid", + details: { settings: [{ setting, constraint }] }, + }); + }, +); + +it("names the session setting on a Rizin capacity refusal", async () => { + const manager = new RizinDebugSessionManager( + { [DEBUGGER_MAX_SESSIONS_SETTING]: "1" }, + { platform: "linux" }, + ); + const first = manager.start({ + path: "/tmp/rea-debugger-limit", + inferior_tty: "/no/such/rea-debugger-tty", + }); + const blocked = await manager.start({ + path: "/tmp/rea-debugger-limit-2", + inferior_tty: "/no/such/rea-debugger-tty", + }); + if (blocked.ok) throw new Error("expected the session cap to refuse"); + expect(projectAnalysisError(blocked.error)).toMatchObject({ + code: "resource_constraint", + details: { + resource: "memory", + reported_limits: { + setting: DEBUGGER_MAX_SESSIONS_SETTING, + effective_value: 1, + source: "setting", + minimum: MINIMUM_DEBUGGER_MAX_SESSIONS, + maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS, + unit: "sessions", + restart_required: true, + max_sessions: 1, + active_or_reserved_sessions: 1, + }, + }, + }); + await first; +}); + +it("keeps the command-output maximum inside one Evidence hash string", () => { + const escaped = JSON.stringify("\u0001"); + const expansion = escaped.length - 2; + const nodeMaxStringLength = 536_870_888; + const envelope = 1024 * 1024; + expect(expansion).toBe(6); + expect(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES).toBe( + Math.floor((nodeMaxStringLength - envelope) / expansion), + ); + expect(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES).toBeGreaterThanOrEqual( + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + ); + expect( + MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES * expansion + envelope, + ).toBeLessThanOrEqual(nodeMaxStringLength); +}); + +it("does not let one Rizin manager consume another manager's session cap", async () => { + const first = new RizinDebugSessionManager( + { [DEBUGGER_MAX_SESSIONS_SETTING]: "1" }, + { platform: "linux" }, + ); + const second = new RizinDebugSessionManager( + { [DEBUGGER_MAX_SESSIONS_SETTING]: "1" }, + { platform: "linux" }, + ); + const input = { + path: "/tmp/rea-debugger-limit", + inferior_tty: "/no/such/rea-debugger-tty", + }; + const held = first.start(input); + const independent = second.start(input); + const blocked = first.start({ + ...input, + path: "/tmp/rea-debugger-limit-2", + }); + try { + const independentResult = await independent; + if (independentResult.ok) + throw new Error("expected the missing terminal to fail"); + expect(projectAnalysisError(independentResult.error)).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["inferior_tty"], reason: "invalid_value" }], + }, + }); + const blockedResult = await blocked; + if (blockedResult.ok) + throw new Error("expected the first manager to stay capped"); + expect(projectAnalysisError(blockedResult.error)).toMatchObject({ + code: "resource_constraint", + details: { + reported_limits: { + setting: DEBUGGER_MAX_SESSIONS_SETTING, + effective_value: 1, + source: "setting", + active_or_reserved_sessions: 1, + }, + }, + }); + } finally { + await held; + await independent; + await blocked; + } +}); + +it("refuses an invalid GDB deadline before a session starts", () => { + expect( + () => + new GdbSessionManager({ + environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" }, + }), + ).toThrow(ConfigurationError); + try { + new GdbSessionManager({ + environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" }, + }); + } catch (error) { + if (!(error instanceof ConfigurationError)) throw error; + expect(projectAnalysisError(error)).toMatchObject({ + code: "configuration_invalid", + details: { + settings: [ + { + setting: DEBUGGER_COMMAND_DEADLINE_SETTING, + constraint: debuggerCommandDeadlineConstraint, + }, + ], + }, + }); + } + expect( + resolveDebuggerSessionLimits({ + [DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500", + }).commandDeadline, + ).toMatchObject({ value: 1500, source: "setting" }); +}); diff --git a/src/config/debuggerSessionLimits.ts b/src/config/debuggerSessionLimits.ts new file mode 100644 index 0000000000..84748434fa --- /dev/null +++ b/src/config/debuggerSessionLimits.ts @@ -0,0 +1,251 @@ +import { + DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS, + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS, + DEFAULT_DEBUGGER_MAX_SESSIONS, + MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MAXIMUM_DEBUGGER_MAX_SESSIONS, + MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MINIMUM_DEBUGGER_MAX_SESSIONS, +} from "../domain/analysisResourceLimits.js"; +import { ConfigurationError } from "../domain/configurationErrors.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; + +/** + * Launch caps shared by GDB and Rizin. Each setting is declared here once. + * Startup validation spreads {@link debuggerSessionLimitEnvironmentShape}; + * it does not restate the name, bounds, or schema. + */ +export const DEBUGGER_COMMAND_DEADLINE_SETTING = + "REA_DEBUGGER_COMMAND_DEADLINE_MS"; +export const DEBUGGER_MAX_SESSIONS_SETTING = "REA_DEBUGGER_MAX_SESSIONS"; +export const DEBUGGER_MAX_PENDING_COMMANDS_SETTING = + "REA_DEBUGGER_MAX_PENDING_COMMANDS"; +export const DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING = + "REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES"; + +/** Effective debugger admission and where its value came from. */ +export interface DebuggerAdmissionLimit { + readonly setting: string; + readonly value: number; + readonly source: "default" | "setting" | "runtime"; + readonly defaultValue: number; + readonly minimum: number; + readonly maximum: number; + readonly unit: "ms" | "bytes" | "sessions" | "commands"; +} + +const limit = ( + setting: string, + value: number, + minimum: number, + maximum: number, + unit: DebuggerAdmissionLimit["unit"], +): DebuggerAdmissionLimit => ({ + setting, + value, + source: "default", + defaultValue: value, + minimum, + maximum, + unit, +}); + +export const DEFAULT_DEBUGGER_COMMAND_DEADLINE: DebuggerAdmissionLimit = limit( + DEBUGGER_COMMAND_DEADLINE_SETTING, + DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS, + MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + "ms", +); +export const DEFAULT_DEBUGGER_MAX_SESSIONS_LIMIT: DebuggerAdmissionLimit = + limit( + DEBUGGER_MAX_SESSIONS_SETTING, + DEFAULT_DEBUGGER_MAX_SESSIONS, + MINIMUM_DEBUGGER_MAX_SESSIONS, + MAXIMUM_DEBUGGER_MAX_SESSIONS, + "sessions", + ); +export const DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS_LIMIT: DebuggerAdmissionLimit = + limit( + DEBUGGER_MAX_PENDING_COMMANDS_SETTING, + DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS, + MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + "commands", + ); +export const DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_LIMIT: DebuggerAdmissionLimit = + limit( + DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + "bytes", + ); + +const constraintFor = (admission: DebuggerAdmissionLimit): string => + `${admission.setting} must be a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)} ${admission.unit}; the default is ${String(admission.defaultValue)} ${admission.unit}.${ + admission.unit === "ms" + ? " Node schedules a delay above that maximum as 1 ms, so a larger value is refused." + : "" + }`; + +export const debuggerCommandDeadlineConstraint = constraintFor( + DEFAULT_DEBUGGER_COMMAND_DEADLINE, +); +export const debuggerMaxSessionsConstraint = constraintFor( + DEFAULT_DEBUGGER_MAX_SESSIONS_LIMIT, +); +export const debuggerMaxPendingCommandsConstraint = constraintFor( + DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS_LIMIT, +); +export const debuggerMaxCommandOutputBytesConstraint = constraintFor( + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_LIMIT, +); + +export const debuggerCommandDeadlineMsSchema = positiveDecimalIntegerSchema( + MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS, + debuggerCommandDeadlineConstraint, +); +export const debuggerMaxSessionsSchema = positiveDecimalIntegerSchema( + MINIMUM_DEBUGGER_MAX_SESSIONS, + MAXIMUM_DEBUGGER_MAX_SESSIONS, + debuggerMaxSessionsConstraint, +); +export const debuggerMaxPendingCommandsSchema = positiveDecimalIntegerSchema( + MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS, + debuggerMaxPendingCommandsConstraint, +); +export const debuggerMaxCommandOutputBytesSchema = positiveDecimalIntegerSchema( + MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + debuggerMaxCommandOutputBytesConstraint, +); + +/** Schemas startup validation spreads. The setting names are not restated. */ +export const debuggerSessionLimitEnvironmentShape = { + [DEBUGGER_COMMAND_DEADLINE_SETTING]: + debuggerCommandDeadlineMsSchema.optional(), + [DEBUGGER_MAX_SESSIONS_SETTING]: debuggerMaxSessionsSchema.optional(), + [DEBUGGER_MAX_PENDING_COMMANDS_SETTING]: + debuggerMaxPendingCommandsSchema.optional(), + [DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]: + debuggerMaxCommandOutputBytesSchema.optional(), +}; + +export interface DebuggerSessionLimits { + readonly commandDeadline: DebuggerAdmissionLimit; + readonly maxSessions: DebuggerAdmissionLimit; + readonly maxPendingCommands: DebuggerAdmissionLimit; + readonly maxCommandOutputBytes: DebuggerAdmissionLimit; +} + +const parseOne = ( + admission: DebuggerAdmissionLimit, + schema: ReturnType, + constraint: string, + value: string | undefined, +): Result => { + if (value === undefined) return ok(admission); + const parsed = schema.safeParse(value); + return parsed.success + ? ok({ ...admission, value: parsed.data, source: "setting" }) + : err( + new ConfigurationError(constraint, { + settings: [{ setting: admission.setting, constraint }], + }), + ); +}; + +/** Resolve every shared debugger cap from one environment. Invalid values are refused. */ +export const parseDebuggerSessionLimits = ( + environment: Readonly>, +): Result => { + const commandDeadline = parseOne( + DEFAULT_DEBUGGER_COMMAND_DEADLINE, + debuggerCommandDeadlineMsSchema, + debuggerCommandDeadlineConstraint, + environment[DEBUGGER_COMMAND_DEADLINE_SETTING], + ); + const maxSessions = parseOne( + DEFAULT_DEBUGGER_MAX_SESSIONS_LIMIT, + debuggerMaxSessionsSchema, + debuggerMaxSessionsConstraint, + environment[DEBUGGER_MAX_SESSIONS_SETTING], + ); + const maxPendingCommands = parseOne( + DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS_LIMIT, + debuggerMaxPendingCommandsSchema, + debuggerMaxPendingCommandsConstraint, + environment[DEBUGGER_MAX_PENDING_COMMANDS_SETTING], + ); + const maxCommandOutputBytes = parseOne( + DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_LIMIT, + debuggerMaxCommandOutputBytesSchema, + debuggerMaxCommandOutputBytesConstraint, + environment[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING], + ); + const settings = [ + commandDeadline, + maxSessions, + maxPendingCommands, + maxCommandOutputBytes, + ].flatMap((result) => (result.ok ? [] : result.error.settings)); + if ( + settings.length > 0 || + !commandDeadline.ok || + !maxSessions.ok || + !maxPendingCommands.ok || + !maxCommandOutputBytes.ok + ) + return err( + new ConfigurationError("Invalid REA environment configuration", { + settings, + }), + ); + return ok({ + commandDeadline: commandDeadline.value, + maxSessions: maxSessions.value, + maxPendingCommands: maxPendingCommands.value, + maxCommandOutputBytes: maxCommandOutputBytes.value, + }); +}; + +/** Refuse an invalid launch cap instead of falling back to the default. */ +export const resolveDebuggerSessionLimits = ( + environment: Readonly>, +): DebuggerSessionLimits => { + const parsed = parseDebuggerSessionLimits(environment); + if (!parsed.ok) throw parsed.error; + return parsed.value; +}; + +/** + * Fixed budget for Rizin cleanup readback and GDB startup or detach MI. + * This is not `REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES`. A caller cap below + * this budget must not truncate those required replies, and recovery for + * this budget is not an environment setting. + */ +export const debuggerControlReplyOutput: DebuggerAdmissionLimit = { + setting: "debugger control-reply budget", + value: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + source: "default", + defaultValue: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + minimum: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + maximum: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + unit: "bytes", +}; + +/** Test and caller seams replace the launch value and record that source. */ +export const applyDebuggerAdmissionOverride = ( + admission: DebuggerAdmissionLimit, + value: number | undefined, +): DebuggerAdmissionLimit => + value === undefined ? admission : { ...admission, value, source: "runtime" }; diff --git a/src/config/environment.ts b/src/config/environment.ts index 9014ee91a3..ef9b25739c 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -21,6 +21,7 @@ import { } from "./providerStartupTimeout.js"; import { DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS, + DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, DEFAULT_HOPPER_STARTUP_TIMEOUT_MS, } from "../domain/analysisResourceLimits.js"; import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js"; @@ -29,6 +30,7 @@ import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js"; import { asarMaxHeaderBytesSchema } from "./asarHeaderAdmission.js"; import { bridgeMaxRequestBytesSchema } from "./bridgeMaxRequestBytes.js"; import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js"; +import { debuggerSessionLimitEnvironmentShape } from "./debuggerSessionLimits.js"; import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js"; import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js"; import { @@ -43,6 +45,7 @@ import { processStartConfigurationSchema } from "./processStart.js"; import { parseJebMcpEndpoint } from "./jebMcpEndpoint.js"; const environmentSchema = z.object({ + ...debuggerSessionLimitEnvironmentShape, REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"), REA_BRIDGE_MAX_REQUEST_BYTES: bridgeMaxRequestBytesSchema.optional(), REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(), @@ -100,6 +103,9 @@ const environmentSchema = z.object({ REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default( DEFAULT_HOPPER_STARTUP_TIMEOUT_MS, ), + REA_HOPPER_CALLER_SCAN_BUDGET_MS: providerStartupTimeoutSchema.default( + DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + ), REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default( DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS, ), diff --git a/src/config/hopperCallerScanBudget.test.ts b/src/config/hopperCallerScanBudget.test.ts new file mode 100644 index 0000000000..7e38b94e06 --- /dev/null +++ b/src/config/hopperCallerScanBudget.test.ts @@ -0,0 +1,92 @@ +import { expect, it } from "vitest"; + +import { explicitSetupRegistrationEnvironment } from "../application/SetupRegistrationEnvironment.js"; +import { + DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + MAX_PROVIDER_STARTUP_TIMEOUT_MS, + MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS, +} from "../domain/analysisResourceLimits.js"; +import { + HOPPER_CALLER_PAGE_ALLOWANCE_BYTES, + type HopperCallerScanFacts, +} from "../domain/native/hopperCallerReconciliation.js"; +import { parseConfig } from "./parseConfig.js"; +import { + HOPPER_CALLER_SCAN_BUDGET_SETTING, + hopperCallerScanRecovery, +} from "./hopperCallerScanBudget.js"; + +const facts = ( + overrides: Partial & + Pick, +): HopperCallerScanFacts => ({ + examined_procedures: 1, + procedure_total: 2, + retained_bytes: 32, + instructions_complete: true, + native_callers_complete: true, + ...overrides, +}); + +it("records the default caller-scan budget and forwards an explicit setting", () => { + const unset = parseConfig({}); + if (!unset.ok) throw unset.error; + expect(unset.value.hopperCallerScanBudget).toMatchObject({ + milliseconds: DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + source: "default", + floorMilliseconds: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS, + maximumMilliseconds: MAX_PROVIDER_STARTUP_TIMEOUT_MS, + }); + const selected = parseConfig({ + [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250", + }); + if (!selected.ok) throw selected.error; + expect(selected.value.hopperCallerScanBudget).toMatchObject({ + milliseconds: 250, + source: "setting", + }); + expect( + explicitSetupRegistrationEnvironment({ + [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250", + UNRELATED: "dropped", + }), + ).toEqual({ [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250" }); +}); + +it("explains the time budget and that fixed byte allowances have no override", () => { + const parsed = parseConfig({ [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250" }); + if (!parsed.ok) throw parsed.error; + const budget = parsed.value.hopperCallerScanBudget; + const time = hopperCallerScanRecovery( + facts({ stop: { code: "work_budget", budget_ms: 250 } }), + budget, + ); + expect(time).toContain(`${HOPPER_CALLER_SCAN_BUDGET_SETTING} is 250 ms`); + expect(time).toContain( + `The default is ${String(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS)} ms`, + ); + expect(time).toContain( + `from ${String(MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS)} to ${String(MAX_PROVIDER_STARTUP_TIMEOUT_MS)} ms`, + ); + expect(time).toContain("no runtime override"); + expect(time).toContain( + `${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`, + ); + const page = hopperCallerScanRecovery( + facts({ + stop: { + code: "page_allowance", + allowance_bytes: HOPPER_CALLER_PAGE_ALLOWANCE_BYTES, + }, + }), + budget, + ); + expect(page).toContain("no runtime override"); + expect(page).toContain( + `${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`, + ); + expect(page).not.toContain("is 250 ms"); + expect( + hopperCallerScanRecovery(facts({ stop: { code: "complete" } }), budget), + ).toBe(""); +}); diff --git a/src/config/hopperCallerScanBudget.ts b/src/config/hopperCallerScanBudget.ts new file mode 100644 index 0000000000..e8f191068f --- /dev/null +++ b/src/config/hopperCallerScanBudget.ts @@ -0,0 +1,70 @@ +import { + DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + MAX_PROVIDER_STARTUP_TIMEOUT_MS, + MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS, +} from "../domain/analysisResourceLimits.js"; +import { + HOPPER_CALLER_PAGE_ALLOWANCE_BYTES, + HOPPER_CALLER_SCAN_RETENTION_BYTES, + type HopperCallerScanFacts, +} from "../domain/native/hopperCallerReconciliation.js"; + +export const HOPPER_CALLER_SCAN_BUDGET_SETTING = + "REA_HOPPER_CALLER_SCAN_BUDGET_MS"; + +/** Effective caller-scan budget and where its milliseconds came from. */ +export interface HopperCallerScanBudget { + readonly setting: typeof HOPPER_CALLER_SCAN_BUDGET_SETTING; + readonly milliseconds: number; + readonly source: "default" | "setting"; + readonly defaultMilliseconds: number; + readonly floorMilliseconds: number; + readonly maximumMilliseconds: number; +} + +export const defaultHopperCallerScanBudget = (): HopperCallerScanBudget => ({ + setting: HOPPER_CALLER_SCAN_BUDGET_SETTING, + milliseconds: DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + source: "default", + defaultMilliseconds: DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS, + floorMilliseconds: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS, + maximumMilliseconds: MAX_PROVIDER_STARTUP_TIMEOUT_MS, +}); + +/** Record whether the launch environment supplied the budget. */ +export const hopperCallerScanBudgetFromEnvironment = ( + configured: string | undefined, + milliseconds: number, +): HopperCallerScanBudget => ({ + ...defaultHopperCallerScanBudget(), + milliseconds, + source: configured === undefined ? "default" : "setting", +}); + +/** Budget record for a caller that already resolved the millisecond count. */ +export const hopperCallerScanBudgetForMilliseconds = ( + milliseconds: number, +): HopperCallerScanBudget => + hopperCallerScanBudgetFromEnvironment( + milliseconds === DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS + ? undefined + : String(milliseconds), + milliseconds, + ); + +const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. ${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them.`; + +/** Environment recovery for a caller-scan limit. Domain facts stay separate. */ +export const hopperCallerScanRecovery = ( + facts: HopperCallerScanFacts, + budget: HopperCallerScanBudget, +): string => { + const fixedLimit = + facts.stop.code === "page_allowance" || + facts.stop.code === "retention_budget" || + !facts.instructions_complete || + facts.native_callers_complete === false; + if (facts.stop.code === "work_budget") + return `${budget.setting} is ${String(budget.milliseconds)} ms (source: ${budget.source}). The default is ${String(budget.defaultMilliseconds)} ms. Allowed values are integers from ${String(budget.floorMilliseconds)} to ${String(budget.maximumMilliseconds)} ms. Set it in the environment that starts REA and restart REA. ${fixedAllowanceRecovery}`; + return fixedLimit ? fixedAllowanceRecovery : ""; +}; diff --git a/src/config/parseConfig.ts b/src/config/parseConfig.ts index 3f8c325493..e401d43405 100644 --- a/src/config/parseConfig.ts +++ b/src/config/parseConfig.ts @@ -9,6 +9,7 @@ import { parseEnvironment } from "./environment.js"; import { providerStartupTimeoutSchema } from "./providerStartupTimeout.js"; import { homeDirectoryFromEnvironment } from "./homeDirectory.js"; import { DEFAULT_BRIDGE_MAX_REQUEST } from "./bridgeMaxRequestBytes.js"; +import { hopperCallerScanBudgetFromEnvironment } from "./hopperCallerScanBudget.js"; import { DEFAULT_REGEX_MATCHING_DEADLINE } from "./regexMatchingDeadline.js"; import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "./regexWorkerHeapRequest.js"; import type { AppConfig } from "./types.js"; @@ -168,6 +169,10 @@ export const parseConfig = ( homeDirectoryFromEnvironment(environment, process.platform), ), hopperStartupTimeoutMs: env.REA_HOPPER_STARTUP_TIMEOUT_MS, + hopperCallerScanBudget: hopperCallerScanBudgetFromEnvironment( + environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS, + env.REA_HOPPER_CALLER_SCAN_BUDGET_MS, + ), hopperTargetPath: env.HOPPER_TARGET_PATH, hopperTargetKind: env.HOPPER_TARGET_KIND, hopperLoaderArgs: env.loaderArgs, diff --git a/src/config/types.ts b/src/config/types.ts index d735fc5d83..1abacfd785 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -10,6 +10,7 @@ import type { McpOutputSchemaProfile } from "./mcpOutputSchemaProfile.js"; import type { BridgeMaxRequestBytes } from "./bridgeMaxRequestBytes.js"; import type { RegexMatchingDeadline } from "./regexMatchingDeadline.js"; import type { RegexWorkerHeapRequest } from "./regexWorkerHeapRequest.js"; +import type { HopperCallerScanBudget } from "./hopperCallerScanBudget.js"; export interface AppConfig { readonly mcpToolFamilies?: readonly McpToolFamilies[number][]; @@ -32,6 +33,8 @@ export interface AppConfig { readonly ilspyCmdPath: string | undefined; readonly hopperLauncherPath: string; readonly hopperStartupTimeoutMs: number; + /** Cooperative per-operation budget for forward caller reconciliation. */ + readonly hopperCallerScanBudget: HopperCallerScanBudget; readonly hopperTargetPath: string | undefined; readonly hopperTargetKind: "executable" | "database"; readonly hopperLoaderArgs: readonly string[]; diff --git a/src/contracts/applicationToolContracts.ts b/src/contracts/applicationToolContracts.ts index c47144c83c..762a11b06c 100644 --- a/src/contracts/applicationToolContracts.ts +++ b/src/contracts/applicationToolContracts.ts @@ -27,6 +27,12 @@ import { appleApplicationProjectionResultSchema } from "../domain/apple/appleApp import { harmonyApplicationProjectionResultSchema } from "../domain/harmony/harmonyApplication.js"; import type { ToolContract } from "./toolContractTypes.js"; import { toolContractMetadata } from "./toolEffects.js"; +import { + readGuidanceSectionInputSchema, + readGuidanceSectionOutputSchema, + searchGuidanceInputSchema, + searchGuidanceOutputSchema, +} from "./guidanceContracts.js"; import { guidedWorkflowsInputSchema, guidedWorkflowsOutputSchema, @@ -73,6 +79,21 @@ const retained = (evidence_id: string) => ({ /** Provider-neutral graph workflow contracts shared by MCP and CLI adapters. */ export const APPLICATION_TOOL_CONTRACTS = [ + { + name: "search_guidance", + ...toolContractMetadata("search_guidance"), + description: + "Search only the installed REA workflow guidance. Every non-generic query word must occur in a section. Returns ranked sections, bounded pages, and locators for read_guidance_section. No target or provider is accessed.", + kind: "application", + inputSchema: searchGuidanceInputSchema, + outputSchema: searchGuidanceOutputSchema, + examples: [ + { + title: "Find exact JavaScript module guidance", + input: { query: "exact JavaScript module" }, + }, + ], + }, { name: "guided_workflows", ...toolContractMetadata("guided_workflows"), @@ -96,6 +117,16 @@ export const APPLICATION_TOOL_CONTRACTS = [ }, ], }, + { + name: "read_guidance_section", + ...toolContractMetadata("read_guidance_section"), + description: + "Retrieve a section selected by search_guidance from the installed REA guidance bundle. Reports stale locators and unavailable bundles explicitly. Returns documentation, not target observations or Evidence.", + kind: "application", + inputSchema: readGuidanceSectionInputSchema, + outputSchema: readGuidanceSectionOutputSchema, + examples: [], + }, { name: "trace_application_feature", ...toolContractMetadata("trace_application_feature"), diff --git a/src/contracts/artifactToolContracts.ts b/src/contracts/artifactToolContracts.ts index 1ffaa007a7..dc260387e7 100644 --- a/src/contracts/artifactToolContracts.ts +++ b/src/contracts/artifactToolContracts.ts @@ -83,7 +83,7 @@ const artifact = < export const ARTIFACT_TOOL_CONTRACTS = [ artifact( "inspect_artifact", - "Inspect an active archive or application package, including Node.js SEA carriers, in one call. By default returns the complete content-addressed artifact graph once, as the typed nested inventory Evidence, with root and integrity observations, integrity contradictions, hypotheses, unexplored branches, limitations, and next probes inline. For PCK and large containers prefer detail summary: it retains the complete inspection and returns its derived summary view; read exact paths, batch occurrence/content IDs and selected fields with inspect_analysis_view using parent_evidence_id. This changes delivery, not inventory coverage. Read-only DMG mounting is used automatically on supported macOS hosts. Integrity mismatches fail by default; record-and-continue keeps mismatches explicitly untrusted. It does not extract files; use extract_artifact to materialize regular contents of supported formats. DMG extraction is unavailable; use a caller-managed read-only mount on macOS to access its files.", + "Inspect an active archive or application package, including Node.js SEA carriers, in one call. By default returns the complete content-addressed artifact graph once, as the typed nested inventory Evidence, with root and integrity observations, integrity contradictions, hypotheses, unexplored branches, limitations, and next probes inline. Bun standalone inventory describes stored module, sourcemap and opaque bytecode regions. For PCK, Bun and large containers prefer detail summary: it retains the complete inspection and returns its derived summary view; read exact paths, batch occurrence/content IDs and selected fields with inspect_analysis_view using parent_evidence_id. This changes delivery, not inventory coverage. Read-only DMG mounting is used automatically on supported macOS hosts. Integrity mismatches fail by default; record-and-continue keeps mismatches explicitly untrusted. It does not extract files; use extract_artifact to materialize regular contents of supported formats. DMG extraction is unavailable; use a caller-managed read-only mount on macOS to access its files.", artifactInspectionInputSchema, { evidenceParameters: "preserve-detail" }, ), diff --git a/src/contracts/guidanceContracts.ts b/src/contracts/guidanceContracts.ts new file mode 100644 index 0000000000..8bfdc57d56 --- /dev/null +++ b/src/contracts/guidanceContracts.ts @@ -0,0 +1,91 @@ +import { z } from "zod"; + +export const searchGuidanceLimitSchema = z.number().int().min(1).max(20); + +export const searchGuidanceInputSchema = z.strictObject({ + query: z + .string() + .min(1) + .max(2048) + .describe( + "Lexical query: every non-generic word must occur in the section; generic-only queries return no matches.", + ) + .refine((value) => value.trim().length > 0), + limit: searchGuidanceLimitSchema.default(8), + cursor: z.string().min(1).max(4096).optional(), +}); + +export const readGuidanceSectionInputSchema = z.strictObject({ + locator: z.string().min(1).max(4096), +}); + +export const searchGuidanceOutputSchema = z.discriminatedUnion("status", [ + z.strictObject({ + status: z.literal("matches"), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + package: z.string(), + bundle_sha256: z.string().length(64), + query: z.string(), + matches: z.array( + z.strictObject({ + guide: z.string(), + heading: z.string(), + locator: z.string(), + matched_terms: z.array(z.string()), + relevance: z.string(), + context: z.string(), + }), + ), + total_matches: z.number().int().nonnegative(), + offset: z.number().int().nonnegative(), + limit: z.number().int().positive(), + has_more: z.boolean(), + next_cursor: z.string().min(1).max(4096).optional(), + }), + z.strictObject({ + status: z.literal("no_matches"), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + query: z.string(), + message: z.string(), + total_matches: z.literal(0), + }), + z.strictObject({ + status: z.literal("bundle_unavailable"), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + message: z.string(), + }), + z.strictObject({ + status: z.literal("stale_continuation"), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + message: z.string(), + }), +]); + +export const readGuidanceSectionOutputSchema = z.discriminatedUnion("status", [ + z.strictObject({ + status: z.literal("section"), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + package: z.string(), + bundle_sha256: z.string().length(64), + guide: z.string(), + heading: z.string(), + locator: z.string(), + content: z.string(), + }), + z.strictObject({ + status: z.enum(["stale_locator", "bundle_unavailable"]), + source: z.literal("installed_rea_guidance"), + content_kind: z.literal("documentation"), + message: z.string(), + }), +]); + +export type SearchGuidanceOutput = z.output; +export type ReadGuidanceSectionOutput = z.output< + typeof readGuidanceSectionOutputSchema +>; diff --git a/src/contracts/managed/managedWorkflowExamples.ts b/src/contracts/managed/managedWorkflowExamples.ts index fe2464a0f6..15b1c68bc8 100644 --- a/src/contracts/managed/managedWorkflowExamples.ts +++ b/src/contracts/managed/managedWorkflowExamples.ts @@ -255,6 +255,7 @@ const nativeExportEvidence = () => { }, ]), segments: coverage([]), + encryption: coverage([]), provenance: [], limitations: [ "Selected Mach-O slice container offset is unknown in this synthetic example.", diff --git a/src/contracts/model/modelToolContracts.ts b/src/contracts/model/modelToolContracts.ts index 6b5fbdd7f9..ff5d2e564b 100644 --- a/src/contracts/model/modelToolContracts.ts +++ b/src/contracts/model/modelToolContracts.ts @@ -9,31 +9,31 @@ import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; -export const MODEL_TOOL_CONTRACTS = [ - { - name: "inspect_model_artifact", - ...toolContractMetadata("inspect_model_artifact"), - kind: "artifact-provider", - requiresActiveTarget: true, - description: - "Inspect local GGUF v2/v3 or safetensors declarations by content, without loading models or executing code. Default hash header reads only the header: full-file SHA-256 and Evidence subject are unknown; artifact.header_sha256 is a scoped prefix commitment, not weight identity. hash full additionally streams all file bytes for SHA-256. Optional view returns a selected metadata/tensor/type page plus model context in this call. Summary retains complete Evidence for inspect_analysis_view kind model-artifact; its pages support selection, stable header-scoped indices and paging. Metadata arrays retain eight-element typed previews; scalar metadata is complete. Tensor counts, shapes, numeric type IDs and offsets are observed; textual types and parameter/storage totals use established format facts. Unknown or historically ambiguous GGUF types retain IDs with unknown nonempty byte lengths; ambiguous identities use AMBIGUOUS_ labels. Unresolved Q8_1 and K-block layouts also keep nonempty byte lengths unknown. Tensor payload, model behavior and inference quality are not inspected. One active inspection, 100 MB header ceiling and 32 MiB retained-facts budget; resource refusals are explicit. No network, external files, Python, model runtime, extraction or deserialization of pickle.", - inputSchema: inspectModelArtifactInputSchema, - outputSchema: evidenceResultOf( - z.union([ - modelArtifactSchema, - modelArtifactSummaryViewResultSchema, - modelArtifactPageViewResultSchema, - ]), - ), - examples: [ - { - title: "Read model declarations without reading weights", - input: { - path: "/artifacts/model.gguf", - hash: "header", - detail: "summary", - }, +export const INSPECT_MODEL_ARTIFACT_CONTRACT = { + name: "inspect_model_artifact", + ...toolContractMetadata("inspect_model_artifact"), + kind: "artifact-provider", + requiresActiveTarget: true, + description: + "Inspect local GGUF v2/v3 or safetensors declarations by content, without loading models or executing code. Default hash header reads only the header: full-file SHA-256 and Evidence subject are unknown; artifact.header_sha256 is a scoped prefix commitment, not weight identity. hash full additionally streams all file bytes for SHA-256. Optional view returns a selected metadata/tensor/type page plus model context in this call. Summary retains complete Evidence for inspect_analysis_view kind model-artifact; its pages support selection, stable header-scoped indices and paging. Metadata arrays retain eight-element typed previews; scalar metadata is complete. Tensor counts, shapes, numeric type IDs and offsets are observed; textual types and parameter/storage totals use established format facts. Unknown or historically ambiguous GGUF types retain IDs with unknown nonempty byte lengths; ambiguous identities use AMBIGUOUS_ labels. Unresolved Q8_1 and K-block layouts also keep nonempty byte lengths unknown. Tensor payload, model behavior and inference quality are not inspected. One active inspection, 100 MB header ceiling and 32 MiB retained-facts budget; resource refusals are explicit. No network, external files, Python, model runtime, extraction or deserialization of pickle.", + inputSchema: inspectModelArtifactInputSchema, + outputSchema: evidenceResultOf( + z.union([ + modelArtifactSchema, + modelArtifactSummaryViewResultSchema, + modelArtifactPageViewResultSchema, + ]), + ), + examples: [ + { + title: "Read model declarations without reading weights", + input: { + path: "/artifacts/model.gguf", + hash: "header", + detail: "summary", }, - ], - }, -] as const satisfies readonly ToolContract[]; + }, + ], +} as const satisfies ToolContract; + +export const MODEL_TOOL_CONTRACTS = [INSPECT_MODEL_ARTIFACT_CONTRACT] as const; diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index 81c4c731a9..1c274399f8 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -704,6 +704,8 @@ export const TOOL_EFFECTS: Readonly> = { trace_application_feature: evidence, trace_javascript_semantics: evidence, compare_application_versions: evidence, + search_guidance: effects(), + read_guidance_section: effects(), guided_workflows: effects(), compare_source_to_bundle: evidence, compare_javascript_export_shapes: evidence, @@ -793,6 +795,8 @@ export const TARGET_CONTENT_FREE_TOOLS: ReadonlySet = new Set([ "inspect_analysis_settings", "update_analysis_settings", "export_evidence_bundle", + "search_guidance", + "read_guidance_section", "guided_workflows", "record_unknown", ]); diff --git a/src/contracts/toolFamilies.test.ts b/src/contracts/toolFamilies.test.ts index 9d639445a9..2e9b5ce5b2 100644 --- a/src/contracts/toolFamilies.test.ts +++ b/src/contracts/toolFamilies.test.ts @@ -5,7 +5,11 @@ import { contractsForToolFamilies, toolFamilyId, } from "./toolFamilies.js"; -import { TOOL_FAMILY_IDS } from "./toolFamilyIds.js"; +import { + registerToolFamilies, + TOOL_FAMILY_IDS, + type ToolFamilyId, +} from "./toolFamilyIds.js"; import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js"; import type { officialOutputSchemas, @@ -43,6 +47,20 @@ describe("tool families", () => { ]); }); + it("runs registrars in published id order", () => { + const seen: string[] = []; + const registrars = Object.fromEntries( + TOOL_FAMILY_IDS.map((id) => [ + id, + (): void => { + seen.push(id); + }, + ]), + ) as unknown as { readonly [Id in ToolFamilyId]: () => void }; + registerToolFamilies(registrars); + expect(seen).toEqual([...TOOL_FAMILY_IDS]); + }); + it("covers every contract exactly once", () => { const owners = new Map(); for (const { id, contracts } of TOOL_FAMILY_DEFINITIONS) diff --git a/src/contracts/toolFamilies.ts b/src/contracts/toolFamilies.ts index fcb149a8f2..1980cfcad9 100644 --- a/src/contracts/toolFamilies.ts +++ b/src/contracts/toolFamilies.ts @@ -44,7 +44,7 @@ import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js"; import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js"; import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js"; import { TOOL_CONTRACTS } from "./toolContracts.js"; -import { TOOL_FAMILY_IDS, type ToolFamilyId } from "./toolFamilyIds.js"; +import type { ToolFamilyId } from "./toolFamilyIds.js"; /** One selectable family: a stable selection id and its complete contract set. */ export interface ToolFamilyDefinition { diff --git a/src/contracts/toolFamilyIds.ts b/src/contracts/toolFamilyIds.ts index 1931e947b2..5370c196cb 100644 --- a/src/contracts/toolFamilyIds.ts +++ b/src/contracts/toolFamilyIds.ts @@ -31,3 +31,15 @@ export type ToolFamilyId = (typeof TOOL_FAMILY_IDS)[number]; /** Whether a parsed selection value names a published tool family. */ export const isToolFamilyId = (value: string): value is ToolFamilyId => (TOOL_FAMILY_IDS as readonly string[]).includes(value); + +/** + * Invoke one registrar per published family, in {@link TOOL_FAMILY_IDS} order. + * Both CLI and MCP pass a complete map; a new id fails those call sites. + * This module stays free of contract schemas so CLI startup can walk the list + * without loading every tool definition. + */ +export const registerToolFamilies = (registrars: { + readonly [Id in ToolFamilyId]: () => void; +}): void => { + for (const id of TOOL_FAMILY_IDS) registrars[id](); +}; diff --git a/src/domain/analysisErrorBase.ts b/src/domain/analysisErrorBase.ts index d758ee0739..252180cc6b 100644 --- a/src/domain/analysisErrorBase.ts +++ b/src/domain/analysisErrorBase.ts @@ -251,6 +251,7 @@ import type { } from "./process/processCapture.js"; import type { FileOffsetPartialObservation } from "./native/fileOffsetPartialObservation.js"; import type { NativeCallPartialObservation } from "./native/nativeCallPartialObservation.js"; +import type { HopperCallerReconciliationPartial } from "./native/hopperCallerReconciliation.js"; import type { ElectronActivePartialObservation } from "./javascript/electronActiveObservation.js"; import type { JavaScriptRuntimeObservation } from "./javascript/javascriptRuntimeObservation.js"; import type { @@ -270,7 +271,10 @@ import type { AdbPackagePullPartialObservation, AdbDisplayPartialObservation, } from "./adb/adbDeviceAnalysis.js"; -import type { InspectSignature } from "./native/nativeInspection.js"; +import type { + InspectMacho, + InspectSignature, +} from "./native/nativeInspection.js"; import type { NativeUiObservationResult } from "./native/nativeUiObservation.js"; import type { BinaryLayout } from "./native/binaryLayout.js"; import type { @@ -314,6 +318,7 @@ export type AnalysisPartialObservation = | FileSnapshotPartialObservation | PartialProcessCaptureObservation | NativeCallPartialObservation + | HopperCallerReconciliationPartial | FileOffsetPartialObservation | ElectronActivePartialObservation | BrowserScenarioPartialObservation @@ -337,6 +342,7 @@ export type AnalysisPartialObservation = | FirmwarePartialObservation | NativeUiObservationResult | InspectSignature + | InspectMacho | ElectronPagePartialObservation | AdbActivityStartPartialObservation | AdbAcquiredFilePartialObservation diff --git a/src/domain/analysisErrorCore.ts b/src/domain/analysisErrorCore.ts index c998455519..5d1e96f1f3 100644 --- a/src/domain/analysisErrorCore.ts +++ b/src/domain/analysisErrorCore.ts @@ -33,19 +33,12 @@ export const ANALYSIS_RESOURCE_KINDS = [ /** A measured resource constraint or bounded admission refusal. */ export class AnalysisResourceConstraintError extends AnalysisError { readonly _tag = "AnalysisResourceConstraintError"; - readonly remediationAction: string | undefined; /** - * The fixed format, decoder, or capture allowance that refused the request. - * Worker limits and host resources do not raise it, so recovery derives from - * this allowance rather than from `resource`. + * The one recovery this refusal carries. A fixed allowance and a separate + * remediation sentence do not co-occur; retryability belongs to that same + * object. */ - readonly fixedAllowance: FixedAllowance | undefined; - /** - * Whether retrying the same request can succeed. A caller-selected bound is - * deterministic for the same input, so it is not retryable even when its - * resource kind would otherwise be treated as admission capacity. - */ - readonly retryable: boolean | undefined; + readonly recovery: ResourceConstraintRecovery; constructor( readonly operation: string, @@ -55,36 +48,79 @@ export class AnalysisResourceConstraintError extends AnalysisError { options?: AnalysisErrorOptions & ResourceConstraintRecovery, ) { super(`Resource constraint during ${operation}: ${reason}`, options); - this.remediationAction = options?.remediationAction; - this.fixedAllowance = options?.fixedAllowance; - this.retryable = options?.retryable; + this.recovery = resourceConstraintRecoveryFrom(options); + } + + /** Projection of `recovery`. Absent when recovery names a fixed allowance. */ + get remediationAction(): string | undefined { + return this.recovery.remediationAction; + } + + /** + * Projection of `recovery`. The fixed format, decoder, or capture allowance + * that refused the request. Worker limits and host resources do not raise + * it, so recovery derives from this allowance rather than from `resource`. + */ + get fixedAllowance(): FixedAllowance | undefined { + return this.recovery.fixedAllowance; + } + + /** + * Projection of `recovery`. Whether retrying the same request can succeed. + * A caller-selected bound is deterministic for the same input, so it is not + * retryable even when its resource kind would otherwise be treated as + * admission capacity. + */ + get retryable(): boolean | undefined { + return this.recovery.retryable; } } +const resourceConstraintRecoveryFrom = ( + options: ResourceConstraintRecovery | undefined, +): ResourceConstraintRecovery => { + if (options?.fixedAllowance !== undefined) + return { + fixedAllowance: options.fixedAllowance, + ...(options.retryable === undefined + ? {} + : { retryable: options.retryable }), + }; + return { + ...(options?.remediationAction === undefined + ? {} + : { remediationAction: options.remediationAction }), + ...(options?.retryable === undefined + ? {} + : { retryable: options.retryable }), + }; +}; + /** Carry one refusal's recovery to an error re-attributed or redacted elsewhere. */ export const resourceConstraintRecovery = ( error: AnalysisResourceConstraintError, text: (value: string) => string = (value) => value, -): ResourceConstraintRecovery => - error.fixedAllowance !== undefined +): ResourceConstraintRecovery => { + const recovery = error.recovery; + return recovery.fixedAllowance !== undefined ? { fixedAllowance: { - name: text(error.fixedAllowance.name), - kind: error.fixedAllowance.kind, + name: text(recovery.fixedAllowance.name), + kind: recovery.fixedAllowance.kind, }, - ...retryable(error), + ...(recovery.retryable === undefined + ? {} + : { retryable: recovery.retryable }), } : { - ...(error.remediationAction === undefined + ...(recovery.remediationAction === undefined ? {} - : { remediationAction: text(error.remediationAction) }), - ...retryable(error), + : { remediationAction: text(recovery.remediationAction) }), + ...(recovery.retryable === undefined + ? {} + : { retryable: recovery.retryable }), }; - -const retryable = ( - error: AnalysisResourceConstraintError, -): { readonly retryable?: boolean } => - error.retryable === undefined ? {} : { retryable: error.retryable }; +}; /** Producer-specific recovery, or a fixed allowance whose recovery is derived. */ export type ResourceConstraintRecovery = diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index fcab0bb0fd..5015503487 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -21,6 +21,7 @@ import { EvidenceIntegrityError, EvidenceReferenceError, AnalysisSnapshotMismatchError, + ResourceSnapshotMismatchError, } from "./evidenceErrors.js"; import { HopperRemoteError, @@ -79,6 +80,10 @@ export const analysisErrorRemediationAction = ( error.remediationAction !== undefined ) return error.remediationAction; + if (error instanceof ResourceSnapshotMismatchError) + return error.operation === "export" + ? "Call close_binary without snapshot_path to close this resource-only session." + : "Open an executable analysis target before importing a binary analysis snapshot."; if (error instanceof AnalysisSnapshotMismatchError) return "Run analysis without this snapshot, then save a fresh snapshot using the intended artifact, provider, and analysis profile."; if (error instanceof AnalysisUnsupportedTargetError) @@ -260,7 +265,10 @@ export const analysisErrorCategory = ( return "unavailable"; if (error instanceof ArtifactOperationError) return artifactErrorCategory(error.reason); - if (error instanceof EvidenceFileError && error.reason === "path-limit") + if ( + error instanceof EvidenceFileError && + (error.reason === "path-limit" || error.reason === "storage-limit") + ) return "resource_constraint"; if ( error instanceof EvidenceFileError && @@ -565,11 +573,18 @@ const evidenceFileMessage = ({ operation, reason, published, + systemCode, }: EvidenceFileError): string => { if (published !== undefined) return "The output file was published, but staging cleanup remains incomplete."; if (reason === "path-limit") return "Filesystem path-length constraints prevented reading or atomic publication at the selected path. Choose a shorter local path."; + if (reason === "storage-limit") + return systemCode === "EFBIG" + ? "The host file-size limit prevented Evidence publication. Increase the inherited file-size allowance and retry." + : systemCode === undefined + ? "A host filesystem resource limit prevented Evidence publication. Check the file-size allowance, filesystem space and quota before retrying." + : "The filesystem lacks space or quota for Evidence publication. Free space or increase quota and retry."; if (reason === "symlink-traversal") return "The selected path cannot be resolved because its symbolic-link chain loops or exceeds the host traversal limit. Repair the chain or select another path."; if (reason === "missing") diff --git a/src/domain/analysisErrorProjection.ts b/src/domain/analysisErrorProjection.ts index dcf0a984ed..576cd7de92 100644 --- a/src/domain/analysisErrorProjection.ts +++ b/src/domain/analysisErrorProjection.ts @@ -236,7 +236,8 @@ const evidenceFileCode = ( reason: EvidenceFileError["reason"], ): AnalysisErrorProjection["code"] => { if (reason === "invalid-json") return "evidence_integrity_mismatch"; - if (reason === "path-limit") return "resource_constraint"; + if (reason === "path-limit" || reason === "storage-limit") + return "resource_constraint"; // A missing, non-file or already-existing path is the caller's selection. if (reason === "io") return "execution_failure"; return "invalid_request"; @@ -418,6 +419,9 @@ const artifactStateErrorDetails = ( operation: error.operation, reason: error.reason, ...(error.reason === "path-limit" ? { boundary: "filesystem-path" } : {}), + ...(error.systemCode === undefined + ? {} + : { system_code: error.systemCode }), ...(error.path === undefined ? {} : { path: error.path }), ...(error.contentIssues === undefined ? {} diff --git a/src/domain/analysisResourceLimits.ts b/src/domain/analysisResourceLimits.ts index 83d6dba0f7..1be07efa08 100644 --- a/src/domain/analysisResourceLimits.ts +++ b/src/domain/analysisResourceLimits.ts @@ -31,6 +31,16 @@ export const DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS = 330_000; /** Default absolute deadline for Hopper launch, analysis, bridge, and readiness. */ export const DEFAULT_HOPPER_STARTUP_TIMEOUT_MS = 120_000; +/** + * Default wall-clock budget for one Hopper caller-scan reconciliation, in + * milliseconds. The first page always runs; a later page is not requested + * after this budget. + */ +export const DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS = 2_000; + +/** Smallest caller-scan budget. Zero is not a deadline. */ +export const MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS = 1; + /** * Largest delay Node schedules exactly. Delays above this signed 32-bit * boundary become a 1 ms timer, so startup and swiftc timeouts stop here. @@ -97,3 +107,67 @@ export const CAPTURE_DECODER_SEMI_SPACE_MIB = 8; */ export const MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB = 16_384; export const DEFAULT_JADX_OPERATION_TIMEOUT_MS = 120_000; + +/** + * Shared GDB and Rizin command deadline used when the setting is unset. + * Today's fixed 120 s constant. + */ +export const DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS = 120_000; + +/** + * Smallest command deadline. Zero is not a deadline. The maximum is the + * largest delay Node schedules exactly; a `setTimeout` delay above that + * signed 32-bit bound becomes 1 ms. + */ +export const MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS = 1; +export const MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS = 2_147_483_647; + +/** Debugger sessions admitted when the setting is unset. Today's constant. */ +export const DEFAULT_DEBUGGER_MAX_SESSIONS = 8; + +/** + * Smallest useful session cap. Zero admits no session. 64 is REA's refusal + * so one process cannot spawn an unbounded set of debugger processes; it is + * not a measurement of host process capacity. + */ +export const MINIMUM_DEBUGGER_MAX_SESSIONS = 1; +export const MAXIMUM_DEBUGGER_MAX_SESSIONS = 64; + +/** Queued commands admitted on one session when the setting is unset. */ +export const DEFAULT_DEBUGGER_MAX_PENDING_COMMANDS = 16; + +/** + * Smallest useful queue. Zero admits no command. 256 is REA's refusal so + * one session cannot retain an unbounded command queue. Each queued command + * can also retain output up to the command-output cap. + */ +export const MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS = 1; +export const MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS = 256; + +/** Command output retained when the setting is unset. Today's 16 MiB cap. */ +export const DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024; + +/** + * Node's single-string ceiling, in UTF-16 code units. This is the same V8 + * bound `MAXIMUM_BRIDGE_MAX_REQUEST_BYTES` leaves room under. + */ +const NODE_MAX_STRING_LENGTH = 536_870_888; + +/** + * `JSON.stringify` encodes U+0000–U+001F as `\uXXXX` (6 code units). Evidence + * hashing stringifies the retained command output as one string. One mebibyte + * is reserved for the surrounding evidence document. + */ +const DEBUGGER_OUTPUT_JSON_ESCAPE_CODE_UNITS = 6; +const DEBUGGER_OUTPUT_EVIDENCE_ENVELOPE_CODE_UNITS = 1024 * 1024; + +/** + * Largest configurable command-output retention. A lower value only retains + * less caller command output, so the floor is one byte. The maximum keeps + * the worst-case JSON expansion of that text under Node's string ceiling. + */ +export const MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES = 1; +export const MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES = Math.floor( + (NODE_MAX_STRING_LENGTH - DEBUGGER_OUTPUT_EVIDENCE_ENVELOPE_CODE_UNITS) / + DEBUGGER_OUTPUT_JSON_ESCAPE_CODE_UNITS, +); diff --git a/src/domain/analysisView/EnvironmentManifestAccumulator.ts b/src/domain/analysisView/EnvironmentManifestAccumulator.ts index 5dd44d28d2..fb3018c064 100644 --- a/src/domain/analysisView/EnvironmentManifestAccumulator.ts +++ b/src/domain/analysisView/EnvironmentManifestAccumulator.ts @@ -16,7 +16,8 @@ import { } from "./recordedResponseBody.js"; import { readRecordedRequestBody } from "./recordedExchangeBody.js"; import { isJson, isJsonLike, mediaTypeEssence } from "./recordedMediaType.js"; -import type { UnsignedAnalysisView } from "./analysisView.js"; +import { z } from "zod"; +import { environmentManifestFields } from "./environmentManifestSchema.js"; import type { ScenarioResponseBodyRetention } from "./scenarioCaptureRecords.js"; import { WebNetworkProjectionAccumulator } from "./WebNetworkProjectionAccumulator.js"; import { @@ -55,21 +56,13 @@ import { type RouteTemplate, } from "./webNetworkRouteTemplates.js"; -type Manifest = Extract; -type ManifestFields = Pick< - Manifest, - | "origin" - | "name" - | "tools" - | "records" - | "excluded" - | "unknowns" - | "templated_groups" - | "coverage" - | "limitations" ->; -type Tool = Manifest["tools"][number]; -type Exclusion = Manifest["excluded"][number]; +type ManifestCore = { + [Key in keyof typeof environmentManifestFields]: z.output< + (typeof environmentManifestFields)[Key] + >; +}; +type Tool = ManifestCore["tools"][number]; +type Exclusion = ManifestCore["excluded"][number]; type RequestLine = Extract; interface RecordLine { @@ -134,7 +127,7 @@ const WRITE_METHODS = new Set(["POST", "PUT", "PATCH"]); const BODILESS_WRITE_METHODS = new Set([...WRITE_METHODS, "DELETE"]); /** Quotes, separators and brackets a four-part composite key adds around its texts. */ const KEY_OVERHEAD_CHARS = 32; -const UNKNOWNS: Manifest["unknowns"] = [ +const UNKNOWNS: ManifestCore["unknowns"] = [ { kind: "authorization-model", statement: @@ -477,7 +470,7 @@ export class EnvironmentManifestAccumulator extends WebNetworkProjectionAccumula } /** Project the complete manifest; tools, records and exclusions are sorted deterministically. */ - finish(): Result { + finish() { const primary = primaryOrigin(this.#origins); const toolGroups: GroupState[] = []; const exclusions = new Map(); diff --git a/src/domain/analysisView/EnvironmentStateAccumulator.ts b/src/domain/analysisView/EnvironmentStateAccumulator.ts index bf8b2763c5..4808d9506f 100644 --- a/src/domain/analysisView/EnvironmentStateAccumulator.ts +++ b/src/domain/analysisView/EnvironmentStateAccumulator.ts @@ -11,7 +11,6 @@ import { WEB_NETWORK_CAPTURE_LIMITS, type WebNetworkCaptureRecord, } from "../webNetworkCapture.js"; -import type { UnsignedAnalysisView } from "./analysisView.js"; import { EnvironmentManifestAccumulator } from "./EnvironmentManifestAccumulator.js"; import { isJsonLike, mediaTypeEssence } from "./recordedMediaType.js"; import { readRecordedRequestBody } from "./recordedExchangeBody.js"; @@ -40,23 +39,6 @@ import { textBytes, } from "./webNetworkRouteTemplates.js"; -type StateView = Extract; -type StateFields = Pick< - StateView, - | "origin" - | "name" - | "timeline" - | "types" - | "entities" - | "documents" - | "views" - | "writes" - | "write_clock" - | "unknowns" - | "coverage" - | "limitations" ->; - /** * Levels the state's own nesting adds above a recorded value: Evidence, a `data/get` reply and a * JSON-RPC response hold a body's members about ten levels down, so a body must leave this much @@ -102,19 +84,19 @@ interface RecordEntry { readonly record: WebNetworkCaptureRecord; } -const UNKNOWNS: StateView["unknowns"] = [ +const UNKNOWNS = [ { - kind: "authorization-model", + kind: "authorization-model" as const, statement: "Authorization headers and cookies are structurally excluded from captures; the state states no authorization model.", }, { - kind: "response-semantics", + kind: "response-semantics" as const, statement: "Views reproduce their recorded responses from the state; they do not establish how the application computes responses, derived values or values copied outside entity objects.", }, { - kind: "state-transition", + kind: "state-transition" as const, statement: "The state is one snapshot from before the first related recorded write; write effects are modelled only from the calls the capture recorded, and served only where its later reads confirmed them.", }, @@ -340,7 +322,7 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator } /** Project the init state of the manifest's primary origin; records keep timeline order. */ - finishState(): Result { + finishState() { const manifest = this.finish(); if (!manifest.ok) return manifest; const entries = [...this.#entries.values()].filter( diff --git a/src/domain/analysisView/EnvironmentStateWriteSimulation.ts b/src/domain/analysisView/EnvironmentStateWriteSimulation.ts index 597e4d0ca3..28c6527cc4 100644 --- a/src/domain/analysisView/EnvironmentStateWriteSimulation.ts +++ b/src/domain/analysisView/EnvironmentStateWriteSimulation.ts @@ -109,6 +109,26 @@ const keptInOrder = ( ); }); +/** The sorts by a field every listed item holds as a string or number, and whether the items follow each. */ +const listedSorts = ( + items: readonly JsonObject[], +): { + readonly rule: Extract; + readonly follows: boolean; +}[] => + [...new Set(items.flatMap((item) => Object.keys(item)))] + .filter((field) => + items.every((item) => + ["string", "number"].includes(typeof ownMember(item, field)), + ), + ) + .flatMap((field) => + (["ascending", "descending"] as const).map((direction) => ({ + rule: { rule: "sorted", field, direction } as const, + follows: orderedBy(items, field, direction), + })), + ); + /** Whether a list read's query sets a parameter the view's query model reads as a sort or its direction. */ const sortedByQuery = ( view: Extract, @@ -182,6 +202,10 @@ export class EnvironmentStateWriteSimulation { readonly #refutedSorts = new Map>(); /** Creates whose insert rule a passing list check relied on, so it is no longer changed. */ readonly #placed = new Set(); + /** Placed creates a read other than a whole listing in collection order placed, whose insert stays as it is. */ + readonly #settledInserts = new Set(); + /** Sorts a whole listing that placed a create did not follow, by create, which no later listing adds back. */ + readonly #unfollowedSorts = new Map>(); /** The order a read showed a type's collections sorted in after an update, when no create of it showed one. */ readonly #learnedSorts = new Map(); /** Entities writes no plan models made, taken when an item read first shows them. */ @@ -297,6 +321,7 @@ export class EnvironmentStateWriteSimulation { sequences: new Map(), starts: new Map(), createCollections: [], + sentLargest: new Map(), steady: new Set(), unstamped: new Set(), kept: new Set(), @@ -796,7 +821,9 @@ export class EnvironmentStateWriteSimulation { this.#drafts.get(name)!.confirmed.push(record.ordinal); if (showsPlacement(view, outcome, this.#inserted, name)) { this.#placed.add(name); - if (view.shape === "list") this.#narrowInserts(view, record, name); + // Only whole listings in collection order narrow the inserts a later list may still choose among. + if (view.shape !== "list" || !this.#narrowInserts(view, record, name)) + this.#settledInserts.add(name); } } return shown.unseen; @@ -936,23 +963,44 @@ export class EnvironmentStateWriteSimulation { }); } - /** A whole-collection read in collection order that placed a create leaves it only the sorted inserts whose order the read follows; one its query sorts shows the query's order instead. */ + /** A whole-collection read in collection order that placed a create leaves it only the sorted inserts whose order the read follows, adding those by a field its items all hold, and says whether it did; one its query sorts shows the query's order instead. */ #narrowInserts( view: Extract, record: StateRecordFacts, name: string, - ): void { - if (sortedByQuery(view, record)) return; + ): boolean { + if (sortedByQuery(view, record)) return false; const draft = this.#drafts.get(name)!; - const kept = this.#wholeListing(view, record, (items) => - draft.candidates.filter( + const kept = this.#wholeListing(view, record, (items) => { + const followed = draft.candidates.filter( (candidate) => candidate.rule !== "sorted" || same(candidate, draft.plan.insert) || orderedBy(items, candidate.field, candidate.direction), - ), - ); - if (kept !== undefined) draft.candidates = kept; + ); + const sorts = listedSorts(items); + const unfollowed = this.#unfollowedSorts.get(name) ?? new Set(); + for (const { rule, follows } of sorts) + if (!follows) unfollowed.add(JSON.stringify(rule)); + this.#unfollowedSorts.set(name, unfollowed); + // A list shown in the order of the field that names its items says less than one in a field people read. + const idField = this.#types.get(view.type)?.id_field; + const learned = sorts + .map(({ rule }) => rule) + .filter( + (rule) => + !unfollowed.has(JSON.stringify(rule)) && + !followed.some((candidate) => same(candidate, rule)), + ) + .sort( + (left, right) => + Number(left.field === idField) - Number(right.field === idField), + ); + return [...followed, ...learned]; + }); + if (kept === undefined) return false; + draft.candidates = kept; + return true; } /** @@ -1109,9 +1157,25 @@ export class EnvironmentStateWriteSimulation { if (creates.size !== 1) return undefined; const [name] = creates; const draft = this.#drafts.get(name!)!; - // An item read confirms the entity, not where a list places it. - if (!exercised.has(name!) || this.#placed.has(name!)) return undefined; - for (const candidate of draft.candidates.slice(1)) { + // An item read confirms the entity, not where a list places it; a read that placed it otherwise than a whole + // listing did settles where it goes. + if (!exercised.has(name!) || this.#settledInserts.has(name!)) + return undefined; + // A create whole listings placed may still take a sort they followed: one placed last fits it before the next shows the order. + const options = this.#placed.has(name!) + ? draft.candidates.filter( + (candidate) => + candidate.rule === "sorted" && !same(candidate, draft.plan.insert), + ) + : draft.candidates.slice(1); + // A sort an earlier update's whole list ruled out must not become this create's order: serving would move those updates by it. + const refuted = this.#refutedSorts.get(view.type); + for (const candidate of options) { + if ( + candidate.rule === "sorted" && + refuted?.has(JSON.stringify(candidate)) === true + ) + continue; this.#state.reinsert(view.type, line.collection, inserted, candidate); const outcome = this.#check(view, record); if (outcome?.pass === true) { @@ -1119,6 +1183,13 @@ export class EnvironmentStateWriteSimulation { return outcome; } } + if (draft.plan.insert !== null) + this.#state.reinsert( + view.type, + line.collection, + inserted, + draft.plan.insert, + ); return undefined; } } diff --git a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts index 5f603b26b3..fbbf65426d 100644 --- a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts +++ b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts @@ -7,7 +7,8 @@ import { type WebNetworkCaptureRecord, } from "../webNetworkCapture.js"; import { pageViewCoverage } from "./analysisViewCoverage.js"; -import type { UnsignedAnalysisView } from "./analysisView.js"; +import { z } from "zod"; +import { apiSurfaceFields } from "./apiSurfaceSchema.js"; import { WebNetworkProjectionAccumulator } from "./WebNetworkProjectionAccumulator.js"; import { CAPTURE_CONTAINER_BYTES, @@ -23,9 +24,13 @@ import { type RouteTemplate, } from "./webNetworkRouteTemplates.js"; -type Surface = Extract; -type Group = Surface["groups"][number]; -type Exclusion = Surface["excluded_records"][number]; +type ApiSurfaceFields = { + [Key in keyof typeof apiSurfaceFields]: z.output< + (typeof apiSurfaceFields)[Key] + >; +}; +type Group = ApiSurfaceFields["groups"][number]; +type Exclusion = ApiSurfaceFields["excluded_records"][number]; interface GroupState { readonly method: string; readonly origin: string; @@ -181,18 +186,7 @@ export class WebNetworkApiSurfaceAccumulator extends WebNetworkProjectionAccumul return ok(undefined); } - page( - offset: number, - limit: number, - ): Pick< - Surface, - | "coverage" - | "groups" - | "templated_groups" - | "excluded_records" - | "limitations" - | "unknowns" - > { + page(offset: number, limit: number) { this.#groups.sort( (a, b) => compare(a.origin, b.origin) || diff --git a/src/domain/analysisView/WebNetworkProjectionAccumulator.ts b/src/domain/analysisView/WebNetworkProjectionAccumulator.ts index 0c9d7128e3..05099da027 100644 --- a/src/domain/analysisView/WebNetworkProjectionAccumulator.ts +++ b/src/domain/analysisView/WebNetworkProjectionAccumulator.ts @@ -14,7 +14,7 @@ import { import { admitAnalysisViewData } from "./analysisViewDataAdmission.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { analysisViewResultInputError } from "./analysisViewParent.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { ENTRY_BYTES, textBytes, diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index 357d538896..7b278e1184 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -1,184 +1,32 @@ -import { analysisViewCoverageSchema } from "./analysisViewCoverage.js"; -import { - javaScriptRouteInventorySourceSchema, - javaScriptRouteInventorySchema, -} from "../javascript/javascriptRouteInventory.js"; -import { nativeFunctionMatchingContextSchema } from "../native/functionMatching.js"; -import { - nativeObservationViewRequestSchema, - nativeContextViewRequestSchema, - nativeObservationItemsSchemas, - nativeContextItemsSchemas, - nativeSourceContextReferenceSchema, - nativeObservationOperationSchema, -} from "./nativeObservationSchemas.js"; -import { - NATIVE_OBSERVATION_OPERATIONS, - type NativeObservationOperation, -} from "../native/nativeObservation.js"; +import { NATIVE_OBSERVATION_OPERATIONS } from "../native/nativeObservation.js"; import { projectNativeObservationView } from "./nativeObservationView.js"; -import { - devicetreeViewRequestSchema, - devicetreeViewItemsSchema, - devicetreeSummarySchema, - devicetreePageContextSchema, - projectDevicetreeView, -} from "./devicetreeView.js"; -import { - apkSignatureViewRequestSchema, - apkSignatureViewItemsSchema, -} from "../apkSignature/apkSignatureQuery.js"; -import { - projectApkSignatureView, - apkSignatureSummarySchema, - apkSignatureSourceCoverageSchema, -} from "./apkSignatureView.js"; +import { projectDevicetreeView } from "./devicetreeView.js"; +import { projectApkSignatureView } from "./apkSignatureView.js"; import { byteEntropySchema } from "../native/byteEntropy.js"; -import { - erlangBeamViewRequestSchema, - erlangBeamViewPageSchema, - erlangBeamSummarySchema, - projectErlangBeamView, -} from "./erlangBeamView.js"; -import { - pythonPycViewRequestSchema, - pythonPycViewItemsSchema, - pythonPycSummarySchema, - projectPythonPycView, -} from "./pythonPycView.js"; -import { - nativeExportsViewRequestSchema, - nativeExportsViewItemsSchema, - nativeExportsSummarySchema, - projectNativeExportsView, -} from "./nativeExportsView.js"; -import { - byteEntropySummarySchema, - projectByteEntropyView, -} from "./byteEntropyView.js"; +import { projectErlangBeamView } from "./erlangBeamView.js"; +import { projectPythonPycView } from "./pythonPycView.js"; +import { projectNativeExportsView } from "./nativeExportsView.js"; +import { projectByteEntropyView } from "./byteEntropyView.js"; import { parseAnalysisViewResult, analysisViewResultInputError, } from "./analysisViewParent.js"; -import { - javaScriptRoutesViewRequestSchema, - javaScriptRoutesViewItemsSchema, - javaScriptRoutesViewSummarySchema, - projectJavaScriptRoutesView, -} from "./javascriptRoutesView.js"; -import { - webBundleViewRequestSchema, - webBundleViewItemsSchema, - webBundleSummarySchema, - projectWebBundleView, -} from "./webBundleView.js"; -import { webBundleAnalysisSchema } from "../webBundleAnalysis.js"; -import { recordedBrowserOriginSchema } from "../browserObservation.js"; -import { - sourceToBundleViewRequestSchema, - sourceToBundleViewItemsSchema, - sourceToBundleViewContextSchema, - projectSourceToBundleView, -} from "./sourceToBundleView.js"; -import { - gpuCodeObjectsViewRequestSchema, - gpuCodeObjectsSummarySchema, - gpuCodeObjectsItemsSchema, - gpuCodeObjectsPtxPageSchema, - projectGpuCodeObjectsView, -} from "./gpuCodeObjectsView.js"; -import { gpuCodeObjectsObjectSchema } from "../gpu/gpuCodeObjects.js"; -import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js"; -import { - projectModelArtifactView, - modelArtifactSummarySchema, - modelArtifactViewItemsSchema, - modelArtifactViewContextSchema, -} from "./modelArtifactView.js"; -import { - commandSummaryContext, - gdbCommandSummarySchema, - projectCommandObservationView, -} from "./commandObservationView.js"; -import { - fridaSessionStatusSummarySchema, - fridaSessionStatusViewContext, - projectFridaSessionStatusView, -} from "./fridaSessionStatusView.js"; -import { commandObservationSchema } from "../commandObservation.js"; -import { - rustBinaryViewPageSchema, - rustBinaryViewRequestSchema, -} from "../rust/rustBinaryQuery.js"; -import { - goBinarySummarySchema, - goBinaryViewContextSchema, - goBinaryViewPageSchema, - goBinaryViewRequestSchema, - projectGoBinaryView, -} from "./goBinaryView.js"; -import { - rustBinarySummarySchema, - rustBinaryViewContextSchema, - projectRustBinaryView, -} from "./rustBinaryView.js"; -import { - projectNativeFunctionMatchingView, - nativeFunctionMatchingViewRequestSchema, - nativeFunctionMatchingViewItemsSchema, -} from "./nativeFunctionMatchingView.js"; -import { - protobufViewRequestSchema, - protobufViewItemsSchema, - protobufViewContextSchema, -} from "./protobufCaptureView.js"; -import { - nativeInventoryViewRequestSchema, - nativeInventoryViewRowSchema, - nativeInventorySummarySchema, - projectNativeInventoryView, -} from "./nativeInventoryView.js"; -import { nativeViewSourceContext } from "./nativeViewContext.js"; -import { - NATIVE_INVENTORY_OPERATIONS, - nativeInventoryOperationSchema, -} from "../native/nativeInventory.js"; - -import { - evidenceFileReferenceSchema, - retainedEvidenceReferenceSchema, -} from "../evidenceReference.js"; +import { projectJavaScriptRoutesView } from "./javascriptRoutesView.js"; +import { projectWebBundleView } from "./webBundleView.js"; +import { projectSourceToBundleView } from "./sourceToBundleView.js"; +import { projectGpuCodeObjectsView } from "./gpuCodeObjectsView.js"; +import { projectModelArtifactView } from "./modelArtifactView.js"; +import { projectCommandObservationView } from "./commandObservationView.js"; +import { projectFridaSessionStatusView } from "./fridaSessionStatusView.js"; +import { projectGoBinaryView } from "./goBinaryView.js"; +import { projectRustBinaryView } from "./rustBinaryView.js"; +import { projectNativeFunctionMatchingView } from "./nativeFunctionMatchingView.js"; +import { projectNativeInventoryView } from "./nativeInventoryView.js"; +import { NATIVE_INVENTORY_OPERATIONS } from "../native/nativeInventory.js"; import { z } from "zod"; -import { - browserScenarioSummarySchema, - browserScenarioViewContextSchema, - browserScenarioViewItemsSchema, - browserScenarioViewRequestSchema, -} from "./browserScenarioViewSchemas.js"; import { projectBrowserScenarioView } from "./browserScenarioView.js"; -import { - nativeLoadImageViewRequestSchema, - nativeLoadImageViewItemsSchema, - nativeLoadImageSummarySchema, - projectNativeLoadImageView, -} from "./nativeLoadImageView.js"; -import { - desktopViewRequestSchema, - desktopViewItemsSchema, - desktopSummarySchema, - projectDesktopApplicationView, -} from "./desktopApplicationView.js"; -import { desktopCoverageSchema } from "../desktopApplication.js"; -import { - javascriptModuleReferencesRequestSchema, - javascriptModuleReferenceSchema, -} from "./javascriptModuleReferences.js"; -import { javascriptApplicationSourceCoverageSchema } from "./javascriptApplicationCoverage.js"; -import { - javascriptFactsViewRequestSchema, - javascriptFactRowSchema, -} from "./javascriptFactsView.js"; -import { javaScriptSourceSelectionSchema } from "../javascript/javascriptSourceSelection.js"; +import { projectNativeLoadImageView } from "./nativeLoadImageView.js"; +import { projectDesktopApplicationView } from "./desktopApplicationView.js"; import { AnalysisUnsupportedTargetError } from "../analysisErrorCore.js"; import { @@ -186,25 +34,11 @@ import { analysisViewInputError, } from "./analysisViewErrors.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; -import { digestSchema, prefixedDigestSchema } from "../digests.js"; -import { evidenceSchema, type Evidence } from "../evidence.js"; -import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js"; +import { digestSchema } from "../digests.js"; import { err, ok, type Result } from "../result.js"; import type { AnalysisError } from "../analysisErrorBase.js"; -import { - binaryLayoutSchema, - safeParseOwnedBinaryLayout, -} from "../native/binaryLayout.js"; -import { bundleResourceRootSchema } from "../apple/bundleResourceRoot.js"; -import { - functionDossierSchema, - functionFacetAvailabilitySchema, -} from "../native/functionDossier.js"; -import { - artifactGraphManifestSchema, - artifactInventoryCoverageSchema, -} from "../artifactGraph.js"; -import { artifactInspectionResultSchema } from "../artifactInspection.js"; +import { safeParseOwnedBinaryLayout } from "../native/binaryLayout.js"; +import { functionDossierSchema } from "../native/functionDossier.js"; import { parseOwnedJavaScriptApplicationAnalysisSteps, type JavaScriptApplicationAnalysisResult, @@ -214,62 +48,16 @@ import { projectJavaScriptApplicationView } from "./javascriptApplicationView.js import { createApiSurfaceAccumulator, projectWebNetworkApiSurfaceView, - apiSurfaceFields, - apiSurfaceViewSchema, } from "./webNetworkApiSurfaceView.js"; import { projectNativeFunctionView } from "./nativeFunctionView.js"; -import { - projectEvmInterfaceView, - evmViewRequestSchema, - evmViewItemSchema, - evmSummarySchema, -} from "./evmInterfaceView.js"; -import { - projectArtifactInventoryView, - artifactInventoryViewRequestSchema, - artifactInventoryItemsSchema, - artifactInventorySummarySchema, -} from "./artifactInventoryView.js"; -import { - projectManagedArtifactView, - managedViewRequestSchema, - managedViewItemSchema, - managedSummarySchema, - managedViewContextSchema, -} from "./managedArtifactView.js"; -import { - managedMembersViewRequestSchema, - managedMembersViewPageSchema, - managedMemberSummarySchema, - managedMembersViewContextSchema, -} from "./managedMemberViewSchemas.js"; +import { projectEvmInterfaceView } from "./evmInterfaceView.js"; +import { projectArtifactInventoryView } from "./artifactInventoryView.js"; +import { projectManagedArtifactView } from "./managedArtifactView.js"; import { projectManagedMemberView } from "./managedMemberView.js"; -import { - projectWasmArtifactView, - wasmViewRequestSchema, - wasmViewItemSchema, - wasmSummarySchema, -} from "./wasmArtifactView.js"; -import { - machoSummarySchema, - machoViewContextSchema, - machoViewRows, - machoViewRequestSchema, - projectMachoView, -} from "./machoView.js"; -import { - interfaceBuilderSummarySchema, - interfaceBuilderViewContextSchema, - interfaceBuilderViewItemsSchema, - interfaceBuilderViewRequestSchema, - projectInterfaceBuilderView, -} from "./interfaceBuilderView.js"; -import { - projectWebNetworkCaptureView, - captureViewRequestSchema, - captureViewItemsSchema, - captureSummarySchema, -} from "./webNetworkCaptureView.js"; +import { projectWasmArtifactView } from "./wasmArtifactView.js"; +import { projectMachoView } from "./machoView.js"; +import { projectInterfaceBuilderView } from "./interfaceBuilderView.js"; +import { projectWebNetworkCaptureView } from "./webNetworkCaptureView.js"; import type { WebNetworkApiSurfaceAccumulator } from "./WebNetworkApiSurfaceAccumulator.js"; import type { EnvironmentManifestAccumulator } from "./EnvironmentManifestAccumulator.js"; import type { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js"; @@ -277,1261 +65,148 @@ import { admitAnalysisViewData } from "./analysisViewDataAdmission.js"; import { createEnvironmentManifestAccumulator, projectEnvironmentManifestView, - environmentManifestFields, - environmentManifestViewRequestSchema, } from "./environmentManifestView.js"; import { createEnvironmentStateAccumulator, - environmentStateFields, - environmentStateViewRequestSchema, projectEnvironmentStateView, } from "./environmentStateView.js"; +import { projectEnvironmentCoverageView } from "./environmentCoverageView.js"; +import { projectPcapCaptureView } from "./pcapCaptureView.js"; +import { projectAndroidPackageView } from "./androidPackageView.js"; +import { projectAndroidSymbolSummary } from "./androidSymbolView.js"; +import { projectAndroidJniView } from "./androidJniView.js"; +import { projectDartAotView } from "./dartAotView.js"; +import { projectIl2cppMetadataView } from "./il2cppMetadataView.js"; + +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import type { GroupingViewKind } from "./analysisViewShape.js"; import { - environmentCoverageFields, - environmentCoverageViewRequestSchema, - projectEnvironmentCoverageView, -} from "./environmentCoverageView.js"; + analysisViewRequestSchema, + inspectAnalysisViewInputSchema, + type AnalysisViewRequest, + type InspectAnalysisViewInput, +} from "./analysisViewRequest.js"; import { - pcapViewRequestSchema, - pcapViewPageSchema, - pcapSummarySchema, - projectPcapCaptureView, -} from "./pcapCaptureView.js"; -import { - qlogViewRequestSchema, - qlogViewPageSchema, - qlogViewContextSchema, -} from "./qlogCaptureView.js"; -import { - projectAndroidPackageView, - androidPackageSummarySchema, - androidViewRequestSchema, - androidManifestViewItemSchema, - androidManifestViewRequestSchema, - androidComponentsViewRequestSchema, - androidComponentSummaryViewRequestSchema, - androidComponentViewRowSchema, - androidComponentViewContextSchema, - androidComponentSummarySchema, -} from "./androidPackageView.js"; -import { - projectAndroidSymbolSummary, - androidClassSummarySchema, - androidMethodSummarySchema, -} from "./androidSymbolView.js"; -import { - projectAndroidJniView, - androidJniSummarySchema, -} from "./androidJniView.js"; -import { - androidJniViewRequestSchema, - androidJniViewItemsSchema, -} from "../android/androidJniQuery.js"; -import { - projectDartAotView, - dartAotSummarySchema, - dartAotViewRequestSchema, - dartAotViewContextSchema, -} from "./dartAotView.js"; -import { - projectIl2cppMetadataView, - il2cppMetadataSummarySchema, - il2cppMetadataViewRequestSchema, - il2cppMetadataViewContextSchema, - il2cppMetadataViewItemsSchema, -} from "./il2cppMetadataView.js"; - -const summaryViewSchema = z.strictObject({ kind: z.literal("summary") }); -const facetViewSchema = z.strictObject({ - kind: z.literal("facet"), - facet: z.enum(["mitigations", "linkage"]), -}); -const indexSelectorSchema = z.strictObject({ - index: z.int().nonnegative(), -}); -const nameSelectorSchema = z.strictObject({ - name: z - .string() - .describe( - "Exact UTF-8 section or symbol name, matched against retained source bytes. Use index for non-UTF-8 names or unresolved name bytes.", - ), -}); -const pathSelectorSchema = z.strictObject({ - path: z - .string() - .describe( - "Exact observed module path or original source-map reference from a module page; an empty reference is valid.", - ), -}); -const nodeIdSelectorSchema = z.strictObject({ - node_id: prefixedDigestSchema("jag_node"), -}); -const moduleItemViewSchema = z.strictObject({ - kind: z.literal("item"), - collection: z.literal("modules"), - selector: z.union([pathSelectorSchema, nodeIdSelectorSchema]), -}); -const sectionOrSymbolItemViewSchema = z.strictObject({ - kind: z.literal("item"), - collection: z.enum(["sections", "symbols"]), - selector: z.union([indexSelectorSchema, nameSelectorSchema]), -}); -/** - * The collection-specific selector alternatives are expressed in the owning - * contract so the advertised JSON Schema preserves them: modules accept - * path/node_id, sections and symbols accept index/name. Refinement-only - * constraints do not survive JSON Schema conversion, so the advertised - * schema previously accepted combinations that runtime input validation - * rejects. - */ -const itemViewSchema = z.discriminatedUnion("collection", [ - moduleItemViewSchema, - sectionOrSymbolItemViewSchema, -]); -const pageViewSchema = z.strictObject({ - kind: z.literal("page"), - collection: z.enum(["sections", "symbols", "modules"]), - offset: z.int().nonnegative(), - limit: z.int().positive(), -}); - -/** Bounded native function facets of already authenticated analyze_function Evidence. */ -const nativeViewSchema = z.strictObject({ - kind: z.literal("native"), - facet: z.enum([ - "procedure", - "pseudocode", - "assembly", - "basic_blocks", - "comments", - "callers", - "callees", - "incoming_references", - "outgoing_references", - "unresolved_calls", - "referenced_strings", - "referenced_names", - "swift_string_literals", - "swift_string_literal_omissions", - "native_api", - "value_flow_summary", - "value_flow_operations", - "value_flow_def_use", - "value_flow_effects", - "value_flow_parameters", - "value_flow_parameter_uses", - ]), - offset: z.int().nonnegative().default(0), - limit: z.int().positive().default(64), -}); - -/** Caller-selected projection of already completed analysis Evidence. */ -export const analysisViewRequestSchema = z.discriminatedUnion("kind", [ - nativeObservationViewRequestSchema, - nativeContextViewRequestSchema, - devicetreeViewRequestSchema, - erlangBeamViewRequestSchema, - pythonPycViewRequestSchema, - nativeExportsViewRequestSchema, - javaScriptRoutesViewRequestSchema, - webBundleViewRequestSchema, - nativeFunctionMatchingViewRequestSchema, - sourceToBundleViewRequestSchema, - summaryViewSchema, - goBinaryViewRequestSchema, - rustBinaryViewRequestSchema, - desktopViewRequestSchema, - facetViewSchema, - itemViewSchema, - pageViewSchema, - javascriptModuleReferencesRequestSchema, - javascriptFactsViewRequestSchema, - nativeViewSchema, - nativeInventoryViewRequestSchema, - nativeLoadImageViewRequestSchema, - artifactInventoryViewRequestSchema, - wasmViewRequestSchema, - machoViewRequestSchema, - interfaceBuilderViewRequestSchema, - evmViewRequestSchema, - managedViewRequestSchema, - managedMembersViewRequestSchema, - androidViewRequestSchema, - androidJniViewRequestSchema, - apiSurfaceViewSchema, - environmentManifestViewRequestSchema, - environmentStateViewRequestSchema, - environmentCoverageViewRequestSchema, - captureViewRequestSchema, - browserScenarioViewRequestSchema, - protobufViewRequestSchema, - qlogViewRequestSchema, - pcapViewRequestSchema, - dartAotViewRequestSchema, - il2cppMetadataViewRequestSchema, - apkSignatureViewRequestSchema, - modelArtifactViewRequestSchema, - gpuCodeObjectsViewRequestSchema, -]); - -// The application owns admission and authenticated Evidence parsing. SDK and -// CLI envelope parsing must borrow this unknown value rather than clone it -// before admission. Advertise the owning Evidence contract in both dialects. -const inlineEvidenceInputSchema = z.unknown().nonoptional(); -inlineEvidenceInputSchema._zod.processJSONSchema = - evidenceSchema._zod.processJSONSchema; - -/** Unauthenticated inline input, an exact same-session retained reference, or one local Evidence file. */ -export const analysisViewSourceSchema = z.discriminatedUnion("kind", [ - retainedEvidenceReferenceSchema, - z.strictObject({ - kind: z.literal("inline"), - evidence: inlineEvidenceInputSchema, - }), - evidenceFileReferenceSchema, -]); - -/** Public inspect_analysis_view input. */ -export const inspectAnalysisViewInputSchema = z.strictObject({ - source: analysisViewSourceSchema, - view: analysisViewRequestSchema, -}); - -const artifactSchema = z.strictObject({ - path: z - .string() - .describe( - "Selected artifact path, or the canonical resource root when resource-only Evidence has no executable subject.", - ), - sha256: digestSchema.nullable(), -}); - -const layoutSummarySchema = z.strictObject({ - bytes: binaryLayoutSchema.shape.artifact.shape.bytes, - format: binaryLayoutSchema.shape.format, - architecture: binaryLayoutSchema.shape.architecture, - image_type: binaryLayoutSchema.shape.image_type, - entry: z.strictObject({ - reported_value: z.string().min(1), - meaning: z.enum(["linked-virtual-address", "not-applicable", "absent"]), - execution_status: z.literal("unknown"), - }), - counts: z.strictObject({ - sections: z.int().nonnegative(), - segments: z.int().nonnegative(), - symbols: z.int().nonnegative(), - relocations: z.int().nonnegative(), - }), - limitation_count: z.int().nonnegative(), -}); - -const javascriptSummarySchema = z.strictObject({ - input_path: z.string().min(1), - format: z.enum(["asar", "directory"]), - root_artifact_sha256: digestSchema, - source_selection: javaScriptSourceSelectionSchema.exactOptional(), - statistics: jsonObjectSchema, - electron: jsonObjectSchema, - coverage: jsonObjectSchema, - limitation_count: z.int().nonnegative(), -}); - -const viewResultBase = { - parent_evidence_id: prefixedDigestSchema("ev"), - parent_operation: z.enum([ - "inspect_python_pyc", - "inspect_erlang_beam", - "inspect_devicetree", - "inspect_byte_entropy", - "inspect_native_exports", - "inspect_binary_layout", - "inspect_go_binary", - "gdb_console", - "frida_session_status", - "inspect_with_objdump", - "inspect_desktop_application", - "analyze_javascript_application", - "analyze_web_bundle", - "analyze_function", - "inspect_artifact", - "inventory_artifact", - "inspect_wasm_artifact", - "inspect_macho", - "decode_interface_builder", - "inspect_evm_interface", - "inspect_managed_artifact", - "inspect_managed_members", - "inspect_web_network_capture", - "inspect_android_package", - "inspect_android_class", - "inspect_android_method", - "inspect_android_jni_bindings", - "inspect_pcap_capture", - "inspect_dart_aot", - "inspect_il2cpp_metadata", - "inspect_model_artifact", - "inspect_gpu_code_objects", - ...NATIVE_INVENTORY_OPERATIONS, - ...NATIVE_OBSERVATION_OPERATIONS, - "match_native_functions", - ]), - parent_digest: digestSchema, - view_digest: digestSchema, - artifact: artifactSchema, - coverage: analysisViewCoverageSchema, - limitations: z.array(z.string()), - unknowns: z.array(z.string()), -} as const; - -const summaryViewResultBase = { - kind: z.literal("summary"), - view: summaryViewSchema, - ...viewResultBase, -} as const; - -const nativeObservationSummaryFor = < - Operation extends NativeObservationOperation, ->( - operation: Operation, -) => - z.strictObject({ - ...summaryViewResultBase, - ...nativeViewSourceContext, - artifact: artifactSchema.nullable(), - parent_operation: z.literal(operation), - source_context: nativeSourceContextReferenceSchema, - summary: z.strictObject({ observations: z.int().nonnegative() }), - items: nativeObservationItemsSchemas[operation], - }); -export const nativeObservationSummaryViewResultSchemas = { - procedure_address: nativeObservationSummaryFor("procedure_address"), - procedure_callees: nativeObservationSummaryFor("procedure_callees"), - procedure_callers: nativeObservationSummaryFor("procedure_callers"), - resolve_containing_procedure: nativeObservationSummaryFor( - "resolve_containing_procedure", - ), - get_call_graph: nativeObservationSummaryFor("get_call_graph"), -}; -const nativeObservationSummaryViewResultSchema = z.discriminatedUnion( - "parent_operation", - [ - nativeObservationSummaryViewResultSchemas.procedure_address, - nativeObservationSummaryViewResultSchemas.procedure_callees, - nativeObservationSummaryViewResultSchemas.procedure_callers, - nativeObservationSummaryViewResultSchemas.resolve_containing_procedure, - nativeObservationSummaryViewResultSchemas.get_call_graph, - ], -); -const nativeObservationPageFor = ( - operation: Operation, -) => - nativeObservationSummaryFor(operation) - .omit({ kind: true, view: true }) - .extend({ - kind: z.literal("native-observation"), - view: nativeObservationViewRequestSchema, - }); -const nativeObservationPageViewResultSchema = z.discriminatedUnion( - "parent_operation", - [ - nativeObservationPageFor("procedure_address"), - nativeObservationPageFor("procedure_callees"), - nativeObservationPageFor("procedure_callers"), - nativeObservationPageFor("resolve_containing_procedure"), - nativeObservationPageFor("get_call_graph"), - ], -); -const nativeContextFor = ( - facet: Facet, -) => - z.strictObject({ - ...viewResultBase, - ...nativeViewSourceContext, - kind: z.literal("native-context"), - artifact: artifactSchema.nullable(), - parent_operation: nativeObservationOperationSchema, - source_context: nativeSourceContextReferenceSchema, - facet: z.literal(facet), - view: nativeContextViewRequestSchema.extend({ facet: z.literal(facet) }), - items: nativeContextItemsSchemas[facet], - }); -const nativeContextViewResultSchema = z.discriminatedUnion("facet", [ - nativeContextFor("overview"), - nativeContextFor("analysis-profile"), - nativeContextFor("limitations"), - nativeContextFor("result-limitations"), -]); - -export const byteEntropySummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - summary: byteEntropySummarySchema, - parent_operation: z.literal("inspect_byte_entropy"), -}); - -export const nativeExportsSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_native_exports"), - summary: nativeExportsSummarySchema, -}); - -export const devicetreeSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_devicetree"), - summary: devicetreeSummarySchema, -}); -export const erlangBeamSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_erlang_beam"), - summary: erlangBeamSummarySchema, -}); -export const erlangBeamTableViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("beam"), - parent_operation: z.literal("inspect_erlang_beam"), - view: erlangBeamViewRequestSchema, - beam: erlangBeamSummarySchema, - page: erlangBeamViewPageSchema, -}); - -export const pythonPycSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_python_pyc"), - summary: pythonPycSummarySchema, -}); - -export const pythonPycCodeObjectsViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("python-pyc"), - parent_operation: z.literal("inspect_python_pyc"), - view: pythonPycViewRequestSchema, - pyc: pythonPycSummarySchema, - items: pythonPycViewItemsSchema, -}); - -/** Opt-in command delivery keeps output once and retains the complete parent. */ -export const gdbCommandSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...commandSummaryContext, - parent_operation: z.literal("gdb_console"), - artifact: artifactSchema.nullable(), - summary: gdbCommandSummarySchema, -}); - -export const objdumpSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...commandSummaryContext, - parent_operation: z.literal("inspect_with_objdump"), - artifact: artifactSchema.nullable(), - summary: commandObservationSchema, -}); - -export const fridaSessionStatusSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...fridaSessionStatusViewContext, - parent_operation: z.literal("frida_session_status"), - artifact: artifactSchema.nullable(), - summary: fridaSessionStatusSummarySchema, -}); - -/** Opt-in Go delivery retains original bytes and exposes build identity/counts. */ -export const goBinarySummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...goBinaryViewContextSchema.shape, - parent_operation: z.literal("inspect_go_binary"), - summary: goBinarySummarySchema, -}); - -/** Compact source identity and counts, retaining the complete embedded graph. */ -export const rustBinarySummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...rustBinaryViewContextSchema.shape, - parent_operation: z.literal("inspect_rust_binary"), - summary: rustBinarySummarySchema, -}); - -export const rustBinaryPageViewResultSchema = z.strictObject({ - ...viewResultBase, - ...rustBinaryViewContextSchema.shape, - kind: z.literal("rust"), - parent_operation: z.literal("inspect_rust_binary"), - view: rustBinaryViewRequestSchema, - rust: rustBinarySummarySchema, - page: rustBinaryViewPageSchema, -}); - -/** Summary `inspect_binary_layout` detail returns; no other view kind. */ -export const binaryLayoutSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - summary: layoutSummarySchema, - parent_operation: z.literal("inspect_binary_layout"), -}); - -/** Summary `analyze_javascript_application` detail returns; no other view kind. */ -export const javascriptApplicationSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - summary: javascriptSummarySchema, - parent_operation: z.literal("analyze_javascript_application"), -}); - -/** Typed reference page; full item observations remain available separately. */ -export const javascriptModuleReferencesViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("javascript"), - view: javascriptModuleReferencesRequestSchema, - parent_operation: z.literal("analyze_javascript_application"), - rows: z.array(javascriptModuleReferenceSchema), - source_coverage: javascriptApplicationSourceCoverageSchema, -}); - -/** Paged retained observations with exact selected properties and provenance. */ -const javascriptFactsViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("javascript-facts"), - view: javascriptFactsViewRequestSchema, - parent_operation: z.literal("analyze_javascript_application"), - rows: z.array(javascriptFactRowSchema), - source_coverage: javascriptApplicationSourceCoverageSchema, -}); - -/** Exact summary carrier for artifact inspection's optional delivery contract. */ -export const artifactInventorySummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.enum(["inspect_artifact", "inventory_artifact"]), - summary: artifactInventorySummarySchema, -}); - -export const desktopApplicationSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_desktop_application"), - summary: desktopSummarySchema, -}); - -/** Exact summary carrier for WASM inspection's optional delivery contract. */ -export const wasmArtifactSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_wasm_artifact"), - summary: wasmSummarySchema, -}); - -/** Exact slice identity and collection counts for Mach-O's selected delivery. */ -export const machoSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_macho"), - summary: machoSummarySchema, -}); - -/** Decoded documents and graph counts for Interface Builder's selected delivery. */ -export const interfaceBuilderSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("decode_interface_builder"), - resource_root: bundleResourceRootSchema.optional(), - summary: interfaceBuilderSummarySchema, -}); - -/** Exact inferred-interface summary for EVM's caller-selected delivery. */ -export const evmInterfaceSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_evm_interface"), - summary: evmSummarySchema, -}); - -/** Exact metadata overview for the managed artifact's selected delivery. */ -export const managedArtifactSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_managed_artifact"), - summary: managedSummarySchema, -}); - -/** Exact member selection, coverage and counts for the selected delivery. */ -export const managedMemberSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_managed_members"), - summary: managedMemberSummarySchema, -}); - -/** Exact historical capture summary, with no body or member-inventory payload. */ -export const webNetworkCaptureSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_web_network_capture"), - summary: captureSummarySchema, -}); - -/** Compact outcomes and the focus screen from a retained browser scenario. */ -export const browserScenarioSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - artifact: z.null(), - parent_operation: z.literal("capture_browser_scenario"), - summary: browserScenarioSummarySchema, -}); - -/** Exact package declarations with retained decoded manifest text. */ -export const androidPackageSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_android_package"), - summary: androidPackageSummarySchema, -}); -export const androidJniSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_android_jni_bindings"), - summary: androidJniSummarySchema, -}); -export const androidJniPageViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("android-jni"), - parent_operation: z.literal("inspect_android_jni_bindings"), - view: androidJniViewRequestSchema, - items: androidJniViewItemsSchema, - source: androidJniSummarySchema.pick({ - native_inventory: true, - library_inventory: true, - selection: true, - }), -}); - -/** Exact class facts with complete raw protocol Evidence retained separately. */ -export const androidClassSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_android_class"), - summary: androidClassSummarySchema, -}); - -/** Exact selected method body, source coverage and optional mapping facts. */ -export const androidMethodSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_android_method"), - summary: androidMethodSummarySchema, -}); - -/** Legacy manifest views keep their original shape; new facets are explicit. */ -export const androidManifestViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("android"), - facet: z.literal("manifest").optional(), - view: androidManifestViewRequestSchema, - item: androidManifestViewItemSchema, - parent_operation: z.literal("inspect_android_package"), -}); -export const androidComponentsViewResultSchema = z.strictObject({ - ...viewResultBase, - ...androidComponentViewContextSchema.shape, - kind: z.literal("android"), - facet: z.literal("components"), - view: androidComponentsViewRequestSchema, - items: z.array(androidComponentViewRowSchema), - byte_limited: z.boolean(), - parent_operation: z.literal("inspect_android_package"), -}); -export const androidComponentSummaryViewResultSchema = z.strictObject({ - ...viewResultBase, - ...androidComponentViewContextSchema.shape, - kind: z.literal("android"), - facet: z.literal("component-summary"), - view: androidComponentSummaryViewRequestSchema, - summary: androidComponentSummarySchema, - parent_operation: z.literal("inspect_android_package"), -}); -const androidPackageViewResultSchema = z.discriminatedUnion("facet", [ + nativeObservationSummaryViewResultSchemas, + byteEntropySummaryViewResultSchema, + nativeExportsSummaryViewResultSchema, + devicetreeSummaryViewResultSchema, + erlangBeamSummaryViewResultSchema, + erlangBeamTableViewResultSchema, + pythonPycSummaryViewResultSchema, + pythonPycCodeObjectsViewResultSchema, + gdbCommandSummaryViewResultSchema, + objdumpSummaryViewResultSchema, + fridaSessionStatusSummaryViewResultSchema, + goBinarySummaryViewResultSchema, + rustBinarySummaryViewResultSchema, + rustBinaryPageViewResultSchema, + binaryLayoutSummaryViewResultSchema, + javascriptApplicationSummaryViewResultSchema, + artifactInventorySummaryViewResultSchema, + desktopApplicationSummaryViewResultSchema, + wasmArtifactSummaryViewResultSchema, + machoSummaryViewResultSchema, + interfaceBuilderSummaryViewResultSchema, + evmInterfaceSummaryViewResultSchema, + managedArtifactSummaryViewResultSchema, + managedMemberSummaryViewResultSchema, + webNetworkCaptureSummaryViewResultSchema, + browserScenarioSummaryViewResultSchema, + androidPackageSummaryViewResultSchema, + androidJniSummaryViewResultSchema, + androidJniPageViewResultSchema, + androidClassSummaryViewResultSchema, + androidMethodSummaryViewResultSchema, androidManifestViewResultSchema, androidComponentsViewResultSchema, androidComponentSummaryViewResultSchema, -]); - -export const pcapCaptureSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_pcap_capture"), - summary: pcapSummarySchema, -}); - -/** Exact snapshot and pool-coverage carrier for selected AOT delivery. */ -export const dartAotSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_dart_aot"), - summary: dartAotSummarySchema, -}); - -export const javaScriptRoutesViewResultSchema = z.strictObject({ - ...viewResultBase, - artifact: artifactSchema.nullable(), - kind: z.literal("javascript-routes"), - parent_operation: z.enum([ - "analyze_javascript_application", - "analyze_web_bundle", - ]), - view: javaScriptRoutesViewRequestSchema, - inventory_id: javaScriptRouteInventorySchema.shape.inventory_id, - source_manifest_sha256: digestSchema, - source_coverage: javaScriptRouteInventorySchema.shape.coverage, - inventory_capacity: javaScriptRouteInventorySchema.shape.capacity, - capture_url: z.string().nullable(), - items: javaScriptRoutesViewItemsSchema, - sources: z.array(javaScriptRouteInventorySourceSchema), - summary: javaScriptRoutesViewSummarySchema.nullable(), - byte_limited: z.boolean(), -}); - -/** Compact web bundle findings retain endpoint observations and inference candidates. */ -export const webBundleSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - artifact: artifactSchema.nullable(), - parent_operation: z.literal("analyze_web_bundle"), - target_url: z.string(), - target_origin: recordedBrowserOriginSchema.nullable().optional(), - source_unknowns: webBundleAnalysisSchema.shape.unknowns, - summary: webBundleSummarySchema, -}); - -export const webBundlePageViewResultSchema = z.strictObject({ - ...viewResultBase, - artifact: artifactSchema.nullable(), - kind: z.literal("web-bundle"), - parent_operation: z.literal("analyze_web_bundle"), - target_url: z.string(), - target_origin: recordedBrowserOriginSchema.nullable().optional(), - source_unknowns: webBundleAnalysisSchema.shape.unknowns, - view: webBundleViewRequestSchema, - completeness: webBundleAnalysisSchema.shape.completeness, - items: webBundleViewItemsSchema, - summary: webBundleSummarySchema.nullable(), - byte_limited: z.boolean(), -}); - -export const gpuCodeObjectsSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_gpu_code_objects"), - summary: gpuCodeObjectsSummarySchema, -}); -export const gpuCodeObjectsPageViewResultSchema = z.strictObject({ - kind: z.literal("gpu-code-objects"), - view: gpuCodeObjectsViewRequestSchema, - items: gpuCodeObjectsItemsSchema, - ptx: gpuCodeObjectsPtxPageSchema.nullable(), - source_coverage: gpuCodeObjectsObjectSchema.shape.coverage, - ...viewResultBase, - parent_operation: z.literal("inspect_gpu_code_objects"), -}); - -export const apkSignatureSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_apk_signature"), - summary: apkSignatureSummarySchema, -}); -export const apkSignaturePageViewResultSchema = z.strictObject({ - ...viewResultBase, - kind: z.literal("apk-signature"), - parent_operation: z.literal("inspect_apk_signature"), - view: apkSignatureViewRequestSchema, - items: apkSignatureViewItemsSchema, - source_coverage: apkSignatureSourceCoverageSchema, -}); - -export const modelArtifactSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_model_artifact"), - summary: modelArtifactSummarySchema, -}); - -export const modelArtifactPageViewResultSchema = z.strictObject({ - kind: z.literal("model-artifact"), - view: modelArtifactViewRequestSchema, - items: modelArtifactViewItemsSchema, - ...viewResultBase, - ...modelArtifactViewContextSchema.shape, - parent_operation: z.literal("inspect_model_artifact"), -}); - -/** Metadata identity and row counts for selected IL2CPP delivery. */ -export const il2cppMetadataSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("inspect_il2cpp_metadata"), - summary: il2cppMetadataSummarySchema, -}); - -/** Exact inventory summary with the original observation's provider and query. */ -export const nativeInventorySummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...nativeViewSourceContext, - parent_operation: nativeInventoryOperationSchema, - artifact: artifactSchema.nullable(), - summary: nativeInventorySummarySchema, -}); - -/** Compact provider load observations retain the independent verification state. */ -export const nativeLoadImageSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - ...nativeViewSourceContext, - artifact: artifactSchema.nullable(), - parent_operation: z.literal("inspect_native_load_image"), - summary: nativeLoadImageSummarySchema, -}); - -/** Compact native matching discovery retains the complete matching report. */ -export const nativeFunctionMatchingSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - artifact: z.null(), - parent_operation: z.literal("match_native_functions"), - summary: nativeFunctionMatchingContextSchema, -}); - -/** Rows observed for one facet, or null when the provider declared that facet unavailable. */ -const declaredFacetCountSchema = z - .number() - .int() - .nonnegative() - .nullable() - .describe( - "Observed rows; null when facet_availability declares the facet unavailable, so no rows is not evidence of none.", - ); - -/** Rows of an optional dossier record, or null when the dossier omitted it. */ -const recordedFacetCountSchema = z - .number() - .int() - .nonnegative() - .nullable() - .describe( - "Observed rows; null when the dossier did not record the facet (the native facet view reports it not recorded), so no rows is not evidence of none.", - ); - -/** Rows of one value-flow facet, or null when value flow is unavailable. */ -const valueFlowFacetCountSchema = z - .number() - .int() - .nonnegative() - .nullable() - .describe( - "Observed rows; null when value_flow.available is false because native_value_flow was not recorded or the provider declared it unavailable (unknowns names which), so no rows is not evidence of none.", - ); - -/** Compact analyze_function dossier; the complete dossier stays retained for facet paging. */ -export const nativeFunctionSummarySchema = z.strictObject({ - procedure: z.strictObject({ - name: z.string(), - address: z.string(), - }), - pseudocode: z.strictObject({ - available: z - .boolean() - .describe( - "Provider facet_availability for pseudocode when declared; otherwise whether decompiled text is present. The flag is authoritative over raw length.", - ), - code_units: z - .number() - .int() - .nonnegative() - .describe( - "Pseudocode length in UTF-16 code units, the unit inspect_analysis_view pages pseudocode by.", - ), - }), - value_flow: z.strictObject({ - available: z.boolean(), - truncated: z - .boolean() - .describe( - "True only when value-flow is available and the provider truncated it; false when value-flow is unavailable (see available).", - ), - }), - facet_availability: functionFacetAvailabilitySchema - .nullable() - .describe( - "The provider's declared availability per facet, or null when it declared none. Unavailable facets name their reason here and in unknowns.", - ), - counts: z.strictObject({ - assembly: declaredFacetCountSchema, - basic_blocks: declaredFacetCountSchema, - comments: declaredFacetCountSchema, - callers: declaredFacetCountSchema, - callees: declaredFacetCountSchema, - incoming_references: declaredFacetCountSchema, - outgoing_references: declaredFacetCountSchema, - unresolved_calls: recordedFacetCountSchema, - referenced_strings: declaredFacetCountSchema, - referenced_names: declaredFacetCountSchema, - swift_string_literals: recordedFacetCountSchema, - swift_string_literal_omissions: recordedFacetCountSchema, - value_flow_operations: valueFlowFacetCountSchema, - value_flow_def_use: valueFlowFacetCountSchema, - value_flow_effects: valueFlowFacetCountSchema, - value_flow_parameters: valueFlowFacetCountSchema, - value_flow_parameter_uses: valueFlowFacetCountSchema, - }), - limitation_count: z.number().int().nonnegative(), -}); - -/** Compact analyze_function summary that retains the complete native dossier Evidence. */ -export const nativeFunctionSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - parent_operation: z.literal("analyze_function"), - procedure_address: z.string().nullable(), - summary: nativeFunctionSummarySchema, -}); - -export const sourceToBundleSummaryViewResultSchema = z.strictObject({ - ...summaryViewResultBase, - artifact: z.null(), - parent_operation: z.literal("compare_source_to_bundle"), - summary: sourceToBundleViewContextSchema, -}); -export const sourceToBundlePageViewResultSchema = z.strictObject({ - ...viewResultBase, - ...sourceToBundleViewContextSchema.shape, - kind: z.literal("source-to-bundle"), - artifact: z.null(), - parent_operation: z.literal("compare_source_to_bundle"), - view: sourceToBundleViewRequestSchema, - items: sourceToBundleViewItemsSchema, -}); - -/** Bind the existing nested facet selection to its producer-owned rows. */ -const machoPage = (facet: Facet) => - z.strictObject({ - ...viewResultBase, - ...machoViewContextSchema.shape, - kind: z.literal("macho"), - parent_operation: z.literal("inspect_macho"), - view: machoViewRequestSchema.extend({ facet: z.literal(facet) }), - item: machoViewRows[facet], - }); - -// The input tag preserves kind dispatch; the output projection advertises the -// correlated pages without adding a discriminator to their public wire shape. -const machoPageViewResultSchema = z - .object({ kind: z.literal("macho") }) - .passthrough() - .pipe( - z.union([ - machoPage("entrypoints"), - machoPage("architectures"), - machoPage("build_metadata"), - machoPage("load_commands"), - machoPage("dependencies"), - machoPage("imports"), - machoPage("exports"), - machoPage("segments"), - machoPage("provenance"), - ]), - ); - -/** Projected facts for one selected view of retained analysis Evidence. */ -export const analysisViewResultSchema = z.discriminatedUnion("kind", [ - nativeObservationPageViewResultSchema, - nativeContextViewResultSchema, + pcapCaptureSummaryViewResultSchema, + dartAotSummaryViewResultSchema, javaScriptRoutesViewResultSchema, + webBundleSummaryViewResultSchema, webBundlePageViewResultSchema, - sourceToBundlePageViewResultSchema, - z.strictObject({ - ...viewResultBase, - ...nativeFunctionMatchingContextSchema.shape, - kind: z.literal("function-matches"), - artifact: z.null(), - view: nativeFunctionMatchingViewRequestSchema, - items: nativeFunctionMatchingViewItemsSchema, - parent_operation: z.literal("match_native_functions"), - }), - z.strictObject({ - ...viewResultBase, - ...apiSurfaceFields, - kind: z.literal("api-surface"), - parent_operation: z.literal("inspect_web_network_capture"), - view: apiSurfaceViewSchema, - artifact: z.strictObject({ - path: z.string(), - sha256: digestSchema.nullable(), - }), - }), - z.strictObject({ - ...viewResultBase, - ...environmentManifestFields, - kind: z.literal("environment-manifest"), - parent_operation: z.enum([ - "inspect_web_network_capture", - "capture_browser_scenario", - ]), - view: environmentManifestViewRequestSchema, - }), - z.strictObject({ - ...viewResultBase, - ...environmentStateFields, - kind: z.literal("environment-state"), - parent_operation: z.enum([ - "inspect_web_network_capture", - "capture_browser_scenario", - ]), - view: environmentStateViewRequestSchema, - }), - z.strictObject({ - ...viewResultBase, - ...environmentCoverageFields, - kind: z.literal("environment-coverage"), - parent_operation: z.enum([ - "inspect_web_network_capture", - "capture_browser_scenario", - ]), - view: environmentCoverageViewRequestSchema, - }), - z.discriminatedUnion("parent_operation", [ - nativeObservationSummaryViewResultSchema, - erlangBeamSummaryViewResultSchema, - devicetreeSummaryViewResultSchema, - pythonPycSummaryViewResultSchema, - nativeExportsSummaryViewResultSchema, - byteEntropySummaryViewResultSchema, - gdbCommandSummaryViewResultSchema, - objdumpSummaryViewResultSchema, - fridaSessionStatusSummaryViewResultSchema, - goBinarySummaryViewResultSchema, - rustBinarySummaryViewResultSchema, - binaryLayoutSummaryViewResultSchema, - desktopApplicationSummaryViewResultSchema, - javascriptApplicationSummaryViewResultSchema, - artifactInventorySummaryViewResultSchema, - wasmArtifactSummaryViewResultSchema, - machoSummaryViewResultSchema, - interfaceBuilderSummaryViewResultSchema, - evmInterfaceSummaryViewResultSchema, - managedArtifactSummaryViewResultSchema, - managedMemberSummaryViewResultSchema, - webNetworkCaptureSummaryViewResultSchema, - browserScenarioSummaryViewResultSchema, - androidPackageSummaryViewResultSchema, - androidJniSummaryViewResultSchema, - androidClassSummaryViewResultSchema, - androidMethodSummaryViewResultSchema, - pcapCaptureSummaryViewResultSchema, - dartAotSummaryViewResultSchema, - il2cppMetadataSummaryViewResultSchema, - apkSignatureSummaryViewResultSchema, - modelArtifactSummaryViewResultSchema, - gpuCodeObjectsSummaryViewResultSchema, - nativeInventorySummaryViewResultSchema, - nativeLoadImageSummaryViewResultSchema, - nativeFunctionMatchingSummaryViewResultSchema, - nativeFunctionSummaryViewResultSchema, - sourceToBundleSummaryViewResultSchema, - webBundleSummaryViewResultSchema, - ]), - z.strictObject({ - ...viewResultBase, - kind: z.literal("devicetree"), - parent_operation: z.literal("inspect_devicetree"), - view: devicetreeViewRequestSchema, - devicetree: devicetreePageContextSchema, - items: devicetreeViewItemsSchema, - }), - erlangBeamTableViewResultSchema, - pythonPycCodeObjectsViewResultSchema, - z.strictObject({ - ...viewResultBase, - kind: z.literal("native-exports"), - parent_operation: z.literal("inspect_native_exports"), - view: nativeExportsViewRequestSchema, - exports: nativeExportsSummarySchema, - items: nativeExportsViewItemsSchema, - }), - z.strictObject({ - ...viewResultBase, - ...nativeViewSourceContext, - kind: z.literal("native-inventory"), - parent_operation: nativeInventoryOperationSchema, - artifact: artifactSchema.nullable(), - view: nativeInventoryViewRequestSchema, - rows: z.array(nativeInventoryViewRowSchema), - }), - z.strictObject({ - ...viewResultBase, - kind: z.literal("native-load-image"), - ...nativeViewSourceContext, - verification_status: z.enum(["unsupported", "verified", "mismatch"]), - parent_operation: z.literal("inspect_native_load_image"), - artifact: artifactSchema.nullable(), - view: nativeLoadImageViewRequestSchema, - items: nativeLoadImageViewItemsSchema, - }), - z.strictObject({ - kind: z.literal("protobuf"), - view: protobufViewRequestSchema, - items: protobufViewItemsSchema, - ...viewResultBase, - ...protobufViewContextSchema.shape, - parent_operation: z.literal("inspect_web_network_capture"), - }), - z.strictObject({ - kind: z.literal("capture"), - view: captureViewRequestSchema, - items: captureViewItemsSchema, - ...viewResultBase, - parent_operation: z.literal("inspect_web_network_capture"), - }), - z.strictObject({ - kind: z.literal("browser-scenario"), - view: browserScenarioViewRequestSchema, - items: browserScenarioViewItemsSchema, - ...viewResultBase, - ...browserScenarioViewContextSchema.shape, - artifact: z.null(), - parent_operation: z.literal("capture_browser_scenario"), - }), - z.strictObject({ - kind: z.literal("qlog"), - view: qlogViewRequestSchema, - page: qlogViewPageSchema, - context: qlogViewContextSchema, - ...viewResultBase, - parent_operation: z.literal("inspect_web_network_capture"), - }), - z.strictObject({ - kind: z.literal("pcap"), - view: pcapViewRequestSchema, - page: pcapViewPageSchema, - source_coverage: z.enum(["complete", "partial"]), - ...viewResultBase, - parent_operation: z.literal("inspect_pcap_capture"), - }), - z.strictObject({ - kind: z.literal("facet"), - view: facetViewSchema, - facet: jsonValueSchema, - ...viewResultBase, - }), - z.strictObject({ - ...viewResultBase, - ...goBinaryViewContextSchema.shape, - kind: z.literal("go"), - parent_operation: z.literal("inspect_go_binary"), - view: goBinaryViewRequestSchema, - page: goBinaryViewPageSchema, - }), - rustBinaryPageViewResultSchema, - z.strictObject({ - kind: z.literal("item"), - view: itemViewSchema, - item: jsonValueSchema, - ...viewResultBase, - }), - z.strictObject({ - kind: z.literal("page"), - view: pageViewSchema, - items: z.array(jsonValueSchema), - ...viewResultBase, - }), - javascriptModuleReferencesViewResultSchema, - javascriptFactsViewResultSchema, - z.strictObject({ - kind: z.literal("native"), - view: nativeViewSchema, - procedure_address: z.string().nullable(), - item: jsonValueSchema, - ...viewResultBase, - }), - z.strictObject({ - kind: z.literal("wasm"), - view: wasmViewRequestSchema, - item: wasmViewItemSchema, - ...viewResultBase, - }), - z.strictObject({ - kind: z.literal("desktop-application"), - view: desktopViewRequestSchema, - items: desktopViewItemsSchema, - ...viewResultBase, - source_coverage: desktopCoverageSchema, - parent_operation: z.literal("inspect_desktop_application"), - }), - machoPageViewResultSchema, - z.strictObject({ - kind: z.literal("interface-builder"), - view: interfaceBuilderViewRequestSchema, - items: interfaceBuilderViewItemsSchema, - ...viewResultBase, - resource_root: bundleResourceRootSchema.optional(), - ...interfaceBuilderViewContextSchema.shape, - parent_operation: z.literal("decode_interface_builder"), - }), - z.strictObject({ - kind: z.literal("evm"), - view: evmViewRequestSchema, - item: evmViewItemSchema, - ...viewResultBase, - parent_operation: z.literal("inspect_evm_interface"), - }), - z.strictObject({ - kind: z.literal("managed"), - view: managedViewRequestSchema, - item: managedViewItemSchema, - ...viewResultBase, - ...managedViewContextSchema.shape, - parent_operation: z.literal("inspect_managed_artifact"), - }), - z.strictObject({ - kind: z.literal("managed-members"), - view: managedMembersViewRequestSchema, - page: managedMembersViewPageSchema, - ...viewResultBase, - ...managedMembersViewContextSchema.shape, - parent_operation: z.literal("inspect_managed_members"), - }), - androidPackageViewResultSchema, - androidJniPageViewResultSchema, - z.strictObject({ - kind: z.literal("dart-aot"), - view: dartAotViewRequestSchema, - items: z.array(z.string()), - ...viewResultBase, - ...dartAotViewContextSchema.shape, - parent_operation: z.literal("inspect_dart_aot"), - }), - apkSignaturePageViewResultSchema, - modelArtifactPageViewResultSchema, + gpuCodeObjectsSummaryViewResultSchema, gpuCodeObjectsPageViewResultSchema, - z.strictObject({ - kind: z.literal("il2cpp-metadata"), - view: il2cppMetadataViewRequestSchema, - items: il2cppMetadataViewItemsSchema, - ...viewResultBase, - ...il2cppMetadataViewContextSchema.shape, - parent_operation: z.literal("inspect_il2cpp_metadata"), - }), - z.strictObject({ - kind: z.literal("inventory"), - view: artifactInventoryViewRequestSchema, - source_coverage: artifactInventoryCoverageSchema.exactOptional(), - manifest: artifactGraphManifestSchema, - inventory_evidence_ids: z.array(prefixedDigestSchema("ev")).min(1), - source_complete: z.boolean(), - items: artifactInventoryItemsSchema, - ...viewResultBase, - }), -]); - -export type AnalysisViewRequest = z.output; -export type InspectAnalysisViewInput = z.output< - typeof inspectAnalysisViewInputSchema ->; -export type AnalysisViewResult = z.output; - -/** Parent record already resolved from inline Evidence or a session reference. */ -export interface AnalysisViewParent { - readonly evidenceId: string; - readonly operation: string; - readonly normalizedResult: unknown; - readonly provider?: Evidence["provider"]; - readonly analysisProfile?: Evidence["analysis_profile"]; - readonly authority?: Evidence["authority"]; - readonly environment?: Evidence["environment"]; - /** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */ - readonly sealedResult?: boolean; - /** Subject presentation for this delivery, including explicit absence. */ - readonly subject?: Evidence["subject"]; - readonly limitations: readonly string[]; - readonly artifact?: { readonly path: string; readonly sha256: string } | null; - /** Parent Evidence parameters, when the parent operation records them. */ - readonly parameters?: unknown; -} - -/** View payload before the content digest is attached. */ -type WithoutViewDigest = T extends { readonly view_digest: string } - ? Omit - : never; -export type UnsignedAnalysisView = WithoutViewDigest; + apkSignatureSummaryViewResultSchema, + apkSignaturePageViewResultSchema, + modelArtifactSummaryViewResultSchema, + modelArtifactPageViewResultSchema, + il2cppMetadataSummaryViewResultSchema, + nativeInventorySummaryViewResultSchema, + nativeLoadImageSummaryViewResultSchema, + nativeFunctionMatchingSummaryViewResultSchema, + nativeFunctionSummaryViewResultSchema, + sourceToBundleSummaryViewResultSchema, + sourceToBundlePageViewResultSchema, + analysisViewResultSchema, + type AnalysisViewResult, + type UnsignedAnalysisView, +} from "./analysisViewResult.js"; +export { + analysisViewRequestSchema, + inspectAnalysisViewInputSchema, + type AnalysisViewRequest, + type InspectAnalysisViewInput, + type AnalysisViewParent, +}; +export { + nativeObservationSummaryViewResultSchemas, + byteEntropySummaryViewResultSchema, + nativeExportsSummaryViewResultSchema, + devicetreeSummaryViewResultSchema, + erlangBeamSummaryViewResultSchema, + erlangBeamTableViewResultSchema, + pythonPycSummaryViewResultSchema, + pythonPycCodeObjectsViewResultSchema, + gdbCommandSummaryViewResultSchema, + objdumpSummaryViewResultSchema, + fridaSessionStatusSummaryViewResultSchema, + goBinarySummaryViewResultSchema, + rustBinarySummaryViewResultSchema, + rustBinaryPageViewResultSchema, + binaryLayoutSummaryViewResultSchema, + javascriptApplicationSummaryViewResultSchema, + artifactInventorySummaryViewResultSchema, + desktopApplicationSummaryViewResultSchema, + wasmArtifactSummaryViewResultSchema, + machoSummaryViewResultSchema, + interfaceBuilderSummaryViewResultSchema, + evmInterfaceSummaryViewResultSchema, + managedArtifactSummaryViewResultSchema, + managedMemberSummaryViewResultSchema, + webNetworkCaptureSummaryViewResultSchema, + browserScenarioSummaryViewResultSchema, + androidPackageSummaryViewResultSchema, + androidJniSummaryViewResultSchema, + androidJniPageViewResultSchema, + androidClassSummaryViewResultSchema, + androidMethodSummaryViewResultSchema, + androidManifestViewResultSchema, + androidComponentsViewResultSchema, + androidComponentSummaryViewResultSchema, + pcapCaptureSummaryViewResultSchema, + dartAotSummaryViewResultSchema, + javaScriptRoutesViewResultSchema, + webBundleSummaryViewResultSchema, + webBundlePageViewResultSchema, + gpuCodeObjectsSummaryViewResultSchema, + gpuCodeObjectsPageViewResultSchema, + apkSignatureSummaryViewResultSchema, + apkSignaturePageViewResultSchema, + modelArtifactSummaryViewResultSchema, + modelArtifactPageViewResultSchema, + il2cppMetadataSummaryViewResultSchema, + nativeInventorySummaryViewResultSchema, + nativeLoadImageSummaryViewResultSchema, + nativeFunctionMatchingSummaryViewResultSchema, + nativeFunctionSummaryViewResultSchema, + sourceToBundleSummaryViewResultSchema, + sourceToBundlePageViewResultSchema, + analysisViewResultSchema, + type AnalysisViewResult, + type UnsignedAnalysisView, +}; /** Attach the content digest that must change when projected bytes change. */ export const sealAnalysisView = ( @@ -1590,46 +265,63 @@ const parentArtifactPath = (parent: AnalysisViewParent): string => { return parent.evidenceId; }; +/** Check a projector result against the result union before sealing it. */ +const sealProjected = ( + projected: Result, +): Result => + projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + /** Named source owners for views that need no extra decoder or accumulator. */ -const retainedViewProjectors = new Map([ - ...NATIVE_OBSERVATION_OPERATIONS.map( - (operation) => [operation, projectNativeObservationView] as const, +type RetainedViewProjector = ( + parent: AnalysisViewParent, + view: AnalysisViewRequest, +) => Result; + +/** Register a projector whose inferred result must be in the result union. */ +const retain = ( + operation: string, + project: RetainedViewProjector, +): readonly [string, RetainedViewProjector] => [operation, project]; + +const retainedViewProjectors = new Map([ + ...NATIVE_OBSERVATION_OPERATIONS.map((operation) => + retain(operation, projectNativeObservationView), ), - ["inspect_erlang_beam", projectErlangBeamView], - ["inspect_devicetree", projectDevicetreeView], - ["inspect_native_exports", projectNativeExportsView], - ["inspect_native_load_image", projectNativeLoadImageView], - ["inspect_python_pyc", projectPythonPycView], - ["frida_session_status", projectFridaSessionStatusView], - ["gdb_console", projectCommandObservationView], - ["inspect_with_objdump", projectCommandObservationView], - ["inspect_go_binary", projectGoBinaryView], - ["inspect_rust_binary", projectRustBinaryView], - ...NATIVE_INVENTORY_OPERATIONS.map( - (operation) => [operation, projectNativeInventoryView] as const, + retain("inspect_erlang_beam", projectErlangBeamView), + retain("inspect_devicetree", projectDevicetreeView), + retain("inspect_native_exports", projectNativeExportsView), + retain("inspect_native_load_image", projectNativeLoadImageView), + retain("inspect_python_pyc", projectPythonPycView), + retain("frida_session_status", projectFridaSessionStatusView), + retain("gdb_console", projectCommandObservationView), + retain("inspect_with_objdump", projectCommandObservationView), + retain("inspect_go_binary", projectGoBinaryView), + retain("inspect_rust_binary", projectRustBinaryView), + ...NATIVE_INVENTORY_OPERATIONS.map((operation) => + retain(operation, projectNativeInventoryView), ), - ["inspect_desktop_application", projectDesktopApplicationView], - ["inspect_wasm_artifact", projectWasmArtifactView], - ["inspect_macho", projectMachoView], - ["decode_interface_builder", projectInterfaceBuilderView], - ["inspect_evm_interface", projectEvmInterfaceView], - ["inspect_managed_artifact", projectManagedArtifactView], - ["inspect_managed_members", projectManagedMemberView], - ["inspect_android_package", projectAndroidPackageView], - ["inspect_android_class", projectAndroidSymbolSummary], - ["inspect_android_method", projectAndroidSymbolSummary], - ["inspect_android_jni_bindings", projectAndroidJniView], - ["capture_browser_scenario", projectBrowserScenarioView], - ["inspect_dart_aot", projectDartAotView], - ["inspect_il2cpp_metadata", projectIl2cppMetadataView], - ["inspect_apk_signature", projectApkSignatureView], - ["inspect_model_artifact", projectModelArtifactView], - ["inspect_gpu_code_objects", projectGpuCodeObjectsView], - ["match_native_functions", projectNativeFunctionMatchingView], - ["compare_source_to_bundle", projectSourceToBundleView], - ["inspect_artifact", projectArtifactInventoryView], - ["inventory_artifact", projectArtifactInventoryView], - ["analyze_web_bundle", projectWebBundleView], + retain("inspect_desktop_application", projectDesktopApplicationView), + retain("inspect_wasm_artifact", projectWasmArtifactView), + retain("inspect_macho", projectMachoView), + retain("decode_interface_builder", projectInterfaceBuilderView), + retain("inspect_evm_interface", projectEvmInterfaceView), + retain("inspect_managed_artifact", projectManagedArtifactView), + retain("inspect_managed_members", projectManagedMemberView), + retain("inspect_android_package", projectAndroidPackageView), + retain("inspect_android_class", projectAndroidSymbolSummary), + retain("inspect_android_method", projectAndroidSymbolSummary), + retain("inspect_android_jni_bindings", projectAndroidJniView), + retain("capture_browser_scenario", projectBrowserScenarioView), + retain("inspect_dart_aot", projectDartAotView), + retain("inspect_il2cpp_metadata", projectIl2cppMetadataView), + retain("inspect_apk_signature", projectApkSignatureView), + retain("inspect_model_artifact", projectModelArtifactView), + retain("inspect_gpu_code_objects", projectGpuCodeObjectsView), + retain("match_native_functions", projectNativeFunctionMatchingView), + retain("compare_source_to_bundle", projectSourceToBundleView), + retain("inspect_artifact", projectArtifactInventoryView), + retain("inventory_artifact", projectArtifactInventoryView), + retain("analyze_web_bundle", projectWebBundleView), ]); /** Budgets retained across source admission and the selected grouping projection. */ @@ -1673,13 +365,6 @@ export const prepareAnalysisViewProjection = ( }; }; -/** Views that group a capture's records into tools. */ -export type GroupingViewKind = - | "api-surface" - | "environment-manifest" - | "environment-state" - | "environment-coverage"; - type GroupingViewRequest = Extract< AnalysisViewRequest, { kind: GroupingViewKind } @@ -1698,7 +383,7 @@ const projectGroupingView = ( parent: AnalysisViewParent, view: GroupingViewRequest, accumulators: AnalysisViewAccumulators, -): Result => { +) => { if (view.kind === "api-surface") return projectWebNetworkApiSurfaceView( parent, @@ -1731,29 +416,29 @@ export const projectAnalysisView = ( ); if (view.kind === "javascript-routes") { const projected = projectJavaScriptRoutesView(parent, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (view.kind === "web-bundle") { const projected = projectWebBundleView(parent, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (isGroupingView(view)) { const grouped = projectGroupingView(parent, view, accumulators); - return grouped.ok ? ok(sealAnalysisView(grouped.value)) : grouped; + return sealProjected(grouped); } if (parent.operation === "inspect_web_network_capture") { const projected = projectWebNetworkCaptureView(parent, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (parent.operation === "inspect_pcap_capture") { const projected = projectPcapCaptureView(parent, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (parent.operation === "inspect_byte_entropy") { const report = parseAnalysisViewResult(byteEntropySchema, parent); if (!report.ok) return report; const projected = projectByteEntropyView(parent, report.value, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (parent.operation === "inspect_binary_layout") { const layout = parseAnalysisViewResult( @@ -1762,7 +447,7 @@ export const projectAnalysisView = ( ); if (!layout.ok) return layout; const projected = projectBinaryLayoutView(parent, layout.value, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (parent.operation === "analyze_javascript_application") { const analysis = parseJavaScriptParent(parent.normalizedResult); @@ -1772,18 +457,18 @@ export const projectAnalysisView = ( analysis.value, view, ); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } if (parent.operation === "analyze_function") { const native = parseAnalysisViewResult(functionDossierSchema, parent); if (!native.ok) return native; const projected = projectNativeFunctionView(parent, native.value, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } const project = retainedViewProjectors.get(parent.operation); if (project !== undefined) { const projected = project(parent, view); - return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; + return sealProjected(projected); } return err(unsupportedParent(parent)); }; diff --git a/src/domain/analysisView/analysisViewCycles.test.ts b/src/domain/analysisView/analysisViewCycles.test.ts new file mode 100644 index 0000000000..791fc192ad --- /dev/null +++ b/src/domain/analysisView/analysisViewCycles.test.ts @@ -0,0 +1,116 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +import { readImportSpecifiers } from "../../../scripts/lib/import-specifiers.mjs"; + +const root = dirname(fileURLToPath(import.meta.url)); + +const relativeSpecifiers = (source: string): readonly string[] => + readImportSpecifiers(source) + .map(({ specifier }) => specifier) + .filter((specifier) => specifier.startsWith(".")); + +/** + * Cycles that already existed outside the analysis-view hub. Membership is + * exact so a new edge cannot hide inside the allow-list. + */ +const allowedCycles = [ + [ + "StateRecordChronology.ts", + "environmentStateListQuery.ts", + "environmentStateRules.ts", + "environmentStateWriteRules.ts", + ], + ["environmentStateGate.ts", "environmentStateListFacts.ts"], + ["environmentStateWriteAddress.ts", "environmentStateWriteEffects.ts"], +]; + +const productionFiles = (directory: string): string[] => { + const files: string[] = []; + for (const name of readdirSync(directory)) { + const path = join(directory, name); + if (statSync(path).isDirectory()) files.push(...productionFiles(path)); + else if (name.endsWith(".ts") && !name.includes(".test.")) files.push(path); + } + return files; +}; + +const importGraph = (): Map> => { + const files = new Set( + productionFiles(root).map((path) => relative(root, path)), + ); + const graph = new Map>(); + for (const file of files) { + const source = readFileSync(join(root, file), "utf8"); + const edges = new Set(); + for (const spec of relativeSpecifiers(source)) { + const resolved = resolve(dirname(join(root, file)), spec).replace( + /\.js$/, + ".ts", + ); + const target = relative(root, resolved); + if (files.has(target)) edges.add(target); + } + graph.set(file, edges); + } + return graph; +}; + +const stronglyConnected = ( + graph: Map>, +): readonly (readonly string[])[] => { + let index = 0; + const stack: string[] = []; + const onStack = new Set(); + const indices = new Map(); + const low = new Map(); + const components: string[][] = []; + const visit = (node: string): void => { + indices.set(node, index); + low.set(node, index); + index += 1; + stack.push(node); + onStack.add(node); + for (const next of graph.get(node) ?? []) { + if (!indices.has(next)) { + visit(next); + low.set(node, Math.min(low.get(node) ?? index, low.get(next) ?? index)); + } else if (onStack.has(next)) { + low.set( + node, + Math.min(low.get(node) ?? index, indices.get(next) ?? index), + ); + } + } + if (low.get(node) === indices.get(node)) { + const component: string[] = []; + let member = ""; + while (member !== node) { + member = stack.pop() ?? node; + onStack.delete(member); + component.push(member); + } + if (component.length > 1) components.push(component.sort()); + } + }; + for (const node of [...graph.keys()].sort()) { + if (!indices.has(node)) visit(node); + } + return components.sort( + (left, right) => + right.length - left.length || left.join().localeCompare(right.join()), + ); +}; + +describe("analysis view import cycles", () => { + it("keeps projectors from importing the request and result hub", () => { + const cycles = stronglyConnected(importGraph()).map((cycle) => + cycle.join("\n"), + ); + expect(cycles).toEqual( + allowedCycles.map((cycle) => [...cycle].sort().join("\n")), + ); + }); +}); diff --git a/src/domain/analysisView/analysisViewParent.ts b/src/domain/analysisView/analysisViewParent.ts index 9e2d9e2311..4f1698ccd1 100644 --- a/src/domain/analysisView/analysisViewParent.ts +++ b/src/domain/analysisView/analysisViewParent.ts @@ -1,10 +1,29 @@ import type { z } from "zod"; import type { AnalysisInputError } from "../analysisErrorCore.js"; import type { AnalysisError } from "../analysisErrorBase.js"; +import type { Evidence } from "../evidence.js"; import { analysisInputErrorFromIssues } from "../inputIssueProjection.js"; import { err, ok, type Result } from "../result.js"; -import type { AnalysisViewParent } from "./analysisView.js"; import { ownedJsonValueSchema } from "../jsonValue.js"; + +/** Parent record already resolved from inline Evidence or a session reference. */ +export interface AnalysisViewParent { + readonly evidenceId: string; + readonly operation: string; + readonly normalizedResult: unknown; + readonly provider?: Evidence["provider"]; + readonly analysisProfile?: Evidence["analysis_profile"]; + readonly authority?: Evidence["authority"]; + readonly environment?: Evidence["environment"]; + /** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */ + readonly sealedResult?: boolean; + /** Subject presentation for this delivery, including explicit absence. */ + readonly subject?: Evidence["subject"]; + readonly limitations: readonly string[]; + readonly artifact?: { readonly path: string; readonly sha256: string } | null; + /** Parent Evidence parameters, when the parent operation records them. */ + readonly parameters?: unknown; +} import { canBorrowParsedJsonSnapshot, freezeOwnedJsonTreeSnapshot, diff --git a/src/domain/analysisView/analysisViewRequest.ts b/src/domain/analysisView/analysisViewRequest.ts new file mode 100644 index 0000000000..697b0ac9c8 --- /dev/null +++ b/src/domain/analysisView/analysisViewRequest.ts @@ -0,0 +1,132 @@ +import { + nativeObservationViewRequestSchema, + nativeContextViewRequestSchema, +} from "./nativeObservationSchemas.js"; +import { devicetreeViewRequestSchema } from "./devicetreeView.js"; +import { apkSignatureViewRequestSchema } from "../apkSignature/apkSignatureQuery.js"; +import { erlangBeamViewRequestSchema } from "./erlangBeamView.js"; +import { pythonPycViewRequestSchema } from "./pythonPycView.js"; +import { nativeExportsViewRequestSchema } from "./nativeExportsView.js"; +import { javaScriptRoutesViewRequestSchema } from "./javascriptRoutesView.js"; +import { webBundleViewRequestSchema } from "./webBundleView.js"; +import { sourceToBundleViewRequestSchema } from "./sourceToBundleView.js"; +import { gpuCodeObjectsViewRequestSchema } from "./gpuCodeObjectsView.js"; +import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js"; +import { rustBinaryViewRequestSchema } from "../rust/rustBinaryQuery.js"; +import { goBinaryViewRequestSchema } from "./goBinaryView.js"; +import { nativeFunctionMatchingViewRequestSchema } from "./nativeFunctionMatchingView.js"; +import { protobufViewRequestSchema } from "./protobufCaptureView.js"; +import { nativeInventoryViewRequestSchema } from "./nativeInventoryView.js"; + +import { + evidenceFileReferenceSchema, + retainedEvidenceReferenceSchema, +} from "../evidenceReference.js"; +import { z } from "zod"; +import { browserScenarioViewRequestSchema } from "./browserScenarioViewSchemas.js"; +import { nativeLoadImageViewRequestSchema } from "./nativeLoadImageView.js"; +import { desktopViewRequestSchema } from "./desktopApplicationView.js"; +import { javascriptModuleReferencesRequestSchema } from "./javascriptModuleReferences.js"; +import { javascriptFactsViewRequestSchema } from "./javascriptFactsView.js"; +import { evidenceSchema } from "../evidence.js"; +import { apiSurfaceViewSchema } from "./webNetworkApiSurfaceView.js"; +import { evmViewRequestSchema } from "./evmInterfaceView.js"; +import { artifactInventoryViewRequestSchema } from "./artifactInventoryView.js"; +import { managedViewRequestSchema } from "./managedArtifactView.js"; +import { managedMembersViewRequestSchema } from "./managedMemberViewSchemas.js"; +import { wasmViewRequestSchema } from "./wasmArtifactView.js"; +import { machoViewRequestSchema } from "./machoView.js"; +import { interfaceBuilderViewRequestSchema } from "./interfaceBuilderView.js"; +import { captureViewRequestSchema } from "./webNetworkCaptureView.js"; +import { environmentManifestViewRequestSchema } from "./environmentManifestView.js"; +import { environmentStateViewRequestSchema } from "./environmentStateView.js"; +import { environmentCoverageViewRequestSchema } from "./environmentCoverageView.js"; +import { pcapViewRequestSchema } from "./pcapCaptureView.js"; +import { qlogViewRequestSchema } from "./qlogCaptureView.js"; +import { androidViewRequestSchema } from "./androidPackageView.js"; +import { androidJniViewRequestSchema } from "../android/androidJniQuery.js"; +import { dartAotViewRequestSchema } from "./dartAotView.js"; +import { il2cppMetadataViewRequestSchema } from "./il2cppMetadataView.js"; +import { + facetViewSchema, + itemViewSchema, + nativeViewSchema, + pageViewSchema, + summaryViewSchema, +} from "./analysisViewShape.js"; +/** Caller-selected projection of already completed analysis Evidence. */ +export const analysisViewRequestSchema = z.discriminatedUnion("kind", [ + nativeObservationViewRequestSchema, + nativeContextViewRequestSchema, + devicetreeViewRequestSchema, + erlangBeamViewRequestSchema, + pythonPycViewRequestSchema, + nativeExportsViewRequestSchema, + javaScriptRoutesViewRequestSchema, + webBundleViewRequestSchema, + nativeFunctionMatchingViewRequestSchema, + sourceToBundleViewRequestSchema, + summaryViewSchema, + goBinaryViewRequestSchema, + rustBinaryViewRequestSchema, + desktopViewRequestSchema, + facetViewSchema, + itemViewSchema, + pageViewSchema, + javascriptModuleReferencesRequestSchema, + javascriptFactsViewRequestSchema, + nativeViewSchema, + nativeInventoryViewRequestSchema, + nativeLoadImageViewRequestSchema, + artifactInventoryViewRequestSchema, + wasmViewRequestSchema, + machoViewRequestSchema, + interfaceBuilderViewRequestSchema, + evmViewRequestSchema, + managedViewRequestSchema, + managedMembersViewRequestSchema, + androidViewRequestSchema, + androidJniViewRequestSchema, + apiSurfaceViewSchema, + environmentManifestViewRequestSchema, + environmentStateViewRequestSchema, + environmentCoverageViewRequestSchema, + captureViewRequestSchema, + browserScenarioViewRequestSchema, + protobufViewRequestSchema, + qlogViewRequestSchema, + pcapViewRequestSchema, + dartAotViewRequestSchema, + il2cppMetadataViewRequestSchema, + apkSignatureViewRequestSchema, + modelArtifactViewRequestSchema, + gpuCodeObjectsViewRequestSchema, +]); + +// The application owns admission and authenticated Evidence parsing. SDK and +// CLI envelope parsing must borrow this unknown value rather than clone it +// before admission. Advertise the owning Evidence contract in both dialects. +const inlineEvidenceInputSchema = z.unknown().nonoptional(); +inlineEvidenceInputSchema._zod.processJSONSchema = + evidenceSchema._zod.processJSONSchema; + +/** Unauthenticated inline input, an exact same-session retained reference, or one local Evidence file. */ +export const analysisViewSourceSchema = z.discriminatedUnion("kind", [ + retainedEvidenceReferenceSchema, + z.strictObject({ + kind: z.literal("inline"), + evidence: inlineEvidenceInputSchema, + }), + evidenceFileReferenceSchema, +]); + +/** Public inspect_analysis_view input. */ +export const inspectAnalysisViewInputSchema = z.strictObject({ + source: analysisViewSourceSchema, + view: analysisViewRequestSchema, +}); + +export type AnalysisViewRequest = z.output; +export type InspectAnalysisViewInput = z.output< + typeof inspectAnalysisViewInputSchema +>; diff --git a/src/domain/analysisView/analysisViewResult.ts b/src/domain/analysisView/analysisViewResult.ts new file mode 100644 index 0000000000..f7ee6a1aa1 --- /dev/null +++ b/src/domain/analysisView/analysisViewResult.ts @@ -0,0 +1,1277 @@ +import { analysisViewCoverageSchema } from "./analysisViewCoverage.js"; +import { + javaScriptRouteInventorySourceSchema, + javaScriptRouteInventorySchema, +} from "../javascript/javascriptRouteInventory.js"; +import { nativeFunctionMatchingContextSchema } from "../native/functionMatching.js"; +import { + nativeObservationViewRequestSchema, + nativeContextViewRequestSchema, + nativeObservationItemsSchemas, + nativeContextItemsSchemas, + nativeSourceContextReferenceSchema, + nativeObservationOperationSchema, +} from "./nativeObservationSchemas.js"; +import { + NATIVE_OBSERVATION_OPERATIONS, + type NativeObservationOperation, +} from "../native/nativeObservation.js"; +import { + devicetreeViewRequestSchema, + devicetreeViewItemsSchema, + devicetreeSummarySchema, + devicetreePageContextSchema, +} from "./devicetreeView.js"; +import { + apkSignatureViewRequestSchema, + apkSignatureViewItemsSchema, +} from "../apkSignature/apkSignatureQuery.js"; +import { + apkSignatureSummarySchema, + apkSignatureSourceCoverageSchema, +} from "./apkSignatureView.js"; +import { + erlangBeamViewRequestSchema, + erlangBeamViewPageSchema, + erlangBeamSummarySchema, +} from "./erlangBeamView.js"; +import { + pythonPycViewRequestSchema, + pythonPycViewItemsSchema, + pythonPycSummarySchema, +} from "./pythonPycView.js"; +import { + nativeExportsViewRequestSchema, + nativeExportsViewItemsSchema, + nativeExportsSummarySchema, +} from "./nativeExportsView.js"; +import { byteEntropySummarySchema } from "./byteEntropyView.js"; +import { + javaScriptRoutesViewRequestSchema, + javaScriptRoutesViewItemsSchema, + javaScriptRoutesViewSummarySchema, +} from "./javascriptRoutesView.js"; +import { + webBundleViewRequestSchema, + webBundleViewItemsSchema, + webBundleSummarySchema, +} from "./webBundleView.js"; +import { webBundleAnalysisSchema } from "../webBundleAnalysis.js"; +import { recordedBrowserOriginSchema } from "../browserObservation.js"; +import { + sourceToBundleViewRequestSchema, + sourceToBundleViewItemsSchema, + sourceToBundleViewContextSchema, +} from "./sourceToBundleView.js"; +import { + gpuCodeObjectsViewRequestSchema, + gpuCodeObjectsSummarySchema, + gpuCodeObjectsItemsSchema, + gpuCodeObjectsPtxPageSchema, +} from "./gpuCodeObjectsView.js"; +import { gpuCodeObjectsObjectSchema } from "../gpu/gpuCodeObjects.js"; +import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js"; +import { + modelArtifactSummarySchema, + modelArtifactViewItemsSchema, + modelArtifactViewContextSchema, +} from "./modelArtifactView.js"; +import { + commandSummaryContext, + gdbCommandSummarySchema, +} from "./commandObservationView.js"; +import { + fridaSessionStatusSummarySchema, + fridaSessionStatusViewContext, +} from "./fridaSessionStatusView.js"; +import { commandObservationSchema } from "../commandObservation.js"; +import { + rustBinaryViewPageSchema, + rustBinaryViewRequestSchema, +} from "../rust/rustBinaryQuery.js"; +import { + goBinarySummarySchema, + goBinaryViewContextSchema, + goBinaryViewPageSchema, + goBinaryViewRequestSchema, +} from "./goBinaryView.js"; +import { + rustBinarySummarySchema, + rustBinaryViewContextSchema, +} from "./rustBinaryView.js"; +import { + nativeFunctionMatchingViewRequestSchema, + nativeFunctionMatchingViewItemsSchema, +} from "./nativeFunctionMatchingView.js"; +import { + protobufViewRequestSchema, + protobufViewItemsSchema, + protobufViewContextSchema, +} from "./protobufCaptureView.js"; +import { + nativeInventoryViewRequestSchema, + nativeInventoryViewRowSchema, + nativeInventorySummarySchema, +} from "./nativeInventoryView.js"; +import { nativeViewSourceContext } from "./nativeViewContext.js"; +import { + NATIVE_INVENTORY_OPERATIONS, + nativeInventoryOperationSchema, +} from "../native/nativeInventory.js"; +import { z } from "zod"; +import { + browserScenarioSummarySchema, + browserScenarioViewContextSchema, + browserScenarioViewItemsSchema, + browserScenarioViewRequestSchema, +} from "./browserScenarioViewSchemas.js"; +import { + nativeLoadImageViewRequestSchema, + nativeLoadImageViewItemsSchema, + nativeLoadImageSummarySchema, +} from "./nativeLoadImageView.js"; +import { + desktopViewRequestSchema, + desktopViewItemsSchema, + desktopSummarySchema, +} from "./desktopApplicationView.js"; +import { desktopCoverageSchema } from "../desktopApplication.js"; +import { + javascriptModuleReferencesRequestSchema, + javascriptModuleReferenceSchema, +} from "./javascriptModuleReferences.js"; +import { javascriptApplicationSourceCoverageSchema } from "./javascriptApplicationCoverage.js"; +import { + javascriptFactsViewRequestSchema, + javascriptFactRowSchema, +} from "./javascriptFactsView.js"; +import { javaScriptSourceSelectionSchema } from "../javascript/javascriptSourceSelection.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; +import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js"; +import { binaryLayoutSchema } from "../native/binaryLayout.js"; +import { bundleResourceRootSchema } from "../apple/bundleResourceRoot.js"; +import { functionFacetAvailabilitySchema } from "../native/functionDossier.js"; +import { + artifactGraphManifestSchema, + artifactInventoryCoverageSchema, +} from "../artifactGraph.js"; +import { + apiSurfaceFields, + apiSurfaceViewSchema, +} from "./webNetworkApiSurfaceView.js"; +import { + evmViewRequestSchema, + evmViewItemSchema, + evmSummarySchema, +} from "./evmInterfaceView.js"; +import { + artifactInventoryViewRequestSchema, + artifactInventoryItemsSchema, + artifactInventorySummarySchema, +} from "./artifactInventoryView.js"; +import { + managedViewRequestSchema, + managedViewItemSchema, + managedSummarySchema, + managedViewContextSchema, +} from "./managedArtifactView.js"; +import { + managedMembersViewRequestSchema, + managedMembersViewPageSchema, + managedMemberSummarySchema, + managedMembersViewContextSchema, +} from "./managedMemberViewSchemas.js"; +import { + wasmViewRequestSchema, + wasmViewItemSchema, + wasmSummarySchema, +} from "./wasmArtifactView.js"; +import { + machoSummarySchema, + machoViewContextSchema, + machoViewRows, + machoViewRequestSchema, +} from "./machoView.js"; +import { + interfaceBuilderSummarySchema, + interfaceBuilderViewContextSchema, + interfaceBuilderViewItemsSchema, + interfaceBuilderViewRequestSchema, +} from "./interfaceBuilderView.js"; +import { + captureViewRequestSchema, + captureViewItemsSchema, + captureSummarySchema, +} from "./webNetworkCaptureView.js"; +import { + environmentManifestFields, + environmentManifestViewRequestSchema, +} from "./environmentManifestView.js"; +import { + environmentStateFields, + environmentStateViewRequestSchema, +} from "./environmentStateView.js"; +import { + environmentCoverageFields, + environmentCoverageViewRequestSchema, +} from "./environmentCoverageView.js"; +import { + pcapViewRequestSchema, + pcapViewPageSchema, + pcapSummarySchema, +} from "./pcapCaptureView.js"; +import { + qlogViewRequestSchema, + qlogViewPageSchema, + qlogViewContextSchema, +} from "./qlogCaptureView.js"; +import { + androidPackageSummarySchema, + androidManifestViewItemSchema, + androidManifestViewRequestSchema, + androidComponentsViewRequestSchema, + androidComponentSummaryViewRequestSchema, + androidComponentViewRowSchema, + androidComponentViewContextSchema, + androidComponentSummarySchema, +} from "./androidPackageView.js"; +import { + androidClassSummarySchema, + androidMethodSummarySchema, +} from "./androidSymbolView.js"; +import { androidJniSummarySchema } from "./androidJniView.js"; +import { + androidJniViewRequestSchema, + androidJniViewItemsSchema, +} from "../android/androidJniQuery.js"; +import { + dartAotSummarySchema, + dartAotViewRequestSchema, + dartAotViewContextSchema, +} from "./dartAotView.js"; +import { + il2cppMetadataSummarySchema, + il2cppMetadataViewRequestSchema, + il2cppMetadataViewContextSchema, + il2cppMetadataViewItemsSchema, +} from "./il2cppMetadataView.js"; +import { + facetViewSchema, + itemViewSchema, + nativeViewSchema, + pageViewSchema, + summaryViewSchema, +} from "./analysisViewShape.js"; +const artifactSchema = z.strictObject({ + path: z + .string() + .describe( + "Selected artifact path, or the canonical resource root when resource-only Evidence has no executable subject.", + ), + sha256: digestSchema.nullable(), +}); + +const layoutSummarySchema = z.strictObject({ + bytes: binaryLayoutSchema.shape.artifact.shape.bytes, + format: binaryLayoutSchema.shape.format, + architecture: binaryLayoutSchema.shape.architecture, + image_type: binaryLayoutSchema.shape.image_type, + entry: z.strictObject({ + reported_value: z.string().min(1), + meaning: z.enum(["linked-virtual-address", "not-applicable", "absent"]), + execution_status: z.literal("unknown"), + }), + counts: z.strictObject({ + sections: z.int().nonnegative(), + segments: z.int().nonnegative(), + symbols: z.int().nonnegative(), + relocations: z.int().nonnegative(), + }), + limitation_count: z.int().nonnegative(), +}); + +const javascriptSummarySchema = z.strictObject({ + input_path: z.string().min(1), + format: z.enum(["asar", "directory"]), + root_artifact_sha256: digestSchema, + source_selection: javaScriptSourceSelectionSchema.exactOptional(), + statistics: jsonObjectSchema, + electron: jsonObjectSchema, + coverage: jsonObjectSchema, + limitation_count: z.int().nonnegative(), +}); + +const viewResultBase = { + parent_evidence_id: prefixedDigestSchema("ev"), + parent_operation: z.enum([ + "inspect_python_pyc", + "inspect_erlang_beam", + "inspect_devicetree", + "inspect_byte_entropy", + "inspect_native_exports", + "inspect_binary_layout", + "inspect_go_binary", + "gdb_console", + "frida_session_status", + "inspect_with_objdump", + "inspect_desktop_application", + "analyze_javascript_application", + "analyze_web_bundle", + "analyze_function", + "inspect_artifact", + "inventory_artifact", + "inspect_wasm_artifact", + "inspect_macho", + "decode_interface_builder", + "inspect_evm_interface", + "inspect_managed_artifact", + "inspect_managed_members", + "inspect_web_network_capture", + "inspect_android_package", + "inspect_android_class", + "inspect_android_method", + "inspect_android_jni_bindings", + "inspect_pcap_capture", + "inspect_dart_aot", + "inspect_il2cpp_metadata", + "inspect_model_artifact", + "inspect_gpu_code_objects", + ...NATIVE_INVENTORY_OPERATIONS, + ...NATIVE_OBSERVATION_OPERATIONS, + "match_native_functions", + ]), + parent_digest: digestSchema, + view_digest: digestSchema, + artifact: artifactSchema, + coverage: analysisViewCoverageSchema, + limitations: z.array(z.string()), + unknowns: z.array(z.string()), +} as const; + +const summaryViewResultBase = { + kind: z.literal("summary"), + view: summaryViewSchema, + ...viewResultBase, +} as const; + +const nativeObservationSummaryFor = < + Operation extends NativeObservationOperation, +>( + operation: Operation, +) => + z.strictObject({ + ...summaryViewResultBase, + ...nativeViewSourceContext, + artifact: artifactSchema.nullable(), + parent_operation: z.literal(operation), + source_context: nativeSourceContextReferenceSchema, + summary: z.strictObject({ observations: z.int().nonnegative() }), + items: nativeObservationItemsSchemas[operation], + }); +export const nativeObservationSummaryViewResultSchemas = { + procedure_address: nativeObservationSummaryFor("procedure_address"), + procedure_callees: nativeObservationSummaryFor("procedure_callees"), + procedure_callers: nativeObservationSummaryFor("procedure_callers"), + resolve_containing_procedure: nativeObservationSummaryFor( + "resolve_containing_procedure", + ), + get_call_graph: nativeObservationSummaryFor("get_call_graph"), +}; +const nativeObservationSummaryViewResultSchema = z.discriminatedUnion( + "parent_operation", + [ + nativeObservationSummaryViewResultSchemas.procedure_address, + nativeObservationSummaryViewResultSchemas.procedure_callees, + nativeObservationSummaryViewResultSchemas.procedure_callers, + nativeObservationSummaryViewResultSchemas.resolve_containing_procedure, + nativeObservationSummaryViewResultSchemas.get_call_graph, + ], +); +const nativeObservationPageFor = ( + operation: Operation, +) => + nativeObservationSummaryFor(operation) + .omit({ kind: true, view: true }) + .extend({ + kind: z.literal("native-observation"), + view: nativeObservationViewRequestSchema, + }); +const nativeObservationPageViewResultSchema = z.discriminatedUnion( + "parent_operation", + [ + nativeObservationPageFor("procedure_address"), + nativeObservationPageFor("procedure_callees"), + nativeObservationPageFor("procedure_callers"), + nativeObservationPageFor("resolve_containing_procedure"), + nativeObservationPageFor("get_call_graph"), + ], +); +const nativeContextFor = ( + facet: Facet, +) => + z.strictObject({ + ...viewResultBase, + ...nativeViewSourceContext, + kind: z.literal("native-context"), + artifact: artifactSchema.nullable(), + parent_operation: nativeObservationOperationSchema, + source_context: nativeSourceContextReferenceSchema, + facet: z.literal(facet), + view: nativeContextViewRequestSchema.extend({ facet: z.literal(facet) }), + items: nativeContextItemsSchemas[facet], + }); +const nativeContextViewResultSchema = z.discriminatedUnion("facet", [ + nativeContextFor("overview"), + nativeContextFor("analysis-profile"), + nativeContextFor("limitations"), + nativeContextFor("result-limitations"), +]); + +export const byteEntropySummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + summary: byteEntropySummarySchema, + parent_operation: z.literal("inspect_byte_entropy"), +}); + +export const nativeExportsSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_native_exports"), + summary: nativeExportsSummarySchema, +}); + +export const devicetreeSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_devicetree"), + summary: devicetreeSummarySchema, +}); +export const erlangBeamSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_erlang_beam"), + summary: erlangBeamSummarySchema, +}); +export const erlangBeamTableViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("beam"), + parent_operation: z.literal("inspect_erlang_beam"), + view: erlangBeamViewRequestSchema, + beam: erlangBeamSummarySchema, + page: erlangBeamViewPageSchema, +}); + +export const pythonPycSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_python_pyc"), + summary: pythonPycSummarySchema, +}); + +export const pythonPycCodeObjectsViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("python-pyc"), + parent_operation: z.literal("inspect_python_pyc"), + view: pythonPycViewRequestSchema, + pyc: pythonPycSummarySchema, + items: pythonPycViewItemsSchema, +}); + +/** Opt-in command delivery keeps output once and retains the complete parent. */ +export const gdbCommandSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...commandSummaryContext, + parent_operation: z.literal("gdb_console"), + artifact: artifactSchema.nullable(), + summary: gdbCommandSummarySchema, +}); + +export const objdumpSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...commandSummaryContext, + parent_operation: z.literal("inspect_with_objdump"), + artifact: artifactSchema.nullable(), + summary: commandObservationSchema, +}); + +export const fridaSessionStatusSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...fridaSessionStatusViewContext, + parent_operation: z.literal("frida_session_status"), + artifact: artifactSchema.nullable(), + summary: fridaSessionStatusSummarySchema, +}); + +/** Opt-in Go delivery retains original bytes and exposes build identity/counts. */ +export const goBinarySummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...goBinaryViewContextSchema.shape, + parent_operation: z.literal("inspect_go_binary"), + summary: goBinarySummarySchema, +}); + +/** Compact source identity and counts, retaining the complete embedded graph. */ +export const rustBinarySummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...rustBinaryViewContextSchema.shape, + parent_operation: z.literal("inspect_rust_binary"), + summary: rustBinarySummarySchema, +}); + +export const rustBinaryPageViewResultSchema = z.strictObject({ + ...viewResultBase, + ...rustBinaryViewContextSchema.shape, + kind: z.literal("rust"), + parent_operation: z.literal("inspect_rust_binary"), + view: rustBinaryViewRequestSchema, + rust: rustBinarySummarySchema, + page: rustBinaryViewPageSchema, +}); + +/** Summary `inspect_binary_layout` detail returns; no other view kind. */ +export const binaryLayoutSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + summary: layoutSummarySchema, + parent_operation: z.literal("inspect_binary_layout"), +}); + +/** Summary `analyze_javascript_application` detail returns; no other view kind. */ +export const javascriptApplicationSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + summary: javascriptSummarySchema, + parent_operation: z.literal("analyze_javascript_application"), +}); + +/** Typed reference page; full item observations remain available separately. */ +export const javascriptModuleReferencesViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("javascript"), + view: javascriptModuleReferencesRequestSchema, + parent_operation: z.literal("analyze_javascript_application"), + rows: z.array(javascriptModuleReferenceSchema), + source_coverage: javascriptApplicationSourceCoverageSchema, +}); + +/** Paged retained observations with exact selected properties and provenance. */ +const javascriptFactsViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("javascript-facts"), + view: javascriptFactsViewRequestSchema, + parent_operation: z.literal("analyze_javascript_application"), + rows: z.array(javascriptFactRowSchema), + source_coverage: javascriptApplicationSourceCoverageSchema, +}); + +/** Exact summary carrier for artifact inspection's optional delivery contract. */ +export const artifactInventorySummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.enum(["inspect_artifact", "inventory_artifact"]), + summary: artifactInventorySummarySchema, +}); + +export const desktopApplicationSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_desktop_application"), + summary: desktopSummarySchema, +}); + +/** Exact summary carrier for WASM inspection's optional delivery contract. */ +export const wasmArtifactSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_wasm_artifact"), + summary: wasmSummarySchema, +}); + +/** Exact slice identity and collection counts for Mach-O's selected delivery. */ +export const machoSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_macho"), + summary: machoSummarySchema, +}); + +/** Decoded documents and graph counts for Interface Builder's selected delivery. */ +export const interfaceBuilderSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("decode_interface_builder"), + resource_root: bundleResourceRootSchema.optional(), + summary: interfaceBuilderSummarySchema, +}); + +/** Exact inferred-interface summary for EVM's caller-selected delivery. */ +export const evmInterfaceSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_evm_interface"), + summary: evmSummarySchema, +}); + +/** Exact metadata overview for the managed artifact's selected delivery. */ +export const managedArtifactSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_managed_artifact"), + summary: managedSummarySchema, +}); + +/** Exact member selection, coverage and counts for the selected delivery. */ +export const managedMemberSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_managed_members"), + summary: managedMemberSummarySchema, +}); + +/** Exact historical capture summary, with no body or member-inventory payload. */ +export const webNetworkCaptureSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_web_network_capture"), + summary: captureSummarySchema, +}); + +/** Compact outcomes and the focus screen from a retained browser scenario. */ +export const browserScenarioSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + artifact: z.null(), + parent_operation: z.literal("capture_browser_scenario"), + summary: browserScenarioSummarySchema, +}); + +/** Exact package declarations with retained decoded manifest text. */ +export const androidPackageSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_android_package"), + summary: androidPackageSummarySchema, +}); +export const androidJniSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_android_jni_bindings"), + summary: androidJniSummarySchema, +}); +export const androidJniPageViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("android-jni"), + parent_operation: z.literal("inspect_android_jni_bindings"), + view: androidJniViewRequestSchema, + items: androidJniViewItemsSchema, + source: androidJniSummarySchema.pick({ + native_inventory: true, + library_inventory: true, + selection: true, + }), +}); + +/** Exact class facts with complete raw protocol Evidence retained separately. */ +export const androidClassSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_android_class"), + summary: androidClassSummarySchema, +}); + +/** Exact selected method body, source coverage and optional mapping facts. */ +export const androidMethodSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_android_method"), + summary: androidMethodSummarySchema, +}); + +/** Legacy manifest views keep their original shape; new facets are explicit. */ +export const androidManifestViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("android"), + facet: z.literal("manifest").optional(), + view: androidManifestViewRequestSchema, + item: androidManifestViewItemSchema, + parent_operation: z.literal("inspect_android_package"), +}); +export const androidComponentsViewResultSchema = z.strictObject({ + ...viewResultBase, + ...androidComponentViewContextSchema.shape, + kind: z.literal("android"), + facet: z.literal("components"), + view: androidComponentsViewRequestSchema, + items: z.array(androidComponentViewRowSchema), + byte_limited: z.boolean(), + parent_operation: z.literal("inspect_android_package"), +}); +export const androidComponentSummaryViewResultSchema = z.strictObject({ + ...viewResultBase, + ...androidComponentViewContextSchema.shape, + kind: z.literal("android"), + facet: z.literal("component-summary"), + view: androidComponentSummaryViewRequestSchema, + summary: androidComponentSummarySchema, + parent_operation: z.literal("inspect_android_package"), +}); +const androidPackageViewResultSchema = z.discriminatedUnion("facet", [ + androidManifestViewResultSchema, + androidComponentsViewResultSchema, + androidComponentSummaryViewResultSchema, +]); + +export const pcapCaptureSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_pcap_capture"), + summary: pcapSummarySchema, +}); + +/** Exact snapshot and pool-coverage carrier for selected AOT delivery. */ +export const dartAotSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_dart_aot"), + summary: dartAotSummarySchema, +}); + +export const javaScriptRoutesViewResultSchema = z.strictObject({ + ...viewResultBase, + artifact: artifactSchema.nullable(), + kind: z.literal("javascript-routes"), + parent_operation: z.enum([ + "analyze_javascript_application", + "analyze_web_bundle", + ]), + view: javaScriptRoutesViewRequestSchema, + inventory_id: javaScriptRouteInventorySchema.shape.inventory_id, + source_manifest_sha256: digestSchema, + source_coverage: javaScriptRouteInventorySchema.shape.coverage, + inventory_capacity: javaScriptRouteInventorySchema.shape.capacity, + capture_url: z.string().nullable(), + items: javaScriptRoutesViewItemsSchema, + sources: z.array(javaScriptRouteInventorySourceSchema), + summary: javaScriptRoutesViewSummarySchema.nullable(), + byte_limited: z.boolean(), +}); + +/** Compact web bundle findings retain endpoint observations and inference candidates. */ +export const webBundleSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + artifact: artifactSchema.nullable(), + parent_operation: z.literal("analyze_web_bundle"), + target_url: z.string(), + target_origin: recordedBrowserOriginSchema.nullable().optional(), + source_unknowns: webBundleAnalysisSchema.shape.unknowns, + summary: webBundleSummarySchema, +}); + +export const webBundlePageViewResultSchema = z.strictObject({ + ...viewResultBase, + artifact: artifactSchema.nullable(), + kind: z.literal("web-bundle"), + parent_operation: z.literal("analyze_web_bundle"), + target_url: z.string(), + target_origin: recordedBrowserOriginSchema.nullable().optional(), + source_unknowns: webBundleAnalysisSchema.shape.unknowns, + view: webBundleViewRequestSchema, + completeness: webBundleAnalysisSchema.shape.completeness, + items: webBundleViewItemsSchema, + summary: webBundleSummarySchema.nullable(), + byte_limited: z.boolean(), +}); + +export const gpuCodeObjectsSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_gpu_code_objects"), + summary: gpuCodeObjectsSummarySchema, +}); +export const gpuCodeObjectsPageViewResultSchema = z.strictObject({ + kind: z.literal("gpu-code-objects"), + view: gpuCodeObjectsViewRequestSchema, + items: gpuCodeObjectsItemsSchema, + ptx: gpuCodeObjectsPtxPageSchema.nullable(), + source_coverage: gpuCodeObjectsObjectSchema.shape.coverage, + ...viewResultBase, + parent_operation: z.literal("inspect_gpu_code_objects"), +}); + +export const apkSignatureSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_apk_signature"), + summary: apkSignatureSummarySchema, +}); +export const apkSignaturePageViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("apk-signature"), + parent_operation: z.literal("inspect_apk_signature"), + view: apkSignatureViewRequestSchema, + items: apkSignatureViewItemsSchema, + source_coverage: apkSignatureSourceCoverageSchema, +}); + +export const modelArtifactSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_model_artifact"), + summary: modelArtifactSummarySchema, +}); + +export const modelArtifactPageViewResultSchema = z.strictObject({ + kind: z.literal("model-artifact"), + view: modelArtifactViewRequestSchema, + items: modelArtifactViewItemsSchema, + ...viewResultBase, + ...modelArtifactViewContextSchema.shape, + parent_operation: z.literal("inspect_model_artifact"), +}); + +/** Metadata identity and row counts for selected IL2CPP delivery. */ +export const il2cppMetadataSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_il2cpp_metadata"), + summary: il2cppMetadataSummarySchema, +}); + +/** Exact inventory summary with the original observation's provider and query. */ +export const nativeInventorySummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...nativeViewSourceContext, + parent_operation: nativeInventoryOperationSchema, + artifact: artifactSchema.nullable(), + summary: nativeInventorySummarySchema, +}); + +/** Compact provider load observations retain the independent verification state. */ +export const nativeLoadImageSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + ...nativeViewSourceContext, + artifact: artifactSchema.nullable(), + parent_operation: z.literal("inspect_native_load_image"), + summary: nativeLoadImageSummarySchema, +}); + +/** Compact native matching discovery retains the complete matching report. */ +export const nativeFunctionMatchingSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + artifact: z.null(), + parent_operation: z.literal("match_native_functions"), + summary: nativeFunctionMatchingContextSchema, +}); + +/** Rows observed for one facet, or null when the provider declared that facet unavailable. */ +const declaredFacetCountSchema = z + .number() + .int() + .nonnegative() + .nullable() + .describe( + "Observed rows; null when facet_availability declares the facet unavailable, so no rows is not evidence of none.", + ); + +/** Rows of an optional dossier record, or null when the dossier omitted it. */ +const recordedFacetCountSchema = z + .number() + .int() + .nonnegative() + .nullable() + .describe( + "Observed rows; null when the dossier did not record the facet (the native facet view reports it not recorded), so no rows is not evidence of none.", + ); + +/** Rows of one value-flow facet, or null when value flow is unavailable. */ +const valueFlowFacetCountSchema = z + .number() + .int() + .nonnegative() + .nullable() + .describe( + "Observed rows; null when value_flow.available is false because native_value_flow was not recorded or the provider declared it unavailable (unknowns names which), so no rows is not evidence of none.", + ); + +/** Compact analyze_function dossier; the complete dossier stays retained for facet paging. */ +export const nativeFunctionSummarySchema = z.strictObject({ + procedure: z.strictObject({ + name: z.string(), + address: z.string(), + }), + pseudocode: z.strictObject({ + available: z + .boolean() + .describe( + "Provider facet_availability for pseudocode when declared; otherwise whether decompiled text is present. The flag is authoritative over raw length.", + ), + code_units: z + .number() + .int() + .nonnegative() + .describe( + "Pseudocode length in UTF-16 code units, the unit inspect_analysis_view pages pseudocode by.", + ), + }), + value_flow: z.strictObject({ + available: z.boolean(), + truncated: z + .boolean() + .describe( + "True only when value-flow is available and the provider truncated it; false when value-flow is unavailable (see available).", + ), + }), + facet_availability: functionFacetAvailabilitySchema + .nullable() + .describe( + "The provider's declared availability per facet, or null when it declared none. Unavailable facets name their reason here and in unknowns.", + ), + counts: z.strictObject({ + assembly: declaredFacetCountSchema, + basic_blocks: declaredFacetCountSchema, + comments: declaredFacetCountSchema, + callers: declaredFacetCountSchema, + callees: declaredFacetCountSchema, + incoming_references: declaredFacetCountSchema, + outgoing_references: declaredFacetCountSchema, + unresolved_calls: recordedFacetCountSchema, + referenced_strings: declaredFacetCountSchema, + referenced_names: declaredFacetCountSchema, + swift_string_literals: recordedFacetCountSchema, + swift_string_literal_omissions: recordedFacetCountSchema, + value_flow_operations: valueFlowFacetCountSchema, + value_flow_def_use: valueFlowFacetCountSchema, + value_flow_effects: valueFlowFacetCountSchema, + value_flow_parameters: valueFlowFacetCountSchema, + value_flow_parameter_uses: valueFlowFacetCountSchema, + }), + limitation_count: z.number().int().nonnegative(), +}); + +/** Compact analyze_function summary that retains the complete native dossier Evidence. */ +export const nativeFunctionSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("analyze_function"), + procedure_address: z.string().nullable(), + summary: nativeFunctionSummarySchema, +}); + +export const sourceToBundleSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + artifact: z.null(), + parent_operation: z.literal("compare_source_to_bundle"), + summary: sourceToBundleViewContextSchema, +}); +export const sourceToBundlePageViewResultSchema = z.strictObject({ + ...viewResultBase, + ...sourceToBundleViewContextSchema.shape, + kind: z.literal("source-to-bundle"), + artifact: z.null(), + parent_operation: z.literal("compare_source_to_bundle"), + view: sourceToBundleViewRequestSchema, + items: sourceToBundleViewItemsSchema, +}); + +/** Bind the existing nested facet selection to its producer-owned rows. */ +const machoPage = (facet: Facet) => + z.strictObject({ + ...viewResultBase, + ...machoViewContextSchema.shape, + kind: z.literal("macho"), + parent_operation: z.literal("inspect_macho"), + view: machoViewRequestSchema.extend({ facet: z.literal(facet) }), + item: machoViewRows[facet], + }); + +// The input tag preserves kind dispatch; the output projection advertises the +// correlated pages without adding a discriminator to their public wire shape. +const machoPageViewResultSchema = z + .object({ kind: z.literal("macho") }) + .passthrough() + .pipe( + z.union([ + machoPage("entrypoints"), + machoPage("architectures"), + machoPage("build_metadata"), + machoPage("load_commands"), + machoPage("dependencies"), + machoPage("imports"), + machoPage("exports"), + machoPage("segments"), + machoPage("encryption"), + machoPage("provenance"), + ]), + ); + +/** Projected facts for one selected view of retained analysis Evidence. */ +export const analysisViewResultSchema = z.discriminatedUnion("kind", [ + nativeObservationPageViewResultSchema, + nativeContextViewResultSchema, + javaScriptRoutesViewResultSchema, + webBundlePageViewResultSchema, + sourceToBundlePageViewResultSchema, + z.strictObject({ + ...viewResultBase, + ...nativeFunctionMatchingContextSchema.shape, + kind: z.literal("function-matches"), + artifact: z.null(), + view: nativeFunctionMatchingViewRequestSchema, + items: nativeFunctionMatchingViewItemsSchema, + parent_operation: z.literal("match_native_functions"), + }), + z.strictObject({ + ...viewResultBase, + ...apiSurfaceFields, + kind: z.literal("api-surface"), + parent_operation: z.literal("inspect_web_network_capture"), + view: apiSurfaceViewSchema, + artifact: z.strictObject({ + path: z.string(), + sha256: digestSchema.nullable(), + }), + }), + z.strictObject({ + ...viewResultBase, + ...environmentManifestFields, + kind: z.literal("environment-manifest"), + parent_operation: z.enum([ + "inspect_web_network_capture", + "capture_browser_scenario", + ]), + view: environmentManifestViewRequestSchema, + }), + z.strictObject({ + ...viewResultBase, + ...environmentStateFields, + kind: z.literal("environment-state"), + parent_operation: z.enum([ + "inspect_web_network_capture", + "capture_browser_scenario", + ]), + view: environmentStateViewRequestSchema, + }), + z.strictObject({ + ...viewResultBase, + ...environmentCoverageFields, + kind: z.literal("environment-coverage"), + parent_operation: z.enum([ + "inspect_web_network_capture", + "capture_browser_scenario", + ]), + view: environmentCoverageViewRequestSchema, + }), + z.discriminatedUnion("parent_operation", [ + nativeObservationSummaryViewResultSchema, + erlangBeamSummaryViewResultSchema, + devicetreeSummaryViewResultSchema, + pythonPycSummaryViewResultSchema, + nativeExportsSummaryViewResultSchema, + byteEntropySummaryViewResultSchema, + gdbCommandSummaryViewResultSchema, + objdumpSummaryViewResultSchema, + fridaSessionStatusSummaryViewResultSchema, + goBinarySummaryViewResultSchema, + rustBinarySummaryViewResultSchema, + binaryLayoutSummaryViewResultSchema, + desktopApplicationSummaryViewResultSchema, + javascriptApplicationSummaryViewResultSchema, + artifactInventorySummaryViewResultSchema, + wasmArtifactSummaryViewResultSchema, + machoSummaryViewResultSchema, + interfaceBuilderSummaryViewResultSchema, + evmInterfaceSummaryViewResultSchema, + managedArtifactSummaryViewResultSchema, + managedMemberSummaryViewResultSchema, + webNetworkCaptureSummaryViewResultSchema, + browserScenarioSummaryViewResultSchema, + androidPackageSummaryViewResultSchema, + androidJniSummaryViewResultSchema, + androidClassSummaryViewResultSchema, + androidMethodSummaryViewResultSchema, + pcapCaptureSummaryViewResultSchema, + dartAotSummaryViewResultSchema, + il2cppMetadataSummaryViewResultSchema, + apkSignatureSummaryViewResultSchema, + modelArtifactSummaryViewResultSchema, + gpuCodeObjectsSummaryViewResultSchema, + nativeInventorySummaryViewResultSchema, + nativeLoadImageSummaryViewResultSchema, + nativeFunctionMatchingSummaryViewResultSchema, + nativeFunctionSummaryViewResultSchema, + sourceToBundleSummaryViewResultSchema, + webBundleSummaryViewResultSchema, + ]), + z.strictObject({ + ...viewResultBase, + kind: z.literal("devicetree"), + parent_operation: z.literal("inspect_devicetree"), + view: devicetreeViewRequestSchema, + devicetree: devicetreePageContextSchema, + items: devicetreeViewItemsSchema, + }), + erlangBeamTableViewResultSchema, + pythonPycCodeObjectsViewResultSchema, + z.strictObject({ + ...viewResultBase, + kind: z.literal("native-exports"), + parent_operation: z.literal("inspect_native_exports"), + view: nativeExportsViewRequestSchema, + exports: nativeExportsSummarySchema, + items: nativeExportsViewItemsSchema, + }), + z.strictObject({ + ...viewResultBase, + ...nativeViewSourceContext, + kind: z.literal("native-inventory"), + parent_operation: nativeInventoryOperationSchema, + artifact: artifactSchema.nullable(), + view: nativeInventoryViewRequestSchema, + rows: z.array(nativeInventoryViewRowSchema), + }), + z.strictObject({ + ...viewResultBase, + kind: z.literal("native-load-image"), + ...nativeViewSourceContext, + verification_status: z.enum(["unsupported", "verified", "mismatch"]), + parent_operation: z.literal("inspect_native_load_image"), + artifact: artifactSchema.nullable(), + view: nativeLoadImageViewRequestSchema, + items: nativeLoadImageViewItemsSchema, + }), + z.strictObject({ + kind: z.literal("protobuf"), + view: protobufViewRequestSchema, + items: protobufViewItemsSchema, + ...viewResultBase, + ...protobufViewContextSchema.shape, + parent_operation: z.literal("inspect_web_network_capture"), + }), + z.strictObject({ + kind: z.literal("capture"), + view: captureViewRequestSchema, + items: captureViewItemsSchema, + ...viewResultBase, + parent_operation: z.literal("inspect_web_network_capture"), + }), + z.strictObject({ + kind: z.literal("browser-scenario"), + view: browserScenarioViewRequestSchema, + items: browserScenarioViewItemsSchema, + ...viewResultBase, + ...browserScenarioViewContextSchema.shape, + artifact: z.null(), + parent_operation: z.literal("capture_browser_scenario"), + }), + z.strictObject({ + kind: z.literal("qlog"), + view: qlogViewRequestSchema, + page: qlogViewPageSchema, + context: qlogViewContextSchema, + ...viewResultBase, + parent_operation: z.literal("inspect_web_network_capture"), + }), + z.strictObject({ + kind: z.literal("pcap"), + view: pcapViewRequestSchema, + page: pcapViewPageSchema, + source_coverage: z.enum(["complete", "partial"]), + ...viewResultBase, + parent_operation: z.literal("inspect_pcap_capture"), + }), + z.strictObject({ + kind: z.literal("facet"), + view: facetViewSchema, + facet: jsonValueSchema, + ...viewResultBase, + }), + z.strictObject({ + ...viewResultBase, + ...goBinaryViewContextSchema.shape, + kind: z.literal("go"), + parent_operation: z.literal("inspect_go_binary"), + view: goBinaryViewRequestSchema, + page: goBinaryViewPageSchema, + }), + rustBinaryPageViewResultSchema, + z.strictObject({ + kind: z.literal("item"), + view: itemViewSchema, + item: jsonValueSchema, + ...viewResultBase, + }), + z.strictObject({ + kind: z.literal("page"), + view: pageViewSchema, + items: z.array(jsonValueSchema), + ...viewResultBase, + }), + javascriptModuleReferencesViewResultSchema, + javascriptFactsViewResultSchema, + z.strictObject({ + kind: z.literal("native"), + view: nativeViewSchema, + procedure_address: z.string().nullable(), + item: jsonValueSchema, + ...viewResultBase, + }), + z.strictObject({ + kind: z.literal("wasm"), + view: wasmViewRequestSchema, + item: wasmViewItemSchema, + ...viewResultBase, + }), + z.strictObject({ + kind: z.literal("desktop-application"), + view: desktopViewRequestSchema, + items: desktopViewItemsSchema, + ...viewResultBase, + source_coverage: desktopCoverageSchema, + parent_operation: z.literal("inspect_desktop_application"), + }), + machoPageViewResultSchema, + z.strictObject({ + kind: z.literal("interface-builder"), + view: interfaceBuilderViewRequestSchema, + items: interfaceBuilderViewItemsSchema, + ...viewResultBase, + resource_root: bundleResourceRootSchema.optional(), + ...interfaceBuilderViewContextSchema.shape, + parent_operation: z.literal("decode_interface_builder"), + }), + z.strictObject({ + kind: z.literal("evm"), + view: evmViewRequestSchema, + item: evmViewItemSchema, + ...viewResultBase, + parent_operation: z.literal("inspect_evm_interface"), + }), + z.strictObject({ + kind: z.literal("managed"), + view: managedViewRequestSchema, + item: managedViewItemSchema, + ...viewResultBase, + ...managedViewContextSchema.shape, + parent_operation: z.literal("inspect_managed_artifact"), + }), + z.strictObject({ + kind: z.literal("managed-members"), + view: managedMembersViewRequestSchema, + page: managedMembersViewPageSchema, + ...viewResultBase, + ...managedMembersViewContextSchema.shape, + parent_operation: z.literal("inspect_managed_members"), + }), + androidPackageViewResultSchema, + androidJniPageViewResultSchema, + z.strictObject({ + kind: z.literal("dart-aot"), + view: dartAotViewRequestSchema, + items: z.array(z.string()), + ...viewResultBase, + ...dartAotViewContextSchema.shape, + parent_operation: z.literal("inspect_dart_aot"), + }), + apkSignaturePageViewResultSchema, + modelArtifactPageViewResultSchema, + gpuCodeObjectsPageViewResultSchema, + z.strictObject({ + kind: z.literal("il2cpp-metadata"), + view: il2cppMetadataViewRequestSchema, + items: il2cppMetadataViewItemsSchema, + ...viewResultBase, + ...il2cppMetadataViewContextSchema.shape, + parent_operation: z.literal("inspect_il2cpp_metadata"), + }), + z.strictObject({ + kind: z.literal("inventory"), + view: artifactInventoryViewRequestSchema, + source_coverage: artifactInventoryCoverageSchema.exactOptional(), + manifest: artifactGraphManifestSchema, + inventory_evidence_ids: z.array(prefixedDigestSchema("ev")).min(1), + source_complete: z.boolean(), + items: artifactInventoryItemsSchema, + ...viewResultBase, + }), +]); + +export type AnalysisViewResult = z.output; + +/** View payload before the content digest is attached. */ +type WithoutViewDigest = T extends { readonly view_digest: string } + ? Omit + : never; +export type UnsignedAnalysisView = WithoutViewDigest; diff --git a/src/domain/analysisView/analysisViewShape.ts b/src/domain/analysisView/analysisViewShape.ts new file mode 100644 index 0000000000..8a9bf062b8 --- /dev/null +++ b/src/domain/analysisView/analysisViewShape.ts @@ -0,0 +1,127 @@ +import { z } from "zod"; +import { prefixedDigestSchema } from "../digests.js"; + +export const summaryViewSchema = z.strictObject({ kind: z.literal("summary") }); +export const facetViewSchema = z.strictObject({ + kind: z.literal("facet"), + facet: z.enum(["mitigations", "linkage"]), +}); +const indexSelectorSchema = z.strictObject({ + index: z.int().nonnegative(), +}); +const nameSelectorSchema = z.strictObject({ + name: z + .string() + .describe( + "Exact UTF-8 section or symbol name, matched against retained source bytes. Use index for non-UTF-8 names or unresolved name bytes.", + ), +}); +const pathSelectorSchema = z.strictObject({ + path: z + .string() + .describe( + "Exact observed module path or original source-map reference from a module page; an empty reference is valid.", + ), +}); +const nodeIdSelectorSchema = z.strictObject({ + node_id: prefixedDigestSchema("jag_node"), +}); +const moduleItemViewSchema = z.strictObject({ + kind: z.literal("item"), + collection: z.literal("modules"), + selector: z.union([pathSelectorSchema, nodeIdSelectorSchema]), +}); +const sectionOrSymbolItemViewSchema = z.strictObject({ + kind: z.literal("item"), + collection: z.enum(["sections", "symbols"]), + selector: z.union([indexSelectorSchema, nameSelectorSchema]), +}); +/** + * The collection-specific selector alternatives are expressed in the owning + * contract so the advertised JSON Schema preserves them: modules accept + * path/node_id, sections and symbols accept index/name. Refinement-only + * constraints do not survive JSON Schema conversion, so the advertised + * schema previously accepted combinations that runtime input validation + * rejects. + */ +export const itemViewSchema = z.discriminatedUnion("collection", [ + moduleItemViewSchema, + sectionOrSymbolItemViewSchema, +]); +export const pageViewSchema = z.strictObject({ + kind: z.literal("page"), + collection: z.enum(["sections", "symbols", "modules"]), + offset: z.int().nonnegative(), + limit: z.int().positive(), +}); + +/** Bounded native function facets of already authenticated analyze_function Evidence. */ +export const nativeViewSchema = z.strictObject({ + kind: z.literal("native"), + facet: z.enum([ + "procedure", + "pseudocode", + "assembly", + "basic_blocks", + "comments", + "callers", + "callees", + "incoming_references", + "outgoing_references", + "unresolved_calls", + "referenced_strings", + "referenced_names", + "swift_string_literals", + "swift_string_literal_omissions", + "native_api", + "value_flow_summary", + "value_flow_operations", + "value_flow_def_use", + "value_flow_effects", + "value_flow_parameters", + "value_flow_parameter_uses", + ]), + offset: z.int().nonnegative().default(0), + limit: z.int().positive().default(64), +}); + +export type SummaryAnalysisView = z.output; +export type FacetAnalysisView = z.output; +export type ItemAnalysisView = z.output; +export type PageAnalysisView = z.output; +export type NativeFacetAnalysisView = z.output; + +/** Views that group a capture's records into tools. */ +export type GroupingViewKind = + | "api-surface" + | "environment-manifest" + | "environment-state" + | "environment-coverage"; + +/** + * View request already admitted by analysisViewRequestSchema. + * Projectors use this supertype so they do not import the request union. + */ +export type AdmittedView = { readonly kind: string }; + +/** + * Restore the owning schema output after admission. + * analysisViewRequestSchema already checked the fields for these kinds. + */ +export const ownedView = ( + view: AdmittedView, + ...kinds: readonly View["kind"][] +): View | undefined => + (kinds as readonly string[]).includes(view.kind) ? (view as View) : undefined; + +/** Narrow an admitted view after the projector has rejected other kinds. */ +export function assertOwnedView( + view: AdmittedView, + ...kinds: readonly View["kind"][] +): asserts view is View { + if (!(kinds as readonly string[]).includes(view.kind)) { + throw new Error( + `Analysis view kind ${JSON.stringify(view.kind)} is not owned by this projector.`, + ); + } +} diff --git a/src/domain/analysisView/androidJniView.ts b/src/domain/analysisView/androidJniView.ts index c91407956b..6c978d81cb 100644 --- a/src/domain/analysisView/androidJniView.ts +++ b/src/domain/analysisView/androidJniView.ts @@ -6,17 +6,20 @@ import { androidJniDeclarationSchema, androidJniLibrarySchema, } from "../android/androidJniBindings.js"; -import { androidJniViewItemsSchema } from "../android/androidJniQuery.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; +import { + androidJniViewItemsSchema, + androidJniViewRequestSchema, +} from "../android/androidJniQuery.js"; import { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; import type { JsonValue } from "../jsonValue.js"; import { jsonValueByteLength } from "../jsonValueByteLength.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY, analysisViewInputError, @@ -63,8 +66,8 @@ export const androidJniSummarySchema = reportSchema /** Source facts are already parsed; views never reopen the APK or rerun either reader. */ export const projectAndroidJniView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if ( parent.operation !== "inspect_android_jni_bindings" || (view.kind !== "summary" && view.kind !== "android-jni") @@ -79,6 +82,9 @@ export const projectAndroidJniView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "android-jni"); const parsed = readReport(parent); if (!parsed.ok) return parsed; const report = parsed.value; @@ -98,7 +104,19 @@ export const projectAndroidJniView = ( "unknowns", ), }; - let result: UnsignedAnalysisView; + const overBudget = (value: object): boolean => + jsonValueByteLength(value as unknown as JsonValue, 1024 * 1024 - 128) > + 1024 * 1024 - 128; + const budgetError = () => + err( + new AnalysisResourceConstraintError( + "inspect_analysis_view", + "transport", + "JNI view exceeds its 1 MiB serialized allowance.", + { parent_evidence_id: parent.evidenceId, limit_bytes: 1024 * 1024 }, + ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY, + ), + ); if (view.kind === "summary") { const bindings = report.bindings.slice(0, 64); const candidateIds = new Set( @@ -124,9 +142,9 @@ export const projectAndroidJniView = ( unknown: 0, }; for (const binding of report.bindings) counts[binding.status]++; - result = { + const summary = { ...shared, - kind: "summary", + kind: "summary" as const, view, coverage: pageViewCoverage(0, bindings.length, report.bindings.length), summary: { @@ -152,6 +170,8 @@ export const projectAndroidJniView = ( ), }, }; + if (overBudget(summary)) return budgetError(); + return ok(summary); } else { const indices = view.indices === undefined ? undefined : new Set(view.indices); @@ -240,9 +260,9 @@ export const projectAndroidJniView = ( library_inventory: report.library_inventory, selection: report.selection, }; - result = { + const page = { ...shared, - kind: "android-jni", + kind: "android-jni" as const, view, source, items: androidJniViewItemsSchema.parse(selected.rows), @@ -252,19 +272,7 @@ export const projectAndroidJniView = ( selected.total, ), }; + if (overBudget(page)) return budgetError(); + return ok(page); } - if ( - jsonValueByteLength(result as unknown as JsonValue, 1024 * 1024 - 128) > - 1024 * 1024 - 128 - ) - return err( - new AnalysisResourceConstraintError( - "inspect_analysis_view", - "transport", - "JNI view exceeds its 1 MiB serialized allowance.", - { parent_evidence_id: parent.evidenceId, limit_bytes: 1024 * 1024 }, - ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY, - ), - ); - return ok(result); }; diff --git a/src/domain/analysisView/androidPackageView.ts b/src/domain/analysisView/androidPackageView.ts index e4a029ed4a..ec6809186d 100644 --- a/src/domain/analysisView/androidPackageView.ts +++ b/src/domain/analysisView/androidPackageView.ts @@ -11,11 +11,13 @@ import { jsonValueByteLength } from "../jsonValueByteLength.js"; import type { JsonValue } from "../jsonValue.js"; import { err, ok, type Result } from "../result.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { androidViewRequestSchema } from "../android/androidPackageQuery.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -106,8 +108,8 @@ const choose = ( /** Project authenticated retained facts without reopening the APK or engine. */ export const projectAndroidPackageView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "android") return err( analysisViewInputError([ @@ -119,6 +121,9 @@ export const projectAndroidPackageView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "android"); if (parent.operation !== "inspect_android_package") return err( analysisViewInputError([ @@ -166,7 +171,7 @@ export const projectAndroidPackageView = ( if (view.kind === "summary") { const { text, ...manifest } = report.manifest; return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -174,7 +179,7 @@ export const projectAndroidPackageView = ( manifest: { ...manifest, text_code_units: text.length, - text_included: false, + text_included: false as const, }, }, coverage: completeWithinViewCoverage(1, 1), @@ -189,7 +194,7 @@ export const projectAndroidPackageView = ( ); if (!page.ok) return page; return ok({ - kind: "android", + kind: "android" as const, view, ...shared, item: { @@ -295,10 +300,10 @@ export const projectAndroidPackageView = ( ]++; summary.declaration_coverage[row.declaration_coverage]++; } - const result: UnsignedAnalysisView = { + const result = { ...context, - kind: "android", - facet: "component-summary", + kind: "android" as const, + facet: "component-summary" as const, view, summary, coverage: completeWithinViewCoverage(rows.length, rows.length), @@ -365,10 +370,10 @@ export const projectAndroidPackageView = ( bytes += added; items.push(item); } - const result: UnsignedAnalysisView = { + const result = { ...context, - kind: "android", - facet: "components", + kind: "android" as const, + facet: "components" as const, view, items, byte_limited, diff --git a/src/domain/analysisView/androidSymbolView.test.ts b/src/domain/analysisView/androidSymbolView.test.ts index 32397d1a06..72c38013f0 100644 --- a/src/domain/analysisView/androidSymbolView.test.ts +++ b/src/domain/analysisView/androidSymbolView.test.ts @@ -1,4 +1,6 @@ import { expect, it, vi } from "vitest"; +import { z } from "zod"; +import { androidComponentsViewRequestSchema } from "../android/androidPackageQuery.js"; import { createEvidence } from "../evidence.js"; import { immutableJsonSnapshotMember } from "../immutableJson.js"; import { @@ -7,11 +9,18 @@ import { projectAnalysisView, type AnalysisViewParent, } from "./analysisView.js"; +import type { AdmittedView } from "./analysisViewShape.js"; import { androidClassSummarySchema, - projectAndroidSymbolSummary, + projectAndroidSymbolSummary as projectAdmittedAndroidSymbolSummary, } from "./androidSymbolView.js"; +const projectAndroidSymbolSummary = ( + parent: Parameters[0], + view: z.input, +): ReturnType => + projectAdmittedAndroidSymbolSummary(parent, view satisfies AdmittedView); + type Operation = "inspect_android_class" | "inspect_android_method"; const engine = { name: "jadx", diff --git a/src/domain/analysisView/androidSymbolView.ts b/src/domain/analysisView/androidSymbolView.ts index 688d7b8401..3a39270ecf 100644 --- a/src/domain/analysisView/androidSymbolView.ts +++ b/src/domain/analysisView/androidSymbolView.ts @@ -1,14 +1,14 @@ -import type { AnalysisError } from "../analysisErrorBase.js"; import { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; import { androidResultSchemas } from "../android/androidAnalysis.js"; import type { JsonValue } from "../jsonValue.js"; import { jsonValueByteLength } from "../jsonValueByteLength.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -27,9 +27,9 @@ const PAGE_BYTES = 1024 * 1024; const DIGEST_BYTES = jsonValueByteLength({ view_digest: "0".repeat(64) }) - 1; const PROJECTION_BYTES = PAGE_BYTES - DIGEST_BYTES; -const boundedSummary = ( - result: UnsignedAnalysisView, -): Result => +const boundedSummary = ( + result: View, +) => jsonValueByteLength(result as unknown as JsonValue, PROJECTION_BYTES) > PROJECTION_BYTES ? err( @@ -53,9 +53,10 @@ const boundedSummary = ( /** Reuse authenticated Android facts without reopening the APK, mapping or engine. */ export const projectAndroidSymbolSummary = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary") + admitted: AdmittedView, +) => { + const view = ownedView(admitted, "summary"); + if (view === undefined) return err( analysisViewInputError([ { @@ -115,7 +116,7 @@ export const projectAndroidSymbolSummary = ( if (!parsed.ok) return parsed; return boundedSummary({ ...shared, - parent_operation: "inspect_android_class", + parent_operation: "inspect_android_class" as const, summary: parsed.value, }); } @@ -123,7 +124,7 @@ export const projectAndroidSymbolSummary = ( if (!parsed.ok) return parsed; return boundedSummary({ ...shared, - parent_operation: "inspect_android_method", + parent_operation: "inspect_android_method" as const, summary: parsed.value, unknowns: [ ...shared.unknowns, diff --git a/src/domain/analysisView/apiSurfaceSchema.ts b/src/domain/analysisView/apiSurfaceSchema.ts new file mode 100644 index 0000000000..b788e95ab5 --- /dev/null +++ b/src/domain/analysisView/apiSurfaceSchema.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; +import { ROUTE_RULES } from "./webNetworkRouteTemplates.js"; + +export const apiSurfaceViewSchema = z.strictObject({ + kind: z.literal("api-surface"), + offset: z.int().nonnegative().default(0), + limit: z.int().positive().default(50), + include_query_parameter_names: z + .boolean() + .optional() + .describe( + "Include distinct observed query parameter names and their original record ordinals; names do not establish required parameters or a complete API schema.", + ), +}); +const ruleSchema = z.enum(ROUTE_RULES); +const groupSchema = z.strictObject({ + method: z.string(), + origin: z.string(), + path_template: z.string(), + templated_segments: z.array( + z.strictObject({ + segment_index: z.int().nonnegative(), + rule: ruleSchema, + basis: z.literal("inferred"), + observed_values: z.array(z.string()), + }), + ), + record_ordinals: z.array(z.int().nonnegative()), + statuses: z.array(z.number()), + content_types: z.array(z.string()), + query_parameter_names: z + .array( + z.strictObject({ + name: z.string(), + record_ordinals: z.array(z.int().nonnegative()), + }), + ) + .optional(), + flags: z.array( + z.enum(["single-sample", "mixed-statuses", "mixed-content-types"]), + ), +}); +export const apiSurfaceFields = { + records: z.strictObject({ + examined: z.int().nonnegative(), + total: z.int().nonnegative(), + excluded: z.int().nonnegative(), + }), + templated_groups: z + .int() + .nonnegative() + .describe( + "Groups across every page that carry at least one templated segment.", + ), + groups: z.array(groupSchema), + excluded_records: z.array( + z.strictObject({ + ordinal: z.int().nonnegative(), + reason: z.enum([ + "url-redacted", + "url-unparseable", + "request-unavailable", + "method-unavailable", + ]), + }), + ), +}; diff --git a/src/domain/analysisView/apkSignatureView.ts b/src/domain/analysisView/apkSignatureView.ts index 8172766d2e..812c3cf2c7 100644 --- a/src/domain/analysisView/apkSignatureView.ts +++ b/src/domain/analysisView/apkSignatureView.ts @@ -9,17 +9,18 @@ import { import { apkSignatureSchemeSchema, apkSignatureViewItemsSchema, - type apkSignatureViewRequestSchema, + apkSignatureViewRequestSchema, } from "../apkSignature/apkSignatureQuery.js"; import { isImmutableJsonSnapshot } from "../immutableJson.js"; import type { JsonValue } from "../jsonValue.js"; import { err, ok, type Result } from "../result.js"; import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { @@ -263,8 +264,8 @@ function* records( export const projectApkSignatureView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "apk-signature") return err( analysisViewInputError([ @@ -276,6 +277,9 @@ export const projectApkSignatureView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "apk-signature"); const parsed = read(parent); if (!parsed.ok) return parsed; const { report, summary } = parsed.value; @@ -297,7 +301,7 @@ export const projectApkSignatureView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary, @@ -324,7 +328,7 @@ export const projectApkSignatureView = ( items.push(item); } return ok({ - kind: "apk-signature", + kind: "apk-signature" as const, view, ...shared, source_coverage: summary.source_coverage, diff --git a/src/domain/analysisView/artifactInventoryView.test.ts b/src/domain/analysisView/artifactInventoryView.test.ts index fc510639e4..4323daebd5 100644 --- a/src/domain/analysisView/artifactInventoryView.test.ts +++ b/src/domain/analysisView/artifactInventoryView.test.ts @@ -100,8 +100,7 @@ const page = (source: AnalysisViewParent, view: unknown) => { artifactInventoryViewRequestSchema.parse(view), ); if (!result.ok) throw result.error; - if (result.value.kind !== "inventory") - throw new Error("Expected an inventory page"); + if (!("items" in result.value)) throw new Error("Expected an inventory page"); return result.value; }; afterEach(() => vi.restoreAllMocks()); diff --git a/src/domain/analysisView/artifactInventoryView.ts b/src/domain/analysisView/artifactInventoryView.ts index d0b8773edb..f3608826ef 100644 --- a/src/domain/analysisView/artifactInventoryView.ts +++ b/src/domain/analysisView/artifactInventoryView.ts @@ -27,11 +27,12 @@ import { type InventorySet, } from "../artifactInventoryEvidence.js"; import type { ArtifactInspectionResult } from "../artifactInspection.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -231,8 +232,8 @@ const sourceFailure = ( /** Project exact source records; exhausted pages cover available records, not runtime behavior. */ export const projectArtifactInventoryView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { const operation = parent.operation; if (operation !== "inventory_artifact" && operation !== "inspect_artifact") return err( @@ -248,6 +249,9 @@ export const projectArtifactInventoryView = ( "Artifact Evidence supports summary and inventory views.", ), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "inventory"); const parsed = parseSource(parent); if (!parsed.ok) return parsed; const source = parsed.value; @@ -301,22 +305,21 @@ export const projectArtifactInventoryView = ( "Parent inventory is partial; counts and exhausted pages cover only available records. Missing matches do not establish absence.", ]), ], - } satisfies Pick< - UnsignedAnalysisView, - | "parent_evidence_id" - | "parent_operation" - | "parent_digest" - | "artifact" - | "limitations" - | "unknowns" - >; + } satisfies { + parent_evidence_id: string; + parent_operation: "inspect_artifact" | "inventory_artifact"; + parent_digest: string; + artifact: { path: string; sha256: string }; + limitations: readonly string[]; + unknowns: readonly string[]; + }; if (view.kind === "summary") { const examined = inventory.nodes.length + inventory.occurrences.length + inventory.edges.length; return ok({ - kind: "summary", + kind: "summary" as const, view, summary: { manifest: inventory.manifest, @@ -373,7 +376,7 @@ export const projectArtifactInventoryView = ( : inventory.integrityContradictions.length, }; return ok({ - kind: "inventory", + kind: "inventory" as const, view, manifest: inventory.manifest, inventory_evidence_ids: [...evidenceIds], @@ -406,7 +409,7 @@ const occurrencePage = (source: InventorySource, view: OccurrenceView) => { const nodePage = ( source: InventorySource, view: Extract< - AnalysisViewRequest, + z.output, { kind: "inventory"; collection: "nodes" } >, ) => { @@ -451,7 +454,7 @@ const inventoryCounts = (source: InventorySource): InventoryCounts => { }; type OccurrenceView = Extract< - AnalysisViewRequest, + z.output, { kind: "inventory"; collection: "occurrences" } >; diff --git a/src/domain/analysisView/binaryLayoutView.ts b/src/domain/analysisView/binaryLayoutView.ts index b4b7865ff4..f3d38e7aec 100644 --- a/src/domain/analysisView/binaryLayoutView.ts +++ b/src/domain/analysisView/binaryLayoutView.ts @@ -5,12 +5,15 @@ import { import type { AnalysisError } from "../analysisErrorBase.js"; import type { BinaryLayout } from "../native/binaryLayout.js"; import { err, ok, type Result } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, - GroupingViewKind, -} from "./analysisView.js"; + AdmittedView, + FacetAnalysisView, + ItemAnalysisView, + PageAnalysisView, + SummaryAnalysisView, +} from "./analysisViewShape.js"; +import { assertOwnedView } from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage as completeCoverage, @@ -171,8 +174,8 @@ const layoutSummary = (layout: BinaryLayout) => ({ export const projectBinaryLayoutView = ( parent: AnalysisViewParent, layout: BinaryLayout, - view: Exclude, -): Result => { + view: AdmittedView, +) => { const shared = parentFields(parent, layout); if (view.kind === "native") return err( @@ -190,9 +193,10 @@ export const projectBinaryLayoutView = ( return err( layoutIncompatible("EVM views require inspect_evm_interface Evidence."), ); - if (view.kind === "summary") + if (view.kind === "summary") { + assertOwnedView(view, "summary"); return ok({ - kind: "summary", + kind: "summary" as const, view, summary: layoutSummary(layout), coverage: completeCoverage( @@ -207,15 +211,19 @@ export const projectBinaryLayoutView = ( ), ...shared, }); - if (view.kind === "facet") + } + if (view.kind === "facet") { + assertOwnedView(view, "facet"); return ok({ - kind: "facet", + kind: "facet" as const, view, facet: view.facet === "mitigations" ? layout.mitigations : layout.linkage, coverage: completeCoverage(1, 1), ...shared, }); + } if (view.kind === "item") { + assertOwnedView(view, "item"); if (view.collection === "modules") return err( layoutIncompatible( @@ -228,7 +236,7 @@ export const projectBinaryLayoutView = ( : selectLayoutItem(view.collection, layout.symbols, view.selector); if (!selected.ok) return selected; return ok({ - kind: "item", + kind: "item" as const, view, item: selected.value, coverage: completeCoverage( @@ -246,6 +254,7 @@ export const projectBinaryLayoutView = ( "Layout views require summary, facet, item or page selections.", ), ); + assertOwnedView(view, "page"); if (view.collection === "modules") return err( layoutIncompatible( @@ -259,7 +268,7 @@ export const projectBinaryLayoutView = ( ? pageRows(layout.sections, view.offset, view.limit) : pageRows(layout.symbols, view.offset, view.limit); return ok({ - kind: "page", + kind: "page" as const, view, items: [...items], coverage: pageCoverage(view.offset, items.length, rows.length), @@ -270,7 +279,7 @@ export const projectBinaryLayoutView = ( const selectLayoutItem = ( collection: "sections" | "symbols", rows: readonly Row[], - selector: Extract["selector"], + selector: ItemAnalysisView["selector"], ): Result => { if ("index" in selector) return selectIndexed(collection, rows, selector.index); diff --git a/src/domain/analysisView/browserScenarioView.ts b/src/domain/analysisView/browserScenarioView.ts index a64363a275..b2df96a119 100644 --- a/src/domain/analysisView/browserScenarioView.ts +++ b/src/domain/analysisView/browserScenarioView.ts @@ -6,7 +6,7 @@ import { } from "../browserScenarioCapture.js"; import type { BrowserScenarioStep as Step } from "../browserScenarioCaptureValues.js"; import { compareAsciiStrings } from "../canonicalOrdering.js"; -import { err, ok, type Result } from "../result.js"; +import { err, ok } from "../result.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { completeWithinViewCoverage, @@ -14,11 +14,12 @@ import { type AnalysisViewCoverage, } from "./analysisViewCoverage.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { ARTIFACT_SECTIONS, stepRowSchema, @@ -314,10 +315,15 @@ const pageRecords = ( /** Project retained scenario Evidence without relaunching the browser or replaying actions. */ export const projectBrowserScenarioView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "browser-scenario") return err(unsupportedView()); + assertOwnedView( + view, + "summary", + "browser-scenario", + ); const parsed = readCapture(parent); if (!parsed.ok) return parsed; const capture = parsed.value; @@ -339,7 +345,7 @@ export const projectBrowserScenarioView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -371,7 +377,7 @@ export const projectBrowserScenarioView = ( ]), ); return ok({ - kind: "browser-scenario", + kind: "browser-scenario" as const, view, ...shared, source_completeness: capture.completeness, diff --git a/src/domain/analysisView/byteEntropyView.ts b/src/domain/analysisView/byteEntropyView.ts index c00e15a8a1..ae04201a6e 100644 --- a/src/domain/analysisView/byteEntropyView.ts +++ b/src/domain/analysisView/byteEntropyView.ts @@ -1,13 +1,13 @@ import { z } from "zod"; import { AnalysisInputError } from "../analysisErrorCore.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { byteEntropySchema, type ByteEntropy } from "../native/byteEntropy.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage } from "./analysisViewCoverage.js"; export const byteEntropySummarySchema = byteEntropySchema @@ -22,8 +22,8 @@ export const byteEntropySummarySchema = byteEntropySchema export const projectByteEntropyView = ( parent: AnalysisViewParent, report: ByteEntropy, - view: Exclude, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary") return err( new AnalysisInputError("inspect_analysis_view", undefined, [ @@ -35,11 +35,12 @@ export const projectByteEntropyView = ( }, ]), ); + assertOwnedView(view, "summary"); return ok({ - kind: "summary", + kind: "summary" as const, view, parent_evidence_id: parent.evidenceId, - parent_operation: "inspect_byte_entropy", + parent_operation: "inspect_byte_entropy" as const, parent_digest: parent.evidenceId.slice(3), artifact: { path: report.artifact.path, sha256: report.artifact.sha256 }, summary: { @@ -61,6 +62,6 @@ export const projectByteEntropyView = ( limitations: [ ...new Set([...parent.limitations, ...report.limitations]), ].sort(), - unknowns: [], + unknowns: [] as string[], }); }; diff --git a/src/domain/analysisView/commandObservationView.ts b/src/domain/analysisView/commandObservationView.ts index 6779a7255e..19d5482d88 100644 --- a/src/domain/analysisView/commandObservationView.ts +++ b/src/domain/analysisView/commandObservationView.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { commandObservationSchema, gdbCommandResultSchema, @@ -8,12 +7,13 @@ import { digestSchema } from "../digests.js"; import { providerSchema } from "../evidence.js"; import { analysisInputErrorFromIssues } from "../inputIssueProjection.js"; import { jsonValueSchema } from "../jsonValue.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -42,9 +42,10 @@ export const gdbCommandSummarySchema = gdbCommandResultSchema /** Select existing owner facts without parsing a command or its output protocol. */ export const projectCommandObservationView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary") + admitted: AdmittedView, +) => { + const view = ownedView(admitted, "summary"); + if (view === undefined) return err( analysisViewInputError([ { @@ -136,7 +137,7 @@ export const projectCommandObservationView = ( const { console, target, log, ...records } = parsed.value; return ok({ ...shared, - parent_operation: "gdb_console", + parent_operation: "gdb_console" as const, summary: { ...records, available_counts: { @@ -170,7 +171,7 @@ export const projectCommandObservationView = ( ); return ok({ ...shared, - parent_operation: "inspect_with_objdump", + parent_operation: "inspect_with_objdump" as const, summary: report, }); }; diff --git a/src/domain/analysisView/dartAotView.ts b/src/domain/analysisView/dartAotView.ts index 83625736f0..d1c196bf24 100644 --- a/src/domain/analysisView/dartAotView.ts +++ b/src/domain/analysisView/dartAotView.ts @@ -1,14 +1,14 @@ import { analysisViewInputError } from "./analysisViewErrors.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { flutterResultSchemas } from "../flutter/flutterBuildAnalysis.js"; -import { err, ok, type Result } from "../result.js"; +import { err, ok } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -47,9 +47,12 @@ export const dartAotViewContextSchema = z.strictObject({ /** Reuse authenticated AOT Evidence without opening the APK or rescanning its ELF. */ export const projectDartAotView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "dart-aot") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "dart-aot"); + if (view === undefined) return err( analysisViewInputError([ { @@ -90,7 +93,7 @@ export const projectDartAotView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { ...report, string_pool }, @@ -99,7 +102,7 @@ export const projectDartAotView = ( const collection = report.string_pool[view.collection]; const items = collection.slice(view.offset, view.offset + view.limit); return ok({ - kind: "dart-aot", + kind: "dart-aot" as const, view, ...shared, items, diff --git a/src/domain/analysisView/desktopApplicationView.ts b/src/domain/analysisView/desktopApplicationView.ts index a179baf873..3be4848187 100644 --- a/src/domain/analysisView/desktopApplicationView.ts +++ b/src/domain/analysisView/desktopApplicationView.ts @@ -16,13 +16,13 @@ import { completeWithinViewCoverage, pageViewCoverage, } from "./analysisViewCoverage.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { ok, err, type Result } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; +import { ok, err } from "../result.js"; // Bound diagnostic context independently of the retained report size. Previews // are display values; stable file IDs select the original paths and all issues. @@ -146,9 +146,12 @@ type DesktopViewRow = DesktopApplication[DesktopCollection][number]; const validatedParents = new WeakSet(); export const projectDesktopApplicationView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "desktop-application") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "desktop-application"); + if (view === undefined) return err( analysisViewInputError([ { @@ -272,7 +275,7 @@ export const projectDesktopApplicationView = ( else group.count++; } return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -342,7 +345,7 @@ export const projectDesktopApplicationView = ( ), ); return ok({ - kind: "desktop-application", + kind: "desktop-application" as const, view, ...shared, source_coverage: report.coverage, diff --git a/src/domain/analysisView/devicetreeView.ts b/src/domain/analysisView/devicetreeView.ts index c27e1a12ed..54232e38f4 100644 --- a/src/domain/analysisView/devicetreeView.ts +++ b/src/domain/analysisView/devicetreeView.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { devicetreeSchema } from "../firmware/devicetree.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -57,8 +57,8 @@ export const devicetreePageContextSchema = devicetreeSchema const readReport = createAnalysisViewResultReader(devicetreeSchema); export const projectDevicetreeView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { const invalid = (message: string) => err( analysisViewInputError([ @@ -69,6 +69,9 @@ export const projectDevicetreeView = ( return invalid( "Devicetree Evidence supports summary and devicetree views.", ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "devicetree"); const parsed = readReport(parent); if (!parsed.ok) return parsed; const r = parsed.value; @@ -99,7 +102,7 @@ export const projectDevicetreeView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary: { ...context, summary: r.summary }, coverage: completeWithinViewCoverage(r.nodes.length, r.nodes.length), @@ -148,7 +151,7 @@ export const projectDevicetreeView = ( const { items, total } = selected; return ok({ ...shared, - kind: "devicetree", + kind: "devicetree" as const, view, devicetree: { ...context, alias_count: r.summary.aliases.length }, items, diff --git a/src/domain/analysisView/environmentCoverageView.ts b/src/domain/analysisView/environmentCoverageView.ts index 40cca47794..4cd86896d4 100644 --- a/src/domain/analysisView/environmentCoverageView.ts +++ b/src/domain/analysisView/environmentCoverageView.ts @@ -1,11 +1,6 @@ import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { templateRulesSchema } from "./environmentManifestView.js"; import type { StateView } from "./environmentStateRules.js"; import { @@ -154,18 +149,18 @@ export const summarizeEnvironmentCoverage = (state: { /** Project the state an environment would serve and summarize which tools it answers from that state. */ export const projectEnvironmentCoverageView = ( parent: AnalysisViewParent, - view: Extract, + view: z.output, accumulator = createEnvironmentStateAccumulator(view), -): Result => { +) => { const projected = projectEnvironmentStateView( parent, - { kind: "environment-state", template_rules: view.template_rules }, + { kind: "environment-state" as const, template_rules: view.template_rules }, accumulator, ); if (!projected.ok) return projected; const state = projected.value; return ok({ - kind: "environment-coverage", + kind: "environment-coverage" as const, view, parent_evidence_id: state.parent_evidence_id, parent_operation: state.parent_operation, diff --git a/src/domain/analysisView/environmentManifestSchema.ts b/src/domain/analysisView/environmentManifestSchema.ts new file mode 100644 index 0000000000..9abea82b90 --- /dev/null +++ b/src/domain/analysisView/environmentManifestSchema.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; +import { jsonRecordSchema } from "../jsonRecord.js"; +import { TEMPLATE_RULES } from "./environmentManifestRules.js"; +import { ROUTE_RULES } from "./webNetworkRouteTemplates.js"; + +const manifestRuleSchema = z.enum([...ROUTE_RULES, ...TEMPLATE_RULES]); +const jsonTypeNameSchema = z.enum([ + "string", + "number", + "boolean", + "object", + "array", + "null", +]); +const toolSchema = z.strictObject({ + name: z.string(), + method: z.string(), + path_template: z.string(), + api_surface_template: z + .string() + .describe("Four-rule template of the tool's earliest record."), + params: z.array( + z.strictObject({ + index: z.number().int().nonnegative(), + name: z.string(), + rule: manifestRuleSchema, + basis: z.literal("inferred"), + observed_values: z.array(z.string()), + }), + ), + query_parameter_names: z.array(z.string()), + body_shape: z + .strictObject({ + type: z.literal("object"), + properties: jsonRecordSchema(jsonTypeNameSchema), + }) + .nullable(), + statuses: z.array(z.number()), + content_types: z.array(z.string()), + record_ordinals: z.array(z.number().int().nonnegative()), +}); +export const environmentManifestFields = { + origin: z + .string() + .describe("Primary origin: the origin with the most JSON-like records."), + name: z + .string() + .describe( + "Kebab-case environment name derived from the primary origin host.", + ), + tools: z.array(toolSchema), + records: z + .array( + z.strictObject({ + ordinal: z.number().int().nonnegative(), + method: z.string(), + path: z.string(), + status: z.number().nullable(), + content_type: z.string().nullable(), + }), + ) + .describe( + "Pointers to the retained parent records cited by tools; no bodies.", + ), + excluded: z.array( + z.strictObject({ + method: z.string(), + origin: z.string(), + path_template: z.string(), + reason: z.enum(["other-origin", "ui-page-or-asset", "non-json"]), + }), + ), + unknowns: z.array( + z.strictObject({ + kind: z.enum([ + "authorization-model", + "response-semantics", + "state-transition", + ]), + statement: z.string(), + }), + ), + templated_groups: z + .number() + .int() + .nonnegative() + .describe( + "Tools and excluded groups that carry at least one templated segment.", + ), +}; diff --git a/src/domain/analysisView/environmentManifestView.ts b/src/domain/analysisView/environmentManifestView.ts index 9dcd88f8f4..cabf1085d4 100644 --- a/src/domain/analysisView/environmentManifestView.ts +++ b/src/domain/analysisView/environmentManifestView.ts @@ -1,15 +1,11 @@ import { z } from "zod"; -import { jsonRecordSchema } from "../jsonRecord.js"; import type { WebNetworkCaptureRecord } from "../webNetworkCapture.js"; import { AnalysisUnsupportedTargetError } from "../analysisErrorCore.js"; import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { ownedView, type AdmittedView } from "./analysisViewShape.js"; import { EnvironmentManifestAccumulator } from "./EnvironmentManifestAccumulator.js"; import { TEMPLATE_RULES, @@ -20,7 +16,9 @@ import { admitScenarioCaptureParent, type ScenarioResponseBodyRetention, } from "./scenarioCaptureRecords.js"; -import { ROUTE_RULES } from "./webNetworkRouteTemplates.js"; +import { environmentManifestFields } from "./environmentManifestSchema.js"; + +export { environmentManifestFields }; /** Opt-in template rules, each listed at most once; shared by every view that groups tools. */ export const templateRulesSchema = z @@ -44,92 +42,6 @@ export const environmentManifestViewRequestSchema = z.strictObject({ ), }); -const manifestRuleSchema = z.enum([...ROUTE_RULES, ...TEMPLATE_RULES]); -const jsonTypeNameSchema = z.enum([ - "string", - "number", - "boolean", - "object", - "array", - "null", -]); -const toolSchema = z.strictObject({ - name: z.string(), - method: z.string(), - path_template: z.string(), - api_surface_template: z - .string() - .describe("Four-rule template of the tool's earliest record."), - params: z.array( - z.strictObject({ - index: z.number().int().nonnegative(), - name: z.string(), - rule: manifestRuleSchema, - basis: z.literal("inferred"), - observed_values: z.array(z.string()), - }), - ), - query_parameter_names: z.array(z.string()), - body_shape: z - .strictObject({ - type: z.literal("object"), - properties: jsonRecordSchema(jsonTypeNameSchema), - }) - .nullable(), - statuses: z.array(z.number()), - content_types: z.array(z.string()), - record_ordinals: z.array(z.number().int().nonnegative()), -}); -export const environmentManifestFields = { - origin: z - .string() - .describe("Primary origin: the origin with the most JSON-like records."), - name: z - .string() - .describe( - "Kebab-case environment name derived from the primary origin host.", - ), - tools: z.array(toolSchema), - records: z - .array( - z.strictObject({ - ordinal: z.number().int().nonnegative(), - method: z.string(), - path: z.string(), - status: z.number().nullable(), - content_type: z.string().nullable(), - }), - ) - .describe( - "Pointers to the retained parent records cited by tools; no bodies.", - ), - excluded: z.array( - z.strictObject({ - method: z.string(), - origin: z.string(), - path_template: z.string(), - reason: z.enum(["other-origin", "ui-page-or-asset", "non-json"]), - }), - ), - unknowns: z.array( - z.strictObject({ - kind: z.enum([ - "authorization-model", - "response-semantics", - "state-transition", - ]), - statement: z.string(), - }), - ), - templated_groups: z - .number() - .int() - .nonnegative() - .describe( - "Tools and excluded groups that carry at least one templated segment.", - ), -}; - /** The records, coverage and identity a manifest projects from, for either parent kind. */ export interface CaptureParent { readonly operation: @@ -227,28 +139,36 @@ const addCaptureRecords = ( /** Create the manifest budget with its selected query names and template rules. */ export const createEnvironmentManifestAccumulator = ( - view: AnalysisViewRequest, -): EnvironmentManifestAccumulator => - view.kind === "environment-manifest" - ? new EnvironmentManifestAccumulator( + view: AdmittedView, +): EnvironmentManifestAccumulator => { + const selected = ownedView< + z.output + >(view, "environment-manifest"); + return selected === undefined + ? new EnvironmentManifestAccumulator() + : new EnvironmentManifestAccumulator( undefined, - view.include_query_parameter_names, - view.template_rules, - ) - : new EnvironmentManifestAccumulator(); + selected.include_query_parameter_names, + selected.template_rules, + ); +}; /** Project retained request lines, from a HAR or a browser scenario, into a replayable tool surface; templates are inferences. */ export const projectEnvironmentManifestView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, - accumulator = createEnvironmentManifestAccumulator(view), -): Result => { - if (view.kind !== "environment-manifest") + admitted: AdmittedView, + accumulator = createEnvironmentManifestAccumulator(admitted), +) => { + const view = ownedView>( + admitted, + "environment-manifest", + ); + if (view === undefined) return err( new AnalysisUnsupportedTargetError( "inspect_analysis_view", parent.evidenceId, - `View ${view.kind} is unsupported for parent operation ${parent.operation}; environment-manifest requires inspect_web_network_capture or capture_browser_scenario.`, + `View ${admitted.kind} is unsupported for parent operation ${parent.operation}; environment-manifest requires inspect_web_network_capture or capture_browser_scenario.`, ), ); const grouped = groupCaptureRecords(parent, view.kind, view.template_rules, { @@ -265,7 +185,7 @@ export const projectEnvironmentManifestView = ( const manifest = accumulator.finish(); if (!manifest.ok) return manifest; return ok({ - kind: "environment-manifest", + kind: "environment-manifest" as const, view, parent_evidence_id: parent.evidenceId, parent_operation: capture.operation, diff --git a/src/domain/analysisView/environmentStateItemWrites.test.ts b/src/domain/analysisView/environmentStateItemWrites.test.ts new file mode 100644 index 0000000000..e1fccc2b61 --- /dev/null +++ b/src/domain/analysisView/environmentStateItemWrites.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; + +import { + timedCaptureEvidence, + type TimedCaptureRow, +} from "../../../tests/fixtures/analysisView.js"; +import { + ADDED_EMPTY_REFERENCE_ROWS, + BOOKMARK_TOGGLE_ROWS, + CREATES_BY_TITLE_ROWS, + ITEM_REFERENCES_ROWS, + NULL_THEN_STARRED_ROWS, + RANKED_AFTER_DELETE_ROWS, + REFUTED_TITLE_SORT_ROWS, + feedEntry, +} from "../../../tests/fixtures/environmentStateItemWrites.js"; +import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js"; + +const planOf = (rows: readonly TimedCaptureRow[], tool: string) => { + const plan = environmentStateViewOf(timedCaptureEvidence(rows)).writes.find( + (candidate) => candidate.tool === tool, + ); + if (plan === undefined) throw new Error(`No plan for ${tool}`); + return plan; +}; + +describe("item writes", () => { + it("learns a create's sort from a later list its full order follows, after one create was placed last", () => { + expect(planOf(CREATES_BY_TITLE_ROWS, "post_folders")).toMatchObject({ + served: true, + insert: { rule: "sorted", field: "title", direction: "ascending" }, + }); + expect(planOf(CREATES_BY_TITLE_ROWS, "delete_folders_by_id")).toMatchObject( + { served: true }, + ); + }); + + it("keeps a sort an earlier list ruled out away from a create, after the entity that ruled it out is deleted", () => { + expect(planOf(RANKED_AFTER_DELETE_ROWS, "post_folders")).toMatchObject({ + served: true, + insert: { rule: "sorted", field: "position", direction: "ascending" }, + }); + }); + + it("serves a create sending no parent after an earlier create added the parent reference", () => { + const plan = planOf(ADDED_EMPTY_REFERENCE_ROWS, "post_items"); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual( + expect.objectContaining({ + field: "parent", + rule: "reference", + member: "parentId", + }), + ); + }); + + it("serves a create whose parent and tags a call names by id, one call naming none", () => { + const plan = planOf(ITEM_REFERENCES_ROWS, "post_items"); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual( + expect.objectContaining({ + field: "parent", + rule: "reference", + member: "parentId", + type: "/api/locations", + }), + ); + expect(plan.rules).toContainEqual( + expect.objectContaining({ + field: "tags", + rule: "reference", + member: "tagIds", + type: "/api/tags", + many: true, + }), + ); + }); + + it("serves a body-less action that calls show flipping its flag both ways", () => { + expect( + planOf(BOOKMARK_TOGGLE_ROWS, "put_entries_bookmark_by_id"), + ).toMatchObject({ + served: true, + address: { by: "action", segment: "bookmark" }, + rules: expect.arrayContaining([{ field: "starred", rule: "toggle" }]), + }); + }); + + it("serves no action whose calls flipped its flag one way only", () => { + const starred = feedEntry(1, true, "2026-10-17T18:00:03.120000Z"); + expect( + planOf( + [ + ...BOOKMARK_TOGGLE_ROWS.slice(0, 4), + ["GET", "/api/entries/1", starred], + ], + "put_entries_bookmark_by_id", + ), + ).toMatchObject({ served: false }); + }); + + it("does not learn a title sort an earlier update's kept order ruled out", () => { + const ascending = { + rule: "sorted", + field: "title", + direction: "ascending", + }; + const update = planOf(REFUTED_TITLE_SORT_ROWS, "patch_folders_by_id"); + const created = planOf(REFUTED_TITLE_SORT_ROWS, "post_folders"); + expect(update).toMatchObject({ served: true }); + expect(update.insert).not.toEqual(ascending); + expect(created.insert).not.toEqual(ascending); + }); + + it("serves no toggle when an earlier call found the flag null", () => { + const plan = planOf(NULL_THEN_STARRED_ROWS, "put_entries_bookmark_by_id"); + expect(plan).toMatchObject({ served: false }); + expect(plan.rules).not.toContainEqual({ + field: "starred", + rule: "toggle", + }); + }); + + it("serves no toggle a call left its flag unchanged under", () => { + const unchanged = feedEntry(1, false, "2026-10-17T18:00:08.050000Z"); + expect( + planOf( + [ + ...BOOKMARK_TOGGLE_ROWS, + ["PUT", "/api/entries/1/bookmark", "", 204], + ["GET", "/api/entries/1", unchanged], + ], + "put_entries_bookmark_by_id", + ), + ).toMatchObject({ served: false }); + }); +}); diff --git a/src/domain/analysisView/environmentStateVariedCalls.test.ts b/src/domain/analysisView/environmentStateVariedCalls.test.ts new file mode 100644 index 0000000000..f157859c3b --- /dev/null +++ b/src/domain/analysisView/environmentStateVariedCalls.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, it } from "vitest"; + +import { + timedCaptureEvidence, + type TimedCaptureRow, +} from "../../../tests/fixtures/analysisView.js"; +import { + CLEARED_THEN_OMITTED_KEPT_ROWS, + DEFAULT_THEN_NUMBERED_ROWS, + DONE_CLOCK_ROWS, + DONE_MICRO_ROWS, + NUMBERED_THEN_BLANK_ROWS, + OWNER_CLEARED_THEN_NAMED_ROWS, + SENT_DELETED_THEN_NUMBERED_ROWS, + SENT_IN_ANOTHER_PROJECT_ROWS, + OWNER_NAMED_THEN_CLEARED_ROWS, + DONE_MIXED_ROWS, + DONE_ZONE_ROWS, + OMITTED_KEPT_THEN_CLEARED_ROWS, + FEWER_MEMBERS_ROWS, + NUMBERED_UNLESS_SENT_ROWS, + NUMBERED_THEN_SENT_ROWS, + SENT_THEN_DELETED_ROWS, + KEPT_THEN_CLEARED_ROWS, + CLEARED_THEN_KEPT_ROWS, + PUT_CLEARS_ROWS, +} from "../../../tests/fixtures/environmentStateVariedCalls.js"; +import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js"; + +const planOf = (rows: readonly TimedCaptureRow[], tool: string) => { + const plan = environmentStateViewOf(timedCaptureEvidence(rows)).writes.find( + (candidate) => candidate.tool === tool, + ); + if (plan === undefined) throw new Error(`No plan for ${tool}`); + return plan; +}; + +describe("writes whose calls send different members", () => { + it("serves an update a later call sends fewer members to, requiring only those every call sent", () => { + const plan = planOf(FEWER_MEMBERS_ROWS, "put_contacts_by_id"); + expect(plan).toMatchObject({ served: true }); + expect(plan.body?.required).toEqual(["email"]); + expect(plan.body?.accepted).toEqual( + expect.arrayContaining(["name", "city", "email"]), + ); + }); + + it("serves a create whose member one call sent and another left for the application to number", () => { + const plan = planOf(NUMBERED_UNLESS_SENT_ROWS, "post_bundles"); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual({ + field: "order", + rule: "sequence", + scope: "collection", + sent: true, + }); + expect(plan.body?.required).toEqual(["name"]); + }); + + it("serves a create whose first call left a member to be numbered and whose second sent one", () => { + const plan = planOf(NUMBERED_THEN_SENT_ROWS, "post_bundles"); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual({ + field: "order", + rule: "sequence", + scope: "collection", + sent: true, + }); + }); + + it.each([ + ["kept, then cleared", KEPT_THEN_CLEARED_ROWS], + ["cleared, then kept", CLEARED_THEN_KEPT_ROWS], + ])( + "withholds a PUT whose calls left a member out and stored it otherwise (%s)", + (_order, rows) => { + expect(planOf(rows, "put_tags_by_id")).toMatchObject({ + served: false, + reason: "unexplained-change", + }); + }, + ); + + it("confirms an update whose read shows the clock it stamped to the second", () => { + expect(planOf(DONE_CLOCK_ROWS, "patch_tasks_by_id")).toMatchObject({ + served: true, + }); + }); + + it("serves no update whose clock the read after it spells in another zone, which serving cannot store", () => { + // The read spells the answer's instant at the second, but as +00:00: serving would keep the answer's spelling. + expect(planOf(DONE_ZONE_ROWS, "patch_tasks_by_id")).toMatchObject({ + served: false, + }); + }); + + it.each([ + [ + "answered without the owner its read shows kept, then cleared", + OMITTED_KEPT_THEN_CLEARED_ROWS, + ], + [ + "cleared, then answered without the owner its read shows kept", + CLEARED_THEN_OMITTED_KEPT_ROWS, + ], + ])( + "withholds a PUT default an earlier or later read contradicts: %s", + (_order, rows) => { + expect(planOf(rows, "put_tags_by_id")).toMatchObject({ served: false }); + }, + ); + + it("serves a PUT that clears a member it left out, as a default", () => { + const plan = planOf(PUT_CLEARS_ROWS, "put_tags_by_id"); + expect(plan).toMatchObject({ served: true }); + expect(plan.body?.defaults).toEqual([{ member: "owner", value: null }]); + }); + + it("recognizes a sent array when learning a later omitted-member default", () => { + const a = { id: 1, name: "A", tags: ["x"] }; + const b = { id: 2, name: "B", tags: ["y"] }; + const changedA = { ...a, name: "AA" }; + const changedB = { ...b, name: "BB", tags: [] }; + const rows: readonly TimedCaptureRow[] = [ + ["GET", "/api/tags", [a, b]], + ["GET", "/api/tags/1", a], + ["GET", "/api/tags/2", b], + ["PUT", "/api/tags/1", changedA, 200, { name: "AA", tags: ["x"] }], + ["GET", "/api/tags/1", changedA], + ["PUT", "/api/tags/2", changedB, 200, { name: "BB" }], + ["GET", "/api/tags/2", changedB], + ["GET", "/api/tags", [changedA, changedB]], + ]; + expect(planOf(rows, "put_tags_by_id")).toMatchObject({ served: true }); + }); + + it("withholds a sent sequence value contradicted by its recorded answer", () => { + const rows: readonly TimedCaptureRow[] = [ + ...NUMBERED_UNLESS_SENT_ROWS.map((row) => + row[0] === "GET" && row[1] === "/api/bundles" && Array.isArray(row[2]) + ? ([ + row[0], + row[1], + (row[2] as { id: number; name: string }[]).map( + ({ id, name }) => ({ id, name }), + ), + ] as TimedCaptureRow) + : row, + ), + [ + "POST", + "/api/bundles", + { id: 5, name: "Echo", order: 10 }, + 201, + { name: "Echo", order: 9 }, + ], + [ + "GET", + "/api/bundles", + [ + { id: 1, name: "Alpha" }, + { id: 2, name: "Bravo" }, + { id: 3, name: "Charlie" }, + { id: 4, name: "Delta" }, + { id: 5, name: "Echo" }, + ], + ], + ]; + expect(planOf(rows, "post_bundles")).toMatchObject({ + served: false, + reason: "unexplained-change", + }); + }); +}); + +describe("writes replay holds as serving does", () => { + it("serves no create numbered below a sent value it deleted, as serving counts past it", () => { + // Serving keeps the largest order sent, 9, and would number the last create 10. + expect(planOf(SENT_THEN_DELETED_ROWS, "post_bundles")).toMatchObject({ + served: false, + }); + }); + + it("serves no update whose read spells its clock otherwise than the read the plan keeps", () => { + // Serving stores every update's clock as the first read spells it, to the millisecond. + expect(planOf(DONE_MIXED_ROWS, "patch_tasks_by_id")).toMatchObject({ + served: false, + }); + }); +}); + +describe("writes whose calls send different members, alike in replay and serving", () => { + it("serves no create numbered when left out after a call that left it out stored a fixed value", () => { + const plan = planOf(DEFAULT_THEN_NUMBERED_ROWS, "post_bundles"); + expect(plan.rules).not.toContainEqual( + expect.objectContaining({ field: "order", rule: "sequence" }), + ); + expect(plan).toMatchObject({ served: false }); + }); + + it("confirms an update whose read shows the clock it stamped to the microsecond", () => { + expect(planOf(DONE_MICRO_ROWS, "patch_tasks_by_id")).toMatchObject({ + served: true, + }); + }); + + it.each([ + ["named first", OWNER_NAMED_THEN_CLEARED_ROWS], + ["cleared first", OWNER_CLEARED_THEN_NAMED_ROWS], + ])( + "serves a PUT that names a reference by its member and one that leaves it out and clears it: %s", + (_order, rows) => { + const plan = planOf(rows, "put_tags_by_id"); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual( + expect.objectContaining({ + field: "owner", + rule: "reference", + member: "ownerId", + }), + ); + }, + ); + + it("serves no create numbered below a value an earlier create sent, which serving counts past", () => { + const plan = planOf(SENT_DELETED_THEN_NUMBERED_ROWS, "post_bundles"); + expect(plan.rules).not.toContainEqual( + expect.objectContaining({ field: "order", rule: "sequence" }), + ); + expect(plan).toMatchObject({ served: false }); + }); + + it("serves a create numbered in its own project past what it holds, though another project's create sent more", () => { + const plan = planOf( + SENT_IN_ANOTHER_PROJECT_ROWS, + "post_projects_bundles_by_id", + ); + expect(plan).toMatchObject({ served: true }); + expect(plan.rules).toContainEqual( + expect.objectContaining({ + field: "order", + rule: "sequence", + scope: "collection", + sent: true, + }), + ); + }); +}); diff --git a/src/domain/analysisView/environmentStateView.ts b/src/domain/analysisView/environmentStateView.ts index 1802d850f1..044dd91931 100644 --- a/src/domain/analysisView/environmentStateView.ts +++ b/src/domain/analysisView/environmentStateView.ts @@ -1,13 +1,8 @@ import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js"; import { jsonRecordSchema } from "../jsonRecord.js"; -import { ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js"; import { DELETED_FLAGS } from "./environmentStateRules.js"; import { isClockZone } from "./environmentStateWriteValues.js"; @@ -247,6 +242,7 @@ const fieldRuleSchema = z.discriminatedUnion("rule", [ rule: z.literal("sequence"), scope: z.enum(["collection", "type"]), start: z.number().int().exactOptional(), + sent: z.literal(true).exactOptional(), }), z.strictObject({ ...ruled, rule: z.literal("unique") }), z.strictObject({ @@ -322,6 +318,7 @@ const fieldRuleSchema = z.discriminatedUnion("rule", [ member: z.string(), type: z.string(), fields: z.array(z.string()).min(1), + many: z.literal(true).exactOptional(), }), z.strictObject({ ...ruled, rule: z.literal("increment") }), z.strictObject({ @@ -342,6 +339,7 @@ const fieldRuleSchema = z.discriminatedUnion("rule", [ member: z.string(), }), z.strictObject({ ...ruled, rule: z.literal("set"), value: jsonValueSchema }), + z.strictObject({ ...ruled, rule: z.literal("toggle") }), ]); const insertRuleSchema = z.union([ z.strictObject({ rule: z.enum(["first", "last"]) }), @@ -396,7 +394,7 @@ const writePlanSchema = z.strictObject({ }) .exactOptional() .describe( - "A create's later read of the entity: an item read lends fields the response lacks and its clock spellings, a list read only adds fields the response lacks.", + "A write's later read of the entity: an item read lends a create fields the response lacks, and its clock spellings; a list read only adds fields the response lacks.", ), }) .nullable() @@ -555,23 +553,17 @@ export const environmentStateFields = { }; /** State extraction and coverage share the same budget and template-rule policy. */ -export const createEnvironmentStateAccumulator = ( - view: Extract< - AnalysisViewRequest, - { kind: "environment-state" | "environment-coverage" } - >, -): EnvironmentStateAccumulator => +export const createEnvironmentStateAccumulator = (view: { + readonly template_rules: z.output; +}): EnvironmentStateAccumulator => new EnvironmentStateAccumulator(undefined, view.template_rules); /** Extract the init state a capture's reads observed and each read tool's render plan; types and identity are inferences. */ export const projectEnvironmentStateView = ( parent: AnalysisViewParent, - view: Extract, + view: z.output, accumulator = createEnvironmentStateAccumulator(view), -): Result< - Extract, - AnalysisError -> => { +) => { const grouped = groupCaptureRecords(parent, view.kind, view.template_rules, { accumulator, noun: "state", @@ -583,7 +575,7 @@ export const projectEnvironmentStateView = ( const state = accumulator.finishState(); if (!state.ok) return state; return ok({ - kind: "environment-state", + kind: "environment-state" as const, view, parent_evidence_id: parent.evidenceId, parent_operation: grouped.value.operation, diff --git a/src/domain/analysisView/environmentStateWriteApply.ts b/src/domain/analysisView/environmentStateWriteApply.ts index 05a93adefe..255dd25d89 100644 --- a/src/domain/analysisView/environmentStateWriteApply.ts +++ b/src/domain/analysisView/environmentStateWriteApply.ts @@ -223,14 +223,25 @@ const ruledValue = ( observed: JsonObject, call: WriteCall, generate: GenerateValue, - referenced?: ReferencedEntity, + references: { + readonly referenced: ReferencedEntity | undefined; + readonly blank: WriteBody["blank"]; + }, ): Applied => { const body = call.body ?? {}; switch (rule.rule) { case "reference": - return referencedValue(rule, body, referenced); - case "identity": + return referencedValue( + rule, + body, + references.referenced, + references.blank, + ); case "sequence": + return rule.sent === true && Object.hasOwn(body, rule.field) + ? ok(body[rule.field]!) + : generate(rule); + case "identity": case "unique": case "renewed": case "clock": @@ -330,12 +341,17 @@ const copiedShapes = ( }), ); -/** A create's call with the members it left out that recorded calls sent empty, as the earliest sent them; one a call that left it out showed a default for keeps that default. */ -const withBlanks = (body: WriteBody, call: WriteCall): WriteCall => { +/** A create's call with the members it left out that recorded calls sent empty, as the earliest sent them; one a call that left it out showed a default for keeps that default, and one `unheld` names stays left out. */ +const withBlanks = ( + body: WriteBody, + call: WriteCall, + unheld: ReadonlySet, +): WriteCall => { const given = call.body ?? {}; const left = (body.blank ?? []).filter( ({ member }, index, blank) => !Object.hasOwn(given, member) && + !unheld.has(member) && !(body.defaults ?? []).some((entry) => entry.member === member) && blank.findIndex((entry) => entry.member === member) === index, ); @@ -360,7 +376,30 @@ export const createdFields = ( idField?: string, ): Applied => { if (plan.body === null) return err("The tool has no modelled body."); - const call = withBlanks(plan.body, sentCall); + // A reference the observed entity lacks is one a create left out and stored nothing for, as a call leaving it out does. + const unheld = new Set( + plan.rules.flatMap((rule) => + rule.rule === "reference" && !Object.hasOwn(observed, rule.field) + ? [rule.member] + : [], + ), + ); + // A member a `sequence` with `sent` numbers when a call leaves it out takes the next number, not an empty value one sent. + const numbered = new Set( + plan.rules.flatMap((rule) => + rule.rule === "sequence" && rule.sent === true ? [rule.field] : [], + ), + ); + const call = withBlanks( + { + ...plan.body, + blank: (plan.body.blank ?? []).filter( + ({ member }) => !numbered.has(member), + ), + }, + sentCall, + unheld, + ); const problem = bodyProblem(plan.body, call, { entity: { ...copiedShapes(plan.rules, observed), ...observed }, }); @@ -370,7 +409,10 @@ export const createdFields = ( (plan.body.defaults ?? []).map(({ member, value }) => [member, value]), ); for (const rule of plan.rules) { - const value = ruledValue(rule, observed, call, generate, referenced); + const value = ruledValue(rule, observed, call, generate, { + referenced, + blank: plan.body.blank, + }); if (!value.ok) return value; if (value.value !== undefined) setOwnMember(written, rule.field, value.value); @@ -465,8 +507,13 @@ export const updatedFields = ( for (const rule of plan.rules) { const stated = statedValue(rule, sent); if (stated !== undefined) setOwnMember(written, rule.field, stated); - else if (rule.rule === "reference" && Object.hasOwn(sent, rule.member)) { - const value = referencedValue(rule, sent, referenced); + else if (rule.rule === "toggle") { + const was = ownMember(before, rule.field); + if (typeof was !== "boolean") + return err(`The entity holds no flag ${rule.field} to flip.`); + setOwnMember(written, rule.field, !was); + } else if (rule.rule === "reference" && Object.hasOwn(sent, rule.member)) { + const value = referencedValue(rule, sent, referenced, plan.body.blank); if (!value.ok) return value; if (value.value !== undefined) setOwnMember(written, rule.field, value.value); @@ -489,6 +536,18 @@ export const updatedFields = ( setOwnMember(written, rule.field, value.value); } } + // A member the call leaves out takes the default a whole-object update showed; one a reference names by its member is not left out. + for (const { member, value } of plan.body.defaults ?? []) + if ( + !Object.hasOwn(sent, member) && + !plan.rules.some( + (rule) => + rule.rule === "reference" && + rule.field === member && + Object.hasOwn(sent, rule.member), + ) + ) + setOwnMember(written, member, value); const unspelled = derive( plan.rules.filter( (rule) => rule.rule !== "slug" || Object.hasOwn(sent, rule.from), diff --git a/src/domain/analysisView/environmentStateWriteCalls.ts b/src/domain/analysisView/environmentStateWriteCalls.ts index a649471573..00fca10b4d 100644 --- a/src/domain/analysisView/environmentStateWriteCalls.ts +++ b/src/domain/analysisView/environmentStateWriteCalls.ts @@ -24,7 +24,8 @@ export const unsentChange = (rule: FieldRule): boolean => rule.rule === "composed" || rule.rule === "stamp" || rule.rule === "derived" || - rule.rule === "set"; + rule.rule === "set" || + rule.rule === "toggle"; /** Whether serving gives a rule's field a new value on every call, so no call may have kept it. */ export const regenerates = (rule: FieldRule): boolean => @@ -56,6 +57,34 @@ export const disagreesWithCall = ( (regenerates(rule) && call.kept.includes(rule.field)), ); +/** A plan's set rules a later call flipped back, and that call's own, as toggles; `steady` names fields a call found already set, which an action sets rather than toggles. Every earlier call must have started from a boolean too, or serving rejects the first one. */ +export const toggledRules = ( + rules: readonly FieldRule[], + later: readonly FieldRule[], + before: JsonObject, + steady: ReadonlySet, + earlier: readonly { readonly startedBoolean: ReadonlySet }[], +): { readonly rules: FieldRule[]; readonly later: FieldRule[] } => { + const flipped = new Set( + later.flatMap((rule) => { + const was = ownMember(before, rule.field); + const held = rules.find((other) => other.field === rule.field); + return rule.rule === "set" && + typeof was === "boolean" && + rule.value === !was && + !steady.has(rule.field) && + earlier.every((call) => call.startedBoolean.has(rule.field)) && + ((held?.rule === "set" && held.value === was) || + held?.rule === "toggle") + ? [rule.field] + : []; + }), + ); + const toggle = (rule: FieldRule): FieldRule => + flipped.has(rule.field) ? { field: rule.field, rule: "toggle" } : rule; + return { rules: rules.map(toggle), later: later.map(toggle) }; +}; + /** The fields a call found already at the value a set rule gives them: the action sets them rather than toggling them. */ export const steadyFields = ( rules: readonly FieldRule[], @@ -104,8 +133,13 @@ const sameObserved = (left: ObservedValue, right: ObservedValue): boolean => /** The equality facts a modelled update showed, scalar inputs it sent, and fields it kept, which later rules must reproduce. */ export interface UpdateCall { readonly shown: ReadonlyMap; + /** Members its answer left out that the read after it showed, which a default must agree with too. */ + readonly read: ReadonlyMap; readonly sent: ReadonlyMap; + readonly sentMembers: ReadonlySet; readonly kept: readonly string[]; + /** Fields whose value before the call was a boolean. A toggle requires this of every call. */ + readonly startedBoolean: ReadonlySet; } /** How a later update's entity and the plan's calls and held entities showed `&` in the slugs it learns. */ @@ -191,8 +225,8 @@ export const contradictsEarlierCalls = ( integerOf(observation.value) !== undefined ); if (rule.rule === "set") return !sameObserved(observation, expected!); - // Serving stamps a clock on every call, so a field an earlier call kept contradicts it. - if (rule.rule === "clock") return true; + // Serving stamps a clock and flips a toggle on every call, so a field an earlier call kept contradicts it. + if (rule.rule === "clock" || rule.rule === "toggle") return true; if (rule.rule === "stamp") return !sameObserved(observation, expected!); if (rule.rule === "derived") return sent.has(rule.member) && !sameObserved(observation, expected!); @@ -228,11 +262,21 @@ export const refinedBody = ( const accepted = later.accepted.filter( (member) => !body.accepted.includes(member), ); + const held = body.defaults ?? []; + const defaults = [ + ...held, + ...(later.defaults ?? []).filter( + (entry) => !held.some((kept) => kept.member === entry.member), + ), + ]; return { ...body, + ...(defaults.length === 0 ? {} : { defaults }), // A later call may be the first to show the body repeating the path's id. identity: body.identity ?? later.identity, accepted: [...body.accepted, ...accepted], + // A member is required only when every call sent it. + required: body.required.filter((member) => later.required.includes(member)), fixed: [ ...body.fixed, ...later.fixed.filter((entry) => !fixed.has(entry.member)), @@ -314,6 +358,42 @@ export const appliedUpdate = ( }; }; +/** Whether defaults disagree with a call that left their member out: a remembered call, or this one, showed it otherwise. */ +export const defaultsDisagree = ( + defaults: WriteBody["defaults"], + calls: { + readonly earlier: readonly UpdateCall[]; + readonly sent?: JsonObject; + readonly after?: JsonObject; + }, + rules: readonly FieldRule[], +): boolean => { + // A call that named a reference's entity by its member supplied the field. + const sources = new Map( + rules.flatMap((rule) => + rule.rule === "reference" ? [[rule.field, rule.member] as const] : [], + ), + ); + const supplied = (sent: (member: string) => boolean, member: string) => + sent(member) || (sources.has(member) && sent(sources.get(member)!)); + return (defaults ?? []).some( + ({ member, value }) => + calls.earlier.some((call) => { + const stored = call.shown.get(member) ?? call.read.get(member); + return ( + !supplied((name) => call.sentMembers.has(name), member) && + stored !== undefined && + !same(stored, observedValue(value)) + ); + }) || + (calls.sent !== undefined && + calls.after !== undefined && + !supplied((name) => Object.hasOwn(calls.sent!, name), member) && + Object.hasOwn(calls.after, member) && + !same(calls.after[member], value)), + ); +}; + /** Keep what a modelled update call showed, which later calls' rules must reproduce, and the fields it found already set. */ export const rememberCall = ( retain: (bytes: number) => void, @@ -328,6 +408,8 @@ export const rememberCall = ( readonly after: JsonObject; readonly sent: JsonObject; readonly kept: readonly string[]; + /** The entity as the read after the call showed it, when one did. */ + readonly read?: JsonObject | undefined; }, /** Clocks the call left as they were in its own second, which show neither a stamp nor a kept value. */ sameSecond: readonly string[], @@ -342,8 +424,35 @@ export const rememberCall = ( ]), ); for (const field of unstamped) shown.delete(field); - const facts = { shown, sent: scalars(call.sent), kept }; - retain(64 + jsonTextCodeUnits([...facts.shown, ...facts.sent, kept]) * 2); + const read = new Map( + Object.entries(call.read ?? {}) + .filter(([field]) => !Object.hasOwn(call.after, field)) + .map(([field, value]) => [field, observedValue(value)]), + ); + const facts = { + shown, + read, + sent: scalars(call.sent), + sentMembers: new Set(Object.keys(call.sent)), + kept, + startedBoolean: new Set( + Object.entries(call.before).flatMap(([field, value]) => + typeof value === "boolean" ? [field] : [], + ), + ), + }; + retain( + 64 + + jsonTextCodeUnits([ + ...facts.shown, + ...facts.read, + ...facts.sent, + [...facts.sentMembers], + kept, + [...facts.startedBoolean], + ]) * + 2, + ); draft.updates.push(facts); for (const field of steadyFields(draft.plan.rules, call.before, call.after)) draft.steady.add(field); diff --git a/src/domain/analysisView/environmentStateWriteCreate.ts b/src/domain/analysisView/environmentStateWriteCreate.ts index 694c45d0c5..04a67a121f 100644 --- a/src/domain/analysisView/environmentStateWriteCreate.ts +++ b/src/domain/analysisView/environmentStateWriteCreate.ts @@ -295,8 +295,8 @@ const refineCreate = ( { rules: draft.plan.rules, body: draft.plan.body! }, draft.observed!, later, - earlierOf(draft), - ) ?? reexplained(draft, explained.rules, later); + earlierOf(draft, evidence.collection), + ) ?? reexplained(draft, explained.rules, later, evidence.collection); if (widened === undefined) return false; retainFixed(context, widened.body.fixed.slice(draft.plan.body!.fixed.length)); draft.plan.rules = widened.rules; @@ -307,11 +307,15 @@ const refineCreate = ( }; /** What the plan's earlier calls showed, for merging a later one. */ -const earlierOf = (draft: WriteDraft) => ({ +const earlierOf = (draft: WriteDraft, collection: string) => ({ kept: draft.kept, clockOf: (field: string) => clockEvidence(draft, field), startOf: (field: string) => draft.starts.get(field), createCollections: draft.createCollections, + sentLargest: (field: string, scope: "collection" | "type") => + draft.sentLargest.get( + scope === "collection" ? `${field} ${collection}` : field, + ), ampersands: draft.ampersands, }); @@ -338,6 +342,7 @@ const reexplained = ( draft: WriteDraft, learned: readonly FieldRule[], later: ReturnType, + collection: string, ) => { const input = draft.exemplarInput; if (input === undefined) return undefined; @@ -360,7 +365,7 @@ const reexplained = ( }, draft.observed!, later, - earlierOf(draft), + earlierOf(draft, collection), ); }; @@ -504,13 +509,21 @@ const sequencesOf = ( : [], ); -/** Whether a later call's sequences hold what serving generates: one past the largest value their scope holds or started with and past the plan's last, spelled as the exemplar spells it. */ +/** Whether a later call's sequences hold what serving generates: one past the largest value their scope holds or started with and past the plan's last, spelled as the exemplar spells it; or, for one that takes a sent value, the value the call sent. */ const followsSequences = ( context: EffectContext, draft: WriteDraft, - created: Pick, + created: Pick & { + readonly sent: JsonObject; + readonly observed: JsonObject; + }, ): boolean => sequencesOf(draft, created.collection).every(({ rule, key }) => { + if (rule.sent === true && Object.hasOwn(created.sent, rule.field)) + return ( + !Object.hasOwn(created.observed, rule.field) || + same(ownMember(created.observed, rule.field), created.sent[rule.field]) + ); const scope = [ ...context.state.ofType(created.type), ...context.startedWith(created.type), @@ -550,6 +563,7 @@ const agreesLater = ( ...created, observed: call.observed, startMs: call.startMs, + sent: call.body, }) && !contradictsDefault(draft.plan.rules, call, created.fields) && !unruledDiffers(draft.plan.rules, call.observed, { @@ -598,6 +612,7 @@ const followsCreateRules = ( created: Pick & { readonly observed: JsonObject; readonly startMs: number | undefined; + readonly sent: JsonObject; }, ): boolean => draft.plan.rules.every((rule) => { @@ -706,27 +721,45 @@ const fitClocks = (draft: WriteDraft, fields: JsonObject): void => { noteFixedWidths(draft.fixedWidth, fields); }; -/** Record the values a create's sequences took, which the next call follows. */ +/** Record the values a create's sequences took, which the next call follows; as serving does, the largest stays, so a lower sent value does not lower it. */ const takeSequences = ( draft: WriteDraft, created: Pick, ): void => { - for (const { rule, key } of sequencesOf(draft, created.collection)) { - const value = sequenceNumber( - ownMember(created.fields, rule.field), - ownMember(draft.observed, rule.field), + const take = (key: string, value: number | undefined) => { + if (value === undefined) return; + const taken = draft.sequences.get(key); + draft.sequences.set( + key, + taken === undefined ? value : Math.max(taken, value), + ); + }; + for (const { rule, key } of sequencesOf(draft, created.collection)) + take( + key, + sequenceNumber( + ownMember(created.fields, rule.field), + ownMember(draft.observed, rule.field), + ), ); - if (value !== undefined) draft.sequences.set(key, value); - } for (const rule of draft.plan.rules) { if (rule.rule !== "made-object") continue; const made = ownMember(created.fields, rule.field); for (const { member } of rule.counts) { - const value = isObject(made) - ? sequenceNumber(ownMember(made, member)) - : undefined; - if (value !== undefined) - draft.sequences.set(madeKey(rule.field, member), value); + take( + madeKey(rule.field, member), + isObject(made) ? sequenceNumber(ownMember(made, member)) : undefined, + ); + } + } + for (const rule of draft.plan.rules) { + if (rule.rule !== "echo") continue; + const value = sequenceNumber(ownMember(created.fields, rule.field)); + // By type and by collection, as a sequence adopted later may count either. + for (const key of [rule.field, `${rule.field} ${created.collection}`]) { + const held = draft.sentLargest.get(key); + if (value !== undefined && (held === undefined || value > held)) + draft.sentLargest.set(key, value); } } }; diff --git a/src/domain/analysisView/environmentStateWriteCreateCalls.ts b/src/domain/analysisView/environmentStateWriteCreateCalls.ts index 1383545146..bbd92bbefb 100644 --- a/src/domain/analysisView/environmentStateWriteCreateCalls.ts +++ b/src/domain/analysisView/environmentStateWriteCreateCalls.ts @@ -9,6 +9,7 @@ import { import { storedKinds } from "./environmentStateWriteApply.js"; import { fittedClockFormat } from "./environmentStateWriteClockFormats.js"; import { composedValue } from "./environmentStateWriteComposed.js"; +import { emptyReference } from "./environmentStateWriteReferences.js"; import { alikeRules, isBlankValue, @@ -33,6 +34,11 @@ export interface EarlierEvidence { /** The integer the exemplar showed in an unruled field above every value its type held, where a sequence may start. */ readonly startOf?: (field: string) => number | undefined; readonly createCollections?: readonly string[]; + /** The largest integer an echoed field held on the plan's accepted creates, across the type or in the later call's collection. */ + readonly sentLargest?: ( + field: string, + scope: "collection" | "type", + ) => number | undefined; /** How the plan's accepted calls and the type's held entities spelled `&` in each field's slug; absent where none showed one. */ readonly ampersands?: ReadonlyMap; } @@ -298,6 +304,10 @@ interface FieldCalls { readonly exemplar: JsonObject; readonly later: { readonly entity: JsonObject; readonly body: JsonObject }; readonly sentBefore: ReadonlySet; + /** The values the plan's calls sent members it fixes or saw sent empty. */ + readonly pinned: ReadonlyMap; + /** Members an earlier call left out and the application stored a fixed value for (`body.defaults`). */ + readonly defaulted: ReadonlySet; readonly required: readonly string[]; readonly earlier: EarlierEvidence; } @@ -313,6 +323,12 @@ const addedReference = ( return Object.hasOwn(later.body, added.member) ? undefined : { rule: added }; + // A call that named nothing and stored nothing agrees, as it does with a reference the first create showed. + if ( + after === undefined && + emptyReference(later.body[added.member], later.entity[field]) + ) + return { rule: added }; const rule = after === undefined ? undefined @@ -339,6 +355,42 @@ const shownAlone = ( return sent ? undefined : { rule: own }; }; +/** A member one call sent and stored as sent, and another left out and the application numbered: the value a call sends, else the next number. */ +const sentOrNumbered = ( + before: FieldRule, + after: FieldRule, + field: string, + calls: FieldCalls, + agree: (held: FieldRule) => FieldRule | undefined, +): FieldRule | undefined => { + const laterSent = Object.hasOwn(calls.later.body, field); + if (before.rule === "echo") { + // Serving numbers past every value a call sent, so a later call numbered at or below one cannot be repeated. + const numbered = sequenceNumber(ownMember(calls.later.entity, field)); + const largest = + after.rule === "sequence" + ? calls.earlier.sentLargest?.(field, after.scope) + : undefined; + // An earlier call that left the member out stored a fixed value, so the application does not number it. + return after.rule === "sequence" && + !laterSent && + !calls.defaulted.has(field) && + (largest === undefined || (numbered !== undefined && numbered > largest)) + ? { ...after, sent: true } + : undefined; + } + if (before.rule !== "sequence") return undefined; + if (after.rule === "echo") + return before.sent === true || !calls.sentBefore.has(field) + ? { ...before, sent: true } + : undefined; + if (before.sent !== true || after.rule !== "sequence" || laterSent) + return undefined; + const { sent: _sent, ...numbered } = before; + const merged = agree(numbered); + return merged?.rule === "sequence" ? { ...merged, sent: true } : undefined; +}; + /** One field's rule in the merged plan, and the default it gives a member; undefined when the calls disagree on it. */ const mergedField = ( field: string, @@ -351,15 +403,33 @@ const mergedField = ( if (!inExemplar || !Object.hasOwn(later.entity, field)) return shownAlone(inExemplar, calls); if (before !== undefined && after !== undefined) { - const rule = agreeing(before, after, { + const values = { field, later: later.entity[field], earlier: calls.earlier, exemplar, laterEntity: later.entity, - }); + }; + const rule = + agreeing(before, after, values) ?? + sentOrNumbered(before, after, field, calls, (held) => + agreeing(held, after, values), + ); return rule === undefined ? undefined : { rule }; } + // A reference one call showed and another that named nothing, storing nothing, agree. + if ( + before?.rule === "reference" && + after === undefined && + emptyReference(later.body[before.member], later.entity[field]) + ) + return { rule: before }; + if ( + before === undefined && + after?.rule === "reference" && + emptyReference(calls.pinned.get(after.member), exemplar[field]) + ) + return { rule: after }; // The plan echoes a member this call left out: what it stored there is the member's default. if (before !== undefined) return before.rule === "echo" && !Object.hasOwn(later.body, field) @@ -412,6 +482,16 @@ export const mergedCreatePlan = ( ...plan.body.fixed.map(({ member }) => member), ...plan.body.required, ]); + const pinned = new Map([ + ...(plan.body.blank ?? []).map(({ member, sent }): [string, JsonValue] => [ + member, + sent, + ]), + ...plan.body.fixed.map(({ member, sent }): [string, JsonValue] => [ + member, + sent, + ]), + ]); const merged = new Map(); const defaults: Default[] = []; for (const field of new Set([ @@ -425,6 +505,10 @@ export const mergedCreatePlan = ( exemplar, later, sentBefore, + pinned, + defaulted: new Set( + (plan.body.defaults ?? []).map(({ member }) => member), + ), required: plan.body.required, earlier, }); @@ -432,7 +516,20 @@ export const mergedCreatePlan = ( if (outcome.rule !== undefined) merged.set(field, outcome.rule); if (outcome.default !== undefined) defaults.push(outcome.default); } - const body = mergedBody(plan.body, later, exemplar); + // A member a reference reads is accepted with any id, though a call that named nothing showed it fixed. + const read = new Set( + [...merged.values()].flatMap((rule) => + rule.rule === "reference" ? [rule.member] : [], + ), + ); + const body = mergedBody( + released(plan.body, read), + { + ...later, + contract: released(later.contract, read), + }, + exemplar, + ); const joined = body === undefined ? undefined @@ -467,6 +564,30 @@ const joinedBlanks = ( return blank; }; +/** A body contract with the members references read accepted, and kept as sent empty, rather than fixed at the empty value a call sent. */ +const released = (body: WriteBody, read: ReadonlySet): WriteBody => { + const freed = body.fixed.filter( + ({ member, sent }) => read.has(member) && emptyReference(sent, sent), + ); + if (freed.length === 0) return body; + return { + ...body, + fixed: body.fixed.filter((entry) => !freed.includes(entry)), + blank: [ + ...(body.blank ?? []), + ...freed.filter( + ({ member }) => !body.blank?.some((entry) => entry.member === member), + ), + ], + accepted: [ + ...body.accepted, + ...freed + .map(({ member }) => member) + .filter((member) => !body.accepted.includes(member)), + ], + }; +}; + /** Two calls' body contracts joined, undefined when a member one accepts with any value the other fixes, or both fix it at different values. */ const mergedBody = ( body: WriteBody, diff --git a/src/domain/analysisView/environmentStateWriteEffects.ts b/src/domain/analysisView/environmentStateWriteEffects.ts index 4ace3c1121..53c9380954 100644 --- a/src/domain/analysisView/environmentStateWriteEffects.ts +++ b/src/domain/analysisView/environmentStateWriteEffects.ts @@ -6,6 +6,7 @@ import { isObject, locateItem, at, + same, type JsonObject, type ParsedRequestBody, requestJson, @@ -34,6 +35,7 @@ import { import { otherFields } from "./environmentStateWriteGenerated.js"; import { contradictsEarlierCalls, + defaultsDisagree, disagreesWithCall, keptFields, rememberCall, @@ -42,14 +44,23 @@ import { conflictsWithPlan, refinedBody, reconciledUpdateRule, + toggledRules, } from "./environmentStateWriteCalls.js"; import { laterObservation, + respelledInstant, shownBefore, viewFields, type Observed, + type ObservationSearch, + type ObservedRead, } from "./environmentStateWriteObserve.js"; -import { plannedBody, restampedFields } from "./environmentStateWriteStamps.js"; +import { + plannedBody, + readSpellsCoarser, + readSpelledClocks, + restampedFields, +} from "./environmentStateWriteStamps.js"; import { readClock } from "./environmentStateWriteValues.js"; import type { CreateEvidence } from "./environmentStateWriteExplain.js"; import { @@ -98,6 +109,8 @@ export interface WriteDraft { readonly starts: Map; /** Collections of accepted create calls, used to check late sequence adoption. */ readonly createCollections: string[]; + /** The largest integer each echoed field held on an accepted create, which a sequence adopted later must count past. */ + readonly sentLargest: Map; /** The collection a create writes into when its own path names no state type: the one its returned id was read under. */ readonly linked?: string | undefined; /** Fields an item action's call found already at the value it sets them to. */ @@ -487,6 +500,49 @@ const outcomes = ( return { targets, target, recorded }; }; +/** The item read after an update that spells a clock the update stamped at a coarser precision, which serving stores it as. */ +const spelledReadBack = ( + context: EffectContext, + step: WriteStep, + target: { + readonly type: string; + readonly idField: string; + readonly collection: string; + readonly id: string; + }, + call: { readonly rules: readonly FieldRule[]; readonly after: JsonObject }, +): ObservedRead | undefined => { + const read = laterObservation( + searchFrom(context, step), + itemReads(context, target), + ); + if (read === undefined || context.viewOf(read.ordinal)?.shape !== "item") + return undefined; + const respelled = call.rules.some((rule) => { + if (rule.rule !== "clock") return false; + const [own, shown] = [ + ownMember(call.after, rule.field), + ownMember(read.object, rule.field), + ]; + return respelledInstant(own, shown) && readSpellsCoarser(own, shown); + }); + return respelled + ? { ordinal: read.ordinal, pointer: read.pointer, shape: "item" } + : undefined; +}; + +/** The object the plan's kept read-back shows, which serving spells every update's clocks by. */ +const keptRead = ( + search: ObservationSearch, + read: ObservedRead | undefined, +): JsonObject | undefined => + read === undefined + ? undefined + : search.withBody(read.ordinal, (body) => { + const object = body === undefined ? undefined : at(body, read.pointer); + return isObject(object) ? object : undefined; + }); + /** Build an update plan from its exemplar, or add what a later call showed. */ const buildUpdate = ( context: EffectContext, @@ -612,7 +668,7 @@ export const updateEntity = ( } const unknown = context.unknown(type, collection, id); const call = { ...callOf(step, body), body }; - const { explained, rules } = fitUpdateClocks( + const fitted = fitUpdateClocks( draft, after.object, explainUpdate({ @@ -633,12 +689,26 @@ export const updateEntity = ( references: draft.plan.rules.filter((rule) => rule.rule === "reference"), }), ); + // An action a later call shows flipping back what an earlier one set toggles it. + const { rules, later } = toggledRules( + fitted.rules, + fitted.explained.rules, + entity.fields, + draft.steady, + draft.updates, + ); + const explained = { ...fitted.explained, rules: later }; for (const member of keptOrders(call.body, after.object)) draft.kept.add(member); if (explained.unexplained.length > 0 && !unknown) { refuse(draft, step, "unexplained-change"); return recorded(); } + // What the read after the call showed, for members its answer left out that a default must agree with. + const read = laterObservation( + searchFrom(context, step), + itemReads(context, { type, idField, collection, id }), + )?.object; // Each call must agree with the plan; one on an unknown entity cannot be checked, so it adds nothing to the plan. if ( built(draft) && @@ -648,6 +718,17 @@ export const updateEntity = ( entity.fields, after.object, explained, + ) || + // A member calls left out is cleared by every one or by none. + // A reference this call or an earlier one showed counts its member as the field's, whichever came first. + defaultsDisagree(explained.body.defaults, { earlier: draft.updates }, [ + ...rules, + ...explained.rules, + ]) || + defaultsDisagree( + draft.plan.body?.defaults, + { earlier: [], sent: body, after: { ...read, ...after.object } }, + [...rules, ...explained.rules], )) ) { if (!unknown) refuse(draft, step, "unexplained-change"); @@ -656,6 +737,21 @@ export const updateEntity = ( draft.plan.rules = rules; noteFixedWidths(draft.fixedWidth, after.object); buildUpdate(context, draft, step, { after, explained, idField }); + // Serving stores the clocks it stamps as the read after the update spells them. + const readBack = spelledReadBack( + context, + step, + { type, idField, collection, id }, + { rules: explained.rules, after: after.object }, + ); + if ( + readBack !== undefined && + draft.plan.observation !== null && + draft.plan.observation.read === undefined + ) + Object.assign(draft.plan, { + observation: { ...draft.plan.observation, read: readBack }, + }); noteAmpersands(draft.ampersands, explained.rules, after.object); // Serving reads the plan's observation, so the gate does too; generated values are this call's. const written = updatedFields( @@ -684,15 +780,20 @@ export const updateEntity = ( rememberCall( context.io.retain, draft, - { before: entity.fields, after: after.object, sent: body, kept }, + { before: entity.fields, after: after.object, sent: body, kept, read }, explained.unstamped, ); - // Response reproducibility is established before the read owner adopts a stored clock spelling. - const stored = restampedFields( - searchFrom(context, step), - itemReads(context, { type, idField, collection, id }), - written.value, + // Response reproducibility is established before the read owner adopts a stored clock spelling; serving spells every update's clocks as the plan's kept read-back does, so replay does too. + const search = searchFrom(context, step); + const stored = readSpelledClocks( draft.plan.rules, + restampedFields( + search, + itemReads(context, { type, idField, collection, id }), + written.value, + draft.plan.rules, + ), + keptRead(search, draft.plan.observation?.read), ); context.io.retain( 64 + diff --git a/src/domain/analysisView/environmentStateWriteExplain.ts b/src/domain/analysisView/environmentStateWriteExplain.ts index 6b2563724e..bc8aa8ad25 100644 --- a/src/domain/analysisView/environmentStateWriteExplain.ts +++ b/src/domain/analysisView/environmentStateWriteExplain.ts @@ -522,6 +522,8 @@ export const explainUpdate = ( const rules: FieldRule[] = []; const echoed = new Set(); const unexplained: string[] = []; + // A whole-object PUT clears a member it leaves out: that value is the member's default. + const defaults: { readonly member: string; readonly value: JsonValue }[] = []; // A member the application replaces with a value it generates is accepted with any value. const generated = new Set(); let identity: string | null = null; @@ -593,8 +595,14 @@ export const explainUpdate = ( body, knownEntities, ); - if (rule === undefined) unexplained.push(field); - else rules.push(rule); + if (rule !== undefined) rules.push(rule); + else if ( + evidence.method === "PUT" && + isBlankValue(ownMember(evidence.after, field)) && + !isBlankValue(ownMember(evidence.before, field)) + ) + defaults.push({ member: field, value: evidence.after[field]! }); + else unexplained.push(field); } rules.push(...pairedStamps(evidence, rules)); // A member a reference reads names an entity, which any held one may be. @@ -614,7 +622,11 @@ export const explainUpdate = ( ); return { rules, - body: { ...shaped, identity }, + body: { + ...shaped, + identity, + ...(defaults.length === 0 ? {} : { defaults }), + }, unexplained, unstamped: sameSecondClocks(evidence), }; diff --git a/src/domain/analysisView/environmentStateWriteReferences.test.ts b/src/domain/analysisView/environmentStateWriteReferences.test.ts index 9c4d22fb22..e676f21700 100644 --- a/src/domain/analysisView/environmentStateWriteReferences.test.ts +++ b/src/domain/analysisView/environmentStateWriteReferences.test.ts @@ -116,6 +116,44 @@ describe("objects that repeat the entity a member names", () => { ).toBeUndefined(); }); + it("learns a list reference only when every element lists the same nonempty fields", () => { + const named = (id: string) => + id === "1" || id === "2" + ? [ + held("/api/tags", id, { + id, + name: id === "1" ? "Tools" : "Winter", + color: "#aa0000", + }), + ] + : []; + expect( + referenceRule( + "tags", + [ + { id: "1", name: "Tools" }, + { id: "2", name: "Winter" }, + ], + { tagIds: ["1", "2"] }, + named, + ), + ).toMatchObject({ many: true, fields: ["id", "name"] }); + expect( + referenceRule( + "tags", + [ + { id: "1", name: "Tools" }, + { id: "2", name: "Winter", color: "#aa0000" }, + ], + { tagIds: ["1", "2"] }, + named, + ), + ).toBeUndefined(); + expect( + referenceRule("tags", [{}], { tagIds: ["1"] }, named), + ).toBeUndefined(); + }); + it("refuses a call naming an entity the environment does not hold, or holds in several collections", () => { const rule: Extract = { field: "shelf", diff --git a/src/domain/analysisView/environmentStateWriteReferences.ts b/src/domain/analysisView/environmentStateWriteReferences.ts index 5d5a68525a..3bf24322ed 100644 --- a/src/domain/analysisView/environmentStateWriteReferences.ts +++ b/src/domain/analysisView/environmentStateWriteReferences.ts @@ -7,7 +7,7 @@ import { type JsonObject, type StateEntity, } from "./environmentStateRules.js"; -import type { FieldRule } from "./environmentStateWriteRules.js"; +import type { FieldRule, WriteBody } from "./environmentStateWriteRules.js"; type ReferenceRule = Extract; @@ -42,6 +42,7 @@ export const referenceRule = ( body: JsonObject, named: (id: string) => readonly StateEntity[], ): ReferenceRule | undefined => { + if (Array.isArray(value)) return listReferenceRule(field, value, body, named); if (!isObject(value) || Object.keys(value).length < 2) return undefined; const matches = new Map(); for (const [member, sent] of Object.entries(body)) { @@ -71,14 +72,101 @@ export const referenceRule = ( : undefined; }; +/** + * A created or updated list field holding, in order, the entities a sent list of ids names, as the state holds them: + * tags beside `tagIds`. The member is the one named like the field; every element must project one entity of one type. + */ +const listReferenceRule = ( + field: string, + value: readonly JsonValue[], + body: JsonObject, + named: (id: string) => readonly StateEntity[], +): ReferenceRule | undefined => { + if (value.length === 0 || !value.every(isObject)) return undefined; + // A list field is named for its items, as `tags` beside `tagIds`. + const stems = new Set([stemOf(field), stemOf(field).replace(/s$/u, "")]); + const members = Object.entries(body).filter( + ([member, sent]) => + stems.has(stemOf(member)) && + Array.isArray(sent) && + sent.length === value.length, + ); + if (members.length !== 1) return undefined; + const [member, sent] = members[0]! as [string, JsonValue[]]; + const types = new Set(); + for (const [index, element] of value.entries()) { + const id = scalarText(sent[index]); + const held = id === undefined ? [] : named(id); + const match = held.filter((entity) => + projects(element as JsonObject, entity.fields), + ); + if (match.length !== 1) return undefined; + types.add(match[0]!.type); + } + if (types.size !== 1) return undefined; + // Every recorded element must list the same fields. A union would add a key one element omitted, and an empty object lists none. + const [recorded, ...rest] = value.map((element) => + Object.keys(element as JsonObject), + ); + if ( + recorded === undefined || + recorded.length === 0 || + rest.some( + (fields) => + fields.length !== recorded.length || + fields.some((field) => !recorded.includes(field)), + ) + ) + return undefined; + return { + field, + rule: "reference", + member, + type: [...types][0]!, + fields: recorded, + many: true, + }; +}; + +/** Whether a call's member and the entity's field show a reference to nothing: a `null` id and field, or empty lists. */ +export const emptyReference = ( + sent: JsonValue | undefined, + stored: JsonValue | undefined, +): boolean => + (sent === null && stored === null) || + (Array.isArray(sent) && + sent.length === 0 && + Array.isArray(stored) && + stored.length === 0); + /** A reference's value for a call: the named entity's fields it lists, or undefined when the call does not send its member. */ export const referencedValue = ( rule: ReferenceRule, body: JsonObject, referenced: ReferencedEntity | undefined, + blank?: WriteBody["blank"], ): Result => { if (!Object.hasOwn(body, rule.member)) return ok(undefined); - const id = scalarText(body[rule.member]); + const sent = body[rule.member]!; + // A call naming nothing stores nothing, as a recorded call that sent the member `null` did. + if ( + sent === null && + blank?.some((entry) => entry.member === rule.member && entry.sent === null) + ) + return ok(null); + if (rule.many === true) { + if (!Array.isArray(sent)) + return err(`Member ${rule.member} sends no list of ids.`); + const { many: _many, ...single } = rule; + const values: JsonValue[] = []; + for (const one of sent) { + const value = referencedValue(single, { [rule.member]: one }, referenced); + if (!value.ok) return value; + values.push(value.value ?? null); + } + return ok(values); + } + const id = scalarText(sent); const held = id === undefined ? [] : (referenced?.(rule.type, id) ?? []); const [fields] = held; if (fields === undefined) diff --git a/src/domain/analysisView/environmentStateWriteRules.ts b/src/domain/analysisView/environmentStateWriteRules.ts index 3378f67be8..093bf08b5f 100644 --- a/src/domain/analysisView/environmentStateWriteRules.ts +++ b/src/domain/analysisView/environmentStateWriteRules.ts @@ -53,12 +53,13 @@ export type GeneratedRule = readonly rule: "identity"; readonly generator: "sequence" | "uuid" | "token"; } - /** One past the largest value its scope holds, and never below `start`, a value the first create took above them. */ + /** One past the largest value its scope holds, and never below `start`, a value the first create took above them; with `sent`, the value a call sends instead. */ | { readonly field: string; readonly rule: "sequence"; readonly scope: "collection" | "type"; readonly start?: number; + readonly sent?: true; } | { readonly field: string; readonly rule: "unique" } /** The value kept with each UUID at the pointers (`*` for any array index) replaced by a fresh one, as rewritten child rows are. */ @@ -123,6 +124,8 @@ export type FieldRule = readonly member: string; readonly type: string; readonly fields: string[]; + /** The member sends a list of ids, and the field holds each named entity in that order. */ + readonly many?: true; } | { readonly field: string; readonly rule: "increment" } /** An update stores `value` whenever the call sends the member, as the call that showed it did. */ @@ -150,6 +153,11 @@ export type FieldRule = readonly field: string; readonly rule: "set"; readonly value: JsonValue; + } + /** An item action flips a boolean on every call. */ + | { + readonly field: string; + readonly rule: "toggle"; }; /** Clock rules spelled as a trimmed sibling is, since an application spells its clocks alike: fields whose `values`, and every `later` call's, show no fixed width. */ diff --git a/src/domain/analysisView/environmentStateWriteStamps.ts b/src/domain/analysisView/environmentStateWriteStamps.ts index ae6f49bef9..ec0347c17a 100644 --- a/src/domain/analysisView/environmentStateWriteStamps.ts +++ b/src/domain/analysisView/environmentStateWriteStamps.ts @@ -20,6 +20,7 @@ import { containsDelimited, isUuid, readClock, + formatClock, sequenceNumber, spelledLike, type ClockFormat, @@ -246,6 +247,44 @@ export const createdNestedClock = ( return stamped ? nestedRule(field, value, members, evidence.type) : undefined; }; +/** Whether an application that answers a clock as `written` stores it as `read` spells it: both ISO, the read at a coarser precision and otherwise alike. Replay and serving respell an update's clocks by this one rule. */ +export const readSpellsCoarser = ( + written: JsonValue | undefined, + read: JsonValue | undefined, +): boolean => { + const [value, stored] = [readClock(written), readClock(read)]; + return ( + value?.format.kind === "iso" && + stored?.format.kind === "iso" && + stored.format.fraction < value.format.fraction && + same({ ...stored.format, fraction: 0 }, { ...value.format, fraction: 0 }) + ); +}; + +/** An update's fields as its application stores them: each clock at the coarser precision its plan's read-back spelled it. */ +export const readSpelledClocks = ( + rules: readonly FieldRule[], + written: JsonObject, + read: JsonObject | undefined, +): JsonObject => { + if (read === undefined) return written; + const fields = { ...written }; + for (const rule of rules) { + if (rule.rule !== "clock") continue; + const answered = ownMember(written, rule.field); + const shown = ownMember(read, rule.field); + if (!readSpellsCoarser(answered, shown)) continue; + const clock = readClock(answered)!; + const spelled = formatClock( + clock.ms, + readClock(shown)!.format, + clock.subMillisecond, + ); + if (spelled !== undefined) setOwnMember(fields, rule.field, spelled); + } + return fields; +}; + /** Whether a clock is one a request starting at `startMs` stamped, in the given format. */ export const stampedBy = ( value: JsonValue | undefined, @@ -666,7 +705,10 @@ export const pinnedEchoes = ( ); }; -/** An update's fields as stored: each nested clock as the first later read of the entity shows it restamped, else as the call showed it. */ +/** + * A write's fields as stored: each nested clock as the first later read of the entity shows it restamped, else as the + * call showed it. + */ export const restampedFields = ( search: ObservationSearch, target: Parameters[1], diff --git a/src/domain/analysisView/environmentStateWriteValues.ts b/src/domain/analysisView/environmentStateWriteValues.ts index b2135f7ae0..ce96e87bcd 100644 --- a/src/domain/analysisView/environmentStateWriteValues.ts +++ b/src/domain/analysisView/environmentStateWriteValues.ts @@ -114,18 +114,20 @@ const isoText = ( return `${pad(year, 4)}-${pad(at.getUTCMonth() + 1, 2)}-${pad(at.getUTCDate(), 2)}${separator}${pad(at.getUTCHours(), 2)}:${pad(at.getUTCMinutes(), 2)}:${pad(at.getUTCSeconds(), 2)}${digits === "" ? "" : `.${digits}`}${zone}`; }; -/** An instant in a recorded format; undefined outside its four-digit ISO year range. Fraction digits past milliseconds are zeros. */ +/** An instant in a recorded format; undefined outside its four-digit ISO year range. Fraction digits past milliseconds are `subMillisecond`'s, else zeros. */ export const formatClock = ( ms: number, format: ClockFormat, + subMillisecond = 0, ): JsonValue | undefined => { if (format.kind === "epoch") return format.unit === "s" ? Math.floor(ms / 1000) : ms; const at = new Date(ms + zoneMinutesForFormat(format.zone) * MINUTE_MS); - const digits = `${pad(at.getUTCMilliseconds(), 3)}${"0".repeat(9)}`.slice( - 0, - format.fraction, - ); + const digits = + `${pad(at.getUTCMilliseconds(), 3)}${pad(subMillisecond, 6)}`.slice( + 0, + format.fraction, + ); return isoText( at, format.separator, diff --git a/src/domain/analysisView/environmentStateWriteVerdict.ts b/src/domain/analysisView/environmentStateWriteVerdict.ts index d5834ee5d1..3ea6cd308a 100644 --- a/src/domain/analysisView/environmentStateWriteVerdict.ts +++ b/src/domain/analysisView/environmentStateWriteVerdict.ts @@ -56,13 +56,17 @@ const renders = ( ); }; -/** Whether an item action sets something, and a call showed each field it sets already at that value, so the action sets rather than toggles it. */ +/** Whether an item action sets or toggles something, and a call showed each field it sets already at that value, so it sets rather than toggles those. */ const steady = (plan: StateWritePlan, draft: WriteDraft): boolean => { if (plan.address?.by !== "action") return true; const set = plan.rules.flatMap((rule) => rule.rule === "set" ? [rule.field] : [], ); - return set.length > 0 && set.every((field) => draft.steady.has(field)); + // A toggle is learned only from calls that flipped its field both ways. + return ( + (set.length > 0 || plan.rules.some((rule) => rule.rule === "toggle")) && + set.every((field) => draft.steady.has(field)) + ); }; /** Whether a clock a call left as it was in its own second has no rule another call showed, so no call showed whether the write stamps it. */ diff --git a/src/domain/analysisView/erlangBeamView.ts b/src/domain/analysisView/erlangBeamView.ts index e2e25c2939..7f14f88ee2 100644 --- a/src/domain/analysisView/erlangBeamView.ts +++ b/src/domain/analysisView/erlangBeamView.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { erlangBeamSchema, type ErlangBeam } from "../erlang/beam.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -165,9 +165,12 @@ const selectRows = (rows: readonly Row[] | null, view: BeamView) => { /** Project retained BEAM facts without reopening files or reinterpreting chunks. */ export const projectErlangBeamView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "beam") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "beam"); + if (view === undefined) return err( analysisViewInputError([ { @@ -215,7 +218,7 @@ export const projectErlangBeamView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary, coverage: completeWithinViewCoverage(chunks.length, chunks.length), @@ -223,7 +226,7 @@ export const projectErlangBeamView = ( const selected = selectRows(parsed.value[view.collection], view); return ok({ ...shared, - kind: "beam", + kind: "beam" as const, view, beam: summary, page: { collection: view.collection, items: selected.rows }, diff --git a/src/domain/analysisView/evmInterfaceView.ts b/src/domain/analysisView/evmInterfaceView.ts index 246e525a0f..9a5148aa0a 100644 --- a/src/domain/analysisView/evmInterfaceView.ts +++ b/src/domain/analysisView/evmInterfaceView.ts @@ -1,15 +1,15 @@ import { analysisViewInputError } from "./analysisViewErrors.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { evmInterfaceSchema } from "../evm/evmInterface.js"; -import { err, ok, type Result } from "../result.js"; +import { err, ok } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -51,9 +51,12 @@ export const evmSummarySchema = z.strictObject({ /** Select already authenticated producer facts; no interpreter or provider is invoked. */ export const projectEvmInterfaceView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "evm") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "evm"); + if (view === undefined) return err( analysisViewInputError([ { @@ -96,7 +99,7 @@ export const projectEvmInterfaceView = ( const { hex: _hex, ...bytecode } = report.bytecode; const { stdout, ...diagnostics } = report.diagnostics; return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -118,7 +121,7 @@ export const projectEvmInterfaceView = ( if (view.facet === "functions") { const item = report.functions.slice(view.offset, view.offset + view.limit); return ok({ - kind: "evm", + kind: "evm" as const, view, item, ...shared, @@ -134,5 +137,7 @@ export const projectEvmInterfaceView = ( ? report.bytecode.hex : report.diagnostics[view.facet]; const page = analysisTextPage(text, view.offset, view.limit, view.facet); - return page.ok ? ok({ kind: "evm", view, ...page.value, ...shared }) : page; + return page.ok + ? ok({ kind: "evm" as const, view, ...page.value, ...shared }) + : page; }; diff --git a/src/domain/analysisView/fridaSessionStatusView.ts b/src/domain/analysisView/fridaSessionStatusView.ts index 87ee04b696..b2bf3989cb 100644 --- a/src/domain/analysisView/fridaSessionStatusView.ts +++ b/src/domain/analysisView/fridaSessionStatusView.ts @@ -1,13 +1,13 @@ import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { providerSchema } from "../evidence.js"; import { fridaSessionStatusSchema } from "../fridaSessionStatus.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -27,9 +27,10 @@ export const fridaSessionStatusViewContext = { /** Summarize captured facts without interpreting caller-authored message payloads. */ export const projectFridaSessionStatusView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary") + admitted: AdmittedView, +) => { + const view = ownedView(admitted, "summary"); + if (view === undefined) return err( analysisViewInputError([ { @@ -60,11 +61,11 @@ export const projectFridaSessionStatusView = ( ); const { messages, ...status } = report.value; return ok({ - kind: "summary", + kind: "summary" as const, view, parent_evidence_id: parent.evidenceId, parent_digest: parent.evidenceId.slice(3), - parent_operation: "frida_session_status", + parent_operation: "frida_session_status" as const, artifact: null, source_provider: parent.provider ?? null, source_parameters: parameters.data, diff --git a/src/domain/analysisView/goBinaryView.ts b/src/domain/analysisView/goBinaryView.ts index 83e2510a07..54e11cfb09 100644 --- a/src/domain/analysisView/goBinaryView.ts +++ b/src/domain/analysisView/goBinaryView.ts @@ -1,19 +1,19 @@ import { binaryMetadataArchitecture } from "../binaryMetadataArchitecture.js"; import { metadataReportSubjectFormat } from "../apple/machoTargetSelection.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { goBinarySchema, goBuildInfoSchema, goModuleMetadataSchema, goModuleSchema, } from "../go/goBinary.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -107,9 +107,12 @@ const parseReport = createAnalysisViewResultReader(goBinarySchema); /** Query the existing metadata owner's facts; never reopen or reinterpret a file. */ export const projectGoBinaryView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "go") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "go"); + if (view === undefined) return err( analysisViewInputError([ { @@ -183,7 +186,7 @@ export const projectGoBinaryView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -242,10 +245,10 @@ export const projectGoBinaryView = ( }), ); return ok({ - kind: "go", + kind: "go" as const, view, ...shared, - page: { collection: "dependencies", rows: selected.rows }, + page: { collection: "dependencies" as const, rows: selected.rows }, coverage: pageViewCoverage( view.offset, selected.rows.length, @@ -266,10 +269,10 @@ export const projectGoBinaryView = ( }), ); return ok({ - kind: "go", + kind: "go" as const, view, ...shared, - page: { collection: "settings", rows: selected.rows }, + page: { collection: "settings" as const, rows: selected.rows }, coverage: pageViewCoverage( view.offset, selected.rows.length, diff --git a/src/domain/analysisView/gpuCodeObjectsView.ts b/src/domain/analysisView/gpuCodeObjectsView.ts index 671489002b..f8d82e6b29 100644 --- a/src/domain/analysisView/gpuCodeObjectsView.ts +++ b/src/domain/analysisView/gpuCodeObjectsView.ts @@ -12,11 +12,12 @@ import { type GpuCodeObjects, } from "../gpu/gpuCodeObjects.js"; import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY, analysisViewInputError, @@ -222,9 +223,12 @@ const readGpuFacts = ( }; export const projectGpuCodeObjectsView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "gpu-code-objects") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "gpu-code-objects"); + if (view === undefined) return err( analysisViewInputError([ { @@ -258,7 +262,7 @@ export const projectGpuCodeObjectsView = ( }; if (view.kind === "summary") { return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { ...summary, limitations: shared.limitations }, @@ -312,7 +316,7 @@ export const projectGpuCodeObjectsView = ( ), ); return ok({ - kind: "gpu-code-objects", + kind: "gpu-code-objects" as const, view, ...shared, source_coverage: report.coverage, @@ -324,7 +328,7 @@ export const projectGpuCodeObjectsView = ( byte_length: artifact.byte_length, text, offset: view.offset, - offset_unit: "utf16-code-unit", + offset_unit: "utf16-code-unit" as const, }, coverage: pageViewCoverage( view.offset, @@ -419,7 +423,7 @@ export const projectGpuCodeObjectsView = ( ), ); return ok({ - kind: "gpu-code-objects", + kind: "gpu-code-objects" as const, view, ...shared, source_coverage: report.coverage, diff --git a/src/domain/analysisView/il2cppMetadataView.test.ts b/src/domain/analysisView/il2cppMetadataView.test.ts index 67d1109700..9aa9a4b9e4 100644 --- a/src/domain/analysisView/il2cppMetadataView.test.ts +++ b/src/domain/analysisView/il2cppMetadataView.test.ts @@ -1,5 +1,6 @@ import { createEmptyIl2cppMetadataFixture } from "../il2cpp/il2cppMetadata.fixture.js"; import { afterEach, expect, it, vi } from "vitest"; +import { z } from "zod"; import { il2cppMetadataParentSchema, type Il2cppMetadata, @@ -7,7 +8,22 @@ import { import { createEvidence } from "../evidence.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { jsonObjectSchema } from "../jsonValue.js"; -import { projectIl2cppMetadataView } from "./il2cppMetadataView.js"; +import type { AdmittedView, SummaryAnalysisView } from "./analysisViewShape.js"; +import { + il2cppMetadataViewRequestSchema, + projectIl2cppMetadataView as projectAdmittedIl2cppMetadataView, +} from "./il2cppMetadataView.js"; + +type Il2cppMetadataViewInput = + | SummaryAnalysisView + | z.input; + +/** Check a caller selection against the closed request, then pass the admitted view. */ +const projectIl2cppMetadataView = ( + parent: Parameters[0], + view: Il2cppMetadataViewInput, +): ReturnType => + projectAdmittedIl2cppMetadataView(parent, view satisfies AdmittedView); afterEach(() => { vi.restoreAllMocks(); @@ -638,7 +654,15 @@ const genericLinked = (): Il2cppMetadata => { it("pages generic method instances by definition, state, address, name and field", () => { const source = parent(freezeJsonSnapshot(genericLinked()), true); - const view = (filters: Record) => { + const view = ( + filters: Omit< + Extract< + Il2cppMetadataViewInput, + { collection: "generic_method_instances" } + >, + "kind" | "collection" | "offset" | "limit" + >, + ) => { const result = projectIl2cppMetadataView(source, { kind: "il2cpp-metadata", collection: "generic_method_instances", @@ -649,7 +673,15 @@ it("pages generic method instances by definition, state, address, name and field if (!result.ok) throw result.error; return result.value; }; - const items = (filters: Record) => { + const items = ( + filters: Omit< + Extract< + Il2cppMetadataViewInput, + { collection: "generic_method_instances" } + >, + "kind" | "collection" | "offset" | "limit" + >, + ) => { const value = view(filters); return "items" in value ? value.items : undefined; }; diff --git a/src/domain/analysisView/il2cppMetadataView.ts b/src/domain/analysisView/il2cppMetadataView.ts index e62d2a4fd2..1d89c1c26a 100644 --- a/src/domain/analysisView/il2cppMetadataView.ts +++ b/src/domain/analysisView/il2cppMetadataView.ts @@ -25,11 +25,12 @@ import { } from "../il2cpp/il2cppMetadata.js"; import { err, ok, type Result } from "../result.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -519,9 +520,12 @@ const selection = (view: Request, report: Report) => { /** Reuse retained facts; source indices stay distinct from filtered page offsets. */ export const projectIl2cppMetadataView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "il2cpp-metadata") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "il2cpp-metadata"); + if (view === undefined) return err( analysisViewInputError([ { @@ -653,7 +657,7 @@ export const projectIl2cppMetadataView = ( }), }))(report.binary); return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -729,7 +733,7 @@ export const projectIl2cppMetadataView = ( } } return ok({ - kind: "il2cpp-metadata", + kind: "il2cpp-metadata" as const, view, ...shared, items, diff --git a/src/domain/analysisView/interfaceBuilderView.ts b/src/domain/analysisView/interfaceBuilderView.ts index 06e132f150..2d5a32757f 100644 --- a/src/domain/analysisView/interfaceBuilderView.ts +++ b/src/domain/analysisView/interfaceBuilderView.ts @@ -15,11 +15,12 @@ import { import { err, ok, type Result } from "../result.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { selectViewRows } from "./selectViewRows.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -183,9 +184,12 @@ const summarizeInterfaceBuilder = ( /** Project retained decode_interface_builder Evidence without reopening archives. */ export const projectInterfaceBuilderView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "interface-builder") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "interface-builder"); + if (view === undefined) return err( analysisViewInputError([ { @@ -230,7 +234,7 @@ export const projectInterfaceBuilderView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, ...summarizeInterfaceBuilder(report, limitations.length), @@ -262,7 +266,7 @@ export const projectInterfaceBuilderView = ( ? selectDocumentRows(graph.nodes, view, document) : selectDocumentRows(graph.edges, view, document); return ok({ - kind: "interface-builder", + kind: "interface-builder" as const, view, items: selected.rows, ...shared, diff --git a/src/domain/analysisView/javascriptApplicationView.ts b/src/domain/analysisView/javascriptApplicationView.ts index b63898d0b8..46872962ec 100644 --- a/src/domain/analysisView/javascriptApplicationView.ts +++ b/src/domain/analysisView/javascriptApplicationView.ts @@ -8,24 +8,31 @@ import { jsonObjectSchema, type JsonValue } from "../jsonValue.js"; import type { JavaScriptApplicationAnalysisResult } from "../javascript/javascriptApplicationAnalysis.js"; import type { ApplicationNode } from "../javascript/javascriptApplicationGraphSchemas.js"; import { err, ok, type Result } from "../result.js"; +import { z } from "zod"; import type { JavaScriptModuleReference } from "./javascriptModuleReferences.js"; +import { javascriptModuleReferencesRequestSchema } from "./javascriptModuleReferences.js"; import { javascriptApplicationSourceCoverage, javascriptStaticSourceCoverageSummary, type JavaScriptApplicationSourceCoverage, } from "./javascriptApplicationCoverage.js"; -import { projectJavaScriptFacts } from "./javascriptFactsView.js"; +import { + projectJavaScriptFacts, + javascriptFactsViewRequestSchema, +} from "./javascriptFactsView.js"; import { boundedViewMessages, completeWithinViewCoverage as completeCoverage, pageViewCoverage as pageCoverage, } from "./analysisViewCoverage.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, - GroupingViewKind, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type ItemAnalysisView, + type PageAnalysisView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; const OPERATION = "inspect_analysis_view"; @@ -96,9 +103,8 @@ const modulePage = ( view: { readonly offset: number; readonly limit: number; - readonly where?: Extract< - AnalysisViewRequest, - { kind: "javascript" } + readonly where?: z.output< + typeof javascriptModuleReferencesRequestSchema >["where"]; }, ): { entries: ModuleEntry[]; total: number } => { @@ -281,7 +287,7 @@ const parentFields = ( const selectModule = ( modules: readonly ModuleEntry[], - selector: Extract["selector"], + selector: ItemAnalysisView["selector"], ): Result => { if ("node_id" in selector) { const matches = modules.filter( @@ -342,8 +348,8 @@ const selectModule = ( export const projectJavaScriptApplicationView = ( parent: AnalysisViewParent, analysis: JavaScriptApplicationAnalysisResult, - view: Exclude, -): Result => { + view: AdmittedView, +) => { if ( view.kind !== "summary" && view.kind !== "item" && @@ -356,6 +362,13 @@ export const projectJavaScriptApplicationView = ( "JavaScript application Evidence supports summary, module item/page, typed module references or selected JavaScript facts.", ), ); + assertOwnedView< + | SummaryAnalysisView + | ItemAnalysisView + | PageAnalysisView + | z.output + | z.output + >(view, "summary", "item", "page", "javascript", "javascript-facts"); const sourceCoverage = javascriptApplicationSourceCoverage(analysis); const { fields: shared, limitationCount } = parentFields( parent, @@ -366,7 +379,7 @@ export const projectJavaScriptApplicationView = ( const { rows, total } = projectJavaScriptFacts(analysis.graph.nodes, view); return ok({ ...shared, - kind: "javascript-facts", + kind: "javascript-facts" as const, view, rows, source_coverage: sourceCoverage, @@ -378,7 +391,7 @@ export const projectJavaScriptApplicationView = ( const rows = entries.map(moduleReference); return ok({ ...shared, - kind: "javascript", + kind: "javascript" as const, view, rows, source_coverage: sourceCoverage, @@ -393,7 +406,7 @@ export const projectJavaScriptApplicationView = ( 0, ); return ok({ - kind: "summary", + kind: "summary" as const, view, summary: { input_path: analysis.input_path, @@ -438,7 +451,7 @@ export const projectJavaScriptApplicationView = ( const selected = selectModule(modules, view.selector); if (!selected.ok) return selected; return ok({ - kind: "item", + kind: "item" as const, view, item: moduleItem(selected.value.node, selected.value.path), coverage: completeCoverage(1, modules.length), @@ -458,7 +471,7 @@ export const projectJavaScriptApplicationView = ( path, })); return ok({ - kind: "page", + kind: "page" as const, view, items, coverage: pageCoverage(view.offset, items.length, total), diff --git a/src/domain/analysisView/javascriptRoutesView.ts b/src/domain/analysisView/javascriptRoutesView.ts index cabb87dd7c..f28569c133 100644 --- a/src/domain/analysisView/javascriptRoutesView.ts +++ b/src/domain/analysisView/javascriptRoutesView.ts @@ -21,11 +21,8 @@ import { type JavaScriptRouteInventorySource, } from "../javascript/javascriptRouteInventory.js"; import { javaScriptRouteMechanismSchema } from "../javascript/javascriptRouteFacts.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { assertOwnedView, type AdmittedView } from "./analysisViewShape.js"; import { ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY, analysisViewInputError, @@ -319,10 +316,14 @@ const captureIdentity = z.object({ /** Historical projection: no source file reads, browser connection or reanalysis. */ export const projectJavaScriptRoutesView = ( parent: AnalysisViewParent, - selected: AnalysisViewRequest, -): Result => { + selected: AdmittedView, +) => { if (selected.kind !== "javascript-routes") return err(inputError("Select a javascript-routes view.")); + assertOwnedView>( + selected, + "javascript-routes", + ); const operation = parent.operation; if ( operation !== "analyze_javascript_application" && @@ -452,7 +453,7 @@ export const projectJavaScriptRoutesView = ( item: Record; sources: readonly string[]; }, - ): Result => { + ) => { const rows = candidates(source, ids).filter(predicate), items: Record[] = [], refs = new Map(); diff --git a/src/domain/analysisView/machoView.test.ts b/src/domain/analysisView/machoView.test.ts index 6674bc10d6..40f650a2cf 100644 --- a/src/domain/analysisView/machoView.test.ts +++ b/src/domain/analysisView/machoView.test.ts @@ -140,6 +140,36 @@ const report = { sections: coverage([]), }, ]), + encryption: coverage([ + { + architecture: "arm64", + cpu_type: 16777228, + cpu_subtype: 0, + container_file_offset: 0, + size: 4096, + status: "not-encrypted", + reason: null, + commands: [ + { + kind: "LC_ENCRYPTION_INFO_64", + index: 1, + cmdsize: 24, + cryptoff: 16384, + cryptsize: 0, + cryptid: 0, + pad: 0, + }, + ], + range: { + content: "plaintext", + slice_start: 16384, + slice_end: 16384, + container_start: 16384, + container_end: 16384, + mapping: { status: "mapped", fragments: [], sections: [] }, + }, + }, + ]), provenance: [ { tool: "nm", @@ -295,11 +325,12 @@ describe("Mach-O analysis views", () => { imports: { returned: 3, total: null, exhaustive: false }, exports: { returned: 1, total: 1, exhaustive: true }, segments: { returned: 1, total: 1, exhaustive: true }, + encryption: { returned: 1, total: 1, exhaustive: true }, }, provenance_count: 1, limitation_count: 3, }, - coverage: { status: "complete-within-view", examined: 10, total: 10 }, + coverage: { status: "complete-within-view", examined: 11, total: 11 }, limitations: [ "Imports and exports combine dyld_info and nm.", "Parent limitation.", @@ -329,7 +360,7 @@ describe("Mach-O analysis views", () => { "nm output may omit stripped symbols.", ); // Rows whose keys include an earlier row shape's keys stay whole. - for (const facet of ["architectures", "segments"] as const) { + for (const facet of ["architectures", "segments", "encryption"] as const) { const page = projectAnalysisView(parent(), { kind: "macho", facet, @@ -367,6 +398,23 @@ describe("Mach-O analysis views", () => { }); }); + it("keeps encryption unknown for Evidence retained before it was reported", () => { + const { encryption: _omitted, ...legacy } = report; + const projected = projectAnalysisView(parent(legacy), { kind: "summary" }); + if (!projected.ok) throw projected.error; + expect(projected.value).toMatchObject({ + summary: { + collections: { + encryption: { returned: 0, total: null, exhaustive: false }, + }, + }, + unknowns: [expect.stringContaining("exhaustive imports, encryption;")], + }); + expect(projected.value.limitations).toContain( + "This inspect_macho Evidence predates encryption reporting; slice encryption status is unknown.", + ); + }); + it("keeps a missing subject unknown instead of inventing identity", () => { const projected = projectAnalysisView(parent(report, null), { kind: "summary", diff --git a/src/domain/analysisView/machoView.ts b/src/domain/analysisView/machoView.ts index 99fb309716..6c471e5880 100644 --- a/src/domain/analysisView/machoView.ts +++ b/src/domain/analysisView/machoView.ts @@ -3,14 +3,18 @@ import { z } from "zod"; import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { isMachOFormat } from "../binaryTargetTypes.js"; -import { inspectMachoSchema } from "../native/nativeInspection.js"; +import { + inspectMachoSchema, + retainedInspectMachoSchema, +} from "../native/nativeInspection.js"; import { err, ok, type Result } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -25,6 +29,7 @@ const MACHO_COLLECTIONS = [ "imports", "exports", "segments", + "encryption", ] as const; type MachoCollection = (typeof MACHO_COLLECTIONS)[number]; @@ -51,6 +56,7 @@ export const machoViewRows = { imports: exactRows(shape.imports.shape.items), exports: exactRows(shape.exports.shape.items), segments: exactRows(shape.segments.shape.items), + encryption: exactRows(shape.encryption.shape.items), provenance: exactRows(shape.provenance), }; @@ -90,7 +96,7 @@ type MachoReport = z.output; const parseMachoReport = ( parent: AnalysisViewParent, ): Result => { - const parsed = parseAnalysisViewResult(inspectMachoSchema, parent); + const parsed = parseAnalysisViewResult(retainedInspectMachoSchema, parent); if (!parsed.ok) return parsed; if (parent.subject != null && !isMachOFormat(parent.subject.format)) return err( @@ -150,9 +156,12 @@ const summarizeMacho = ( /** Project retained inspect_macho Evidence without rerunning native tools. */ export const projectMachoView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "macho") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "macho"); + if (view === undefined) return err( analysisViewInputError([ { @@ -198,7 +207,7 @@ export const projectMachoView = ( parent.limitations, ); return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, limitations, @@ -255,6 +264,8 @@ export const projectMachoView = ( return ok(page("exports", report.exports.items)); case "segments": return ok(page("segments", report.segments.items)); + case "encryption": + return ok(page("encryption", report.encryption.items)); case "provenance": return ok(page("provenance", report.provenance)); } diff --git a/src/domain/analysisView/managedArtifactView.ts b/src/domain/analysisView/managedArtifactView.ts index 0e64e4c30d..5dcb92b1d5 100644 --- a/src/domain/analysisView/managedArtifactView.ts +++ b/src/domain/analysisView/managedArtifactView.ts @@ -1,18 +1,18 @@ import { analysisViewInputError } from "./analysisViewErrors.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { managedArtifactInspectionSchema, managedMemberInspectionSchema, } from "../managed/managedArtifact.js"; -import { err, ok, type Result } from "../result.js"; +import { err, ok } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -72,9 +72,12 @@ export const managedViewIdentityUnknowns = ( /** Project authenticated metadata without reading a path or invoking a provider. */ export const projectManagedArtifactView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "managed") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "managed"); + if (view === undefined) return err( analysisViewInputError([ { @@ -116,7 +119,7 @@ export const projectManagedArtifactView = ( }; const total = references.length + resources.length + attributes.length; return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { ...summary, reported_counts, identity_scope }, @@ -126,7 +129,7 @@ export const projectManagedArtifactView = ( const collection = report[view.collection]; const item = collection.slice(view.offset, view.offset + view.limit); return ok({ - kind: "managed", + kind: "managed" as const, view, item, ...shared, diff --git a/src/domain/analysisView/managedMemberView.test.ts b/src/domain/analysisView/managedMemberView.test.ts index 7bafab84a8..e6d8f401f4 100644 --- a/src/domain/analysisView/managedMemberView.test.ts +++ b/src/domain/analysisView/managedMemberView.test.ts @@ -107,7 +107,7 @@ it("intersects exact filters before paging and preserves identity while omitting request, ); if (!result.ok) throw result.error; - if (result.value.kind !== "managed-members") throw new Error("Wrong view"); + if (!("page" in result.value)) throw new Error("Wrong view"); expect(result.value.page).toEqual({ collection: "methods", source_count: 4, @@ -161,7 +161,7 @@ it("keeps unresolved exact-filter candidates unknown under complete and partial }), ); if (!result.ok) throw result.error; - if (result.value.kind !== "managed-members") throw new Error("Wrong view"); + if (!("page" in result.value)) throw new Error("Wrong view"); expect(result.value).toMatchObject({ source_coverage: { state }, coverage: { total: 1 }, @@ -220,7 +220,7 @@ it("distinguishes empty selectors from omitted selectors and pages projections w expect(full.value).toMatchObject({ page: { collection: "methods", rows: report.methods.slice(1, 3) }, }); - if (full.value.kind !== "managed-members") throw new Error("Wrong view"); + if (!("page" in full.value)) throw new Error("Wrong view"); expect(full.value.page.rows).toEqual(report.methods.slice(1, 3)); }); diff --git a/src/domain/analysisView/managedMemberView.ts b/src/domain/analysisView/managedMemberView.ts index 1888c9afa5..f11c4b04b0 100644 --- a/src/domain/analysisView/managedMemberView.ts +++ b/src/domain/analysisView/managedMemberView.ts @@ -1,20 +1,22 @@ import { analysisViewInputError } from "./analysisViewErrors.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { managedMemberInspectionSchema, type ManagedMemberInspection, } from "../managed/managedArtifact.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, } from "./analysisViewCoverage.js"; +import { z } from "zod"; +import { managedMembersViewRequestSchema } from "./managedMemberViewSchemas.js"; import { managedViewIdentityUnknowns } from "./managedArtifactView.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; @@ -275,8 +277,8 @@ const summarizeMembers = ( /** Project authenticated member Evidence without reading a path or invoking a provider. */ export const projectManagedMemberView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "managed-members") return err( analysisViewInputError([ @@ -288,6 +290,9 @@ export const projectManagedMemberView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "managed-members"); const parsed = parseMemberReport(parent); if (!parsed.ok) return parsed; const report = parsed.value; @@ -317,7 +322,7 @@ export const projectManagedMemberView = ( 0, ); return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary, @@ -345,7 +350,7 @@ export const projectManagedMemberView = ( `${page.unresolved_filter_rows} source rows have unknown requested names or declaring types and may match the filters; the matched total excludes them.`, ); return ok({ - kind: "managed-members", + kind: "managed-members" as const, view, page, ...shared, diff --git a/src/domain/analysisView/modelArtifactView.ts b/src/domain/analysisView/modelArtifactView.ts index e71d6a9815..9ddb513059 100644 --- a/src/domain/analysisView/modelArtifactView.ts +++ b/src/domain/analysisView/modelArtifactView.ts @@ -1,4 +1,7 @@ -import { modelTypeRowSchema } from "../model/modelArtifactQuery.js"; +import { + modelArtifactViewRequestSchema, + modelTypeRowSchema, +} from "../model/modelArtifactQuery.js"; import { z } from "zod"; import { modelArtifactSchema, @@ -14,11 +17,12 @@ import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { isImmutableJsonSnapshot } from "../immutableJson.js"; import type { JsonValue } from "../jsonValue.js"; import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, boundedViewMessages, @@ -180,8 +184,8 @@ const read = (parent: AnalysisViewParent): Result => { /** Historical pages never reopen weights or report a scoped header digest as full-file identity. */ export const projectModelArtifactView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "model-artifact") return err( analysisViewInputError([ @@ -193,6 +197,9 @@ export const projectModelArtifactView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "model-artifact"); const parsed = read(parent); if (!parsed.ok) return parsed; const { @@ -225,7 +232,7 @@ export const projectModelArtifactView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -318,7 +325,7 @@ export const projectModelArtifactView = ( return result; }); return ok({ - kind: "model-artifact", + kind: "model-artifact" as const, view, ...shared, source: report.artifact, diff --git a/src/domain/analysisView/nativeExportsView.ts b/src/domain/analysisView/nativeExportsView.ts index d0d8d1ee97..1d9be08030 100644 --- a/src/domain/analysisView/nativeExportsView.ts +++ b/src/domain/analysisView/nativeExportsView.ts @@ -5,13 +5,13 @@ import { elfNativeExportsSchema as elf, type NativeExports, } from "../native/nativeExports.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -88,8 +88,8 @@ const readReport = createAnalysisViewResultReader(nativeExportsSchema); /** Read selected export facts without reopening the artifact or reinterpreting its tables. */ export const projectNativeExportsView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { const invalid = (message: string) => err( analysisViewInputError([ @@ -100,6 +100,9 @@ export const projectNativeExportsView = ( return invalid( "Native export Evidence supports summary and native-exports views.", ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "native-exports"); const parsed = readReport(parent); if (!parsed.ok) return parsed; const report = parsed.value; @@ -131,7 +134,7 @@ export const projectNativeExportsView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary, coverage: completeWithinViewCoverage(exports.length, exports.length), @@ -158,7 +161,7 @@ export const projectNativeExportsView = ( ); return ok({ ...shared, - kind: "native-exports", + kind: "native-exports" as const, view, exports: summary, items: selected.rows, @@ -246,7 +249,7 @@ export const projectNativeExportsView = ( ); return ok({ ...shared, - kind: "native-exports", + kind: "native-exports" as const, view, exports: summary, items: selected.rows, diff --git a/src/domain/analysisView/nativeFunctionMatchingView.ts b/src/domain/analysisView/nativeFunctionMatchingView.ts index e6870322aa..4d5d8cfe3d 100644 --- a/src/domain/analysisView/nativeFunctionMatchingView.ts +++ b/src/domain/analysisView/nativeFunctionMatchingView.ts @@ -16,11 +16,12 @@ import { } from "./analysisViewCoverage.js"; import { err, ok, type Result } from "../result.js"; import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import type { JsonValue } from "../jsonValue.js"; const matchRow = nativeFunctionMatchSchema.extend({ @@ -259,9 +260,13 @@ const selectedItems = ( /** Query immutable matching facts without copying full alias groups into selected pages. */ export const projectNativeFunctionMatchingView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "function-matches") + admitted: AdmittedView, +) => { + const view = ownedView< + | SummaryAnalysisView + | z.output + >(admitted, "summary", "function-matches"); + if (view === undefined) return err( analysisViewInputError([ { @@ -283,7 +288,7 @@ export const projectNativeFunctionMatchingView = ( const shared = sharedContext(parent, entry.report); if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: context, @@ -292,7 +297,7 @@ export const projectNativeFunctionMatchingView = ( const rows = filterRows(entry, view), items = selectedItems(entry, view, rows); return ok({ - kind: "function-matches", + kind: "function-matches" as const, view, ...shared, ...context, diff --git a/src/domain/analysisView/nativeFunctionView.ts b/src/domain/analysisView/nativeFunctionView.ts index dea093806e..8ce34ee53f 100644 --- a/src/domain/analysisView/nativeFunctionView.ts +++ b/src/domain/analysisView/nativeFunctionView.ts @@ -4,12 +4,14 @@ import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import type { FunctionDossier } from "../native/functionDossier.js"; import { jsonValueSchema } from "../jsonValue.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type NativeFacetAnalysisView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -62,7 +64,7 @@ const parentFields = ( const nativeRows = ( dossier: FunctionDossier, facet: Exclude< - Extract["facet"], + NativeFacetAnalysisView["facet"], "procedure" | "pseudocode" | "native_api" | "value_flow_summary" >, ): readonly unknown[] | undefined => { @@ -196,11 +198,11 @@ const summarizeDossier = (dossier: FunctionDossier) => { }; const summaryView = ( - view: Extract, + view: SummaryAnalysisView, dossier: FunctionDossier, common: ReturnType, -): UnsignedAnalysisView => ({ - kind: "summary", +) => ({ + kind: "summary" as const, view, summary: summarizeDossier(dossier), coverage: completeWithinViewCoverage(1, 1), @@ -216,12 +218,17 @@ const summaryView = ( export const projectNativeFunctionView = ( parent: AnalysisViewParent, dossier: FunctionDossier, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "native" && view.kind !== "summary") return err( viewError("analyze_function Evidence requires a native or summary view."), ); + assertOwnedView( + view, + "summary", + "native", + ); const artifact = parent.artifact; if (!artifact) return err( @@ -239,8 +246,8 @@ export const projectNativeFunctionView = ( const availability = facetAvailability(dossier, view.facet); if (availability?.available === false) common.unknowns = uniqueSorted([...common.unknowns, availability.reason]); - const singleton = (item: unknown): UnsignedAnalysisView => ({ - kind: "native", + const singleton = (item: unknown) => ({ + kind: "native" as const, view, item: jsonValueSchema.parse(item), coverage: completeWithinViewCoverage(1, 1), @@ -286,7 +293,7 @@ export const projectNativeFunctionView = ( if (view.facet === "pseudocode") { const page = analysisTextPage(dossier.pseudocode, view.offset, view.limit); return page.ok - ? ok({ kind: "native", view, ...page.value, ...common }) + ? ok({ kind: "native" as const, view, ...page.value, ...common }) : page; } const rows = nativeRows(dossier, view.facet); @@ -305,7 +312,7 @@ export const projectNativeFunctionView = ( ); const items = rows.slice(view.offset, view.offset + view.limit); return ok({ - kind: "native", + kind: "native" as const, view, item: jsonValueSchema.parse(items), coverage: pageViewCoverage(view.offset, items.length, rows.length), diff --git a/src/domain/analysisView/nativeInventoryView.ts b/src/domain/analysisView/nativeInventoryView.ts index 2ef5dedccc..01c22de8c0 100644 --- a/src/domain/analysisView/nativeInventoryView.ts +++ b/src/domain/analysisView/nativeInventoryView.ts @@ -6,13 +6,13 @@ import { isNativeInventoryOperation, nativeInventoryRowsSchemas, } from "../native/nativeInventory.js"; -import { err, ok, type Result } from "../result.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -68,8 +68,8 @@ const readStrings = createAnalysisViewResultReader( /** Project already completed provider observations without another provider read or sort. */ export const projectNativeInventoryView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if ( !isNativeInventoryOperation(parent.operation) || (view.kind !== "summary" && view.kind !== "native-inventory") @@ -84,6 +84,9 @@ export const projectNativeInventoryView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "native-inventory"); const strings = parent.operation === "list_strings" || parent.operation === "search_strings"; @@ -113,16 +116,16 @@ export const projectNativeInventoryView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary: { collection: strings - ? "strings" + ? ("strings" as const) : parent.operation === "list_names" - ? "names" - : "procedures", + ? ("names" as const) + : ("procedures" as const), row_count: rows.length, - discovery_coverage: "unknown", + discovery_coverage: "unknown" as const, selectable_fields: fieldSchema.options, }, coverage: completeWithinViewCoverage(rows.length, rows.length), @@ -137,7 +140,7 @@ export const projectNativeInventoryView = ( const selected = rows.slice(view.offset, view.offset + view.limit); return ok({ ...shared, - kind: "native-inventory", + kind: "native-inventory" as const, view, rows: selected.map((record, index) => ({ source_index: view.offset + index, diff --git a/src/domain/analysisView/nativeLoadImageView.ts b/src/domain/analysisView/nativeLoadImageView.ts index c8908989de..c4975e00dc 100644 --- a/src/domain/analysisView/nativeLoadImageView.ts +++ b/src/domain/analysisView/nativeLoadImageView.ts @@ -10,13 +10,13 @@ import { } from "../native/nativeLoadImage.js"; import { nativeAddressObservationSchema } from "../native/nativeAddress.js"; import { jsonValueSchema } from "../jsonValue.js"; -import { err, ok, type Result } from "../result.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -199,9 +199,12 @@ const selectRows = (source: readonly (Row | string)[], view: PageRequest) => { /** Query the owner's parsed historical observations without reopening the target. */ export const projectNativeLoadImageView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "native-load-image") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "native-load-image"); + if (view === undefined) return err( analysisViewInputError([ { @@ -257,7 +260,7 @@ export const projectNativeLoadImageView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary: { verification: smallVerification, @@ -289,7 +292,7 @@ export const projectNativeLoadImageView = ( const page = selectRows(selected ?? [], view); return ok({ ...shared, - kind: "native-load-image", + kind: "native-load-image" as const, verification_status: report.status, view, items: nativeLoadImageViewItemsSchema.parse({ diff --git a/src/domain/analysisView/nativeObservationView.ts b/src/domain/analysisView/nativeObservationView.ts index a8c9d85ceb..a532242c2d 100644 --- a/src/domain/analysisView/nativeObservationView.ts +++ b/src/domain/analysisView/nativeObservationView.ts @@ -1,4 +1,5 @@ import type { AnalysisError } from "../analysisErrorBase.js"; +import { z } from "zod"; import { evidenceSubjectIdentity } from "../evidence.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { jsonValueSchema } from "../jsonValue.js"; @@ -9,11 +10,12 @@ import { nativeObservationSourceSchemas, type NativeObservation, } from "../native/nativeObservation.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { @@ -21,6 +23,8 @@ import { completeWithinViewCoverage, } from "./analysisViewCoverage.js"; import { + nativeContextViewRequestSchema, + nativeObservationViewRequestSchema, nativeSourceContextSchema, type NativeSourceContext, } from "./nativeObservationSchemas.js"; @@ -166,7 +170,7 @@ const observationRows = ( }; const contextRows = ( source: NativeSourceContext, - view: Extract, + view: z.output, ) => { switch (view.facet) { case "overview": { @@ -245,8 +249,8 @@ const contextRows = ( /** Select recorded native facts without repeating provider profiles or complete caveat inventories. */ export const projectNativeObservationView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if ( view.kind !== "summary" && view.kind !== "native-observation" && @@ -262,6 +266,11 @@ export const projectNativeObservationView = ( }, ]), ); + assertOwnedView< + | SummaryAnalysisView + | z.output + | z.output + >(view, "summary", "native-observation", "native-context"); const source = readSource(parent); if (!source.ok) return source; const context = readContext(parent, source.value); @@ -296,7 +305,7 @@ export const projectNativeObservationView = ( const { total, ...page } = contextRows(context.value, view); return ok({ ...common, - kind: "native-context", + kind: "native-context" as const, ...page, parent_operation: source.value.operation, coverage: @@ -321,6 +330,6 @@ export const projectNativeObservationView = ( coverage: pageViewCoverage(selection.offset, page.items.rows.length, total), }; return view.kind === "summary" - ? ok({ ...observation, kind: "summary", view }) - : ok({ ...observation, kind: "native-observation", view }); + ? ok({ ...observation, kind: "summary" as const, view }) + : ok({ ...observation, kind: "native-observation" as const, view }); }; diff --git a/src/domain/analysisView/pcapCaptureView.ts b/src/domain/analysisView/pcapCaptureView.ts index 5ca03a1875..cc7f05b9b7 100644 --- a/src/domain/analysisView/pcapCaptureView.ts +++ b/src/domain/analysisView/pcapCaptureView.ts @@ -4,11 +4,12 @@ import { pcapEvidenceSchemas } from "../pcap/pcapAnalysis.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; import type { AnalysisError } from "../analysisErrorBase.js"; import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -253,9 +254,12 @@ const selectRows = ( /** Historical packet facts are paged without reopening the source or rerunning Wireshark. */ export const projectPcapCaptureView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "pcap") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "pcap"); + if (view === undefined) return err( analysisViewInputError([ { @@ -284,7 +288,7 @@ export const projectPcapCaptureView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: summarize(capture), @@ -303,14 +307,15 @@ export const projectPcapCaptureView = ( ); if (!text.ok) return text; return ok({ - kind: "pcap", + kind: "pcap" as const, view, ...shared, source_coverage: capture.coverage, page: { - facet: "diagnostics", + facet: "diagnostics" as const, text: text.value.item.text, - status: value === undefined ? "unknown" : "retained", + status: + value === undefined ? ("unknown" as const) : ("retained" as const), }, coverage: text.value.coverage, unknowns: @@ -325,7 +330,7 @@ export const projectPcapCaptureView = ( const selected = selectRows(capture, view); return selected.ok ? ok({ - kind: "pcap", + kind: "pcap" as const, view, ...shared, source_coverage: capture.coverage, diff --git a/src/domain/analysisView/pythonPycView.ts b/src/domain/analysisView/pythonPycView.ts index 1cd0381c08..8676db842e 100644 --- a/src/domain/analysisView/pythonPycView.ts +++ b/src/domain/analysisView/pythonPycView.ts @@ -3,13 +3,13 @@ import { pythonPycSchema, type PythonPycCodeObject, } from "../python/pythonPyc.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -62,9 +62,12 @@ const readReport = createAnalysisViewResultReader(pythonPycSchema); /** Project code-constant references from historical facts, without reopening the pyc. */ export const projectPythonPycView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "python-pyc") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "python-pyc"); + if (view === undefined) return err( analysisViewInputError([ { @@ -96,7 +99,7 @@ export const projectPythonPycView = ( if (view.kind === "summary") return ok({ ...shared, - kind: "summary", + kind: "summary" as const, view, summary, coverage: completeWithinViewCoverage( @@ -132,7 +135,7 @@ export const projectPythonPycView = ( ); return ok({ ...shared, - kind: "python-pyc", + kind: "python-pyc" as const, view, pyc: summary, items: selected.rows, diff --git a/src/domain/analysisView/rustBinaryView.ts b/src/domain/analysisView/rustBinaryView.ts index 5354e2441e..027d17eeda 100644 --- a/src/domain/analysisView/rustBinaryView.ts +++ b/src/domain/analysisView/rustBinaryView.ts @@ -1,26 +1,29 @@ import { binaryMetadataArchitecture } from "../binaryMetadataArchitecture.js"; import { metadataReportSubjectFormat } from "../apple/machoTargetSelection.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { rustBinarySchema, rustAuditableMetadataSchema, rustcCommitSchema, type RustBinary, } from "../rust/rustBinary.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import { + createAnalysisViewResultReader, + type AnalysisViewParent, +} from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; +import { rustBinaryViewRequestSchema } from "../rust/rustBinaryQuery.js"; import { boundedViewMessages, completeWithinViewCoverage, pageViewCoverage, } from "./analysisViewCoverage.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; -import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { selectViewRows } from "./selectViewRows.js"; const count = z.number().int().nonnegative(); @@ -108,8 +111,8 @@ const summarize = ( /** Query owner-normalized facts without reopening a source or walking dependency paths. */ export const projectRustBinaryView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if (view.kind !== "summary" && view.kind !== "rust") return err( analysisViewInputError([ @@ -120,6 +123,9 @@ export const projectRustBinaryView = ( }, ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(view, "summary", "rust"); const parsed = parseReport(parent); if (!parsed.ok) return parsed; const report = parsed.value; @@ -177,7 +183,7 @@ export const projectRustBinaryView = ( const summary = summarize(report); if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary, @@ -214,11 +220,11 @@ export const projectRustBinaryView = ( }), ); return ok({ - kind: "rust", + kind: "rust" as const, view, ...shared, rust: summary, - page: { collection: "packages", rows: selected.rows }, + page: { collection: "packages" as const, rows: selected.rows }, coverage: pageViewCoverage( view.offset, selected.rows.length, @@ -246,11 +252,11 @@ export const projectRustBinaryView = ( }), ); return ok({ - kind: "rust", + kind: "rust" as const, view, ...shared, rust: summary, - page: { collection: "edges", rows: selected.rows }, + page: { collection: "edges" as const, rows: selected.rows }, coverage: pageViewCoverage( view.offset, selected.rows.length, diff --git a/src/domain/analysisView/scenarioCaptureRecords.test.ts b/src/domain/analysisView/scenarioCaptureRecords.test.ts index f63645f6ad..8b05021e83 100644 --- a/src/domain/analysisView/scenarioCaptureRecords.test.ts +++ b/src/domain/analysisView/scenarioCaptureRecords.test.ts @@ -86,6 +86,26 @@ describe("scenarioCaptureRecords", () => { ); }); + it("records a hidden request body as absent only when the request names no Content-Type", () => { + const toggle = { + method: "PUT", + url: "/api/entries/1/bookmark", + status: 204, + requestBodyHidden: true, + } as const; + const projected = scenarioCaptureRecords( + scenarioCaptureFixture([ + { ...toggle, requestHeaderNames: ["accept", "authorization"] }, + { ...toggle, requestHeaderNames: ["accept", "Content-Type"] }, + ]), + "manifest", + ); + expect(projected.records.map((entry) => entry.reported)).toMatchObject([ + { request: { bodySize: 0 } }, + { request: { bodySize: -1 } }, + ]); + }); + it("leaves a transaction that cannot be attributed to one request and response unrepresented", () => { const capture = scenarioCaptureFixture(SCENARIO_EXCHANGES.slice(1, 2)); const response = capture.events.items.find( diff --git a/src/domain/analysisView/scenarioCaptureRecords.ts b/src/domain/analysisView/scenarioCaptureRecords.ts index 2a5cc3f094..59e7294293 100644 --- a/src/domain/analysisView/scenarioCaptureRecords.ts +++ b/src/domain/analysisView/scenarioCaptureRecords.ts @@ -17,7 +17,7 @@ import type { JsonValue } from "../jsonValue.js"; import { err, ok, type Result } from "../result.js"; import type { WebNetworkCaptureRecord } from "../webNetworkCapture.js"; import { analysisViewResultInputError } from "./analysisViewParent.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import type { WebNetworkProjectionAccumulator } from "./WebNetworkProjectionAccumulator.js"; import { ENTRY_BYTES, textBytes } from "./webNetworkRouteTemplates.js"; @@ -88,6 +88,14 @@ export const scenarioContentMediaType = ( captured(content)?.media_type ?? headerValue(content?.headers, "content-type"); +/** Whether a request sent no body: Playwright exposed none and the request names no Content-Type, which the bodies browsers send carry. */ +const unsentBody = ( + request: RequestEvent, + content: ContentEvent | undefined, +): boolean => + content?.body.state === "not_exposed" && + !request.header_names.some((name) => name.toLowerCase() === "content-type"); + /** The request object HAR readers expect; a redacted URL is withheld so no path is templated from it. */ const reportedRequest = ( request: RequestEvent, @@ -97,9 +105,15 @@ const reportedRequest = ( const body = purpose === "manifest" ? captured(content) : undefined; const text = body === undefined ? undefined : utf8Text(body); const mimeType = scenarioContentMediaType(content); + const size = + body !== undefined && !body.redacted + ? body.bytes + : purpose === "manifest" && unsentBody(request, content) + ? 0 + : -1; return { method: request.method, - bodySize: body !== undefined && !body.redacted ? body.bytes : -1, + bodySize: size, url: request.url.redacted ? null : request.url.url, ...(text === undefined || mimeType === undefined ? {} diff --git a/src/domain/analysisView/sourceToBundleView.ts b/src/domain/analysisView/sourceToBundleView.ts index 38a2f616b5..9443b52b67 100644 --- a/src/domain/analysisView/sourceToBundleView.ts +++ b/src/domain/analysisView/sourceToBundleView.ts @@ -4,13 +4,13 @@ import { sourceToBundleComparisonResultSchema, type SourceToBundleComparisonResult, } from "../javascript/sourceToBundleComparisonSchemas.js"; -import { err, ok, type Result } from "../result.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; import { @@ -109,9 +109,12 @@ const columns = ( /** Page facts owned by a complete comparison; page exhaustion does not upgrade source coverage. */ export const projectSourceToBundleView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "source-to-bundle") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "source-to-bundle"); + if (view === undefined) return err( analysisViewInputError([ { @@ -153,7 +156,7 @@ export const projectSourceToBundleView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: context, @@ -234,7 +237,7 @@ export const projectSourceToBundleView = ( } } return ok({ - kind: "source-to-bundle", + kind: "source-to-bundle" as const, view, ...shared, ...context, diff --git a/src/domain/analysisView/wasmArtifactView.ts b/src/domain/analysisView/wasmArtifactView.ts index de6b65fb31..4d16729658 100644 --- a/src/domain/analysisView/wasmArtifactView.ts +++ b/src/domain/analysisView/wasmArtifactView.ts @@ -1,18 +1,18 @@ import { analysisViewInputError } from "./analysisViewErrors.js"; import { z } from "zod"; -import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; -import { err, ok, type Result } from "../result.js"; +import { err, ok } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; import { wasmArtifactSchema, wasmArtifactMatchesSubject, } from "../wasm/wasmArtifact.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + ownedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, @@ -74,9 +74,12 @@ export const wasmSummarySchema = z.strictObject({ /** Read the exact retained WABT forms with source identity and explicit uncertainty. */ export const projectWasmArtifactView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { - if (view.kind !== "summary" && view.kind !== "wasm") + admitted: AdmittedView, +) => { + const view = ownedView< + SummaryAnalysisView | z.output + >(admitted, "summary", "wasm"); + if (view === undefined) return err( analysisViewInputError([ { @@ -131,7 +134,7 @@ export const projectWasmArtifactView = ( }; if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -173,7 +176,7 @@ export const projectWasmArtifactView = ( ]), ); return ok({ - kind: "wasm", + kind: "wasm" as const, view, item: report.tool_profile, coverage: completeWithinViewCoverage(1, 1), @@ -188,13 +191,13 @@ export const projectWasmArtifactView = ( const text = view.facet === "wat" ? report.wat.text : report[view.facet]; const page = analysisTextPage(text, view.offset, view.limit, view.facet); return page.ok - ? ok({ kind: "wasm", view, ...page.value, ...shared }) + ? ok({ kind: "wasm" as const, view, ...page.value, ...shared }) : page; } const rows = report[view.facet]; const item = rows.slice(view.offset, view.offset + view.limit); return ok({ - kind: "wasm", + kind: "wasm" as const, view, item, coverage: pageViewCoverage(view.offset, item.length, rows.length), diff --git a/src/domain/analysisView/webBundleView.ts b/src/domain/analysisView/webBundleView.ts index 75b55fe6f2..fadf8b78e5 100644 --- a/src/domain/analysisView/webBundleView.ts +++ b/src/domain/analysisView/webBundleView.ts @@ -9,11 +9,12 @@ import { type WebBundleAnalysis, } from "../webBundleAnalysis.js"; import { recordedBrowserOriginSchema } from "../browserObservation.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { boundedViewMessages, completeWithinViewCoverage, @@ -119,8 +120,8 @@ const choose = ( /** Historical projection of completed analyze_web_bundle Evidence. */ export const projectWebBundleView = ( parent: AnalysisViewParent, - selected: AnalysisViewRequest, -): Result => { + selected: AdmittedView, +) => { if (selected.kind !== "web-bundle" && selected.kind !== "summary") return err( analysisViewInputError([ @@ -132,6 +133,9 @@ export const projectWebBundleView = ( ]), ); + assertOwnedView< + SummaryAnalysisView | z.output + >(selected, "summary", "web-bundle"); if (parent.operation !== "analyze_web_bundle") return err( analysisViewInputError([ @@ -216,8 +220,8 @@ export const projectWebBundleView = ( if (selected.kind === "summary") { const summary = buildSummary(); - const result: UnsignedAnalysisView = { - kind: "summary", + const result = { + kind: "summary" as const, view: selected, ...shared, summary, @@ -234,8 +238,8 @@ export const projectWebBundleView = ( const view = selected; if (view.collection === "summary") { const summary = buildSummary(); - const result: UnsignedAnalysisView = { - kind: "web-bundle", + const result = { + kind: "web-bundle" as const, view, ...shared, completeness: report.completeness, @@ -261,7 +265,7 @@ export const projectWebBundleView = ( rows: readonly T[], matches: ((row: T, source_index: number) => boolean) | undefined, project: (row: T, source_index: number) => Item, - ): Result => { + ) => { const baseEnvelope = { ...shared, kind: "web-bundle" as const, @@ -323,7 +327,7 @@ export const projectWebBundleView = ( } const coverage = pageViewCoverage(view.offset, items.length, total); - const result: UnsignedAnalysisView = { + const result = { ...shared, kind: "web-bundle" as const, view, diff --git a/src/domain/analysisView/webNetworkApiSurfaceView.ts b/src/domain/analysisView/webNetworkApiSurfaceView.ts index 40da068fa3..d1bd0224c7 100644 --- a/src/domain/analysisView/webNetworkApiSurfaceView.ts +++ b/src/domain/analysisView/webNetworkApiSurfaceView.ts @@ -1,96 +1,39 @@ import { z } from "zod"; import { AnalysisUnsupportedTargetError } from "../analysisErrorCore.js"; -import type { AnalysisError } from "../analysisErrorBase.js"; -import { err, ok, type Result } from "../result.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import { err, ok } from "../result.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + ownedView, + type AdmittedView, +} from "./analysisViewShape.js"; import { WebNetworkApiSurfaceAccumulator } from "./WebNetworkApiSurfaceAccumulator.js"; import { admitHarCaptureParent } from "./WebNetworkProjectionAccumulator.js"; -import { ROUTE_RULES } from "./webNetworkRouteTemplates.js"; -export const apiSurfaceViewSchema = z.strictObject({ - kind: z.literal("api-surface"), - offset: z.int().nonnegative().default(0), - limit: z.int().positive().default(50), - include_query_parameter_names: z - .boolean() - .optional() - .describe( - "Include distinct observed query parameter names and their original record ordinals; names do not establish required parameters or a complete API schema.", - ), -}); -const ruleSchema = z.enum(ROUTE_RULES); -const groupSchema = z.strictObject({ - method: z.string(), - origin: z.string(), - path_template: z.string(), - templated_segments: z.array( - z.strictObject({ - segment_index: z.int().nonnegative(), - rule: ruleSchema, - basis: z.literal("inferred"), - observed_values: z.array(z.string()), - }), - ), - record_ordinals: z.array(z.int().nonnegative()), - statuses: z.array(z.number()), - content_types: z.array(z.string()), - query_parameter_names: z - .array( - z.strictObject({ - name: z.string(), - record_ordinals: z.array(z.int().nonnegative()), - }), - ) - .optional(), - flags: z.array( - z.enum(["single-sample", "mixed-statuses", "mixed-content-types"]), - ), -}); -export const apiSurfaceFields = { - records: z.strictObject({ - examined: z.int().nonnegative(), - total: z.int().nonnegative(), - excluded: z.int().nonnegative(), - }), - templated_groups: z - .int() - .nonnegative() - .describe( - "Groups across every page that carry at least one templated segment.", - ), - groups: z.array(groupSchema), - excluded_records: z.array( - z.strictObject({ - ordinal: z.int().nonnegative(), - reason: z.enum([ - "url-redacted", - "url-unparseable", - "request-unavailable", - "method-unavailable", - ]), - }), - ), -}; +import { apiSurfaceFields, apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; + +export { apiSurfaceFields, apiSurfaceViewSchema }; /** Create the budget and query-name policy shared by admission and projection. */ export const createApiSurfaceAccumulator = ( - view: AnalysisViewRequest, -): WebNetworkApiSurfaceAccumulator => - new WebNetworkApiSurfaceAccumulator( - undefined, - view.kind === "api-surface" && view.include_query_parameter_names === true, + view: AdmittedView, +): WebNetworkApiSurfaceAccumulator => { + const selected = ownedView>( + view, + "api-surface", ); + return new WebNetworkApiSurfaceAccumulator( + undefined, + selected?.include_query_parameter_names === true, + ); +}; /** Group retained HAR request lines; format matches do not establish handler identity. */ export const projectWebNetworkApiSurfaceView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, + view: AdmittedView, accumulator = createApiSurfaceAccumulator(view), -): Result => { +) => { if (view.kind !== "api-surface") return err( new AnalysisUnsupportedTargetError( @@ -99,6 +42,7 @@ export const projectWebNetworkApiSurfaceView = ( `View ${view.kind} is unsupported for parent operation ${parent.operation}; api-surface requires inspect_web_network_capture.`, ), ); + assertOwnedView>(view, "api-surface"); const admitted = admitHarCaptureParent(parent, "api-surface", accumulator); if (!admitted.ok) return admitted; const capture = admitted.value; @@ -124,10 +68,10 @@ export const projectWebNetworkApiSurfaceView = ( } const page = accumulator.page(view.offset, view.limit); return ok({ - kind: "api-surface", + kind: "api-surface" as const, view, parent_evidence_id: parent.evidenceId, - parent_operation: "inspect_web_network_capture", + parent_operation: "inspect_web_network_capture" as const, parent_digest: parent.evidenceId.slice(3), artifact: { path: capture.artifact.path, sha256: capture.artifact.sha256 }, records: { diff --git a/src/domain/analysisView/webNetworkCaptureView.ts b/src/domain/analysisView/webNetworkCaptureView.ts index 90555e317e..83c737ea9b 100644 --- a/src/domain/analysisView/webNetworkCaptureView.ts +++ b/src/domain/analysisView/webNetworkCaptureView.ts @@ -1,4 +1,7 @@ -import { projectProtobufCapturePage } from "./protobufCaptureView.js"; +import { + projectProtobufCapturePage, + protobufViewRequestSchema, +} from "./protobufCaptureView.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { createHash } from "node:crypto"; import { z } from "zod"; @@ -17,17 +20,19 @@ import { type WebNetworkCaptureRecord, type WebNetworkCapture, } from "../webNetworkCapture.js"; -import type { - AnalysisViewParent, - AnalysisViewRequest, - UnsignedAnalysisView, -} from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { + assertOwnedView, + type AdmittedView, + type SummaryAnalysisView, +} from "./analysisViewShape.js"; import { completeWithinViewCoverage, pageViewCoverage, } from "./analysisViewCoverage.js"; import { qlogSummarySchema, + qlogViewRequestSchema, summarizeQlogCapture, projectQlogCapturePage, } from "./qlogCaptureView.js"; @@ -468,8 +473,8 @@ const parseCaptureParent = ( /** Page immutable retained facts; no capture file, provider or network is consulted. */ export const projectWebNetworkCaptureView = ( parent: AnalysisViewParent, - view: AnalysisViewRequest, -): Result => { + view: AdmittedView, +) => { if ( view.kind !== "summary" && view.kind !== "capture" && @@ -486,6 +491,12 @@ export const projectWebNetworkCaptureView = ( }, ]), ); + assertOwnedView< + | SummaryAnalysisView + | z.output + | z.output + | z.output + >(view, "summary", "capture", "protobuf", "qlog"); const parsed = parseCaptureParent(parent); if (!parsed.ok) return parsed; const report = parsed.value; @@ -519,7 +530,7 @@ export const projectWebNetworkCaptureView = ( }); if (view.kind === "summary") return ok({ - kind: "summary", + kind: "summary" as const, view, ...shared, summary: { @@ -558,7 +569,7 @@ export const projectWebNetworkCaptureView = ( const projected = projectQlogCapturePage(report.qlog, view); return projected.ok ? ok({ - kind: "qlog", + kind: "qlog" as const, view, ...shared, ...projected.value, @@ -571,7 +582,7 @@ export const projectWebNetworkCaptureView = ( } const records = report.records.slice(view.offset, view.offset + view.limit); return ok({ - kind: "capture", + kind: "capture" as const, view, ...shared, items: captureViewItemsSchema.parse( diff --git a/src/domain/apple/dyldPaths.ts b/src/domain/apple/dyldPaths.ts index 9209de02c1..63c7cd4a1b 100644 --- a/src/domain/apple/dyldPaths.ts +++ b/src/domain/apple/dyldPaths.ts @@ -12,6 +12,11 @@ const MAX_SYMLINK_HOPS = 32; export type PathLookup = | { readonly kind: "file"; readonly path: string } + | { + readonly kind: "undetermined"; + readonly path: string; + readonly targetHex: string; + } | { readonly kind: "absent" | "escapes" | "not-file" }; /** @@ -40,6 +45,8 @@ export const resolveTreePath = async ( const entry = await view.entry(path); if (entry === undefined) return { kind: "absent" }; if (entry.kind === "symlink") { + if (entry.target === null) + return { kind: "undetermined", path, targetHex: entry.targetHex }; hops += 1; if (hops > MAX_SYMLINK_HOPS) return { kind: "absent" }; if (entry.target.startsWith("/")) return { kind: "escapes" }; diff --git a/src/domain/apple/dylibResolution.ts b/src/domain/apple/dylibResolution.ts index 5850a4253e..3b15b485ed 100644 --- a/src/domain/apple/dylibResolution.ts +++ b/src/domain/apple/dylibResolution.ts @@ -132,7 +132,12 @@ export const machoImageSlices = ( /** One observed path below the analyzed root. Symlink targets are not followed by the view. */ export type DylibTreeEntry = | { readonly kind: "file" | "directory" } - | { readonly kind: "symlink"; readonly target: string }; + | { readonly kind: "symlink"; readonly target: string } + | { + readonly kind: "symlink"; + readonly target: null; + readonly targetHex: string; + }; /** Read-only, lazily probed view of the analyzed root. Paths are root-relative. */ export interface DylibTreeView { @@ -264,6 +269,7 @@ interface ProcessContext { readonly searchOverrides: readonly DyldSearchOverride[]; readonly environmentRoots: Set; readonly unknownPlatformCandidates: Set; + readonly undecodableSymlinkTargets: Map; readonly implicitMacOSPlatformCandidates: Set; readonly platforms: readonly number[]; readonly executable: string | null; @@ -359,6 +365,10 @@ const evaluateCandidate = async ( ); if (lookup.kind === "escapes") return { ...base, outcome: "escapes-target", resolved_path: null }; + if (lookup.kind === "undetermined") { + context.undecodableSymlinkTargets.set(lookup.path, lookup.targetHex); + return { ...base, outcome: "undetermined", resolved_path: null }; + } if (lookup.kind !== "file") return { ...base, outcome: "absent", resolved_path: null }; const facts = await imageFacts(context, lookup.path); @@ -663,6 +673,7 @@ const projectTraceResult = (input: { readonly withoutArchitecture: readonly string[]; readonly environmentRoots: ReadonlySet; readonly unknownPlatformCandidates: ReadonlySet; + readonly undecodableSymlinkTargets: ReadonlyMap; readonly implicitMacOSPlatformCandidates: ReadonlySet; readonly rootEnumeration: DylibRootEnumeration | undefined; }): DylibTrace => { @@ -695,6 +706,7 @@ const projectTraceResult = (input: { input.withoutArchitecture.length === 0 && input.environmentRoots.size === 0 && input.unknownPlatformCandidates.size === 0 && + input.undecodableSymlinkTargets.size === 0 && input.rootEnumeration?.status !== "unknown" ? "complete" : "partial", @@ -704,6 +716,11 @@ const projectTraceResult = (input: { limitations: [ ...DYLIB_RESOLUTION_LIMITATIONS, ...(input.rootEnumeration?.limitations ?? []), + ...(input.undecodableSymlinkTargets.size === 0 + ? [] + : [ + `Symlink targets cannot be represented as UTF-8; their paths and stored target bytes (hex) are ${JSON.stringify(Object.fromEntries(input.undecodableSymlinkTargets))}. Affected dependency resolution remains undetermined.`, + ]), ...(input.environmentRoots.size === 0 ? [] : [ @@ -744,6 +761,7 @@ export const traceDylibLoading = async ( const withoutArchitecture: string[] = []; const environmentRoots = new Set(); const unknownPlatformCandidates = new Set(); + const undecodableSymlinkTargets = new Map(); const implicitMacOSPlatformCandidates = new Set(); for (const root of request.roots) { const facts = await view.image(root); @@ -785,6 +803,7 @@ export const traceDylibLoading = async ( searchOverrides, environmentRoots, unknownPlatformCandidates, + undecodableSymlinkTargets, implicitMacOSPlatformCandidates, platforms: slice.platforms, executable, @@ -806,6 +825,7 @@ export const traceDylibLoading = async ( withoutArchitecture, environmentRoots, unknownPlatformCandidates, + undecodableSymlinkTargets, implicitMacOSPlatformCandidates, rootEnumeration: request.rootEnumeration, }); diff --git a/src/domain/apple/machoEncryption.ts b/src/domain/apple/machoEncryption.ts new file mode 100644 index 0000000000..acb6c46beb --- /dev/null +++ b/src/domain/apple/machoEncryption.ts @@ -0,0 +1,121 @@ +import { z } from "zod"; + +const hexAddress = z.string().regex(/^0x[a-f0-9]+$/u); +const offset = z.number().int().min(0); + +/** One `LC_ENCRYPTION_INFO(_64)` command with its fields as recorded. */ +export const machoEncryptionCommandSchema = z.object({ + kind: z.enum(["LC_ENCRYPTION_INFO", "LC_ENCRYPTION_INFO_64"]), + index: offset.describe("Load-command index within the slice."), + cmdsize: offset, + cryptoff: offset + .nullable() + .describe( + "File offset of the range from the start of the slice; null when cmdsize is too short to hold it.", + ), + cryptsize: offset.nullable(), + cryptid: offset + .nullable() + .describe( + "Encryption system identifier as recorded; loader.h defines 0 as not encrypted.", + ), + pad: offset + .nullable() + .describe("LC_ENCRYPTION_INFO_64 padding word; null for the 32-bit form."), +}); + +const fragmentSchema = z.object({ + slice_start: offset, + slice_end: offset, + segment: z + .string() + .nullable() + .describe("File-backed segment holding these bytes; null when unmapped."), + vm_start: hexAddress + .nullable() + .describe( + "VM address of slice_start; null when unmapped or when the segment has no VM size.", + ), + vm_end: hexAddress.nullable(), +}); + +const mappingSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("mapped"), + fragments: z.array(fragmentSchema), + sections: z + .array(z.object({ segment: z.string(), section: z.string() })) + .describe( + "File-backed sections the range overlaps; zero-fill and empty sections are excluded.", + ), + }), + z.object({ status: z.literal("unknown"), reason: z.string() }), +]); + +/** The range one encryption command names, in every coordinate it maps to. */ +export const machoEncryptedRangeSchema = z.object({ + content: z + .enum(["ciphertext", "plaintext"]) + .describe( + "ciphertext when cryptid is nonzero; analysis derived from those bytes does not describe program code.", + ), + slice_start: offset, + slice_end: offset.describe( + "Exclusive end offset from the start of the slice.", + ), + container_start: offset, + container_end: offset.describe( + "Exclusive end offset from the start of the input file.", + ), + mapping: mappingSchema, +}); + +export const machoEncryptionStatusSchema = z.enum([ + "encrypted", + "not-encrypted", + "absent", + "malformed", + "unknown", +]); + +/** Load-command encryption state of one Mach-O slice. */ +export const machoSliceEncryptionSchema = z.object({ + architecture: z + .string() + .nullable() + .describe( + "lipo slice name matched by CPU type, subtype and offset; null when unmatched.", + ), + cpu_type: offset.nullable(), + cpu_subtype: offset.nullable(), + container_file_offset: offset, + size: offset, + status: machoEncryptionStatusSchema.describe( + "absent only when every load command was read; malformed and unknown carry a reason.", + ), + reason: z.string().nullable(), + commands: z + .array(machoEncryptionCommandSchema) + .describe( + "Encryption load commands in table order; a slice with more than two keeps the first two and states the count in reason.", + ), + range: machoEncryptedRangeSchema.nullable(), +}); + +export type MachoEncryptionCommand = z.infer< + typeof machoEncryptionCommandSchema +>; +export type MachoEncryptedRange = z.infer; +export type MachoSliceEncryption = z.infer; + +/** Every slice's encryption state, or why the file could not be enumerated. */ +export type MachoEncryptionFacts = + | { readonly status: "not-mach-o" } + | { + readonly status: "malformed" | "unsupported"; + readonly reason: string; + } + | { + readonly status: "parsed"; + readonly slices: readonly MachoSliceEncryption[]; + }; diff --git a/src/domain/artifactFormatMetadata.ts b/src/domain/artifactFormatMetadata.ts index 88e843c0c2..f246562867 100644 --- a/src/domain/artifactFormatMetadata.ts +++ b/src/domain/artifactFormatMetadata.ts @@ -122,6 +122,14 @@ export type PyInstallerEntryMetadata = z.infer< typeof pyInstallerEntryMetadataSchema >; +/** Ordinary .NET bundle manifest facts, not proof of managed assembly validity. */ +export const dotnetBundleEntryMetadataSchema = z.strictObject({ + kind: z.literal("dotnet-bundle-entry"), + major_version: uint32, + minor_version: uint32, + file_type: z.number().int().min(0).max(255), +}); + /** * Node.js single-executable blob facts. Lengths follow the Node 24 64-bit * little-endian `size_t` reader. The fuse sentinel is a separate carrier scan. @@ -488,6 +496,62 @@ export const uImageEntryMetadataSchema = z.strictObject({ }); export type UImageEntryMetadata = z.infer; +const bunHex = z + .string() + .regex(/^[0-9a-f]+$/u) + .nullable(); + +/** + * Container facts for one Bun standalone module graph. `flags` stays the raw + * u32 because Bun 1.3 and 1.4 reuse the low bits for different meanings. + */ +export const bunStandaloneMetadataSchema = z.strictObject({ + kind: z.literal("bun-standalone"), + layout_revision: z.enum(["bun-1.3.3", "bun-1.3.9"]), + carrier: z.enum([ + "mach-o-section", + "pe-section", + "elf-section", + "appended-trailer", + ]), + payload_offset: offset, + payload_length: offset, + module_count: uint32, + entry_point_id: uint32, + entry_point_source_name: z.string().nullable(), + entry_point_logical_path: z.string().nullable(), + compile_exec_argv: z.string().nullable(), + compile_exec_argv_hex: bunHex, + flags: uint32, +}); +export type BunStandaloneMetadata = z.infer; + +/** One stored region of a Bun module. Siblings repeat the module's source facts. */ +export const bunStandaloneEntryMetadataSchema = z.strictObject({ + kind: z.literal("bun-standalone-entry"), + index: uint32, + region: z.enum(["contents", "sourcemap", "bytecode", "module-info"]), + source_name: z.string().nullable(), + source_name_hex: bunHex, + encoding: uint32, + encoding_name: z.enum(["binary", "latin1", "utf8", "utf16"]).nullable(), + loader: uint32, + loader_name: z.string().nullable(), + module_format: uint32, + module_format_name: z.enum(["none", "esm", "cjs"]).nullable(), + side: uint32, + side_name: z.enum(["server", "client"]).nullable(), + entry_point: z.boolean(), + sourcemap_bytes: offset, + bytecode_bytes: offset, + module_info_bytes: offset, + bytecode_origin: z.string().nullable(), + bytecode_origin_hex: bunHex, +}); +export type BunStandaloneEntryMetadata = z.infer< + typeof bunStandaloneEntryMetadataSchema +>; + export const artifactFormatMetadataSchema = z.discriminatedUnion("kind", [ godotPckMetadataSchema, godotPckEntryMetadataSchema, @@ -495,6 +559,9 @@ export const artifactFormatMetadataSchema = z.discriminatedUnion("kind", [ unityFsEntryMetadataSchema, pyInstallerMetadataSchema, pyInstallerEntryMetadataSchema, + dotnetBundleEntryMetadataSchema, + bunStandaloneMetadataSchema, + bunStandaloneEntryMetadataSchema, nodeSeaMetadataSchema, nodeSeaEntryMetadataSchema, hexRecordsMetadataSchema, diff --git a/src/domain/artifactGraph.ts b/src/domain/artifactGraph.ts index e09b370abd..f81474bc45 100644 --- a/src/domain/artifactGraph.ts +++ b/src/domain/artifactGraph.ts @@ -55,7 +55,9 @@ const artifactKindSchema = z.enum([ ]); /** Recognized storage or application formats; unknown bytes remain explicit. */ -const artifactFormatSchema = z.enum([ +export const ARTIFACT_FORMAT_IDS = [ + "dotnet-bundle", + "bun-standalone", "dmg", "zip", "pkg", @@ -93,7 +95,11 @@ const artifactFormatSchema = z.enum([ "esp-image", "fit", "unknown", -]); +] as const; + +export type ArtifactFormatId = (typeof ARTIFACT_FORMAT_IDS)[number]; + +const artifactFormatSchema = z.enum(ARTIFACT_FORMAT_IDS); /** Formats established from bytes or a virtual directory tree, independent of path roles. */ const artifactContentFormatSchema = artifactFormatSchema.extract([ diff --git a/src/domain/artifactInspection.ts b/src/domain/artifactInspection.ts index 6f9adff4b5..c649940f54 100644 --- a/src/domain/artifactInspection.ts +++ b/src/domain/artifactInspection.ts @@ -423,6 +423,24 @@ const directoryProbes = ( const probesForFormat = ( format: ArtifactInventoryResult["manifest"]["root_format"], ): ArtifactInspectionResult["next_probes"] => { + if (format === "bun-standalone") + return [ + { + operation: "extract_artifact", + authority: "artifact-bytes", + rationale: + "Extract the stored module regions into a fresh owned directory, then inspect compatible source separately. Bytecode and source-map regions remain stored evidence; they are not decoded or executed.", + }, + ]; + if (format === "dotnet-bundle") + return [ + { + operation: "extract_artifact", + rationale: + "Extract members into a fresh owned directory, then inspect candidate files separately; bundle declarations do not establish their format.", + authority: "artifact-bytes", + }, + ]; if (["asar", "javascript-bundle"].includes(format)) return [ { diff --git a/src/domain/binaryTarget.ts b/src/domain/binaryTarget.ts index 4646bcc8d7..16d75ac880 100644 --- a/src/domain/binaryTarget.ts +++ b/src/domain/binaryTarget.ts @@ -5,6 +5,7 @@ import type { MipsElfMetadata, PeCodeViewRecord, UniversalMachOSlice, + UniversalMachOSliceIdentity, } from "./binaryTargetTypes.js"; import { err, ok, type Result } from "./result.js"; import { @@ -76,6 +77,7 @@ export type ExecutableMetadata = readonly architecture: HeaderArchitecture; readonly availableArchitectures: readonly HeaderArchitecture[]; readonly universalSlice: UniversalMachOSlice; + readonly universalSlices: readonly UniversalMachOSliceIdentity[]; } | { readonly format: "pe"; @@ -301,6 +303,15 @@ const parseFatMachO = ( architecture: selected.architecture, availableArchitectures: declaredFamilies(declarations), universalSlice: { offset: selected.offset, size: selected.size }, + universalSlices: + header.state === "parsed" + ? header.slices.map(({ offset, size, cpuType, cpuSubtype }) => ({ + offset, + size, + cpuType, + cpuSubtype, + })) + : [], }); }; diff --git a/src/domain/binaryTargetTypes.ts b/src/domain/binaryTargetTypes.ts index 341f9e9e92..888d411e5e 100644 --- a/src/domain/binaryTargetTypes.ts +++ b/src/domain/binaryTargetTypes.ts @@ -116,6 +116,12 @@ export interface UniversalMachOSlice { readonly size: number; } +/** FAT-table identity; CPU family alone cannot identify a universal slice. */ +export interface UniversalMachOSliceIdentity extends UniversalMachOSlice { + readonly cpuType: number; + readonly cpuSubtype: number; +} + interface BinaryTargetIdentity { readonly path: string; readonly sourcePath?: string; @@ -200,6 +206,8 @@ export type BinaryTarget = readonly format: "mach-o-universal"; /** Selected slice range a provider must analyze instead of the container. */ readonly universalSlice: UniversalMachOSlice; + /** Exact validated FAT declarations, when supplied by target admission. */ + readonly universalSlices?: readonly UniversalMachOSliceIdentity[]; readonly executableRole?: never; readonly managed?: never; }) diff --git a/src/domain/browserScenarioDiffEvidence.ts b/src/domain/browserScenarioDiffEvidence.ts index d450de383a..f10f1b5d10 100644 --- a/src/domain/browserScenarioDiffEvidence.ts +++ b/src/domain/browserScenarioDiffEvidence.ts @@ -7,6 +7,7 @@ import type { import type { RetainedBrowserScenarioCapture } from "./browserScenarioCapture.js"; import { fingerprintScenarioNetworkTransactions, + ScenarioNetworkFingerprintError, scenarioNetworkTransactions, scenarioNetworkCoverageReason, } from "./browserScenarioNetworkComparison.js"; @@ -163,8 +164,7 @@ export const prepareScenarioEvents = ( for (const [index, node] of nodes.entries()) { const step = networkByStep.get(node.step_index) ?? []; const fingerprint = fingerprints[index]; - if (fingerprint === undefined) - throw new Error("Network comparison node has no fingerprint"); + if (fingerprint === undefined) throw new ScenarioNetworkFingerprintError(); step.push(fingerprint); networkByStep.set(node.step_index, step); } diff --git a/src/domain/browserScenarioNetworkComparison.ts b/src/domain/browserScenarioNetworkComparison.ts index 187cf86d8e..e94cbe4605 100644 --- a/src/domain/browserScenarioNetworkComparison.ts +++ b/src/domain/browserScenarioNetworkComparison.ts @@ -5,6 +5,17 @@ import { digestCanonicalValue } from "./canonicalDigest.js"; import { normalizeScenarioValue } from "./browserScenarioNormalization.js"; import { indexBrowserScenarioTransactions } from "./browserScenarioTransactions.js"; +/** A comparison node lost the fingerprint its producer should have stored. */ +export class ScenarioNetworkFingerprintError extends Error { + readonly _tag = "ScenarioNetworkFingerprintError"; + readonly code = "missing_fingerprint" as const; + + constructor() { + super("missing_fingerprint"); + this.name = "ScenarioNetworkFingerprintError"; + } +} + /** Numeric links are temporary local indices, never durable transaction identity. */ export const scenarioNetworkTransactionSchema = z.strictObject({ events: z.array(z.unknown()), @@ -241,8 +252,7 @@ export const fingerprintScenarioNetworkTransactions = ( } return nodes.map((_, index) => { const fingerprint = fingerprints.get(index); - if (fingerprint === undefined) - throw new Error("Network comparison node has no fingerprint"); + if (fingerprint === undefined) throw new ScenarioNetworkFingerprintError(); return fingerprint; }); }; diff --git a/src/domain/canonicalDigest.test.ts b/src/domain/canonicalDigest.test.ts index 09b67c3a69..649e8b0fdf 100644 --- a/src/domain/canonicalDigest.test.ts +++ b/src/domain/canonicalDigest.test.ts @@ -4,7 +4,10 @@ import { fc, it } from "@fast-check/vitest"; import canonicalize from "canonicalize"; import { describe, expect } from "vitest"; -import { digestCanonicalValue } from "./canonicalDigest.js"; +import { + CanonicalDigestError, + digestCanonicalValue, +} from "./canonicalDigest.js"; const legacyDigest = (value: unknown): string => { const encoded = canonicalize(value); @@ -52,8 +55,27 @@ describe("incremental canonical digest", () => { const cyclic: Record = {}; cyclic.self = cyclic; const recursiveJson = { toJSON: (): unknown => recursiveJson }; - for (const value of [NaN, Infinity, -Infinity, 1n, cyclic, recursiveJson]) - expect(() => digestCanonicalValue(value)).toThrow(); + for (const [value, code] of [ + [Number.NaN, "nan"], + [Number.POSITIVE_INFINITY, "infinity"], + [Number.NEGATIVE_INFINITY, "infinity"], + ] as const) { + expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError); + try { + digestCanonicalValue(value); + } catch (cause) { + expect(cause).toMatchObject({ _tag: "CanonicalDigestError", code }); + } + } + for (const value of [cyclic, recursiveJson]) { + expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError); + try { + digestCanonicalValue(value); + } catch (cause) { + expect(cause).toMatchObject({ code: "circular" }); + } + } + expect(() => digestCanonicalValue(1n)).toThrow(); }); it("preserves canonical bytes across large keys, repeated leaves and buffer flushes", () => { diff --git a/src/domain/canonicalDigest.ts b/src/domain/canonicalDigest.ts index 5e7467cc95..f14ed309a1 100644 --- a/src/domain/canonicalDigest.ts +++ b/src/domain/canonicalDigest.ts @@ -38,22 +38,30 @@ export const digestCanonicalValue = ( type Emit = (part: string) => void; +/** Canonical-JSON refusal. The code is the stable identity. */ +export class CanonicalDigestError extends Error { + readonly _tag = "CanonicalDigestError"; + + constructor(readonly code: "nan" | "infinity" | "circular") { + super(code); + this.name = "CanonicalDigestError"; + } +} + const emitCanonical = ( value: unknown, emit: Emit, ancestors: Set, ): boolean => { if (typeof value === "number" && !Number.isFinite(value)) - throw new Error( - Number.isNaN(value) ? "NaN is not allowed" : "Infinity is not allowed", - ); + throw new CanonicalDigestError(Number.isNaN(value) ? "nan" : "infinity"); if (value === null || typeof value !== "object") { const encoded = JSON.stringify(value); if (encoded === undefined) return false; emit(encoded); return true; } - if (ancestors.has(value)) throw new Error("Circular reference detected"); + if (ancestors.has(value)) throw new CanonicalDigestError("circular"); ancestors.add(value); try { if ("toJSON" in value && typeof value.toJSON === "function") { diff --git a/src/domain/canonicalJsonDigestSteps.test.ts b/src/domain/canonicalJsonDigestSteps.test.ts index a15893fdc2..c51d1a296a 100644 --- a/src/domain/canonicalJsonDigestSteps.test.ts +++ b/src/domain/canonicalJsonDigestSteps.test.ts @@ -1,7 +1,10 @@ import { fc, it } from "@fast-check/vitest"; import { describe, expect } from "vitest"; import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js"; -import { digestCanonicalValue } from "./canonicalDigest.js"; +import { + CanonicalDigestError, + digestCanonicalValue, +} from "./canonicalDigest.js"; import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; const complete = (steps: Iterator): Value => { @@ -78,7 +81,22 @@ describe("cooperative canonical JSON digest", () => { sparse.length = 2; const cycle: { [key: string]: JsonValue } = {}; cycle.self = cycle; - for (const value of [sparse, Number.NaN, cycle]) - expect(() => complete(canonicalJsonDigestSteps(value))).toThrow(); + expect(() => complete(canonicalJsonDigestSteps(sparse))).toThrow(); + expect(() => complete(canonicalJsonDigestSteps(Number.NaN))).toThrow( + CanonicalDigestError, + ); + expect(() => complete(canonicalJsonDigestSteps(cycle))).toThrow( + CanonicalDigestError, + ); + try { + complete(canonicalJsonDigestSteps(Number.NaN)); + } catch (cause) { + expect(cause).toMatchObject({ code: "nan" }); + } + try { + complete(canonicalJsonDigestSteps(cycle)); + } catch (cause) { + expect(cause).toMatchObject({ code: "circular" }); + } }); }); diff --git a/src/domain/canonicalJsonDigestSteps.ts b/src/domain/canonicalJsonDigestSteps.ts index 35cbb84913..2b7183289d 100644 --- a/src/domain/canonicalJsonDigestSteps.ts +++ b/src/domain/canonicalJsonDigestSteps.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { CanonicalDigestError } from "./canonicalDigest.js"; import type { JsonValue } from "./jsonValue.js"; type DigestFrame = @@ -137,14 +138,12 @@ export function* canonicalJsonDigestSteps( stack.push({ kind: "string", value: item, index: 0 }); } else if (item === null || typeof item !== "object") { if (typeof item === "number" && !Number.isFinite(item)) - throw new Error( - Number.isNaN(item) - ? "NaN is not allowed" - : "Infinity is not allowed", + throw new CanonicalDigestError( + Number.isNaN(item) ? "nan" : "infinity", ); emit(JSON.stringify(item)); } else { - if (ancestors.has(item)) throw new Error("Circular reference detected"); + if (ancestors.has(item)) throw new CanonicalDigestError("circular"); ancestors.add(item); if (Array.isArray(item)) { emit("["); diff --git a/src/domain/evidenceErrors.ts b/src/domain/evidenceErrors.ts index 167de2a6b7..56b844b7a2 100644 --- a/src/domain/evidenceErrors.ts +++ b/src/domain/evidenceErrors.ts @@ -33,6 +33,15 @@ export class EvidenceIntegrityError extends AnalysisError { /** A valid snapshot belongs to a different target, provider, or analysis profile. */ export class AnalysisSnapshotMismatchError extends EvidenceIntegrityError {} +/** A resource session cannot import or export a binary analysis snapshot. */ +export class ResourceSnapshotMismatchError extends AnalysisSnapshotMismatchError { + constructor(readonly operation: "import" | "export") { + super( + "Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.", + ); + } +} + /** A session Evidence reference is missing or has the wrong semantic identity. */ export class EvidenceReferenceError extends EvidenceIntegrityError { constructor( @@ -60,6 +69,7 @@ export class EvidenceFileError extends AnalysisError { readonly published: PublishedEvidenceFileObservation | undefined; /** Schema issues relative to the selected file's JSON root, never the request envelope. */ readonly contentIssues: readonly AnalysisInputIssue[] | undefined; + readonly systemCode: "EFBIG" | "ENOSPC" | "EDQUOT" | undefined; constructor( readonly operation: "read" | "write", @@ -70,6 +80,7 @@ export class EvidenceFileError extends AnalysisError { | "invalid-json" | "missing" | "path-limit" + | "storage-limit" | "symlink-traversal" | "io", options?: AnalysisErrorOptions & { @@ -77,6 +88,7 @@ export class EvidenceFileError extends AnalysisError { readonly published?: PublishedEvidenceFileObservation; readonly userMessage?: string; readonly contentIssues?: readonly AnalysisInputIssue[]; + readonly systemCode?: "EFBIG" | "ENOSPC" | "EDQUOT"; }, ) { super(`Evidence bundle ${operation} failed: ${reason}`, options); @@ -84,5 +96,6 @@ export class EvidenceFileError extends AnalysisError { this.published = options?.published; this.userMessage = options?.userMessage; this.contentIssues = options?.contentIssues; + this.systemCode = options?.systemCode; } } diff --git a/src/domain/evidenceSubject.ts b/src/domain/evidenceSubject.ts index 2ec4b0eeae..fcfb5bd489 100644 --- a/src/domain/evidenceSubject.ts +++ b/src/domain/evidenceSubject.ts @@ -40,6 +40,8 @@ export const evidenceSubjectFormatSchema = z.enum([ "entitlements", "pyinstaller", "node-sea", + "dotnet-bundle", + "bun-standalone", "intel-hex", "srec", "esp-image", diff --git a/src/domain/evm/solidityMetadata.ts b/src/domain/evm/solidityMetadata.ts index ba2e146a57..85eceb32f3 100644 --- a/src/domain/evm/solidityMetadata.ts +++ b/src/domain/evm/solidityMetadata.ts @@ -47,6 +47,17 @@ export const solidityMetadataSchema = z }) .nullable(), diagnostic: z.string().max(512).nullable(), + admission: z + .strictObject({ + code: z.string().min(1).max(64), + detail: z + .record( + z.string().min(1).max(64), + z.union([z.string().max(512), z.number()]), + ) + .optional(), + }) + .optional(), }) .superRefine((value, context) => { const unavailable = value.status === "unavailable"; diff --git a/src/domain/explicitSensitiveFailure.ts b/src/domain/explicitSensitiveFailure.ts index 67babfcc73..3237aa96f8 100644 --- a/src/domain/explicitSensitiveFailure.ts +++ b/src/domain/explicitSensitiveFailure.ts @@ -189,6 +189,10 @@ const redactFailure = ( const publishedPath = identity(error.published?.path); return new EvidenceFileError(error.operation, error.reason, { ...context, + ...(error.systemCode === undefined || + text(error.systemCode) !== error.systemCode + ? {} + : { systemCode: error.systemCode }), ...(retainedPath === undefined ? {} : { path: retainedPath }), ...(error.userMessage === undefined ? {} diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 9a1434fda5..d7e3801d23 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -196,6 +196,11 @@ export const firmwareResultSchemas = { relative_path: z.string(), kind: z.enum(["symlink", "special"]), link_target: z.string().nullable(), + // Older retained Evidence may lack raw target bytes. + link_target_hex: z + .string() + .regex(/^(?:[0-9a-f]{2})*$/u) + .optional(), }), ), chunks: z.array(chunk), diff --git a/src/domain/fixedAllowanceRemediation.test.ts b/src/domain/fixedAllowanceRemediation.test.ts index 7691620689..e2214f1804 100644 --- a/src/domain/fixedAllowanceRemediation.test.ts +++ b/src/domain/fixedAllowanceRemediation.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { AnalysisResourceConstraintError } from "./analysisErrorCore.js"; +import { + AnalysisResourceConstraintError, + resourceConstraintRecovery, +} from "./analysisErrorCore.js"; import { projectAnalysisError } from "./analysisErrorProjection.js"; import { fixedAllowanceOptions, @@ -25,6 +28,61 @@ const forbidden = { work: [/worker CPU limits and observed signal/u], } as const; +describe("resource constraint recovery shape", () => { + it("keeps a fixed allowance and a remediation action from co-occurring", () => { + const allowance = new AnalysisResourceConstraintError( + "inspect_fixture", + "memory", + "allowance_exceeded", + null, + fixedAllowanceOptions("parser_allowance", "parser"), + ); + expect(allowance.recovery).toEqual({ + retryable: false, + fixedAllowance: { name: "parser_allowance", kind: "parser" }, + }); + expect("remediationAction" in allowance.recovery).toBe(false); + + const guided = new AnalysisResourceConstraintError( + "inspect_fixture", + "capacity", + "slots_occupied", + null, + { remediationAction: "wait_for_slot", retryable: true }, + ); + expect(guided.recovery).toEqual({ + remediationAction: "wait_for_slot", + retryable: true, + }); + expect("fixedAllowance" in guided.recovery).toBe(false); + expect(resourceConstraintRecovery(guided)).toEqual(guided.recovery); + }); + + it("keeps only the fixed allowance when both recovery fields are supplied", () => { + const conflict = { + fixedAllowance: { name: "parser_allowance", kind: "parser" as const }, + remediationAction: "wait_for_slot", + retryable: false as const, + }; + const error = new AnalysisResourceConstraintError( + "inspect_fixture", + "memory", + "allowance_exceeded", + null, + // The public options type cannot express both fields. This is the + // runtime object the constructor still has to normalize. + conflict as never, + ); + expect(error.recovery).toEqual({ + fixedAllowance: { name: "parser_allowance", kind: "parser" }, + retryable: false, + }); + expect(error.fixedAllowance).toEqual(conflict.fixedAllowance); + expect(error.remediationAction).toBeUndefined(); + expect(resourceConstraintRecovery(error)).toEqual(error.recovery); + }); +}); + describe("fixed allowance recovery", () => { it.each([ ["parser", "memory"], diff --git a/src/domain/flutter/flutterBuildAnalysis.ts b/src/domain/flutter/flutterBuildAnalysis.ts index 30939f3afa..d1f00ed5a1 100644 --- a/src/domain/flutter/flutterBuildAnalysis.ts +++ b/src/domain/flutter/flutterBuildAnalysis.ts @@ -27,7 +27,8 @@ const ABI_SELECTOR_MAX_CHARACTERS = * the label; Zod max() counts UTF-16 units. Advertise the same code-point * budget as JSON Schema maxLength, including every trailing line terminator. */ -const ABI_SELECTOR_MESSAGE = `ABI must be the exact lib/ directory label: 1–${String(ABI_SELECTOR_MAX_CHARACTERS)} Unicode characters without '/'.`; +/** Shared refusal for an ABI label that is not a lib/ directory name. */ +export const ABI_SELECTOR_MESSAGE = `ABI must be the exact lib/ directory label: 1–${String(ABI_SELECTOR_MAX_CHARACTERS)} Unicode characters without '/'.`; const abiSelectorSchema = z .string() .min(1, ABI_SELECTOR_MESSAGE) diff --git a/src/domain/functionReferenceCompatibility.ts b/src/domain/functionReferenceCompatibility.ts index dd73548549..decdf8a098 100644 --- a/src/domain/functionReferenceCompatibility.ts +++ b/src/domain/functionReferenceCompatibility.ts @@ -17,6 +17,18 @@ interface FlowEdge { remaining: number; } +/** Residual-network invariant. The code is the stable identity. */ +class ReferenceFlowError extends Error { + readonly _tag = "ReferenceFlowError"; + + constructor( + readonly code: "endpoint_unallocated" | "reverse_edge_unallocated", + ) { + super(code); + this.name = "ReferenceFlowError"; + } +} + /** A local residual network: no recursive walk or pairwise adjacency matrix. */ class ReferenceFlow { private readonly edges: FlowEdge[][] = []; @@ -30,7 +42,7 @@ class ReferenceFlow { const forward = this.edges[from]; const reverse = this.edges[to]; if (forward === undefined || reverse === undefined) - throw new Error("Reference flow endpoint was not allocated."); + throw new ReferenceFlowError("endpoint_unallocated"); forward.push({ to, reverse: reverse.length, remaining: capacity }); reverse.push({ to: from, reverse: forward.length - 1, remaining: 0 }); } @@ -87,7 +99,7 @@ class ReferenceFlow { edge.remaining -= amount; const reverse = this.edges[edge.to]?.[edge.reverse]; if (reverse === undefined) - throw new Error("Reference flow reverse edge was not allocated."); + throw new ReferenceFlowError("reverse_edge_unallocated"); reverse.remaining += amount; } return amount; diff --git a/src/domain/inputIssueProjection.test.ts b/src/domain/inputIssueProjection.test.ts index 8dfe406d5e..3ba9fe8aaf 100644 --- a/src/domain/inputIssueProjection.test.ts +++ b/src/domain/inputIssueProjection.test.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { analysisInputErrorFromIssues, projectInputIssues, + projectInputIssuesWithinBudget, } from "./inputIssueProjection.js"; import { processScenarioSchema } from "./process/processScenario.js"; @@ -117,3 +118,75 @@ describe("input issue projection", () => { ); }); }); + +describe("bounded input issue projection", () => { + it("bounds union expansion before constructing field diagnostics", () => { + const schema = z.object({ + value: z.union([ + z.object({ a: z.string(), b: z.number() }), + z.object({ a: z.number(), c: z.string() }), + ]), + }); + const input = { value: { a: false } }; + const parsed = schema.safeParse(input); + if (parsed.success) throw new Error("expected invalid input"); + expect( + projectInputIssuesWithinBudget(parsed.error.issues, input, 32_768), + ).toEqual(projectInputIssues(parsed.error.issues, input)); + expect( + projectInputIssuesWithinBudget(parsed.error.issues, input, 300), + ).toBeUndefined(); + }); + + it("checks a nested path before copying its components", () => { + const input = { value: {} }; + const parsed = z + .object({ + value: z.union([ + z.object({ a: z.string() }), + z.object({ b: z.number() }), + ]), + }) + .safeParse(input); + if (parsed.success) throw new Error("expected invalid input"); + const issue = parsed.error.issues[0]; + if (issue?.code !== "invalid_union") + throw new Error("expected union issue"); + const child = issue.errors[0]?.[0]; + if (child === undefined) throw new Error("expected branch issue"); + let copied = false; + const path = new Proxy( + Array.from({ length: 1000 }, () => "field"), + { + get(target, key, receiver) { + if (key === Symbol.iterator) copied = true; + return Reflect.get(target, key, receiver); + }, + }, + ); + const largePathIssue: z.core.$ZodIssue = { + code: "invalid_union", + path: issue.path, + message: issue.message, + errors: [[{ ...child, path }]], + }; + expect( + projectInputIssuesWithinBudget([largePathIssue], input, 1024), + ).toBeUndefined(); + expect(copied).toBe(false); + }); + + it("accounts for the repeated path on every unknown-key issue", () => { + const issue: z.core.$ZodIssue = { + code: "unrecognized_keys", + path: Array.from({ length: 20 }, () => "nested"), + keys: Array.from({ length: 10 }, (_, index) => `key_${String(index)}`), + message: "Unrecognized keys", + }; + expect(projectInputIssues([issue], {})).toHaveLength(10); + expect(projectInputIssuesWithinBudget([issue], {}, 5000)).toBeUndefined(); + expect(projectInputIssuesWithinBudget([issue], {}, 32_000)).toHaveLength( + 10, + ); + }); +}); diff --git a/src/domain/inputIssueProjection.ts b/src/domain/inputIssueProjection.ts index a768d819d6..afdf8a4f7b 100644 --- a/src/domain/inputIssueProjection.ts +++ b/src/domain/inputIssueProjection.ts @@ -50,10 +50,92 @@ export const projectInputIssues = ( ): readonly AnalysisInputIssue[] => issues.flatMap((issue) => projectIssue(issue, input)); +const projectionBudgetExceeded = Symbol( + "input issue projection budget exceeded", +); + +/** Bound representation expansion before constructing projected diagnostics. */ +export const projectInputIssuesWithinBudget = ( + issues: readonly z.core.$ZodIssue[], + input: unknown, + budgetBytes: number, +): readonly AnalysisInputIssue[] | undefined => { + return projectInputIssueGroupsWithinBudget( + issues, + input, + budgetBytes, + )?.flat(); +}; + +/** Retain each projected issue's source through bounded SDK normalization. */ +export const projectInputIssueGroupsWithinBudget = ( + issues: readonly z.core.$ZodIssue[], + input: unknown, + budgetBytes: number, +): readonly (readonly AnalysisInputIssue[])[] | undefined => { + const budget = { remaining: budgetBytes }; + try { + const groups: (readonly AnalysisInputIssue[])[] = []; + for (const issue of issues) groups.push(projectIssue(issue, input, budget)); + return groups; + } catch (error) { + if (error === projectionBudgetExceeded) return undefined; + throw error; + } +}; + +const chargeProjection = ( + issue: z.core.$ZodIssue, + budget: { remaining: number }, +): void => { + // Six bytes per UTF-16 code unit covers JSON escaping. The fixed allowance + // covers the projected object, property names, scalar values and delimiters. + const charge = (amount: number) => { + budget.remaining -= amount; + if (budget.remaining < 0) throw projectionBudgetExceeded; + }; + charge(256); + let pathCost = 0; + for (const part of issue.path) { + const cost = typeof part === "string" ? 6 * part.length + 16 : 32; + pathCost += cost; + charge(cost); + } + charge(6 * issue.message.length); + if (issue.code === "invalid_union") { + for (const branch of issue.errors) charge(32 + 32 * branch.length); + if ("options" in issue && Array.isArray(issue.options)) + for (const value of issue.options) + charge(typeof value === "string" ? 6 * value.length + 32 : 32); + } + if (issue.code === "invalid_key") charge(32 * issue.issues.length); + if (issue.code === "unrecognized_keys") + for (const key of issue.keys) charge(256 + pathCost + 6 * key.length); + if (issue.code === "invalid_value") + for (const value of issue.values) + charge(typeof value === "string" ? 6 * value.length + 32 : 32); +}; + +const nestedIssue = ( + parent: z.core.$ZodIssue, + child: z.core.$ZodIssue, + budget: { remaining: number } | undefined, +): z.core.$ZodIssue => { + if (budget !== undefined) { + // The combined path is allocated before the child is projected. Charge + // its references first so a long nested path cannot bypass the allowance. + budget.remaining -= 32 + 8 * (parent.path.length + child.path.length); + if (budget.remaining < 0) throw projectionBudgetExceeded; + } + return { ...child, path: [...parent.path, ...child.path] }; +}; + const projectIssue = ( issue: z.core.$ZodIssue, input: unknown, + budget?: { remaining: number }, ): readonly AnalysisInputIssue[] => { + if (budget !== undefined) chargeProjection(issue, budget); const path = issue.path.flatMap((part) => typeof part === "string" || typeof part === "number" ? [part] : [], ); @@ -78,20 +160,14 @@ const projectIssue = ( return uniqueIssues( issue.errors.flatMap((branch) => branch.flatMap((branchIssue) => - projectIssue( - { ...branchIssue, path: [...issue.path, ...branchIssue.path] }, - input, - ), + projectIssue(nestedIssue(issue, branchIssue, budget), input, budget), ), ), ); } if (issue.code === "invalid_key") return issue.issues.flatMap((keyIssue) => - projectIssue( - { ...keyIssue, path: [...issue.path, ...keyIssue.path] }, - input, - ), + projectIssue(nestedIssue(issue, keyIssue, budget), input, budget), ); if (issue.code === "unrecognized_keys") return issue.keys.map((key) => ({ diff --git a/src/domain/javascript/javascriptCallLiteral.test.ts b/src/domain/javascript/javascriptCallLiteral.test.ts index e9a8212978..ca8727dfc6 100644 --- a/src/domain/javascript/javascriptCallLiteral.test.ts +++ b/src/domain/javascript/javascriptCallLiteral.test.ts @@ -41,5 +41,13 @@ it("retains wrapped TypeScript call literals for semantic queries", () => { direction: "forward-influence", }), ); - expect(result.seed_node_ids).toContain(call.node_id); + const argument = graph.nodes.find( + (node) => + node.kind === "expression" && + node.properties.literal_argument_value === -7 && + node.properties.literal_argument_call_site_node_id === call.node_id, + ); + if (argument === undefined) throw new Error("Expected literal argument"); + expect(result.seed_node_ids).toContain(argument.node_id); + expect(result.nodes.map(({ node_id }) => node_id)).toContain(call.node_id); }); diff --git a/src/domain/javascript/javascriptCommonJsExportReceivers.ts b/src/domain/javascript/javascriptCommonJsExportReceivers.ts index b3dca48d6d..10d4a9b1bf 100644 --- a/src/domain/javascript/javascriptCommonJsExportReceivers.ts +++ b/src/domain/javascript/javascriptCommonJsExportReceivers.ts @@ -7,7 +7,6 @@ import { unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; import { - isUnshadowedGlobal, resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; @@ -18,7 +17,10 @@ import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { evaluateSemanticExpression } from "./javascriptSemanticValues.js"; import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.js"; import { semanticProjectedSourcesAtRead } from "./javascriptSemanticProjectedSources.js"; -import { semanticEffectTiming } from "./javascriptSemanticMutationInitializers.js"; +import { + isUnshadowedGlobal, + semanticEffectTiming, +} from "./javascriptSemanticMutationInitializers.js"; import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js"; const exportInvocations = new WeakMap< diff --git a/src/domain/javascript/javascriptGraphqlOperations.ts b/src/domain/javascript/javascriptGraphqlOperations.ts index 820379b784..af7b922122 100644 --- a/src/domain/javascript/javascriptGraphqlOperations.ts +++ b/src/domain/javascript/javascriptGraphqlOperations.ts @@ -16,7 +16,7 @@ import { import type { JavaScriptRpcEndpoint, JavaScriptRpcScan, -} from "./javascriptRpcEndpoints.js"; +} from "./javascriptRpcEndpoint.js"; /** * Find source-body string values owned by a GraphQL Source-shaped literal. diff --git a/src/domain/javascript/javascriptModuleInterop.ts b/src/domain/javascript/javascriptModuleInterop.ts index e6326d8d30..29957ae9e9 100644 --- a/src/domain/javascript/javascriptModuleInterop.ts +++ b/src/domain/javascript/javascriptModuleInterop.ts @@ -3,14 +3,14 @@ import * as t from "@babel/types"; import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js"; import { - isUnshadowedGlobal, resolveSemanticBindingState, semanticScopeId, semanticStableInitializer, type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; -import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js"; +import { isUnshadowedGlobal } from "./javascriptSemanticMutationInitializers.js"; +import { readSemanticRequireOrigin } from "./javascriptSemanticRequireOriginSlot.js"; import { childNodes, javaScriptAstChildKeys, @@ -500,7 +500,7 @@ export const createSemanticModuleInterop = ( const required = converted === undefined ? undefined - : semanticRequireOrigin(converted.input, state)?.origin; + : readSemanticRequireOrigin(converted.input, state)?.origin; if ( converted === undefined || required === undefined || @@ -596,7 +596,7 @@ export const createSemanticModuleInterop = ( resolveSemanticBindingState(state, input, input.name) !== binding ) return undefined; - const required = semanticRequireOrigin(initial.node, state)?.origin; + const required = readSemanticRequireOrigin(initial.node, state)?.origin; if (required === undefined || required.importedPath.length !== 0) return undefined; if (convertedCall === undefined) return undefined; diff --git a/src/domain/javascript/javascriptRpcEndpoint.ts b/src/domain/javascript/javascriptRpcEndpoint.ts new file mode 100644 index 0000000000..1498d02f5b --- /dev/null +++ b/src/domain/javascript/javascriptRpcEndpoint.ts @@ -0,0 +1,15 @@ +import type * as t from "@babel/types"; + +/** One RPC operation or transport URL recovered from client-side syntax. */ +export interface JavaScriptRpcEndpoint { + readonly value: string; + readonly mechanism: string; +} + +/** Feed every AST node in pre-order, then drain finish() for Source findings. */ +export interface JavaScriptRpcScan { + inspect(node: t.Node): JavaScriptRpcEndpoint | undefined; + finish(): Iterable< + (JavaScriptRpcEndpoint & { readonly node: t.Node }) | void + >; +} diff --git a/src/domain/javascript/javascriptRpcEndpoints.ts b/src/domain/javascript/javascriptRpcEndpoints.ts index c3097aee4e..548bb4bba2 100644 --- a/src/domain/javascript/javascriptRpcEndpoints.ts +++ b/src/domain/javascript/javascriptRpcEndpoints.ts @@ -9,19 +9,12 @@ import { calleeName } from "./javascriptStaticAnalysisHelpers.js"; import type { JavaScriptBindingIndex } from "./javascriptSemanticState.js"; import { createJavaScriptGraphqlScan } from "./javascriptGraphqlOperations.js"; -/** One RPC operation or transport URL recovered from client-side syntax. */ -export interface JavaScriptRpcEndpoint { - readonly value: string; - readonly mechanism: string; -} +import type { + JavaScriptRpcEndpoint, + JavaScriptRpcScan, +} from "./javascriptRpcEndpoint.js"; -/** Feed every AST node in pre-order, then drain finish() for Source findings. */ -export interface JavaScriptRpcScan { - inspect(node: t.Node): JavaScriptRpcEndpoint | undefined; - finish(): Iterable< - (JavaScriptRpcEndpoint & { readonly node: t.Node }) | void - >; -} +export type { JavaScriptRpcScan } from "./javascriptRpcEndpoint.js"; // tRPC proxies preserve router and procedure names as property names, so they // survive identifier minification. Method names follow @trpc/react-query and diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index be004588e0..688ff5c4a8 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -10,16 +10,15 @@ import { } from "./javascriptElectronMemberWrites.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { - isUnshadowedGlobal, resolveSemanticBindingState, semanticStableInitializer, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; -import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js"; import { + isUnshadowedGlobal, semanticBindingAtRead, - semanticEffectTiming, semanticDefiniteEffectPrecedesReference, + semanticEffectTiming, } from "./javascriptSemanticMutationInitializers.js"; import { semanticIterationSources, @@ -30,20 +29,24 @@ import { semanticPropertyPathKeyMatches, } from "./javascriptSemanticPropertyPaths.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; -import { semanticRequireLoaderAt } from "./javascriptSemanticRequireOrigin.js"; +import { + GLOBAL_OBJECT_NAMES, + globalObjectName, + globalReferenceNamespace, + isUnmodifiedGlobalReference, + isUnmodifiedImportMetaUrl, +} from "./javascriptSemanticGlobalReference.js"; +import { readSemanticRequireLoader } from "./javascriptSemanticRequireOriginSlot.js"; import { semanticCopiedReferencePath, semanticReferenceIterationUnknown, semanticReferenceSourcesInactive, } from "./javascriptSemanticReferenceSources.js"; -const GLOBAL_OBJECT_NAMES = ["globalThis", "global", "window", "self"]; - -const globalObjectName = ( - node: t.Node, - state: JavaScriptSemanticAnalysisState, -): string | undefined => - GLOBAL_OBJECT_NAMES.find((name) => isUnshadowedGlobal(node, state, name)); +export { + isUnmodifiedGlobalReference, + isUnmodifiedImportMetaUrl, +} from "./javascriptSemanticGlobalReference.js"; /** Global self references retain object identity, rather than adding a property. */ const canonicalAmbientPath = ( @@ -801,7 +804,7 @@ export function* indexJavaScriptAmbientWritesSteps( (t.isCallExpression(node) || t.isOptionalCallExpression(node)) && !state.ambientWrites.covers(["global", "require", "resolve"]) && !state.ambientEscapes.covers(["global", "require", "resolve"]) && - semanticRequireLoaderAt(node, state, new Set()) === "commonjs-loader" + readSemanticRequireLoader(node, state, new Set()) === "commonjs-loader" ) continue; retainEscapeEffect(effect); @@ -835,42 +838,6 @@ export function* indexJavaScriptAmbientWritesSteps( state.ambientEscapes.retain(["global", null]); } -/** Match lexical global identity; null denotes a direct global root. */ -const globalReferenceNamespace = ( - callee: t.Node, - state: JavaScriptSemanticAnalysisState, - path: readonly [string, ...string[]], -): string | null | undefined => { - const { root, members } = electronMemberPath(callee); - const matches = (expected: readonly string[]) => - members.length === expected.length && - members.every((key, index) => key === expected[index]); - if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1))) - return null; - const namespace = globalObjectName(root, state); - return namespace !== undefined && matches(path) ? namespace : undefined; -}; - -/** - * Recognize an ambient property path, optionally through a global object, without a visible - * source-local overwrite. This is a syntax-local identity fact, not proof that - * the runtime's globals have never been patched by other code. - */ -export const isUnmodifiedGlobalReference = ( - callee: t.Node, - state: JavaScriptSemanticAnalysisState, - name: string | readonly [string, ...string[]], -): boolean => { - const path: readonly [string, ...string[]] = - typeof name === "string" ? [name] : name; - if (state.ambientWrites.covers(["global", ...path])) return false; - const namespace = globalReferenceNamespace(callee, state, path); - return ( - namespace !== undefined && - (namespace === null || !state.ambientWrites.covers(["global", namespace])) - ); -}; - export type GlobalReferenceRead = | { readonly status: "unmodified" | "unrelated" | "unknown" } | { readonly status: "replacement"; readonly value: t.Expression }; @@ -975,10 +942,3 @@ export const createGlobalReferenceReader = ( return { status: "replacement", value: confirmed.right }; }; }; - -/** A module URL base is known only while its property has no visible write. */ -export const isUnmodifiedImportMetaUrl = ( - node: t.Node | undefined, - state: JavaScriptSemanticAnalysisState, -): boolean => - isImportMetaUrl(node) && !state.ambientWrites.covers(["import-meta", "url"]); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts index 794a53ffbd..148cf73fef 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts @@ -7,6 +7,44 @@ import { topLevelBinding, } from "./javascriptSemanticAnalysis.fixture.js"; +describe("JavaScript semantic call result use", () => { + it.each([ + 'consume("unique");', + 'consume("unique")!;', + 'consume("unique") as unknown;', + '(consume("unique") as unknown)!;', + ])("does not treat %s as a used call result", (source) => { + const ir = analyzeJavaScriptSemantics(source); + expect(ir.callSites[0]?.resultUsed).toBe(false); + }); + + it("treats an asserted call initializer as a used result", () => { + const used = analyzeJavaScriptSemantics( + 'const value = consume("unique")!;', + ); + expect(used.callSites[0]?.resultUsed).toBe(true); + }); + + it.each([ + 'for (notify("token"); running;) {}', + 'for (; running; notify("token")) {}', + 'for (notify("token")!; running;) {}', + ])("does not treat %s as a used call result", (source) => { + const ir = analyzeJavaScriptSemantics(source); + const call = ir.callSites.find((site) => + site.arguments.some((argument) => argument.literal === "token"), + ); + expect(call?.resultUsed).toBe(false); + }); + + it("treats a for-loop variable initializer call as a used result", () => { + const ir = analyzeJavaScriptSemantics( + 'for (const value = notify("token"); running;) {}', + ); + expect(ir.callSites[0]?.resultUsed).toBe(true); + }); +}); + describe("JavaScript semantic analysis: calls 1", () => { it("does not assign computed method-name reads to method closures", () => { const ir = analyzeJavaScriptSemantics(` diff --git a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts index c77700a252..d60b62d226 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts @@ -44,7 +44,11 @@ describe("JavaScript semantic analysis: rejection 1", () => { expect(collectJavaScriptExports(parsed).values).toEqual([]); expect(fingerprintJavaScriptAst(parsed)).toMatch(/^[a-f0-9]{64}$/u); const ir = analyzeParsedJavaScriptSemantics(parsed); - expect(ir.coverage.status).toBe("complete"); + expect(ir.coverage).toMatchObject({ + status: "partial", + omittedCount: null, + }); + expect(ir.expressionFlowsTruncated).toBe(true); const binding = topLevelBinding(ir, "result"); expect(binding.value).toEqual({ status: "literal", value: 12000 }); expect(ir.references).toEqual([ diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 3f9b3d6610..ac0dd13d7c 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -5,6 +5,7 @@ import { semanticBindingCanBeRebound, semanticOpaqueBindingReadMayHaveChanged, } from "./javascriptSemanticOpaqueEffects.js"; +import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; import * as t from "@babel/types"; import { @@ -204,7 +205,10 @@ function* indexSemanticAmbientWritesSteps( yield* indexJavaScriptAmbientWritesSteps( state.program, state, - createSemanticOpaqueGlobalObserver(state), + createSemanticOpaqueGlobalObserver(state, { + createAmbientPathResolver, + createSemanticModuleInterop, + }), (node) => observePossibleCommonJsDelegateWrite(node, state), ); if (state.opaqueEffects.globalSites.length > 0) @@ -420,6 +424,7 @@ export const analyzeParsedJavaScriptSemantics = ( export function* analyzeParsedJavaScriptSemanticsSteps( file: ParsedJavaScriptSource, staticImportResolutionMode: JavaScriptModuleResolutionMode = "import", + expressionNodeBudget?: number, ): Generator { const state = createState(file.program, staticImportResolutionMode); yield* collectDefinitionsSteps(file.program, state); @@ -447,6 +452,14 @@ export function* analyzeParsedJavaScriptSemanticsSteps( callables, references, parserPartial, + expressionNodeBudget, + ); + const coverage = semanticCoverage( + parserPartial, + [ + ...new Set([...state.valueResourceLimits, ...state.getterResourceLimits]), + ].sort(), + [...state.returnedReferenceWidenings].map(range), ); return { schema: "JavaScriptSemanticIR", @@ -456,17 +469,15 @@ export function* analyzeParsedJavaScriptSemanticsSteps( references, moduleLinks, ...derived, - coverage: semanticCoverage( - parserPartial, - [ - ...new Set([ - ...state.valueResourceLimits, - ...state.getterResourceLimits, - ]), - ].sort(), - [...state.returnedReferenceWidenings].map(range), - ), + coverage: derived.expressionFlowsTruncated + ? { ...coverage, status: "partial", omittedCount: null } + : coverage, limitations: [ + ...(derived.expressionFlowsTruncated + ? [ + "The offered semantic node capacity bounded expression dependencies; omitted influence remains unknown.", + ] + : []), ...(state.valueResourceLimits.has("alias-selections") ? [semanticResourceLimitReason("alias-selections")] : []), @@ -499,15 +510,7 @@ export function* analyzeParsedJavaScriptSemanticsSteps( * Receiver identity facts for the overloaded `.open` syntax, plus * `configuration` for `.get` reads of a `getConfiguration(...)` result. */ -export type JavaScriptOpenReceiverFact = - | "window" - | "indexed-db" - | "cache-storage" - | "local" - | "local-indexed-db" - | "local-cache-storage" - | "configuration" - | "extension-state"; +export type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; /** Lexical receiver identities that select static endpoint interpretations. */ export interface JavaScriptReceiverFacts { diff --git a/src/domain/javascript/javascriptSemanticCalls.ts b/src/domain/javascript/javascriptSemanticCalls.ts index ca53ffa67c..a6a116dfeb 100644 --- a/src/domain/javascript/javascriptSemanticCalls.ts +++ b/src/domain/javascript/javascriptSemanticCalls.ts @@ -12,6 +12,7 @@ import type { import { semanticIdentifierRole } from "./javascriptSemanticProjection.js"; import { createSemanticCallableStack } from "./javascriptSemanticCallableStack.js"; import { + javaScriptExpressionWrapperTarget, readExactJavaScriptLiteral, semanticStaticPropertyKey, unwrapJavaScriptExpression, @@ -124,6 +125,32 @@ export const collectJavaScriptSemanticCalls = ( }; }; +/** TypeScript and grouping wrappers do not consume a call result. */ +const callResultIsUsed = ( + call: t.Node, + parent: t.Node | null, + ancestors: readonly t.Node[], +): boolean => { + let current = parent; + let child = call; + let index = ancestors.length - 1; + while ( + current !== null && + javaScriptExpressionWrapperTarget(current) !== undefined + ) { + child = current; + index -= 1; + current = index >= 0 ? (ancestors[index] ?? null) : null; + } + if (t.isExpressionStatement(current)) return false; + // for/of and for/in heads use their expressions. A C-style for initializer + // or update discards the value. + return !( + t.isForStatement(current) && + (current.init === child || current.update === child) + ); +}; + const collectCallSite = ( node: t.CallExpression | t.OptionalCallExpression | t.NewExpression, parent: t.Node | null, @@ -146,6 +173,7 @@ const collectCallSite = ( callerCallableId: owner?.callableId ?? null, location: range(node), calleeLocation: range(node.callee), + resultUsed: callResultIsUsed(node, parent, context.ancestors), resolution: resolution.callableIds.length === 1 && resolution.complete ? "exact" diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts index 7668963085..ea8c872e19 100644 --- a/src/domain/javascript/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts @@ -20,16 +20,18 @@ import { unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; import { - isUnshadowedGlobal, resolveSemanticBindingState, semanticStableInitializer, type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; +import { + isUnshadowedGlobal, + semanticBindingAtRead, +} from "./javascriptSemanticMutationInitializers.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; -import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { builtinDataEffectMethod, dataEffectLiteralString, diff --git a/src/domain/javascript/javascriptSemanticCycles.test.ts b/src/domain/javascript/javascriptSemanticCycles.test.ts new file mode 100644 index 0000000000..fa2ed595a8 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticCycles.test.ts @@ -0,0 +1,119 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +import { readImportSpecifiers } from "../../../scripts/lib/import-specifiers.mjs"; + +const root = dirname(fileURLToPath(import.meta.url)); + +const relativeSpecifiers = (source: string): readonly string[] => + readImportSpecifiers(source) + .map(({ specifier }) => specifier) + .filter((specifier) => specifier.startsWith(".")); + +/** + * Cycles that already existed outside the semantic analysis hub. Membership is + * exact so a new edge cannot hide inside the allow-list. + */ +const allowedCycles = [ + [ + "electronStaticAnalysisTypes.ts", + "javascriptRouteFacts.ts", + "javascriptSourceCoordinates.ts", + "javascriptStaticAnalysisTypes.ts", + ], + [ + "javascriptRuntimeReconciliationActive.ts", + "javascriptRuntimeReconciliationCaptureParsing.ts", + ], + ["javascriptStaticAnalysisFindings.ts", "javascriptStaticAnalysisState.ts"], +]; + +const productionFiles = (directory: string): string[] => { + const files: string[] = []; + for (const name of readdirSync(directory)) { + const path = join(directory, name); + if (statSync(path).isDirectory()) files.push(...productionFiles(path)); + else if (name.endsWith(".ts") && !name.includes(".test.")) files.push(path); + } + return files; +}; + +const importGraph = (): Map> => { + const files = new Set( + productionFiles(root).map((path) => relative(root, path)), + ); + const graph = new Map>(); + for (const file of files) { + const source = readFileSync(join(root, file), "utf8"); + const edges = new Set(); + for (const spec of relativeSpecifiers(source)) { + const resolved = resolve(dirname(join(root, file)), spec).replace( + /\.js$/, + ".ts", + ); + const target = relative(root, resolved); + if (files.has(target)) edges.add(target); + } + graph.set(file, edges); + } + return graph; +}; + +const stronglyConnected = ( + graph: Map>, +): readonly (readonly string[])[] => { + let index = 0; + const stack: string[] = []; + const onStack = new Set(); + const indices = new Map(); + const low = new Map(); + const components: string[][] = []; + const visit = (node: string): void => { + indices.set(node, index); + low.set(node, index); + index += 1; + stack.push(node); + onStack.add(node); + for (const next of graph.get(node) ?? []) { + if (!indices.has(next)) { + visit(next); + low.set(node, Math.min(low.get(node) ?? index, low.get(next) ?? index)); + } else if (onStack.has(next)) { + low.set( + node, + Math.min(low.get(node) ?? index, indices.get(next) ?? index), + ); + } + } + if (low.get(node) === indices.get(node)) { + const component: string[] = []; + let member = ""; + while (member !== node) { + member = stack.pop() ?? node; + onStack.delete(member); + component.push(member); + } + if (component.length > 1) components.push(component.sort()); + } + }; + for (const node of [...graph.keys()].sort()) { + if (!indices.has(node)) visit(node); + } + return components.sort( + (left, right) => + right.length - left.length || left.join().localeCompare(right.join()), + ); +}; + +describe("javascript semantic import cycles", () => { + it("keeps the semantic modules out of one import cycle", () => { + const cycles = stronglyConnected(importGraph()).map((cycle) => + cycle.join("\n"), + ); + expect(cycles).toEqual( + allowedCycles.map((cycle) => [...cycle].sort().join("\n")), + ); + }); +}); diff --git a/src/domain/javascript/javascriptSemanticDerivedAnalysis.ts b/src/domain/javascript/javascriptSemanticDerivedAnalysis.ts index b523f7d10d..3752482346 100644 --- a/src/domain/javascript/javascriptSemanticDerivedAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticDerivedAnalysis.ts @@ -18,11 +18,20 @@ import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState. import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js"; import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; -import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; +import { + readExactJavaScriptLiteral, + unwrapJavaScriptExpression, +} from "./javascriptAstValues.js"; +import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js"; +import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "./javascriptAnalysisResourceControls.js"; type DerivedSemanticAnalysis = Pick< JavaScriptSemanticIr, | "assignmentFlows" + | "expressionFlows" + | "initializerFlows" + | "literalReturns" + | "expressionFlowsTruncated" | "argumentFlows" | "callResultFlows" | "callReturnFlows" @@ -53,6 +62,7 @@ export function* collectJavaScriptDerivedSemanticsSteps( callables: readonly JavaScriptSemanticCallable[], references: readonly JavaScriptSemanticReference[], parserPartial: boolean, + expressionNodeBudget: number = JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.semanticNodesPerFile, ): Generator { const calls = collectJavaScriptSemanticCalls(program, state, callables); yield; @@ -97,6 +107,13 @@ export function* collectJavaScriptDerivedSemanticsSteps( state, callables, ); + const expressionFlows = yield* collectExpressionFlowsSteps( + program, + state, + callables, + expressionNodeBudget, + ); + const initializerFlows = expressionFlows.initializers; const functionFingerprints = yield* collectJavaScriptSemanticFingerprintsSteps({ state, @@ -113,6 +130,10 @@ export function* collectJavaScriptDerivedSemanticsSteps( }); return { assignmentFlows, + expressionFlows: expressionFlows.expressions, + initializerFlows, + literalReturns: expressionFlows.literalReturns, + expressionFlowsTruncated: expressionFlows.truncated, ...calls, promiseOperations, ...asyncEffects, @@ -124,6 +145,164 @@ export function* collectJavaScriptDerivedSemanticsSteps( }; } +const isInfluenceExpression = (node: t.Node): boolean => + t.isBinaryExpression(node) || + t.isLogicalExpression(node) || + t.isUnaryExpression(node) || + t.isTemplateLiteral(node) || + t.isConditionalExpression(node) || + t.isSequenceExpression(node); + +/** One cooperative pass retains only immediate value operands, not subtree ranges. */ +function* collectExpressionFlowsSteps( + program: t.Program, + state: JavaScriptSemanticAnalysisState, + callables: readonly JavaScriptSemanticCallable[], + budget: number, +): Generator< + void, + { + expressions: JavaScriptSemanticIr["expressionFlows"]; + initializers: JavaScriptSemanticIr["initializerFlows"]; + literalReturns: JavaScriptSemanticIr["literalReturns"]; + truncated: boolean; + } +> { + const expressions: JavaScriptSemanticIr["expressionFlows"][number][] = []; + const initializers: JavaScriptSemanticIr["initializerFlows"][number][] = []; + const literalReturns: JavaScriptSemanticIr["literalReturns"][number][] = []; + const candidates: { node: t.Node; ownerCallableId: string | null }[] = []; + let truncated = false; + const stack = createSemanticCallableIdStack( + new Set(callables.map((item) => item.callableId)), + ); + yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, { + enter: (node, parent) => { + stack.enter(node, parent); + const returnValue = t.isReturnStatement(node) + ? node.argument + : t.isArrowFunctionExpression(node) && !t.isBlockStatement(node.body) + ? node.body + : null; + if (returnValue != null) { + const expression = unwrapJavaScriptExpression(returnValue).node; + const exact = readExactJavaScriptLiteral(expression); + if (exact.found) { + const value = semanticPrimitiveSet([exact.value]); + if (value.status === "literal" && literalReturns.length < budget) + literalReturns.push({ + location: range(expression), + value: value.value, + }); + else if (value.status === "literal") truncated = true; + else if ( + value.status === "unknown" && + value.resourceLimit === "primitive-bytes" + ) + state.valueResourceLimits.add("primitive-bytes"); + } + } + if ( + t.isVariableDeclarator(node) && + t.isIdentifier(node.id) && + node.init != null + ) { + const init = unwrapJavaScriptExpression(node.init).node; + if (isInfluenceExpression(init)) { + if (initializers.length < budget) + initializers.push({ target: range(node.id), source: range(init) }); + else truncated = true; + } + } + if ( + isInfluenceExpression(node) && + !(t.isTemplateLiteral(node) && t.isTaggedTemplateExpression(parent)) + ) + if (candidates.length < budget) + candidates.push({ + node, + ownerCallableId: stack.callables.at(-1) ?? null, + }); + else truncated = true; + }, + exit: (node) => stack.exit(node), + }); + let processedOperands = 0; + for (const [ + candidateIndex, + { node, ownerCallableId }, + ] of candidates.entries()) { + if (candidateIndex % 64 === 0) yield; + let operator: string; + let values: readonly t.Node[]; + if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) { + operator = node.operator; + values = [node.left, node.right]; + } else if (t.isUnaryExpression(node)) { + operator = node.operator; + values = [node.argument]; + } else if (t.isTemplateLiteral(node)) { + operator = "template"; + values = node.expressions; + } else if (t.isConditionalExpression(node)) { + operator = "?:"; + values = [node.test, node.consequent, node.alternate]; + } else if (t.isSequenceExpression(node)) { + operator = ","; + values = node.expressions; + } else continue; + const operands: JavaScriptSemanticIr["expressionFlows"][number]["operands"][number][] = + []; + for (const [index, value] of values.entries()) { + if (processedOperands >= budget) { + truncated = true; + break; + } + if (processedOperands % 64 === 0) yield; + processedOperands += 1; + const conditional = + (t.isLogicalExpression(node) && index === 1) || + (t.isConditionalExpression(node) && index > 0); + // Earlier sequence expressions may change the final result via side effects. + const unsupported = + (t.isUnaryExpression(node) && + (node.operator === "void" || node.operator === "delete")) || + (t.isSequenceExpression(node) && index !== values.length - 1); + const operand = unwrapJavaScriptExpression(value).node; + const exact = readExactJavaScriptLiteral(operand); + const literal = exact.found ? semanticPrimitiveSet([exact.value]) : null; + if ( + literal?.status === "unknown" && + literal.resourceLimit === "primitive-bytes" + ) + state.valueResourceLimits.add("primitive-bytes"); + operands.push({ + location: range(operand), + conditional, + kind: unsupported + ? "unsupported" + : t.isIdentifier(operand) + ? "reference" + : literal?.status === "literal" + ? "literal" + : isInfluenceExpression(operand) + ? "expression" + : "unsupported", + ...(!unsupported && literal?.status === "literal" + ? { literal: literal.value } + : {}), + }); + } + expressions.push({ + location: range(node), + operator, + ownerCallableId, + operands, + }); + } + return { expressions, initializers, literalReturns, truncated }; +} + /** Keep assignment syntax in the IR so graph consumers never reparse source. */ function* collectAssignmentFlowsSteps( program: t.Program, diff --git a/src/domain/javascript/javascriptSemanticExpressionShapes.ts b/src/domain/javascript/javascriptSemanticExpressionShapes.ts index f31ec62430..423bf91dd3 100644 --- a/src/domain/javascript/javascriptSemanticExpressionShapes.ts +++ b/src/domain/javascript/javascriptSemanticExpressionShapes.ts @@ -12,10 +12,12 @@ import { isErasedJavaScriptDeclaration, isUnmodeledJavaScriptRuntimeScope, } from "./javascriptModuleDeclaration.js"; -import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js"; import { isUnshadowedGlobal, + semanticBindingAtRead, +} from "./javascriptSemanticMutationInitializers.js"; +import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js"; +import { resolveSemanticBindingState, semanticStableInitializer, type JavaScriptSemanticAnalysisState, diff --git a/src/domain/javascript/javascriptSemanticGetterQueries.ts b/src/domain/javascript/javascriptSemanticGetterQueries.ts index 904e7b65d2..b87eac3e8a 100644 --- a/src/domain/javascript/javascriptSemanticGetterQueries.ts +++ b/src/domain/javascript/javascriptSemanticGetterQueries.ts @@ -1,5 +1,5 @@ import type * as t from "@babel/types"; -import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReference.js"; import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { SEMANTIC_GETTER_EXPANSION_LIMIT } from "./javascriptSemanticResourceLimits.js"; diff --git a/src/domain/javascript/javascriptSemanticGetterResultWalker.ts b/src/domain/javascript/javascriptSemanticGetterResultWalker.ts new file mode 100644 index 0000000000..7c2fe5f831 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticGetterResultWalker.ts @@ -0,0 +1,408 @@ +import * as t from "@babel/types"; +import { + unwrapJavaScriptExpression, + semanticStaticPropertyKey, +} from "./javascriptAstValues.js"; +import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js"; +import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; +import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js"; +import { + resolveSemanticBindingState, + type JavaScriptSemanticBindingState, + type JavaScriptSemanticAnalysisState, +} from "./javascriptSemanticState.js"; +import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReference.js"; +import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js"; +import { + SemanticGetterQuery, + type GetterQueryJob, +} from "./javascriptSemanticGetterQueries.js"; + +type ClassNode = t.ClassDeclaration | t.ClassExpression; +interface AliasVisit { + readonly identity: string; + readonly pathLength: number; + readonly parent?: AliasVisit; +} +interface GetterWork { + readonly node: t.Node; + readonly path: PropertyPath; + readonly aliases?: AliasVisit | undefined; + readonly escapes?: true; + readonly privateRead?: { + readonly node: t.MemberExpression | t.OptionalMemberExpression; + readonly name: string; + readonly path: PropertyPath; + }; +} +export interface GetterResultResolvers { + readonly classReferences: ( + lookup: { + readonly owner: ClassNode; + readonly receiver?: ClassNode; + readonly kind: "static" | "instance" | "prototype"; + }, + path: PropertyPath, + escapes: boolean, + ) => readonly Reference[]; + readonly objectReferences: ( + receiver: t.ObjectExpression, + path: PropertyPath, + ) => readonly Reference[]; + readonly privateReceiver: ( + receiver: ClassNode, + owner: ClassNode, + path: PropertyPath, + isStatic: boolean, + ) => boolean; + readonly bases: (node: t.Node) => readonly ClassNode[]; + readonly containerReferences?: ( + node: t.ObjectExpression | t.ArrayExpression, + path: PropertyPath, + ) => readonly Reference[]; +} + +/** Branch syntax contributes alternatives without evaluating application code. */ +const resultBranches = (node: t.Node): readonly t.Node[] | undefined => { + if (t.isConditionalExpression(node)) return [node.consequent, node.alternate]; + if (t.isLogicalExpression(node)) return [node.left, node.right]; + if (t.isSequenceExpression(node)) { + const last = node.expressions.at(-1); + return last === undefined ? [] : [last]; + } + if (t.isAssignmentExpression(node)) { + if (node.operator === "=") return [node.right]; + if (["&&=", "||=", "??="].includes(node.operator)) + return [node.left, node.right]; + } + return undefined; +}; + +/** Each walk owns local aliases; getter-to-getter traversal belongs to the query worklist. */ +export class GetterResultWalker { + readonly #pending: GetterWork[] = []; + readonly #aliases = new Map>(); + readonly #scope: string | undefined; + + constructor( + readonly job: GetterQueryJob, + readonly query: SemanticGetterQuery, + readonly resolvers: GetterResultResolvers, + roots: readonly { + readonly node: t.Node | null; + }[] = semanticCallableResultExpressions(job.invocation.getter), + ) { + for (const { node } of roots) + if (node !== null) this.#enqueue(() => ({ node, path: job.path })); + this.#scope = query.state.scopeByNode.get(job.invocation.getter)?.scopeId; + } + + run(): void { + for (let cursor = 0; cursor < this.#pending.length; cursor++) { + const current = this.#pending[cursor]; + if (current !== undefined) this.#visit(current); + } + } + + #enqueue(create: () => GetterWork): void { + if (!this.query.reserveResult()) return; + const work = create(); + // The fallback escapes receiver roots. Keeping local suffix permutations + // here would repeat the precise expansion inside a single getter. + this.#pending.push( + this.query.widening + ? { + ...work, + path: [], + escapes: true, + ...(work.privateRead === undefined + ? {} + : { privateRead: { ...work.privateRead, path: [] } }), + } + : work, + ); + } + + #retain(current: GetterWork, reference: Reference): void { + this.query.retain(reference, current.escapes || this.job.escapes); + } + + #privateRead(current: GetterWork): void { + if (current.privateRead !== undefined) + this.#retain(current, { + node: current.privateRead.node, + path: current.escapes ? [] : current.privateRead.path, + }); + } + + #localBinding(expression: t.Identifier) { + const state = this.query.state; + const binding = resolveSemanticBindingState( + state, + expression, + expression.name, + ); + let scope = + binding === undefined ? undefined : state.scopesById.get(binding.scopeId); + while (scope !== undefined && scope.scopeId !== this.#scope) { + if (scope.kind === "function" || scope.parentScopeId === null) + return undefined; + scope = state.scopesById.get(scope.parentScopeId); + } + return scope !== undefined && + binding?.referenceInitializers.every( + (initializer) => initializer.copyKind === undefined, + ) + ? binding + : undefined; + } + + #cycle(current: GetterWork, identity: string): AliasVisit | undefined { + for (let visit = current.aliases; visit !== undefined; visit = visit.parent) + if (visit.identity === identity) return visit; + return undefined; + } + + #alias(current: GetterWork, expression: t.Identifier): boolean { + const binding = this.#localBinding(expression); + if (binding === undefined) return false; + const identity = JSON.stringify([ + binding.bindingId, + expression.start, + expression.end, + ]); + const previous = this.#cycle(current, identity); + if (previous !== undefined) { + // Growing aliases can reach arbitrarily deep children of the same roots. + if (current.path.length > previous.pathLength && !current.escapes) + this.#enqueue(() => ({ + ...current, + path: [], + aliases: undefined, + escapes: true, + })); + return true; + } + const key = JSON.stringify([ + current.path, + current.privateRead?.name, + current.privateRead?.path, + current.escapes, + ]); + const known = this.#aliases.get(identity) ?? new Set(); + if (!known.has(key)) { + known.add(key); + this.#aliases.set(identity, known); + this.#aliasOrigins(current, expression, binding, identity); + } + return true; + } + + #aliasOrigins( + current: GetterWork, + expression: t.Identifier, + binding: JavaScriptSemanticBindingState, + identity: string, + ): void { + const state = this.query.state; + const { binding: origins } = semanticBindingAtRead( + binding, + expression, + state.parentsByNode, + ); + const aliases: AliasVisit = { + identity, + pathLength: current.path.length, + ...(current.aliases === undefined ? {} : { parent: current.aliases }), + }; + for (const initializer of [ + ...origins.initializers, + ...origins.referenceInitializers.filter( + (origin) => !semanticReferenceSourcesInactive(origin, state), + ), + ]) + this.#enqueue(() => ({ + ...current, + node: initializer.node, + path: [...initializer.projection, ...current.path], + aliases, + })); + } + + #ownsThis(expression: t.ThisExpression | t.Super): boolean { + let owner = this.query.state.parentsByNode.get(expression); + while ( + owner !== undefined && + !t.isClass(owner) && + !(t.isFunction(owner) && !t.isArrowFunctionExpression(owner)) + ) + owner = this.query.state.parentsByNode.get(owner); + return owner === this.job.invocation.getter; + } + + #privateValue(current: GetterWork, owner: ClassNode): void { + const read = current.privateRead; + if (read === undefined) return; + const { getter, receiver = owner } = this.job.invocation; + const isStatic = t.isClassMethod(getter) && getter.static; + if ( + !t.isClass(receiver) || + !this.resolvers.privateReceiver(receiver, owner, current.path, isStatic) + ) { + this.#privateRead(current); + return; + } + for (const member of owner.body.body) + if ( + t.isClassPrivateProperty(member) && + member.static === isStatic && + member.key.id.name === read.name && + member.value != null + ) + this.#retain(current, { + node: member.value, + path: current.escapes ? [] : read.path, + }); + } + + #thisValue( + current: GetterWork, + expression: t.ThisExpression | t.Super, + ): void { + if (!this.#ownsThis(expression)) return; + const { getter, owner, receiver = owner } = this.job.invocation; + if (current.privateRead !== undefined && t.isClass(owner)) { + this.#privateValue(current, owner); + return; + } + const references = t.isClass(receiver) + ? this.resolvers.classReferences( + { + owner: receiver, + kind: + t.isClassMethod(getter) && getter.static ? "static" : "instance", + }, + current.path, + current.escapes || this.job.escapes, + ) + : this.resolvers.objectReferences(receiver, current.path); + for (const reference of references) this.#retain(current, reference); + } + + #superValue( + current: GetterWork, + expression: t.MemberExpression | t.OptionalMemberExpression, + ): boolean { + const { getter, owner, receiver = owner } = this.job.invocation; + if ( + !t.isSuper(expression.object) || + !t.isClass(owner) || + owner.superClass == null + ) + return false; + const key = semanticStaticPropertyKey( + expression.property, + expression.computed, + ); + for (const base of this.resolvers.bases(owner.superClass)) + for (const reference of this.resolvers.classReferences( + { + owner: base, + receiver: t.isClass(receiver) ? receiver : owner, + kind: + t.isClassMethod(getter) && getter.static ? "static" : "prototype", + }, + [key, ...current.path], + current.escapes || this.job.escapes, + )) + this.#retain(current, reference); + return true; + } + + #member( + current: GetterWork, + expression: t.MemberExpression | t.OptionalMemberExpression, + ): void { + if ( + current.privateRead !== undefined && + t.isPrivateName(expression.property) + ) { + this.#privateRead(current); + return; + } + if ( + t.isPrivateName(expression.property) && + t.isClass(this.job.invocation.owner) + ) { + const name = expression.property.id.name; + this.#enqueue(() => ({ + ...current, + node: expression.object, + path: [], + privateRead: { + node: expression, + name, + path: current.path, + }, + })); + return; + } + if (!this.#superValue(current, expression)) + this.#enqueue(() => ({ + ...current, + node: expression.object, + path: [ + semanticStaticPropertyKey(expression.property, expression.computed), + ...current.path, + ], + })); + } + + #visit(current: GetterWork): void { + const expression = unwrapJavaScriptExpression(current.node).node; + if (t.isIdentifier(expression) && this.#alias(current, expression)) return; + if (t.isForOfStatement(expression)) { + this.#enqueue(() => ({ + ...current, + node: expression.right, + path: [null, ...current.path], + })); + return; + } + if (t.isThisExpression(expression) || t.isSuper(expression)) + this.#thisValue(current, expression); + else if ( + t.isMemberExpression(expression) || + t.isOptionalMemberExpression(expression) + ) + this.#member(current, expression); + else { + const branches = resultBranches(expression); + if (branches !== undefined) + for (const node of branches) + this.#enqueue(() => ({ ...current, node })); + else if (current.privateRead !== undefined) this.#privateRead(current); + else if ( + current.path.length > 0 && + (t.isObjectExpression(expression) || t.isArrayExpression(expression)) && + this.resolvers.containerReferences !== undefined + ) { + for (const reference of this.resolvers.containerReferences( + expression, + current.path, + )) + this.#enqueue(() => ({ + ...current, + node: reference.node, + path: reference.path, + })); + } else this.#retain(current, { node: expression, path: current.path }); + } + } +} + +export const expandSemanticGetterResult = ( + job: GetterQueryJob, + query: SemanticGetterQuery, + resolvers: GetterResultResolvers, +): void => new GetterResultWalker(job, query, resolvers).run(); diff --git a/src/domain/javascript/javascriptSemanticGetterResults.ts b/src/domain/javascript/javascriptSemanticGetterResults.ts index 5b8433cd82..ab47ec150b 100644 --- a/src/domain/javascript/javascriptSemanticGetterResults.ts +++ b/src/domain/javascript/javascriptSemanticGetterResults.ts @@ -1,415 +1,12 @@ import * as t from "@babel/types"; -import { - unwrapJavaScriptExpression, - semanticStaticPropertyKey, -} from "./javascriptAstValues.js"; -import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js"; -import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js"; -import { - resolveSemanticBindingState, - type JavaScriptSemanticBindingState, - type JavaScriptSemanticAnalysisState, -} from "./javascriptSemanticState.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, type JavaScriptSemanticObjectReference as Reference, } from "./javascriptSemanticObjectReferences.js"; -import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js"; -import { - SemanticGetterQuery, - type GetterQueryJob, -} from "./javascriptSemanticGetterQueries.js"; - -type ClassNode = t.ClassDeclaration | t.ClassExpression; -interface AliasVisit { - readonly identity: string; - readonly pathLength: number; - readonly parent?: AliasVisit; -} -interface GetterWork { - readonly node: t.Node; - readonly path: PropertyPath; - readonly aliases?: AliasVisit | undefined; - readonly escapes?: true; - readonly privateRead?: { - readonly node: t.MemberExpression | t.OptionalMemberExpression; - readonly name: string; - readonly path: PropertyPath; - }; -} -export interface GetterResultResolvers { - readonly classReferences: ( - lookup: { - readonly owner: ClassNode; - readonly receiver?: ClassNode; - readonly kind: "static" | "instance" | "prototype"; - }, - path: PropertyPath, - escapes: boolean, - ) => readonly Reference[]; - readonly objectReferences: ( - receiver: t.ObjectExpression, - path: PropertyPath, - ) => readonly Reference[]; - readonly privateReceiver: ( - receiver: ClassNode, - owner: ClassNode, - path: PropertyPath, - isStatic: boolean, - ) => boolean; - readonly bases: (node: t.Node) => readonly ClassNode[]; - readonly containerReferences?: ( - node: t.ObjectExpression | t.ArrayExpression, - path: PropertyPath, - ) => readonly Reference[]; -} - -/** Branch syntax contributes alternatives without evaluating application code. */ -const resultBranches = (node: t.Node): readonly t.Node[] | undefined => { - if (t.isConditionalExpression(node)) return [node.consequent, node.alternate]; - if (t.isLogicalExpression(node)) return [node.left, node.right]; - if (t.isSequenceExpression(node)) { - const last = node.expressions.at(-1); - return last === undefined ? [] : [last]; - } - if (t.isAssignmentExpression(node)) { - if (node.operator === "=") return [node.right]; - if (["&&=", "||=", "??="].includes(node.operator)) - return [node.left, node.right]; - } - return undefined; -}; - -/** Each walk owns local aliases; getter-to-getter traversal belongs to the query worklist. */ -class GetterResultWalker { - readonly #pending: GetterWork[] = []; - readonly #aliases = new Map>(); - readonly #scope: string | undefined; - - constructor( - readonly job: GetterQueryJob, - readonly query: SemanticGetterQuery, - readonly resolvers: GetterResultResolvers, - roots: readonly { - readonly node: t.Node | null; - }[] = semanticCallableResultExpressions(job.invocation.getter), - ) { - for (const { node } of roots) - if (node !== null) this.#enqueue(() => ({ node, path: job.path })); - this.#scope = query.state.scopeByNode.get(job.invocation.getter)?.scopeId; - } - - run(): void { - for (let cursor = 0; cursor < this.#pending.length; cursor++) { - const current = this.#pending[cursor]; - if (current !== undefined) this.#visit(current); - } - } - - #enqueue(create: () => GetterWork): void { - if (!this.query.reserveResult()) return; - const work = create(); - // The fallback escapes receiver roots. Keeping local suffix permutations - // here would repeat the precise expansion inside a single getter. - this.#pending.push( - this.query.widening - ? { - ...work, - path: [], - escapes: true, - ...(work.privateRead === undefined - ? {} - : { privateRead: { ...work.privateRead, path: [] } }), - } - : work, - ); - } - - #retain(current: GetterWork, reference: Reference): void { - this.query.retain(reference, current.escapes || this.job.escapes); - } - - #privateRead(current: GetterWork): void { - if (current.privateRead !== undefined) - this.#retain(current, { - node: current.privateRead.node, - path: current.escapes ? [] : current.privateRead.path, - }); - } - - #localBinding(expression: t.Identifier) { - const state = this.query.state; - const binding = resolveSemanticBindingState( - state, - expression, - expression.name, - ); - let scope = - binding === undefined ? undefined : state.scopesById.get(binding.scopeId); - while (scope !== undefined && scope.scopeId !== this.#scope) { - if (scope.kind === "function" || scope.parentScopeId === null) - return undefined; - scope = state.scopesById.get(scope.parentScopeId); - } - return scope !== undefined && - binding?.referenceInitializers.every( - (initializer) => initializer.copyKind === undefined, - ) - ? binding - : undefined; - } - - #cycle(current: GetterWork, identity: string): AliasVisit | undefined { - for (let visit = current.aliases; visit !== undefined; visit = visit.parent) - if (visit.identity === identity) return visit; - return undefined; - } - - #alias(current: GetterWork, expression: t.Identifier): boolean { - const binding = this.#localBinding(expression); - if (binding === undefined) return false; - const identity = JSON.stringify([ - binding.bindingId, - expression.start, - expression.end, - ]); - const previous = this.#cycle(current, identity); - if (previous !== undefined) { - // Growing aliases can reach arbitrarily deep children of the same roots. - if (current.path.length > previous.pathLength && !current.escapes) - this.#enqueue(() => ({ - ...current, - path: [], - aliases: undefined, - escapes: true, - })); - return true; - } - const key = JSON.stringify([ - current.path, - current.privateRead?.name, - current.privateRead?.path, - current.escapes, - ]); - const known = this.#aliases.get(identity) ?? new Set(); - if (!known.has(key)) { - known.add(key); - this.#aliases.set(identity, known); - this.#aliasOrigins(current, expression, binding, identity); - } - return true; - } - - #aliasOrigins( - current: GetterWork, - expression: t.Identifier, - binding: JavaScriptSemanticBindingState, - identity: string, - ): void { - const state = this.query.state; - const { binding: origins } = semanticBindingAtRead( - binding, - expression, - state.parentsByNode, - ); - const aliases: AliasVisit = { - identity, - pathLength: current.path.length, - ...(current.aliases === undefined ? {} : { parent: current.aliases }), - }; - for (const initializer of [ - ...origins.initializers, - ...origins.referenceInitializers.filter( - (origin) => !semanticReferenceSourcesInactive(origin, state), - ), - ]) - this.#enqueue(() => ({ - ...current, - node: initializer.node, - path: [...initializer.projection, ...current.path], - aliases, - })); - } - - #ownsThis(expression: t.ThisExpression | t.Super): boolean { - let owner = this.query.state.parentsByNode.get(expression); - while ( - owner !== undefined && - !t.isClass(owner) && - !(t.isFunction(owner) && !t.isArrowFunctionExpression(owner)) - ) - owner = this.query.state.parentsByNode.get(owner); - return owner === this.job.invocation.getter; - } - - #privateValue(current: GetterWork, owner: ClassNode): void { - const read = current.privateRead; - if (read === undefined) return; - const { getter, receiver = owner } = this.job.invocation; - const isStatic = t.isClassMethod(getter) && getter.static; - if ( - !t.isClass(receiver) || - !this.resolvers.privateReceiver(receiver, owner, current.path, isStatic) - ) { - this.#privateRead(current); - return; - } - for (const member of owner.body.body) - if ( - t.isClassPrivateProperty(member) && - member.static === isStatic && - member.key.id.name === read.name && - member.value != null - ) - this.#retain(current, { - node: member.value, - path: current.escapes ? [] : read.path, - }); - } - - #thisValue( - current: GetterWork, - expression: t.ThisExpression | t.Super, - ): void { - if (!this.#ownsThis(expression)) return; - const { getter, owner, receiver = owner } = this.job.invocation; - if (current.privateRead !== undefined && t.isClass(owner)) { - this.#privateValue(current, owner); - return; - } - const references = t.isClass(receiver) - ? this.resolvers.classReferences( - { - owner: receiver, - kind: - t.isClassMethod(getter) && getter.static ? "static" : "instance", - }, - current.path, - current.escapes || this.job.escapes, - ) - : this.resolvers.objectReferences(receiver, current.path); - for (const reference of references) this.#retain(current, reference); - } - - #superValue( - current: GetterWork, - expression: t.MemberExpression | t.OptionalMemberExpression, - ): boolean { - const { getter, owner, receiver = owner } = this.job.invocation; - if ( - !t.isSuper(expression.object) || - !t.isClass(owner) || - owner.superClass == null - ) - return false; - const key = semanticStaticPropertyKey( - expression.property, - expression.computed, - ); - for (const base of this.resolvers.bases(owner.superClass)) - for (const reference of this.resolvers.classReferences( - { - owner: base, - receiver: t.isClass(receiver) ? receiver : owner, - kind: - t.isClassMethod(getter) && getter.static ? "static" : "prototype", - }, - [key, ...current.path], - current.escapes || this.job.escapes, - )) - this.#retain(current, reference); - return true; - } - - #member( - current: GetterWork, - expression: t.MemberExpression | t.OptionalMemberExpression, - ): void { - if ( - current.privateRead !== undefined && - t.isPrivateName(expression.property) - ) { - this.#privateRead(current); - return; - } - if ( - t.isPrivateName(expression.property) && - t.isClass(this.job.invocation.owner) - ) { - const name = expression.property.id.name; - this.#enqueue(() => ({ - ...current, - node: expression.object, - path: [], - privateRead: { - node: expression, - name, - path: current.path, - }, - })); - return; - } - if (!this.#superValue(current, expression)) - this.#enqueue(() => ({ - ...current, - node: expression.object, - path: [ - semanticStaticPropertyKey(expression.property, expression.computed), - ...current.path, - ], - })); - } - - #visit(current: GetterWork): void { - const expression = unwrapJavaScriptExpression(current.node).node; - if (t.isIdentifier(expression) && this.#alias(current, expression)) return; - if (t.isForOfStatement(expression)) { - this.#enqueue(() => ({ - ...current, - node: expression.right, - path: [null, ...current.path], - })); - return; - } - if (t.isThisExpression(expression) || t.isSuper(expression)) - this.#thisValue(current, expression); - else if ( - t.isMemberExpression(expression) || - t.isOptionalMemberExpression(expression) - ) - this.#member(current, expression); - else { - const branches = resultBranches(expression); - if (branches !== undefined) - for (const node of branches) - this.#enqueue(() => ({ ...current, node })); - else if (current.privateRead !== undefined) this.#privateRead(current); - else if ( - current.path.length > 0 && - (t.isObjectExpression(expression) || t.isArrayExpression(expression)) && - this.resolvers.containerReferences !== undefined - ) { - for (const reference of this.resolvers.containerReferences( - expression, - current.path, - )) - this.#enqueue(() => ({ - ...current, - node: reference.node, - path: reference.path, - })); - } else this.#retain(current, { node: expression, path: current.path }); - } - } -} - -export const expandSemanticGetterResult = ( - job: GetterQueryJob, - query: SemanticGetterQuery, - resolvers: GetterResultResolvers, -): void => new GetterResultWalker(job, query, resolvers).run(); +import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; +import { SemanticGetterQuery } from "./javascriptSemanticGetterQueries.js"; +import { GetterResultWalker } from "./javascriptSemanticGetterResultWalker.js"; /** Project an effect operand through the same local aliases and `this` owner as getter results. */ export const semanticGetterReceiverReferences = ( diff --git a/src/domain/javascript/javascriptSemanticGlobalReference.ts b/src/domain/javascript/javascriptSemanticGlobalReference.ts new file mode 100644 index 0000000000..8f991523fc --- /dev/null +++ b/src/domain/javascript/javascriptSemanticGlobalReference.ts @@ -0,0 +1,56 @@ +import * as t from "@babel/types"; +import { electronMemberPath } from "./javascriptElectronMemberWrites.js"; +import { isUnshadowedGlobal } from "./javascriptSemanticMutationInitializers.js"; +import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; +import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js"; + +export const GLOBAL_OBJECT_NAMES = ["globalThis", "global", "window", "self"]; + +export const globalObjectName = ( + node: t.Node, + state: JavaScriptSemanticAnalysisState, +): string | undefined => + GLOBAL_OBJECT_NAMES.find((name) => isUnshadowedGlobal(node, state, name)); + +/** Match lexical global identity; null denotes a direct global root. */ +export const globalReferenceNamespace = ( + callee: t.Node, + state: JavaScriptSemanticAnalysisState, + path: readonly [string, ...string[]], +): string | null | undefined => { + const { root, members } = electronMemberPath(callee); + const matches = (expected: readonly string[]) => + members.length === expected.length && + members.every((key, index) => key === expected[index]); + if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1))) + return null; + const namespace = globalObjectName(root, state); + return namespace !== undefined && matches(path) ? namespace : undefined; +}; + +/** + * Recognize an ambient property path, optionally through a global object, without a visible + * source-local overwrite. This is a syntax-local identity fact, not proof that + * the runtime's globals have never been patched by other code. + */ +export const isUnmodifiedGlobalReference = ( + callee: t.Node, + state: JavaScriptSemanticAnalysisState, + name: string | readonly [string, ...string[]], +): boolean => { + const path: readonly [string, ...string[]] = + typeof name === "string" ? [name] : name; + if (state.ambientWrites.covers(["global", ...path])) return false; + const namespace = globalReferenceNamespace(callee, state, path); + return ( + namespace !== undefined && + (namespace === null || !state.ambientWrites.covers(["global", namespace])) + ); +}; + +/** A module URL base is known only while its property has no visible write. */ +export const isUnmodifiedImportMetaUrl = ( + node: t.Node | undefined, + state: JavaScriptSemanticAnalysisState, +): boolean => + isImportMetaUrl(node) && !state.ambientWrites.covers(["import-meta", "url"]); diff --git a/src/domain/javascript/javascriptSemanticGraphSchemas.ts b/src/domain/javascript/javascriptSemanticGraphSchemas.ts index 7e4097124a..78abf10781 100644 --- a/src/domain/javascript/javascriptSemanticGraphSchemas.ts +++ b/src/domain/javascript/javascriptSemanticGraphSchemas.ts @@ -8,8 +8,7 @@ import { applicationGraphEvidenceSchema, } from "./javascriptApplicationEvidenceSchemas.js"; import { jsonValueSchema } from "../jsonValue.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const boundedTextSchema = z.string().min(1); const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node"); @@ -101,6 +100,7 @@ export const JAVASCRIPT_SEMANTIC_RELATIONS = [ "detaches-task", "dispatches-candidate", "forwards-signal", + "influences", "listens-error", "listens-exit", "owns", @@ -149,6 +149,7 @@ export const JAVASCRIPT_SEMANTIC_RELATION_FAMILY = { "detaches-task": "promise-ownership", "dispatches-candidate": "event", "forwards-signal": "child-process", + influences: "data-flow", "listens-error": "child-process", "listens-exit": "child-process", owns: "promise-ownership", diff --git a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts index 14fc0b8eef..1b731ca877 100644 --- a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts @@ -270,13 +270,37 @@ it("keeps relevant dynamic frontiers unknown", () => { direction: "forward-influence", }), ); - expect(result.status).toBe("found"); + expect(result.status).toBe("partial"); expect(result.coverage.status).toBe("partial"); expect(result.unknowns).toMatchObject([ { reason: "dynamic-call", relation_kinds: ["calls"] }, ]); }); +it("reports partial status when retained graph facts were truncated", () => { + const graph = semanticFixtureGraph(); + const partial = createJavaScriptSemanticGraph({ + ...constructionInput(graph), + coverage: { + ...graph.coverage, + status: "partial", + truncated: true, + omitted_nodes: null, + omitted_relations: null, + limits: [{ name: "nodes", value: 1, unit: "items" }], + }, + limitations: ["The node ceiling left omission extents unknown."], + }); + const result = queryJavaScriptSemanticGraph( + partial, + javaScriptSemanticQueryInputSchema.parse({ + seed: { kind: "literal", value: "TOKEN" }, + direction: "forward-influence", + }), + ); + expect(result.status).toBe("partial"); +}); + it("excludes candidate edges unless the caller explicitly opts in", () => { const graph = graphWithCandidateEdge(); const input = { diff --git a/src/domain/javascript/javascriptSemanticIr.ts b/src/domain/javascript/javascriptSemanticIr.ts index 545b952f49..a4374f8bed 100644 --- a/src/domain/javascript/javascriptSemanticIr.ts +++ b/src/domain/javascript/javascriptSemanticIr.ts @@ -128,6 +128,8 @@ export interface JavaScriptSemanticCallSite { readonly callerCallableId: string | null; readonly location: JavaScriptSourceRange; readonly calleeLocation: JavaScriptSourceRange; + /** The call's value appears in an enclosing expression or return. */ + readonly resultUsed: boolean; readonly resolution: "exact" | "ambiguous" | "unresolved"; readonly calleeCallableIds: readonly string[]; readonly arguments: readonly JavaScriptSemanticCallArgument[]; @@ -441,6 +443,25 @@ export interface JavaScriptSemanticAssignmentFlow { readonly ownerCallableId: string | null; } +/** Syntactic inputs to one operator result; this is influence, not identity. */ +export interface JavaScriptSemanticExpressionFlow { + readonly location: JavaScriptSourceRange; + readonly operator: string; + readonly operands: readonly { + readonly location: JavaScriptSourceRange; + readonly kind: "reference" | "expression" | "literal" | "unsupported"; + readonly conditional: boolean; + readonly literal?: string | number | boolean | null; + }[]; + readonly ownerCallableId: string | null; +} + +/** A direct identifier declaration initialized by an operator expression. */ +export interface JavaScriptSemanticInitializerFlow { + readonly target: JavaScriptSourceRange; + readonly source: JavaScriptSourceRange; +} + /** Provider-neutral JavaScript semantic IR. */ export interface JavaScriptSemanticIr { readonly schema: "JavaScriptSemanticIR"; @@ -464,6 +485,13 @@ export interface JavaScriptSemanticIr { readonly boundaryOperations: readonly JavaScriptSemanticBoundaryOperation[]; readonly resourceOperations: readonly JavaScriptSemanticResourceOperation[]; readonly assignmentFlows: readonly JavaScriptSemanticAssignmentFlow[]; + readonly expressionFlows: readonly JavaScriptSemanticExpressionFlow[]; + readonly initializerFlows: readonly JavaScriptSemanticInitializerFlow[]; + readonly literalReturns: readonly { + readonly location: JavaScriptSourceRange; + readonly value: string | number | boolean | null; + }[]; + readonly expressionFlowsTruncated: boolean; readonly objectOperations: readonly JavaScriptSemanticObjectOperation[]; readonly functionFingerprints: readonly JavaScriptSemanticFunctionFingerprint[]; readonly frontiers: readonly JavaScriptSemanticFrontier[]; @@ -494,6 +522,10 @@ export const failedJavaScriptSemanticIr = (): JavaScriptSemanticIr => ({ boundaryOperations: [], resourceOperations: [], assignmentFlows: [], + expressionFlows: [], + initializerFlows: [], + literalReturns: [], + expressionFlowsTruncated: false, objectOperations: [], functionFingerprints: [], frontiers: [], diff --git a/src/domain/javascript/javascriptSemanticIterationFacts.ts b/src/domain/javascript/javascriptSemanticIterationFacts.ts new file mode 100644 index 0000000000..a6efca5827 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticIterationFacts.ts @@ -0,0 +1,19 @@ +import type * as t from "@babel/types"; +import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.js"; + +/** Shared shape of an iterator or property-read source. */ +export interface IterationSourceValue { + readonly node: t.Node; + readonly projection: JavaScriptSemanticPropertyPath; + readonly iterationSources?: readonly IterationSourceValue[]; +} + +/** An iterator receiver or property reference store, with default guards. */ +export interface SemanticIterationSource extends IterationSourceValue { + readonly iterationSources?: readonly IterationSourceValue[]; + readonly kind: "iteration" | "reference-store"; + readonly mutation: t.Node; + readonly target?: t.MemberExpression; + readonly fallbackSources?: readonly IterationSourceValue[]; + readonly requiredSources?: readonly IterationSourceValue[]; +} diff --git a/src/domain/javascript/javascriptSemanticIterationSources.ts b/src/domain/javascript/javascriptSemanticIterationSources.ts index a863e3d55e..70e8581ffa 100644 --- a/src/domain/javascript/javascriptSemanticIterationSources.ts +++ b/src/domain/javascript/javascriptSemanticIterationSources.ts @@ -12,22 +12,15 @@ import { } from "./javascriptSemanticPropertyPaths.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticCopiedReferencePath } from "./javascriptSemanticReferenceSources.js"; +import type { + IterationSourceValue, + SemanticIterationSource, +} from "./javascriptSemanticIterationFacts.js"; -interface IterationSourceValue { - readonly node: t.Node; - readonly projection: JavaScriptSemanticPropertyPath; - readonly iterationSources?: readonly IterationSourceValue[]; -} - -/** An iterator receiver or property reference store, with default guards. */ -export interface SemanticIterationSource extends IterationSourceValue { - readonly iterationSources?: readonly IterationSourceValue[]; - readonly kind: "iteration" | "reference-store"; - readonly mutation: t.Node; - readonly target?: t.MemberExpression; - readonly fallbackSources?: readonly IterationSourceValue[]; - readonly requiredSources?: readonly IterationSourceValue[]; -} +export type { + IterationSourceValue, + SemanticIterationSource, +} from "./javascriptSemanticIterationFacts.js"; /** A property value obtained by destructuring or an own-enumerable copy. */ export interface SemanticPropertyReadSource diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 01eb3b1383..2276d8a1ba 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -11,11 +11,13 @@ import { semanticStaticPropertyKey, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { semanticScopeId } from "./javascriptSemanticState.js"; -import type { - JavaScriptSemanticBindingState, - JavaScriptSemanticAnalysisState, - JavaScriptSemanticEffectTiming, +import { + resolveSemanticBindingState, + semanticResolutionBlocked, + semanticScopeId, + type JavaScriptSemanticBindingState, + type JavaScriptSemanticAnalysisState, + type JavaScriptSemanticEffectTiming, } from "./javascriptSemanticState.js"; interface StatementPosition { @@ -1207,3 +1209,100 @@ export const semanticEscapeFollowsCapture = ( positionPrecedes(capturePosition, escapePosition) ); }; + +/** Explicit wrapper facts and program `var` redeclarations retain their lexical identity. */ +export const commonJsWrapperBindingDeclaration = ( + node: t.Node, + name: string, + state: JavaScriptSemanticAnalysisState, +): JavaScriptSemanticBindingState | undefined => { + if ( + !["module", "exports", "require", "__dirname", "__filename"].includes( + name, + ) || + !t.isIdentifier(node, { name }) + ) + return undefined; + const binding = resolveSemanticBindingState(state, node, name); + if (binding === undefined) return undefined; + const scope = state.scopesById.get(binding.scopeId); + if ( + scope?.kind !== "program" || + ![...binding.declarationKinds].every((kind) => kind === "var") + ) + return undefined; + // The index owns explicit wrapper parameters, including ones with no source + // redeclaration. Babel's module classification does not invalidate that fact. + if (binding.hostOrigin === "commonjs-wrapper") return binding; + // Without an explicit host fact, preserve the legacy script-var inference. + return state.program.sourceType === "script" && + binding.declarationKinds.size > 0 + ? binding + : undefined; +}; + +/** Retained value and pattern stores share one source-ordered write boundary. */ +export const semanticBindingHasPriorValueWrite = ( + site: t.Node, + binding: JavaScriptSemanticBindingState, + state: JavaScriptSemanticAnalysisState, +): boolean => { + if (binding.definitions.some(({ kind }) => kind === "function")) return true; + const writes = state.bindingWrites.get(binding.bindingId) ?? []; + const read = semanticBindingAtRead(binding, site, state.parentsByNode); + const retained = + read.status === "ordered" + ? new Set([ + ...read.binding.initializers, + ...read.binding.referenceInitializers, + ]) + : undefined; + const representedWrites = new Set(); + const retainedWrites = new Set(); + for (const source of [ + ...binding.initializers, + ...binding.referenceInitializers, + ]) { + let owner: t.Node | undefined = source.node; + while (owner !== undefined && !writes.includes(owner)) + owner = state.parentsByNode.get(owner); + if (owner !== undefined) { + representedWrites.add(owner); + if (retained === undefined || retained.has(source)) + retainedWrites.add(owner); + } else if ( + (retained === undefined || retained.has(source)) && + semanticEffectTiming(site, source.node, state) !== "after" + ) + return true; + } + return writes.some( + (write) => + (!representedWrites.has(write) || retainedWrites.has(write)) && + semanticEffectTiming(site, write, state) !== "after", + ); +}; + +/** A program-level `var` redeclaration still denotes its untouched CJS wrapper parameter. */ +export const isUnmodifiedCommonJsWrapperBinding = ( + node: t.Node, + name: string, + state: JavaScriptSemanticAnalysisState, +): boolean => { + const binding = commonJsWrapperBindingDeclaration(node, name, state); + return ( + binding !== undefined && + !semanticBindingHasPriorValueWrite(node, binding, state) + ); +}; + +/** Identify a global name only when no lexical or dynamic environment shadows it. */ +export const isUnshadowedGlobal = ( + node: t.Node, + state: JavaScriptSemanticAnalysisState, + name: string, +): boolean => + t.isIdentifier(node, { name }) && + (resolveSemanticBindingState(state, node, name) === undefined + ? !semanticResolutionBlocked(state, node, name) + : isUnmodifiedCommonJsWrapperBinding(node, name, state)); diff --git a/src/domain/javascript/javascriptSemanticObjectReference.ts b/src/domain/javascript/javascriptSemanticObjectReference.ts new file mode 100644 index 0000000000..b826081aee --- /dev/null +++ b/src/domain/javascript/javascriptSemanticObjectReference.ts @@ -0,0 +1,19 @@ +import type * as t from "@babel/types"; +import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js"; + +/** One selected object or class property, including prototype and copy facts. */ +export interface JavaScriptSemanticObjectReference { + readonly node: t.Node; + readonly path: PropertyPath; + /** This selection crossed the object's prototype, rather than an own slot. */ + readonly inherited?: true; + /** A shallow copy selects this prefix now while retaining its child identity. */ + readonly capturedSelectionAt?: t.Node; + readonly ownEnumerable?: true; + readonly iterableFallbackPath?: PropertyPath; + readonly escapes?: true; + /** A retry omitted suffixes, so escaping constructed children must cover them. */ + readonly widenedGetterResult?: true; + /** The allocation owned by this binding, rather than a read of its prior value. */ + readonly ownerInitializer?: t.Node; +} diff --git a/src/domain/javascript/javascriptSemanticObjectReferences.ts b/src/domain/javascript/javascriptSemanticObjectReferences.ts index a400e8f3dd..7cf109bf47 100644 --- a/src/domain/javascript/javascriptSemanticObjectReferences.ts +++ b/src/domain/javascript/javascriptSemanticObjectReferences.ts @@ -19,23 +19,11 @@ import { SemanticGetterQuery, type GetterQueryJob, } from "./javascriptSemanticGetterQueries.js"; -import { expandSemanticGetterResult } from "./javascriptSemanticGetterResults.js"; +import { expandSemanticGetterResult } from "./javascriptSemanticGetterResultWalker.js"; -export interface JavaScriptSemanticObjectReference { - readonly node: t.Node; - readonly path: PropertyPath; - /** This selection crossed the object's prototype, rather than an own slot. */ - readonly inherited?: true; - /** A shallow copy selects this prefix now while retaining its child identity. */ - readonly capturedSelectionAt?: t.Node; - readonly ownEnumerable?: true; - readonly iterableFallbackPath?: PropertyPath; - readonly escapes?: true; - /** A retry omitted suffixes, so escaping constructed children must cover them. */ - readonly widenedGetterResult?: true; - /** The allocation owned by this binding, rather than a read of its prior value. */ - readonly ownerInitializer?: t.Node; -} +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; + +export type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; export const semanticClassCandidates = ( root: t.Node, diff --git a/src/domain/javascript/javascriptSemanticOpaqueEffects.ts b/src/domain/javascript/javascriptSemanticOpaqueEffects.ts index efdd17bbb7..c213557f4e 100644 --- a/src/domain/javascript/javascriptSemanticOpaqueEffects.ts +++ b/src/domain/javascript/javascriptSemanticOpaqueEffects.ts @@ -1,32 +1,19 @@ import * as t from "@babel/types"; import { javaScriptProgramStrictness } from "./javascriptSourceParser.js"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; -import { createAmbientPathResolver } from "./javascriptSemanticAmbientWrites.js"; -import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { + isUnshadowedGlobal, semanticEffectTiming, semanticBindingEffectFollowsCapture, semanticBindingEffectFollowsRead, semanticReadPrecedesEffectWithinActivation, } from "./javascriptSemanticMutationInitializers.js"; import { - isUnshadowedGlobal, resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; -/** One source owns sites and the minimal scope tree reaching them; O(AST) storage. */ -export interface JavaScriptOpaqueScopeEffects { - /** Cheap source evidence collected with declarations, before alias queries. */ - mayReferenceGlobalEval: boolean; - readonly sites: WeakSet; - readonly sloppySites: WeakSet; - readonly byScope: Map; - readonly children: Map>; - readonly globalSites: (t.CallExpression | t.OptionalCallExpression)[]; -} - export const indexSemanticOpaqueEffects = ( candidates: readonly { node: t.CallExpression; @@ -59,11 +46,30 @@ export const indexSemanticOpaqueEffects = ( * neither retain a second call list nor create another alias resolver. */ export const createSemanticOpaqueGlobalObserver = ( state: JavaScriptSemanticAnalysisState, + dependencies: { + createAmbientPathResolver: ( + state: JavaScriptSemanticAnalysisState, + observeGlobals: boolean, + ) => ( + node: t.Node, + site: t.Node | undefined, + ) => { paths: readonly (readonly (string | null)[])[] }; + createSemanticModuleInterop: ( + state: JavaScriptSemanticAnalysisState, + options?: { + intrinsicPath: (node: t.Node) => readonly string[] | undefined; + }, + ) => { + intrinsicInvocation: ( + node: t.CallExpression | t.OptionalCallExpression, + ) => { readonly path: readonly string[] } | undefined; + }; + }, ): ((node: t.Node) => void) | undefined => { if (!state.opaqueEffects.mayReferenceGlobalEval) return undefined; - const ambientPaths = createAmbientPathResolver(state, true); + const ambientPaths = dependencies.createAmbientPathResolver(state, true); let invocationSite: t.Node | undefined; - const interop = createSemanticModuleInterop(state, { + const interop = dependencies.createSemanticModuleInterop(state, { intrinsicPath: (node) => { const paths = ambientPaths(node, invocationSite).paths; const path = paths.find((path) => path[1] === "eval") ?? paths[0]; diff --git a/src/domain/javascript/javascriptSemanticPropertyKeys.ts b/src/domain/javascript/javascriptSemanticPropertyKeys.ts index 37751fa386..b33f61a43d 100644 --- a/src/domain/javascript/javascriptSemanticPropertyKeys.ts +++ b/src/domain/javascript/javascriptSemanticPropertyKeys.ts @@ -3,7 +3,7 @@ import { semanticStaticPropertyKey, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; // These intrinsic properties hold symbols, so ToPropertyKey cannot produce a diff --git a/src/domain/javascript/javascriptSemanticQueryAssessment.ts b/src/domain/javascript/javascriptSemanticQueryAssessment.ts index 001f38f95a..6079c14344 100644 --- a/src/domain/javascript/javascriptSemanticQueryAssessment.ts +++ b/src/domain/javascript/javascriptSemanticQueryAssessment.ts @@ -48,6 +48,8 @@ const queryStatus = ( ): JavaScriptSemanticQueryResult["status"] => { if (input.graph.coverage.status === "unavailable") return "unsupported"; if (input.candidateRelations > 0) return "ambiguous"; + if (input.unknowns.length > 0 || input.graph.coverage.truncated) + return "partial"; if ( input.totalSeeds === 0 || (input.hasExpectation && input.expectedMatches === 0) diff --git a/src/domain/javascript/javascriptSemanticQueryIdentity.ts b/src/domain/javascript/javascriptSemanticQueryIdentity.ts index e67fdbda96..c858ccac89 100644 --- a/src/domain/javascript/javascriptSemanticQueryIdentity.ts +++ b/src/domain/javascript/javascriptSemanticQueryIdentity.ts @@ -56,9 +56,27 @@ export const resolveJavaScriptSemanticQuerySeeds = ( ) : [], ); + const seed = input.seed; + const representedCalls = new Set( + seed.kind === "literal" + ? graph.nodes.flatMap((node) => + node.kind === "expression" && + node.properties.literal_argument_value === seed.value && + typeof node.properties.literal_argument_call_site_node_id === "string" + ? [node.properties.literal_argument_call_site_node_id] + : [], + ) + : [], + ); return graph.nodes .filter((node) => - seedMatches(node, input, fingerprintFunctions, unresolvedAccesses), + seedMatches( + node, + input, + fingerprintFunctions, + unresolvedAccesses, + representedCalls, + ), ) .map(({ node_id }) => node_id) .sort(compareUnicodeCodePoints); @@ -69,6 +87,7 @@ const seedMatches = ( input: JavaScriptSemanticQueryInput, fingerprintFunctions: ReadonlySet, unresolvedAccesses: ReadonlySet, + representedCalls: ReadonlySet, ): boolean => { const seed = input.seed; if (seed.kind === "semantic-node") return node.node_id === seed.node_id; @@ -77,6 +96,12 @@ const seedMatches = ( if (seed.kind === "function") return fingerprintFunctions.has(node.node_id); if (seed.kind === "literal") { if (node.kind === "literal") return node.properties.value === seed.value; + if ( + node.kind === "expression" && + typeof node.properties.literal_argument_call_site_node_id === "string" + ) + return node.properties.literal_argument_value === seed.value; + if (representedCalls.has(node.node_id)) return false; return semanticCallLiteralValues(node).some( (value) => value === seed.value, ); diff --git a/src/domain/javascript/javascriptSemanticReceiverFacts.ts b/src/domain/javascript/javascriptSemanticReceiverFacts.ts new file mode 100644 index 0000000000..84b6d12b71 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticReceiverFacts.ts @@ -0,0 +1,10 @@ +/** Lexical receiver identities that select static endpoint interpretations. */ +export type JavaScriptOpenReceiverFact = + | "window" + | "indexed-db" + | "cache-storage" + | "local" + | "local-indexed-db" + | "local-cache-storage" + | "configuration" + | "extension-state"; diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index 5386568620..426caf95f3 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -14,13 +14,18 @@ import { javaScriptRuntimeContext } from "./javascriptSourceParser.js"; import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js"; import { resolveSemanticBindingState, - isUnmodifiedCommonJsWrapperBinding, - commonJsWrapperBindingDeclaration, semanticResolutionBlocked, semanticStableInitializer, - semanticBindingHasPriorValueWrite, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; +import { + commonJsWrapperBindingDeclaration, + isUnmodifiedCommonJsWrapperBinding, + semanticBindingAtRead, + semanticBindingHasPriorValueWrite, + semanticEffectTiming, + semanticTopLevelEffectFollowsCapture, +} from "./javascriptSemanticMutationInitializers.js"; import { semanticStaticPropertyKey, semanticMemberChain, @@ -31,11 +36,6 @@ import { staticSourceRelativeUrlPath, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; -import { - semanticEffectTiming, - semanticBindingAtRead, - semanticTopLevelEffectFollowsCapture, -} from "./javascriptSemanticMutationInitializers.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; import { semanticIterationSources } from "./javascriptSemanticIterationSources.js"; @@ -53,22 +53,20 @@ import { type SemanticAliasSelectionPath, } from "./javascriptSemanticAliasSelections.js"; import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js"; +import { + semanticRequireOriginReaders, + type JavaScriptRequireLoaderFact, + type JavaScriptRequireOrigin, +} from "./javascriptSemanticRequireOriginSlot.js"; import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, } from "./javascriptSemanticAmbientWrites.js"; -export interface JavaScriptRequireOrigin { - readonly origin: JavaScriptModuleOrigin; - readonly call: t.CallExpression; -} - -export type JavaScriptRequireLoaderFact = - | "commonjs-loader" - | "factory-loader" - | "create-require" - | "local" - | "unknown"; +export type { + JavaScriptRequireLoaderFact, + JavaScriptRequireOrigin, +} from "./javascriptSemanticRequireOriginSlot.js"; const possibleCommonJsDelegateWriteStates = new WeakSet(); @@ -1661,3 +1659,6 @@ export const semanticMutableBuiltinOrigin = ( ? { specifier, path } : undefined; }; + +semanticRequireOriginReaders.origin = semanticRequireOrigin; +semanticRequireOriginReaders.loader = semanticRequireLoaderAt; diff --git a/src/domain/javascript/javascriptSemanticRequireOriginSlot.ts b/src/domain/javascript/javascriptSemanticRequireOriginSlot.ts new file mode 100644 index 0000000000..df53f15b56 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticRequireOriginSlot.ts @@ -0,0 +1,48 @@ +import type * as t from "@babel/types"; +import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js"; +import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; + +/** An unshadowed literal require and the member path selected from its result. */ +export interface JavaScriptRequireOrigin { + readonly origin: JavaScriptModuleOrigin; + readonly call: t.CallExpression; +} + +export type JavaScriptRequireLoaderFact = + | "commonjs-loader" + | "factory-loader" + | "create-require" + | "local" + | "unknown"; + +type RequireOriginReader = ( + node: t.Node | null | undefined, + state: JavaScriptSemanticAnalysisState, +) => JavaScriptRequireOrigin | undefined; + +type RequireLoaderReader = ( + node: t.CallExpression | t.OptionalCallExpression | t.NewExpression, + state: JavaScriptSemanticAnalysisState, + factoryBindingIds: ReadonlySet, +) => JavaScriptRequireLoaderFact; + +/** + * Require-origin queries are owned by javascriptSemanticRequireOrigin.ts, which + * also depends on evaluation. Lower layers read them through this slot so the + * import edge stays one way. The owner assigns both readers when it loads. + */ +export const semanticRequireOriginReaders: { + origin: RequireOriginReader | undefined; + loader: RequireLoaderReader | undefined; +} = { origin: undefined, loader: undefined }; + +export const readSemanticRequireOrigin: RequireOriginReader = (node, state) => + semanticRequireOriginReaders.origin?.(node, state); + +export const readSemanticRequireLoader: RequireLoaderReader = ( + node, + state, + factoryBindingIds, +) => + semanticRequireOriginReaders.loader?.(node, state, factoryBindingIds) ?? + "unknown"; diff --git a/src/domain/javascript/javascriptSemanticState.ts b/src/domain/javascript/javascriptSemanticState.ts index 1c85fcb456..946ac31977 100644 --- a/src/domain/javascript/javascriptSemanticState.ts +++ b/src/domain/javascript/javascriptSemanticState.ts @@ -1,15 +1,10 @@ -import type { JavaScriptOpaqueScopeEffects } from "./javascriptSemanticOpaqueEffects.js"; -import type { SemanticIterationSource } from "./javascriptSemanticIterationSources.js"; import * as t from "@babel/types"; import { semanticStaticPropertyKey, staticJavaScriptTruthiness, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { - semanticBindingAtRead, - semanticEffectTiming, -} from "./javascriptSemanticMutationInitializers.js"; +import type { SemanticIterationSource } from "./javascriptSemanticIterationFacts.js"; import type { JavaScriptSemanticPropertyPath, @@ -121,6 +116,17 @@ export interface JavaScriptSemanticScopeState { readonly implicitArguments?: true; } +/** One source owns sites and the minimal scope tree reaching them; O(AST) storage. */ +export interface JavaScriptOpaqueScopeEffects { + /** Cheap source evidence collected with declarations, before alias queries. */ + mayReferenceGlobalEval: boolean; + readonly sites: WeakSet; + readonly sloppySites: WeakSet; + readonly byScope: Map; + readonly children: Map>; + readonly globalSites: (t.CallExpression | t.OptionalCallExpression)[]; +} + /** Shared state for semantic collection and evaluation. */ export interface JavaScriptSemanticAnalysisState { arrayIterationUnknown: boolean; @@ -547,100 +553,3 @@ export const semanticVariableScope = ( } return candidate; }; - -/** Identify a global name only when no lexical or dynamic environment shadows it. */ -export const isUnshadowedGlobal = ( - node: t.Node, - state: JavaScriptSemanticAnalysisState, - name: string, -): boolean => - t.isIdentifier(node, { name }) && - (resolveSemanticBindingState(state, node, name) === undefined - ? !semanticResolutionBlocked(state, node, name) - : isUnmodifiedCommonJsWrapperBinding(node, name, state)); - -/** A program-level `var` redeclaration still denotes its untouched CJS wrapper parameter. */ -export const isUnmodifiedCommonJsWrapperBinding = ( - node: t.Node, - name: string, - state: JavaScriptSemanticAnalysisState, -): boolean => { - const binding = commonJsWrapperBindingDeclaration(node, name, state); - return ( - binding !== undefined && - !semanticBindingHasPriorValueWrite(node, binding, state) - ); -}; - -/** Retained value and pattern stores share one source-ordered write boundary. */ -export const semanticBindingHasPriorValueWrite = ( - site: t.Node, - binding: JavaScriptSemanticBindingState, - state: JavaScriptSemanticAnalysisState, -): boolean => { - if (binding.definitions.some(({ kind }) => kind === "function")) return true; - const writes = state.bindingWrites.get(binding.bindingId) ?? []; - const read = semanticBindingAtRead(binding, site, state.parentsByNode); - const retained = - read.status === "ordered" - ? new Set([ - ...read.binding.initializers, - ...read.binding.referenceInitializers, - ]) - : undefined; - const representedWrites = new Set(); - const retainedWrites = new Set(); - for (const source of [ - ...binding.initializers, - ...binding.referenceInitializers, - ]) { - let owner: t.Node | undefined = source.node; - while (owner !== undefined && !writes.includes(owner)) - owner = state.parentsByNode.get(owner); - if (owner !== undefined) { - representedWrites.add(owner); - if (retained === undefined || retained.has(source)) - retainedWrites.add(owner); - } else if ( - (retained === undefined || retained.has(source)) && - semanticEffectTiming(site, source.node, state) !== "after" - ) - return true; - } - return writes.some( - (write) => - (!representedWrites.has(write) || retainedWrites.has(write)) && - semanticEffectTiming(site, write, state) !== "after", - ); -}; - -/** Explicit wrapper facts and program `var` redeclarations retain their lexical identity. */ -export const commonJsWrapperBindingDeclaration = ( - node: t.Node, - name: string, - state: JavaScriptSemanticAnalysisState, -): JavaScriptSemanticBindingState | undefined => { - if ( - !["module", "exports", "require", "__dirname", "__filename"].includes( - name, - ) || - !t.isIdentifier(node, { name }) - ) - return undefined; - const binding = resolveSemanticBindingState(state, node, name); - if (binding === undefined) return undefined; - const scope = state.scopesById.get(binding.scopeId); - if ( - scope?.kind !== "program" || - ![...binding.declarationKinds].every((kind) => kind === "var") - ) - return undefined; - // The index owns explicit wrapper parameters, including ones with no source - // redeclaration. Babel's module classification does not invalidate that fact. - if (binding.hostOrigin === "commonjs-wrapper") return binding; - // Without an explicit host fact, preserve the legacy script-var inference. - return state.program.sourceType === "script" && - binding.declarationKinds.size > 0 - ? binding - : undefined; -}; diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index e2fe0f63b0..11b48f9663 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -42,7 +42,7 @@ import { type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; -import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js"; +import { readSemanticRequireOrigin } from "./javascriptSemanticRequireOriginSlot.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { readExactJavaScriptLiteral, @@ -1277,7 +1277,7 @@ const provenanceForExpression = ( expressionDepth: context.expressionDepth + unwrapped.depth, }); } - const required = semanticRequireOrigin(node, context.state); + const required = readSemanticRequireOrigin(node, context.state); if (required !== undefined) return semanticOriginsProvenance([required.origin]); if (t.isSequenceExpression(node)) { diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index d732aa1921..f892d9a9b4 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -17,7 +17,7 @@ import { semanticStaticPropertyName, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js"; +import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; import { compareUnicodeCodePoints, compositeKey, diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index 6a149cb3fc..b9498a413a 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -1,6 +1,6 @@ import type * as t from "@babel/types"; import type { JavaScriptBundlerFactoryRoles } from "./javascriptBundlerFactoryRoles.js"; -import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js"; +import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js"; import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js"; import type { JavaScriptExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js"; diff --git a/src/domain/managed/managedApplicationGraph.ts b/src/domain/managed/managedApplicationGraph.ts index f0def87f6e..51811b7be5 100644 --- a/src/domain/managed/managedApplicationGraph.ts +++ b/src/domain/managed/managedApplicationGraph.ts @@ -300,6 +300,17 @@ export const readManagedGraphProjection = ( const OPERATION = "project_managed_application_graph"; +/** Validated graph input still had no artifact source. */ +class ManagedGraphSourceError extends Error { + readonly _tag = "ManagedGraphSourceError"; + readonly code = "missing_source" as const; + + constructor() { + super("missing_source"); + this.name = "ManagedGraphSourceError"; + } +} + const chooseArtifact = ( parsed: ParsedManagedGraphInput, ): { readonly sha256: string; readonly path: string } => { @@ -307,8 +318,7 @@ const chooseArtifact = ( parsed.artifact?.result ?? parsed.members?.result ?? parsed.boundaries?.result; - if (result === undefined) - throw new Error("Validated managed graph projection has no source"); + if (result === undefined) throw new ManagedGraphSourceError(); return { sha256: result.artifact.sha256, path: result.artifact.path }; }; diff --git a/src/domain/managed/managedNativeVerificationSource.test.ts b/src/domain/managed/managedNativeVerificationSource.test.ts new file mode 100644 index 0000000000..05889d68e6 --- /dev/null +++ b/src/domain/managed/managedNativeVerificationSource.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js"; +import { createEvidence } from "../evidence.js"; +import { inspectMachoSchema } from "../native/nativeInspection.js"; +import { readManagedNativeVerificationSource } from "./managedNativeVerificationSource.js"; + +describe("managed/native verification source", () => { + it("accepts inspect_macho Evidence retained before encryption reporting", () => { + const original = MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0]; + if (original === undefined) + throw new Error( + "Managed verification example is missing native Evidence", + ); + const { encryption: _omitted, ...legacy } = inspectMachoSchema.parse( + original.normalized_result, + ); + const evidence = createEvidence(undefined, original.provider, { + operation: "inspect_macho", + parameters: original.parameters, + result: legacy, + rawResult: null, + limitations: [], + }); + const source = readManagedNativeVerificationSource({ + managed_boundaries: + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + native_observations: [evidence], + }); + if (!source.ok) throw source.error; + const [observation] = source.value.nativeObservations; + expect(observation?.supported).toBe(true); + expect(observation?.symbols.map(({ name }) => name)).toEqual( + legacy.exports.items.map(({ name }) => name), + ); + }); +}); diff --git a/src/domain/managed/managedNativeVerificationSource.ts b/src/domain/managed/managedNativeVerificationSource.ts index c49071e5bf..e5fe294467 100644 --- a/src/domain/managed/managedNativeVerificationSource.ts +++ b/src/domain/managed/managedNativeVerificationSource.ts @@ -8,7 +8,7 @@ import { analysisInputErrorFromIssues } from "../inputIssueProjection.js"; import { readEvidence, readEvidencePayload } from "../evidenceValidation.js"; import { err, ok, type Result } from "../result.js"; import { functionDossierSchema } from "../native/functionDossier.js"; -import { inspectMachoSchema } from "../native/nativeInspection.js"; +import { retainedInspectMachoSchema } from "../native/nativeInspection.js"; import { managedNativeBoundaryInspectionSchema, type ManagedNativeBoundaryInspection, @@ -55,7 +55,7 @@ const readNativeObservation = ( const evidence = read.value; if (evidence.operation === "inspect_macho") { const payload = readEvidencePayload( - inspectMachoSchema, + retainedInspectMachoSchema, evidence.normalized_result, ); if (!payload.ok) return payload; diff --git a/src/domain/native/hopperCallerReconciliation.ts b/src/domain/native/hopperCallerReconciliation.ts new file mode 100644 index 0000000000..7655e78673 --- /dev/null +++ b/src/domain/native/hopperCallerReconciliation.ts @@ -0,0 +1,102 @@ +import { z } from "zod"; + +import { jsonValueSchema } from "../jsonValue.js"; +import { unknownRelationshipCoverageSchema } from "../relationshipCoverage.js"; + +/** Hopper bridge page allowance for one procedure in a caller scan. */ +export const HOPPER_CALLER_PAGE_ALLOWANCE_BYTES = 512 * 1024; + +/** Encoded scan facts retained for one reconciliation. */ +export const HOPPER_CALLER_SCAN_RETENTION_BYTES = 4 * 1024 * 1024; + +const hopperCallerScanStopSchema = z.discriminatedUnion("code", [ + z.strictObject({ code: z.literal("complete") }), + z.strictObject({ + code: z.literal("work_budget"), + budget_ms: z.int().positive(), + }), + z.strictObject({ code: z.literal("inventory_changed") }), + z.strictObject({ + code: z.literal("page_allowance"), + allowance_bytes: z.literal(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES), + }), + z.strictObject({ + code: z.literal("retention_budget"), + allowance_bytes: z.literal(HOPPER_CALLER_SCAN_RETENTION_BYTES), + }), + z.strictObject({ code: z.literal("cancelled") }), + z.strictObject({ code: z.literal("page_failure") }), + z.strictObject({ code: z.literal("invalid_page") }), + z.strictObject({ code: z.literal("invalid_progress") }), +]); + +/** Structured progress for one Hopper caller scan, separate from its wording. */ +export const hopperCallerScanFactsSchema = z.strictObject({ + examined_procedures: z.int().nonnegative(), + procedure_total: z.int().nonnegative().nullable(), + retained_bytes: z.int().nonnegative(), + stop: hopperCallerScanStopSchema, + instructions_complete: z.boolean(), + native_callers_complete: z.boolean().nullable(), +}); + +export type HopperCallerScanFacts = z.infer; +export type HopperCallerScanStop = HopperCallerScanFacts["stop"]; + +/** Counts collected before instruction and native-caller flags are known. */ +export type HopperCallerScanCounts = Omit< + HopperCallerScanFacts, + "instructions_complete" | "native_callers_complete" +>; + +/** Native reply and forward scan facts retained when reconciliation stops. */ +export const hopperCallerReconciliationPartialSchema = z.strictObject({ + kind: z.literal("hopper-caller-reconciliation"), + native_result: jsonValueSchema, + scan: hopperCallerScanFactsSchema.nullable(), + forward_destinations: jsonValueSchema.nullable(), + unread_page: jsonValueSchema.optional(), + coverage: unknownRelationshipCoverageSchema, +}); + +export type HopperCallerReconciliationPartial = z.infer< + typeof hopperCallerReconciliationPartialSchema +>; + +const stopDetail = (stop: HopperCallerScanStop): string => { + switch (stop.code) { + case "complete": + return ""; + case "work_budget": + return `the ${String(stop.budget_ms)} ms reconciliation work budget was reached`; + case "inventory_changed": + return "the procedure inventory changed during reconciliation"; + case "page_allowance": + return `a single procedure exceeded the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte page allowance`; + case "retention_budget": + return `the ${String(stop.allowance_bytes)} byte reconciliation retention budget was reached`; + case "cancelled": + return "cancellation was observed after completed scan observations"; + case "page_failure": + return "a later scan page failed"; + case "invalid_page": + return "a scan page reply did not match the scan schema"; + case "invalid_progress": + return "a scan page reported inconsistent progress"; + } +}; + +/** Wording for scan facts. Tests compare this formatter and the facts separately. */ +export const formatHopperCallerScanReason = ( + facts: HopperCallerScanFacts, +): string => { + const total = + facts.procedure_total === null ? "unknown" : String(facts.procedure_total); + const detail = stopDetail(facts.stop); + let reason = `Hopper caller reconciliation examined ${String(facts.examined_procedures)} of ${total} procedures and retained ${String(facts.retained_bytes)} encoded bytes${detail === "" ? "" : `; ${detail}`}. Forward and reverse API coverage and consistency during the live scan remain unknown.`; + if (!facts.instructions_complete) + reason += ` Target instruction enumeration reached the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte allowance; incoming call-site coverage is incomplete.`; + if (facts.native_callers_complete === false) + reason += ` Native caller enumeration reached the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte allowance.`; + return reason; +}; diff --git a/src/domain/native/nativeAotPe.budget.test.ts b/src/domain/native/nativeAotPe.budget.test.ts index 6a77227d6c..b7fb540ee5 100644 --- a/src/domain/native/nativeAotPe.budget.test.ts +++ b/src/domain/native/nativeAotPe.budget.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import type { NativeAotParserLimits } from "./nativeAotPe.js"; +import { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; import { NATIVE_AOT_MAX_REPORT_BYTES, parseNativeAotPe, @@ -29,8 +30,15 @@ describe("NativeAOT parser resource budgets", () => { ).summary; expect(report).toMatchObject({ status: "partial", + reason: { + code: "source_byte_budget", + limit_bytes: bytes.length - 1, + observed_bytes: bytes.length, + runtime_override: false, + }, analysis_artifact_sha256: sha, header_address: null, + diagnostics: ["source_byte_budget"], coverage: { source_bytes_examined: 0, source_byte_budget_bytes: bytes.length - 1, @@ -76,17 +84,32 @@ describe("NativeAOT parser report admission", () => { it("never returns an over-budget report from early exits", () => { const bytes = nativeAotPeFixture(); const sha = nativeAotPeDigest(bytes); - expect(() => + const expectReportBudget = (run: () => void): void => { + let caught: unknown; + try { + run(); + } catch (cause: unknown) { + caught = cause; + } + expect(caught).toMatchObject({ + _tag: "AnalysisResourceConstraintError", + resource: "capacity", + retryable: false, + diagnostics: { code: "report_budget_too_small" }, + reportedLimits: { maximum_summary_bytes: 0 }, + }); + }; + expectReportBudget(() => { parseNativeAotPe(bytes, sha, 0, undefined, { sourceBytes: bytes.length - 1, workUnits: 1024, workingMemoryBytes: 1024, - }), - ).toThrow(/minimum truthful report/iu); + }); + }); const nonPe = Buffer.from("not a PE image"); - expect(() => parseNativeAotPe(nonPe, nativeAotPeDigest(nonPe), 0)).toThrow( - /minimum truthful report/iu, - ); + expectReportBudget(() => { + parseNativeAotPe(nonPe, nativeAotPeDigest(nonPe), 0); + }); }); it("does not claim a missing RTR header when signature coverage hits the work limit", () => { @@ -113,7 +136,10 @@ describe("NativeAOT parser report admission", () => { truncation_reason: "work-unit-budget", }, }); - expect(report.reason).not.toMatch(/No supported ReadyToRun directory/iu); + expect(report.reason).toEqual({ + code: "rtr_scan_budget", + runtime_override: false, + }); }); it("does not claim a missing root when table discovery is incomplete", () => { @@ -130,10 +156,10 @@ describe("NativeAOT parser report admission", () => { }, ).summary; expect(report.truncated).toBe(true); - expect(report.reason).toMatch(/before determining whether.*root exists/iu); - expect(report.reason).not.toMatch( - /no unique System\.Object.*fully examined/iu, - ); + expect(report.reason).toMatchObject({ + code: "system_object_root_unproven", + runtime_override: false, + }); }); it("explains a hydration work budget on the partial report", () => { @@ -155,18 +181,13 @@ describe("NativeAOT parser report admission", () => { workingMemoryBytes: 1024 * 1024, }, ).summary; - const visible = [summary.reason ?? "", ...summary.diagnostics].join("\n"); expect(summary.status).toBe("partial"); - expect(visible).toContain("NativeAOT parser work budget is exhausted"); - expect(visible).toContain( - "This release has no runtime override for that NativeAOT parser budget", - ); - expect(visible).toContain( - "Repeating the unchanged request fails the same way", - ); - expect(summary.diagnostics.join("\n")).toContain( - "Read-only hydration stopped:", - ); + expect(summary.reason).toEqual({ + code: "system_object_root_unproven", + runtime_override: false, + hydration: { detail: "work_budget", runtime_override: false }, + }); + expect(summary.diagnostics).toEqual(["work_budget"]); }); }); @@ -195,9 +216,7 @@ describe("NativeAOT detail and summary byte accounting", () => { low, ); expect(belowBoundary?.derived_memory ?? null).toBeNull(); - expect(belowBoundary?.diagnostics.join(" ")).toContain( - "omitted because the final serialized report exceeded its byte budget", - ); + expect(belowBoundary?.diagnostics).toContain("detail_rows_omitted"); expect( belowBoundary === undefined ? 0 @@ -221,8 +240,10 @@ describe("NativeAOT detail and summary byte accounting", () => { } } catch (cause: unknown) { // Budgets below the fixed report header cannot carry an honest summary. + expect(cause).toBeInstanceOf(AnalysisResourceConstraintError); expect(cause).toMatchObject({ - message: "NativeAOT metadata summary exceeds its report-byte budget", + diagnostics: { code: "summary_exceeds_budget" }, + retryable: false, }); } budget--; diff --git a/src/domain/native/nativeAotPe.headerRanges.test.ts b/src/domain/native/nativeAotPe.headerRanges.test.ts index 7a59bd8e8f..56995e8c1a 100644 --- a/src/domain/native/nativeAotPe.headerRanges.test.ts +++ b/src/domain/native/nativeAotPe.headerRanges.test.ts @@ -27,8 +27,7 @@ it.each([ expect(result.summary).toMatchObject({ status: "not_applicable", method_tables: 0, - reason: - "No supported ReadyToRun directory signature was found in initialized non-executable PE sections.", + reason: { code: "no_rtr_directory" }, }); }, ); @@ -50,7 +49,7 @@ it.each([ expect(result.summary).toMatchObject({ status: "not_applicable", method_tables: 0, - reason: "PE optional header is too short for image mapping fields", + reason: { code: "optional_header_too_short" }, }); }, ); diff --git a/src/domain/native/nativeAotPe.hydration.test.ts b/src/domain/native/nativeAotPe.hydration.test.ts index 68668bb683..26b89947da 100644 --- a/src/domain/native/nativeAotPe.hydration.test.ts +++ b/src/domain/native/nativeAotPe.hydration.test.ts @@ -117,12 +117,12 @@ describe("NativeAOT DEHYDRATED_DATA hydration output", () => { [ "COPY past the mapped section", [{ copy: fill(NATIVE_AOT_HYDRATION_CAPACITY + 1) }], - "Hydrated metadata exceeds its mapped PE section", + "exceeds_mapped_section", ], [ "pointer fixup past the mapped section after growth", [{ copy: fill(NATIVE_AOT_HYDRATION_CAPACITY - 4) }, { fixupPtr: TARGET }], - "Hydrated metadata exceeds its mapped PE section", + "exceeds_mapped_section", ], [ "COPY payload past DEHYDRATED_DATA after growth", @@ -130,7 +130,7 @@ describe("NativeAOT DEHYDRATED_DATA hydration output", () => { { copy: fill(INITIAL_OUTPUT + 1) }, { raw: Buffer.from([4 << 3, 1, 2]) }, ], - "DEHYDRATED_DATA COPY leaves its section", + "copy_leaves_section", ], [ "fixup index outside captured sections after growth", @@ -138,12 +138,61 @@ describe("NativeAOT DEHYDRATED_DATA hydration output", () => { { copy: fill(INITIAL_OUTPUT + 1) }, { raw: Buffer.from([(31 << 3) | 3, 0xff, 0xff, 0xff]) }, ], - "DEHYDRATED_DATA fixup leaves captured PE sections", + "fixup_leaves_sections", ], - ])("reports %s as partial without an overlay", (_case, commands, reason) => { + ])("reports %s as partial without an overlay", (_case, commands, detail) => { const { summary } = hydrate(commands); expect(summary.status).toBe("partial"); expect(summary.derived_overlay).toBeNull(); - expect(summary.diagnostics.join("\n")).toContain(reason); + expect(summary.reason).toMatchObject({ + code: "hydration_failed", + detail, + }); + expect(summary.diagnostics).toEqual([detail]); }); }); + +describe("NativeAOT hydration pointer domain", () => { + it.each([ + { + label: "negative", + imageBase: 0x10000n, + delta: -0x80000000, + }, + { + label: "above uint64", + imageBase: (1n << 64n) - 0x62000n, + delta: 0x7fffffff, + }, + ])( + "keeps a partial report when an inline pointer is $label", + ({ imageBase, delta }) => { + const { bytes } = nativeAotPeHydrationFixture([ + { inlinePtr: [NATIVE_AOT_HYDRATION_BASE] }, + ]); + const fixtureBase = 0x140000000n; + bytes.writeBigUInt64LE(imageBase, 0x98 + 24); + // Directory rows store absolute VAs. Move them with the image base so + // the directory stays admitted and hydration still runs. + for (const offset of [0x618, 0x620, 0x630, 0x638]) { + const value = bytes.readBigUInt64LE(offset); + if (value >= fixtureBase) + bytes.writeBigUInt64LE(value - fixtureBase + imageBase, offset); + } + // The first inline pointer is at RVA 0x3005. Image base 0x10000 makes + // that VA 0x13005, and delta -0x80000000 leaves the unsigned domain. + bytes.writeInt32LE(delta, 0x1605); + const summary = parseNativeAotPe( + bytes, + nativeAotPeDigest(bytes), + NATIVE_AOT_MAX_REPORT_BYTES, + ).summary; + expect(summary.status).toBe("partial"); + expect(summary.derived_overlay).toBeNull(); + expect(summary.reason).toMatchObject({ + code: "no_system_object_root", + hydration: { detail: "pointer_outside_address_domain" }, + }); + }, + ); +}); diff --git a/src/domain/native/nativeAotPe.sectionRanges.test.ts b/src/domain/native/nativeAotPe.sectionRanges.test.ts index 75d482ccd0..f873426294 100644 --- a/src/domain/native/nativeAotPe.sectionRanges.test.ts +++ b/src/domain/native/nativeAotPe.sectionRanges.test.ts @@ -63,7 +63,7 @@ it.each([ ); expect(result.summary).toMatchObject({ status: "not_applicable", - reason: "PE virtual section overlaps headers or another section", + reason: { code: "virtual_section_overlap" }, coverage: { basis: "unsupported-layout" }, }); }, @@ -138,7 +138,7 @@ it("rejects a PE header that reuses DOS header fields", () => { ); expect(result.summary).toMatchObject({ status: "not_applicable", - reason: "PE header overlaps the DOS header", + reason: { code: "pe_header_overlaps_dos" }, coverage: { basis: "unsupported-layout" }, }); }); @@ -163,7 +163,7 @@ it.each([ } else { expect(result.summary).toMatchObject({ status: "not_applicable", - reason: "PE image exceeds the 64-bit address domain", + reason: { code: "address_domain" }, coverage: { basis: "unsupported-layout" }, }); } diff --git a/src/domain/native/nativeAotPe.test.ts b/src/domain/native/nativeAotPe.test.ts index 619fab011b..ac1ecb163d 100644 --- a/src/domain/native/nativeAotPe.test.ts +++ b/src/domain/native/nativeAotPe.test.ts @@ -77,7 +77,7 @@ describe("NativeAOT indexed MethodTable closure", () => { SUMMARY_BUDGET, ); expect(parsed.summary.method_tables).toBe(0); - expect(parsed.summary.reason).toContain("unique System.Object"); + expect(parsed.summary.reason).toEqual({ code: "no_system_object_root" }); }); }); @@ -162,8 +162,7 @@ describe("read-only NativeAOT PE parser", () => { ); expect(parsed.summary).toMatchObject({ status: "not_applicable", - reason: - "No supported ReadyToRun directory signature was found in initialized non-executable PE sections.", + reason: { code: "no_rtr_directory" }, method_tables: 0, }); }); @@ -215,7 +214,7 @@ describe("read-only NativeAOT PE parser", () => { SUMMARY_BUDGET, "0x180000000", ).summary.reason, - ).toMatch(/preferred image base/iu); + ).toEqual({ code: "rebased_image" }); }); it("reports an unsupported hydration opcode as partial without mutating the snapshot", () => { @@ -231,18 +230,21 @@ describe("read-only NativeAOT PE parser", () => { expect(parsed.summary).toMatchObject({ status: "partial", derived_overlay: null, - diagnostics: [ - expect.stringContaining("Unsupported DEHYDRATED_DATA command"), - ], + reason: { + code: "hydration_failed", + detail: "unsupported_command", + command: 6, + }, + diagnostics: ["unsupported_command"], }); }); }); describe("NativeAOT PE signature admission", () => { it.each([ - ["MZ", "Not a PE image"], - ["PE", "MZ header does not declare a PE image"], - ])( + ["MZ", { code: "not_pe_image" }], + ["PE", { code: "mz_header_not_pe" }], + ] as const)( "rejects a high-bit lookalike of the %s signature", (signature, reason) => { const bytes = nativeAotPeFixture(); diff --git a/src/domain/native/nativeAotPe.ts b/src/domain/native/nativeAotPe.ts index d3073e7357..617d72771f 100644 --- a/src/domain/native/nativeAotPe.ts +++ b/src/domain/native/nativeAotPe.ts @@ -3,7 +3,20 @@ import { createHash } from "node:crypto"; import { readPeOptionalHeaderFixedFields } from "../peOptionalHeader.js"; import { readPeCoffHeader } from "../peCoffHeader.js"; import { readPeSectionHeader } from "../peSectionHeader.js"; +import { + AnalysisInputError, + AnalysisResourceConstraintError, +} from "../analysisErrorCore.js"; import type { NativeMetadataRecovery } from "./nativeMetadataRecovery.js"; +import { + NativeAotAdmissionError, + nativeAotHydrationFacts, + nativeAotHydrationReason, + WINDOWS_IMAGE_LOADER_SECTION_LIMIT, + type NativeAotHydrationFacts, + type NativeAotReason, + type NativeAotTruncationReason, +} from "./nativeAotReason.js"; const RTR_SIGNATURE = 0x00525452; const RTR_ROW_SIZE = 24; @@ -92,12 +105,10 @@ export class NativeAotSnapshotIdentityError extends Error { /** Hydration stops by throwing; other scans record truncation and continue. */ class NativeAotParserBudgetError extends Error { + readonly code = "parser_budget" as const; + constructor(readonly budget: "work" | "working-memory") { - super( - budget === "work" - ? "NativeAOT parser work budget is exhausted" - : "NativeAOT working-memory budget is exhausted", - ); + super(budget); this.name = "NativeAotParserBudgetError"; } } @@ -108,15 +119,36 @@ const parserBudgetExhausted = ( throw new NativeAotParserBudgetError(budget); }; -const NATIVE_AOT_PARSER_BUDGET_RECOVERY = - "This release has no runtime override for that NativeAOT parser budget. Repeating the unchanged request fails the same way."; +const nativeAotHydrationFailure = (cause: unknown): NativeAotHydrationFacts => { + if (cause instanceof NativeAotParserBudgetError) + return nativeAotHydrationFacts( + cause.budget === "work" ? "work_budget" : "working_memory_budget", + ); + if ( + cause instanceof NativeAotAdmissionError && + cause.reason.code === "hydration_failed" + ) + return nativeAotHydrationFacts(cause.reason.detail, cause.reason.command); + throw cause; +}; -const nativeAotHydrationFailure = (cause: unknown): string => - cause instanceof NativeAotParserBudgetError - ? `${cause.message}. ${NATIVE_AOT_PARSER_BUDGET_RECOVERY}` - : cause instanceof Error - ? cause.message - : "DEHYDRATED_DATA could not be hydrated"; +const reportBudgetRefusal = ( + code: + | "report_budget_too_small" + | "summary_exceeds_budget" + | "summary_exceeded_after_serialization", + maximumSummaryBytes: number, +): AnalysisResourceConstraintError => + new AnalysisResourceConstraintError( + "parse_native_aot_pe", + "capacity", + code, + { maximum_summary_bytes: maximumSummaryBytes }, + { + diagnostics: { code }, + retryable: false, + }, + ); type Section = { readonly rva: number; @@ -332,63 +364,111 @@ export type NativeAotString = { readonly address: string; readonly value: string; }; -export type NativeAotReport = { +type NativeAotCoverage = { + readonly pointer_candidates_examined: number; + readonly pointer_slots_examined?: number; + readonly scan_bytes_examined?: number; + readonly scan_bytes_omitted?: number; + readonly rtr_signature_scan_bytes_examined?: number; + readonly rtr_signature_scan_bytes_omitted?: number; + readonly method_table_validation_items_examined?: number; + readonly work_units_examined?: number; + readonly method_tables_recovered: number; + readonly frozen_string_candidates: number; + readonly frozen_scan_bytes_examined?: number; + readonly frozen_scan_bytes_omitted?: number; + readonly frozen_string_data_bytes_examined?: number; + readonly source_bytes_examined?: number; + readonly source_bytes_omitted?: number; + readonly work_budget_units?: number; + readonly source_byte_budget_bytes?: number; + readonly working_memory_budget_bytes?: number; + readonly working_memory_bytes_peak?: number; + readonly truncation_reason?: NativeAotTruncationReason; + readonly basis: + | "raw-image-pointer-scan" + | "raw-image-and-rehydrated-pointer-scan" + | "unsupported-layout" + | "no-rtr-header"; +}; + +type NativeAotReportBase = { readonly format: "dotnet-nativeaot"; - readonly status: "complete" | "partial" | "not_applicable"; - readonly reason: string | null; readonly analysis_artifact_sha256: string; readonly analysis_mode: "read-only-derived-overlay"; - readonly header_address: string | null; - readonly discovery: "signature-heuristic" | null; - readonly format_major: number | null; - readonly format_minor: number | null; readonly method_tables: number; readonly types: readonly NativeAotType[]; readonly frozen_strings: readonly NativeAotString[]; - readonly truncated?: boolean; - readonly types_omitted?: number; - readonly frozen_strings_omitted?: number; + readonly derived_memory: null; + readonly coverage: NativeAotCoverage; + readonly diagnostics: readonly string[]; + readonly limitations: readonly string[]; readonly output_budget_bytes?: number; +}; + +type NativeAotLocatedHeader = { + readonly header_address: string; + readonly discovery: "signature-heuristic"; + readonly format_major: number; + readonly format_minor: number; readonly derived_overlay: { readonly address: string; readonly size_bytes: number; readonly sha256: string; } | null; - readonly derived_memory: null; - readonly coverage: { - readonly pointer_candidates_examined: number; - readonly pointer_slots_examined?: number; - readonly scan_bytes_examined?: number; - readonly scan_bytes_omitted?: number; - readonly rtr_signature_scan_bytes_examined?: number; - readonly rtr_signature_scan_bytes_omitted?: number; - readonly method_table_validation_items_examined?: number; - readonly work_units_examined?: number; - readonly method_tables_recovered: number; - readonly frozen_string_candidates: number; - readonly frozen_scan_bytes_examined?: number; - readonly frozen_scan_bytes_omitted?: number; - readonly frozen_string_data_bytes_examined?: number; - readonly source_bytes_examined?: number; - readonly source_bytes_omitted?: number; - readonly work_budget_units?: number; - readonly source_byte_budget_bytes?: number; - readonly working_memory_budget_bytes?: number; - readonly working_memory_bytes_peak?: number; - readonly truncation_reason?: - | "source-byte-budget" - | "work-unit-budget" - | "working-memory-budget" - | "response-budget"; - readonly basis: - | "raw-image-pointer-scan" - | "raw-image-and-rehydrated-pointer-scan" - | "unsupported-layout" - | "no-rtr-header"; - }; - readonly diagnostics: readonly string[]; - readonly limitations: readonly string[]; }; + +type NativeAotAbsentHeader = { + readonly header_address: null; + readonly discovery: null; + readonly format_major: null; + readonly format_minor: null; + readonly derived_overlay: null; +}; + +type NativeAotTruncated = { + readonly truncated: true; + readonly types_omitted: number; + readonly frozen_strings_omitted: number; + readonly output_budget_bytes: number; +}; + +type NativeAotUntruncated = { + readonly truncated?: false; + readonly types_omitted?: number; + readonly frozen_strings_omitted?: number; +}; + +type NativeAotNotApplicableReport = NativeAotReportBase & + NativeAotAbsentHeader & + NativeAotUntruncated & { + readonly status: "not_applicable"; + readonly reason: NativeAotReason; + }; + +/** Complete reports have a header and no reason. Failures carry a reason code. */ +export type NativeAotReport = + | (NativeAotReportBase & + NativeAotLocatedHeader & + NativeAotUntruncated & { + readonly status: "complete"; + readonly reason: null; + readonly types_omitted?: 0; + readonly frozen_strings_omitted?: 0; + }) + | (NativeAotReportBase & + NativeAotLocatedHeader & + (NativeAotTruncated | NativeAotUntruncated) & { + readonly status: "partial"; + readonly reason: NativeAotReason; + }) + | (NativeAotReportBase & + NativeAotAbsentHeader & + NativeAotTruncated & { + readonly status: "partial"; + readonly reason: NativeAotReason; + }) + | NativeAotNotApplicableReport; export type NativeAotPeResult = { readonly summary: NativeAotReport; readonly readTypeDetail: ( @@ -407,7 +487,12 @@ export const nativeAotSnapshotOverCapacity = ( { format: "dotnet-nativeaot", status: "partial", - reason: `The captured PE exceeds the read-only NativeAOT snapshot byte budget of ${String(limits.sourceBytes)} bytes. This release has no runtime override.`, + reason: { + code: "source_byte_budget", + limit_bytes: limits.sourceBytes, + observed_bytes: sourceBytesAtLeast, + runtime_override: false, + }, analysis_artifact_sha256: sha256, analysis_mode: "read-only-derived-overlay", header_address: null, @@ -442,9 +527,7 @@ export const nativeAotSnapshotOverCapacity = ( truncation_reason: "source-byte-budget", basis: "unsupported-layout", }, - diagnostics: [ - `The private snapshot was observed to contain at least ${sourceBytesAtLeast} bytes, above the ${limits.sourceBytes} byte NativeAOT source limit; no parser snapshot was admitted.`, - ], + diagnostics: ["source_byte_budget"], limitations: [ "NativeAOT metadata was not inspected because the captured snapshot exceeded the local source-byte budget.", "The artifact digest identifies the selected target; no metadata facts are inferred from the omitted snapshot.", @@ -463,9 +546,7 @@ const assertNativeAotReportFits = ( maximumBytes < 0 || Buffer.byteLength(JSON.stringify(report)) > maximumBytes ) - throw new Error( - "NativeAOT report-byte budget is too small for the minimum truthful report", - ); + throw reportBudgetRefusal("report_budget_too_small", maximumBytes); return report; }; @@ -490,42 +571,52 @@ class PeImage { constructor(bytes: Buffer) { this.#bytes = bytes; if (bytes.length < 0x40 || bytes.toString("latin1", 0, 2) !== "MZ") - throw new Error("Not a PE image"); + throw new NativeAotAdmissionError({ code: "not_pe_image" }); const observed = observeMzWindowsHeader(bytes); if (observed.state !== "declared" || observed.signature !== "PE") - throw new Error("MZ header does not declare a PE image"); + throw new NativeAotAdmissionError({ code: "mz_header_not_pe" }); const peOffset = observed.offset; - if (peOffset < 0x40) throw new Error("PE header overlaps the DOS header"); + if (peOffset < 0x40) + throw new NativeAotAdmissionError({ code: "pe_header_overlaps_dos" }); this.#range(peOffset, 24); const coff = readPeCoffHeader(bytes.subarray(peOffset + 4, peOffset + 24)); - if (!coff.ok) throw new Error(coff.error.message); + if (!coff.ok) + throw new NativeAotAdmissionError({ code: "coff_header_truncated" }); this.#machine = coff.value.machine; const { sectionCount: count, optionalHeaderSize: optionalSize } = coff.value; // Microsoft documents a 96-section Windows image-loader limit. This // parser accepts executable PE images, not arbitrary COFF object files. - if (count > 96) - throw new Error("PE image exceeds the Windows 96-section limit"); + if (count > WINDOWS_IMAGE_LOADER_SECTION_LIMIT) + throw new NativeAotAdmissionError({ + code: "section_limit", + limit: WINDOWS_IMAGE_LOADER_SECTION_LIMIT, + observed: count, + }); const optional = peOffset + 24; this.#range(optional, optionalSize); // Mapping fields through SizeOfHeaders must belong to this header, // independently of whether the following file bytes are available. if (optionalSize < 64) - throw new Error( - "PE optional header is too short for image mapping fields", - ); + throw new NativeAotAdmissionError({ code: "optional_header_too_short" }); const fixed = readPeOptionalHeaderFixedFields( bytes.subarray(optional, optional + optionalSize), ); - if (!fixed.ok || fixed.value.magic !== 0x20b || this.#machine !== 0x8664) - throw new Error("Only x64 PE32+ is supported"); + if (!fixed.ok) + throw new NativeAotAdmissionError( + fixed.error.reason === "unsupported-magic" + ? { code: "unsupported_pe32_plus" } + : { code: "optional_header_truncated" }, + ); + if (fixed.value.magic !== 0x20b || this.#machine !== 0x8664) + throw new NativeAotAdmissionError({ code: "unsupported_pe32_plus" }); this.imageBase = fixed.value.imageBase; this.sizeOfImage = fixed.value.sizeOfImage; this.#sizeOfHeaders = fixed.value.sizeOfHeaders; if (count < 1 || this.sizeOfImage === 0) - throw new Error("Unsupported PE image extent"); + throw new NativeAotAdmissionError({ code: "unsupported_image_extent" }); if (this.imageBase + BigInt(this.sizeOfImage) > 1n << 64n) - throw new Error("PE image exceeds the 64-bit address domain"); + throw new NativeAotAdmissionError({ code: "address_domain" }); const sectionOffset = optional + optionalSize; this.#range(sectionOffset, count * 40); if ( @@ -533,13 +624,12 @@ class PeImage { this.#sizeOfHeaders > this.#bytes.length || this.#sizeOfHeaders > this.sizeOfImage ) - throw new Error( - "PE declared headers do not fit the captured image layout", - ); + throw new NativeAotAdmissionError({ code: "headers_do_not_fit" }); this.sections = Array.from({ length: count }, (_, index) => { const p = sectionOffset + index * 40; const parsed = readPeSectionHeader(bytes.subarray(p, p + 40)); - if (!parsed.ok) throw new Error(parsed.error.message); + if (!parsed.ok) + throw new NativeAotAdmissionError({ code: "section_header_truncated" }); const section = parsed.value; return { virtualSize: section.virtual_size, @@ -552,7 +642,7 @@ class PeImage { for (const section of this.sections) { this.#range(section.rawOffset, section.rawSize); if (section.rva + virtualExtent(section) > this.sizeOfImage) - throw new Error("PE section exceeds the declared image extent"); + throw new NativeAotAdmissionError({ code: "section_exceeds_image" }); } // Establish one virtual owner before any scan, read, permission query or // hydration. Empty sections own no bytes; adjacent intervals remain valid. @@ -562,9 +652,7 @@ class PeImage { let mappedEnd = this.#sizeOfHeaders; for (const section of mapped) { if (section.rva < mappedEnd) - throw new Error( - "PE virtual section overlaps headers or another section", - ); + throw new NativeAotAdmissionError({ code: "virtual_section_overlap" }); mappedEnd = section.rva + virtualExtent(section); } } @@ -661,7 +749,7 @@ class PeImage { size < 0 || offset > this.#bytes.length - size ) - throw new Error("PE field leaves the captured file"); + throw new NativeAotAdmissionError({ code: "field_outside_file" }); } } @@ -676,9 +764,9 @@ const readU32 = (bytes: Buffer | null, offset = 0): number | null => : bytes.readUInt32LE(offset); const notApplicable = ( sha: string, - reason: string, + reason: NativeAotReason, basis: NativeAotReport["coverage"]["basis"], -): NativeAotReport => ({ +): NativeAotNotApplicableReport => ({ format: "dotnet-nativeaot", status: "not_applicable", reason, @@ -927,22 +1015,32 @@ const hydrate = ( length < 5n || length > BigInt(image.virtualBytesRemaining(dehydrated.start)) ) - throw new Error("DEHYDRATED_DATA length is outside its mapped section"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "length_outside_section", + }); const first = image.read(dehydrated.start, Number(length)); if (first === null) - throw new Error("DEHYDRATED_DATA leaves captured PE sections"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "leaves_captured_sections", + }); const base = readRelativePointer(image, dehydrated.start); if (base === null) - throw new Error("DEHYDRATED_DATA has no hydration base pointer"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "missing_base_pointer", + }); const outputLimit = Math.min( image.virtualBytesRemaining(base), budget.limits.workUnits - budget.workUnits, budget.limits.workingMemoryBytes - budget.workingMemoryBytes, ); if (outputLimit === 0) - throw new Error( - "DEHYDRATED_DATA hydration base is outside mapped PE sections", - ); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "base_outside_mapped_sections", + }); let output = Buffer.alloc(Math.min(outputLimit, 64 * 1024)); if (!budget.reserveWorkingMemory(output.byteLength)) parserBudgetExhausted("working-memory"); @@ -961,7 +1059,10 @@ const hydrate = ( ) { if (length > budget.limits.workUnits - budget.workUnits) parserBudgetExhausted("work"); - throw new Error("Hydrated metadata exceeds its mapped PE section"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "exceeds_mapped_section", + }); } const start = outputLength; const required = start + length; @@ -995,18 +1096,31 @@ const hydrate = ( if (!budget.consumeWork(4)) parserBudgetExhausted("work"); const delta = target - (base + BigInt(outputLength)); if (delta < -0x80000000n || delta > 0x7fffffffn) - throw new Error("Hydrated relative pointer does not fit int32"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "relative_pointer_overflow", + }); reserveOutput(4).writeInt32LE(Number(delta), 0); }; const appendPtr = (target: bigint): void => { if (!budget.consumeWork(8)) parserBudgetExhausted("work"); + // writeBigUInt64LE throws RangeError outside the unsigned 64-bit domain. + // That is a malformed fixup, not an unexpected parser failure. + if (target < 0n || target > 0xffffffffffffffffn) + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "pointer_outside_address_domain", + }); reserveOutput(8).writeBigUInt64LE(target, 0); }; const readFixup = (index: number): bigint => { const fixupLocation = dehydrated.end + BigInt(index * 4); const target = readRelativePointer(image, fixupLocation); if (target === null) - throw new Error("DEHYDRATED_DATA fixup leaves captured PE sections"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "fixup_leaves_sections", + }); return target; }; try { @@ -1020,7 +1134,10 @@ const hydrate = ( const extraBytes = payload - 28; if (extraBytes > 0) { if (extraBytes > 3 || cursor > first.length - extraBytes) - throw new Error("Invalid DEHYDRATED_DATA payload encoding"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "invalid_payload_encoding", + }); payload = 0; for (let i = 0; i < extraBytes; i++) payload += (first[cursor++] ?? 0) * 2 ** (8 * i); @@ -1029,7 +1146,10 @@ const hydrate = ( switch (command) { case 0: { if (cursor > first.length - payload) - throw new Error("DEHYDRATED_DATA COPY leaves its section"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "copy_leaves_section", + }); append(first.subarray(cursor, cursor + payload)); cursor += payload; break; @@ -1049,14 +1169,20 @@ const hydrate = ( payload > Math.floor((first.length - cursor) / 4) || payload > Math.floor((outputLimit - outputLength) / 4) ) - throw new Error("Inline relative fixups leave DEHYDRATED_DATA"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "inline_relative_fixups_leave", + }); for (let i = 0; i < payload; i++) { const target = readRelativePointer( image, dehydrated.start + BigInt(cursor), ); if (target === null) - throw new Error("Invalid inline relative pointer"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "invalid_inline_relative_pointer", + }); cursor += 4; appendRel(target); } @@ -1067,19 +1193,30 @@ const hydrate = ( payload > Math.floor((first.length - cursor) / 4) || payload > Math.floor((outputLimit - outputLength) / 8) ) - throw new Error("Inline pointer fixups leave DEHYDRATED_DATA"); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "inline_pointer_fixups_leave", + }); for (let i = 0; i < payload; i++) { const target = readRelativePointer( image, dehydrated.start + BigInt(cursor), ); - if (target === null) throw new Error("Invalid inline pointer"); + if (target === null) + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "invalid_inline_pointer", + }); cursor += 4; appendPtr(target); } break; default: - throw new Error(`Unsupported DEHYDRATED_DATA command ${command}`); + throw new NativeAotAdmissionError({ + code: "hydration_failed", + detail: "unsupported_command", + command, + }); } } } catch (cause: unknown) { @@ -1247,14 +1384,30 @@ export const parseNativeAotPe = ( limits = DEFAULT_NATIVE_AOT_PARSER_LIMITS, ): NativeAotPeResult => { if (!Number.isSafeInteger(maximumSummaryBytes) || maximumSummaryBytes < 0) - throw new Error( - "NativeAOT summary budget must be a non-negative safe integer", - ); - for (const limit of Object.values(limits)) - if (!Number.isSafeInteger(limit) || limit < 0) - throw new Error( - "NativeAOT parser limits must be non-negative safe integers", - ); + throw new AnalysisInputError("parse_native_aot_pe", undefined, [ + { + path: ["maximumSummaryBytes"], + reason: "out_of_range", + minimum: 0, + ...(Number.isFinite(maximumSummaryBytes) + ? { observed: maximumSummaryBytes } + : {}), + }, + ]); + const invalidLimit = ( + Object.entries(limits) as readonly (readonly [string, number])[] + ).find(([, limit]) => !Number.isSafeInteger(limit) || limit < 0); + if (invalidLimit !== undefined) + throw new AnalysisInputError("parse_native_aot_pe", undefined, [ + { + path: ["limits", invalidLimit[0]], + reason: "out_of_range", + minimum: 0, + ...(Number.isFinite(invalidLimit[1]) + ? { observed: invalidLimit[1] } + : {}), + }, + ]); if (bytes.byteLength > limits.sourceBytes) return parsedSummary( nativeAotSnapshotOverCapacity( @@ -1272,12 +1425,9 @@ export const parseNativeAotPe = ( try { image = new PeImage(bytes); } catch (cause: unknown) { + if (!(cause instanceof NativeAotAdmissionError)) throw cause; return parsedSummary( - notApplicable( - sha256, - cause instanceof Error ? cause.message : "Unsupported PE image", - "unsupported-layout", - ), + notApplicable(sha256, cause.reason, "unsupported-layout"), maximumSummaryBytes, ); } @@ -1286,11 +1436,7 @@ export const parseNativeAotPe = ( BigInt(loadedImageBase) !== image.imageBase ) return parsedSummary( - notApplicable( - sha256, - "Ghidra loaded the PE at a base different from its preferred image base; relocation-aware metadata mapping is not implemented.", - "unsupported-layout", - ), + notApplicable(sha256, { code: "rebased_image" }, "unsupported-layout"), maximumSummaryBytes, ); const budget = new NativeAotParserBudget(limits); @@ -1301,11 +1447,13 @@ export const parseNativeAotPe = ( { ...notApplicable( sha256, - "RTR signature scanning reached the NativeAOT parser work budget; a unique ReadyToRun directory was not established.", + { code: "rtr_scan_budget", runtime_override: false }, "no-rtr-header", ), status: "partial", truncated: true, + types_omitted: 0, + frozen_strings_omitted: 0, output_budget_bytes: maximumSummaryBytes, coverage: { pointer_candidates_examined: 0, @@ -1326,26 +1474,20 @@ export const parseNativeAotPe = ( : { truncation_reason: budget.truncationReason }), basis: "no-rtr-header", }, - diagnostics: [ - "No metadata hydration was attempted because signature coverage was incomplete.", - ], + diagnostics: ["rtr_scan_incomplete"], }, maximumSummaryBytes, ); if (directorySearch.directory === null && directorySearch.matches === 0) return parsedSummary( - notApplicable( - sha256, - "No supported ReadyToRun directory signature was found in initialized non-executable PE sections.", - "no-rtr-header", - ), + notApplicable(sha256, { code: "no_rtr_directory" }, "no-rtr-header"), maximumSummaryBytes, ); if (directorySearch.matches !== 1 || directorySearch.directory === null) return parsedSummary( notApplicable( sha256, - "Multiple plausible ReadyToRun directories were found; metadata identity is ambiguous.", + { code: "ambiguous_rtr_directory" }, "unsupported-layout", ), maximumSummaryBytes, @@ -1356,7 +1498,11 @@ export const parseNativeAotPe = ( { ...notApplicable( sha256, - `RTR ${directory.major}.${directory.minor} is detected but this parser supports only the .NET 8 RTR 9.1 method-table layout.`, + { + code: "unsupported_rtr_version", + format_major: directory.major, + format_minor: directory.minor, + }, "unsupported-layout", ), status: "partial", @@ -1375,7 +1521,7 @@ export const parseNativeAotPe = ( { ...notApplicable( sha256, - "RTR 9.1 directory does not contain exactly one DEHYDRATED_DATA section; metadata recovery is incomplete.", + { code: "dehydrated_section_count" }, "unsupported-layout", ), status: "partial", @@ -1387,13 +1533,11 @@ export const parseNativeAotPe = ( maximumSummaryBytes, ); let overlay: ReturnType = null; - let hydrationError: string | null = null; - let hydrationBudgetLimited = false; + let hydration: NativeAotHydrationFacts | null = null; try { overlay = hydrate(image, dehydrated[0] as RtrSection, budget); } catch (cause: unknown) { - hydrationBudgetLimited = cause instanceof NativeAotParserBudgetError; - hydrationError = nativeAotHydrationFailure(cause); + hydration = nativeAotHydrationFailure(cause); } const discovered = discoverTables(image, overlay, budget); const frozen = directory.sections.find( @@ -1432,18 +1576,19 @@ export const parseNativeAotPe = ( frozen === undefined || frozen.end < frozenFirstCandidate + 14n ? 0 : Number((frozen.end - 14n - frozenFirstCandidate) / 8n + 1n); - const discoveryReason = + const hydrationAttachment = hydration === null ? {} : { hydration }; + const discoveryReason: NativeAotReason | null = discovered.truncated && discovered.tables.includedCount === 0 - ? "RTR 9.1 was found, but method-table discovery reached its resource budget before determining whether a unique System.Object root exists." + ? { + code: "system_object_root_unproven", + runtime_override: false, + ...hydrationAttachment, + } : discovered.tables.includedCount === 0 - ? "RTR 9.1 was found, but no unique System.Object method-table root could be recovered from the fully examined image pointers." - : hydrationError; - const recoveryReason = - hydrationBudgetLimited && - hydrationError !== null && - discoveryReason !== hydrationError - ? `${discoveryReason} ${hydrationError}` - : discoveryReason; + ? { code: "no_system_object_root", ...hydrationAttachment } + : hydration === null + ? null + : nativeAotHydrationReason(hydration); const reportLimitations = [ "Only x64 PE32+ with RTR 9.1 is supported.", "No Ghidra database types, labels, functions, or bytes are created or changed.", @@ -1455,12 +1600,12 @@ export const parseNativeAotPe = ( : []), ]; const reportFacts = { - format: "dotnet-nativeaot", - reason: recoveryReason, + format: "dotnet-nativeaot" as const, + reason: discoveryReason, analysis_artifact_sha256: sha256, - analysis_mode: "read-only-derived-overlay", + analysis_mode: "read-only-derived-overlay" as const, header_address: hex(directory.address), - discovery: "signature-heuristic", + discovery: "signature-heuristic" as const, format_major: directory.major, format_minor: directory.minor, method_tables: discovered.tables.includedCount, @@ -1473,16 +1618,10 @@ export const parseNativeAotPe = ( size_bytes: overlay.sizeBytes, sha256: overlay.sha256, }, - derived_memory: null, - diagnostics: - hydrationError === null - ? [] - : [`Read-only hydration stopped: ${hydrationError}`], + derived_memory: null as null, + diagnostics: hydration === null ? [] : [hydration.detail], limitations: reportLimitations, - } satisfies Omit< - NativeAotReport, - "status" | "types" | "frozen_strings" | "coverage" - >; + }; const coverageFacts = { pointer_candidates_examined: discovered.pointerCount, pointer_slots_examined: discovered.pointerSlotsExamined, @@ -1501,8 +1640,16 @@ export const parseNativeAotPe = ( ? "raw-image-pointer-scan" : "raw-image-and-rehydrated-pointer-scan", } satisfies Omit; - const budgetSkeleton: NativeAotReport = { + const budgetSkeleton = { ...reportFacts, + // A later truncated report replaces a null discovery reason. Reserve the + // longest truncation code so that replacement still fits the same budget. + reason: + discoveryReason ?? + ({ + code: "truncated", + truncation_reason: "working-memory-budget", + } satisfies NativeAotReason), status: "complete", types: [], frozen_strings: [], @@ -1515,7 +1662,7 @@ export const parseNativeAotPe = ( // fields so the summary budget remains valid after frozen-region work. work_units_examined: budget.limits.workUnits, working_memory_bytes_peak: budget.limits.workingMemoryBytes, - truncation_reason: "response-budget", + truncation_reason: "working-memory-budget", frozen_string_candidates: 0, frozen_scan_bytes_examined: frozenCandidateBound * 8, frozen_string_data_bytes_examined: @@ -1526,9 +1673,7 @@ export const parseNativeAotPe = ( let remainingSummaryBytes = maximumSummaryBytes - Buffer.byteLength(JSON.stringify(budgetSkeleton)); if (remainingSummaryBytes < 0) - throw new Error( - "NativeAOT metadata summary exceeds its report-byte budget", - ); + throw reportBudgetRefusal("summary_exceeds_budget", maximumSummaryBytes); let typesOmitted = 0; discovered.tables.forEachIncluded((table) => { const row: NativeAotType = { address: hex(table.address), type: null }; @@ -1611,38 +1756,66 @@ export const parseNativeAotPe = ( (typesOmitted > 0 || frozenStringsOmitted > 0 ? "response-budget" : undefined); - const summary: NativeAotReport = { - ...reportFacts, - status: - truncated || - hydrationError !== null || - discovered.tables.includedCount === 0 - ? "partial" - : "complete", - types, - frozen_strings: frozenStrings, - truncated, - types_omitted: typesOmitted, - frozen_strings_omitted: frozenStringsOmitted, - coverage: { - ...coverageFacts, - work_units_examined: budget.workUnits, - working_memory_bytes_peak: budget.workingMemoryBytesPeak, - ...(truncationReason === undefined - ? {} - : { truncation_reason: truncationReason }), - frozen_string_candidates: frozenCandidates, - frozen_scan_bytes_examined: frozenScanBytesExamined, - frozen_string_data_bytes_examined: frozenStringDataBytesExamined, - frozen_scan_bytes_omitted: Math.max( - 0, - frozenCandidateBound * 8 - frozenScanBytesExamined, - ), - }, + const partial = + truncated || hydration !== null || discovered.tables.includedCount === 0; + const publishedReason: NativeAotReason = discoveryReason ?? { + code: "truncated", + truncation_reason: truncationReason ?? "response-budget", }; + const examinedCoverage = { + ...coverageFacts, + work_units_examined: budget.workUnits, + working_memory_bytes_peak: budget.workingMemoryBytesPeak, + frozen_string_candidates: frozenCandidates, + frozen_scan_bytes_examined: frozenScanBytesExamined, + frozen_string_data_bytes_examined: frozenStringDataBytesExamined, + frozen_scan_bytes_omitted: Math.max( + 0, + frozenCandidateBound * 8 - frozenScanBytesExamined, + ), + ...(truncationReason === undefined + ? {} + : { truncation_reason: truncationReason }), + }; + const summary: NativeAotReport = partial + ? truncated + ? { + ...reportFacts, + status: "partial", + reason: publishedReason, + types, + frozen_strings: frozenStrings, + truncated: true, + types_omitted: typesOmitted, + frozen_strings_omitted: frozenStringsOmitted, + coverage: examinedCoverage, + } + : { + ...reportFacts, + status: "partial", + reason: publishedReason, + types, + frozen_strings: frozenStrings, + truncated: false, + types_omitted: 0, + frozen_strings_omitted: 0, + coverage: examinedCoverage, + } + : { + ...reportFacts, + status: "complete", + reason: null, + types, + frozen_strings: frozenStrings, + truncated: false, + types_omitted: 0, + frozen_strings_omitted: 0, + coverage: examinedCoverage, + }; if (Buffer.byteLength(JSON.stringify(summary)) > maximumSummaryBytes) - throw new Error( - "NativeAOT metadata summary exceeded its report-byte budget after serialization", + throw reportBudgetRefusal( + "summary_exceeded_after_serialization", + maximumSummaryBytes, ); const readTypeDetail = ( address: string, @@ -1694,9 +1867,7 @@ export const parseNativeAotPe = ( }; let remaining = maximumBytes - Buffer.byteLength(JSON.stringify(metadata)); if (remaining < 0) { - metadata.diagnostics.push( - "The NativeAOT detail report budget is too small for the requested MethodTable summary.", - ); + metadata.diagnostics.push("detail_budget_too_small"); metadata.truncated = true; metadata.status = "partial"; return Buffer.byteLength(JSON.stringify(metadata)) <= maximumBytes @@ -1808,14 +1979,12 @@ export const parseNativeAotPe = ( } } if (detailBudget.truncationReason !== undefined) - metadata.diagnostics.push( - "MethodTable memory hashing stopped at the parser work budget.", - ); + metadata.diagnostics.push("method_table_hash_work_budget"); metadata.interfaces_omitted = interfacesOmitted; metadata.virtual_slots_omitted = slotsOmitted; metadata.truncated = discovered.truncated || - hydrationError !== null || + hydration !== null || detailBudget.truncationReason !== undefined || interfacesOmitted > 0 || slotsOmitted > 0 || @@ -1827,9 +1996,7 @@ export const parseNativeAotPe = ( metadata.interfaces.length = 0; metadata.virtual_slots_omitted += metadata.virtual_slots.length; metadata.virtual_slots.length = 0; - metadata.diagnostics.push( - "MethodTable detail rows were omitted because the final serialized report exceeded its byte budget.", - ); + metadata.diagnostics.push("detail_rows_omitted"); metadata.truncated = true; metadata.status = "partial"; if (Buffer.byteLength(JSON.stringify(metadata)) > maximumBytes) diff --git a/src/domain/native/nativeAotReason.ts b/src/domain/native/nativeAotReason.ts new file mode 100644 index 0000000000..8d5b0aaf8e --- /dev/null +++ b/src/domain/native/nativeAotReason.ts @@ -0,0 +1,159 @@ +import { z } from "zod"; + +/** + * Windows image-loader maximum for a PE executable. This is a format + * constraint, not a REA resource budget, so it is not an environment setting. + */ +export const WINDOWS_IMAGE_LOADER_SECTION_LIMIT = 96; + +const NATIVE_AOT_HYDRATION_DETAILS = [ + "length_outside_section", + "leaves_captured_sections", + "missing_base_pointer", + "base_outside_mapped_sections", + "exceeds_mapped_section", + "relative_pointer_overflow", + "fixup_leaves_sections", + "invalid_payload_encoding", + "copy_leaves_section", + "inline_relative_fixups_leave", + "invalid_inline_relative_pointer", + "inline_pointer_fixups_leave", + "invalid_inline_pointer", + "pointer_outside_address_domain", + "unsupported_command", + "work_budget", + "working_memory_budget", +] as const; + +/** Stable hydration failure, independent of the sentence previously thrown. */ +export type NativeAotHydrationDetail = + (typeof NATIVE_AOT_HYDRATION_DETAILS)[number]; + +const NATIVE_AOT_TRUNCATION_REASONS = [ + "source-byte-budget", + "work-unit-budget", + "working-memory-budget", + "response-budget", +] as const; + +export type NativeAotTruncationReason = + (typeof NATIVE_AOT_TRUNCATION_REASONS)[number]; + +const NATIVE_AOT_SIMPLE_REASON_CODES = [ + "not_pe_image", + "mz_header_not_pe", + "pe_header_overlaps_dos", + "coff_header_truncated", + "section_header_truncated", + "optional_header_too_short", + "optional_header_truncated", + "unsupported_pe32_plus", + "unsupported_image_extent", + "address_domain", + "headers_do_not_fit", + "section_exceeds_image", + "virtual_section_overlap", + "field_outside_file", + "rebased_image", + "no_rtr_directory", + "ambiguous_rtr_directory", + "dehydrated_section_count", +] as const; + +const nativeAotHydrationFactsSchema = z.strictObject({ + detail: z.enum(NATIVE_AOT_HYDRATION_DETAILS), + command: z.int().nonnegative().optional(), + runtime_override: z.literal(false).optional(), +}); + +/** Hydration facts that can ride along with a discovery reason. */ +export type NativeAotHydrationFacts = z.infer< + typeof nativeAotHydrationFactsSchema +>; + +const nonnegative = z.int().nonnegative(); + +/** Public NativeAOT admission and recovery reason. Display text is not the code. */ +export const nativeAotReasonSchema = z.discriminatedUnion("code", [ + z.strictObject({ code: z.enum(NATIVE_AOT_SIMPLE_REASON_CODES) }), + z.strictObject({ + code: z.literal("section_limit"), + limit: z.literal(WINDOWS_IMAGE_LOADER_SECTION_LIMIT), + observed: z.int().positive(), + }), + z.strictObject({ + code: z.literal("source_byte_budget"), + limit_bytes: nonnegative, + observed_bytes: nonnegative, + runtime_override: z.literal(false), + }), + z.strictObject({ + code: z.literal("rtr_scan_budget"), + runtime_override: z.literal(false), + }), + z.strictObject({ + code: z.literal("unsupported_rtr_version"), + format_major: nonnegative, + format_minor: nonnegative, + }), + z.strictObject({ + code: z.literal("hydration_failed"), + detail: z.enum(NATIVE_AOT_HYDRATION_DETAILS), + command: z.int().nonnegative().optional(), + runtime_override: z.literal(false).optional(), + }), + z.strictObject({ + code: z.literal("no_system_object_root"), + hydration: nativeAotHydrationFactsSchema.optional(), + }), + z.strictObject({ + code: z.literal("system_object_root_unproven"), + runtime_override: z.literal(false), + hydration: nativeAotHydrationFactsSchema.optional(), + }), + z.strictObject({ + code: z.literal("truncated"), + truncation_reason: z.enum(NATIVE_AOT_TRUNCATION_REASONS), + }), +]); + +export type NativeAotReason = z.infer; + +/** Admission failure whose public identity is `reason.code`, not `message`. */ +export class NativeAotAdmissionError extends Error { + readonly code: NativeAotReason["code"]; + + constructor(readonly reason: NativeAotReason) { + super(reason.code); + this.name = "NativeAotAdmissionError"; + this.code = reason.code; + } +} + +/** Hydration detail carried on a reason without copying an Error message. */ +export const nativeAotHydrationFacts = ( + detail: NativeAotHydrationDetail, + command?: number, +): NativeAotHydrationFacts => { + const budget = + detail === "work_budget" || detail === "working_memory_budget" + ? { runtime_override: false as const } + : {}; + return command === undefined + ? { detail, ...budget } + : { detail, command, ...budget }; +}; + +const BUDGET_HYDRATION_DETAILS = new Set([ + "work_budget", + "working_memory_budget", +]); + +/** Reason for a hydration failure, including the no-override fact for parser budgets. */ +export const nativeAotHydrationReason = ( + facts: NativeAotHydrationFacts, +): NativeAotReason => + BUDGET_HYDRATION_DETAILS.has(facts.detail) + ? { code: "hydration_failed", ...facts, runtime_override: false } + : { code: "hydration_failed", ...facts }; diff --git a/src/domain/native/nativeInspection.ts b/src/domain/native/nativeInspection.ts index be8673d648..cdc445c092 100644 --- a/src/domain/native/nativeInspection.ts +++ b/src/domain/native/nativeInspection.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { bundleResourceRootSchema } from "../apple/bundleResourceRoot.js"; import { jsonRecordSchema } from "../jsonRecord.js"; import { jsonValueSchema } from "../jsonValue.js"; +import { machoSliceEncryptionSchema } from "../apple/machoEncryption.js"; const hexAddress = z.string().regex(/^0x[a-fA-F0-9]+$/u); const bytesHex = z.string().regex(/^(?:[0-9a-f]{2})*$/u); @@ -209,10 +210,32 @@ export const inspectMachoSchema = z.object({ imports: coverage(importSymbolSchema), exports: coverage(symbolSchema), segments: coverage(segmentSchema), + encryption: coverage(machoSliceEncryptionSchema).describe( + "LC_ENCRYPTION_INFO(_64) status of every slice, read from load commands only.", + ), provenance: z.array(nativeCommandInvocationSchema), limitations: z.array(z.string()), }); +/** + * Retained inspect_macho Evidence, including results recorded before + * `encryption` was reported. Not recorded is unknown, never evidence that no + * slice is encrypted. + */ +export const retainedInspectMachoSchema = inspectMachoSchema + .extend({ encryption: inspectMachoSchema.shape.encryption.optional() }) + .transform(({ encryption, ...report }) => ({ + ...report, + encryption: encryption ?? { + items: [], + total: null, + exhaustive: false, + limitations: [ + "This inspect_macho Evidence predates encryption reporting; slice encryption status is unknown.", + ], + }, + })); + export const inspectSignatureSchema = z.object({ signed: z.boolean(), identifier: z.string().nullable(), diff --git a/src/domain/native/nativeMetadataRecovery.test.ts b/src/domain/native/nativeMetadataRecovery.test.ts index 0f1d486fcb..4cacc7735c 100644 --- a/src/domain/native/nativeMetadataRecovery.test.ts +++ b/src/domain/native/nativeMetadataRecovery.test.ts @@ -59,7 +59,7 @@ describe("NativeAOT recovery evidence", () => { const summary = { format: "dotnet-nativeaot", status: "not_applicable", - reason: "No supported row layout found", + reason: { code: "no_rtr_directory" }, analysis_artifact_sha256: "b".repeat(64), header_address: null, discovery: null, @@ -74,4 +74,25 @@ describe("NativeAOT recovery evidence", () => { }; expect(nativeMetadataRecoverySummarySchema.parse(summary)).toEqual(summary); }); + it("accepts the optional Ghidra extension's string reason", () => { + // NativeAotExtension.inspectImage copies this not_applicable summary, and + // ReaGhidraExtensions adds the artifact digest before load-image parsing. + const summary = { + format: "dotnet-nativeaot", + status: "not_applicable", + reason: "No NativeAOT directory with the supported row layout was found.", + analysis_artifact_sha256: "c".repeat(64), + header_address: null, + discovery: null, + format_major: null, + format_minor: null, + method_tables: 0, + types: [], + derived_memory: null, + coverage: null, + diagnostics: [], + limitations: ["extension-limitation"], + }; + expect(nativeMetadataRecoverySummarySchema.parse(summary)).toEqual(summary); + }); }); diff --git a/src/domain/native/nativeMetadataRecovery.ts b/src/domain/native/nativeMetadataRecovery.ts index 8de5f6d9db..73b45bc0bc 100644 --- a/src/domain/native/nativeMetadataRecovery.ts +++ b/src/domain/native/nativeMetadataRecovery.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { nativeAotReasonSchema } from "./nativeAotReason.js"; const address = z.string().regex(/^0x[0-9a-f]+$/u); const relatedType = z.strictObject({ address, type: z.string().nullable() }); @@ -51,7 +52,10 @@ export const nativeMetadataRecoverySummarySchema = z.strictObject({ format: z.literal("dotnet-nativeaot"), analysis_mode: z.literal("read-only-derived-overlay").optional(), status: z.enum(["complete", "partial", "not_applicable"]), - reason: z.string().nullable(), + // The read-only parser publishes a code. The optional Ghidra extension + // still sends the historical string on not_applicable, unsupported, and + // failed image summaries. Both shapes remain valid at this boundary. + reason: z.union([nativeAotReasonSchema, z.string(), z.null()]), analysis_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u), header_address: address.nullable(), discovery: z.enum(["symbol", "signature-heuristic"]).nullable(), diff --git a/src/domain/process/processCapture.ts b/src/domain/process/processCapture.ts index 6ee34b42cd..784d4c1c4a 100644 --- a/src/domain/process/processCapture.ts +++ b/src/domain/process/processCapture.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { observedSymlinkTarget } from "./symlinkTarget.js"; +import { observedSymlinkTarget } from "../symlinkTarget.js"; import { utcDateTimeSchema } from "../offsetDateTime.js"; import { jsonValueSchema } from "../jsonValue.js"; diff --git a/src/domain/process/symlinkTarget.ts b/src/domain/process/symlinkTarget.ts index 2f13596fea..9d93b1d184 100644 --- a/src/domain/process/symlinkTarget.ts +++ b/src/domain/process/symlinkTarget.ts @@ -1,16 +1,5 @@ import type { FileState } from "./processCapture.js"; -/** Preserve stored bytes; undecodable targets have no invented text spelling. */ -export const observedSymlinkTarget = ( - raw: Buffer, -): { text: string | null; hex: string } => { - const decoded = raw.toString("utf8"); - return { - text: Buffer.from(decoded, "utf8").equals(raw) ? decoded : null, - hex: raw.toString("hex"), - }; -}; - /** Old replacement characters cannot establish the target's original bytes. */ export const symlinkTargetIdentity = ( file: Extract, diff --git a/src/domain/propertyListOpenStep.test.ts b/src/domain/propertyListOpenStep.test.ts index ddc9ca7ad4..47d6538e59 100644 --- a/src/domain/propertyListOpenStep.test.ts +++ b/src/domain/propertyListOpenStep.test.ts @@ -4,6 +4,7 @@ import { parseOpenStep } from "plist"; import { plistNestingLimitMessage } from "./apple/plistValue.js"; import { decodeOpenStepPropertyList, + OpenStepPropertyListError, startsOpenStepPropertyList, } from "./propertyListOpenStep.js"; @@ -30,28 +31,53 @@ const syntaxFailure = (read: () => unknown): Error => { throw new Error("Expected syntax failure"); }; +const containerRefusal = ( + code: OpenStepPropertyListError["code"], + position: number, +) => ({ code, position }); + +const containerRefusals = [ + ["(}", containerRefusal("unexpected_character", 1)], + ["{)", containerRefusal("unexpected_character", 1)], + ["(@", containerRefusal("unexpected_character", 1)], + ["{key @", containerRefusal("expected_equals", 5)], + ["(x @", containerRefusal("expected_separator", 3)], + [ + "(x ", + containerRefusal("unterminated_array", 3), + containerRefusal("expected_separator", 3), + ], + ["{key ", containerRefusal("expected_equals", 5)], + ["{key = x,", containerRefusal("unexpected_character", 8)], + ["{ /* unterminated", containerRefusal("unterminated_block_comment", 2)], + ["(x, /* unterminated", containerRefusal("unterminated_block_comment", 4)], + ["{ { toString = text; } @", containerRefusal("expected_equals", 23)], +] as const; + +it.each(containerRefusals)( + "reports %s before and after a deep suffix", + (text, diagnosis, deep = diagnosis) => { + expect(syntaxFailure(() => decodeOpenStepPropertyList(text))).toMatchObject( + { _tag: "OpenStepPropertyListError", ...diagnosis }, + ); + expect( + syntaxFailure(() => decodeOpenStepPropertyList(text + "(".repeat(130))), + ).toMatchObject({ _tag: "OpenStepPropertyListError", ...deep }); + }, +); + it.each([ - "(}", - "{)", - "(@", - "{key @", - "(x @", - "(x ", - "{key ", - "{key = x,", '{ key = "unterminated', '{ key = "escape\\', "{ key = <01", - "{ /* unterminated", - "(x, /* unterminated", - "{ { toString = text; } @", "{ { toString = text; } = value; }", ])( - "preserves pinned syntax diagnosis before later deep input after %s", + "keeps the pinned scalar decoder's diagnosis for %s before a deep suffix", (prefix) => { for (const text of [prefix, prefix + "(".repeat(130)]) { const pinned = syntaxFailure(() => parseOpenStep(text)); const owned = syntaxFailure(() => decodeOpenStepPropertyList(text)); + expect(owned).not.toBeInstanceOf(OpenStepPropertyListError); expect(owned.constructor).toBe(pinned.constructor); expect(owned.message).toBe(pinned.message); } @@ -89,14 +115,16 @@ it("bounds compound keys and later siblings at their owning parse", () => { ); }); -it.each(["(x,", "{ key ="])( - "retains incomplete-value syntax for %s", - (text) => { - expect(syntaxFailure(() => decodeOpenStepPropertyList(text)).message).toBe( - syntaxFailure(() => parseOpenStep(text)).message, - ); - }, -); +it.each([ + ["(x,", "unexpected_end", 3], + ["{ key =", "unexpected_end", 7], +] as const)("reports incomplete %s as %s at %s", (text, code, position) => { + expect(syntaxFailure(() => decodeOpenStepPropertyList(text))).toMatchObject({ + _tag: "OpenStepPropertyListError", + code, + position, + }); +}); it.each(["\n", "\r\n", "\r", "\u2028", "\u2029"])( "ends OpenStep line comments at %j without dropping following values", @@ -134,5 +162,10 @@ it("does not treat nonterminating whitespace or quoted comment text as line ends ); expect(() => decodeOpenStepPropertyList("// comment without a line end"), - ).toThrow("Unexpected end of input"); + ).toThrow(OpenStepPropertyListError); + expect( + syntaxFailure(() => + decodeOpenStepPropertyList("// comment without a line end"), + ), + ).toMatchObject({ code: "unexpected_end" }); }); diff --git a/src/domain/propertyListOpenStep.ts b/src/domain/propertyListOpenStep.ts index 1ef00773ca..cd54c6c51c 100644 --- a/src/domain/propertyListOpenStep.ts +++ b/src/domain/propertyListOpenStep.ts @@ -3,6 +3,31 @@ import { parseOpenStep } from "plist"; import { MAX_PLIST_DEPTH, PlistNestingLimitError } from "./apple/plistValue.js"; import { markPlistDictionary } from "./apple/plistUid.js"; +const OPEN_STEP_SYNTAX_CODES = [ + "unterminated_block_comment", + "unexpected_end", + "unterminated_dictionary", + "expected_equals", + "unterminated_array", + "expected_separator", + "unexpected_character", +] as const; + +export type OpenStepPropertyListCode = (typeof OPEN_STEP_SYNTAX_CODES)[number]; + +/** OpenStep syntax refusal. `code` is the contract; `position` is the source offset. */ +export class OpenStepPropertyListError extends Error { + readonly _tag = "OpenStepPropertyListError"; + + constructor( + readonly code: OpenStepPropertyListCode, + readonly position?: number, + ) { + super(code); + this.name = "OpenStepPropertyListError"; + } +} + /** Locate the first value for the XML owner's existing OpenStep autodetection. */ export const startsOpenStepPropertyList = (text: string): boolean => { const cursor = skipTrivia(text, 0, false); @@ -25,7 +50,11 @@ const skipTrivia = (text: string, start: number, strict = true): number => { } else if (text.startsWith("/*", cursor)) { const end = text.indexOf("*/", cursor + 2); if (end < 0) { - if (strict) throw new Error("Unterminated block comment"); + if (strict) + throw new OpenStepPropertyListError( + "unterminated_block_comment", + cursor, + ); return text.length; } cursor = end + 2; @@ -51,7 +80,8 @@ export const decodeOpenStepPropertyList = ( }; const value = (depth: number, dictionaryKey = false): unknown => { trivia(); - if (cursor >= text.length) throw new Error("Unexpected end of input"); + if (cursor >= text.length) + throw new OpenStepPropertyListError("unexpected_end", cursor); const character = text[cursor]!; if (character === "{") { assertDepth(depth); @@ -60,7 +90,11 @@ export const decodeOpenStepPropertyList = ( markPlistDictionary(dictionary); while (true) { trivia(); - if (cursor >= text.length) throw new Error("Unterminated dictionary"); + if (cursor >= text.length) + throw new OpenStepPropertyListError( + "unterminated_dictionary", + cursor, + ); if (text[cursor] === "}") { cursor += 1; return dictionary; @@ -68,9 +102,7 @@ export const decodeOpenStepPropertyList = ( const key = value(depth + 1, true); trivia(); if (text[cursor] !== "=") - throw new Error( - `Expected '=' after key "${String(key)}" at position ${cursor}`, - ); + throw new OpenStepPropertyListError("expected_equals", cursor); cursor += 1; const entry = value(depth + 1); Object.defineProperty(dictionary, String(key), { @@ -95,13 +127,14 @@ export const decodeOpenStepPropertyList = ( while (true) { array.push(value(depth + 1)); trivia(); - if (cursor >= text.length) throw new Error("Unterminated array"); + if (cursor >= text.length) + throw new OpenStepPropertyListError("unterminated_array", cursor); if (text[cursor] === ")") { cursor += 1; return array; } if (text[cursor] !== ",") - throw new Error(`Expected ',' or ')' in array at position ${cursor}`); + throw new OpenStepPropertyListError("expected_separator", cursor); cursor += 1; trivia(); if (text[cursor] === ")") { @@ -125,9 +158,7 @@ export const decodeOpenStepPropertyList = ( while (cursor < text.length && /[a-zA-Z0-9._/$:-]/u.test(text[cursor]!)) cursor += 1; if (cursor === start) - throw new Error( - `Unexpected character '${character}' at position ${cursor}`, - ); + throw new OpenStepPropertyListError("unexpected_character", cursor); } // Scalar dictionary keys do not add source-value nesting; compound keys do. if (!dictionaryKey) assertDepth(depth); diff --git a/src/domain/symlinkTarget.ts b/src/domain/symlinkTarget.ts new file mode 100644 index 0000000000..70f6d064aa --- /dev/null +++ b/src/domain/symlinkTarget.ts @@ -0,0 +1,10 @@ +/** Preserve stored bytes; undecodable targets have no invented text spelling. */ +export const observedSymlinkTarget = ( + raw: Buffer, +): { text: string | null; hex: string } => { + const decoded = raw.toString("utf8"); + return { + text: Buffer.from(decoded, "utf8").equals(raw) ? decoded : null, + hex: raw.toString("hex"), + }; +}; diff --git a/src/domain/webCaptureDiff.ts b/src/domain/webCaptureDiff.ts index 5fc7aa6e09..b1e799267e 100644 --- a/src/domain/webCaptureDiff.ts +++ b/src/domain/webCaptureDiff.ts @@ -1132,6 +1132,180 @@ const domDimension = ( }; }; +type ScriptMapCandidate = { + readonly metadata: Metadata; + readonly declaration: string | null; +}; +type ScriptMapSlot = ScriptMapCandidate & { + readonly unknown: boolean; +}; + +/** A declaration needs more placements than its exact slots plus every unknown. */ +const declarationCountsExceed = ( + needed: readonly ScriptMapCandidate[], + slots: readonly ScriptMapSlot[], +): boolean => { + if (needed.length > slots.length) return true; + let unknown = 0; + const exact = new Map(); + for (const slot of slots) { + if (slot.unknown) { + unknown += 1; + continue; + } + exact.set(slot.declaration, (exact.get(slot.declaration) ?? 0) + 1); + } + const required = new Map(); + for (const item of needed) + required.set(item.declaration, (required.get(item.declaration) ?? 0) + 1); + let surplus = 0; + for (const [declaration, count] of required) { + const available = exact.get(declaration) ?? 0; + if (count > available + unknown) return true; + if (count > available) surplus += count - available; + } + return surplus > unknown; +}; + +const metadataEntries = (metadata: Metadata) => { + const record = metadata as Record< + string, + string | number | boolean | undefined + >; + return Object.keys(record) + .sort() + .map((key) => [key, record[key]] as const); +}; + +type CountedScriptMapGroup = ScriptMapCandidate & { + count: number; +}; +type CountedScriptMapSlot = ScriptMapSlot & { + count: number; +}; + +const groupScriptMapFacts = < + Metadata, + Item extends ScriptMapCandidate, +>( + items: readonly Item[], + keyFor: (item: Item) => string, +): Array => { + const groups = new Map(); + for (const item of items) { + const key = keyFor(item); + const existing = groups.get(key); + if (existing === undefined) groups.set(key, { ...item, count: 1 }); + else existing.count += 1; + } + return [...groups.values()]; +}; + +/** Every declaration can use every slot, so counts already decide the result. */ +const scriptMapMetadataFitsAll = ( + needed: readonly CountedScriptMapGroup[], + slots: readonly CountedScriptMapSlot[], + metadataCompatible: (left: Metadata, right: Metadata) => boolean, +): boolean => + needed.every((item) => + slots.every((slot) => metadataCompatible(item.metadata, slot.metadata)), + ); + +/** + * True when identical groups cannot be placed. Flow moves whole counts, so + * repeated scripts do not become a dense assignment graph. + */ +const scriptMapGroupAssignmentExceeds = ( + needed: readonly CountedScriptMapGroup[], + slots: readonly CountedScriptMapSlot[], + metadataCompatible: (left: Metadata, right: Metadata) => boolean, +): boolean => { + const residual = slots.map((slot) => slot.count); + const demand = needed.map((item) => item.count); + const edges = needed.map((item) => + slots.flatMap((slot, index) => + metadataCompatible(item.metadata, slot.metadata) && + (slot.unknown || slot.declaration === item.declaration) + ? [index] + : [], + ), + ); + const flow = new Map(); + const flowKey = (needIndex: number, slotIndex: number): string => + `${String(needIndex)}:${String(slotIndex)}`; + const send = (needIndex: number, seen: Set, cap: number): number => { + for (const slotIndex of edges[needIndex] ?? []) { + if (seen.has(slotIndex)) continue; + const free = residual[slotIndex] ?? 0; + if (free <= 0) continue; + seen.add(slotIndex); + const pushed = Math.min(cap, free); + residual[slotIndex] = free - pushed; + flow.set( + flowKey(needIndex, slotIndex), + (flow.get(flowKey(needIndex, slotIndex)) ?? 0) + pushed, + ); + return pushed; + } + for (const slotIndex of edges[needIndex] ?? []) { + if (seen.has(slotIndex)) continue; + seen.add(slotIndex); + for (let other = 0; other < needed.length; other += 1) { + if (other === needIndex) continue; + const moved = flow.get(flowKey(other, slotIndex)) ?? 0; + if (moved <= 0) continue; + const rerouted = send(other, seen, Math.min(cap, moved)); + if (rerouted <= 0) continue; + flow.set(flowKey(other, slotIndex), moved - rerouted); + flow.set( + flowKey(needIndex, slotIndex), + (flow.get(flowKey(needIndex, slotIndex)) ?? 0) + rerouted, + ); + return rerouted; + } + } + return 0; + }; + for (let index = 0; index < needed.length; index += 1) + while ((demand[index] ?? 0) > 0) { + const pushed = send(index, new Set(), demand[index] ?? 0); + if (pushed === 0) return true; + demand[index] = (demand[index] ?? 0) - pushed; + } + return false; +}; + +/** + * True when observed declarations cannot be placed on the other capture. + * Identical scripts share one count, so repeated declarations are not retried + * as separate permutations. + */ +const scriptMapFactsExceed = ( + needed: readonly ScriptMapCandidate[], + slots: readonly ScriptMapSlot[], + metadataCompatible: (left: Metadata, right: Metadata) => boolean, +): boolean => { + if (needed.length === 0) return false; + if (declarationCountsExceed(needed, slots)) return true; + const needGroups = groupScriptMapFacts(needed, (item) => + compositeKey([metadataEntries(item.metadata), item.declaration]), + ); + const slotGroups = groupScriptMapFacts(slots, (slot) => + compositeKey([ + metadataEntries(slot.metadata), + slot.declaration, + slot.unknown, + ]), + ); + if (scriptMapMetadataFitsAll(needGroups, slotGroups, metadataCompatible)) + return false; + return scriptMapGroupAssignmentExceeds( + needGroups, + slotGroups, + metadataCompatible, + ); +}; + const scriptsDimension = ( before: WebPageInspection, after: WebPageInspection, @@ -1157,6 +1331,25 @@ const scriptsDimension = ( const afterComplete = sectionsComplete(after, ["scripts"]) && after.completeness.dropped_events.scripts === 0; + // A null declaration may represent an omitted URL. The capture ledger is + // section-wide, so it cannot identify which null script was affected. + // Unavailable map bodies alone do not erase an observed declaration. + const declarationMayBeOmitted = (inspection: WebPageInspection) => + inspection.completeness.policy_filtered_sections.includes("source_maps") || + inspection.completeness.truncated_sections.includes("source_maps") || + inspection.completeness.excluded.some( + (item) => + item.section === "source_maps" && + item.count !== 0 && + [ + "disallowed_origin", + "unsupported_url", + "resource_budget_exhausted", + "invalid_protocol_value", + ].includes(item.reason), + ); + const beforeMayOmitDeclaration = declarationMayBeOmitted(before); + const afterMayOmitDeclaration = declarationMayBeOmitted(after); const left = new Map(); const right = new Map(); let valuesComplete = true; @@ -1171,6 +1364,94 @@ const scriptsDimension = ( prior.length > 0 && later.length > 0 && all.every((script) => script.source.included); + const mapComparable = + prior.every( + (script) => script.source_map_url !== null || !beforeMayOmitDeclaration, + ) && + later.every( + (script) => script.source_map_url !== null || !afterMayOmitDeclaration, + ); + // Match declarations only within the same fully observed metadata group. + // An omitted declaration can explain one value in its own group, not a + // declaration associated with a different observed script hash. + const metadataFields = [ + "cdp_hash", + "length", + "is_module", + "language", + ] as const; + type ScriptMetadata = Partial< + Record< + (typeof metadataFields)[number] | "source_sha256", + string | number | boolean + > + >; + const scriptMetadata = (script: Script): ScriptMetadata => { + const metadata: ScriptMetadata = {}; + for (const field of metadataFields) { + const value = script[field]; + if (value !== null) metadata[field] = value; + } + if (sourceComparable && script.source.included) + metadata.source_sha256 = script.source.artifact.sha256; + return metadata; + }; + const metadataCompatible = ( + left: ScriptMetadata, + right: ScriptMetadata, + ): boolean => + [...metadataFields, "source_sha256" as const].every( + (field) => + left[field] === undefined || + right[field] === undefined || + left[field] === right[field], + ); + // Each script keeps the metadata it actually observed. A null field does + // not erase that field for the rest of the URL group, and an omitted + // declaration can explain only one compatible script. + const mapFactsExceed = ( + observed: readonly Script[], + observedMayOmit: boolean, + complete: readonly Script[], + completeMayOmit: boolean, + ): boolean => { + const needed = observed.flatMap((script) => + script.source_map_url === null && observedMayOmit + ? [] + : [ + { + metadata: scriptMetadata(script), + declaration: script.source_map_url, + }, + ], + ); + const slots = complete.map((script) => ({ + metadata: scriptMetadata(script), + declaration: script.source_map_url, + unknown: script.source_map_url === null && completeMayOmit, + })); + return scriptMapFactsExceed(needed, slots, metadataCompatible); + }; + const provenMapDifference = + !mapComparable && + prior.length > 0 && + later.length > 0 && + ((afterComplete && + mapFactsExceed( + prior, + beforeMayOmitDeclaration, + later, + afterMayOmitDeclaration, + )) || + (beforeComplete && + mapFactsExceed( + later, + afterMayOmitDeclaration, + prior, + beforeMayOmitDeclaration, + ))); + if (prior.length > 0 && later.length > 0 && !mapComparable) + valuesComplete = false; if ( prior.length > 0 && later.length > 0 && @@ -1186,7 +1467,7 @@ const scriptsDimension = ( items.map((script) => digestCanonicalValue({ url: script.url, - source_map_url: script.source_map_url, + ...(mapComparable ? { source_map_url: script.source_map_url } : {}), ...Object.fromEntries(fields.map((field) => [field, script[field]])), ...(sourceComparable && script.source.included ? { source_sha256: script.source.artifact.sha256 } @@ -1196,21 +1477,34 @@ const scriptsDimension = ( const priorSamples = project(prior); const laterSamples = project(later); if ( + !provenMapDifference && !(beforeComplete && afterComplete) && !(beforeComplete && !samplesContained(laterSamples, priorSamples)) && !(afterComplete && !samplesContained(priorSamples, laterSamples)) ) continue; if (prior.length > 0) - left.set(identity, digestCanonicalValue(priorSamples.sort())); + left.set( + identity, + digestCanonicalValue({ + samples: priorSamples.sort(), + proof: provenMapDifference ? "before" : null, + }), + ); if (later.length > 0) - right.set(identity, digestCanonicalValue(laterSamples.sort())); + right.set( + identity, + digestCanonicalValue({ + samples: laterSamples.sort(), + proof: provenMapDifference ? "after" : null, + }), + ); } return compareDimension( left, right, beforeComplete && afterComplete && valuesComplete, - "Script inventory or corresponding source/metadata evidence was incomplete in at least one observation.", + "Script inventory, source-map declarations, or corresponding source/metadata evidence was incomplete in at least one observation.", true, // Selected map entries already carry their directional absence proof. ); }; diff --git a/src/domain/webCaptureFrameComparison.ts b/src/domain/webCaptureFrameComparison.ts index e85f03e2c1..e345f35be1 100644 --- a/src/domain/webCaptureFrameComparison.ts +++ b/src/domain/webCaptureFrameComparison.ts @@ -1,6 +1,16 @@ import type { WebPageInspection } from "./browserObservationSchemas.js"; import { digestCanonicalValue } from "./canonicalDigest.js"; +/** Frame-forest invariant. The code is the stable identity. */ +class WebCaptureFrameError extends Error { + readonly _tag = "WebCaptureFrameError"; + + constructor(readonly code: "missing_ready_frame" | "missing_parent_frame") { + super(code); + this.name = "WebCaptureFrameError"; + } +} + /** Compare complete frame forests without treating capture-local IDs as identity. */ export const fingerprintWebCaptureFrames = ( frames: WebPageInspection["frames"], @@ -20,7 +30,8 @@ export const fingerprintWebCaptureFrames = ( let examined = 0; for (let index = 0; index < ready.length; index += 1) { const frame = ready[index]; - if (frame === undefined) throw new Error("Missing ready frame"); + if (frame === undefined) + throw new WebCaptureFrameError("missing_ready_frame"); const fingerprint = digestCanonicalValue({ url: frame.url, origin: frame.origin, @@ -40,7 +51,7 @@ export const fingerprintWebCaptureFrames = ( if (count === 0) { const parent = byId.get(parentId); if (parent === undefined) - throw new Error("Missing admitted parent frame"); + throw new WebCaptureFrameError("missing_parent_frame"); ready.push(parent); } } diff --git a/src/evm/EvmAdmissionError.ts b/src/evm/EvmAdmissionError.ts new file mode 100644 index 0000000000..7f0993c979 --- /dev/null +++ b/src/evm/EvmAdmissionError.ts @@ -0,0 +1,31 @@ +const EVM_ADMISSION_CODES = [ + "prlimit_not_absolute", + "prlimit_not_file", + "cleanup_incomplete", + "cleanup_unconfirmed", + "marker_mismatch", + "request_path_missing", + "tag_or_float", + "text_bytes_unavailable", + "collection_exceeds_bound", + "nesting_exceeds_profile", + "value_outside_profile", + "repeated_map_key", + "root_not_map", +] as const; + +export type EvmAdmissionCode = (typeof EVM_ADMISSION_CODES)[number]; + +/** EVM limiter, worker, or metadata admission failure. `code` is the stable identity. */ +export class EvmAdmissionError extends Error { + readonly _tag = "EvmAdmissionError"; + + constructor( + readonly code: EvmAdmissionCode, + readonly detail?: Readonly>, + options?: ErrorOptions, + ) { + super(code, options); + this.name = "EvmAdmissionError"; + } +} diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts index fd532d6aee..4f50055207 100644 --- a/src/evm/EvmoleInterfaceProvider.ts +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -1,4 +1,6 @@ +import { EvmAdmissionError } from "./EvmAdmissionError.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; +import type { JsonValue } from "../domain/jsonValue.js"; import { safeParseUtf8Json } from "../domain/safeJson.js"; import { createHash, randomUUID } from "node:crypto"; import { access, readFile, stat, writeFile } from "node:fs/promises"; @@ -110,7 +112,7 @@ export interface EvmLimiterProbeOptions { type EvmLimiterSelection = { readonly path: string; - readonly diagnostics: Readonly>; + readonly diagnostics: Readonly>; }; type EvmLimiterUnavailable = EvmLimiterSelection & { readonly reason: string; @@ -142,20 +144,29 @@ const selectEvmLimiter = async ( const diagnostics = { limiter_path: path, platform, arch }; try { if (!isAbsolute(path)) - throw new Error( - "REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.", - ); + throw new EvmAdmissionError("prlimit_not_absolute", { + setting: "REA_EVM_PRLIMIT_COMMAND", + }); const fs = probes?.fs ?? { access, stat }; await fs.access(path, constants.X_OK); throwIfAborted(); if (!(await fs.stat(path)).isFile()) - throw new Error( - "Configured prlimit must be a regular executable file, not a directory.", - ); + throw new EvmAdmissionError("prlimit_not_file"); throwIfAborted(); return ok({ path, diagnostics }); } catch (cause: unknown) { throwIfAborted(); + if (cause instanceof EvmAdmissionError) + return err({ + path, + diagnostics: { + ...diagnostics, + code: cause.code, + ...cause.detail, + }, + reason: cause.code, + cause, + }); const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`; return err({ path, diagnostics, reason, cause }); } @@ -452,10 +463,13 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort { stopped.status === "incomplete" || stopped.status === "not-owned" ) - throw new Error( + throw new EvmAdmissionError( stopped.status === "incomplete" - ? stopped.reason - : "Owned EVM worker cleanup did not confirm ownership.", + ? "cleanup_incomplete" + : "cleanup_unconfirmed", + stopped.status === "incomplete" + ? { reason: stopped.reason } + : undefined, { cause: stopped }, ); }, @@ -511,6 +525,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort { selectedLimiter.error.reason, { cause: selectedLimiter.error.cause, + diagnostics: selectedLimiter.error.diagnostics, userMessage: selectedLimiter.error.reason, }, ); @@ -813,9 +828,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort { marker = completed; } if (!marker.bytes.equals(Buffer.from("F"))) - throw new Error( - "Private worker failure marker does not match the observed exit status.", - ); + throw new EvmAdmissionError("marker_mismatch"); fileFailure = { verified: true, failure: null }; } catch (markerFailure: unknown) { fileFailure = { diff --git a/src/evm/EvmoleInterfaceWorker.ts b/src/evm/EvmoleInterfaceWorker.ts index 214235f7cf..96e795749b 100644 --- a/src/evm/EvmoleInterfaceWorker.ts +++ b/src/evm/EvmoleInterfaceWorker.ts @@ -1,3 +1,4 @@ +import { EvmAdmissionError } from "./EvmAdmissionError.js"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { z } from "zod"; @@ -23,7 +24,7 @@ class UnsupportedContainer extends Error {} const main = async (): Promise => { const requestPath = process.argv[2]; if (requestPath === undefined) - throw new Error("Owned worker request path missing."); + throw new EvmAdmissionError("request_path_missing"); const request = requestSchema.parse( JSON.parse(await readFile(requestPath, "utf8")), ); diff --git a/src/evm/SolidityMetadata.test.ts b/src/evm/SolidityMetadata.test.ts index 8d9fade9a6..73dbb407cb 100644 --- a/src/evm/SolidityMetadata.test.ts +++ b/src/evm/SolidityMetadata.test.ts @@ -163,11 +163,28 @@ it("rejects repeated deeply nested container keys promptly", () => { inspectSolidityMetadata(bytecode("a2" + key + "01" + key + "02")), ).toMatchObject({ status: "not-decoded", - diagnostic: expect.stringContaining("found repeat map key"), + diagnostic: "repeated_map_key", + admission: { + code: "repeated_map_key", + detail: { key_length: expect.any(Number) }, + }, }); expect(performance.now() - started).toBeLessThan(1000); }); +it("records the profile depth when CBOR nesting is refused", () => { + expect( + inspectSolidityMetadata(bytecode("a16178" + "81".repeat(65) + "00")), + ).toMatchObject({ + status: "not-decoded", + diagnostic: "nesting_exceeds_profile", + admission: { + code: "nesting_exceeds_profile", + detail: { max_depth: 64 }, + }, + }); +}); + it.each(["", "60006000f3", "000000", "00010001"])( "does not claim absent Solidity metadata for an unavailable trailer: %s", (hex) => diff --git a/src/evm/SolidityMetadata.ts b/src/evm/SolidityMetadata.ts index 0afd626358..e43c06c63d 100644 --- a/src/evm/SolidityMetadata.ts +++ b/src/evm/SolidityMetadata.ts @@ -1,3 +1,4 @@ +import { EvmAdmissionError } from "./EvmAdmissionError.js"; import { decode, Tokenizer, Type, type Token } from "cborg"; import type { SolidityMetadata, @@ -14,39 +15,52 @@ const decodeOptions = { allowUndefined: false, allowInfinity: false, allowNaN: false, - rejectDuplicateMapKeys: true, + rejectDuplicateMapKeys: false, retainStringBytes: true, } as const; /** Bound recursive decoder entry before collection interpretation/allocation. */ class BoundedMetadataTokenizer extends Tokenizer { readonly remaining: number[] = []; + readonly #mapKeys: Array | undefined> = []; constructor(bytes: Uint8Array) { super(bytes, decodeOptions); } override next(): Token { - while (this.remaining.at(-1) === 0) this.remaining.pop(); + while (this.remaining.at(-1) === 0) { + this.remaining.pop(); + this.#mapKeys.pop(); + } const token = super.next(); if ( Type.equals(token.type, Type.tag) || Type.equals(token.type, Type.float) ) - throw new Error( - "CBOR tags and floating-point values are outside this metadata profile.", - ); + throw new EvmAdmissionError("tag_or_float"); if (Type.equals(token.type, Type.string)) { if (token.byteValue !== undefined) token.value = utf8.decode(token.byteValue); // cborg answers the shortest-form empty text (0x60) with a shared token that has no // bytes; it can only mean "", which is already its value. else if (token.value !== "" || token.encodedLength !== 1) - throw new Error( - "CBOR text bytes were unavailable for strict UTF-8 validation.", - ); + throw new EvmAdmissionError("text_bytes_unavailable"); } const parent = this.remaining.length - 1; + const mapKeys = parent >= 0 ? this.#mapKeys[parent] : undefined; + const onKey = + mapKeys !== undefined && parent >= 0 && this.remaining[parent]! % 2 === 0; if (parent >= 0) this.remaining[parent]!--; + if ( + onKey && + mapKeys !== undefined && + !Type.equals(token.type, Type.map) && + !Type.equals(token.type, Type.array) + ) { + const identity = keyIdentity(token.value); + if (mapKeys.has(identity)) throw repeatedMapKey(identity); + mapKeys.add(identity); + } if ( Type.equals(token.type, Type.map) || Type.equals(token.type, Type.array) @@ -61,11 +75,14 @@ class BoundedMetadataTokenizer extends Tokenizer { children < 0 || children > this.data.length ) - throw new Error( - "Declared CBOR collection exceeds the encoded item-byte bound.", - ); + throw new EvmAdmissionError("collection_exceeds_bound"); if (this.remaining.length >= MAX_DEPTH) - throw new Error("CBOR nesting exceeds the 64-level metadata profile."); + throw new EvmAdmissionError("nesting_exceeds_profile", { + max_depth: MAX_DEPTH, + }); + this.#mapKeys.push( + Type.equals(token.type, Type.map) ? new Set() : undefined, + ); this.remaining.push(children); } return token; @@ -85,9 +102,7 @@ const scalar = (value: unknown): SolidityMetadataScalar => { if (value instanceof Map) return { kind: "uninterpreted", container: "map" }; if (Array.isArray(value)) return { kind: "uninterpreted", container: "array" }; - throw new Error( - "CBOR decoder returned a value outside the admitted profile.", - ); + throw new EvmAdmissionError("value_outside_profile"); }; /** Value identity for duplicate-key checks; map entry order is not significant. */ @@ -101,14 +116,21 @@ const keyIdentity = (value: unknown): string => { return JSON.stringify(scalar(value)); }; +const repeatedMapKey = (identity: string): EvmAdmissionError => + new EvmAdmissionError( + "repeated_map_key", + identity.length <= 128 + ? { key: identity } + : { key_length: identity.length }, + ); + /** Check every map boundary, even when nested values remain uninterpreted. */ const validateMapKeys = (value: unknown): void => { if (value instanceof Map) { const seen = new Set(); for (const [key, item] of value) { const identity = keyIdentity(key); - if (seen.has(identity)) - throw new Error(`CBOR decode error: found repeat map key ${identity}`); + if (seen.has(identity)) throw repeatedMapKey(identity); seen.add(identity); validateMapKeys(key); validateMapKeys(item); @@ -118,6 +140,32 @@ const validateMapKeys = (value: unknown): void => { } }; +/** Keep the typed code and its detail; map only the decoder's duplicate-key throw. */ +const solidityFailure = ( + cause: unknown, +): { + readonly diagnostic: string; + readonly admission?: { + readonly code: string; + readonly detail?: Readonly>; + }; +} => { + if (cause instanceof EvmAdmissionError) + return { + diagnostic: cause.code, + admission: { + code: cause.code, + ...(cause.detail === undefined ? {} : { detail: cause.detail }), + }, + }; + return { + diagnostic: (cause instanceof Error ? cause.message : String(cause)).slice( + 0, + 512, + ), + }; +}; + /** Parse only a plausible terminal root-map candidate; never establish absence. */ export const inspectSolidityMetadata = ( bytes: Uint8Array, @@ -148,8 +196,7 @@ export const inspectSolidityMetadata = ( ...decodeOptions, tokenizer: new BoundedMetadataTokenizer(selected), }); - if (!(raw instanceof Map)) - throw new Error("Terminal CBOR candidate did not decode to a root map."); + if (!(raw instanceof Map)) throw new EvmAdmissionError("root_not_map"); validateMapKeys(raw); // cborg compares Map keys by identity, so repeated byte-string or container // keys (each its own object) pass its duplicate check; compare by value at @@ -185,10 +232,7 @@ export const inspectSolidityMetadata = ( status: "not-decoded", entries: [], compiler_version: null, - diagnostic: (cause instanceof Error - ? cause.message - : String(cause) - ).slice(0, 512), + ...solidityFailure(cause), }; } }; diff --git a/src/filesystem/OwnedFileHandle.ts b/src/filesystem/OwnedFileHandle.ts index 8c1b7ede4e..3f54ffeb08 100644 --- a/src/filesystem/OwnedFileHandle.ts +++ b/src/filesystem/OwnedFileHandle.ts @@ -1,5 +1,15 @@ import type { FileHandle } from "node:fs/promises"; +/** Close admission for one owned file handle. `code` is the stable identity. */ +export class OwnedFileHandleError extends Error { + readonly _tag = "OwnedFileHandleError"; + + constructor(readonly code: "handle_invalidated") { + super(code); + this.name = "OwnedFileHandleError"; + } +} + type CloseState = | { readonly kind: "open" } | { readonly kind: "closing"; readonly attempt: Promise } @@ -20,9 +30,7 @@ export class OwnedFileHandle< if (this.#state.kind === "closing") return this.#state.attempt; if (this.#state.kind === "failed") return Promise.reject(this.#state.cause); if (this.handle.fd < 0) { - const cause = new Error( - "File handle was invalidated before this owner confirmed its close", - ); + const cause = new OwnedFileHandleError("handle_invalidated"); this.#state = { kind: "failed", cause }; return Promise.reject(cause); } diff --git a/src/filesystem/OwnedStagedOutput.ts b/src/filesystem/OwnedStagedOutput.ts index 6e8e65894e..e0c0319842 100644 --- a/src/filesystem/OwnedStagedOutput.ts +++ b/src/filesystem/OwnedStagedOutput.ts @@ -1,3 +1,4 @@ +import { StagedOutputError } from "./StagedOutputError.js"; import { lstat, readdir, rmdir, unlink } from "node:fs/promises"; import type { Stats } from "node:fs"; @@ -48,7 +49,7 @@ export class OwnedStagedOutput { setFile(file: OwnedFileHandle): void { if (this.#file !== undefined) - throw new Error("Staged output already owns a file handle"); + throw new StagedOutputError("handle_already_owned"); this.#file = file; } @@ -58,7 +59,7 @@ export class OwnedStagedOutput { if (this.#file !== undefined && !this.#fileClosed) await this.#observeFileIdentity(); if (this.#fileIdentity === undefined) - throw new Error("Cannot verify staged file identity", { + throw new StagedOutputError("identity_unverified", undefined, { cause: this.#identityFailure, }); const stagedFile = await lstat(content); @@ -67,7 +68,7 @@ export class OwnedStagedOutput { stagedFile.isSymbolicLink() || !sameIdentity(stagedFile, this.#fileIdentity) ) - throw new Error(`Staged file identity changed: ${content}`); + throw new StagedOutputError("identity_changed", { path: content }); } /** Observe the descriptor's identity after its last write, then close it. */ @@ -134,9 +135,11 @@ export class OwnedStagedOutput { ) { stagedFile = undefined; } else if (errorCode(cause) === "ENOENT") { - throw new Error(`Owned staged file disappeared: ${content}`, { - cause, - }); + throw new StagedOutputError( + "file_disappeared", + { path: content }, + { cause }, + ); } else { throw cause; } @@ -148,7 +151,7 @@ export class OwnedStagedOutput { this.#fileIdentity === undefined || !sameIdentity(stagedFile, this.#fileIdentity) ) - throw new Error(`Staged file identity changed: ${content}`); + throw new StagedOutputError("identity_changed", { path: content }); await unlink(content); this.#fileRemovedByOwner = true; } @@ -156,20 +159,20 @@ export class OwnedStagedOutput { const remaining = await readdir(this.directory); if (remaining.length > 0) - throw new Error( - `Unexpected staging entries remain in ${this.directory}: ${remaining.join(", ")}`, - ); + throw new StagedOutputError("unexpected_entries", { + count: remaining.length, + entries: remaining.join(","), + }); await this.#verifiedDirectory(); await rmdir(this.directory); } async #verifiedDirectory(): Promise { if (this.directoryIdentity === undefined) - throw new Error( - `Cannot verify staging directory ownership: ${this.directory}`, - { - cause: this.#identityFailure, - }, + throw new StagedOutputError( + "directory_unverified", + { directory: this.directory }, + { cause: this.#identityFailure }, ); const directory = await lstat(this.directory); if ( @@ -177,7 +180,9 @@ export class OwnedStagedOutput { directory.isSymbolicLink() || !sameIdentity(directory, this.directoryIdentity) ) - throw new Error(`Staging directory identity changed: ${this.directory}`); + throw new StagedOutputError("directory_identity_changed", { + directory: this.directory, + }); } } diff --git a/src/filesystem/StagedOutputError.ts b/src/filesystem/StagedOutputError.ts new file mode 100644 index 0000000000..ed5ab193ac --- /dev/null +++ b/src/filesystem/StagedOutputError.ts @@ -0,0 +1,25 @@ +const STAGED_OUTPUT_CODES = [ + "handle_already_owned", + "identity_unverified", + "identity_changed", + "file_disappeared", + "unexpected_entries", + "directory_unverified", + "directory_identity_changed", +] as const; + +export type StagedOutputCode = (typeof STAGED_OUTPUT_CODES)[number]; + +/** Owned staged-output admission failure. `code` is the stable identity. */ +export class StagedOutputError extends Error { + readonly _tag = "StagedOutputError"; + + constructor( + readonly code: StagedOutputCode, + readonly detail?: Readonly>, + options?: ErrorOptions, + ) { + super(code, options); + this.name = "StagedOutputError"; + } +} diff --git a/src/firmware/FirmwareFiles.ts b/src/firmware/FirmwareFiles.ts index dde5cf30da..e0eeeb7532 100644 --- a/src/firmware/FirmwareFiles.ts +++ b/src/firmware/FirmwareFiles.ts @@ -35,6 +35,7 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { readArtifactFileChunks } from "../artifacts/readArtifactFileChunks.js"; import { decodeDirectoryEntryName } from "../filesystem/decodeDirectoryEntryName.js"; import { NonRegularFileReadError } from "../filesystem/RegularFile.js"; +import { observedSymlinkTarget } from "../domain/symlinkTarget.js"; /** One regular file or unpublished filesystem entry in a private extraction. */ export interface FirmwareEntry { @@ -43,6 +44,7 @@ export interface FirmwareEntry { readonly size: number; readonly kind: "file" | "symlink" | "special"; readonly linkTarget: string | null; + readonly linkTargetHex?: string; } const throwIfFirmwareInventoryCancelled = ( @@ -428,6 +430,9 @@ export const inventoryFirmwareOutput = async ( if (stat.isFile()) bytes += stat.size; if (bytes > maxBytes) throw firmwareStagingLimit(operation, "max_output_bytes", maxBytes); + const linkTarget = stat.isSymbolicLink() + ? observedSymlinkTarget(await readlink(path, { encoding: "buffer" })) + : undefined; entries.push({ path, relativePath: relative(root, path).split(sep).join("/"), @@ -437,7 +442,8 @@ export const inventoryFirmwareOutput = async ( : stat.isFile() ? "file" : "special", - linkTarget: stat.isSymbolicLink() ? await readlink(path) : null, + linkTarget: linkTarget?.text ?? null, + ...(linkTarget === undefined ? {} : { linkTargetHex: linkTarget.hex }), }); } } diff --git a/src/firmware/FirmwarePublication.ts b/src/firmware/FirmwarePublication.ts index 86e26e26d7..1f40772ce4 100644 --- a/src/firmware/FirmwarePublication.ts +++ b/src/firmware/FirmwarePublication.ts @@ -141,6 +141,9 @@ export const publishFirmwareExtraction = async (context: { relative_path: entry.relativePath, kind: entry.kind, link_target: entry.linkTarget, + ...(entry.linkTargetHex === undefined + ? {} + : { link_target_hex: entry.linkTargetHex }), }); continue; } diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts index 02be67de68..639b2c0573 100644 --- a/src/gdb/GdbSessionManager.ts +++ b/src/gdb/GdbSessionManager.ts @@ -10,7 +10,6 @@ import { AnalysisCapabilityUnavailableError, AnalysisCancelledError, AnalysisInputError, - AnalysisResourceConstraintError, AnalysisTimeoutError, } from "../domain/analysisErrorCore.js"; import { @@ -19,6 +18,7 @@ import { } from "../domain/analysisErrorBase.js"; import { createEvidence, type Evidence } from "../domain/evidence.js"; import type { GdbCommandResult } from "../domain/commandObservation.js"; +import { DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES } from "../domain/analysisResourceLimits.js"; import { err, ok, type Result } from "../domain/result.js"; import { ProviderProcessSupervisor, @@ -26,8 +26,18 @@ import { type OwnedProviderProcessSpawnOptions, type SpawnedOwnedProviderProcess, } from "../process/ProviderProcess.js"; +import { + applyDebuggerAdmissionOverride, + resolveDebuggerSessionLimits, + type DebuggerAdmissionLimit, +} from "../config/debuggerSessionLimits.js"; import { closeOwnedDebuggerSessions, + configurableDeadlineGuidance, + debuggerAdmissionDiagnostics, + debuggerAdmissionRecovery, + debuggerResourceConstraintError, + fixedDeadlineGuidance, readLinuxProcessStartTime, stopOwnedDebuggerProcess, type OwnedDebuggerDetachResult, @@ -37,22 +47,13 @@ import { } from "../process/ownedDebuggerSession.js"; const STARTUP_TIMEOUT_MS = 10_000; -const COMMAND_TIMEOUT_MS = 120_000; - -/** Fixed debugger deadline. Callers cannot raise it in this release. */ -const fixedDeadlineGuidance = ( - what: string, - timeoutMs: number, -): { readonly userMessage: string; readonly remediationAction: string } => ({ - userMessage: `${what} reached its ${String(timeoutMs)} ms deadline (deadlineExpired). This release has no runtime override.`, - remediationAction: `This ${String(timeoutMs)} ms deadline has no runtime override. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`, -}); const copiedTimeoutGuidance = ( cause: AnalysisTimeoutError, ): { readonly userMessage?: string; readonly remediationAction?: string; + readonly diagnostics?: AnalysisTimeoutError["diagnostics"]; } => ({ ...(cause.userMessage === undefined ? {} @@ -60,6 +61,9 @@ const copiedTimeoutGuidance = ( ...(cause.remediationAction === undefined ? {} : { remediationAction: cause.remediationAction }), + ...(cause.diagnostics === undefined + ? {} + : { diagnostics: cause.diagnostics }), }); /** A host executable refusal cannot be repaired by changing MI dialect. */ class GdbExecutableRefusal extends AnalysisCapabilityUnavailableError { @@ -78,15 +82,15 @@ class GdbExecutableRefusal extends AnalysisCapabilityUnavailableError { } } -const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024; const MAX_COMMAND_OUTPUT_RECORDS = 16_384; const MAX_MI_LINE_BYTES = 16 * 1024 * 1024; const MAX_RECENT_MI_LINE_BYTES = 64 * 1024; const MAX_DIAGNOSTIC_BYTES = 64 * 1024; -const DEFAULT_MAX_SESSIONS = 8; -const MAX_PENDING_GDB_COMMANDS = 16; const RECENT_RECORD_TRUNCATION_MARKER = "[recent record truncated]"; +const gdbRetainedOutputLimitation = (output: DebuggerAdmissionLimit): string => + `Retained MI records reached the ${String(output.value)} byte command-output cap or the ${String(MAX_COMMAND_OUTPUT_RECORDS)} record cap. ${debuggerAdmissionRecovery(output)} The record cap has no separate setting. Output completeness is unknown.`; + const cancelledWithCallerReason = ( operation: string, signal: AbortSignal, @@ -123,6 +127,8 @@ interface PendingCommand { readonly evidenceCommand: string; readonly lines: string[]; outputBytes: number; + /** Caller output uses the setting. Startup and detach replies use the control budget. */ + readonly outputBudgetBytes: number; outputTruncated: boolean; outputInvalidUtf8: boolean; terminalRecord: string | undefined; @@ -137,10 +143,10 @@ class GdbCommandTimeoutError extends AnalysisTimeoutError { constructor( readonly partial: GdbCommandResult, readonly evidence: Evidence, - timeoutMs: number, + deadline: DebuggerAdmissionLimit, ) { - super("gdb_console", timeoutMs, { - ...fixedDeadlineGuidance("The GDB MI command", timeoutMs), + super("gdb_console", deadline.value, { + ...configurableDeadlineGuidance("The GDB MI command", deadline), }); } } @@ -503,6 +509,8 @@ interface GdbSession { readonly knownInferiorIdentities: Map; readonly inferiorTty: string | undefined; readonly maxCommandOutputBytes: number; + /** Startup and detach MI. Separate from the caller command-output cap. */ + readonly controlReplyBytes: number; readonly stdoutSettled: Promise; readonly settleStdout: () => void; inferiorTtyVerified: boolean; @@ -558,6 +566,7 @@ export class GdbSessionManager { >(); readonly #command: string; readonly #environment: NodeJS.ProcessEnv; + readonly #commandDeadline: DebuggerAdmissionLimit; readonly #commandTimeoutMs: number; readonly #startupTimeoutMs: number; readonly #platform: NodeJS.Platform; @@ -569,8 +578,11 @@ export class GdbSessionManager { rememberIdentity: (identity: OwnedInferiorIdentity) => void, ) => Promise; readonly #readInferiorProcessStartTime: ProcessStartTimeReader | undefined; - readonly #maxSessions: number; + readonly #maxSessions: DebuggerAdmissionLimit; + readonly #maxPendingCommands: DebuggerAdmissionLimit; + readonly #commandOutput: DebuggerAdmissionLimit; readonly #maxCommandOutputBytes: number; + readonly #controlReplyBytes: number; #sessionReservations = 0; #closing = false; #closePromise: Promise | undefined; @@ -578,6 +590,7 @@ export class GdbSessionManager { constructor( options: { readonly environment?: Readonly; + /** @internal Test seam. Production uses REA_DEBUGGER_COMMAND_DEADLINE_MS. */ readonly commandTimeoutMs?: number; readonly startupTimeoutMs?: number; readonly platform?: NodeJS.Platform; @@ -591,14 +604,27 @@ export class GdbSessionManager { ) => Promise; /** @internal Read-only process identity observer for lifecycle tests. */ readonly readInferiorProcessStartTime?: ProcessStartTimeReader; + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_SESSIONS. */ readonly maxSessions?: number; - /** @internal Test seam; production callers should use the 16 MiB default. */ + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_PENDING_COMMANDS. */ + readonly maxPendingCommands?: number; + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES. */ readonly maxCommandOutputBytes?: number; + /** + * @internal Test seam for startup and detach MI truncation. + * Production uses the fixed control-reply budget. + */ + readonly controlReplyBytes?: number; } = {}, ) { this.#environment = { ...(options.environment ?? process.env) }; this.#command = this.#environment.REA_GDB_COMMAND ?? "gdb"; - this.#commandTimeoutMs = options.commandTimeoutMs ?? COMMAND_TIMEOUT_MS; + const limits = resolveDebuggerSessionLimits(this.#environment); + this.#commandDeadline = applyDebuggerAdmissionOverride( + limits.commandDeadline, + options.commandTimeoutMs, + ); + this.#commandTimeoutMs = this.#commandDeadline.value; this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS; this.#platform = options.platform ?? process.platform; this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess; @@ -606,9 +632,22 @@ export class GdbSessionManager { this.#verifyInferiorOutputSink = options.verifyInferiorOutputSink ?? verifyGdbInferiorOutputSink; this.#readInferiorProcessStartTime = options.readInferiorProcessStartTime; - this.#maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS; - this.#maxCommandOutputBytes = - options.maxCommandOutputBytes ?? MAX_COMMAND_OUTPUT_BYTES; + this.#maxSessions = applyDebuggerAdmissionOverride( + limits.maxSessions, + options.maxSessions, + ); + this.#maxPendingCommands = applyDebuggerAdmissionOverride( + limits.maxPendingCommands, + options.maxPendingCommands, + ); + this.#commandOutput = applyDebuggerAdmissionOverride( + limits.maxCommandOutputBytes, + options.maxCommandOutputBytes, + ); + this.#maxCommandOutputBytes = this.#commandOutput.value; + // Control replies keep the fixed allowance even when caller output is capped lower. + this.#controlReplyBytes = + options.controlReplyBytes ?? DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES; } async start( @@ -698,19 +737,18 @@ export class GdbSessionManager { }), ); const occupied = this.#sessions.size + this.#sessionReservations; - if (occupied >= this.#maxSessions) + if (occupied >= this.#maxSessions.value) return err( - new AnalysisResourceConstraintError( + debuggerResourceConstraintError( "start_gdb_session", "memory", - `GDB session capacity reached (${String(this.#maxSessions)} sessions). This release has no runtime override.`, + `GDB session capacity reached (${String(this.#maxSessions.value)} sessions).`, + "Close a GDB session before starting another.", + this.#maxSessions, { - max_sessions: this.#maxSessions, + max_sessions: this.#maxSessions.value, active_or_reserved_sessions: occupied, }, - { - remediationAction: `Close a GDB session before starting another. This capacity of ${String(this.#maxSessions)} sessions has no runtime override.`, - }, ), ); this.#sessionReservations += 1; @@ -833,6 +871,7 @@ export class GdbSessionManager { knownInferiorIdentities: new Map(), inferiorTty, maxCommandOutputBytes: this.#maxCommandOutputBytes, + controlReplyBytes: this.#controlReplyBytes, stdoutSettled, settleStdout, inferiorTtyVerified: false, @@ -1272,9 +1311,7 @@ export class GdbSessionManager { : []), "MI records are returned as decoded text; their semantics depend on GDB version and target.", ...(result.output_truncated - ? [ - `Retained MI records reached the ${String(this.#maxCommandOutputBytes)} byte or ${String(MAX_COMMAND_OUTPUT_RECORDS)} record cap. This release has no runtime override. Output completeness is unknown.`, - ] + ? [gdbRetainedOutputLimitation(this.#commandOutput)] : []), ...(result.output_invalid_utf8 ? [ @@ -1316,9 +1353,7 @@ export class GdbSessionManager { limitations: [ "GDB exited before returning a correlated MI result record; only records retained for the active command are returned raw.", ...(result.output_truncated - ? [ - `Retained MI records reached the ${String(this.#maxCommandOutputBytes)} byte or ${String(MAX_COMMAND_OUTPUT_RECORDS)} record cap. This release has no runtime override. Output completeness is unknown.`, - ] + ? [gdbRetainedOutputLimitation(this.#commandOutput)] : []), ], }, @@ -1330,7 +1365,17 @@ export class GdbSessionManager { session, "gdb_console", stopped.reason, - { cause, partialObservation: evidence }, + { + cause, + partialObservation: evidence, + ...(result.output_truncated + ? { + diagnostics: debuggerAdmissionDiagnostics( + this.#commandOutput, + ), + } + : {}), + }, ), ); return ok({ result, evidence }); @@ -1338,9 +1383,9 @@ export class GdbSessionManager { return err( new AnalysisTimeoutError("gdb_console", this.#commandTimeoutMs, { cause, - ...fixedDeadlineGuidance( + ...configurableDeadlineGuidance( "The GDB MI command", - this.#commandTimeoutMs, + this.#commandDeadline, ), }), ); @@ -1571,18 +1616,17 @@ export class GdbSessionManager { signal?: AbortSignal, operationName: "gdb_console" | "close_gdb_session" = "gdb_console", ): Promise { - if (session.pendingCommandCount >= MAX_PENDING_GDB_COMMANDS) - throw new AnalysisResourceConstraintError( + if (session.pendingCommandCount >= this.#maxPendingCommands.value) + throw debuggerResourceConstraintError( operationName, "transport", - `GDB command queue capacity reached (${String(MAX_PENDING_GDB_COMMANDS)} pending commands). This release has no runtime override.`, + `GDB command queue capacity reached (${String(this.#maxPendingCommands.value)} pending commands).`, + "Wait for a GDB command on this session to finish, then retry.", + this.#maxPendingCommands, { - max_pending_commands: MAX_PENDING_GDB_COMMANDS, + max_pending_commands: this.#maxPendingCommands.value, active_or_queued_commands: session.pendingCommandCount, }, - { - remediationAction: `Wait for a GDB command on this session to finish, then retry. This queue of ${String(MAX_PENDING_GDB_COMMANDS)} commands has no runtime override.`, - }, ); session.pendingCommandCount += 1; const previous = session.commandTail; @@ -1677,8 +1721,7 @@ export class GdbSessionManager { for (const pending of session.pending.values()) { const lineBytes = Buffer.byteLength(line, "utf8") + 1; if ( - pending.outputBytes + lineBytes <= session.maxCommandOutputBytes && - pending.outputBytes + lineBytes <= MAX_COMMAND_OUTPUT_BYTES && + pending.outputBytes + lineBytes <= pending.outputBudgetBytes && pending.lines.length < MAX_COMMAND_OUTPUT_RECORDS ) { pending.lines.push(line); @@ -2063,9 +2106,9 @@ export class GdbSessionManager { this.#commandTimeoutMs, { cause: new Error(`GDB MI command timed out: ${command}`), - ...fixedDeadlineGuidance( + ...configurableDeadlineGuidance( "The GDB MI command", - this.#commandTimeoutMs, + this.#commandDeadline, ), }, ); @@ -2084,7 +2127,7 @@ export class GdbSessionManager { subjectUnavailableReason: "The active GDB inferior identity is not established by the debugger session identifier.", limitations: [ - `The GDB command exceeded its ${String(this.#commandTimeoutMs)} ms deadline (deadlineExpired). This release has no runtime override. Retained MI and stream records are partial and command completion is unknown.`, + `The GDB command exceeded its ${String(this.#commandDeadline.value)} ms deadline (deadlineExpired). ${debuggerAdmissionRecovery(this.#commandDeadline)} Retained MI and stream records are partial and command completion is unknown.`, "Cleanup of the owned GDB process was requested because output correlation could no longer be guaranteed.", "The unrestricted GDB console can access local shell, filesystem, network, and target controls.", ], @@ -2122,7 +2165,7 @@ export class GdbSessionManager { : new GdbCommandTimeoutError( partial, evidence, - this.#commandTimeoutMs, + this.#commandDeadline, ), ); }); @@ -2143,6 +2186,9 @@ export class GdbSessionManager { cleanup, timer, outputBytes: 0, + outputBudgetBytes: command.startsWith("-interpreter-exec console ") + ? session.maxCommandOutputBytes + : session.controlReplyBytes, outputTruncated: false, outputInvalidUtf8: false, terminalRecord: undefined, diff --git a/src/generatedPackageMetadata.ts b/src/generatedPackageMetadata.ts index 25bb35bb64..9100e716d5 100644 --- a/src/generatedPackageMetadata.ts +++ b/src/generatedPackageMetadata.ts @@ -6,5 +6,5 @@ export const PACKAGE_METADATA = { serverSdkVersion: "2.3.1", clientSdkVersion: "2.3.1", coreSdkVersion: "2.3.1", - skillVersion: "53", + skillVersion: "55", } as const; diff --git a/src/ghidra/GhidraAdmissionError.ts b/src/ghidra/GhidraAdmissionError.ts new file mode 100644 index 0000000000..ed3e4af241 --- /dev/null +++ b/src/ghidra/GhidraAdmissionError.ts @@ -0,0 +1,66 @@ +const GHIDRA_ADMISSION_CODES = [ + "existing_project_windows", + "existing_project_program_name", + "existing_project_psp", + "existing_project_dos", + "existing_project_raw_binary", + "existing_project_language_override", + "existing_project_mips_soft_float", + "existing_project_analysis", + "gamecube_dol_combined", + "psp_combined", + "psp_raw_binary", + "mips_soft_float_scope", + "language_override_dos", + "language_override_raw_binary", + "raw_binary_dos", + "invalid_environment_entry", + "invalid_jvm_argument", + "unterminated_quote", + "platform_override", + "argument_files_exceed_bound", + "nested_argument_file", + "argument_file_unreadable", + "argument_file_path_invalid", + "argument_files_require_scope", + "prepared_options_exceed_bound", + "output_unobserved", + "handshake_identity", + "response_shape", + "response_json", + "seed_path_differs", + "seed_changed", + "canonical_path", + "nesting_exceeds_limit", + "entry_kind", + "component_missing", + "loader_jar_missing", + "bytes_exceed_limit", + "properties_exceed_limit", + "component_changed", + "component_truncated", + "properties_invalid", + "metadata_mismatch", + "host_unsupported", + "inventory_exceeds_limit", + "not_jar", + "unregistered", + "path_differs", + "digest_differs", +] as const; + +export type GhidraAdmissionCode = (typeof GHIDRA_ADMISSION_CODES)[number]; + +/** Ghidra launch, extension, or profile admission failure. `code` is the stable identity. */ +export class GhidraAdmissionError extends Error { + readonly _tag = "GhidraAdmissionError"; + + constructor( + readonly code: GhidraAdmissionCode, + readonly detail?: Readonly>, + options?: ErrorOptions, + ) { + super(code, options); + this.name = "GhidraAdmissionError"; + } +} diff --git a/src/ghidra/GhidraAnalysisSeeds.test.ts b/src/ghidra/GhidraAnalysisSeeds.test.ts index a76b0003c6..7cf12e02a4 100644 --- a/src/ghidra/GhidraAnalysisSeeds.test.ts +++ b/src/ghidra/GhidraAnalysisSeeds.test.ts @@ -135,9 +135,9 @@ describe("Ghidra seed commitment", () => { ).rejects.toMatchObject({ _tag: "ProviderAdapterError", diagnostics: { - reason: expect.stringMatching( - /changed after the analysis profile was committed/u, - ), + code: "seed_changed", + path: resolved.value.path, + reason: "seed_changed", }, }); }); diff --git a/src/ghidra/GhidraAnalysisSeeds.ts b/src/ghidra/GhidraAnalysisSeeds.ts index 52d56a70fa..511bf1aaba 100644 --- a/src/ghidra/GhidraAnalysisSeeds.ts +++ b/src/ghidra/GhidraAnalysisSeeds.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { createHash } from "node:crypto"; import { mkdir, realpath, stat } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; @@ -228,15 +229,13 @@ export const snapshotGhidraAnalysisSeeds = async ( if ( (await resolveSeedPath(commitment.configured_path)) !== commitment.path ) - throw new Error( - "Ghidra seed file resolved path differs from explicit configuration", - ); + throw new GhidraAdmissionError("seed_path_differs"); const entries = await readSeeds(commitment.path, resources, signal); const normalized = normalizedSeeds(entries); if (sha256(normalized) !== commitment.sha256) - throw new Error( - `Ghidra seed file changed after the analysis profile was committed: ${commitment.path}`, - ); + throw new GhidraAdmissionError("seed_changed", { + path: commitment.path, + }); const directory = join(runtimeRoot, "seeds"); await mkdir(directory, { mode: 0o700 }); const path = join(directory, "seeds.tsv"); diff --git a/src/ghidra/GhidraFileArtifacts.ts b/src/ghidra/GhidraFileArtifacts.ts index 0e68bdbd93..2bfbb6abb9 100644 --- a/src/ghidra/GhidraFileArtifacts.ts +++ b/src/ghidra/GhidraFileArtifacts.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { open } from "node:fs/promises"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -100,7 +101,18 @@ export const ghidraFileFailure = ( return primary; }); const { cause: primary, origin } = artifactFilePrimaryFacts(cause); - const reason = primary instanceof Error ? primary.message : String(primary); + const admission = + primary instanceof GhidraAdmissionError ? primary : undefined; + const reason = + admission?.code ?? + (primary instanceof Error ? primary.message : String(primary)); + const systemCode = + admission === undefined && + primary instanceof Error && + "code" in primary && + typeof primary.code === "string" + ? primary.code + : undefined; const cleanup = cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined; if (origin === "owned-or-classified" && primary instanceof AnalysisError) { @@ -134,12 +146,10 @@ export const ghidraFileFailure = ( cause, cleanup, diagnostics: { + ...admission?.detail, ...diagnostics, - ...(primary instanceof Error && - "code" in primary && - typeof primary.code === "string" - ? { system_code: primary.code } - : {}), + ...(admission === undefined ? {} : { code: admission.code }), + ...(systemCode === undefined ? {} : { system_code: systemCode }), ...(primary instanceof Error && "syscall" in primary && typeof primary.syscall === "string" diff --git a/src/ghidra/GhidraInstallation.test.ts b/src/ghidra/GhidraInstallation.test.ts index 4c8e375eed..f830572f61 100644 --- a/src/ghidra/GhidraInstallation.test.ts +++ b/src/ghidra/GhidraInstallation.test.ts @@ -917,7 +917,7 @@ describe("Ghidra malformed property admission", () => { name: "version", status: "failed", code: "version_unresolved", - detail: expect.stringContaining("Malformed Unicode escape"), + detail: expect.stringContaining("malformed_unicode_escape"), }), expect.objectContaining({ name: "java", diff --git a/src/ghidra/GhidraInstallationPolicy.ts b/src/ghidra/GhidraInstallationPolicy.ts index ddc15219ff..f3cdba5013 100644 --- a/src/ghidra/GhidraInstallationPolicy.ts +++ b/src/ghidra/GhidraInstallationPolicy.ts @@ -1,4 +1,7 @@ -import { parseGhidraJavaProperties } from "./GhidraJavaProperties.js"; +import { + parseGhidraJavaProperties, + type GhidraJavaPropertiesCode, +} from "./GhidraJavaProperties.js"; import { ghidraConfigurationVersion } from "./GhidraReleasePolicy.js"; /** One Java major bound declared by `application.properties`. */ @@ -24,7 +27,7 @@ export interface GhidraApplicationProperties { readonly javaMinimum: GhidraJavaBound; readonly javaMaximum: GhidraJavaBound; /** A malformed Java properties escape invalidates the entire file. */ - readonly parseError: string | null; + readonly parseError: GhidraJavaPropertiesCode | null; } /** Read the version and Java bounds Ghidra publishes for one installation. */ diff --git a/src/ghidra/GhidraJavaLaunch.ts b/src/ghidra/GhidraJavaLaunch.ts index 14a45159da..0a3bbc9e73 100644 --- a/src/ghidra/GhidraJavaLaunch.ts +++ b/src/ghidra/GhidraJavaLaunch.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { appendCallerPath } from "../filesystem/callerPath.js"; import { dirname } from "node:path"; @@ -130,9 +131,11 @@ export const ghidraJavaLaunch = async (options: { const key = options.platform === "win32" ? sourceKey.toUpperCase() : sourceKey; if (separator < 1 || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) - throw new Error( - `Invalid Ghidra LaunchSupport environment entry: ${line}`, - ); + throw new GhidraAdmissionError("invalid_environment_entry", { + check: separator < 1 ? "separator" : "key", + line: line.length <= 1024 ? line : line.slice(0, 1024), + ...(line.length <= 1024 ? {} : { line_truncated: 1 }), + }); if ((environment[key] ?? "") === "") environment[key] = line.slice(separator + 1); } @@ -172,12 +175,13 @@ export const ghidraJavaLaunch = async (options: { const vmArguments = configuredArguments.stdout .split(/\r?\n/u) .filter((line) => line !== ""); - if ( - vmArguments.some( - (argument) => !argument.startsWith("-") || argument.includes("\0"), - ) - ) - throw new Error("Invalid Ghidra LaunchSupport JVM argument"); + const invalidArgument = vmArguments.find( + (argument) => !argument.startsWith("-") || argument.includes("\0"), + ); + if (invalidArgument !== undefined) + throw new GhidraAdmissionError("invalid_jvm_argument", { + check: invalidArgument.includes("\0") ? "nul" : "prefix", + }); rejectJvmPlatformOverrides(vmArguments); return { command, diff --git a/src/ghidra/GhidraJavaOptions.ts b/src/ghidra/GhidraJavaOptions.ts index 4dfd295aa9..e53ffbe3df 100644 --- a/src/ghidra/GhidraJavaOptions.ts +++ b/src/ghidra/GhidraJavaOptions.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { randomUUID } from "node:crypto"; import { join } from "node:path"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; @@ -32,19 +33,14 @@ export const jvmEnvironmentArguments = (value: string): readonly string[] => { active = true; } } - if (quote !== undefined) - throw new Error( - "Invalid quoted Ghidra LaunchSupport JVM environment option", - ); + if (quote !== undefined) throw new GhidraAdmissionError("unterminated_quote"); if (active) args.push(token); return args; }; export const rejectJvmPlatformOverrides = (args: readonly string[]): void => { if (args.some((argument) => /^-Dos\.(?:arch|name)(?:=|$)/u.test(argument))) - throw new Error( - "Ghidra LaunchSupport may not override os.arch or os.name; the native decompiler was validated for the inspected JVM platform.", - ); + throw new GhidraAdmissionError("platform_override"); }; /** Java launcher argfile grammar: quotes, escapes inside quotes, comments and line continuation are one parse. */ @@ -148,18 +144,16 @@ const readArgumentFile = ( new RegularFileOperationCancelled(reason), }); if (data === undefined) - throw new Error( - `JDK_JAVA_OPTIONS argument files exceed the ${String(OPTIONS_BYTES)}-byte cumulative admission bound.`, - ); + throw new GhidraAdmissionError("argument_files_exceed_bound", { + budget_bytes: OPTIONS_BYTES, + }); const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true, }).decode(data); const tokens = argumentFileTokens(text); if (tokens.some(fileReference)) - throw new Error( - "Ghidra LaunchSupport JDK_JAVA_OPTIONS argument files may not nest @ references.", - ); + throw new GhidraAdmissionError("nested_argument_file"); rejectJvmPlatformOverrides(tokens); return { text, @@ -170,8 +164,9 @@ const readArgumentFile = ( "follow", ).catch((cause: unknown) => { if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") - throw new Error( - `Ghidra LaunchSupport JDK_JAVA_OPTIONS argument file is unreadable: ${path}`, + throw new GhidraAdmissionError( + "argument_file_unreadable", + { path }, { cause }, ); throw cause; @@ -209,9 +204,7 @@ export const prepareJdkJavaOptions = async ( const resources = options.resources; const directory = options.directory; if (resources === undefined || directory === undefined) - throw new Error( - "JDK_JAVA_OPTIONS argument files require a caller-owned resource scope and runtime root.", - ); + throw new GhidraAdmissionError("argument_files_require_scope"); const snapshots = new Map< string, { @@ -233,9 +226,9 @@ export const prepareJdkJavaOptions = async ( let snapshot = snapshots.get(path); if (snapshot === undefined) { if (path === "" || path.includes("\0")) - throw new Error( - "Ghidra LaunchSupport JDK_JAVA_OPTIONS argument file is unreadable.", - ); + throw new GhidraAdmissionError("argument_file_path_invalid", { + check: path === "" ? "empty" : "nul", + }); const content = await readArgumentFile( path, resources, @@ -257,9 +250,9 @@ export const prepareJdkJavaOptions = async ( } else { bytes += snapshot.bytes; if (bytes > OPTIONS_BYTES) - throw new Error( - `JDK_JAVA_OPTIONS argument files exceed the ${String(OPTIONS_BYTES)}-byte cumulative admission bound.`, - ); + throw new GhidraAdmissionError("argument_files_exceed_bound", { + budget_bytes: OPTIONS_BYTES, + }); } replacement = `@${snapshot.path}`; if (snapshot.disablesExpansion) expansionEnabled = false; @@ -268,9 +261,9 @@ export const prepareJdkJavaOptions = async ( encodedBytes += Buffer.byteLength(encoded) + (prepared.length === 0 ? 0 : 1); if (encodedBytes > OPTIONS_BYTES) - throw new Error( - `Prepared JDK_JAVA_OPTIONS exceeds the ${String(OPTIONS_BYTES)}-byte environment admission bound.`, - ); + throw new GhidraAdmissionError("prepared_options_exceed_bound", { + budget_bytes: OPTIONS_BYTES, + }); prepared.push(encoded); } assertActive(); diff --git a/src/ghidra/GhidraJavaProbe.ts b/src/ghidra/GhidraJavaProbe.ts index 8d4bb59362..e890695d21 100644 --- a/src/ghidra/GhidraJavaProbe.ts +++ b/src/ghidra/GhidraJavaProbe.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { retainOwnedCommandCleanup } from "../providers/OwnedCommandCleanup.js"; import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js"; import { randomUUID } from "node:crypto"; @@ -184,7 +185,7 @@ const observeGhidraJava = async ( }, ); if (interpreted === undefined) - throw new Error("Java command completed without an output observation"); + throw new GhidraAdmissionError("output_unobserved"); return interpreted; } catch (cause: unknown) { const commandFailure = diff --git a/src/ghidra/GhidraJavaProperties.ts b/src/ghidra/GhidraJavaProperties.ts index d41b446bd1..99cf8cf157 100644 --- a/src/ghidra/GhidraJavaProperties.ts +++ b/src/ghidra/GhidraJavaProperties.ts @@ -1,9 +1,18 @@ import type { Result } from "../domain/result.js"; +const GHIDRA_JAVA_PROPERTIES_CODES = [ + "trailing_escape", + "malformed_unicode_escape", +] as const; + +/** Stable identity for a Java properties escape that invalidates the file. */ +export type GhidraJavaPropertiesCode = + (typeof GHIDRA_JAVA_PROPERTIES_CODES)[number]; + /** Parse Java Properties.load(InputStream) syntax from its ISO-8859-1 text. */ export const parseGhidraJavaProperties = ( content: string, -): Result, string> => { +): Result, GhidraJavaPropertiesCode> => { const values = new Map(); for (const line of logicalPropertyLines(content)) { let start = 0; @@ -88,7 +97,9 @@ function* logicalPropertyLines(content: string): Generator { if (continuing) yield logical; } -const decodePropertyEscapes = (value: string): Result => { +const decodePropertyEscapes = ( + value: string, +): Result => { let result = ""; for (let index = 0; index < value.length; index += 1) { const character = value[index]; @@ -97,8 +108,7 @@ const decodePropertyEscapes = (value: string): Result => { continue; } const escaped = value[++index]; - if (escaped === undefined) - return { ok: false, error: "Trailing escape in Java properties value" }; + if (escaped === undefined) return { ok: false, error: "trailing_escape" }; if (escaped === "t") result += "\t"; else if (escaped === "n") result += "\n"; else if (escaped === "r") result += "\r"; @@ -108,7 +118,7 @@ const decodePropertyEscapes = (value: string): Result => { if (digits.length !== 4 || !/^[0-9a-f]{4}$/iu.test(digits)) return { ok: false, - error: "Malformed Unicode escape in Java properties file", + error: "malformed_unicode_escape", }; result += String.fromCharCode(Number.parseInt(digits, 16)); index += 4; diff --git a/src/ghidra/GhidraLauncher.ts b/src/ghidra/GhidraLauncher.ts index 6e1c99cedd..410fc1d7b9 100644 --- a/src/ghidra/GhidraLauncher.ts +++ b/src/ghidra/GhidraLauncher.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { copyExistingGhidraProject } from "./GhidraProjectSnapshot.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { GHIDRA_BRIDGE_SOURCES } from "./GhidraBridgeSources.js"; @@ -810,41 +811,25 @@ const validateExistingProjectOptions = ( // The pre-imported project carries its own analysis; every import-time // selection is therefore rejected instead of silently ignored. if ((options.platform ?? process.platform) === "win32") - throw new Error( - "An existing Ghidra project cannot be opened on the Windows launch path.", - ); + throw new GhidraAdmissionError("existing_project_windows"); if (existing.programName.trim() === "") - throw new Error( - "An existing Ghidra project requires a program name for -process.", - ); + throw new GhidraAdmissionError("existing_project_program_name"); if (options.psp === true) - throw new Error( - "An existing Ghidra project cannot be combined with a PSP ELF import.", - ); + throw new GhidraAdmissionError("existing_project_psp"); if (options.dosMz === true || options.dosCom === true) - throw new Error( - "An existing Ghidra project cannot be combined with a DOS import.", - ); + throw new GhidraAdmissionError("existing_project_dos"); if (rawBinary !== undefined) - throw new Error( - "An existing Ghidra project cannot be combined with a raw-binary import.", - ); + throw new GhidraAdmissionError("existing_project_raw_binary"); if (override !== undefined) - throw new Error( - "A Ghidra language override cannot be combined with an existing project.", - ); + throw new GhidraAdmissionError("existing_project_language_override"); if (options.mipsSoftFloat === true) - throw new Error( - "MIPS soft-float storage applies only to a fresh import.", - ); + throw new GhidraAdmissionError("existing_project_mips_soft_float"); if ( options.seedPath !== undefined || options.analysisSeeds !== undefined || (options.disabledAnalyzers?.length ?? 0) > 0 ) - throw new Error( - "An existing project opens without analysis; analysis seeds and analyzer overrides do not apply.", - ); + throw new GhidraAdmissionError("existing_project_analysis"); } }; @@ -871,22 +856,16 @@ export const ghidraHeadlessArguments = ( rawBinary !== undefined || override !== undefined) ) - throw new Error( - "A GameCube DOL import cannot be combined with another loader, language override, or analysis lane; its language is committed in the gamecubeDol option.", - ); + throw new GhidraAdmissionError("gamecube_dol_combined"); if ( options.psp === true && (options.dosMz === true || options.dosCom === true || override !== undefined) ) - throw new Error( - "A PSP ELF import cannot be combined with DOS or a generic Ghidra language override.", - ); + throw new GhidraAdmissionError("psp_combined"); if (options.psp === true && rawBinary !== undefined) - throw new Error( - "A PSP ELF import cannot be combined with a raw-binary import.", - ); + throw new GhidraAdmissionError("psp_raw_binary"); if ( options.mipsSoftFloat === true && (options.psp === true || @@ -894,27 +873,19 @@ export const ghidraHeadlessArguments = ( options.dosCom === true || rawBinary !== undefined) ) - throw new Error( - "MIPS soft-float storage applies only to a generic MIPS ELF import.", - ); + throw new GhidraAdmissionError("mips_soft_float_scope"); if ( override !== undefined && (options.dosMz === true || options.dosCom === true) ) - throw new Error( - "A Ghidra language override cannot be combined with a DOS import.", - ); + throw new GhidraAdmissionError("language_override_dos"); if (override !== undefined && rawBinary !== undefined) - throw new Error( - "A Ghidra language override cannot be combined with a raw-binary import.", - ); + throw new GhidraAdmissionError("language_override_raw_binary"); if ( rawBinary !== undefined && (options.dosMz === true || options.dosCom === true) ) - throw new Error( - "A raw-binary import cannot be combined with a DOS import.", - ); + throw new GhidraAdmissionError("raw_binary_dos"); return [ options.projectRoot, existing?.projectName ?? "rea-project", diff --git a/src/ghidra/GhidraPspExtension.ts b/src/ghidra/GhidraPspExtension.ts index c3e9b9feb2..2c3257777e 100644 --- a/src/ghidra/GhidraPspExtension.ts +++ b/src/ghidra/GhidraPspExtension.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { createHash } from "node:crypto"; import { lstat, opendir, realpath } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; @@ -70,17 +71,15 @@ const fingerprint = async ( ): Promise => { assertArtifactReadActive(signal); if (!isAbsolute(root) || (await realpath(root)) !== root) - throw new Error( - "The installed Allegrex directory must be a canonical non-symlink path.", - ); + throw new GhidraAdmissionError("canonical_path"); const files: string[] = []; let entries = 1; // The root itself consumes one inventory entry. const visit = async (relative: string, depth: number): Promise => { assertArtifactReadActive(signal); if (depth > MAX_DEPTH) - throw new Error( - "Allegrex extension directory nesting exceeds the inspection limit.", - ); + throw new GhidraAdmissionError("nesting_exceeds_limit", { + max_depth: MAX_DEPTH, + }); const path = join(root, relative); const metadata = await lstat(path); if (metadata.isDirectory()) { @@ -99,17 +98,16 @@ const fingerprint = async ( ); } else if (metadata.isFile()) { files.push(relative); - } else - throw new Error( - `Allegrex extension entries must be regular files or directories: ${relative}`, - ); + } else throw new GhidraAdmissionError("entry_kind", { relative }); }; await visit("", 0); for (const required of REQUIRED) if (!files.includes(required)) - throw new Error(`Missing installed Allegrex component: ${required}`); + throw new GhidraAdmissionError("component_missing", { + component: required, + }); if (!files.some((path) => path.startsWith("lib/") && path.endsWith(".jar"))) - throw new Error("The installed Allegrex extension has no loader JAR."); + throw new GhidraAdmissionError("loader_jar_missing"); const hash = createHash("sha256"); let bytesRead = 0; let properties = Buffer.alloc(0); @@ -125,13 +123,13 @@ const fingerprint = async ( !Number.isSafeInteger(before.size) || before.size > MAX_BYTES - bytesRead ) - throw new Error( - "Allegrex extension bytes exceed the inspection limit.", - ); + throw new GhidraAdmissionError("bytes_exceed_limit", { + max_bytes: MAX_BYTES, + }); if (relative === "extension.properties" && before.size > 65536) - throw new Error( - "Allegrex extension properties exceed the inspection limit.", - ); + throw new GhidraAdmissionError("properties_exceed_limit", { + max_bytes: 65536, + }); hash.update(JSON.stringify([relative, before.size])); const buffer = Buffer.alloc(Math.min(65536, before.size + 1)); let offset = 0; @@ -150,23 +148,19 @@ const fingerprint = async ( if (bytesRead === 0) break; offset += bytesRead; if (offset > before.size) - throw new Error( - `Allegrex component changed during inspection: ${relative}`, - ); + throw new GhidraAdmissionError("component_changed", { relative }); const chunk = buffer.subarray(0, bytesRead); hash.update(chunk); if (relative === "extension.properties") chunks.push(Buffer.from(chunk)); } if (offset !== before.size) - throw new Error(`Truncated Allegrex component: ${relative}`); + throw new GhidraAdmissionError("component_truncated", { relative }); if ( !sameRegularFileState(before, await handle.stat()) || !sameRegularFileState(before, await lstat(path)) ) - throw new Error( - `Allegrex component changed during inspection: ${relative}`, - ); + throw new GhidraAdmissionError("component_changed", { relative }); bytesRead += before.size; if (relative === "extension.properties") properties = Buffer.concat(chunks); @@ -179,18 +173,18 @@ const fingerprint = async ( properties.toString("latin1"), ); if (!parsedProperties.ok) - throw new Error( - `Invalid Allegrex extension properties: ${parsedProperties.error}`, - ); + throw new GhidraAdmissionError("properties_invalid", { + parser_code: parsedProperties.error, + }); const property = (name: string): string | undefined => parsedProperties.value.get(name); if ( property("name") !== "ghidra-allegrex" || property("version") !== ghidraVersion ) - throw new Error( - `Allegrex extension metadata does not match the selected Ghidra ${ghidraVersion}; install or rebuild a compatible extension.`, - ); + throw new GhidraAdmissionError("metadata_mismatch", { + ghidra_version: ghidraVersion, + }); assertArtifactReadActive(signal); return { id: "ghidra-allegrex", @@ -213,9 +207,10 @@ export const resolveGhidraPspExtension = async ( installation.platform !== "linux" || installation.architecture !== "x64" ) - throw new Error( - "The initial PSP profile requires a supported Linux x64 Ghidra installation.", - ); + throw new GhidraAdmissionError("host_unsupported", { + platform: installation.platform, + architecture: installation.architecture, + }); const root = join( await realpath(installation.installDir), "Ghidra", @@ -241,7 +236,16 @@ const ghidraPspInspectionError = (cause: unknown): AnalysisError => { const cleanup = cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined; const { cause: primary } = artifactFilePrimaryFacts(cause); - const reason = `PSP_EXTENSION_UNAVAILABLE: ${primary instanceof Error ? primary.message : String(primary)} Use a compatible caller-installed ghidra-allegrex under GHIDRA_INSTALL_DIR/Ghidra/Extensions; REA does not install it.`; + const admission = + primary instanceof GhidraAdmissionError ? primary : undefined; + const identity = + admission?.code ?? + (primary instanceof Error ? primary.message : String(primary)); + const reason = `PSP_EXTENSION_UNAVAILABLE: ${identity} Use a compatible caller-installed ghidra-allegrex under GHIDRA_INSTALL_DIR/Ghidra/Extensions; REA does not install it.`; + const diagnostics = + admission === undefined + ? undefined + : { code: admission.code, ...admission.detail }; const error = cause instanceof ArtifactReaderFailure && cause.reason === "cancelled" ? new AnalysisCancelledError("resolve_analysis_profile", { @@ -256,6 +260,7 @@ const ghidraPspInspectionError = (cause: unknown): AnalysisError => { cause, userMessage: reason, cleanup, + ...(diagnostics === undefined ? {} : { diagnostics }), }, ); bindArtifactFileCleanup(cause, error); @@ -282,9 +287,9 @@ const readChildren = async ( assertArtifactReadActive(signal); if (child === null) break; if (names.length >= remainingEntries) - throw new Error( - "Allegrex extension inventory exceeds the inspection limit.", - ); + throw new GhidraAdmissionError("inventory_exceeds_limit", { + max_entries: MAX_ENTRIES, + }); names.push(child.name); } outcome = { kind: "completed", names }; diff --git a/src/ghidra/GhidraSessionError.ts b/src/ghidra/GhidraSessionError.ts index a53ecf205c..33f3d6e532 100644 --- a/src/ghidra/GhidraSessionError.ts +++ b/src/ghidra/GhidraSessionError.ts @@ -7,6 +7,7 @@ import type { JsonValue } from "../domain/jsonValue.js"; import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js"; import { boundedUtf8Text } from "../process/boundedUtf8Text.js"; import type { GhidraDiagnosticsScope } from "./GhidraDiagnostics.js"; +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { GhidraLaunchSupportError } from "./GhidraJavaLaunch.js"; import { GhidraLaunchError, @@ -281,6 +282,7 @@ const describeErrorCause = ( message: message.text, ...(message.truncated ? { message_truncated: true } : {}), ...causeCode(cause, redact), + ...causeAdmissionDetail(cause, redact), ...causeSignal(cause, redact), ...causeCommandOutcome(cause, redact), ...causeCompilerOutcome(cause, redact), @@ -319,6 +321,24 @@ const causeCommandOutcome = ( }; }; +const causeAdmissionDetail = ( + cause: Error, + redact: (value: string, incomplete?: boolean) => string, +): Record => { + if (!(cause instanceof GhidraAdmissionError) || cause.detail === undefined) + return {}; + const detail: Record = {}; + for (const [key, value] of Object.entries(cause.detail)) { + if (typeof value === "number") { + detail[key] = value; + continue; + } + const bounded = boundedUtf8Text(redact(value), MAX_CAUSE_FIELD_BYTES); + detail[key] = bounded.text; + } + return { detail }; +}; + const causeCode = ( cause: Error, redact: (value: string, incomplete?: boolean) => string, diff --git a/src/ghidra/GhidraSessionValues.test.ts b/src/ghidra/GhidraSessionValues.test.ts index ee45839c58..f3831fc3fd 100644 --- a/src/ghidra/GhidraSessionValues.test.ts +++ b/src/ghidra/GhidraSessionValues.test.ts @@ -52,14 +52,16 @@ describe("Ghidra DOS import commitment", () => { }); }); it("rejects a 32-bit interpretation even when artifact and profile hashes match", () => { - expect(rejectedHandshake(session("x86:LE:32:default")).message).toBe( - "Ghidra bridge handshake identity is invalid", - ); + expect(rejectedHandshake(session("x86:LE:32:default"))).toMatchObject({ + _tag: "GhidraAdmissionError", + code: "handshake_identity", + }); }); it("rejects compiler drift independently of the language", () => { - expect(rejectedHandshake(session(undefined, "windows")).message).toBe( - "Ghidra bridge handshake identity is invalid", - ); + expect(rejectedHandshake(session(undefined, "windows"))).toMatchObject({ + _tag: "GhidraAdmissionError", + code: "handshake_identity", + }); }); }); diff --git a/src/ghidra/GhidraSessionValues.ts b/src/ghidra/GhidraSessionValues.ts index 8bb0903c75..684330bd8d 100644 --- a/src/ghidra/GhidraSessionValues.ts +++ b/src/ghidra/GhidraSessionValues.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { ghidraSeedReportSchema } from "./GhidraAnalysisSeeds.js"; import { ghidraDisabledAnalyzerReportSchema } from "./GhidraDisabledAnalyzers.js"; import { z } from "zod"; @@ -191,7 +192,7 @@ export const parseGhidraSessionInfo = ( parsed.data.analysis_complete === null || parsed.data.analysis_complete === parsed.data.analysis_timed_out) ) - return err(new Error("Ghidra bridge handshake identity is invalid")); + return err(new GhidraAdmissionError("handshake_identity")); return ok(parsed.data); }; diff --git a/src/ghidra/extensions/GhidraExtensions.test.ts b/src/ghidra/extensions/GhidraExtensions.test.ts index 2df197d977..02242f45bd 100644 --- a/src/ghidra/extensions/GhidraExtensions.test.ts +++ b/src/ghidra/extensions/GhidraExtensions.test.ts @@ -181,7 +181,12 @@ describe("optional Ghidra extension boundary", () => { snapshotGhidraExtensions([extension], await root(), resources), ).rejects.toMatchObject({ _tag: "ProviderAdapterError", - diagnostics: { reason: expect.stringContaining("digest differs") }, + diagnostics: { + code: "digest_differs", + id: extension.id, + reason: "digest_differs", + path: extension.path, + }, }); }); it("rejects nonexistent, empty and non-JAR artifacts with useful reasons", async () => { @@ -268,18 +273,19 @@ describe("optional Ghidra extension boundary", () => { if (!resolved.ok) expect(resolved.error._tag).toBe("AnalysisCancelledError"); }); - it("refuses traversal identities in an imported extension profile", () => { - expect( - ghidraExtensionSchema.safeParse({ - id: "../escape", - path: "/fixture.jar", - configured_path: "/fixture.jar", - sha256: "a".repeat(64), - integration_api: 1, - entry_class: "ignored", - }).success, - ).toBe(false); - }); +}); + +it("refuses traversal identities in an imported extension profile", () => { + expect( + ghidraExtensionSchema.safeParse({ + id: "../escape", + path: "/fixture.jar", + configured_path: "/fixture.jar", + sha256: "a".repeat(64), + integration_api: 1, + entry_class: "ignored", + }).success, + ).toBe(false); }); describe("Ghidra extension profile and producer validation", () => { @@ -384,7 +390,11 @@ describe("Ghidra extension profile and producer validation", () => { ), ).rejects.toMatchObject({ _tag: "ProviderAdapterError", - diagnostics: { reason: expect.stringContaining("resolved path differs") }, + diagnostics: { + code: "path_differs", + id: second.extension.id, + reason: "path_differs", + }, }); }); it("rejects unregistered descriptors before snapshot filesystem writes", async () => { @@ -398,7 +408,11 @@ describe("Ghidra extension profile and producer validation", () => { ), ).rejects.toMatchObject({ _tag: "ProviderAdapterError", - diagnostics: { reason: expect.stringContaining("Unregistered") }, + diagnostics: { + code: "unregistered", + id: "unknown", + reason: "unregistered", + }, }); }); }); diff --git a/src/ghidra/extensions/GhidraExtensions.ts b/src/ghidra/extensions/GhidraExtensions.ts index 890ead74c3..c7aaa74cc0 100644 --- a/src/ghidra/extensions/GhidraExtensions.ts +++ b/src/ghidra/extensions/GhidraExtensions.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "../GhidraAdmissionError.js"; import { createHash } from "node:crypto"; import { realpath, mkdir } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; @@ -86,7 +87,7 @@ const readJar = ( signal, (bytes) => { if (bytes.length < 4 || bytes.readUInt32LE(0) !== 0x04034b50) - throw new Error(`Ghidra extension is not a JAR: ${path}`); + throw new GhidraAdmissionError("not_jar", { path }); return bytes; }, ); @@ -213,13 +214,13 @@ export const snapshotGhidraExtensions = async ( adapter === undefined || adapter.entryClass !== extension.entry_class ) - throw new Error( - `Unregistered Ghidra extension identity: ${extension.id}`, - ); + throw new GhidraAdmissionError("unregistered", { + id: extension.id, + }); if ((await realpath(extension.configured_path)) !== extension.path) - throw new Error( - `Ghidra extension resolved path differs from explicit configuration: ${extension.id}`, - ); + throw new GhidraAdmissionError("path_differs", { + id: extension.id, + }); } const directory = join(runtimeRoot, "extensions"); await mkdir(directory, { mode: 0o700 }); @@ -229,9 +230,10 @@ export const snapshotGhidraExtensions = async ( if ( createHash("sha256").update(bytes).digest("hex") !== extension.sha256 ) - throw new Error( - `Ghidra extension digest differs from committed profile: ${extension.id} (${extension.path})`, - ); + throw new GhidraAdmissionError("digest_differs", { + id: extension.id, + path: extension.path, + }); const path = join(directory, `${extension.id}.jar`); await writeGhidraProfileArtifact(path, bytes, resources, signal); result.push({ ...extension, path }); diff --git a/src/ghidra/protocol.ts b/src/ghidra/protocol.ts index 150486c3c5..89949b968b 100644 --- a/src/ghidra/protocol.ts +++ b/src/ghidra/protocol.ts @@ -1,3 +1,4 @@ +import { GhidraAdmissionError } from "./GhidraAdmissionError.js"; import { z } from "zod"; import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; @@ -44,11 +45,9 @@ export const parseGhidraResponseLine = ( const response = responseSchema.safeParse(parsed); return response.success ? ok(response.data) - : err(new Error("Ghidra bridge response has an invalid shape")); + : err(new GhidraAdmissionError("response_shape")); } catch (cause: unknown) { - return err( - new Error("Ghidra bridge response is not valid JSON", { cause }), - ); + return err(new GhidraAdmissionError("response_json", undefined, { cause })); } }; diff --git a/src/hopper/HopperCallerReconciliation.test.ts b/src/hopper/HopperCallerReconciliation.test.ts new file mode 100644 index 0000000000..fbf9cc7dbc --- /dev/null +++ b/src/hopper/HopperCallerReconciliation.test.ts @@ -0,0 +1,560 @@ +import { expect, it, vi } from "vitest"; +import { err, ok } from "../domain/result.js"; +import type { HopperClient } from "./HopperClient.js"; +import { reconcileHopperCallers } from "./HopperCallerReconciliation.js"; +import { AnalysisWorkScope } from "../application/AnalysisWorkScope.js"; +import { + formatHopperCallerScanReason, + HOPPER_CALLER_SCAN_RETENTION_BYTES, +} from "../domain/native/hopperCallerReconciliation.js"; +import { + hopperCallerScanBudgetForMilliseconds, + hopperCallerScanRecovery, +} from "../config/hopperCallerScanBudget.js"; +import { + AnalysisOutputError, + AnalysisResourceConstraintError, +} from "../domain/analysisErrorCore.js"; + +const identity = (address: string) => ({ + address, + name: address, + symbol: null, + classification: null, + body: { available: false as const, reason: "unknown" }, +}); +const target = identity("0x2000"); +const caller = identity("0x3000"); +const edge = { + source_address: "0x3004", + target_address: "0x2000", + source_procedure: caller, + target_procedure: target, + kind: { + available: false as const, + reason: + "Hopper exposes partial CallReference classification, but not detailed reference flags", + }, + call: { + classification: "direct" as const, + provider_type: 2, + provenance: "hopper-public-python-api:CallReference", + }, +}; +const compactEdge = { + source_address: edge.source_address, + target_address: edge.target_address, + source_procedure: "caller" as const, + target_procedure: target.address, + call: edge.call, +}; +const page = (start: number) => ({ + start, + next: start === 0 ? 64 : 65, + total: 65, + oversized: false, + items: + start === 0 + ? Array.from({ length: 64 }, (_, index) => ({ + caller: identity(`0x${(0x4000 + index).toString(16)}`), + callees: [], + references: [], + })) + : [{ caller, callees: [target.address], references: [compactEdge] }], +}); +const reply = (method: string, start = 0) => + method === "rea_caller_scan_target" + ? { target, instructions: [target.address], instructions_complete: true } + : page(start); +const base = { + target, + instructions: [target.address], + instructions_complete: true, + callers: [], + callers_complete: true, +}; +const referenceResult = { + procedure: target, + direction: "incoming", + reference_kinds_available: false, + unresolved_calls: [], + references: [], +}; + +it("builds one forward index for successive queries in a workflow", async () => { + const callTool = vi + .fn() + .mockImplementation((method, args: { start: number }) => + Promise.resolve(ok(reply(method, args.start))), + ); + const client = { callTool } as unknown as HopperClient; + const scope = new AnalysisWorkScope(); + const options = { workScope: scope }; + const first = await reconcileHopperCallers( + client, + "procedure_callers", + { procedure: "name" }, + base, + options, + 250, + ); + const second = await reconcileHopperCallers( + client, + "procedure_references", + { procedure: "name", direction: "incoming" }, + referenceResult, + options, + 250, + ); + if (!first.ok) throw first.error; + expect(first.value.result).toEqual(["0x3000"]); + expect(first.value.rawResult).toMatchObject({ + native_result: { callers: [] }, + forward_observations: { + target: { address: "0x2000" }, + callee_inventory_callers: [{ address: "0x3000" }], + call_references: [ + { + caller_address: caller.address, + source_address: compactEdge.source_address, + target_address: compactEdge.target_address, + source_procedure: caller.address, + target_procedure: compactEdge.target_procedure, + call: compactEdge.call, + }, + ], + call_site_callers: [], + }, + }); + if ( + first.value.rawResult !== null && + typeof first.value.rawResult === "object" && + !Array.isArray(first.value.rawResult) + ) { + const observations = first.value.rawResult.forward_observations; + const sites = + observations !== null && + typeof observations === "object" && + !Array.isArray(observations) && + Array.isArray(observations.call_references) + ? observations.call_references + : []; + expect(sites[0]).not.toHaveProperty("name"); + expect(sites[0]).not.toHaveProperty("body"); + expect(first.value.rawResult).not.toHaveProperty( + "native_result.instructions", + ); + } + if (!second.ok) throw second.error; + expect(second.value.result).toMatchObject({ + references: [edge], + }); + expect( + callTool.mock.calls.filter(([method]) => method === "rea_caller_scan_page"), + ).toHaveLength(2); + scope.clear(); + await reconcileHopperCallers( + client, + "procedure_callers", + { procedure: "name" }, + base, + options, + 250, + ); + expect( + callTool.mock.calls.filter(([method]) => method === "rea_caller_scan_page"), + ).toHaveLength(4); +}); +it("links each compact call site to its owning caller address", async () => { + const other = identity("0x5000"); + const otherEdge = { + ...compactEdge, + source_address: "0x5004", + source_procedure: "caller" as const, + }; + const callTool = vi.fn().mockImplementation((method: string) => + Promise.resolve( + ok( + method === "rea_caller_scan_target" + ? reply(method) + : { + start: 0, + next: 2, + total: 2, + oversized: false, + items: [ + { caller, callees: [], references: [compactEdge] }, + { + caller: other, + callees: [target.address], + references: [otherEdge], + }, + ], + }, + ), + ), + ); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + { procedure: "name" }, + base, + undefined, + 250, + ); + if (!result.ok) throw result.error; + expect(result.value.rawResult).toMatchObject({ + forward_observations: { + callee_inventory_callers: [{ address: other.address }], + call_site_callers: [{ address: caller.address }], + call_references: [ + { + caller_address: caller.address, + source_address: compactEdge.source_address, + source_procedure: caller.address, + target_address: compactEdge.target_address, + }, + { + caller_address: other.address, + source_address: otherEdge.source_address, + source_procedure: other.address, + target_address: otherEdge.target_address, + }, + ], + }, + }); + expect(result.value.result).toEqual([other.address, caller.address]); +}); + +it("merges paged call sites and pins the selected target", async () => { + const callTool = vi + .fn() + .mockImplementation((method, args: { start: number }) => + Promise.resolve(ok(reply(method, args.start))), + ); + const client = { callTool } as unknown as HopperClient; + const result = await reconcileHopperCallers( + client, + "procedure_references", + { procedure: "name", direction: "incoming" }, + referenceResult, + undefined, + 250, + ); + if (!result.ok) throw result.error; + expect(result.value.result).toMatchObject({ references: [edge] }); + expect(result.value.scan).toMatchObject({ + examined_procedures: 65, + procedure_total: 65, + stop: { code: "complete" }, + instructions_complete: true, + native_callers_complete: null, + }); + if (result.value.scan === undefined) throw new Error("missing scan facts"); + expect(result.value.reason).toBe( + formatHopperCallerScanReason(result.value.scan), + ); + expect(callTool.mock.calls[2]?.[1]).toMatchObject({ start: 64 }); +}); +it("does not reuse a previous scan", async () => { + const callTool = vi + .fn() + .mockImplementation((method, args: { start: number }) => + Promise.resolve(ok(reply(method, args.start))), + ); + const client = { callTool } as unknown as HopperClient; + const result = await reconcileHopperCallers( + client, + "procedure_callers", + { procedure: "name" }, + base, + undefined, + 250, + ); + expect(result.ok && result.value.result).toEqual(["0x3000"]); + callTool.mockImplementation((method, args: { start: number }) => + Promise.resolve( + ok( + method === "rea_caller_scan_target" + ? reply(method) + : { + ...page(args.start), + items: page(args.start).items.map((item) => ({ + ...item, + callees: [], + references: [], + })), + }, + ), + ), + ); + const changed = await reconcileHopperCallers( + client, + "procedure_callers", + { procedure: "name" }, + base, + undefined, + 250, + ); + if (!changed.ok) throw changed.error; + expect(changed.value.result).toEqual([]); +}); +it("stops between pages on cancellation", async () => { + const controller = new AbortController(); + const callTool = vi.fn().mockImplementation((method: string) => { + controller.abort("test cancellation"); + return Promise.resolve(ok(reply(method))); + }); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + base, + { signal: controller.signal }, + 250, + ); + if (result.ok) throw new Error("expected the caller scan to fail"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe("test cancellation"); + expect(result.error.partialObservation).toMatchObject({ + kind: "hopper-caller-reconciliation", + native_result: base, + coverage: { status: "unknown" }, + scan: { + examined_procedures: 64, + procedure_total: 65, + stop: { code: "cancelled" }, + }, + }); + expect(callTool).toHaveBeenCalledTimes(1); +}); +it("preserves cancellation while reporting the last page", async () => { + const controller = new AbortController(); + const callTool = vi + .fn() + .mockImplementation((method: string) => + Promise.resolve( + ok( + method === "rea_caller_scan_target" + ? reply(method) + : { ...page(64), start: 0, next: 1, total: 1 }, + ), + ), + ); + const progress = { + report: async () => { + controller.abort("cancelled in progress"); + }, + }; + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + base, + { signal: controller.signal, progress }, + 250, + ); + if (result.ok) throw new Error("expected the caller scan to fail"); + expect(result.error.cause).toBe("cancelled in progress"); + expect(result.error.partialObservation).toMatchObject({ + kind: "hopper-caller-reconciliation", + native_result: base, + coverage: { status: "unknown" }, + scan: { + examined_procedures: 1, + procedure_total: 1, + stop: { code: "cancelled" }, + }, + forward_destinations: { + identities: [expect.objectContaining({ address: caller.address })], + destinations: [ + expect.objectContaining({ + address: target.address, + caller_addresses: [caller.address], + }), + ], + }, + }); +}); +it("retains completed pages when a later scan page fails", async () => { + let pages = 0; + const callTool = vi.fn().mockImplementation((method: string) => { + if (method === "rea_caller_scan_target") + return Promise.resolve(ok(reply(method))); + pages += 1; + if (pages === 1) return Promise.resolve(ok(page(0))); + return Promise.resolve( + err( + new AnalysisOutputError("rea_caller_scan_page", "bridge page failed"), + ), + ); + }); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + base, + undefined, + 250, + ); + if (result.ok) throw new Error("expected the caller scan to fail"); + expect(result.error).toMatchObject({ _tag: "AnalysisOutputError" }); + expect(result.error.partialObservation).toMatchObject({ + kind: "hopper-caller-reconciliation", + native_result: base, + coverage: { status: "unknown" }, + scan: { + examined_procedures: 64, + procedure_total: 65, + stop: { code: "page_failure" }, + }, + }); +}); +it("reports a changing inventory as incomplete", async () => { + const callTool = vi + .fn() + .mockImplementation((method, args: { start: number }) => + Promise.resolve( + ok( + method === "rea_caller_scan_target" + ? reply(method) + : { ...page(args.start), total: args.start === 0 ? 65 : 66 }, + ), + ), + ); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + base, + undefined, + 250, + ); + if (!result.ok) throw result.error; + expect(result.value.scan).toMatchObject({ + examined_procedures: 64, + procedure_total: 65, + stop: { code: "inventory_changed" }, + }); + if (result.value.scan === undefined) throw new Error("missing scan facts"); + expect(result.value.reason).toBe( + formatHopperCallerScanReason(result.value.scan), + ); +}); + +it("retains original callers and reports a procedure that exceeds the page allowance", async () => { + const callTool = vi + .fn() + .mockResolvedValue( + ok({ start: 0, next: 0, total: 65, oversized: true, items: [] }), + ); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + { ...base, callers: ["0x1000"] }, + undefined, + 250, + ); + if (!result.ok) throw result.error; + expect(result.value.result).toEqual(["0x1000"]); + expect(result.value.scan).toMatchObject({ + examined_procedures: 0, + procedure_total: 65, + stop: { + code: "page_allowance", + allowance_bytes: 512 * 1024, + }, + }); + if (result.value.scan === undefined) throw new Error("missing scan facts"); + expect(result.value.reason).toBe( + `${formatHopperCallerScanReason(result.value.scan)} ${hopperCallerScanRecovery(result.value.scan, hopperCallerScanBudgetForMilliseconds(250))}`, + ); +}); + +it("refuses expanded reference copies that exceed the retained scan budget", async () => { + const huge = "n".repeat(HOPPER_CALLER_SCAN_RETENTION_BYTES); + const heavy = { ...target, name: huge }; + const callTool = vi.fn().mockImplementation((method: string) => + Promise.resolve( + ok( + method === "rea_caller_scan_target" + ? { + target: heavy, + instructions: [target.address], + instructions_complete: true, + } + : { + start: 0, + next: 1, + total: 1, + oversized: false, + items: [ + { + caller, + callees: [target.address], + references: [ + compactEdge, + { ...compactEdge, source_address: "0x3008" }, + ], + }, + ], + }, + ), + ), + ); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_references", + { procedure: "name", direction: "incoming" }, + referenceResult, + undefined, + 250, + ); + if (result.ok) throw new Error("expected the caller scan to fail"); + expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError); + expect(result.error).toMatchObject({ + _tag: "AnalysisResourceConstraintError", + reportedLimits: { + allowance_bytes: HOPPER_CALLER_SCAN_RETENTION_BYTES, + }, + partialObservation: { + kind: "hopper-caller-reconciliation", + scan: { + stop: { + code: "retention_budget", + allowance_bytes: HOPPER_CALLER_SCAN_RETENTION_BYTES, + }, + }, + }, + }); + expect(JSON.stringify(result.error.partialObservation)).not.toContain(huge); +}); + +it("reports truncated target instructions and native callers", async () => { + const callTool = vi + .fn() + .mockResolvedValue( + ok({ start: 0, next: 0, total: 0, oversized: false, items: [] }), + ); + const result = await reconcileHopperCallers( + { callTool } as unknown as HopperClient, + "procedure_callers", + {}, + { ...base, instructions_complete: false, callers_complete: false }, + undefined, + 250, + ); + if (!result.ok) throw result.error; + expect(result.value.scan).toMatchObject({ + examined_procedures: 0, + procedure_total: 0, + stop: { code: "complete" }, + instructions_complete: false, + native_callers_complete: false, + }); + if (result.value.scan === undefined) throw new Error("missing scan facts"); + expect(result.value.reason).toBe( + `${formatHopperCallerScanReason(result.value.scan)} ${hopperCallerScanRecovery(result.value.scan, hopperCallerScanBudgetForMilliseconds(250))}`, + ); +}); diff --git a/src/hopper/HopperCallerReconciliation.ts b/src/hopper/HopperCallerReconciliation.ts new file mode 100644 index 0000000000..aa565d6f57 --- /dev/null +++ b/src/hopper/HopperCallerReconciliation.ts @@ -0,0 +1,749 @@ +import { performance } from "node:perf_hooks"; +import { z } from "zod"; +import type { ExecutionOptions } from "../application/AnalysisProvider.js"; +import { + AnalysisCancelledError, + AnalysisOutputError, + AnalysisResourceConstraintError, +} from "../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { JsonValue } from "../domain/jsonValue.js"; +import { functionDossierSchema } from "../domain/native/functionDossier.js"; +import { + referenceEdgeSchema, + unresolvedCallSchema, +} from "../domain/native/nativeReference.js"; +import { procedureIdentitySchema } from "../domain/native/procedureInfo.js"; +import { nativeCallClassificationSchema } from "../domain/native/nativeCallReference.js"; +import { err, ok, type Result } from "../domain/result.js"; +import type { HopperClient } from "./HopperClient.js"; +import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js"; +import { + hopperCallerScanBudgetForMilliseconds, + hopperCallerScanRecovery, + type HopperCallerScanBudget, +} from "../config/hopperCallerScanBudget.js"; +import { + formatHopperCallerScanReason, + hopperCallerReconciliationPartialSchema, + HOPPER_CALLER_PAGE_ALLOWANCE_BYTES, + HOPPER_CALLER_SCAN_RETENTION_BYTES, + type HopperCallerReconciliationPartial, + type HopperCallerScanCounts, + type HopperCallerScanFacts, + type HopperCallerScanStop, +} from "../domain/native/hopperCallerReconciliation.js"; + +const compactEdgeSchema = z.strictObject({ + source_address: z.string(), + target_address: z.string(), + source_procedure: z.union([ + z.literal("caller"), + procedureIdentitySchema, + z.null(), + ]), + target_procedure: z.string(), + call: nativeCallClassificationSchema, +}); +const pageSchema = z.strictObject({ + start: z.int().nonnegative(), + next: z.int().nonnegative(), + total: z.int().nonnegative(), + oversized: z.boolean(), + items: z.array( + z.strictObject({ + caller: procedureIdentitySchema, + callees: z.array(z.string()), + references: z.array(compactEdgeSchema), + }), + ), +}); +const targetSchema = z.strictObject({ + target: procedureIdentitySchema, + instructions: z.array(z.string()), + instructions_complete: z.boolean(), +}); +const callersBaseSchema = targetSchema.extend({ + callers: z.array(z.string()), + callers_complete: z.boolean(), +}); +const referencesSchema = z.strictObject({ + procedure: procedureIdentitySchema, + direction: z.enum(["incoming", "outgoing"]), + reference_kinds_available: z.boolean(), + unresolved_calls: z.array(unresolvedCallSchema), + references: z.array(referenceEdgeSchema), +}); +type Operation = + | "procedure_callers" + | "procedure_references" + | "analyze_function"; +type Identity = z.infer; +type Edge = z.infer; +type CompactEdge = z.infer; +type Destination = { + callers: Map; + references: Array<{ caller: Identity; edge: CompactEdge }>; +}; +type Index = { + destinations: Map; + counts: HopperCallerScanCounts; +}; +type ScanFailure = { + readonly error: AnalysisError; + readonly counts: HopperCallerScanCounts; + readonly destinations: Map; + readonly unreadPage?: JsonValue; +}; + +/** A child bridge request is a phase of the enclosing user operation. */ +export function reconciliationChildOptions( + options: ExecutionOptions | undefined, +): ExecutionOptions | undefined { + if (options?.progress === undefined) return options; + const progress = options.progress; + return { + ...options, + progress: { + report: (update) => + progress.report({ + ...update, + completed: 0, + total: null, + terminal: false, + }), + }, + }; +} + +async function reportScanProgress( + options: ExecutionOptions | undefined, + start: number, + total: number | undefined, + final = false, +): Promise { + try { + await options?.progress?.report({ + phase: "hopper_caller_reconciliation", + completed: 0, + total: null, + message: `Examined ${start} of ${total ?? "unknown"} procedures`, + ...(final ? { terminal: true } : {}), + }); + } catch { + // Progress is observational and cannot change the analysis result. + } +} + +const scanCounts = ( + stop: HopperCallerScanStop, + examined: number, + procedureTotal: number | undefined, + retainedBytes: number, +): HopperCallerScanCounts => ({ + examined_procedures: examined, + procedure_total: procedureTotal ?? null, + retained_bytes: retainedBytes, + stop, +}); + +async function buildIndex( + client: HopperClient, + operation: Operation, + document: JsonValue | undefined, + options: ExecutionOptions | undefined, + budgetMs: number, +): Promise> { + const destinations = new Map(); + const destination = (address: string): Destination => { + let value = destinations.get(address); + if (value === undefined) { + value = { callers: new Map(), references: [] }; + destinations.set(address, value); + } + return value; + }; + let start = 0, + total: number | undefined, + bytes = 0; + const began = performance.now(); + let stop: HopperCallerScanStop = { code: "complete" }; + const cancelled = (): AnalysisCancelledError => + new AnalysisCancelledError(operation, { cause: options?.signal?.reason }); + const fail = ( + error: AnalysisError, + failureStop: HopperCallerScanStop, + unreadPage?: JsonValue, + ): Result => + err({ + error, + counts: scanCounts(failureStop, start, total, bytes), + destinations, + ...(unreadPage === undefined ? {} : { unreadPage }), + }); + do { + if (options?.signal?.aborted) + return fail(cancelled(), { code: "cancelled" }); + if (start > 0 && performance.now() - began >= budgetMs) { + stop = { code: "work_budget", budget_ms: budgetMs }; + break; + } + if (start > 0) await reportScanProgress(options, start, total); + const args: Record = { start }; + if (document !== undefined) args.document = document; + const pageOptions = + options?.progress === undefined + ? options + : { + ...options, + progress: { + report: (update: { readonly terminal?: boolean }) => + update.terminal === true + ? Promise.resolve() + : reportScanProgress(options, start, total), + }, + }; + const page = await client.callTool( + "rea_caller_scan_page", + args, + pageOptions, + ); + if (!page.ok) + return options?.signal?.aborted + ? fail(cancelled(), { code: "cancelled" }) + : fail( + analysisClientFailureForOperation(page.error, "hopper", operation), + { code: "page_failure" }, + ); + const decoded = pageSchema.safeParse(page.value); + if (!decoded.success) + return options?.signal?.aborted + ? fail(cancelled(), { code: "cancelled" }, page.value) + : fail( + new AnalysisOutputError( + operation, + "Invalid Hopper caller scan page", + { cause: decoded.error }, + ), + { code: "invalid_page" }, + page.value, + ); + const value = decoded.data; + if ( + value.start !== start || + value.next < start || + value.next > value.total || + value.next - start > 64 || + (value.next === start && + value.next !== value.total && + !value.oversized) || + value.items.length !== value.next - start || + (value.oversized && value.next !== start) + ) + return options?.signal?.aborted + ? fail(cancelled(), { code: "cancelled" }, page.value) + : fail( + new AnalysisOutputError( + operation, + "Invalid Hopper caller scan progress", + ), + { code: "invalid_progress" }, + page.value, + ); + if (total !== undefined && value.total !== total) { + stop = { code: "inventory_changed" }; + break; + } + total = value.total; + if (value.oversized) { + stop = { + code: "page_allowance", + allowance_bytes: HOPPER_CALLER_PAGE_ALLOWANCE_BYTES, + }; + break; + } + const pageBytes = Buffer.byteLength(JSON.stringify(page.value)); + if (pageBytes > HOPPER_CALLER_SCAN_RETENTION_BYTES - bytes) { + stop = { + code: "retention_budget", + allowance_bytes: HOPPER_CALLER_SCAN_RETENTION_BYTES, + }; + break; + } + bytes += pageBytes; + for (const item of value.items) { + for (const callee of item.callees) + destination(callee).callers.set(item.caller.address, item.caller); + for (const edge of item.references) { + destination(edge.target_procedure).references.push({ + caller: item.caller, + edge, + }); + } + } + start = value.next; + if (options?.signal?.aborted) + return fail(cancelled(), { code: "cancelled" }); + await reportScanProgress(options, start, total); + if (options?.signal?.aborted) + return fail(cancelled(), { code: "cancelled" }); + } while (start < (total ?? 0)); + await reportScanProgress(options, start, total, true); + if (options?.signal?.aborted) return fail(cancelled(), { code: "cancelled" }); + return ok({ + destinations, + counts: scanCounts(stop, start, total, bytes), + }); +} + +/** Instruction lists are a scan filter, not part of the retained caller reply. */ +const omitInstructionPayload = (value: JsonValue): JsonValue => { + if (value === null || typeof value !== "object" || Array.isArray(value)) + return value; + if (!Object.hasOwn(value, "instructions")) return value; + const { instructions: _instructions, ...rest } = value; + return rest; +}; + +/** + * `source_procedure: "caller"` means the enclosing page item. Keep that + * owner's address on the site so the compact record does not depend on a + * separate inventory list or a copied identity. + */ +const compactCallReference = ( + caller: Identity, + edge: CompactEdge, +): { + readonly caller_address: string; + readonly source_address: string; + readonly target_address: string; + readonly source_procedure: string | null; + readonly target_procedure: string; + readonly call?: CompactEdge["call"]; +} => ({ + caller_address: caller.address, + source_address: edge.source_address, + target_address: edge.target_address, + source_procedure: + edge.source_procedure === null || edge.source_procedure === "caller" + ? edge.source_procedure === "caller" + ? caller.address + : null + : edge.source_procedure.address, + target_procedure: edge.target_procedure, + ...(edge.call === undefined ? {} : { call: edge.call }), +}); + +/** One shared identity record; destinations cite addresses instead of copies. */ +const compactForwardDestinations = ( + destinations: Map, +): JsonValue => { + const identities = new Map(); + const remember = (identity: Identity): void => { + if (!identities.has(identity.address)) + identities.set(identity.address, identity); + }; + const rows = [...destinations.entries()].map(([address, value]) => { + const callerAddresses: string[] = []; + for (const caller of value.callers.values()) { + remember(caller); + callerAddresses.push(caller.address); + } + return { + address, + caller_addresses: callerAddresses, + references: value.references.map(({ caller, edge }) => { + remember(caller); + if ( + edge.source_procedure !== null && + edge.source_procedure !== "caller" + ) + remember(edge.source_procedure); + return compactCallReference(caller, edge); + }), + }; + }); + return { + identities: [...identities.values()], + destinations: rows, + }; +}; + +export const reconcileHopperCallers = async ( + client: HopperClient, + operation: Operation, + parameters: Readonly>, + original: JsonValue, + options: ExecutionOptions | undefined, + budget: number | HopperCallerScanBudget, +): Promise< + Result< + { + result: JsonValue; + rawResult: JsonValue; + reason: string; + scan?: HopperCallerScanFacts; + }, + AnalysisError + > +> => { + const scanBudget = + typeof budget === "number" + ? hopperCallerScanBudgetForMilliseconds(budget) + : budget; + const budgetMs = scanBudget.milliseconds; + const describeScan = (facts: HopperCallerScanFacts): string => { + const observed = formatHopperCallerScanReason(facts); + const recovery = hopperCallerScanRecovery(facts, scanBudget); + return recovery === "" ? observed : `${observed} ${recovery}`; + }; + const retainNative = ( + error: AnalysisError, + partial: { + readonly scan: HopperCallerScanFacts | null; + readonly destinations: Map | null; + readonly unreadPage?: JsonValue; + }, + ): AnalysisError => { + const observation: HopperCallerReconciliationPartial = { + kind: "hopper-caller-reconciliation", + native_result: original, + scan: partial.scan, + forward_destinations: + partial.destinations === null + ? null + : compactForwardDestinations(partial.destinations), + coverage: { + status: "unknown", + reason: + partial.scan === null + ? "Hopper caller reconciliation did not examine the procedure inventory; unexamined procedure relationships are unknown." + : describeScan(partial.scan), + limited_directions: + operation === "analyze_function" + ? ["callers", "callees"] + : ["callers"], + }, + ...(partial.unreadPage === undefined + ? {} + : { unread_page: partial.unreadPage }), + }; + error.retainPartialObservation( + hopperCallerReconciliationPartialSchema.parse(observation), + ); + return error; + }; + const base = + operation === "procedure_callers" + ? callersBaseSchema.safeParse(original) + : undefined; + if (base !== undefined && !base.success) + return err( + retainNative( + new AnalysisOutputError( + operation, + "Invalid Hopper caller observation", + { cause: base.error }, + ), + { scan: null, destinations: null }, + ), + ); + const parsed = + operation === "procedure_callers" + ? z + .array(z.string()) + .safeParse(base?.success ? base.data.callers : undefined) + : operation === "analyze_function" + ? functionDossierSchema.safeParse(original) + : referencesSchema.safeParse(original); + if (!parsed.success) + return err( + retainNative( + new AnalysisOutputError( + operation, + "Invalid Hopper caller observation", + { cause: parsed.error }, + ), + { scan: null, destinations: null }, + ), + ); + const args: Record = {}; + if (parameters.document !== undefined) args.document = parameters.document; + if (parameters.procedure !== undefined) args.procedure = parameters.procedure; + const targetResponse = base?.success + ? ok({ + target: base.data.target, + instructions: base.data.instructions, + instructions_complete: base.data.instructions_complete, + }) + : await client.callTool( + "rea_caller_scan_target", + args, + reconciliationChildOptions(options), + ); + if (!targetResponse.ok) + return err( + retainNative( + analysisClientFailureForOperation( + targetResponse.error, + "hopper", + operation, + ), + { scan: null, destinations: null }, + ), + ); + if (options?.signal?.aborted) + return err( + retainNative( + new AnalysisCancelledError(operation, { + cause: options.signal.reason, + }), + { scan: null, destinations: null }, + ), + ); + const selected = targetSchema.safeParse(targetResponse.value); + if (!selected.success) + return err( + retainNative( + new AnalysisOutputError( + operation, + "Invalid Hopper caller scan target", + { cause: selected.error }, + ), + { scan: null, destinations: null }, + ), + ); + const { target, instructions, instructions_complete } = selected.data; + if ( + operation !== "procedure_callers" && + "procedure" in parsed.data && + parsed.data.procedure.address !== target.address + ) + return ok({ + result: original, + rawResult: original, + reason: + "The selected procedure changed during reconciliation. Relationship coverage is unknown.", + }); + const documentKey = + parameters.document === undefined + ? "default" + : `document:${String(parameters.document)}`; + let indexByDocument = options?.workScope?.get>(client); + let index = indexByDocument?.get(documentKey); + if (index === undefined) { + const scanned = await buildIndex( + client, + operation, + parameters.document, + options, + budgetMs, + ); + if (!scanned.ok) { + const failure = scanned.error; + return err( + retainNative(failure.error, { + scan: { + ...failure.counts, + instructions_complete, + native_callers_complete: base?.success + ? base.data.callers_complete + : null, + }, + destinations: failure.destinations, + ...(failure.unreadPage === undefined + ? {} + : { unreadPage: failure.unreadPage }), + }), + ); + } + index = scanned.value; + if (options?.workScope !== undefined) { + if (indexByDocument === undefined) { + indexByDocument = new Map(); + options.workScope.set(client, indexByDocument); + } + indexByDocument.set(documentKey, index); + } + } + const addresses = new Set(instructions); + addresses.add(target.address); + const selectedDestination = index.destinations.get(target.address); + const callers = new Map(selectedDestination?.callers ?? []); + const selectedReferences: Array<{ caller: Identity; edge: CompactEdge }> = []; + for (const item of selectedDestination?.references ?? []) { + if (!addresses.has(item.edge.target_address)) continue; + const source = + item.edge.source_procedure === "caller" + ? item.caller + : item.edge.source_procedure; + if (source !== null) callers.set(source.address, source); + selectedReferences.push(item); + } + const unavailableKind = { + available: false as const, + reason: + "Hopper exposes partial CallReference classification, but not detailed reference flags", + }; + // The scan budget counted compact pages. Expanded edges repeat each + // identity, so measure that copy before allocating it. + const expandedReferenceBytes = (): number => { + let bytes = 2; + const targetBytes = Buffer.byteLength(JSON.stringify(target)); + const kindBytes = Buffer.byteLength(JSON.stringify(unavailableKind)); + for (const { caller, edge } of selectedReferences) { + const source = + edge.source_procedure === "caller" + ? caller + : edge.source_procedure === null + ? null + : edge.source_procedure; + const sourceBytes = + source === null ? 4 : Buffer.byteLength(JSON.stringify(source)); + const callBytes = + edge.call === undefined + ? 0 + : Buffer.byteLength(JSON.stringify(edge.call)); + bytes += + 64 + + sourceBytes + + targetBytes + + kindBytes + + callBytes + + Buffer.byteLength(edge.source_address) + + Buffer.byteLength(edge.target_address); + if (bytes > HOPPER_CALLER_SCAN_RETENTION_BYTES) return bytes; + } + return bytes; + }; + const retainedNativeResult = omitInstructionPayload(original); + const compactReferences = selectedReferences.map(({ caller, edge }) => + compactCallReference(caller, edge), + ); + const inventoryCallers = selectedDestination?.callers ?? new Map(); + const siteCallers = new Map(); + for (const { caller } of selectedReferences) + if ( + !inventoryCallers.has(caller.address) && + !siteCallers.has(caller.address) + ) + siteCallers.set(caller.address, caller); + if ( + operation !== "procedure_callers" && + expandedReferenceBytes() > HOPPER_CALLER_SCAN_RETENTION_BYTES + ) { + const scan: HopperCallerScanFacts = { + ...index.counts, + instructions_complete, + native_callers_complete: base?.success + ? base.data.callers_complete + : null, + stop: { + code: "retention_budget", + allowance_bytes: HOPPER_CALLER_SCAN_RETENTION_BYTES, + }, + }; + return err( + retainNative( + new AnalysisResourceConstraintError( + operation, + "memory", + describeScan(scan), + { allowance_bytes: HOPPER_CALLER_SCAN_RETENTION_BYTES }, + { + retryable: false, + remediationAction: hopperCallerScanRecovery(scan, scanBudget), + }, + ), + { scan, destinations: index.destinations }, + ), + ); + } + const references = new Map(); + if (operation !== "procedure_callers") + for (const { caller, edge } of selectedReferences) { + const source = + edge.source_procedure === "caller" ? caller : edge.source_procedure; + references.set(`${edge.source_address}:${edge.target_address}`, { + source_address: edge.source_address, + target_address: edge.target_address, + source_procedure: source, + target_procedure: target, + kind: unavailableKind, + call: edge.call, + }); + } + const mergeReferences = ( + existing: z.infer[], + excludeSource?: string, + ) => { + const merged = new Map( + existing.map((edge) => [ + `${edge.source_address}:${edge.target_address}`, + edge, + ]), + ); + for (const [key, edge] of references) { + if ( + excludeSource !== undefined && + edge.source_procedure?.address === excludeSource + ) + continue; + const old = merged.get(key); + merged.set( + key, + old === undefined + ? edge + : old.call === undefined && edge.call !== undefined + ? { ...old, call: edge.call } + : old, + ); + } + return [...merged.values()]; + }; + let result: JsonValue; + if (operation === "procedure_callers") + result = [...new Set([...(parsed.data as string[]), ...callers.keys()])]; + else if (operation === "analyze_function") { + const dossier = functionDossierSchema.parse(parsed.data); + const identities = new Map( + dossier.callers.map((item) => [item.address, item]), + ); + for (const [address, caller] of callers) + if (!identities.has(address)) identities.set(address, caller); + result = { + ...dossier, + callers: [...identities.values()], + incoming_references: mergeReferences( + dossier.incoming_references, + dossier.procedure.address, + ), + }; + } else { + const facts = referencesSchema.parse(parsed.data); + result = { ...facts, references: mergeReferences(facts.references) }; + } + const scan: HopperCallerScanFacts = { + ...index.counts, + instructions_complete, + native_callers_complete: base?.success ? base.data.callers_complete : null, + }; + return ok({ + result, + rawResult: { + native_result: retainedNativeResult, + forward_observations: { + target, + callee_inventory_callers: [ + ...(selectedDestination?.callers.values() ?? []), + ], + call_site_callers: [...siteCallers.values()], + call_references: compactReferences, + provenance: + "hopper-public-python-api:getAllCalleeProcedures and CallReference", + }, + }, + scan, + reason: describeScan(scan), + }); +}; diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index 66ec8448ab..edb7956c59 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -2,11 +2,48 @@ import { describe, expect, it, vi } from "vitest"; import { parseConfig } from "../config/parseConfig.js"; import { silentLogger } from "../logger.js"; -import { ok } from "../domain/result.js"; +import { err, ok } from "../domain/result.js"; +import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; import { HopperClient } from "./HopperClient.js"; import { HopperProvider } from "./HopperProvider.js"; +import { isSnapshotCacheable } from "../application/binary/AnalysisSnapshotCache.js"; +import { routeAllowsSnapshotReplay } from "../application/binary/SessionProviderPolicy.js"; +import type { SessionProviderRoute } from "../application/binary/SessionProviderRouter.js"; describe("Hopper provider capabilities", () => { + it("does not replay snapshots for mutable Hopper document observations", () => { + const config = parseConfig({}); + if (!config.ok) throw config.error; + const provider = new HopperProvider(config.value, silentLogger, {}); + const capabilities = provider.capabilities(); + const route: SessionProviderRoute = { + identity: provider.identity(), + capabilities: new Map(capabilities.map((item) => [item.operation, item])), + profile: null, + binding: null, + selection: undefined, + createClient: () => { + throw new Error("No client needed"); + }, + }; + const readable = capabilities.filter( + (item) => item.available && !item.effects.mutatesArtifact, + ); + expect(readable.length).toBeGreaterThan(0); + for (const descriptor of readable) { + expect(descriptor.cachePolicy).toBe("live"); + expect(routeAllowsSnapshotReplay(route, descriptor.operation)).toBe( + false, + ); + expect( + isSnapshotCacheable(descriptor.operation, descriptor, { + document: "selected", + address: "0x1000", + }), + ).toBe(false); + } + expect(routeAllowsSnapshotReplay(route, "get_call_graph")).toBe(false); + }); it("declines caller-selected gamecube-dol targets before the ELF powerpc gate", () => { const config = parseConfig({}); if (!config.ok) throw new Error("expected valid configuration"); @@ -129,18 +166,65 @@ describe("Hopper relationship coverage", () => { async (operation, directions) => { const config = parseConfig({}); if (!config.ok) throw config.error; + const identity = (address: string) => ({ + address, + name: address, + symbol: null, + classification: null, + body: { available: false as const, reason: "unknown" }, + }); const source = operation === "analyze_function" ? { + procedure: { ...identity("0x1000"), signature: null, locals: [] }, + pseudocode: "", + assembly: [], + comments: [], callers: [], - callees: [{ address: "0x2000", name: "tail_target" }], + callees: [identity("0x2000")], + incoming_references: [], + outgoing_references: [], + referenced_strings: [], + referenced_names: [], + basic_blocks: [], + native_api: null, + native_value_flow: null, + limitations: [], } : operation === "procedure_callees" ? ["0x2000"] : []; const callTool = vi .spyOn(HopperClient.prototype, "callTool") - .mockResolvedValue(ok(source)); + .mockImplementation((method) => + Promise.resolve( + ok( + method === "rea_caller_base" + ? { + target: identity("0x1000"), + instructions: ["0x1000"], + instructions_complete: true, + callers: [], + callers_complete: true, + } + : method === "rea_caller_scan_target" + ? { + target: identity("0x1000"), + instructions: ["0x1000"], + instructions_complete: true, + } + : method === "rea_caller_scan_page" + ? { + start: 0, + next: 0, + total: 0, + items: [], + oversized: false, + } + : source, + ), + ), + ); const client = new HopperProvider( config.value, silentLogger, @@ -158,10 +242,35 @@ describe("Hopper relationship coverage", () => { }); if (!observed.ok) throw observed.error; expect(observed.value.result).toEqual(source); + if (operation === "procedure_callers") { + expect(observed.value.rawResult).toMatchObject({ + native_result: { + callers: [], + callers_complete: true, + instructions_complete: true, + target: { address: "0x1000" }, + }, + forward_observations: { + call_references: [], + call_site_callers: [], + }, + }); + expect(observed.value.rawResult).toMatchObject({ + native_result: expect.not.objectContaining({ + instructions: expect.any(Array), + }), + }); + } + if (operation === "procedure_callees") + expect(observed.value.rawResult).toBeNull(); expect(observed.value.relationshipCoverage).toMatchObject({ status: "unknown", limited_directions: directions, - reason: expect.stringContaining("resolved tail calls"), + reason: expect.stringContaining( + operation === "procedure_callees" + ? "resolved tail calls" + : "reconciliation examined", + ), }); } finally { await client.close(); @@ -170,3 +279,95 @@ describe("Hopper relationship coverage", () => { }, ); }); + +describe("Hopper caller reconciliation failures", () => { + it("keeps the native caller reply when a later scan page fails", async () => { + const config = parseConfig({}); + if (!config.ok) throw config.error; + const identity = (address: string) => ({ + address, + name: address, + symbol: null, + classification: null, + body: { available: false as const, reason: "unknown" }, + }); + const native = { + target: identity("0x1000"), + instructions: ["0x1000"], + instructions_complete: true, + callers: ["0x1111"], + callers_complete: true, + }; + let pages = 0; + const callTool = vi + .spyOn(HopperClient.prototype, "callTool") + .mockImplementation((method) => { + if (method === "rea_caller_base") return Promise.resolve(ok(native)); + if (method !== "rea_caller_scan_page") return Promise.resolve(ok(null)); + pages += 1; + if (pages === 1) + return Promise.resolve( + ok({ + start: 0, + next: 1, + total: 2, + oversized: false, + items: [ + { + caller: identity("0x3000"), + callees: ["0x1000"], + references: [], + }, + ], + }), + ); + return Promise.resolve( + err( + new AnalysisOutputError( + "rea_caller_scan_page", + "bridge page failed", + ), + ), + ); + }); + const client = new HopperProvider( + config.value, + silentLogger, + {}, + "linux", + ).createClient({ + path: "/tmp/fixture.hop", + sha256: "a".repeat(64), + kind: "database", + format: "analysis-database", + }); + try { + const observed = await client.execute("procedure_callers", { + procedure: "0x1000", + }); + if (observed.ok) throw new Error("expected procedure_callers to fail"); + expect(observed.error.partialObservation).toMatchObject({ + kind: "hopper-caller-reconciliation", + native_result: native, + coverage: { status: "unknown", limited_directions: ["callers"] }, + scan: { + examined_procedures: 1, + procedure_total: 2, + stop: { code: "page_failure" }, + }, + forward_destinations: { + identities: [expect.objectContaining({ address: "0x3000" })], + destinations: [ + expect.objectContaining({ + address: "0x1000", + caller_addresses: ["0x3000"], + }), + ], + }, + }); + } finally { + await client.close(); + callTool.mockRestore(); + } + }); +}); diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index 2f6d58010c..c61e1d0c7e 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -44,6 +44,11 @@ import { HopperRegexSearch } from "./HopperRegexSearch.js"; import { HopperClient } from "./HopperClient.js"; import { projectHopperNameInventory } from "./HopperNameInventory.js"; import { hopperMachOImageSchema } from "./HopperMachOImage.js"; +import { + reconcileHopperCallers, + reconciliationChildOptions, +} from "./HopperCallerReconciliation.js"; +import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js"; import { hopperCapabilities, @@ -346,10 +351,14 @@ export class HopperProvider implements AnalysisProviderCandidate { operation: AnalysisOperation, normalized: JsonValue, rawResult: JsonValue | null = null, + reconciliationReason?: string, ) => createAnalysisExecution(normalized, executionProvider, { // Retain the source reply when an adapter-owned projection changes it. rawResult, + ...(reconciliationReason === undefined + ? {} + : { cachePolicy: "live" as const }), ...(profile === undefined ? {} : { analysisProfile: profile }), ...(operation === "procedure_callers" || operation === "procedure_callees" || @@ -364,11 +373,16 @@ export class HopperProvider implements AnalysisProviderCandidate { ? (["callees"] as const) : (["callers", "callees"] as const), reason: + reconciliationReason ?? "Hopper reports caller and callee procedures through independent public API inventories. Their completeness, including resolved tail calls and agreement between reverse and forward relationships, has not been established. Observed relationships are retained.", }, } : {}), limitations: [ + ...(reconciliationReason === undefined || + operation !== "procedure_references" + ? [] + : [reconciliationReason]), ...(target.kind !== "executable" ? [] : [ @@ -395,13 +409,35 @@ export class HopperProvider implements AnalysisProviderCandidate { execute: async (operation, parameters, options) => { try { return await sourceReads.run(async () => { + const willReconcile = + operation === "procedure_callers" || + operation === "analyze_function" || + (operation === "procedure_references" && + parameters.direction === "incoming"); const result = (operation === "search_strings" || operation === "search_procedures") && parameters.mode === "regex" ? await regexSearch.execute(operation, parameters, options) - : await client.callTool(operation, parameters, options); - if (!result.ok) return result; + : await client.callTool( + operation === "procedure_callers" + ? "rea_caller_base" + : operation, + parameters, + willReconcile + ? reconciliationChildOptions(options) + : options, + ); + if (!result.ok) + return operation === "procedure_callers" + ? err( + analysisClientFailureForOperation( + result.error, + "hopper", + operation, + ), + ) + : result; const mapped = operation === "address_to_file_offset" ? await mapHopperFileOffset(target, result.value, { @@ -419,15 +455,35 @@ export class HopperProvider implements AnalysisProviderCandidate { }) : result; if (!mapped.ok) return mapped; + const reconciled = + operation === "procedure_callers" || + operation === "analyze_function" || + (operation === "procedure_references" && + parameters.direction === "incoming") + ? await reconcileHopperCallers( + client, + operation, + parameters, + mapped.value, + options, + this.config.hopperCallerScanBudget, + ) + : undefined; + if (reconciled !== undefined && !reconciled.ok) return reconciled; + const reconciledResult = reconciled?.value.result ?? mapped.value; const inventory = operation === "list_names" - ? projectHopperNameInventory(mapped.value) - : { result: mapped.value, changed: false }; + ? projectHopperNameInventory(reconciledResult) + : { result: reconciledResult, changed: false }; return ok( projectExecution( operation, inventory.result, - inventory.changed || mapped !== result ? result.value : null, + reconciled?.value.rawResult ?? + (inventory.changed || mapped !== result + ? result.value + : null), + reconciled?.value.reason, ), ); }); diff --git a/src/hopper/HopperProviderCapabilities.ts b/src/hopper/HopperProviderCapabilities.ts index 877f56421d..487a0b1733 100644 --- a/src/hopper/HopperProviderCapabilities.ts +++ b/src/hopper/HopperProviderCapabilities.ts @@ -79,6 +79,8 @@ export const hopperCapabilities = ( Object.freeze({ provider: HOPPER_PROVIDER_IDENTITY, operation, + // Hopper's GUI can change the document without a revision token. + cachePolicy: "live" as const, ...(operation === "search_bytes" ? { available: false as const, diff --git a/src/inspector/JavaScriptRuntimeScope.ts b/src/inspector/JavaScriptRuntimeScope.ts index 16fe10d267..c13946eefb 100644 --- a/src/inspector/JavaScriptRuntimeScope.ts +++ b/src/inspector/JavaScriptRuntimeScope.ts @@ -14,7 +14,10 @@ import type { JavaScriptRuntimeTargetLocation, } from "../domain/javascript/javascriptRuntimeObservation.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; -import { authorizedElectronFile } from "../browser/ElectronFileScope.js"; +import { + authorizedElectronFile, + type ElectronFileScope, +} from "../browser/ElectronFileScope.js"; export type RuntimeLocationDecision = | { readonly allowed: true; readonly location: JavaScriptRuntimeLocation } @@ -56,6 +59,7 @@ export const authorizeRuntimeLocation = async ( | "observe_javascript_runtime" | "list_javascript_runtime_targets" = "observe_javascript_runtime", asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION, + files?: ElectronFileScope, ): Promise => { throwIfRuntimeObservationCancelled(signal, operation); if (URL.parse(value)?.protocol === "file:") { @@ -64,6 +68,7 @@ export const authorizeRuntimeLocation = async ( signal, operation, asarHeaderAdmission, + files, ); throwIfRuntimeObservationCancelled(signal, operation); return filePath === undefined @@ -114,6 +119,7 @@ export const authorizeRuntimeTargetLocation = async ( | "observe_javascript_runtime" | "list_javascript_runtime_targets" = "list_javascript_runtime_targets", asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION, + files?: ElectronFileScope, ): Promise< | { readonly allowed: true; @@ -132,6 +138,7 @@ export const authorizeRuntimeTargetLocation = async ( signal, operation, asarHeaderAdmission, + files, ); const reportedPath = value.slice("file://".length); if (!reportedPath.startsWith("/") && !/^[a-z]:[/\\_]/iu.test(reportedPath)) @@ -140,6 +147,7 @@ export const authorizeRuntimeTargetLocation = async ( signal, operation, asarHeaderAdmission, + files, ); // Node concatenates the pathname, then replaces quotes and backslashes with @@ -151,6 +159,7 @@ export const authorizeRuntimeTargetLocation = async ( signal, operation, asarHeaderAdmission, + files, ); if (decision.allowed) return decision; } diff --git a/src/inspector/V8InspectorAdmission.test.ts b/src/inspector/V8InspectorAdmission.test.ts new file mode 100644 index 0000000000..cf59910405 --- /dev/null +++ b/src/inspector/V8InspectorAdmission.test.ts @@ -0,0 +1,89 @@ +import { expect, it } from "vitest"; + +import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js"; +import { V8InspectorAdmission } from "./V8InspectorProvider.js"; + +const gate = (): { promise: Promise; release: () => void } => { + let release = (): void => {}; + const promise = new Promise((resolve) => { + release = resolve; + }); + return { promise, release }; +}; + +it("does not make one inspector admission wait on another", async () => { + const first = new V8InspectorAdmission(); + const second = new V8InspectorAdmission(); + const held = gate(); + const running = first.run("observe_javascript_runtime", () => held.promise); + const closing = first.close(); + try { + let secondRan = false; + await second.run("list_javascript_runtime_targets", async () => { + secondRan = true; + }); + expect(secondRan).toBe(true); + let firstFinished = false; + void closing.finally(() => { + firstFinished = true; + }); + await Promise.resolve(); + expect(firstFinished).toBe(false); + } finally { + held.release(); + await Promise.allSettled([running, closing]); + } +}); + +it("holds a later operation until the same admission finishes closing", async () => { + const admission = new V8InspectorAdmission(); + const held = gate(); + const running = admission.run( + "observe_javascript_runtime", + () => held.promise, + ); + const closing = admission.close(); + let proceeded = false; + const queued = admission.run("list_javascript_runtime_targets", async () => { + proceeded = true; + }); + try { + await Promise.resolve(); + await Promise.resolve(); + expect(proceeded).toBe(false); + } finally { + held.release(); + await Promise.allSettled([running, closing, queued]); + } + expect(proceeded).toBe(true); +}); + +it("closes one inspector file scope without waiting for another", async () => { + const first = new V8InspectorAdmission(); + const second = new V8InspectorAdmission(); + const held = gate(); + let entered!: () => void; + const started = new Promise((resolve) => { + entered = resolve; + }); + const scope = first.files; + const original = scope.retryCleanup; + scope.retryCleanup = function (this: typeof scope) { + entered(); + return held.promise.then(() => original.call(this)); + }; + const closingFirst = first.close(); + const closingSecond = second.close(); + try { + await awaitAcknowledgement(started, closingFirst, "first file cleanup"); + await awaitAcknowledgement( + closingSecond.then(() => undefined), + closingFirst, + "second file cleanup", + ); + } finally { + held.release(); + await Promise.allSettled([closingFirst, closingSecond]); + scope.retryCleanup = original; + } +}); diff --git a/src/inspector/V8InspectorProvider.closeOwnership.test.ts b/src/inspector/V8InspectorProvider.closeOwnership.test.ts index f4bf286cef..3500411f19 100644 --- a/src/inspector/V8InspectorProvider.closeOwnership.test.ts +++ b/src/inspector/V8InspectorProvider.closeOwnership.test.ts @@ -12,7 +12,10 @@ import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { observeJavaScriptRuntime } from "../application/javascript/JavaScriptRuntimeObservationService.js"; import { observeJavaScriptRuntimeInputSchema } from "../domain/javascript/javascriptRuntimeObservation.js"; -import { V8InspectorProvider } from "./V8InspectorProvider.js"; +import { + V8InspectorAdmission, + V8InspectorProvider, +} from "./V8InspectorProvider.js"; const inspectorCloseFixture = async () => { const root = await mkdtemp(join(tmpdir(), "rea-inspector-close-owner-")); @@ -60,6 +63,7 @@ const inspectorCloseFixture = async () => { ? Promise.resolve() : new Promise((resolve) => connectionWaiters.set(count, resolve)); + const admission = new V8InspectorAdmission(); let resolveCloseStarted: (() => void) | undefined; const closeStarted = new Promise((resolve) => { resolveCloseStarted = resolve; @@ -87,7 +91,7 @@ const inspectorCloseFixture = async () => { onTestFinished(async () => { closeSpy.mockRestore(); if (closeCalls > 0) rejectFirstClose?.(new Error("fixture finalization")); - await new V8InspectorProvider().close(); + await new V8InspectorProvider(undefined, admission).close(); for (const socket of sockets.clients) socket.terminate(); await new Promise((resolve) => sockets.close(() => resolve())); if (server.listening) @@ -115,6 +119,7 @@ const inspectorCloseFixture = async () => { get acceptedConnections() { return acceptedConnections; }, + admission, }; }; @@ -128,10 +133,12 @@ it("retains cancelled final-close ownership and retries the exact transport befo closeSpy, waitForConnections, rejectFirstClose, + admission, } = fixture; + const provider = () => new V8InspectorProvider(undefined, admission); const controller = new AbortController(); const cancellationReason = new Error("cancelled during transport close"); - const observing = observeJavaScriptRuntime(new V8InspectorProvider(), input, { + const observing = observeJavaScriptRuntime(provider(), input, { signal: controller.signal, }); await closeStarted; @@ -161,30 +168,21 @@ it("retains cancelled final-close ownership and retries the exact transport befo }); expect(projectAnalysisError(error).code).toBe("cleanup_incomplete"); - const blocked = await observeJavaScriptRuntime( - new V8InspectorProvider(), - input, - ); + const blocked = await observeJavaScriptRuntime(provider(), input); expect(blocked.ok).toBe(false); if (blocked.ok) throw new Error("Expected unresolved transport admission refusal"); expect(blocked.error.cleanupIncomplete).toBe(true); expect(fixture.acceptedConnections).toBe(1); - const recovered = await observeJavaScriptRuntime( - new V8InspectorProvider(), - input, - ); + const recovered = await observeJavaScriptRuntime(provider(), input); expect(recovered.ok).toBe(true); expect(error.cleanupIncomplete).toBe(false); expect(error.cleanupResources).toEqual([]); expect(fixture.acceptedConnections).toBe(2); closeFailures.add(5); - const cleanupOnly = await observeJavaScriptRuntime( - new V8InspectorProvider(), - input, - ); + const cleanupOnly = await observeJavaScriptRuntime(provider(), input); expect(cleanupOnly.ok).toBe(false); if (cleanupOnly.ok) throw new Error("Expected close-only cleanup failure"); expect(cleanupOnly.error.cleanupIncomplete).toBe(true); @@ -201,21 +199,18 @@ it("retains cancelled final-close ownership and retries the exact transport befo observation_ms: 30_000, }); const activeController = new AbortController(); - const activeObservation = observeJavaScriptRuntime( - new V8InspectorProvider(), - activeInput, - { signal: activeController.signal }, - ); + const activeObservation = observeJavaScriptRuntime(provider(), activeInput, { + signal: activeController.signal, + }); try { await waitForConnections(4); let shutdownSettled = false; - const shutdown = new V8InspectorProvider().close().then(() => { - shutdownSettled = true; - }); - const queuedObservation = observeJavaScriptRuntime( - new V8InspectorProvider(), - input, - ); + const shutdown = provider() + .close() + .then(() => { + shutdownSettled = true; + }); + const queuedObservation = observeJavaScriptRuntime(provider(), input); await new Promise((resolve) => setImmediate(resolve)); expect(shutdownSettled).toBe(false); expect(fixture.acceptedConnections).toBe(4); diff --git a/src/inspector/V8InspectorProvider.ts b/src/inspector/V8InspectorProvider.ts index bd56a42e92..7571713151 100644 --- a/src/inspector/V8InspectorProvider.ts +++ b/src/inspector/V8InspectorProvider.ts @@ -32,8 +32,8 @@ import { } from "../browser/CdpScriptContextFacts.js"; import type { CdpConnection, CdpEvent } from "../browser/CdpConnection.js"; import { + ElectronFileScope, mapElectronFileAuthorizationFailure, - retryElectronFileCleanup, } from "../browser/ElectronFileScope.js"; import { CdpTargetSessionOwner, @@ -58,77 +58,94 @@ import { import { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentity.js"; -// Providers are created per application operation, so exact transport cleanup -// ownership must survive their return and gate the next Inspector connection. -const inspectorConnections = new CdpTargetSessionOwner(); -let inspectorActiveOperations = 0; -let inspectorClosing = false; -let inspectorClosePromise: Promise | undefined; -const inspectorIdleWaiters = new Set<() => void>(); +/** + * Admission and transport cleanup for one Inspector host. Providers are created + * per operation; the host passes one admission so those providers share cleanup + * without a process-global counter. + */ +export class V8InspectorAdmission { + readonly files = new ElectronFileScope(); + readonly connections = new CdpTargetSessionOwner(); + #activeOperations = 0; + #closing = false; + #closePromise: Promise | undefined; + readonly #idleWaiters = new Set<() => void>(); -const withInspectorAdmission = async ( - operation: BrowserObservationOperation, - work: () => Promise, -): Promise => { - if (inspectorClosing && inspectorClosePromise !== undefined) - await inspectorClosePromise; - if (inspectorClosing) - throw new ProviderAdapterError( - V8_INSPECTOR_PROVIDER_IDENTITY.id, - operation, - { - diagnostics: { reason: "provider_closing" }, - userMessage: "Inspector provider cleanup is in progress.", - }, - ); - inspectorActiveOperations += 1; - try { - return await work(); - } finally { - inspectorActiveOperations -= 1; - if (inspectorActiveOperations === 0) { - for (const resolve of inspectorIdleWaiters) resolve(); - inspectorIdleWaiters.clear(); + async run( + operation: BrowserObservationOperation, + work: () => Promise, + ): Promise { + if (this.#closing && this.#closePromise !== undefined) + await this.#closePromise; + if (this.#closing) + throw new ProviderAdapterError( + V8_INSPECTOR_PROVIDER_IDENTITY.id, + operation, + { + diagnostics: { reason: "provider_closing" }, + userMessage: "Inspector provider cleanup is in progress.", + }, + ); + this.#activeOperations += 1; + try { + return await work(); + } finally { + this.#activeOperations -= 1; + if (this.#activeOperations === 0) { + for (const resolve of this.#idleWaiters) resolve(); + this.#idleWaiters.clear(); + } } } -}; -const closeInspectorResources = async (): Promise => { - inspectorClosing = true; - try { - while (inspectorActiveOperations > 0) - await new Promise((resolve) => inspectorIdleWaiters.add(resolve)); - const failures: AnalysisError[] = []; + async close(): Promise { + if (this.#closePromise !== undefined) return await this.#closePromise; + const attempt = this.#closeResources(); + this.#closePromise = attempt; try { - await inspectorConnections.retryPending(); - } catch (cause: unknown) { - failures.push(providerError(cause, "observe_javascript_runtime")); + await attempt; + } finally { + if (this.#closePromise === attempt) this.#closePromise = undefined; } - try { - await retryElectronFileCleanup(); - } catch (cause: unknown) { - failures.push(providerError(cause, "observe_javascript_runtime")); - } - if (failures.length === 0) return; - if (failures.length === 1) throw failures[0]!; - const cleanup = new ProviderCleanupError( - V8_INSPECTOR_PROVIDER_IDENTITY.id, - failures.flatMap(({ cleanupResources }) => cleanupResources), - { reason: "Inspector provider cleanup remains incomplete." }, - { - operation: "observe_javascript_runtime", - cause: new AggregateError( - failures, - "Inspector provider cleanup owners remain unresolved.", - ), - }, - ); - cleanup.bindCleanupDependencies(failures); - throw cleanup; - } finally { - inspectorClosing = false; } -}; + + async #closeResources(): Promise { + this.#closing = true; + try { + while (this.#activeOperations > 0) + await new Promise((resolve) => this.#idleWaiters.add(resolve)); + const failures: AnalysisError[] = []; + try { + await this.connections.retryPending(); + } catch (cause: unknown) { + failures.push(providerError(cause, "observe_javascript_runtime")); + } + try { + await this.files.retryCleanup(); + } catch (cause: unknown) { + failures.push(providerError(cause, "observe_javascript_runtime")); + } + if (failures.length === 0) return; + if (failures.length === 1) throw failures[0]!; + const cleanup = new ProviderCleanupError( + V8_INSPECTOR_PROVIDER_IDENTITY.id, + failures.flatMap(({ cleanupResources }) => cleanupResources), + { reason: "Inspector provider cleanup remains incomplete." }, + { + operation: "observe_javascript_runtime", + cause: new AggregateError( + failures, + "Inspector provider cleanup owners remain unresolved.", + ), + }, + ); + cleanup.bindCleanupDependencies(failures); + throw cleanup; + } finally { + this.#closing = false; + } + } +} /** Maximum decoded CDP message accepted while observing one Inspector target. */ export const INSPECTOR_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024; @@ -169,12 +186,19 @@ export interface CaptureState { /** Attach-only provider; sends only Runtime.enable and Debugger.enable. */ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { readonly #asarHeaderAdmission: AsarHeaderAdmission; + readonly #admission: V8InspectorAdmission; - /** The admission comes from the composition root's environment; default otherwise. */ + /** + * Header admission comes from the composition root's environment. + * Pass one {@link V8InspectorAdmission} when several providers must share + * transport cleanup. Omitted means this provider owns a fresh admission. + */ constructor( asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION, + admission: V8InspectorAdmission = new V8InspectorAdmission(), ) { this.#asarHeaderAdmission = asarHeaderAdmission; + this.#admission = admission; } identity(): ProviderIdentity { @@ -182,14 +206,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { } async close(): Promise { - if (inspectorClosePromise !== undefined) return await inspectorClosePromise; - const attempt = closeInspectorResources(); - inspectorClosePromise = attempt; - try { - await attempt; - } finally { - if (inspectorClosePromise === attempt) inspectorClosePromise = undefined; - } + await this.#admission.close(); } async listTargets( @@ -197,9 +214,8 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { options: ExecutionOptions = {}, ): Promise> { try { - return await withInspectorAdmission( - "list_javascript_runtime_targets", - () => this.#listTargets(input, options), + return await this.#admission.run("list_javascript_runtime_targets", () => + this.#listTargets(input, options), ); } catch (cause: unknown) { return err(providerError(cause, "list_javascript_runtime_targets")); @@ -220,7 +236,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { options.signal, "list_javascript_runtime_targets", ); - await retryElectronFileCleanup(); + await this.#admission.files.retryCleanup(); discovery = await discoverV8Inspector( input.inspector_endpoint, "list_javascript_runtime_targets", @@ -237,6 +253,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { options.signal, "list_javascript_runtime_targets", this.#asarHeaderAdmission, + this.#admission.files, ); if (!decision.allowed) { excluded[inspectorExclusionKey(decision.reason)] += 1; @@ -327,7 +344,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { options: ExecutionOptions = {}, ): Promise> { try { - return await withInspectorAdmission("observe_javascript_runtime", () => + return await this.#admission.run("observe_javascript_runtime", () => this.#observe(input, options), ); } catch (cause: unknown) { @@ -352,13 +369,14 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { const closeConnection = (): Promise => { if (connection === undefined) return Promise.resolve(); const ownedConnection = connection; - connectionClosePromise ??= inspectorConnections.finish(ownedConnection); + connectionClosePromise ??= + this.#admission.connections.finish(ownedConnection); return connectionClosePromise; }; try { throwIfRuntimeObservationCancelled(options.signal); - await inspectorConnections.retryPending(); - await retryElectronFileCleanup(); + await this.#admission.connections.retryPending(); + await this.#admission.files.retryCleanup(); discovery = await discoverV8Inspector( input.inspector_endpoint, "observe_javascript_runtime", @@ -369,11 +387,12 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { input, this.#asarHeaderAdmission, options.signal, + this.#admission.files, ); if (input.runtime_kind !== undefined) assertRuntimeKind(target, input.runtime_kind); throwIfRuntimeObservationCancelled(options.signal); - connection = await inspectorConnections.connect( + connection = await this.#admission.connections.connect( target.webSocketUrl, "observe_javascript_runtime", options, @@ -404,6 +423,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { signal, "observe_javascript_runtime", this.#asarHeaderAdmission, + this.#admission.files, ), ...(options.signal === undefined ? {} : { signal: options.signal }), }); @@ -510,6 +530,7 @@ const authorizedTarget = async ( input: ObserveJavaScriptRuntimeInput, asarHeaderAdmission: AsarHeaderAdmission, signal?: AbortSignal, + files?: ElectronFileScope, ): Promise => { throwIfRuntimeObservationCancelled(signal); const target = selectCdpDiscoveryTarget( @@ -529,6 +550,7 @@ const authorizedTarget = async ( signal, "observe_javascript_runtime", asarHeaderAdmission, + files, ); throwIfRuntimeObservationCancelled(signal); if (target.attached === true) diff --git a/src/javascript/analysis/JavaScriptAnalysisWorker.ts b/src/javascript/analysis/JavaScriptAnalysisWorker.ts index f8436d297b..056a23332b 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorker.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorker.ts @@ -47,6 +47,8 @@ import { import { OwnedWorkerRuntime, OwnedWorkerRuntimeCleanupError, + WorkerCleanupRegistry, + processWorkerCleanup, } from "../../process/OwnedWorkerRuntime.js"; import { prepareProcessOwnershipInspection } from "../../process/ProcessOwnershipObservation.js"; import { @@ -168,14 +170,19 @@ export class JavaScriptAnalysisWorker { } }; + readonly #workerCleanup: WorkerCleanupRegistry; + constructor( readonly limits: JavaScriptSourceAnalysisLimits = DEFAULTS, readonly launcher: typeof spawnOwnedProviderProcess = spawnOwnedProviderProcess, sourceCount?: number, + workerCleanup: WorkerCleanupRegistry = processWorkerCleanup, ) { + this.#workerCleanup = workerCleanup; this.#runtime = new OwnedWorkerRuntime({ rootPrefix: "rea-javascript-analysis-", launcher, + cleanupRegistry: workerCleanup, supervisorOptions: { captureStdout: false, maxDiagnosticBytes: 256 * 1024, @@ -301,7 +308,7 @@ export class JavaScriptAnalysisWorker { ); // Host ownership preparation is outside the parser execution deadline. await prepareProcessOwnershipInspection(signal); - await OwnedWorkerRuntime.retryPending(); + await this.#workerCleanup.retry(); deadline = new ProviderStartupDeadline(this.limits.timeoutMs, signal); this.#deadline = deadline; await this.#start(deadline); @@ -355,11 +362,15 @@ export class JavaScriptAnalysisWorker { ); for (;;) { const response = await this.#next(deadline, file.path); - if (response.kind === "ready" || response.id !== id) + // A ready frame is outside this job. A frame for another job is not + // this request's result; keep waiting so a later caller abort is + // still cancellation. + if (response.kind === "ready") throw new AnalysisOutputError( OPERATION, "JavaScript worker response names a different source job", ); + if (response.id !== id) continue; if (response.kind === "phase") { this.#phase = response.phase; await onPhase?.(response.phase); diff --git a/src/kuna/KunaDocumentError.ts b/src/kuna/KunaDocumentError.ts new file mode 100644 index 0000000000..d2086fffb7 --- /dev/null +++ b/src/kuna/KunaDocumentError.ts @@ -0,0 +1,50 @@ +const KUNA_DOCUMENT_CODES = [ + "invalid_address", + "address_exceeds_u64", + "buffered_capacity", + "row_field", + "not_kuna_number", + "not_json_object", + "member_not_container", + "row_not_object", + "row_not_array", + "row_must_be_null", + "field_must_be_empty", + "repeated_field", + "value_without_member", + "rows_not_array", + "not_object", + "not_scalar", + "unexpected_token", + "encoding_contradicts_text", + "length_contradicts_text", + "address_hex_disagrees", + "repeated_identity", + "repeated_function", + "repeated_edge", + "repeated_callee_order", + "xref_identity", + "xref_count", + "xref_alias_hex", + "xref_endpoints", + "xref_function_hex", + "xref_target", + "span_short", + "command_not_file", + "stop_unproven", +] as const; + +export type KunaDocumentCode = (typeof KUNA_DOCUMENT_CODES)[number]; + +/** Kuna document or command admission failure. `code` is the stable identity. */ +export class KunaDocumentError extends Error { + readonly _tag = "KunaDocumentError"; + + constructor( + readonly code: KunaDocumentCode, + readonly detail?: Readonly>, + ) { + super(code); + this.name = "KunaDocumentError"; + } +} diff --git a/src/kuna/KunaJson.ts b/src/kuna/KunaJson.ts index c5f9c17f1f..c9e62316c3 100644 --- a/src/kuna/KunaJson.ts +++ b/src/kuna/KunaJson.ts @@ -1,3 +1,5 @@ +import { KunaDocumentError } from "./KunaDocumentError.js"; + /** Read a numeric u64 address from its exact JSON token, before rounding. */ export const kunaAddressFromJsonNumber = ( source: string | undefined, @@ -8,10 +10,10 @@ export const kunaAddressFromJsonNumber = ( source.length > 20 || !/^(?:0|[1-9][0-9]*)$/u.test(source) ) - throw new Error(`invalid numeric address in ${key}`); + throw new KunaDocumentError("invalid_address", { field: key }); const exact = BigInt(source); if (exact > 0xffffffffffffffffn) - throw new Error(`address in ${key} exceeds u64`); + throw new KunaDocumentError("address_exceeds_u64", { field: key }); return `0x${exact.toString(16)}`; }; @@ -22,7 +24,7 @@ export const parseKunaJson = ( addressKeys: ReadonlySet, ): unknown => { if (Buffer.byteLength(text) > limit) - throw new Error("Kuna JSON exceeds its buffered capacity"); + throw new KunaDocumentError("buffered_capacity"); return JSON.parse( text, ( @@ -32,7 +34,7 @@ export const parseKunaJson = ( ): unknown => { if (!addressKeys.has(key) || value === null) return value; if (typeof value !== "number") - throw new Error(`invalid numeric address in ${key}`); + throw new KunaDocumentError("invalid_address", { field: key }); return kunaAddressFromJsonNumber(context?.source, key); }, ); diff --git a/src/kuna/KunaJsonStream.ts b/src/kuna/KunaJsonStream.ts index e88d8f0a65..10085311be 100644 --- a/src/kuna/KunaJsonStream.ts +++ b/src/kuna/KunaJsonStream.ts @@ -13,6 +13,7 @@ import { AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; import { err, ok, type Result } from "../domain/result.js"; +import { KunaDocumentError } from "./KunaDocumentError.js"; import { kunaAddressFromJsonNumber } from "./KunaJson.js"; /** @@ -101,9 +102,10 @@ export const parseKunaRow = ( ): z.infer => { const parsed = schema.safeParse(fields); if (parsed.success) return parsed.data; - throw new Error( - `${member} row field ${parsed.error.issues[0]?.path.join(".") ?? ""} is not Kuna's`, - ); + throw new KunaDocumentError("row_field", { + member, + field: parsed.error.issues[0]?.path.join(".") ?? "", + }); }; /** V8 copies shorter substrings instead of slicing them. */ @@ -249,7 +251,13 @@ export abstract class KunaJsonDocumentReader extends Writable { this.#failure ??= cause instanceof AnalysisResourceConstraintError ? cause - : this.unusable(describe(cause)); + : cause instanceof KunaDocumentError + ? new AnalysisOutputError(this.producer, cause.code, { + cause, + diagnostics: { code: cause.code, ...cause.detail }, + userMessage: `${this.label} cannot be used`, + }) + : this.unusable(describe(cause)); this.#text = ""; this.#inKey = false; this.#header = {}; @@ -316,7 +324,7 @@ export abstract class KunaJsonDocumentReader extends Writable { this.#close(); return; default: - throw new Error(`unexpected JSON ${token.name}`); + throw new KunaDocumentError("unexpected_token", { token: token.name }); } } @@ -337,7 +345,9 @@ export abstract class KunaJsonDocumentReader extends Writable { if (token.name === "stringChunk") this.#charge(chunk.length * 2, this.#text.length); else if (this.#text.length > MAX_TOKEN_CODE_UNITS) - throw new Error(`${this.#target} is not a number Kuna writes`); + throw new KunaDocumentError("not_kuna_number", { + field: this.#target, + }); } return true; } @@ -348,31 +358,36 @@ export abstract class KunaJsonDocumentReader extends Writable { const key = this.#key!; this.#key = undefined; this.#targetIsAddress = false; - if (frame === undefined) - throw new Error(`${this.label} is not one JSON object`); + if (frame === undefined) throw new KunaDocumentError("not_json_object"); switch (frame.kind) { case "root": if (this.document.rows.has(key) || this.document.sections.has(key)) - throw new Error(`${this.label} member ${key} is not a container`); + throw new KunaDocumentError("member_not_container", { field: key }); this.#target = this.#kept(frame.values, key, this.document.header); return; case "section": this.#target = this.#kept(frame.values, key, frame.fields); return; case "rows": - throw new Error(`${frame.name} row is not an object`); + throw new KunaDocumentError("row_not_object", { member: frame.name }); case "row": if (frame.spec.empty.has(key)) - throw new Error(`${frame.member} row ${key} is not an array`); + throw new KunaDocumentError("row_not_array", { + member: frame.member, + field: key, + }); if (frame.spec.nulls.has(key)) { if (kind !== "null") - throw new Error(`${frame.member} row ${key} must be null`); + throw new KunaDocumentError("row_must_be_null", { + member: frame.member, + field: key, + }); this.#target = this.#kept(frame.values, key, frame.spec.nulls); return; } if (frame.spec.addresses.has(key)) { if (kind !== "number" && kind !== "null") - throw new Error(`invalid numeric address in ${key}`); + throw new KunaDocumentError("invalid_address", { field: key }); this.#target = this.#kept(frame.values, key, frame.spec.addresses); this.#targetIsAddress = kind === "number"; return; @@ -380,7 +395,9 @@ export abstract class KunaJsonDocumentReader extends Writable { this.#target = this.#kept(frame.values, key, frame.spec.fields); return; case "empty": - throw new Error(`${frame.field} must be empty`); + throw new KunaDocumentError("field_must_be_empty", { + field: frame.field, + }); } } @@ -397,7 +414,7 @@ export abstract class KunaJsonDocumentReader extends Writable { #claim(values: Record, key: string): void { if (Object.hasOwn(values, key)) - throw new Error(`${this.label} repeats ${key}`); + throw new KunaDocumentError("repeated_field", { field: key }); } #store( @@ -417,7 +434,7 @@ export abstract class KunaJsonDocumentReader extends Writable { #values(): Record { const frame = this.#frames.at(-1); if (frame === undefined || frame.kind === "rows" || frame.kind === "empty") - throw new Error(`${this.label} value has no member`); + throw new KunaDocumentError("value_without_member"); return frame.values; } @@ -431,10 +448,11 @@ export abstract class KunaJsonDocumentReader extends Writable { const section = this.document.sections.get(key); if (rows !== undefined) { if (kind !== "array" || this.#rowsSeen.has(key)) - throw new Error(`${this.label} rows ${key} are not one array`); + throw new KunaDocumentError("rows_not_array", { field: key }); this.#frames.push({ kind: "rows", name: key, spec: rows }); } else if (section !== undefined) { - if (kind !== "object") throw new Error(`${key} is not an object`); + if (kind !== "object") + throw new KunaDocumentError("not_object", { field: key }); this.#claim(root, key); const values: Record = {}; root[key] = values; @@ -445,7 +463,7 @@ export abstract class KunaJsonDocumentReader extends Writable { values, }); } else if (this.document.header.has(key)) - throw new Error(`${this.label} ${key} is not a scalar`); + throw new KunaDocumentError("not_scalar", { field: key }); else this.#skipping = 1; } @@ -455,7 +473,7 @@ export abstract class KunaJsonDocumentReader extends Writable { this.#key = undefined; if (frame === undefined) { if (kind !== "object" || this.#closed) - throw new Error(`${this.label} is not one JSON object`); + throw new KunaDocumentError("not_json_object"); this.#frames.push({ kind: "root", values: this.#header }); return; } @@ -465,7 +483,7 @@ export abstract class KunaJsonDocumentReader extends Writable { return; case "rows": if (kind !== "object") - throw new Error(`${frame.name} row is not an object`); + throw new KunaDocumentError("row_not_object", { member: frame.name }); this.#frames.push({ kind: "row", member: frame.name, @@ -476,7 +494,10 @@ export abstract class KunaJsonDocumentReader extends Writable { case "row": if (frame.spec.empty.has(key!)) { if (kind !== "array") - throw new Error(`${frame.member} row ${key!} is not an array`); + throw new KunaDocumentError("row_not_array", { + member: frame.member, + field: key!, + }); this.#claim(frame.values, key!); this.#frames.push({ kind: "empty", field: key! }); } else if ( @@ -484,16 +505,22 @@ export abstract class KunaJsonDocumentReader extends Writable { frame.spec.addresses.has(key!) || frame.spec.nulls.has(key!) ) - throw new Error(`${frame.member} row ${key!} is not a scalar`); + throw new KunaDocumentError("not_scalar", { + field: `${frame.member}.${key!}`, + }); else this.#skipping = 1; return; case "section": if (frame.fields.has(key!)) - throw new Error(`${frame.name} ${key!} is not a scalar`); + throw new KunaDocumentError("not_scalar", { + field: `${frame.name}.${key!}`, + }); this.#skipping = 1; return; case "empty": - throw new Error(`${frame.field} must be empty`); + throw new KunaDocumentError("field_must_be_empty", { + field: frame.field, + }); } } diff --git a/src/kuna/KunaProjectReader.ts b/src/kuna/KunaProjectReader.ts index ad8f9b07b2..fb86a7ef9e 100644 --- a/src/kuna/KunaProjectReader.ts +++ b/src/kuna/KunaProjectReader.ts @@ -4,6 +4,7 @@ import { sameRegularFileState, RegularFileOperationCancelled, } from "../filesystem/RegularFile.js"; +import { KunaDocumentError } from "./KunaDocumentError.js"; import { assertKunaFileState } from "./KunaProjectState.js"; import { createReadStream } from "node:fs"; import { open } from "node:fs/promises"; @@ -126,7 +127,7 @@ const readSpan = async ( }, ); if (buffer.length !== length) - throw new Error(`${path} is shorter than its recorded span`); + throw new KunaDocumentError("span_short", { path }); if (!sameRegularFileState(expected, await owner.handle.stat())) throw new RegularFileChangedError(path); await assertKunaFileState(path, expected); diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index f9f137f5b1..c1ade5afcd 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -1,3 +1,4 @@ +import { KunaDocumentError } from "./KunaDocumentError.js"; import { KUNA_STRING_MIN_LENGTH } from "./KunaLimits.js"; import { accessSync, constants, statSync } from "node:fs"; @@ -141,7 +142,8 @@ export class KunaProvider implements AnalysisProviderCandidate { const { command } = configuration.value; try { accessSync(command, constants.X_OK); - if (!statSync(command).isFile()) throw new Error("not a regular file"); + if (!statSync(command).isFile()) + throw new KunaDocumentError("command_not_file"); } catch (cause: unknown) { return { status: "unavailable", diff --git a/src/kuna/KunaReferenceFiles.ts b/src/kuna/KunaReferenceFiles.ts index c63688bc55..832d8eda38 100644 --- a/src/kuna/KunaReferenceFiles.ts +++ b/src/kuna/KunaReferenceFiles.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { err, ok, type Result } from "../domain/result.js"; import { compareKunaAddresses } from "./KunaAddress.js"; +import { KunaDocumentError } from "./KunaDocumentError.js"; import { parseKunaJson } from "./KunaJson.js"; import { KunaJsonDocumentReader, parseKunaRow } from "./KunaJsonStream.js"; @@ -149,7 +150,7 @@ export class KunaCallGraphReader extends KunaJsonDocumentReader { } const node = parseKunaRow(graphNode, "graph function", fields); if (this.#nodes.has(node.address)) - throw new Error("graph repeats a function address"); + throw new KunaDocumentError("repeated_function"); this.#nodes.set(node.address, node); this.#hasIndirectCalls ||= node.hasIndirectCalls; } @@ -174,12 +175,12 @@ export class KunaCallGraphReader extends KunaJsonDocumentReader { const { callerAddress: caller, calleeAddress: callee } = edge; this.#edgeCount += 1; const targets = this.#targets.get(caller) ?? new Set(); - if (targets.has(callee)) throw new Error("graph repeats an edge"); + if (targets.has(callee)) throw new KunaDocumentError("repeated_edge"); targets.add(callee); this.#targets.set(caller, targets); const order = this.#orders.get(caller) ?? new Set(); if (order.has(edge.calleeOrder)) - throw new Error("graph repeats a callee order"); + throw new KunaDocumentError("repeated_callee_order"); order.add(edge.calleeOrder); this.#orders.set(caller, order); if (edge.kind !== "call") return; @@ -244,12 +245,18 @@ export interface KunaIncomingReferences { readonly excludedAliasRows: number; } +/** Code from incoming-reference admission, plus the address field when one failed. */ +export interface KunaIncomingReferenceError { + readonly code: string; + readonly field?: string; +} + /** Native --to unifies forwarding aliases; REA xrefs asks for the exact address. */ export const parseKunaIncomingReferences = ( text: string, input: string, target: string, -): Result => { +): Result => { try { const document = xrefsDocument.parse( parseKunaJson(text, KUNA_XREF_OUTPUT_BYTES, addressKeys), @@ -259,13 +266,13 @@ export const parseKunaIncomingReferences = ( document.target.address !== target || document.target.address_hex !== target ) - throw new Error("xref document names a different input or target"); + throw new KunaDocumentError("xref_identity"); if (document.count !== document.xrefs.length) - throw new Error("xref count disagrees with examined rows"); + throw new KunaDocumentError("xref_count"); const aliases = new Set([target]); for (const alias of document.target.aliases) { if (alias.address !== alias.address_hex) - throw new Error("xref alias address disagrees with its hex field"); + throw new KunaDocumentError("xref_alias_hex"); aliases.add(alias.address); } const addresses = new Set(); @@ -278,16 +285,12 @@ export const parseKunaIncomingReferences = ( row.from_address !== row.from_address_hex || row.to_address !== row.to_address_hex ) - throw new Error( - "xref endpoints disagree with numeric or directional fields", - ); + throw new KunaDocumentError("xref_endpoints"); for (const fn of [row.from_function, row.to_function]) if (fn !== null && fn.address !== fn.address_hex) - throw new Error("xref function address disagrees with its hex field"); + throw new KunaDocumentError("xref_function_hex"); if (!aliases.has(row.to_address)) - throw new Error( - "xref row targets neither the requested address nor an advertised alias", - ); + throw new KunaDocumentError("xref_target"); if (row.to_address === target) addresses.add(row.from_address); else excludedAliasRows += 1; } @@ -299,6 +302,16 @@ export const parseKunaIncomingReferences = ( sha256: createHash("sha256").update(text).digest("hex"), }); } catch (cause: unknown) { - return err(cause instanceof Error ? cause.message : String(cause)); + if (cause instanceof KunaDocumentError) { + const field = cause.detail?.field; + return err( + typeof field === "string" + ? { code: cause.code, field } + : { code: cause.code }, + ); + } + return err({ + code: cause instanceof Error ? cause.message : String(cause), + }); } }; diff --git a/src/kuna/KunaReferenceQueries.ts b/src/kuna/KunaReferenceQueries.ts index 9576da3777..6b1713abed 100644 --- a/src/kuna/KunaReferenceQueries.ts +++ b/src/kuna/KunaReferenceQueries.ts @@ -306,15 +306,22 @@ export class KunaReferenceQueries { } #parsed( - parsed: Result, + parsed: Result< + T, + { readonly code: string; readonly field?: string | undefined } + >, label: string, ): Result { - return parsed.ok - ? parsed - : err( - new ProviderAdapterError(KUNA_PROVIDER_ID, "kuna:references", { - userMessage: `Kuna's ${label} cannot be used: ${parsed.error}`, - }), - ); + if (parsed.ok) return parsed; + const { code, field } = parsed.error; + return err( + new ProviderAdapterError(KUNA_PROVIDER_ID, "kuna:references", { + diagnostics: { + code, + ...(field === undefined ? {} : { field }), + }, + userMessage: `Kuna's ${label} cannot be used: ${code}`, + }), + ); } } diff --git a/src/kuna/KunaStringInventory.ts b/src/kuna/KunaStringInventory.ts index 378f53d144..3f0ae85314 100644 --- a/src/kuna/KunaStringInventory.ts +++ b/src/kuna/KunaStringInventory.ts @@ -4,6 +4,7 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { analysisStringSchema } from "../domain/native/nativeString.js"; import { err, ok, type Result } from "../domain/result.js"; import { compareKunaAddresses } from "./KunaAddress.js"; +import { KunaDocumentError } from "./KunaDocumentError.js"; import { KunaJsonDocumentReader, parseKunaRow } from "./KunaJsonStream.js"; import { KUNA_STRING_MIN_LENGTH } from "./KunaLimits.js"; @@ -66,7 +67,7 @@ const validateStringFacts = (entry: z.infer): void => { (entry.encoding === "ascii" && point > 0x7f) || (entry.encoding === "utf16" && point > 0xff) ) - throw new Error("string text contradicts its reported encoding"); + throw new KunaDocumentError("encoding_contradicts_text"); characters += 1; } const textBytes = @@ -85,9 +86,7 @@ const validateStringFacts = (entry: z.infer): void => { entry.length !== characters || entry.byte_length !== textBytes + terminatorBytes ) - throw new Error( - "string length or termination contradicts its encoded text", - ); + throw new KunaDocumentError("length_contradicts_text"); }; export type KunaStringItem = z.infer; @@ -165,10 +164,10 @@ export class KunaStringInventoryReader extends KunaJsonDocumentReader { const stopped = await outcome.owner?.stop(); if (stopped === undefined || stopped.status === "incomplete") - throw new Error("Kuna process stop remains unproven"); + throw new KunaDocumentError("stop_unproven"); releaseRun?.(); }, outcome.processResources); new AnalysisCleanupLeaseGroup(outcome.error, [ diff --git a/src/native/AppleDispatchMetadata.failure.test.ts b/src/native/AppleDispatchMetadata.failure.test.ts index 8c9a36141a..f60cdd100a 100644 --- a/src/native/AppleDispatchMetadata.failure.test.ts +++ b/src/native/AppleDispatchMetadata.failure.test.ts @@ -1,6 +1,28 @@ import { expect, it } from "vitest"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { appleDispatchMetadataFailure } from "./AppleDispatchMetadata.js"; +import { BinaryFormatFailure } from "./metadata/BinaryContainer.js"; + +it.each(["malformed", "unsupported"] as const)( + "keeps an explicit %s metadata refusal target-specific", + (kind) => { + const cause = new BinaryFormatFailure(kind, "metadata format refused"); + expect(appleDispatchMetadataFailure(cause, "/selected")).toMatchObject({ + _tag: "AnalysisUnsupportedTargetError", + path: "/selected", + reason: "metadata format refused", + }); + }, +); + +it.each([new RangeError("unexpected range"), new TypeError("unexpected type")])( + "keeps an unclassified parser exception as a provider failure (%s)", + (cause) => { + expect(appleDispatchMetadataFailure(cause, "/selected")._tag).toBe( + "ProviderAdapterError", + ); + }, +); // The metadata target is already bound, so losing it is a change to that artifact rather than a new input error. it.each(["integrity", "path"] as const)( diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index 0c871d586b..f064327444 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -18,6 +18,7 @@ import { AnalysisAccessDeniedError, AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, + AnalysisUnsupportedTargetError, } from "../domain/analysisErrorCore.js"; import { ArtifactFileCleanupFailure, @@ -41,6 +42,7 @@ import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import { pushDispatchCoverage } from "./AppleDispatchCoverage.js"; import { parsePointerFixups } from "./AppleMachoFixups.js"; +import { BinaryFormatFailure } from "./metadata/BinaryContainer.js"; import { parseMachoLayout, UnsupportedMachoMetadataFormatError, @@ -230,7 +232,10 @@ export const decodeAppleDispatchMetadata = ( ({ name }) => name === "__swift5_types", )) { if (section.size % 4 !== 0) - throw new RangeError("Misaligned Swift type section"); + throw new BinaryFormatFailure( + "malformed", + "Misaligned Swift type section", + ); for (let index = 0; index < section.size / 4 && index < maxRecords; index++) typeEntries.push(section.address + BigInt(index * 4)); if (section.size / 4 > maxRecords) budget.truncated = true; @@ -445,6 +450,19 @@ const appleDispatchMetadataPrimaryFailure = ( ): AnalysisError => { if (origin === "owned-or-classified") { if (cause instanceof AnalysisError) return cause; + if (cause instanceof BinaryFormatFailure) + return new AnalysisUnsupportedTargetError( + OPERATION, + path, + cause.message, + { + cause, + remediationAction: + cause.kind === "malformed" + ? "Select a Mach-O artifact with well-formed metadata sections, reopen the target, then retry." + : "Select a supported little-endian 64-bit Mach-O artifact, reopen the target, then retry.", + }, + ); if (cause instanceof UnsupportedMachoMetadataFormatError) return new AnalysisCapabilityUnavailableError( IDENTITY.id, diff --git a/src/native/AppleMachoSelection.ts b/src/native/AppleMachoSelection.ts index 2cba27ad30..08f3169cbf 100644 --- a/src/native/AppleMachoSelection.ts +++ b/src/native/AppleMachoSelection.ts @@ -183,7 +183,10 @@ export const parseMachoLayout = ( } if (table.wide && !table.littleEndian) { if (cpuType !== 0x01000012) - throw new TypeError("Unsupported big-endian Mach-O CPU type"); + throw new BinaryFormatFailure( + "unsupported", + "Unsupported big-endian Mach-O CPU type", + ); throw new UnsupportedMachoMetadataFormatError( "Unsupported big-endian 64-bit PowerPC Mach-O metadata", ); diff --git a/src/native/AppleObjcCategories.ts b/src/native/AppleObjcCategories.ts index fc36c311d2..ae4b5dd329 100644 --- a/src/native/AppleObjcCategories.ts +++ b/src/native/AppleObjcCategories.ts @@ -3,6 +3,7 @@ import type { Section } from "./AppleMachoSelection.js"; import type { ObjcDispatchReaders } from "./AppleObjcDispatchFacets.js"; import { readObjcPropertiesOf } from "./AppleObjcProperties.js"; import { issue, type DecodeIssue } from "./AppleDispatchDecodeFacts.js"; +import { BinaryFormatFailure } from "./metadata/BinaryContainer.js"; const CLASS_SYMBOL = /^_OBJC_(?:META)?CLASS_\$_(.+)$/u; @@ -59,7 +60,10 @@ export const decodeObjcCategories = (input: { ({ name }) => name === "__objc_catlist", )) { if (section.size % 8 !== 0) - throw new RangeError("Misaligned Objective-C category list"); + throw new BinaryFormatFailure( + "malformed", + "Misaligned Objective-C category list", + ); for (let index = 0; index < section.size / 8; index++) { if (!input.admit()) break; examined++; diff --git a/src/native/AppleObjcDispatchFacets.ts b/src/native/AppleObjcDispatchFacets.ts index 047642bbd9..fd46e5eab8 100644 --- a/src/native/AppleObjcDispatchFacets.ts +++ b/src/native/AppleObjcDispatchFacets.ts @@ -6,6 +6,7 @@ import { import type { Segment, Section } from "./AppleMachoSelection.js"; import { boundClassName, decodeObjcCategories } from "./AppleObjcCategories.js"; import { readObjcPropertiesOf } from "./AppleObjcProperties.js"; +import { BinaryFormatFailure } from "./metadata/BinaryContainer.js"; import type { DecodeIssue } from "./AppleDispatchDecodeFacts.js"; import { createObjcProtocolReader } from "./AppleObjcProtocols.js"; import type { @@ -250,7 +251,10 @@ export const decodeObjcDispatchFacets = (input: { ({ name }) => name === "__objc_protolist", )) { if (section.size % 8 !== 0) - throw new RangeError("Misaligned Objective-C protocol list"); + throw new BinaryFormatFailure( + "malformed", + "Misaligned Objective-C protocol list", + ); for (let index = 0; index < section.size / 8; index++) { if (budget.isFull()) { budget.markTruncated(); @@ -406,7 +410,10 @@ export const decodeObjcDispatchFacets = (input: { ({ name }) => name === "__objc_classlist", )) { if (section.size % 8 !== 0) - throw new RangeError("Misaligned Objective-C class-list size"); + throw new BinaryFormatFailure( + "malformed", + "Misaligned Objective-C class-list size", + ); for (let index = 0; index < section.size / 8; index++) { if (budget.isFull()) { budget.markTruncated(); diff --git a/src/native/AppleSwiftDispatchFacets.ts b/src/native/AppleSwiftDispatchFacets.ts index 8d223a5b97..c014d49ba1 100644 --- a/src/native/AppleSwiftDispatchFacets.ts +++ b/src/native/AppleSwiftDispatchFacets.ts @@ -5,6 +5,7 @@ import { import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js"; import type { PointerResolution } from "./AppleMachoFixups.js"; import type { Segment, Section } from "./AppleMachoSelection.js"; +import { BinaryFormatFailure } from "./metadata/BinaryContainer.js"; import { issue, type FacetDecodeFacts } from "./AppleDispatchDecodeFacts.js"; import { decodeSwiftConformanceTypeReference, @@ -325,7 +326,10 @@ export const decodeSwiftDispatchFacets = (input: { ({ name }) => name === "__swift5_proto", )) { if (section.size % 4 !== 0) - throw new RangeError("Misaligned Swift conformance section"); + throw new BinaryFormatFailure( + "malformed", + "Misaligned Swift conformance section", + ); for (let index = 0; index < section.size / 4; index++) { if (!budget.admit()) break; swiftExamined++; diff --git a/src/native/MachoArchitectureSelection.ts b/src/native/MachoArchitectureSelection.ts index 40f7616708..4c1e035e3f 100644 --- a/src/native/MachoArchitectureSelection.ts +++ b/src/native/MachoArchitectureSelection.ts @@ -46,6 +46,35 @@ export const machoSubtypeRank = ( return rank < 0 ? policy.subtypes.length : rank; }; +/** Return a tool selector only for an explicitly supported CPU subtype. */ +export const machoKnownCodeSignSelector = ( + cpuType: number, + cpuSubtype: number, +): string | undefined => { + const capabilities = (cpuSubtype >>> 24) & 0xff; + // arm64e pointer authentication and x86_64 LIB64 have known tool selectors. + // Other capability variants may have distinct names; do not collapse them. + if ( + capabilities !== 0 && + !( + cpuType === 0x0100000c && + machoCpuSubtypeBase(cpuSubtype) === 2 && + capabilities === 0x80 + ) && + !( + cpuType === 0x01000007 && + machoCpuSubtypeBase(cpuSubtype) === 3 && + capabilities === 0x80 + ) + ) + return undefined; + return Object.values(MACHO_ARCHITECTURE_POLICIES) + .find((policy) => policy.cpuType === cpuType) + ?.subtypes.find( + ({ subtype }) => subtype === machoCpuSubtypeBase(cpuSubtype), + )?.name; +}; + /** Keep the selected lipo record, including its container coordinates. */ export const selectMachoArchitecture = ( policy: MachoArchitecturePolicy, diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index e0feed4b4c..17533d8955 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -19,6 +19,7 @@ import { ArtifactFileCleanupFailure, type ArtifactFilePrimaryFacts, } from "../artifacts/ArtifactFileOperation.js"; +import { readMachoEncryption } from "../artifacts/apple/MachoEncryptionReader.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, @@ -36,7 +37,10 @@ import { import { NativeUiCleanupError } from "./NativeUiCleanupError.js"; import { LldbCallTracer, type NativeCallTracer } from "./LldbCallTracer.js"; import { observeNativeCalls } from "./NativeCallObservation.js"; -import { realpath } from "node:fs/promises"; +import { realpath, symlink } from "node:fs/promises"; +import { basename, join } from "node:path"; +import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; +import { AnalysisCleanupLeaseGroup } from "../artifacts/AnalysisCleanupLease.js"; import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js"; import { z } from "zod"; @@ -95,9 +99,11 @@ import { import { createPlistNumberProjection } from "../domain/apple/plistNumbers.js"; import type { NativeUiObservationResult } from "../domain/native/nativeUiObservation.js"; import { parseXmlPropertyList } from "../domain/propertyListKeys.js"; +import { machoKnownCodeSignSelector } from "./MachoArchitectureSelection.js"; import { demangleSwiftSchema, inspectPlistSchema, + inspectMachoSchema, inspectSignatureSchema, type InspectSignature, listArchitecturesSchema, @@ -151,7 +157,6 @@ import { assertSelectedMachoSlice, resolveSelectedMachoSlice, } from "./NativeMachoSelectedSlice.js"; -import type { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import { createNativeUiHelperRuntime, type NativeUiHelperRuntime, @@ -230,6 +235,12 @@ export class NativeMacOSProvider implements AnalysisProvider { } } +// Encryption range mapping in every slice shares a sixteenth of the output +// budget at the 256 projected bytes reserved per Mach-O command record. +const MACHO_ENCRYPTION_MAPPING_RECORDS = Math.floor( + NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 16 / 256, +); + class NativeMacOSClient implements AnalysisClient { readonly #pendingNativeUiRuntimes = new Set(); readonly #pendingCommandOperations = new Set>(); @@ -237,6 +248,7 @@ class NativeMacOSClient implements AnalysisClient { readonly #plistResources = new ArtifactResourceScope(); readonly #callResources = new ArtifactResourceScope(); readonly #dispatchResources = new ArtifactResourceScope(); + readonly #machoOperandResources = new ArtifactResourceScope(); readonly #machoResources = new ArtifactResourceScope(); #nativeUiTail: Promise = Promise.resolve(); #closed = false; @@ -422,6 +434,10 @@ class NativeMacOSClient implements AnalysisClient { "native-macho-target", ); const commandCleanup = await this.#retryNativeCommandCleanup(); + const machoOperandCleanup = await closeNativeFileScope( + this.#machoOperandResources, + "macho-operand", + ); let result = await this.tracer.close(); const cleanupErrors = [ ...(uiCleanupFailures.length === 0 @@ -432,6 +448,7 @@ class NativeMacOSClient implements AnalysisClient { ...(plistCleanup === undefined ? [] : [plistCleanup]), ...(callCleanup === undefined ? [] : [callCleanup]), ...(dispatchCleanup === undefined ? [] : [dispatchCleanup]), + ...(machoOperandCleanup === undefined ? [] : [machoOperandCleanup]), ...(machoCleanup === undefined ? [] : [machoCleanup]), ]; for (const cleanupError of cleanupErrors) @@ -774,14 +791,52 @@ class NativeMacOSClient implements AnalysisClient { "No executable architecture is selected.", ), ); - const selectedArchitecture = architecture === "x86" ? "i386" : architecture; + const target = this.target; + const selectedSlice = + target.format === "mach-o-universal" + ? target.universalSlices?.find( + (slice) => slice.offset === target.universalSlice.offset, + ) + : undefined; + if (target.format === "mach-o-universal" && selectedSlice === undefined) + return err( + new AnalysisCapabilityUnavailableError( + IDENTITY.id, + "inspect_signature", + "The selected Mach-O slice identity is unavailable for codesign.", + ), + ); + const selectedArchitecture = + selectedSlice === undefined + ? architecture === "x86" + ? "i386" + : architecture + : machoKnownCodeSignSelector( + selectedSlice.cpuType, + selectedSlice.cpuSubtype, + ); + if (selectedArchitecture === undefined) + return err( + new AnalysisCapabilityUnavailableError( + IDENTITY.id, + "inspect_signature", + "The selected Mach-O slice has no known codesign architecture selector.", + ), + ); + // A thin Mach-O already identifies its only slice. Its admitted architecture + // is a CPU family, which is not necessarily a codesign subtype selector + // (for example arm64e). Only universal containers need a selector. + const selector = + this.target.format === "mach-o-universal" + ? ["-a", selectedArchitecture] + : []; const executablePaths = [ binding.snapshotPath, ...(await canonicalPath(binding.snapshotPath)), ]; const display = await this.#captureSignature( binding, - ["-d", "-a", selectedArchitecture, "--verbose=4"], + ["-d", ...selector, "--verbose=4"], signal, timeoutMs, ); @@ -799,7 +854,7 @@ class NativeMacOSClient implements AnalysisClient { if (displayFailure !== null) return err(displayFailure); const requirements = await this.#captureSignature( binding, - ["-d", "-a", selectedArchitecture, "-r-"], + ["-d", ...selector, "-r-"], signal, timeoutMs, ); @@ -812,7 +867,7 @@ class NativeMacOSClient implements AnalysisClient { if (requirementsFailure !== null) return err(requirementsFailure); const entitlements = await this.#captureSignature( binding, - ["-d", "-a", selectedArchitecture, "--entitlements", ":-"], + ["-d", ...selector, "--entitlements", ":-"], signal, timeoutMs, ); @@ -840,11 +895,18 @@ class NativeMacOSClient implements AnalysisClient { ...entitlementValue.limitations.map((note) => `Entitlements: ${note}`), "Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined.", ]; - const architectures = codeSignArchitectures(parsed.format, this.target); - if (architectures.length > 1) + const { architectures, unknownSlices } = codeSignArchitectures( + parsed.format, + this.target, + ); + if (architectures.length > 1 || unknownSlices) limitations.push( `Partial signature coverage: signature identity, hashes, designated requirement, entitlements and runtime state describe the requested ${selectedArchitecture} architecture; other slices' metadata is not returned.`, ); + if (unknownSlices) + limitations.push( + "Some Mach-O slice identities cannot be mapped to a known codesign selector; their signature states are unknown.", + ); if ( !unsigned && isNonzeroUnsignedObservation( @@ -880,7 +942,7 @@ class NativeMacOSClient implements AnalysisClient { if (architectures.length > 1) { const slices = await this.#inspectSignatureSliceStates( binding, - parsed.format, + architectures, executablePaths, { captures, limitations }, signal, @@ -901,7 +963,7 @@ class NativeMacOSClient implements AnalysisClient { async #inspectSignatureSliceStates( binding: SignatureTargetBinding, - format: string | null, + architectures: readonly string[], executablePaths: readonly string[], observation: { captures: NativeCommandCapture[]; limitations: string[] }, signal: AbortSignal | undefined, @@ -910,7 +972,7 @@ class NativeMacOSClient implements AnalysisClient { const { captures, limitations } = observation; const unsigned: string[] = []; let unsignedNote: number | undefined; - for (const architecture of codeSignArchitectures(format, this.target)) { + for (const architecture of architectures) { const slice = await this.#captureSignature( binding, ["-d", "-a", architecture, "--verbose=4"], @@ -1267,61 +1329,218 @@ class NativeMacOSClient implements AnalysisClient { "Active artifact is not Mach-O.", ), ); - try { - return await this.#machoResources.run(() => - withNativeMachoSource( - this.target, - this.#machoResources, - signal, - (source) => - inspectNativeMacho({ - target: this.target, - ...(signal === undefined ? {} : { signal }), - run: (tool, arguments_, commandSignal, acceptNonZeroIf) => - this.#run("inspect_macho", tool, arguments_, { + const needsPrivateOtoolOperand = /\([^/]*\)$/u.test( + basename(this.target.path), + ); + const inspect = (privateOperand?: string) => + withNativeMachoSource( + this.target, + this.#machoResources, + signal, + (source) => + inspectNativeMacho({ + target: this.target, + ...(signal === undefined ? {} : { signal }), + run: (tool, arguments_, commandSignal, acceptNonZeroIf) => + this.#run( + "inspect_macho", + tool, + privateOperand !== undefined + ? arguments_.map((argument) => + argument === this.target.path ? privateOperand : argument, + ) + : arguments_, + { signal: commandSignal, timeoutMs, ...(acceptNonZeroIf === undefined ? {} : { acceptNonZeroIf }), - }), - invocation: (capture) => invocation(capture, this.target.path), - resolveSlice: (selected) => - resolveSelectedMachoSlice( - source, - selected, - Math.floor(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 256), - ), - readCommandNames: async (offset, size, selected) => { - const length = size ?? source.bytes - offset; - // Binary strings can expand to hex and escaped JSON, and each - // record becomes several objects. Reserve room for those forms. - const maximum = Math.floor( - NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 12, + }, + ), + invocation: (capture) => invocation(capture, this.target.path), + // Encryption facts come from the same identity-bound descriptor. + readEncryption: () => + readMachoEncryption( + (at, length) => + source.readSlice( + 0, + source.bytes, + Math.min(at, source.bytes), + Math.max(0, Math.min(length, source.bytes - at)), + ), + source.bytes, + MACHO_ENCRYPTION_MAPPING_RECORDS, + ), + resolveSlice: (selected) => + resolveSelectedMachoSlice( + source, + selected, + Math.floor(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 256), + ), + readCommandNames: async (offset, size, selected) => { + const length = size ?? source.bytes - offset; + // Binary strings can expand to hex and escaped JSON, and each + // record becomes several objects. Reserve room for those forms. + const maximum = Math.floor( + NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 12, + ); + const maximumRecords = Math.floor( + NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 256, + ); + const binary = await readMachoCommandNames( + (at, bytes) => source.readSlice(offset, length, at, bytes), + length, + maximum, + maximumRecords, + NATIVE_COMMAND_OUTPUT_BUDGET_BYTES, + ); + if (size !== null) { + if (selected === undefined) + throw new TypeError("Selected Mach-O slice is unavailable"); + assertSelectedMachoSlice(source, selected, binary); + } + return binary; + }, + }), + ); + try { + return await this.#machoResources.run(() => + needsPrivateOtoolOperand + ? this.#inspectMachoWithOperand(signal, async (operand) => { + try { + return await inspect(operand); + } catch (cause: unknown) { + return err( + nativeMachoSourceFailure(cause, this.target, parameters), ); - const maximumRecords = Math.floor( - NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 256, - ); - const binary = await readMachoCommandNames( - (at, bytes) => source.readSlice(offset, length, at, bytes), - length, - maximum, - maximumRecords, - NATIVE_COMMAND_OUTPUT_BUDGET_BYTES, - ); - if (size !== null) { - if (selected === undefined) - throw new TypeError("Selected Mach-O slice is unavailable"); - assertSelectedMachoSlice(source, selected, binary); - } - return binary; - }, - }), - ), + } + }) + : inspect(), ); } catch (cause: unknown) { return err(nativeMachoSourceFailure(cause, this.target, parameters)); } } + async #inspectMachoWithOperand( + signal: AbortSignal | undefined, + inspect: ( + operand: string, + ) => Promise>, + ): Promise> { + try { + return await this.#machoOperandResources.run(async () => { + if (signal?.aborted) + return err( + new AnalysisCancelledError("inspect_macho", { + cause: signal.reason, + }), + ); + let root: Pick; + try { + root = await (this.privateRuntimeRootFactory?.() ?? + PrivateRuntimeRoot.create({ prefix: "rea-macho-operand-" })); + } catch (cause: unknown) { + return err( + new ProviderAdapterError(IDENTITY.id, "inspect_macho", { + cause, + diagnostics: { + reason: "Unable to create a private otool operand", + path: this.target.path, + }, + }), + ); + } + const owner = { + kind: "lease" as const, + resource: root.path, + lease: { + release: async () => { + const commandFailure = await this.#retryNativeCommandCleanup(); + if (commandFailure !== undefined) throw commandFailure; + await root.close(); + }, + }, + }; + let result: Result; + try { + const operand = join(root.path, "artifact.macho"); + await symlink(this.target.path, operand); + result = signal?.aborted + ? err( + new AnalysisCancelledError("inspect_macho", { + cause: signal.reason, + }), + ) + : await inspect(operand); + } catch (cause: unknown) { + result = err( + new ProviderAdapterError(IDENTITY.id, "inspect_macho", { + cause, + diagnostics: { + reason: "Unable to prepare the private otool operand", + path: this.target.path, + }, + }), + ); + } + const released = await this.#machoOperandResources.release(owner); + if (released.kind === "failed") { + const cleanup = new ProviderCleanupError( + IDENTITY.id, + [root.path], + { + reason: "The private otool operand could not be released", + }, + { operation: "inspect_macho", cause: released.cause }, + ); + if (result.ok) + cleanup.retainPartialObservation( + inspectMachoSchema.parse(result.value.result), + ); + new AnalysisCleanupLeaseGroup(cleanup, [released.lease]); + const primary = result.ok + ? signal?.aborted + ? new AnalysisCancelledError("inspect_macho", { + cause: signal.reason, + partialObservation: inspectMachoSchema.parse( + result.value.result, + ), + }) + : undefined + : result.error; + return err( + primary === undefined + ? cleanup + : analysisErrorWithCleanupFailure( + primary, + cleanup, + "inspect_macho", + ).bindCleanupDependencies([primary, cleanup]), + ); + } + if (signal?.aborted && result.ok) + return err( + new AnalysisCancelledError("inspect_macho", { + cause: signal.reason, + partialObservation: inspectMachoSchema.parse(result.value.result), + }), + ); + return result; + }); + } catch (cause: unknown) { + const failure = new ProviderCleanupError( + IDENTITY.id, + this.#machoOperandResources.cleanupResources, + { + reason: "A previous private otool operand remains owned", + }, + { operation: "inspect_macho", cause }, + ); + bindArtifactFileCleanup(cause, failure); + return err(failure); + } + } + async #run( operation: NativeToolName, tool: string, @@ -1706,7 +1925,22 @@ const codeSignCaptureFailure = ( const codeSignArchitectures = ( format: string | null, target: BinaryTarget, -): string[] => { +): { architectures: string[]; unknownSlices: boolean } => { + // An architecture-selected codesign display describes the selected slice, + // not the admitted container. The target owns the container's slice list. + if (target.kind === "executable" && target.format === "mach-o-universal") { + if (target.universalSlices === undefined) + return { architectures: [], unknownSlices: true }; + const names = target.universalSlices.map(({ cpuType, cpuSubtype }) => + machoKnownCodeSignSelector(cpuType, cpuSubtype), + ); + const known = names.filter((name): name is string => name !== undefined); + return { + architectures: [...new Set(known)], + unknownSlices: + known.length !== names.length || new Set(known).size !== names.length, + }; + } const universal = /^Mach-O universal \(([^)\r\n]+)\)$/u.exec( format ?? "", )?.[1]; @@ -1714,13 +1948,22 @@ const codeSignArchitectures = ( const architectures = universal .split(/\s+/u) .filter((architecture) => /^[A-Za-z][A-Za-z0-9_.]*$/u.test(architecture)); - if (architectures.length > 0) return [...new Set(architectures)]; + if (architectures.length > 0) + return { + architectures: [...new Set(architectures)], + unknownSlices: false, + }; } - if (format?.startsWith("Mach-O thin ") === true) return []; - if (target.kind !== "executable" || !isMachOFormat(target.format)) return []; - return target.availableArchitectures.map((architecture) => - architecture === "x86" ? "i386" : architecture, - ); + if (format?.startsWith("Mach-O thin ") === true) + return { architectures: [], unknownSlices: false }; + if (target.kind !== "executable" || !isMachOFormat(target.format)) + return { architectures: [], unknownSlices: false }; + return { + architectures: target.availableArchitectures.map((architecture) => + architecture === "x86" ? "i386" : architecture, + ), + unknownSlices: false, + }; }; const invocation = ( diff --git a/src/native/NativeMachoCommandDiagnostics.ts b/src/native/NativeMachoCommandDiagnostics.ts index b002d7e508..0a688060f3 100644 --- a/src/native/NativeMachoCommandDiagnostics.ts +++ b/src/native/NativeMachoCommandDiagnostics.ts @@ -3,12 +3,18 @@ import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; import type { NativeCommandCapture } from "./CommandRunner.js"; import { withoutEchoedPathHeader } from "./parsers/echoedPath.js"; +/** Path the command received. Diagnostics echo this operand, not the selected subject. */ +export const nativeCommandOperand = ( + capture: NativeCommandCapture, +): string | undefined => capture.arguments.at(-1); + /** Recognize vtool's sole settled container-format refusal without hiding other failures. */ export const isVtoolContainerRefusal = ( capture: NativeCommandCapture, - operandPath: string, ): boolean => { + const operandPath = nativeCommandOperand(capture); if ( + operandPath === undefined || capture.tool !== "vtool" || capture.exitCode !== 1 || capture.signal !== null || diff --git a/src/native/NativeMachoInspection.fixture.ts b/src/native/NativeMachoInspection.fixture.ts index d54d5ece48..dac0521bf8 100644 --- a/src/native/NativeMachoInspection.fixture.ts +++ b/src/native/NativeMachoInspection.fixture.ts @@ -51,3 +51,10 @@ export const inspectionReader = (image = inspectionImage()) => () => readMachoCommandNames(readerOf(image), image.length, 1024 * 1024, 10000); + +/** Encryption facts for inspections whose assertions do not involve them. */ +export const unreadEncryption = () => + Promise.resolve({ + status: "unread", + reason: "not read in this test", + } as const); diff --git a/src/native/NativeMachoInspection.test.ts b/src/native/NativeMachoInspection.test.ts index 58aceb7bcc..7810cf42ab 100644 --- a/src/native/NativeMachoInspection.test.ts +++ b/src/native/NativeMachoInspection.test.ts @@ -16,11 +16,17 @@ import { import { err, ok } from "../domain/result.js"; import { parseMachoLayout, selectMachoSlice } from "./AppleMachoSelection.js"; import type { NativeCommandCapture } from "./CommandRunner.js"; -import { inspectNativeMacho } from "./NativeMachoInspection.js"; +import type { MachoSliceEncryption } from "../domain/apple/machoEncryption.js"; +import { inspectMachoSchema } from "../domain/native/nativeInspection.js"; +import { + inspectNativeMacho, + type MachoEncryptionRead, +} from "./NativeMachoInspection.js"; import { readMachoCommandNames } from "./MachoCommandNames.js"; import { inspectionImage, inspectionReader, + unreadEncryption, } from "./NativeMachoInspection.fixture.js"; import { NativeFixtureRunner, @@ -65,6 +71,7 @@ const inspect = async (lipo: string, architecture: "x86" | "arm64") => { invocation: () => { throw new Error("invocation is not reached"); }, + readEncryption: unreadEncryption, }); return { result, commands }; }; @@ -123,6 +130,7 @@ describe("Mach-O tool architecture selection", () => { const result = await inspectNativeMacho({ target: nativeMachoTarget("/fixture/app"), run: (tool, args) => runner.run(tool, args), + readEncryption: unreadEncryption, readCommandNames: (offset, size) => { readOffsets.push(offset); const image = fat.subarray( @@ -229,6 +237,7 @@ describe("Mach-O inspection container format", () => { stdout_bytes: capture.stdoutBytes, stderr_bytes: capture.stderrBytes, }), + readEncryption: unreadEncryption, }); }; @@ -287,6 +296,7 @@ describe("Mach-O inspection container format", () => { ), target: nativeMachoTarget("/fixture/app"), run: (tool, args) => runner.run(tool, args), + readEncryption: unreadEncryption, invocation: (captured) => ({ tool: captured.tool, command: [captured.executable, ...captured.arguments], @@ -335,6 +345,7 @@ describe("Mach-O UUID reconciliation", () => { ), target: nativeMachoTarget(targetPath), run: (tool, args) => runner.run(tool, args), + readEncryption: unreadEncryption, invocation: (capture) => ({ tool: capture.tool, command: [capture.executable, ...capture.arguments], @@ -452,6 +463,7 @@ describe("Mach-O slice command scheduling", () => { readCommandNames: inspectionReader(), target: nativeMachoTarget("/fixture/app"), run, + readEncryption: unreadEncryption, invocation: (capture) => ({ tool: capture.tool, command: [capture.executable, ...capture.arguments], @@ -485,6 +497,139 @@ describe("Mach-O slice command scheduling", () => { }); }); +describe("Mach-O encryption reporting", () => { + const slice = ( + overrides: Partial = {}, + ): MachoSliceEncryption => ({ + architecture: null, + cpu_type: 16777228, + cpu_subtype: 0, + container_file_offset: 32768, + size: 8192, + status: "absent", + reason: null, + commands: [], + range: null, + ...overrides, + }); + const encrypted = (start: number, end: number) => + slice({ + status: "encrypted", + commands: [ + { + kind: "LC_ENCRYPTION_INFO_64", + index: 1, + cmdsize: 24, + cryptoff: start, + cryptsize: end - start, + cryptid: 1, + pad: 0, + }, + ], + range: { + content: "ciphertext", + slice_start: start, + slice_end: end, + container_start: 32768 + start, + container_end: 32768 + end, + mapping: { status: "mapped", fragments: [], sections: [] }, + }, + }); + const inspectEncryption = async (encryption: MachoEncryptionRead) => { + const runner = new NativeFixtureRunner(); + const result = await inspectNativeMacho({ + readCommandNames: inspectionReader(), + target: nativeMachoTarget("/fixture/app"), + run: (tool, args) => runner.run(tool, args), + readEncryption: () => Promise.resolve(encryption), + invocation: (capture) => ({ + tool: capture.tool, + command: [capture.executable, ...capture.arguments], + tool_version: capture.toolVersion, + version_reason: capture.versionReason, + executable_sha256: capture.executableSha256, + exit: { code: capture.exitCode, signal: capture.signal }, + stdout_bytes: capture.stdoutBytes, + stderr_bytes: capture.stderrBytes, + }), + }); + if (!result.ok) throw result.error; + return { + encryption: inspectMachoSchema.parse(result.value.result).encryption, + limitations: result.value.limitations, + }; + }; + + it("labels every slice with the lipo record at its offset and names ciphertext bytes", async () => { + const x86 = slice({ + cpu_type: 16777223, + cpu_subtype: 3, + container_file_offset: 16384, + size: 4096, + }); + const { encryption, limitations } = await inspectEncryption({ + status: "parsed", + slices: [x86, encrypted(4096, 6144)], + }); + expect(encryption).toMatchObject({ total: 2, exhaustive: true }); + expect(encryption.items.map(({ architecture }) => architecture)).toEqual([ + "x86_64", + "arm64", + ]); + expect(limitations).toContain( + "Bytes 36864-38912 (input file offsets, end exclusive) of the arm64 slice are ciphertext (cryptid 1); disassembly, decompilation, strings and references derived from them do not describe program code.", + ); + }); + + it("leaves a slice lipo did not describe unlabeled", async () => { + const { encryption, limitations } = await inspectEncryption({ + status: "parsed", + slices: [ + slice({ + container_file_offset: 49152, + status: "malformed", + reason: "load commands is truncated", + }), + ], + }); + expect(encryption.items[0]?.architecture).toBeNull(); + expect(limitations).toContain( + "Encryption status of the slice at offset 49152 is malformed: load commands is truncated.", + ); + }); + + it("does not mark bytes as ciphertext for an empty encrypted range", async () => { + const { limitations } = await inspectEncryption({ + status: "parsed", + slices: [encrypted(4096, 4096)], + }); + expect(limitations).toContain( + "The arm64 slice declares encryption (cryptid 1) over an empty range; no bytes are marked as ciphertext.", + ); + expect(limitations.some((line) => line.includes("are ciphertext"))).toBe( + false, + ); + }); + + it("keeps encryption unknown when the bytes were not read", async () => { + const { encryption, limitations } = await inspectEncryption({ + status: "unread", + reason: "the target could not be opened for reading (ENOENT)", + }); + expect(encryption).toEqual({ + items: [], + total: null, + exhaustive: false, + limitations: [ + "Encryption load commands were not read: the target could not be opened for reading (ENOENT)", + ], + }); + expect(limitations).toContain( + "Encryption load commands were not read: the target could not be opened for reading (ENOENT)", + ); + }); +}); + it.each(["10", "DSYM", "2"])( "classifies the unbacked-text dyld refusal using observed file type %s", async (fileType) => { @@ -510,6 +655,7 @@ it.each(["10", "DSYM", "2"])( new AnalysisOutputError("inspect_macho", "unaccepted native failure"), ); }, + readEncryption: unreadEncryption, invocation: () => { throw new Error( "Refused target does not produce inspection provenance", @@ -596,6 +742,7 @@ describe("dSYM diagnostic admission", () => { const result = await inspectNativeMacho({ readCommandNames: inspectionReader(inspectionImage({ fileType: 10 })), target: nativeMachoTarget("/fixture/debug-companion"), + readEncryption: unreadEncryption, run: async (tool, args, _signal, acceptNonZeroIf) => { const captured = await runner.run(tool, args); if (!captured.ok || tool !== "dyld_info") return captured; diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index e57a2ea526..9f0b29e7ad 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -33,11 +33,17 @@ import type { MachoCommandNames } from "./MachoCommandNames.js"; import type { ResolvedMachoSlice } from "./NativeMachoSelectedSlice.js"; import { reconcileMachoUuid } from "./parsers/machoUuid.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; +import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; +import type { + MachoEncryptionFacts, + MachoSliceEncryption, +} from "../domain/apple/machoEncryption.js"; import { conflictingUuidCommandEvidence, isMissingUuidDyldFailure, isUnbackedTextDyldFailure, isVtoolContainerRefusal, + nativeCommandOperand, nativeMachoCapturedOutput, missingUuidLimitation, parseDyldFileTypeRefusal, @@ -50,9 +56,18 @@ interface NativeMachoObservation { readonly locations: readonly EvidenceLocation[]; } +/** Structural encryption facts, or why the target bytes were not read. */ +export type MachoEncryptionRead = + | MachoEncryptionFacts + | { readonly status: "unread"; readonly reason: string }; + interface NativeMachoInspectionContext { readonly target: BinaryTarget; readonly signal?: AbortSignal; + /** Read load-command encryption facts from the bytes the tools inspect. */ + readonly readEncryption: ( + signal?: AbortSignal, + ) => Promise; readonly run: ( tool: string, arguments_: readonly string[], @@ -92,6 +107,9 @@ const REQUIRED_SLICE_COMMANDS = [ const OPTIONAL_VTOOL_COMMAND = ["vtool", ["-show-build"]] as const; const VTOOL_UNAVAILABLE_LIMITATION = "vtool is unavailable; build metadata is normalized from binary load commands."; +/** Recorded when vtool refuses the universal container and slice facts remain. */ +export const VTOOL_CONTAINER_REFUSAL_LIMITATION = + "vtool declined the universal container as not Mach-O; build metadata is normalized from binary load commands."; /** Inspect one Mach-O with the required native commands and normalized output. */ export const inspectNativeMacho = async ( context: NativeMachoInspectionContext, @@ -111,7 +129,7 @@ export const inspectNativeMacho = async ( const parsedLipo = parseLipoCapture( lipo, "inspect_macho", - context.target.path, + nativeCommandOperand(lipo) ?? context.target.path, ); if (!parsedLipo.ok) return parsedLipo; const { format, architectures } = parsedLipo.value; @@ -150,7 +168,7 @@ export const inspectNativeMacho = async ( isUnbackedTextDyldFailure(capture) || parseDyldFileTypeRefusal(capture) !== null : tool === "vtool" && format === "fat" - ? (capture) => isVtoolContainerRefusal(capture, context.target.path) + ? (capture) => isVtoolContainerRefusal(capture) : undefined, ); // vtool knows no variant subtypes, so no spelling selects an arm64e.x1 slice. @@ -186,10 +204,8 @@ export const inspectNativeMacho = async ( ); else if (optional.ok) { captures.push(optional.value); - if (isVtoolContainerRefusal(optional.value, context.target.path)) - limitations.push( - "vtool declined the universal container as not Mach-O; build metadata is normalized from binary load commands.", - ); + if (isVtoolContainerRefusal(optional.value)) + limitations.push(VTOOL_CONTAINER_REFUSAL_LIMITATION); } else if (optional.error._tag === "AnalysisCapabilityUnavailableError") limitations.push(VTOOL_UNAVAILABLE_LIMITATION); else return err(optional.error); @@ -317,6 +333,7 @@ export const inspectNativeMacho = async ( new Set(parseNmSymbolNames(importNames.value.stdout)), ); } + const encryption = await context.readEncryption(context.signal); const selectedArchitectureName = (selection.status === "selected" ? selection.architecture.name : null) ?? (architectures.length === 1 ? (architectures[0]?.name ?? null) : null); @@ -331,8 +348,7 @@ export const inspectNativeMacho = async ( binary.sliceSize, parsedImports.items, captures[nmIndex]!, - architectures, - context.target.path, + { format, architectures, encryption }, { name: selectedArchitectureName, container_file_offset: containerOffset ?? null, @@ -413,13 +429,18 @@ const normalizeMacho = ( sliceSize: number, parsedImports: ReturnType["items"], exportNames: NativeCommandCapture, - architectures: readonly LipoArchitecture[], - artifactPath: string, + slices: { + readonly format: "thin" | "fat"; + readonly architectures: readonly LipoArchitecture[]; + readonly encryption: MachoEncryptionRead; + }, selectedArchitecture: { readonly name: string | null; readonly container_file_offset: number | null; }, ) => { + const { architectures } = slices; + const encryption = encryptionCoverage(slices); const byTool = captureLookup(captures); const dyldImportsCapture = byTool("dyld_info", 0); const dyldExportsCapture = byTool("dyld_info", 1); @@ -449,7 +470,7 @@ const normalizeMacho = ( const uuid = reconcileMachoUuid( load.commands, dwarfdump.stdout, - artifactPath, + nativeCommandOperand(dwarfdump) ?? "", selectedArchitecture.name === null ? null : { @@ -481,6 +502,8 @@ const normalizeMacho = ( "Export virtual addresses are unavailable because no file-backed image base was observed.", ] : []), + ...encryption.limitations, + ...encryption.items.flatMap(encryptionLimitation), ]; return inspectMachoSchema.parse({ format: containerFormat === "fat" ? "mach-o-universal" : "mach-o", @@ -540,11 +563,84 @@ const normalizeMacho = ( load.segmentLimitations.length === 0, load.segmentLimitations, ), + encryption, provenance, limitations, }); }; +/** Label each slice with the one lipo record sharing its physical identity. */ +const encryptionCoverage = ({ + format, + architectures, + encryption, +}: { + readonly format: "thin" | "fat"; + readonly architectures: readonly LipoArchitecture[]; + readonly encryption: MachoEncryptionRead; +}) => { + if (encryption.status !== "parsed") + return { + items: [], + total: null, + exhaustive: false, + limitations: [ + encryption.status === "not-mach-o" + ? "Encryption load commands were not read: the structural reader did not recognize a Mach-O image." + : `Encryption load commands were not read: ${encryption.reason}`, + ], + }; + const items = encryption.slices.map((slice) => { + const matches = architectures.filter((architecture) => + lipoDescribesSlice(architecture, slice, format), + ); + return { + ...slice, + architecture: matches.length === 1 ? (matches[0]?.name ?? null) : null, + }; + }); + return covered(items, true); +}; + +const lipoDescribesSlice = ( + architecture: LipoArchitecture, + slice: MachoSliceEncryption, + format: "thin" | "fat", +): boolean => + (architecture.file_offset === null + ? format === "thin" && slice.container_file_offset === 0 + : architecture.file_offset === slice.container_file_offset) && + (architecture.size === null || architecture.size === slice.size) && + (architecture.cpu_type_code === null || + slice.cpu_type === null || + architecture.cpu_type_code === slice.cpu_type) && + (slice.cpu_subtype === null || + (architecture.cpu_subtype_code === null + ? architecture.cpu_subtype_base === null || + architecture.cpu_subtype_base === machoCpuSubtypeBase(slice.cpu_subtype) + : architecture.cpu_subtype_code === slice.cpu_subtype)); + +const encryptionLimitation = (slice: MachoSliceEncryption): string[] => { + const name = + slice.architecture === null + ? `slice at offset ${slice.container_file_offset}` + : `${slice.architecture} slice`; + if (slice.status === "malformed" || slice.status === "unknown") + return [ + `Encryption status of the ${name} is ${slice.status}: ${slice.reason ?? "no reason recorded"}.`, + ]; + const { range } = slice; + if (slice.status !== "encrypted" || range === null) return []; + const cryptid = slice.commands[0]?.cryptid ?? null; + return range.container_start === range.container_end + ? [ + `The ${name} declares encryption (cryptid ${cryptid}) over an empty range; no bytes are marked as ciphertext.`, + ] + : [ + `Bytes ${range.container_start}-${range.container_end} (input file offsets, end exclusive) of the ${name} are ciphertext (cryptid ${cryptid}); disassembly, decompilation, strings and references derived from them do not describe program code.`, + ]; +}; + /** Slice output without the echoed operand path, which can contain any text. */ const sliceOutput = (capture: NativeCommandCapture): string => withoutEchoedPathHeader(capture.stdout, capture.arguments.at(-1)); diff --git a/src/native/NativeMachoOperand.test.ts b/src/native/NativeMachoOperand.test.ts new file mode 100644 index 0000000000..8ff35a26ba --- /dev/null +++ b/src/native/NativeMachoOperand.test.ts @@ -0,0 +1,335 @@ +import { mkdtemp, readlink, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + NativeFixtureRunner, + nativeMachoTargetForFile, +} from "../../tests/fixtures/nativeCommands.js"; +import { NativeMacOSProvider } from "./NativeMacOSProvider.js"; +import type { NativeCallTracer } from "./LldbCallTracer.js"; +import { NativeCommandFailure } from "./CommandRunner.js"; +import { inspectMachoSchema } from "../domain/native/nativeInspection.js"; +import { err, ok } from "../domain/result.js"; +import { VTOOL_CONTAINER_REFUSAL_LIMITATION } from "./NativeMachoInspection.js"; + +const targetDirectories: string[] = []; +afterEach(async () => { + await Promise.all( + targetDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); +/** A FAT container whose arm64 row matches the canned lipo fixture. */ +const parenthesizedFatTarget = () => { + const bytes = Buffer.alloc(40_960); + bytes.writeUInt32BE(0xcafebabe, 0); + bytes.writeUInt32BE(2, 4); + const slices = [ + { cpu: 0x01000007, subtype: 3, offset: 16_384, size: 4_096, align: 14 }, + { cpu: 0x0100000c, subtype: 0, offset: 32_768, size: 8_192, align: 14 }, + ]; + for (const [index, slice] of slices.entries()) { + const at = 8 + index * 20; + bytes.writeUInt32BE(slice.cpu, at); + bytes.writeUInt32BE(slice.subtype, at + 4); + bytes.writeUInt32BE(slice.offset, at + 8); + bytes.writeUInt32BE(slice.size, at + 12); + bytes.writeUInt32BE(slice.align, at + 16); + } + const header = bytes.subarray(32_768, 32_800); + header.writeUInt32LE(0xfeedfacf, 0); + header.writeUInt32LE(0x0100000c, 4); + header.writeUInt32LE(0, 8); + header.writeUInt32LE(2, 12); + return bytes; +}; +const parenthesizedTarget = async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-macho-target-")); + targetDirectories.push(directory); + const path = join(directory, "app(foo)"); + await writeFile(path, parenthesizedFatTarget()); + return { directory, target: await nativeMachoTargetForFile(path) }; +}; + +const tracer: NativeCallTracer = { + trace: async () => { + throw new Error("unexpected trace"); + }, + close: async () => ({ ok: true, value: null }), +}; + +describe("private otool operand lifecycle", () => { + it("uses a private alias only for otool and removes it after all commands settle", async () => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + const selected = await parenthesizedTarget(); + const target = selected.target.path; + const calls: { tool: string; operand: string | undefined }[] = []; + const fixture = new NativeFixtureRunner(); + const runner = { + async run(tool: string, args: readonly string[]) { + calls.push({ tool, operand: args.at(-1) }); + if (args.at(-1) === join(path, "artifact.macho")) + expect(await readlink(args.at(-1)!)).toBe(target); + return fixture.run(tool, args); + }, + }; + const client = new NativeMacOSProvider( + {}, + runner, + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient(selected.target); + try { + const result = await client.execute("inspect_macho", {}); + if (!result.ok) throw result.error; + expect(JSON.stringify(result.value.result)).toContain( + join(path, "artifact.macho"), + ); + const alias = join(path, "artifact.macho"); + expect(calls.some(({ operand }) => operand === alias)).toBe(true); + expect(calls.some(({ operand }) => operand === target)).toBe(false); + await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + await client.close(); + await rm(path, { recursive: true, force: true }); + await rm(selected.directory, { recursive: true, force: true }); + } + }); + + it("keeps completed Mach-O facts when vtool refuses the private alias", async () => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + const selected = await parenthesizedTarget(); + const target = selected.target.path; + const alias = join(path, "artifact.macho"); + const fixture = new NativeFixtureRunner(); + const runner = { + async run(tool: string, args: readonly string[]) { + const captured = await fixture.run(tool, args); + if (!captured.ok || tool !== "vtool") return captured; + const operand = args.at(-1); + const stderr = `/usr/bin/vtool error: ${operand} file is not mach-o\n`; + return ok({ + ...captured.value, + stdout: "", + stdoutBytes: 0, + stderr, + stderrBytes: Buffer.byteLength(stderr), + exitCode: 1, + signal: null, + }); + }, + }; + const client = new NativeMacOSProvider( + {}, + runner, + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient(selected.target); + try { + const result = await client.execute("inspect_macho", {}); + if (!result.ok) throw result.error; + const report = inspectMachoSchema.parse(result.value.result); + expect(report).toMatchObject({ + format: "mach-o-universal", + selected_architecture: { + name: "arm64", + container_file_offset: 32_768, + }, + }); + const vtool = report.provenance.find((entry) => entry.tool === "vtool"); + expect(vtool).toMatchObject({ + exit: { code: 1, signal: null }, + }); + expect(vtool?.command.at(-1)).toBe(alias); + expect(report.limitations).toContain(VTOOL_CONTAINER_REFUSAL_LIMITATION); + expect( + report.provenance.find((entry) => entry.tool === "lipo"), + ).toMatchObject({ exit: { code: 0, signal: null } }); + expect(target.endsWith("app(foo)")).toBe(true); + } finally { + await client.close(); + await rm(path, { recursive: true, force: true }); + await rm(selected.directory, { recursive: true, force: true }); + } + }); + + it("retains the alias until command cleanup is confirmed", async () => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + let unresolved = true; + class Runner extends NativeFixtureRunner { + async retryCleanup() { + if (unresolved) throw new Error("command remains owned"); + return []; + } + } + const client = new NativeMacOSProvider( + {}, + new Runner(), + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient((await parenthesizedTarget()).target); + try { + const result = await client.execute("inspect_macho", {}); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.partialObservation).toMatchObject({ + format: "mach-o-universal", + }); + expect((await stat(path)).isDirectory()).toBe(true); + expect((await client.close()).ok).toBe(false); + expect((await stat(path)).isDirectory()).toBe(true); + unresolved = false; + expect((await client.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + unresolved = false; + await client.close(); + await rm(path, { recursive: true, force: true }); + } + }); +}); + +describe("private otool operand cleanup", () => { + it.each([false, true])( + "preserves exact failed output with cleanup failure %s", + async (cleanupFails) => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + const message = `lipo failed for ${join(path, "artifact.macho")}\n`; + const fixture = new NativeFixtureRunner(); + const runner = { + async run(tool: string, args: readonly string[]) { + if (tool === "lipo") + return err( + new NativeCommandFailure("lipo", "nonzero-exit", 1, undefined, { + stdout: "", + stderr: message, + stdoutBytes: 0, + stderrBytes: Buffer.byteLength(message), + exitCode: 1, + signal: null, + truncated: false, + }), + ); + return fixture.run(tool, args); + }, + }; + let attempts = 0; + const client = new NativeMacOSProvider( + {}, + runner, + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + if (cleanupFails && ++attempts === 1) + throw new Error("fixture cleanup refusal"); + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient((await parenthesizedTarget()).target); + try { + const result = await client.execute("inspect_macho", {}); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected native failure"); + expect(result.error.capturedOutput?.stderr).toBe(message); + expect(result.error.cleanupIncomplete).toBe(cleanupFails); + if (cleanupFails) { + expect(result.error.cleanupResources).toContain(path); + expect((await client.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + } + } finally { + await client.close(); + await rm(path, { recursive: true, force: true }); + } + }, + ); + + it("retains a failed root cleanup and retries it on close", async () => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + let attempts = 0; + const client = new NativeMacOSProvider( + {}, + new NativeFixtureRunner(), + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + if (++attempts < 2) throw new Error("fixture cleanup refusal"); + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient((await parenthesizedTarget()).target); + try { + const result = await client.execute("inspect_macho", {}); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.cleanupResources).toContain(path); + expect((await stat(path)).isDirectory()).toBe(true); + expect((await client.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + await client.close(); + await rm(path, { recursive: true, force: true }); + } + }); + + it("reports cancellation during final root cleanup", async () => { + const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-")); + const controller = new AbortController(); + const client = new NativeMacOSProvider( + {}, + new NativeFixtureRunner(), + "darwin", + () => tracer, + async () => ({ + path, + close: async () => { + controller.abort("cancelled while removing alias"); + await rm(path, { recursive: true, force: true }); + }, + }), + ).createClient((await parenthesizedTarget()).target); + try { + const result = await client.execute( + "inspect_macho", + {}, + { signal: controller.signal }, + ); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected cancellation"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.partialObservation).toMatchObject({ + format: "mach-o-universal", + }); + } finally { + await client.close(); + await rm(path, { recursive: true, force: true }); + } + }); +}); diff --git a/src/native/NativeMachoVtoolRefusal.test.ts b/src/native/NativeMachoVtoolRefusal.test.ts index 0b8b1dbfba..1dda724b52 100644 --- a/src/native/NativeMachoVtoolRefusal.test.ts +++ b/src/native/NativeMachoVtoolRefusal.test.ts @@ -6,8 +6,14 @@ import { NativeFixtureRunner, nativeMachoTarget, } from "../../tests/fixtures/nativeCommands.js"; -import { inspectNativeMacho } from "./NativeMachoInspection.js"; -import { inspectionReader } from "./NativeMachoInspection.fixture.js"; +import { + inspectNativeMacho, + VTOOL_CONTAINER_REFUSAL_LIMITATION, +} from "./NativeMachoInspection.js"; +import { + inspectionReader, + unreadEncryption, +} from "./NativeMachoInspection.fixture.js"; const inspect = async (options: { readonly diagnostic: string; @@ -23,6 +29,7 @@ const inspect = async (options: { ); return inspectNativeMacho({ readCommandNames: inspectionReader(), + readEncryption: unreadEncryption, target: nativeMachoTarget(path), run: async (tool, args, _signal, acceptNonZeroIf) => { const result = await runner.run(tool, args); @@ -59,7 +66,7 @@ it("retains required facts and failed vtool provenance for its universal-contain const result = await inspect({ diagnostic: refusal }); if (!result.ok) throw result.error; expect(result.value.limitations).toContain( - "vtool declined the universal container as not Mach-O; build metadata is normalized from binary load commands.", + VTOOL_CONTAINER_REFUSAL_LIMITATION, ); expect(result.value.result).toMatchObject({ format: "mach-o-universal", diff --git a/src/process/DarwinProcessRunTokenReader.ts b/src/process/DarwinProcessRunTokenReader.ts index fd2a50407f..c54ee09350 100644 --- a/src/process/DarwinProcessRunTokenReader.ts +++ b/src/process/DarwinProcessRunTokenReader.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { rmSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -579,10 +580,10 @@ export const createDarwinProcessRunTokenReader = ( }; const compile = async (signal?: AbortSignal): Promise => { - if (closed) throw new Error("Darwin process token reader is closed"); + if (closed) throw new ProcessAdmissionError("reader_closed"); signal?.throwIfAborted(); while (true) { - if (closed) throw new Error("Darwin process token reader is closed"); + if (closed) throw new ProcessAdmissionError("reader_closed"); signal?.throwIfAborted(); if (executable !== undefined) return executable; if (compilation === undefined && root !== undefined) { @@ -642,7 +643,7 @@ export const createDarwinProcessRunTokenReader = ( signal?: AbortSignal, ): Promise => { if (closed) - return Promise.reject(new Error("Darwin process token reader is closed")); + return Promise.reject(new ProcessAdmissionError("reader_closed")); const pending = (async () => { const binary = await compile(signal); const { stdout } = await execFileOutput(binary, arguments_, { @@ -670,25 +671,21 @@ export const createDarwinProcessRunTokenReader = ( const stdout = await runHelper(pids, signal); const decoded = safeParseJson(stdout); if (!decoded.ok) - throw new Error( - `macOS process token helper returned invalid JSON: ${decoded.error}`, + throw new ProcessAdmissionError( + "token_json_invalid", + { parser: decoded.error }, { cause: decoded.cause }, ); const parsed = responseSchema.safeParse(decoded.value); if (!parsed.success) - throw new Error( - "macOS process token helper returned an invalid response", - { - cause: parsed.error, - }, - ); + throw new ProcessAdmissionError("token_response_invalid", undefined, { + cause: parsed.error, + }); const requested = new Set(processes.map(({ pid }) => pid)); const observations = new Map(); for (const result of parsed.data.results) { if (!requested.has(result.pid) || observations.has(result.pid)) - throw new Error( - "macOS process token helper returned an unexpected PID set", - ); + throw new ProcessAdmissionError("token_pid_set"); observations.set( result.pid, result.state === "readable" @@ -701,9 +698,7 @@ export const createDarwinProcessRunTokenReader = ( ); } if (observations.size !== requested.size) - throw new Error( - "macOS process token helper returned an incomplete PID set", - ); + throw new ProcessAdmissionError("token_pid_incomplete"); return observations; }; @@ -716,25 +711,21 @@ export const createDarwinProcessRunTokenReader = ( const stdout = await runHelper(["--identities", ...pids], signal); const decoded = safeParseJson(stdout); if (!decoded.ok) - throw new Error( - `macOS process identity helper returned invalid JSON: ${decoded.error}`, + throw new ProcessAdmissionError( + "identity_json_invalid", + { parser: decoded.error }, { cause: decoded.cause }, ); const parsed = identityResponseSchema.safeParse(decoded.value); if (!parsed.success) - throw new Error( - "macOS process identity helper returned an invalid response", - { - cause: parsed.error, - }, - ); + throw new ProcessAdmissionError("identity_response_invalid", undefined, { + cause: parsed.error, + }); const requested = new Set(processes.map(({ pid }) => pid)); const observations = new Map(); for (const result of parsed.data.results) { if (!requested.has(result.pid) || observations.has(result.pid)) - throw new Error( - "macOS process identity helper returned an unexpected PID set", - ); + throw new ProcessAdmissionError("identity_pid_set"); observations.set( result.pid, result.state === "readable" @@ -768,9 +759,7 @@ export const createDarwinProcessRunTokenReader = ( ); } if (observations.size !== requested.size) - throw new Error( - "macOS process identity helper returned an incomplete PID set", - ); + throw new ProcessAdmissionError("identity_pid_incomplete"); return observations; }; diff --git a/src/process/LinuxResourceLimits.test.ts b/src/process/LinuxResourceLimits.test.ts index 6cf5f5787d..f80959495a 100644 --- a/src/process/LinuxResourceLimits.test.ts +++ b/src/process/LinuxResourceLimits.test.ts @@ -176,7 +176,9 @@ it.each([ expect(physical.outcome.value).toEqual(configured); else expect(physical.outcome.cause).toMatchObject({ - message: "Cannot establish inherited Linux limit: Max address space", + _tag: "ProcessAdmissionError", + code: "limit_unreadable", + detail: { limit: "address_space_bytes" }, }); expect(physical.owner.handle.fd).toBeGreaterThanOrEqual(0); if (scoped) @@ -267,9 +269,11 @@ it.each([16 * 1024, 16 * 1024 + 1])( const prefix = report("unlimited unlimited"); await kernelReportFixture(prefix.padEnd(bytes, " ")); kernel.maximumRead = Buffer.byteLength(prefix); - await expect(readLinuxResourceLimits(configured)).rejects.toThrow( - "Inherited Linux limits exceed the report budget.", - ); + await expect(readLinuxResourceLimits(configured)).rejects.toMatchObject({ + _tag: "ProcessAdmissionError", + code: "report_exceeds_budget", + detail: { budget_bytes: 16 * 1024 }, + }); expect(kernel.reads.length).toBeGreaterThan(1); expect( kernel.reads.every( diff --git a/src/process/LinuxResourceLimits.ts b/src/process/LinuxResourceLimits.ts index d05a82d0fc..53a8a69d56 100644 --- a/src/process/LinuxResourceLimits.ts +++ b/src/process/LinuxResourceLimits.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; import { open } from "node:fs/promises"; import { readFileHandleRange } from "../filesystem/readFileHandleRange.js"; import { withArtifactFileHandle } from "../artifacts/ArtifactFileOperation.js"; @@ -57,12 +58,12 @@ export const clampLinuxResourceLimits = ( .filter((line) => line.startsWith(`${rows[key]} `)); const values = matches[0]?.slice(rows[key].length).trim().split(/\s+/u); if (matches.length !== 1 || values === undefined || values.length !== 3) - throw new Error(`Cannot establish inherited Linux limit: ${rows[key]}`); + throw new ProcessAdmissionError("limit_unreadable", { limit: key }); let ceiling = BigInt(configured[key]); for (const token of values.slice(0, 2)) { if (token === "unlimited") continue; if (!/^[0-9]+$/u.test(token)) - throw new Error(`Invalid inherited Linux limit: ${rows[key]}`); + throw new ProcessAdmissionError("limit_invalid", { limit: key }); const value = BigInt(token); if (value < ceiling) ceiling = value; } @@ -86,7 +87,9 @@ export const readLinuxResourceLimits = async ( const reportBudget = 16 * 1024; const buffer = await readFileHandleRange(handle, 0, reportBudget); if (buffer.length === reportBudget) - throw new Error("Inherited Linux limits exceed the report budget."); + throw new ProcessAdmissionError("report_exceeds_budget", { + budget_bytes: reportBudget, + }); return clampLinuxResourceLimits(configured, buffer.toString("utf8")); }, ); diff --git a/src/process/OwnedWorkerRuntime.test.ts b/src/process/OwnedWorkerRuntime.test.ts index 6b9f67a223..73e400b366 100644 --- a/src/process/OwnedWorkerRuntime.test.ts +++ b/src/process/OwnedWorkerRuntime.test.ts @@ -331,9 +331,10 @@ it("runs deferred delegated cleanup before registering a pending start acquisiti expect(stopped.status).not.toBe("incomplete"); runtime.retainForCleanup(); - await expect(runtime.start(startOptions())).rejects.toThrow( - "Owned worker runtime is closing", - ); + await expect(runtime.start(startOptions())).rejects.toMatchObject({ + _tag: "ProcessAdmissionError", + code: "runtime_closing", + }); expect(delegateTimedOut).toBe(false); await delegatedClose; expect(runtime.rootPath).toBeUndefined(); diff --git a/src/process/OwnedWorkerRuntime.ts b/src/process/OwnedWorkerRuntime.ts index 75f98e61cf..0b9f66c7ac 100644 --- a/src/process/OwnedWorkerRuntime.ts +++ b/src/process/OwnedWorkerRuntime.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; import { PrivateRuntimeRoot } from "./PrivateRuntimeRoot.js"; import type { Readable, Writable } from "node:stream"; import { @@ -18,8 +19,35 @@ export interface OwnedWorkerRuntimeOptions { readonly onInputFailure?: (cause: Error) => void; /** Let a higher-level owner release its dependent resources before this root. */ readonly retryCleanup?: () => Promise; + /** Host that retries this runtime. Omitted runtimes share the process registry. */ + readonly cleanupRegistry?: WorkerCleanupRegistry; } +/** Retained worker runtimes one host retries before admitting another child. */ +export class WorkerCleanupRegistry { + readonly #pending = new Set(); + + has(runtime: OwnedWorkerRuntime): boolean { + return this.#pending.has(runtime); + } + + add(runtime: OwnedWorkerRuntime): void { + this.#pending.add(runtime); + } + + delete(runtime: OwnedWorkerRuntime): void { + this.#pending.delete(runtime); + } + + async retry(except?: OwnedWorkerRuntime): Promise { + for (const owner of [...this.#pending]) + if (owner !== except) await owner.retryRetainedCleanup(); + } +} + +/** Runtimes that were not given a host registry. */ +export const processWorkerCleanup = new WorkerCleanupRegistry(); + export class OwnedWorkerRuntimeCleanupError extends Error { readonly #cleanupEpoch: OwnedWorkerRuntimeCleanupEpoch | undefined; @@ -92,7 +120,7 @@ class OwnedWorkerRuntimeCleanupEpoch { /** Own the private root and process lifecycle shared by sequential workers. */ export class OwnedWorkerRuntime { - static readonly #pending = new Set(); + readonly #registry: WorkerCleanupRegistry; #root: PrivateRuntimeRoot | undefined; #cleanupEpoch: OwnedWorkerRuntimeCleanupEpoch | undefined; @@ -112,7 +140,9 @@ export class OwnedWorkerRuntime { readonly listener: (chunk: Buffer | string) => void; }>(); - constructor(readonly options: OwnedWorkerRuntimeOptions) {} + constructor(readonly options: OwnedWorkerRuntimeOptions) { + this.#registry = options.cleanupRegistry ?? processWorkerCleanup; + } get rootPath(): string | undefined { return this.#root?.path; @@ -135,7 +165,7 @@ export class OwnedWorkerRuntime { /** Whether this exact owner still retains an unconfirmed resource release. */ get hasPendingCleanup(): boolean { - return OwnedWorkerRuntime.#pending.has(this); + return this.#registry.has(this); } /** First error observed on the current child's request stream, if any. */ @@ -155,13 +185,22 @@ export class OwnedWorkerRuntime { /** Mark this still-owned runtime for retry before any consumer starts a child. */ retainForCleanup(): void { - OwnedWorkerRuntime.#pending.add(this); + this.#registry.add(this); } - /** Retry every earlier worker owner before any consumer admits a new child. */ + /** Retry every retained runtime in this host's registry. */ + retryCohort(): Promise { + return this.#registry.retry(); + } + + /** Called by the owning registry. Not a process-wide sweep. */ + retryRetainedCleanup(): Promise { + return this.#retryPending(); + } + + /** Retry runtimes left on the process registry by owners that named no host. */ static async retryPending(except?: OwnedWorkerRuntime): Promise { - for (const owner of [...this.#pending]) - if (owner !== except) await owner.#retryPending(); + await processWorkerCleanup.retry(except); } /** @@ -173,23 +212,17 @@ export class OwnedWorkerRuntime { initialize?: (supervisor: ProviderProcessSupervisor) => Promise, ): Promise { if (this.#terminalCloseRequested || this.#closing) - return Promise.reject(new Error("Owned worker runtime is closing")); + return Promise.reject(new ProcessAdmissionError("runtime_closing")); if (this.#acquisition !== undefined) return this.#acquisition; - if ( - this.#supervisor !== undefined && - !OwnedWorkerRuntime.#pending.has(this) - ) + if (this.#supervisor !== undefined && !this.#registry.has(this)) return Promise.resolve(); - if ( - this.options.retryCleanup !== undefined && - OwnedWorkerRuntime.#pending.has(this) - ) + if (this.options.retryCleanup !== undefined && this.#registry.has(this)) return this.#retryOwnedCleanup().then(() => { if (this.#terminalCloseRequested || this.#closing) - throw new Error("Owned worker runtime is closing"); + throw new ProcessAdmissionError("runtime_closing"); return this.start(spawnOptions, initialize); }); - const ownRetry = OwnedWorkerRuntime.#pending.has(this) + const ownRetry = this.#registry.has(this) ? this.#retryPending() : Promise.resolve(); const acquisition = ownRetry.then(async () => { @@ -209,7 +242,7 @@ export class OwnedWorkerRuntime { onStdout(listener: (chunk: Buffer | string) => void): void { const stream = this.#supervisor?.launch.process.stdout; if (stream === null || stream === undefined) - throw new Error("Owned worker runtime has no stdout stream"); + throw new ProcessAdmissionError("stdout_missing"); stream.on("data", listener); this.#stdoutListeners.add({ stream, listener }); } @@ -276,7 +309,7 @@ export class OwnedWorkerRuntime { const attempt = Promise.resolve() .then(() => this.options.retryCleanup?.()) .then(() => { - if (OwnedWorkerRuntime.#pending.has(this)) + if (this.#registry.has(this)) throw this.#cleanupError( "Higher-level cleanup retry returned while ownership remains pending", ); @@ -297,13 +330,13 @@ export class OwnedWorkerRuntime { | ((supervisor: ProviderProcessSupervisor) => Promise) | undefined, ): Promise { - await OwnedWorkerRuntime.retryPending(this); - if (this.#closing) throw new Error("Owned worker runtime is closing"); + await this.#registry.retry(this); + if (this.#closing) throw new ProcessAdmissionError("runtime_closing"); this.#root ??= await PrivateRuntimeRoot.create({ prefix: this.options.rootPrefix, }); this.#cleanupEpoch ??= new OwnedWorkerRuntimeCleanupEpoch(this.#root.path); - if (this.#closing) throw new Error("Owned worker runtime is closing"); + if (this.#closing) throw new ProcessAdmissionError("runtime_closing"); this.#lastRunId = spawnOptions.runId; const spawned = await (this.options.launcher ?? spawnOwnedProviderProcess)({ ...spawnOptions, @@ -324,9 +357,9 @@ export class OwnedWorkerRuntime { ); // A close may have begun while spawn was pending. Adopt the process first; // close waits for this acquisition and then stops the exact adopted owner. - if (this.#closing) throw new Error("Owned worker runtime is closing"); + if (this.#closing) throw new ProcessAdmissionError("runtime_closing"); await initialize?.(this.#supervisor); - if (this.#closing) throw new Error("Owned worker runtime is closing"); + if (this.#closing) throw new ProcessAdmissionError("runtime_closing"); } async #stop(): Promise { @@ -337,7 +370,7 @@ export class OwnedWorkerRuntime { this.#detachStdout(); const stopped = await supervisor.stop(); if (stopped.status === "incomplete") { - OwnedWorkerRuntime.#pending.add(this); + this.#registry.add(this); const stdin = supervisor.launch.process.stdin; if ( stdin !== undefined && @@ -372,12 +405,12 @@ export class OwnedWorkerRuntime { this.#cleanupEpoch?.release(); this.#cleanupEpoch = undefined; } catch (cause: unknown) { - OwnedWorkerRuntime.#pending.add(this); + this.#registry.add(this); throw this.#cleanupError(message(cause), cause); } } this.#closed = true; - OwnedWorkerRuntime.#pending.delete(this); + this.#registry.delete(this); } #detachStdout(): void { @@ -398,7 +431,9 @@ export class OwnedWorkerRuntime { this.#inputFailure = cause instanceof Error ? cause - : new Error("Owned worker request stream failed", { cause }); + : new ProcessAdmissionError("request_stream_failed", undefined, { + cause, + }); this.options.onInputFailure?.(this.#inputFailure); }; const onClose = (): void => { diff --git a/src/process/PendingOperations.ts b/src/process/PendingOperations.ts index bcacd18ed8..bfe7882157 100644 --- a/src/process/PendingOperations.ts +++ b/src/process/PendingOperations.ts @@ -1,3 +1,5 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; + /** Timeout and cancellation projections for one correlated operation. */ export type PendingOperationOptions = { readonly signal?: AbortSignal; @@ -36,7 +38,7 @@ export class PendingOperations { /** Register one unique operation key and await its reply or cancellation. */ wait(key: Key, options: PendingOperationOptions): Promise { if (this.#pending.has(key)) - throw new Error("Pending operation key is already registered"); + throw new ProcessAdmissionError("duplicate_key"); if (options.signal?.aborted === true) return Promise.resolve(options.cancelledValue()); diff --git a/src/process/ProcessAdmissionError.ts b/src/process/ProcessAdmissionError.ts new file mode 100644 index 0000000000..3be14df65c --- /dev/null +++ b/src/process/ProcessAdmissionError.ts @@ -0,0 +1,42 @@ +const PROCESS_ADMISSION_CODES = [ + "runtime_closing", + "stdout_missing", + "request_stream_failed", + "finalization_unobserved", + "prefix_identity_lost", + "prefix_unavailable", + "identity_snapshot_unavailable", + "current_process_unreported", + "baseline_unavailable", + "memory_limit_unsupported", + "launcher_pid_missing", + "duplicate_key", + "reader_closed", + "token_json_invalid", + "token_response_invalid", + "token_pid_set", + "token_pid_incomplete", + "identity_json_invalid", + "identity_response_invalid", + "identity_pid_set", + "identity_pid_incomplete", + "limit_unreadable", + "limit_invalid", + "report_exceeds_budget", +] as const; + +export type ProcessAdmissionCode = (typeof PROCESS_ADMISSION_CODES)[number]; + +/** Process ownership, worker, or limit admission failure. `code` is the stable identity. */ +export class ProcessAdmissionError extends Error { + readonly _tag = "ProcessAdmissionError"; + + constructor( + readonly code: ProcessAdmissionCode, + readonly detail?: Readonly>, + options?: ErrorOptions, + ) { + super(code, options); + this.name = "ProcessAdmissionError"; + } +} diff --git a/src/process/ProcessOwnership.exitScope.test.ts b/src/process/ProcessOwnership.exitScope.test.ts index 64df933d6b..86b19d9023 100644 --- a/src/process/ProcessOwnership.exitScope.test.ts +++ b/src/process/ProcessOwnership.exitScope.test.ts @@ -34,13 +34,19 @@ const identity = ( }, }); -it.each(["readable", "retried", "reused", "retained-parent"] as const)( +it.each([ + "readable", + "retried", + "reused", + "retained-parent", + "retained-parent-esrch", +] as const)( "keeps child scope tied to its native generation after parent exit (%s)", async (mode) => { const parent = row(200, 1); const child = row(300, parent.pid); const owner = new ProcessOwnershipEvidenceOwner(); - if (mode === "retained-parent") + if (mode === "retained-parent" || mode === "retained-parent-esrch") owner.reconcile( [ { @@ -64,6 +70,7 @@ it.each(["readable", "retried", "reused", "retained-parent"] as const)( let childTokenReads = 0; let reparented = false; const host: ProcessOwnershipHost = { + platform: "linux", listProcesses: async () => [parent, child], environment: async (pid) => pid === parent.pid ? { REA_PROCESS_RUN_ID: "run" } : {}, @@ -72,9 +79,15 @@ it.each(["readable", "retried", "reused", "retained-parent"] as const)( selected.map((process) => [ process.pid, process.pid === parent.pid - ? mode === "retained-parent" - ? { state: "unavailable", reason: "EACCES" } - : { state: "readable", runId: "run" } + ? mode === "retained-parent-esrch" + ? { + state: "unavailable", + reason: "opaque failure", + code: "ESRCH", + } + : mode === "retained-parent" + ? { state: "unavailable", reason: "EACCES" } + : { state: "readable", runId: "run" } : ++childTokenReads === 1 && mode !== "readable" ? { state: "unavailable", reason: "EACCES" } : { state: "readable", runId: undefined }, diff --git a/src/process/ProcessOwnership.nativeExit.test.ts b/src/process/ProcessOwnership.nativeExit.test.ts index 4768ad5110..115e520ba4 100644 --- a/src/process/ProcessOwnership.nativeExit.test.ts +++ b/src/process/ProcessOwnership.nativeExit.test.ts @@ -1,10 +1,19 @@ +import { spawn } from "node:child_process"; +import { once } from "node:events"; + import { expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, + verifyNoTokenOwnedProcesses, type ProcessIdentityObservation, type ProcessOwnershipHost, type ProcessTableEntry, } from "./ProcessOwnership.js"; +import { + createSystemProcessOwnershipHost, + readProcessOwnershipFacts, + settleVanishedTokenFacts, +} from "./ProcessOwnershipObservation.js"; it.each(["zombie", "absent", "live", "unavailable"] as const)( "rechecks native lifetime when a token disappears during cleanup: %s", @@ -77,3 +86,778 @@ it.each(["zombie", "absent", "live", "unavailable"] as const)( expect(signalGroup).not.toHaveBeenCalled(); }, ); + +// A failing procfs environ read does not establish exit on its own. The +// native observer must corroborate the absence before cleanup drops a PID. +it.each(["ESRCH", "ENOENT"] as const)( + "settles %s from /proc/PID/environ only after native absence confirmation", + async (code) => { + const row: ProcessTableEntry = { + pid: 900, + parentPid: 1, + processGroupId: 900, + state: "S", + command: "short-lived-fixture", + }; + let identityReads = 0; + const live: ProcessIdentityObservation = { + state: "readable", + identity: "start:900", + lifecycle: { state: "observed", lifecycle: "non-zombie" }, + relation: { state: "observed", parentPid: 1, processGroupId: 900 }, + }; + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => [row], + environment: async () => { + throw Object.assign( + new Error(code + ": no such process, open '/proc/900/environ'"), + { code }, + ); + }, + processIdentities: async (rows) => + new Map( + rows.map(({ pid }) => [ + pid, + ++identityReads < 3 + ? live + : { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + }, + ]), + ), + signalGroup: vi.fn(), + }; + const [fact] = await readProcessOwnershipFacts([row], host); + expect(identityReads).toBe(3); + expect(fact?.token).toMatchObject({ + state: "unavailable", + reason: expect.stringContaining(code), + }); + expect(fact?.state).toBe("absent"); + expect(fact?.identityAfter).toEqual({ + state: "absent", + reason: "not-found-in-caller-pid-namespace", + }); + expect(host.signalGroup).not.toHaveBeenCalled(); + }, +); + +it.each([ + { + code: "ESRCH", + mode: "live", + expectedReads: 3, + expectedState: "same-generation", + }, + { + code: "ESRCH", + mode: "reused", + expectedReads: 3, + expectedState: "unavailable", + }, + { + code: "ESRCH", + mode: "reused-zombie", + expectedReads: 3, + expectedState: "unavailable", + }, + { + code: "ESRCH", + mode: "unknown", + expectedReads: 3, + expectedState: "same-generation", + }, + { + code: "EACCES", + mode: "absent", + expectedReads: 2, + expectedState: "same-generation", + }, +] as const)( + "does not clear unverifiable /proc environ failures: $code / $mode", + async ({ code, mode, expectedReads, expectedState }) => { + const row: ProcessTableEntry = { + pid: 901, + parentPid: 1, + processGroupId: 901, + state: "S", + command: "fixture", + }; + let identityReads = 0; + const live: ProcessIdentityObservation = { + state: "readable", + identity: "start:901", + lifecycle: { state: "observed", lifecycle: "non-zombie" }, + relation: { state: "observed", parentPid: 1, processGroupId: 901 }, + }; + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => [row], + environment: async () => { + throw Object.assign(new Error(code + ": open '/proc/901/environ'"), { + code, + }); + }, + processIdentities: async (rows) => + new Map( + rows.map(({ pid }) => { + identityReads += 1; + if (identityReads < 3) return [pid, live] as const; + switch (mode) { + case "absent": + return [ + pid, + { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + }, + ] as const; + case "reused": + return [ + pid, + { ...live, identity: "start:replacement" }, + ] as const; + case "reused-zombie": + return [ + pid, + { + ...live, + identity: "start:zombie-replacement", + lifecycle: { + state: "observed" as const, + lifecycle: "zombie" as const, + }, + }, + ] as const; + case "unknown": + return [ + pid, + { + state: "unavailable" as const, + reason: "permission denied", + }, + ] as const; + default: + return [pid, live] as const; + } + }), + ), + signalGroup: vi.fn(), + }; + const [fact] = await readProcessOwnershipFacts([row], host); + expect(identityReads).toBe(expectedReads); + expect(fact?.state).toBe(expectedState); + expect(fact?.identityAfter.state).not.toBe("absent"); + expect(fact?.token.state).toBe("unavailable"); + expect(host.signalGroup).not.toHaveBeenCalled(); + }, +); + +const itOnLinux = process.platform === "linux" ? it : it.skip; + +itOnLinux( + "verifies a real child exit between the native identity and follow-up read", + async () => { + const child = spawn( + process.execPath, + ["-e", "setInterval(() => {}, 1_000)"], + { + stdio: "ignore", + }, + ); + if (child.pid === undefined) throw new Error("fixture child did not start"); + const pid = child.pid; + const row: ProcessTableEntry = { + pid, + parentPid: process.pid, + processGroupId: pid, + state: "S", + command: process.execPath, + }; + const systemHost = createSystemProcessOwnershipHost("linux", {}); + let nativeReads = 0; + let exitArmed = false; + const host: ProcessOwnershipHost = { + ...systemHost, + environment: async () => { + exitArmed = true; + throw Object.assign( + new Error( + "ESRCH: simulated environ-open race before native process exit", + ), + { code: "ESRCH" }, + ); + }, + processIdentities: async (...args) => { + const observations = await systemHost.processIdentities!(...args); + nativeReads += 1; + if (exitArmed) { + exitArmed = false; + child.kill("SIGTERM"); + await once(child, "exit"); + } + return observations; + }, + signalGroup: vi.fn(), + }; + try { + const [fact] = await readProcessOwnershipFacts([row], host); + expect(nativeReads).toBeGreaterThanOrEqual(3); + expect(fact?.identityBefore).toMatchObject({ + state: "readable", + identity: expect.stringMatching(/^linux-starttime:/u), + }); + expect(fact?.token).toMatchObject({ + state: "unavailable", + reason: expect.stringContaining("ESRCH"), + }); + expect(fact?.identityAfter).toEqual({ + state: "absent", + reason: "not-found-in-caller-pid-namespace", + }); + expect(fact?.state).toBe("absent"); + expect(host.signalGroup).not.toHaveBeenCalled(); + } finally { + if (child.exitCode === null && child.signalCode === null) { + child.kill("SIGKILL"); + await once(child, "exit"); + } + } + }, +); + +it.each([ + "absent", + "delayed-absent", + "live", + "reused", + "delayed-reused", + "unknown", + "reused-zombie", +] as const)( + "resolves a Linux token-read exit race after final sweep retry: %s", + async (lastIdentity) => { + const row: ProcessTableEntry = { + pid: 902, + parentPid: 1, + processGroupId: 902, + state: "S", + command: "short-lived-worker", + }; + const live: ProcessIdentityObservation = { + state: "readable", + identity: "start:902", + lifecycle: { state: "observed", lifecycle: "non-zombie" }, + relation: { state: "observed", parentPid: 1, processGroupId: 902 }, + }; + let tokenReads = 0; + let readsAfterLastToken = 0; + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => [row], // stale snapshot must never prove exit + environment: async () => { + tokenReads += 1; + throw Object.assign(new Error("ESRCH: open '/proc/902/environ'"), { + code: "ESRCH", + }); + }, + processIdentities: async (members) => + new Map( + members.map(({ pid }) => { + if (tokenReads < 4) return [pid, live] as const; + readsAfterLastToken += 1; + const delayed = lastIdentity.startsWith("delayed-"); + if (readsAfterLastToken < (delayed ? 5 : 3)) + return [pid, live] as const; + switch (lastIdentity) { + case "delayed-absent": + case "absent": + return [ + pid, + { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace", + }, + ] as const; + case "delayed-reused": + case "reused": + return [ + pid, + { ...live, identity: "start:replacement" }, + ] as const; + case "reused-zombie": + return [ + pid, + { + ...live, + identity: "start:zombie-replacement", + lifecycle: { + state: "observed" as const, + lifecycle: "zombie" as const, + }, + }, + ] as const; + case "unknown": + return [ + pid, + { + state: "unavailable" as const, + reason: "native identity permission denied", + }, + ] as const; + default: + return [pid, live] as const; + } + }), + ), + signalGroup: vi.fn(), + }; + const result = await verifyNoTokenOwnedProcesses("run-token", host); + expect(tokenReads).toBe(4); + // A live process is not repeatedly scanned without a fixed upper bound. + expect(readsAfterLastToken).toBeLessThanOrEqual(6); + if (lastIdentity === "delayed-absent" || lastIdentity === "delayed-reused") + expect(readsAfterLastToken).toBe(5); + if (lastIdentity === "absent" || lastIdentity === "delayed-absent") + expect(result).toEqual({ cleaned: true, signaled: false }); + else + expect(result).toMatchObject({ + cleaned: false, + failures: [ + { + pid: 902, + reason: lastIdentity.includes("reused") + ? "process-identity-unavailable" + : "environment-unreadable", + }, + ], + }); + expect(host.signalGroup).not.toHaveBeenCalled(); + }, +); + +it.each(["absent", "live", "unavailable", "reused", "reused-zombie"] as const)( + "corroborates group token-open races without signaling %s", + async (outcome) => { + const row: ProcessTableEntry = { + pid: 910, + parentPid: 1, + processGroupId: 910, + state: "S", + command: "fixture", + }; + let tokenRead = false; + let postTokenReads = 0; + let absent = false; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => (absent ? [] : [row]), + environment: async () => ({}), + runTokens: async (members) => { + tokenRead = true; + return new Map( + members.map(({ pid }) => [ + pid, + { + state: "unavailable" as const, + reason: "failure details without errno", + code: "ESRCH", + }, + ]), + ); + }, + processIdentities: async (members) => + new Map( + members.map(({ pid }) => { + const later = tokenRead && ++postTokenReads >= 3; + if (later && outcome === "absent") { + absent = true; + return [ + pid, + { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + }, + ]; + } + if (later && outcome === "unavailable") { + return [ + pid, + { state: "unavailable" as const, reason: "identity denied" }, + ]; + } + return [ + pid, + { + state: "readable" as const, + identity: + later && outcome.startsWith("reused") + ? "replacement" + : "generation-910", + lifecycle: { + state: "observed" as const, + lifecycle: + later && outcome === "reused-zombie" + ? ("zombie" as const) + : ("non-zombie" as const), + }, + relation: { + state: "observed" as const, + parentPid: 1, + processGroupId: 910, + }, + }, + ]; + }), + ), + signalGroup, + }; + const result = await cleanupOwnedProcessGroup( + { runId: "run", leaderPid: 910, processGroupId: 910 }, + host, + ); + if (outcome === "absent") + expect(result).toEqual({ cleaned: true, signaled: false }); + else + expect(result).toMatchObject({ + cleaned: false, + failures: [ + { + pid: 910, + reason: outcome.startsWith("reused") + ? "process-identity-unavailable" + : "environment-unreadable", + }, + ], + }); + expect(signalGroup).not.toHaveBeenCalled(); + }, +); + +it("honors cancellation during token-open exit settlement", async () => { + const row: ProcessTableEntry = { + pid: 911, + parentPid: 1, + processGroupId: 911, + state: "S", + command: "fixture", + }; + const identity: ProcessIdentityObservation = { + state: "readable", + identity: "generation-911", + lifecycle: { state: "observed", lifecycle: "non-zombie" }, + }; + const controller = new AbortController(); + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => [row], + environment: async () => ({}), + signalGroup: vi.fn(), + processIdentities: async () => { + controller.abort(new Error("cancelled")); + return new Map([[row.pid, identity]]); + }, + }; + await expect( + settleVanishedTokenFacts( + [ + { + process: row, + state: "same-generation", + identityBefore: identity, + identityAfter: identity, + token: { state: "unavailable", reason: "vanished", code: "ESRCH" }, + }, + ], + host, + { deadline: undefined }, + controller.signal, + ), + ).rejects.toThrow("cancelled"); +}); + +it("does not signal a stale group after its last member exits during revalidation", async () => { + const row: ProcessTableEntry = { + pid: 912, + parentPid: 1, + processGroupId: 912, + state: "S", + command: "fixture", + }; + let tokenReads = 0; + let laterReads = 0; + let absent = false; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => (absent ? [] : [row]), + environment: async () => ({}), + runTokens: async (members) => { + tokenReads += 1; + return new Map( + members.map(({ pid }) => [ + pid, + tokenReads === 1 + ? { state: "readable" as const, runId: "run" } + : { + state: "unavailable" as const, + reason: "failure details without errno", + code: "ESRCH", + }, + ]), + ); + }, + processIdentities: async (members) => + new Map( + members.map(({ pid }) => { + if (tokenReads >= 2 && ++laterReads >= 3) { + absent = true; + return [ + pid, + { state: "absent", reason: "not-found-in-caller-pid-namespace" }, + ]; + } + return [ + pid, + { + state: "readable", + identity: "generation-912", + lifecycle: { state: "observed", lifecycle: "non-zombie" }, + relation: { + state: "observed", + parentPid: 1, + processGroupId: 912, + }, + }, + ]; + }), + ), + signalGroup, + }; + expect( + await cleanupOwnedProcessGroup( + { runId: "run", leaderPid: 912, processGroupId: 912 }, + host, + ), + ).toEqual({ cleaned: true, signaled: false }); + expect(tokenReads).toBe(2); + expect(signalGroup).not.toHaveBeenCalled(); +}); + +it("revalidates a group after its observed launcher exits and a child appears", async () => { + const launcher: ProcessTableEntry = { + pid: 920, + parentPid: 1, + processGroupId: 920, + state: "S", + command: "launcher", + }; + const child: ProcessTableEntry = { + pid: 921, + parentPid: 1, + processGroupId: 920, + state: "S", + command: "child", + }; + let exited = false; + let signaled = false; + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => (signaled ? [] : exited ? [child] : [launcher]), + environment: async (pid) => { + if (pid === launcher.pid) { + exited = true; + throw Object.assign(new Error("opaque exit"), { code: "ESRCH" }); + } + return { REA_PROCESS_RUN_ID: "run" }; + }, + processIdentities: async (rows) => + new Map( + rows.map(({ pid }) => [ + pid, + pid === launcher.pid && exited + ? { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + } + : { + state: "readable" as const, + identity: `generation-${pid}`, + lifecycle: { + state: "observed" as const, + lifecycle: "non-zombie" as const, + }, + relation: { + state: "observed" as const, + parentPid: 1, + processGroupId: 920, + }, + }, + ]), + ), + signalGroup: vi.fn(() => { + signaled = true; + }), + }; + expect( + await cleanupOwnedProcessGroup( + { runId: "run", leaderPid: 920, processGroupId: 920 }, + host, + ), + ).toEqual({ cleaned: true, signaled: true }); + expect(host.signalGroup).toHaveBeenCalledWith(920, "SIGKILL"); +}); + +it("refreshes a group when its last member exits during revalidation and a child appears", async () => { + const launcher: ProcessTableEntry = { + pid: 940, + parentPid: 1, + processGroupId: 940, + state: "S", + command: "launcher", + }; + const child: ProcessTableEntry = { + pid: 941, + parentPid: 940, + processGroupId: 940, + state: "S", + command: "child", + }; + let planningTokenRead = false; + let phase: "planning" | "snapshot" | "after-snapshot" = "planning"; + let refreshFoundChild = false; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => { + if (phase === "after-snapshot") { + refreshFoundChild = true; + return [child]; + } + // The snapshot under test is the table read after planning has already + // observed the launcher token. Earlier planning reads stay on the launcher. + if (planningTokenRead && phase === "planning") phase = "snapshot"; + return [launcher]; + }, + environment: async (pid) => { + if (pid === launcher.pid && phase === "snapshot") { + phase = "after-snapshot"; + throw Object.assign(new Error("opaque exit"), { code: "ESRCH" }); + } + if (pid === launcher.pid) planningTokenRead = true; + return { REA_PROCESS_RUN_ID: "run" }; + }, + processIdentities: async (rows) => + new Map( + rows.map(({ pid }) => + pid === launcher.pid && phase === "after-snapshot" + ? [ + pid, + { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + }, + ] + : [ + pid, + { + state: "readable" as const, + identity: `generation-${pid}`, + lifecycle: { + state: "observed" as const, + lifecycle: "non-zombie" as const, + }, + relation: { + state: "observed" as const, + parentPid: pid === child.pid ? launcher.pid : 1, + processGroupId: 940, + }, + }, + ], + ), + ), + signalGroup, + }; + expect( + await cleanupOwnedProcessGroup( + { runId: "run", leaderPid: 940, processGroupId: 940 }, + host, + ), + ).toEqual({ cleaned: true, signaled: true }); + expect(refreshFoundChild).toBe(true); + expect(signalGroup).toHaveBeenCalledWith(940, "SIGKILL"); +}); + +it.each([ + { code: "ESRCH", message: "opaque failure", expected: "absent" }, + { + code: undefined, + message: "ESRCH: misleading text", + expected: "same-generation", + }, +] as const)( + "uses structured token error codes instead of message text ($message)", + async ({ code, message, expected }) => { + const row: ProcessTableEntry = { + pid: 930, + parentPid: 1, + processGroupId: 930, + state: "S", + command: "fixture", + }; + let tokenRead = false; + let observedAfterToken = false; + const host: ProcessOwnershipHost = { + platform: "linux", + listProcesses: async () => [row], + environment: async () => { + tokenRead = true; + throw Object.assign( + new Error(message), + code === undefined ? {} : { code }, + ); + }, + processIdentities: async (rows) => + new Map( + rows.map(({ pid }) => { + if (tokenRead && observedAfterToken) + return [ + pid, + { + state: "absent" as const, + reason: "not-found-in-caller-pid-namespace" as const, + }, + ]; + if (tokenRead) observedAfterToken = true; + return [ + pid, + { + state: "readable" as const, + identity: "generation-930", + lifecycle: { + state: "observed" as const, + lifecycle: "non-zombie" as const, + }, + relation: { + state: "observed" as const, + parentPid: 1, + processGroupId: 930, + }, + }, + ]; + }), + ), + signalGroup: vi.fn(), + }; + const [fact] = await readProcessOwnershipFacts([row], host); + expect(fact?.state).toBe(expected); + expect(fact?.token).toMatchObject({ + state: "unavailable", + reason: message, + ...(code === undefined ? {} : { code }), + }); + }, +); diff --git a/src/process/ProcessOwnership.ts b/src/process/ProcessOwnership.ts index e6f373ff83..cc9d838adc 100644 --- a/src/process/ProcessOwnership.ts +++ b/src/process/ProcessOwnership.ts @@ -8,6 +8,7 @@ import { readNativeProcessIdentityMap, readProcessOwnershipFacts, settleExitingProcessIdentities, + settleVanishedTokenFacts, systemProcessOwnershipHost as systemHost, } from "./ProcessOwnershipObservation.js"; import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; @@ -201,7 +202,11 @@ export interface ProcessOwnershipHost { /** Whether one live process exposed its ownership token to the host. */ export type ProcessRunTokenObservation = | { readonly state: "readable"; readonly runId: string | undefined } - | { readonly state: "unavailable"; readonly reason: string }; + | { + readonly state: "unavailable"; + readonly reason: string; + readonly code?: string; + }; /** Narrow Windows P0 seam for bounded process-tree termination. */ export interface WindowsProcessTreeHost { @@ -1045,7 +1050,7 @@ const createOwnedCleanupPlan = async ( observedGroupId === processGroupId, ); if (members.length === 0) continue; - const failures = await processOwnershipFailures( + const { failures, validatedMembers } = await processOwnershipFailures( members, ownership, host, @@ -1054,7 +1059,7 @@ const createOwnedCleanupPlan = async ( ); if (failures.length === 0 && unverifiableSampledGroups.has(processGroupId)) unresolved.push( - ...members.map(({ pid }) => ({ + ...validatedMembers.map(({ pid }) => ({ pid, reason: "process-identity-unavailable" as const, diagnostic: @@ -1134,55 +1139,69 @@ const revalidateOwnedProcessGroup = async ( } | ProcessCleanupResult > => { - let members: readonly ProcessTableEntry[]; - let escapedProcessGroupIds: readonly number[] = []; - try { - const settled = await settleLauncherExit( + const escapedProcessGroupIds = new Set(); + for (let refresh = 0; ; refresh += 1) { + let members: readonly ProcessTableEntry[]; + try { + const settled = await settleLauncherExit( + ownership, + liveProcesses(await host.listProcesses()), + host, + exitSettlement, + ); + if ("cleaned" in settled) return settled; + for (const groupId of settled.escapedProcessGroupIds) + escapedProcessGroupIds.add(groupId); + const relationSnapshot = await readProcessRelationSnapshot( + ownership, + settled.processes, + host, + ); + if (relationSnapshot.failures.length > 0) + return cleanupValidationFailure(relationSnapshot.failures); + members = relationSnapshot.currentRelations.filter( + ({ processGroupId: observedGroupId }) => + observedGroupId === processGroupId, + ); + } catch (cause: unknown) { + return { + cleaned: false, + reason: `process ownership could not be revalidated: ${errorMessage(cause)}`, + }; + } + const { failures, validatedMembers } = await processOwnershipFailures( + members, ownership, - liveProcesses(await host.listProcesses()), host, + expectedIdentities, exitSettlement, ); - if ("cleaned" in settled) return settled; - escapedProcessGroupIds = settled.escapedProcessGroupIds; - const processes = settled.processes; - const relationSnapshot = await readProcessRelationSnapshot( - ownership, - processes, - host, - ); - if (relationSnapshot.failures.length > 0) - return cleanupValidationFailure(relationSnapshot.failures); - members = relationSnapshot.currentRelations.filter( - ({ processGroupId: observedGroupId }) => - observedGroupId === processGroupId, - ); - } catch (cause: unknown) { - return { - cleaned: false, - reason: `process ownership could not be revalidated: ${errorMessage(cause)}`, - }; + if (failures.length > 0) return cleanupValidationFailure(failures); + if ( + processGroupId !== ownership.processGroupId && + validatedMembers.length > 0 && + !validatedMembers.some(({ pid }) => pid === processGroupId) && + ownership.sweepTokenOwnedProcesses !== true + ) + return { + cleaned: false, + reason: + "descendant process-group leader identity could not be revalidated", + }; + // Exit of every row in this table does not prove the group is empty. A + // same-group child can be forked after the read and during the token + // checks, so refresh before skipping the signal. + if (validatedMembers.length > 0 || members.length === 0) + return { + empty: validatedMembers.length === 0, + escapedProcessGroupIds: [...escapedProcessGroupIds], + }; + if (refresh >= OWNED_GROUP_REVALIDATION_LIMIT) + return { + cleaned: false, + reason: `owned process group ${String(processGroupId)} members exited during revalidation after ${String(OWNED_GROUP_REVALIDATION_LIMIT)} table refreshes. This release has no runtime override.`, + }; } - const failures = await processOwnershipFailures( - members, - ownership, - host, - expectedIdentities, - exitSettlement, - ); - if (failures.length > 0) return cleanupValidationFailure(failures); - if ( - processGroupId !== ownership.processGroupId && - members.length > 0 && - !members.some(({ pid }) => pid === processGroupId) && - ownership.sweepTokenOwnedProcesses !== true - ) - return { - cleaned: false, - reason: - "descendant process-group leader identity could not be revalidated", - }; - return { empty: members.length === 0, escapedProcessGroupIds }; }; const cleanupValidationFailure = ( @@ -1282,13 +1301,21 @@ const processOwnershipFailures = async ( host: ProcessOwnershipHost, expectedIdentities: ReadonlyMap = new Map(), exitSettlement: ProcessExitSettlementBudget = { deadline: undefined }, -): Promise => { +): Promise<{ + readonly failures: readonly ProcessOwnershipValidationFailure[]; + readonly validatedMembers: readonly ProcessTableEntry[]; +}> => { const runId = ownership.runId; if (host.processIdentities !== undefined) { - const facts = await readProcessOwnershipFacts(members, host, undefined, { + const facts = await settleVanishedTokenFacts( + await readProcessOwnershipFacts(members, host, undefined, { + exitSettlement, + }), + host, exitSettlement, - }); + ); const failures: ProcessOwnershipValidationFailure[] = []; + const validatedMembers: ProcessTableEntry[] = []; for (const fact of facts) { if (processIdentityHasExited(fact.identityAfter)) continue; if (processIdentityIsExiting(fact.identityAfter)) { @@ -1361,12 +1388,17 @@ const processOwnershipFailures = async ( }); } else if (fact.token.runId !== runId) { failures.push({ pid: fact.process.pid, reason: "run-token-mismatch" }); + } else { + validatedMembers.push(fact.process); } } - return failures; + return { failures, validatedMembers }; } - return processRunTokenFailures(members, runId, host); + return { + failures: await processRunTokenFailures(members, runId, host), + validatedMembers: members, + }; }; const processRunTokenFailures = async ( @@ -1675,6 +1707,9 @@ const scanWithOwnershipEvidence = async ( facts = facts.map((fact) => retryByPid.get(fact.process.pid) ?? fact); } + // Recheck final token-open races at the shared native identity boundary. + facts = await settleVanishedTokenFacts(facts, host, exitSettlement); + const processGroupIds = new Set([ ...(relation === undefined ? [] : [relation.processGroupId]), ...(relation?.sampledProcessGroupIds ?? []), diff --git a/src/process/ProcessOwnershipObservation.ts b/src/process/ProcessOwnershipObservation.ts index d0180ae6fe..15ca968f41 100644 --- a/src/process/ProcessOwnershipObservation.ts +++ b/src/process/ProcessOwnershipObservation.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; import { readFile, stat as statPath } from "node:fs/promises"; import { performance } from "node:perf_hooks"; import { setTimeout as delay } from "node:timers/promises"; @@ -832,6 +833,77 @@ export const settleExitingProcessIdentities = async ( return identities; }; +/** Corroborate Linux token-open exit races without granting signal authority. */ +export const settleVanishedTokenFacts = async ( + facts: readonly ProcessOwnershipFact[], + host: ProcessOwnershipHost, + budget: ProcessExitSettlementBudget, + signal?: AbortSignal, +): Promise => { + if ((host.platform ?? process.platform) !== "linux") return facts; + let pending = facts + .filter( + ({ token, identityAfter }) => + token.state === "unavailable" && + (token.code === "ENOENT" || token.code === "ESRCH") && + !processIdentityHasExited(identityAfter), + ) + .map(({ process }) => process); + if (pending.length === 0) return facts; + let current = facts; + const deadline = (budget.deadline ??= + performance.now() + PROCESS_CLEANUP_VERIFICATION_GRACE_MS); + for (let attempt = 0; attempt < 4 && pending.length > 0; attempt += 1) { + signal?.throwIfAborted(); + if (attempt > 0) { + const remaining = deadline - performance.now(); + if (remaining <= 0) break; + await delay( + Math.min(40, remaining), + undefined, + signal === undefined ? undefined : { signal }, + ); + } + const confirmed = await readNativeProcessIdentityMap(pending, host, signal); + const pendingPids = new Set(pending.map(({ pid }) => pid)); + const nextPending = new Set(); + current = current.map((fact) => { + if (!pendingPids.has(fact.process.pid)) return fact; + const after = confirmed.get(fact.process.pid); + if (after?.state === "absent") + return { + ...fact, + state: "absent" as const, + identityAfter: after, + beforeExitSettlement: fact.beforeExitSettlement ?? fact, + }; + const previous = fact.identityAfter; + if (previous.state !== "readable" || after?.state !== "readable") + return fact; + if (previous.identity !== after.identity) + return { + ...fact, + state: "unavailable" as const, + identityAfter: { + state: "unavailable" as const, + reason: "process_identity_changed_during_final_token_exit_check", + }, + }; + if (processIdentityHasExited(after)) + return { + ...fact, + identityAfter: after, + beforeExitSettlement: fact.beforeExitSettlement ?? fact, + }; + nextPending.add(fact.process.pid); + return fact; + }); + pending = pending.filter(({ pid }) => nextPending.has(pid)); + } + signal?.throwIfAborted(); + return current; +}; + /** * Read identity, token, then identity for caller-provided rows. This owner * never enumerates the process table; a token is tied to a generation only @@ -889,10 +961,56 @@ export const readProcessOwnershipFacts = async ( signal, credentialPids, ); + // An ENOENT/ESRCH environment failure can race with process exit while + // /proc/PID/stat is still readable. Do not treat the token error itself as + // proof of exit: ask the native identity owner again, which corroborates a + // missing procfs entry with signal-zero ESRCH. Keep the previous live + // observation if the refresh is unreadable; PID reuse remains a mismatch. + let finalObservedAfter = observedAfter; + if ((host.platform ?? process.platform) === "linux") { + const disappearing = processes.filter(({ pid }) => { + const token = tokens.get(pid); + return ( + token?.state === "unavailable" && + (token.code === "ENOENT" || token.code === "ESRCH") && + observedAfter.get(pid)?.state === "readable" + ); + }); + if (disappearing.length > 0) { + const refreshed = await readIdentityFacts( + disappearing, + host, + "after", + signal, + credentialPids, + ); + const confirmed = new Map(observedAfter); + for (const { pid } of disappearing) { + const before = observedAfter.get(pid); + const after = refreshed.get(pid); + if (after?.state === "absent") { + confirmed.set(pid, after); + } else if ( + before?.state === "readable" && + after?.state === "readable" + ) { + // A recycled PID (including a zombie replacement) is not proof of + // the original process exit. Keep its ownership unresolved. + if (before.identity !== after.identity) + confirmed.set(pid, { + state: "unavailable", + reason: "process_identity_changed_during_environment_exit_check", + }); + else if (processIdentityHasExited(after)) confirmed.set(pid, after); + } + } + finalObservedAfter = confirmed; + } + } const identityAfter = await settleExitingProcessIdentities( processes, host, - observedAfter, + finalObservedAfter, signal, options.exitSettlement, credentialPids, @@ -1231,9 +1349,9 @@ export const createSystemProcessOwnershipHost = ( liveAfterRetry.some((entry) => entry.pid === pid), ); if (stillUnresolved.length > 0) - throw new Error( - `process identity snapshot is unavailable for ${String(stillUnresolved.length)} live processes`, - ); + throw new ProcessAdmissionError("identity_snapshot_unavailable", { + live_processes: stillUnresolved.length, + }); } return processes.map(({ pid }) => { const observation = identities.get(pid); @@ -1252,7 +1370,7 @@ export const createSystemProcessOwnershipHost = ( try { const processes = await listProcesses(signal); if (!processes.some(({ pid }) => pid === process.pid)) - throw new Error("ps did not report REA's current process"); + throw new ProcessAdmissionError("current_process_unreported"); } catch (cause: unknown) { if (isExpectedAbort(cause, signal)) throw cause; throw new LinuxOwnedProcessPrerequisiteError(cause); diff --git a/src/process/ProcessRunTokenObservations.ts b/src/process/ProcessRunTokenObservations.ts index 893a113413..e220eda1cb 100644 --- a/src/process/ProcessRunTokenObservations.ts +++ b/src/process/ProcessRunTokenObservations.ts @@ -68,8 +68,15 @@ export async function* readProcessRunTokens( } catch (cause: unknown) { signal?.throwIfAborted(); const reason = errorMessage(cause); + const code = + cause instanceof Error && + "code" in cause && + typeof cause.code === "string" + ? cause.code + : undefined; observation = { state: "unavailable", + ...(code === undefined ? {} : { code }), reason: batchFailure === undefined ? reason diff --git a/src/process/ProviderProcess.ts b/src/process/ProviderProcess.ts index 7bb1f1ba2d..83946af8f2 100644 --- a/src/process/ProviderProcess.ts +++ b/src/process/ProviderProcess.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "./ProcessAdmissionError.js"; import { spawn, type ChildProcess } from "node:child_process"; import type { EventEmitter } from "node:events"; import type { Readable, Writable } from "node:stream"; @@ -263,7 +264,7 @@ export const spawnOwnedProviderProcess = async ( if (sweepTokenOwnedProcesses) { const capture = systemProcessOwnershipHost.captureBaseline; if (capture === undefined) - throw new Error("Process ownership baseline is unavailable on this host"); + throw new ProcessAdmissionError("baseline_unavailable"); captureBaseline = await capture(options.signal); options.signal?.throwIfAborted(); } @@ -306,9 +307,7 @@ export const spawnOwnedProviderProcess = async ( }; } if (options.memoryLimitBytes !== undefined) - throw new Error( - "Owned process memory limits require the Windows Job Object launcher; no limit can be enforced on this host.", - ); + throw new ProcessAdmissionError("memory_limit_unsupported"); const child = spawn(options.command, [...options.arguments], { shell: false, windowsHide: true, @@ -320,7 +319,8 @@ export const spawnOwnedProviderProcess = async ( }); await waitForSpawn(child); const pid = child.pid; - if (pid === undefined) throw new Error("Provider launcher has no process ID"); + if (pid === undefined) + throw new ProcessAdmissionError("launcher_pid_missing"); const ownership: OwnedProcessGroup = { runId: options.runId, leaderPid: pid, diff --git a/src/process/WorkerCleanupRegistry.test.ts b/src/process/WorkerCleanupRegistry.test.ts new file mode 100644 index 0000000000..d44dc76474 --- /dev/null +++ b/src/process/WorkerCleanupRegistry.test.ts @@ -0,0 +1,27 @@ +import { expect, it } from "vitest"; + +import { + OwnedWorkerRuntime, + WorkerCleanupRegistry, +} from "./OwnedWorkerRuntime.js"; + +it("retries one host's retained runtime without failing another host", async () => { + const first = new WorkerCleanupRegistry(); + const second = new WorkerCleanupRegistry(); + let fail = true; + const stuck = new OwnedWorkerRuntime({ + rootPrefix: "rea-worker-host-", + cleanupRegistry: first, + retryCleanup: async () => { + if (fail) throw new Error("host still owns its worker"); + }, + }); + stuck.retainForCleanup(); + try { + await expect(second.retry()).resolves.toBeUndefined(); + await expect(first.retry()).rejects.toThrow("host still owns its worker"); + } finally { + fail = false; + await stuck.close(); + } +}); diff --git a/src/process/capture/FilesystemSnapshot.ts b/src/process/capture/FilesystemSnapshot.ts index 1bc48beb84..06366fa376 100644 --- a/src/process/capture/FilesystemSnapshot.ts +++ b/src/process/capture/FilesystemSnapshot.ts @@ -1,4 +1,4 @@ -import { observedSymlinkTarget } from "../../domain/process/symlinkTarget.js"; +import { observedSymlinkTarget } from "../../domain/symlinkTarget.js"; import { createHash } from "node:crypto"; import { constants as fsConstants } from "node:fs"; import { lstat, open, opendir, readlink, realpath } from "node:fs/promises"; diff --git a/src/process/capture/ProcessHarness.ts b/src/process/capture/ProcessHarness.ts index c674063c66..08cf60b3ca 100644 --- a/src/process/capture/ProcessHarness.ts +++ b/src/process/capture/ProcessHarness.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "../ProcessAdmissionError.js"; import type { IPty } from "@lydell/node-pty"; import type { ProcessCapture } from "../../domain/process/processCaptureParsing.js"; import type { ProcessScenario } from "../../domain/process/processScenario.js"; @@ -846,9 +847,7 @@ const runProcessScenario = async ( }; if (exit.reason === "cancelled") throw processCaptureCancelled(signal?.reason); - throw new Error( - "The captured process exit was not observed because the finalization SIGKILL could not be delivered.", - ); + throw new ProcessAdmissionError("finalization_unobserved"); } if (exit.finalization !== undefined) observations.finalization = { diff --git a/src/process/capture/ProcessSensitiveOutput.ts b/src/process/capture/ProcessSensitiveOutput.ts index b2a20b2332..d599d28ed5 100644 --- a/src/process/capture/ProcessSensitiveOutput.ts +++ b/src/process/capture/ProcessSensitiveOutput.ts @@ -1,3 +1,4 @@ +import { ProcessAdmissionError } from "../ProcessAdmissionError.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js"; import { err, ok, type Result } from "../../domain/result.js"; @@ -134,7 +135,7 @@ export class ProcessSensitiveOutput { #prefix(index: number): Prefix { const value = this.prefixes[index]; if (value === undefined) - throw new Error("Sensitive output matcher lost its prefix identity"); + throw new ProcessAdmissionError("prefix_identity_lost"); return value; } @@ -342,7 +343,7 @@ export const createProcessSensitiveOutput = ( const read = (index: number): Prefix => { const node = prefixes[index]; if (node === undefined) - throw new Error("Sensitive output prefix is unavailable"); + throw new ProcessAdmissionError("prefix_unavailable"); return node; }; try { diff --git a/src/process/ownedDebuggerSession.ts b/src/process/ownedDebuggerSession.ts index a5e587c8a1..009bb42e82 100644 --- a/src/process/ownedDebuggerSession.ts +++ b/src/process/ownedDebuggerSession.ts @@ -1,5 +1,8 @@ import { readFile } from "node:fs/promises"; +import type { DebuggerAdmissionLimit } from "../config/debuggerSessionLimits.js"; +import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; +import type { JsonValue } from "../domain/jsonValue.js"; import { parseLinuxProcessStat } from "./LinuxProcessStat.js"; /** Result of terminating one debugger process owned by this application. */ @@ -7,6 +10,84 @@ export type OwnedDebuggerStopResult = | { readonly status: "stopped" | "already_exited" } | { readonly status: "incomplete"; readonly reason: string }; +/** Structured facts a limit error exposes. The sentence is display text. */ +export const debuggerAdmissionDiagnostics = ( + admission: DebuggerAdmissionLimit, +): { + readonly setting: string; + readonly effective_value: number; + readonly source: DebuggerAdmissionLimit["source"]; + readonly default_value: number; + readonly minimum: number; + readonly maximum: number; + readonly unit: DebuggerAdmissionLimit["unit"]; + readonly restart_required: true; +} => ({ + setting: admission.setting, + effective_value: admission.value, + source: admission.source, + default_value: admission.defaultValue, + minimum: admission.minimum, + maximum: admission.maximum, + unit: admission.unit, + restart_required: true, +}); + +/** Name the setting, the effective value, its source, the range, and a restart. */ +export const debuggerAdmissionRecovery = ( + admission: DebuggerAdmissionLimit, +): string => + `The effective ${admission.setting} value is ${String(admission.value)} ${admission.unit} (source ${admission.source}; default ${String(admission.defaultValue)} ${admission.unit}, floor ${String(admission.minimum)} ${admission.unit}, maximum ${String(admission.maximum)} ${admission.unit}). Set ${admission.setting} in the environment that starts REA to a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)}, then restart REA.`; + +/** Fixed startup deadline. Distinct from the shared command deadline setting. */ +export const fixedDeadlineGuidance = ( + what: string, + timeoutMs: number, +): { readonly userMessage: string; readonly remediationAction: string } => ({ + userMessage: `${what} reached its ${String(timeoutMs)} ms deadline (deadlineExpired). This release has no runtime override.`, + remediationAction: `This ${String(timeoutMs)} ms deadline has no runtime override. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`, +}); + +/** Shared command deadline. The setting, not a per-provider constant, owns the cap. */ +export const configurableDeadlineGuidance = ( + what: string, + admission: DebuggerAdmissionLimit, +): { + readonly userMessage: string; + readonly remediationAction: string; + readonly diagnostics: ReturnType; +} => { + const recovery = debuggerAdmissionRecovery(admission); + return { + userMessage: `${what} reached its ${String(admission.value)} ms deadline (deadlineExpired). ${recovery}`, + remediationAction: recovery, + diagnostics: debuggerAdmissionDiagnostics(admission), + }; +}; + +/** Capacity refusal whose projection carries the setting, value, source, and range. */ +export const debuggerResourceConstraintError = ( + operation: string, + resource: "memory" | "transport", + summary: string, + remediation: string, + admission: DebuggerAdmissionLimit, + reported: Readonly>, +): AnalysisResourceConstraintError => { + const facts = debuggerAdmissionDiagnostics(admission); + const recovery = debuggerAdmissionRecovery(admission); + return new AnalysisResourceConstraintError( + operation, + resource, + `${summary} ${recovery}`, + { ...facts, ...reported }, + { + remediationAction: `${remediation} ${recovery}`, + diagnostics: facts, + }, + ); +}; + const DETACH_TIMEOUT_MS = 1_000; const MAX_LATE_DEBUGGER_OUTPUT_BYTES = 64 * 1024; export type OwnedDebuggerDetachResult = diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts index cb3fc70941..050d9eb349 100644 --- a/src/rizin/RizinDebugSessionManager.ts +++ b/src/rizin/RizinDebugSessionManager.ts @@ -9,7 +9,6 @@ import { AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, - AnalysisResourceConstraintError, AnalysisTimeoutError, } from "../domain/analysisErrorCore.js"; import type { @@ -24,8 +23,18 @@ import { spawnOwnedProviderProcess, type SpawnedOwnedProviderProcess, } from "../process/ProviderProcess.js"; +import { + applyDebuggerAdmissionOverride, + debuggerControlReplyOutput, + resolveDebuggerSessionLimits, + type DebuggerAdmissionLimit, +} from "../config/debuggerSessionLimits.js"; import { closeOwnedDebuggerSessions, + configurableDeadlineGuidance, + debuggerAdmissionRecovery, + debuggerResourceConstraintError, + fixedDeadlineGuidance, stopOwnedDebuggerProcess, readLinuxProcessStartTime, verifyOwnedInferiorsEnded, @@ -35,22 +44,9 @@ import { import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js"; -const MAX_SESSIONS = 8; -const MAX_PENDING_COMMANDS = 16; -const FRAME_TIMEOUT_MS = 120_000; const CLEANUP_COMMAND_TIMEOUT_MS = 1_000; const STARTUP_TIMEOUT_MS = 30_000; - -/** Fixed debugger deadline. Callers cannot raise it in this release. */ -const fixedDeadlineGuidance = ( - what: string, - timeoutMs: number, -): { readonly userMessage: string; readonly remediationAction: string } => ({ - userMessage: `${what} reached its ${String(timeoutMs)} ms deadline (deadlineExpired). This release has no runtime override.`, - remediationAction: `This ${String(timeoutMs)} ms deadline has no runtime override. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`, -}); const MAX_FRAME_BYTES = 16 * 1024 * 1024; -const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024; const MAX_QUEUED_FRAME_BYTES = 4 * 1024 * 1024; const MAX_QUEUED_FRAMES = 4096; const MIN_QUEUED_FRAME_CHARGE_BYTES = 64; @@ -148,9 +144,14 @@ interface RizinCommandTimeoutEvidenceInput { readonly inferiorTty: string; readonly cleanupFailure: string | undefined; readonly launchProvenance: RizinLaunchProvenance; - readonly timeoutMs?: number; + readonly deadline?: DebuggerAdmissionLimit; } +export const rizinCommandTimeoutLimitation = ( + deadline: DebuggerAdmissionLimit, +): string => + `The command timed out before its completion marker at ${String(deadline.value)} ms (deadlineExpired). ${debuggerAdmissionRecovery(deadline)} Retained output is partial and completion is unknown.`; + export const createRizinCommandTimeoutEvidence = ({ sessionId, command, @@ -158,7 +159,7 @@ export const createRizinCommandTimeoutEvidence = ({ inferiorTty, cleanupFailure, launchProvenance, - timeoutMs = FRAME_TIMEOUT_MS, + deadline = resolveDebuggerSessionLimits({}).commandDeadline, }: RizinCommandTimeoutEvidenceInput): Evidence => createRizinCommandEvidence({ sessionId, @@ -167,7 +168,7 @@ export const createRizinCommandTimeoutEvidence = ({ inferiorTty, launchProvenance, limitations: [ - `The command timed out before its completion marker at ${String(timeoutMs)} ms (deadlineExpired). This release has no runtime override. Retained output is partial and completion is unknown.`, + rizinCommandTimeoutLimitation(deadline), ...(result.output_invalid_utf8 ? [ "Collected command frame bytes did not form valid UTF-8; displayed output contains replacement characters.", @@ -260,8 +261,12 @@ export class RizinDebugSessionManager { #closingAll = false; readonly #startDrainWaiters: Array<() => void> = []; readonly #command: string; + readonly #commandDeadline: DebuggerAdmissionLimit; readonly #frameTimeoutMs: number; readonly #startupTimeoutMs: number; + readonly #maxSessions: DebuggerAdmissionLimit; + readonly #maxPendingCommands: DebuggerAdmissionLimit; + readonly #commandOutput: DebuggerAdmissionLimit; readonly #maxCommandOutputBytes: number; readonly #platform: NodeJS.Platform; readonly #stopProcess: typeof stopOwnedDebuggerProcess; @@ -270,10 +275,15 @@ export class RizinDebugSessionManager { constructor( environment: Readonly = process.env, options: { + /** @internal Test seam. Production uses REA_DEBUGGER_COMMAND_DEADLINE_MS. */ readonly frameTimeoutMs?: number; /** @internal Test seam; production callers should use the 30 s default. */ readonly startupTimeoutMs?: number; - /** @internal Test seam; production callers should use the 16 MiB default. */ + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_SESSIONS. */ + readonly maxSessions?: number; + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_PENDING_COMMANDS. */ + readonly maxPendingCommands?: number; + /** @internal Test seam. Production uses REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES. */ readonly maxCommandOutputBytes?: number; readonly platform?: NodeJS.Platform; readonly stopProcess?: typeof stopOwnedDebuggerProcess; @@ -282,10 +292,26 @@ export class RizinDebugSessionManager { ) { this.#environment = { ...environment }; this.#command = environment.REA_RIZIN_COMMAND ?? "rizin"; - this.#frameTimeoutMs = options.frameTimeoutMs ?? FRAME_TIMEOUT_MS; + const limits = resolveDebuggerSessionLimits(this.#environment); + this.#commandDeadline = applyDebuggerAdmissionOverride( + limits.commandDeadline, + options.frameTimeoutMs, + ); + this.#frameTimeoutMs = this.#commandDeadline.value; this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS; - this.#maxCommandOutputBytes = - options.maxCommandOutputBytes ?? MAX_COMMAND_OUTPUT_BYTES; + this.#maxSessions = applyDebuggerAdmissionOverride( + limits.maxSessions, + options.maxSessions, + ); + this.#maxPendingCommands = applyDebuggerAdmissionOverride( + limits.maxPendingCommands, + options.maxPendingCommands, + ); + this.#commandOutput = applyDebuggerAdmissionOverride( + limits.maxCommandOutputBytes, + options.maxCommandOutputBytes, + ); + this.#maxCommandOutputBytes = this.#commandOutput.value; this.#platform = options.platform ?? process.platform; this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess; this.#spawnProcess = options.spawnProcess ?? spawnOwnedProviderProcess; @@ -316,15 +342,17 @@ export class RizinDebugSessionManager { ]), ); const occupied = this.#sessions.size + this.#sessionReservations; - if (occupied >= MAX_SESSIONS) + if (occupied >= this.#maxSessions.value) return err( - new AnalysisResourceConstraintError( + debuggerResourceConstraintError( "start_rizin_debug_session", "memory", - `Rizin session capacity reached (${String(MAX_SESSIONS)} sessions). This release has no runtime override.`, - { max_sessions: MAX_SESSIONS, active_or_reserved_sessions: occupied }, + `Rizin session capacity reached (${String(this.#maxSessions.value)} sessions).`, + "Close a Rizin debug session before starting another.", + this.#maxSessions, { - remediationAction: `Close a Rizin debug session before starting another. This capacity of ${String(MAX_SESSIONS)} sessions has no runtime override.`, + max_sessions: this.#maxSessions.value, + active_or_reserved_sessions: occupied, }, ), ); @@ -621,19 +649,18 @@ export class RizinDebugSessionManager { return err(new AnalysisInputError("rizin_debug_command")); if (signal?.aborted) return err(rizinCancellation("rizin_debug_command", signal)); - if (session.pendingCommandCount >= MAX_PENDING_COMMANDS) + if (session.pendingCommandCount >= this.#maxPendingCommands.value) return err( - new AnalysisResourceConstraintError( + debuggerResourceConstraintError( "rizin_debug_command", "transport", - `Rizin command queue capacity reached (${String(MAX_PENDING_COMMANDS)} pending commands). This release has no runtime override.`, + `Rizin command queue capacity reached (${String(this.#maxPendingCommands.value)} pending commands).`, + "Wait for a Rizin debug command on this session to finish, then retry.", + this.#maxPendingCommands, { - max_pending_commands: MAX_PENDING_COMMANDS, + max_pending_commands: this.#maxPendingCommands.value, active_or_queued_commands: session.pendingCommandCount, }, - { - remediationAction: `Wait for a Rizin debug command on this session to finish, then retry. This queue of ${String(MAX_PENDING_COMMANDS)} commands has no runtime override.`, - }, ), ); session.pendingCommandCount += 1; @@ -724,8 +751,8 @@ export class RizinDebugSessionManager { session, marker, this.#frameTimeoutMs, + this.#commandOutput, signal, - this.#maxCommandOutputBytes, ); commandCapture = received.ok ? received.value : received.error.capture; if (!received.ok) { @@ -760,7 +787,7 @@ export class RizinDebugSessionManager { ? [ rizinCaptureStoppedSentence( captured.outputLimit, - this.#maxCommandOutputBytes, + this.#commandOutput, cleanupFailure, ), ] @@ -854,7 +881,7 @@ export class RizinDebugSessionManager { ); } if (interruption?.kind === "timeout") { - session.protocolError = `Rizin command timed out before its completion marker at ${String(this.#frameTimeoutMs)} ms (deadlineExpired). This release has no runtime override. Output correlation is no longer safe.`; + session.protocolError = `Rizin command timed out before its completion marker at ${String(this.#commandDeadline.value)} ms (deadlineExpired). ${debuggerAdmissionRecovery(this.#commandDeadline)} Output correlation is no longer safe.`; const stopped = await this.#stopDebuggerOnly(session, true); const result = { ...commandResult(interruption.capture, "unknown"), @@ -865,10 +892,10 @@ export class RizinDebugSessionManager { command, result, inferiorTty: session.inferiorTty, + deadline: this.#commandDeadline, cleanupFailure: stopped.status === "incomplete" ? stopped.reason : undefined, launchProvenance: session.launchProvenance, - timeoutMs: this.#frameTimeoutMs, }); return this.#finalizeCommandObservation( session, @@ -914,9 +941,9 @@ export class RizinDebugSessionManager { this.#frameTimeoutMs, { ...errorOptions, - ...fixedDeadlineGuidance( + ...configurableDeadlineGuidance( "The Rizin debug command", - this.#frameTimeoutMs, + this.#commandDeadline, ), }, ); @@ -1401,8 +1428,7 @@ export class RizinDebugSessionManager { session, marker, CLEANUP_COMMAND_TIMEOUT_MS, - undefined, - this.#maxCommandOutputBytes, + debuggerControlReplyOutput, ); if (!captured.ok) throw captured.error.cause; if (captured.value.outputTruncated) @@ -1623,7 +1649,7 @@ const bufferedRizinFrame = (session: RizinDebugSession, truncated: boolean) => { const rizinCaptureStoppedSentence = ( limit: RizinOutputLimit | undefined, - maxCommandOutputBytes: number, + output: DebuggerAdmissionLimit, cleanupFailure: string | undefined, ): string => { const cap = @@ -1634,22 +1660,27 @@ const rizinCaptureStoppedSentence = ( : limit === "queued_frames" ? `the queued frame limit of ${String(MAX_QUEUED_FRAMES)} frames or ${String(MAX_QUEUED_FRAME_BYTES)} bytes` : limit === "command_output_bytes" - ? `the ${String(maxCommandOutputBytes)} byte output limit` + ? `the ${String(output.value)} byte output limit` : "a capture limit"; const cleanup = cleanupFailure === undefined ? " The owned debugger session was stopped." : ` The owned debugger session remains retained because cleanup could not be verified: ${cleanupFailure}`; - return `The Rizin command exceeded ${cap}. This release has no runtime override. Retained output is partial and command completion is unknown.${cleanup}`; + const recovery = + limit === "command_output_bytes" + ? ` ${debuggerAdmissionRecovery(output)}` + : " This release has no runtime override."; + return `The Rizin command exceeded ${cap}.${recovery} Retained output is partial and command completion is unknown.${cleanup}`; }; const waitForCommandFrames = async ( session: RizinDebugSession, marker: string, timeoutMs: number, + commandOutput: DebuggerAdmissionLimit, signal?: AbortSignal, - maxCommandOutputBytes = MAX_COMMAND_OUTPUT_BYTES, ): Promise> => { + const maxCommandOutputBytes = commandOutput.value; const deadline = Date.now() + timeoutMs; const output: string[] = []; let outputBytes = 0; @@ -1777,7 +1808,10 @@ const waitForCommandFrames = async ( const markerSeen = append(received); if (outputTruncated) { if (outputLimit === "command_output_bytes") - session.protocolError ??= `Rizin command output exceeded the ${String(maxCommandOutputBytes)} byte (16 MiB) command limit. This release has no runtime override.`; + session.protocolError ??= + commandOutput === debuggerControlReplyOutput + ? `Rizin control readback exceeded the ${String(maxCommandOutputBytes)} byte control-reply budget. That budget is separate from REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES and has no runtime override.` + : `Rizin command output exceeded the ${String(maxCommandOutputBytes)} byte command limit. ${debuggerAdmissionRecovery(commandOutput)}`; else if (outputLimit === "frame_bytes") session.protocolError ??= `Rizin emitted a NUL frame exceeding the ${String(MAX_FRAME_BYTES)} byte (16 MiB) protocol limit. This release has no runtime override.`; return ok(snapshot()); diff --git a/src/server/EvidenceMcpServer.test.ts b/src/server/EvidenceMcpServer.test.ts index a5b69e279e..ae8f130946 100644 --- a/src/server/EvidenceMcpServer.test.ts +++ b/src/server/EvidenceMcpServer.test.ts @@ -46,13 +46,17 @@ const expectFittingSdkValidationRecovery = (result: { }, details: { operation: "procedure_pseudo_code", - issues: [ - { - path: [], - reason: "invalid_value", - message: expect.stringContaining("required"), - }, - ], + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ["required"], + reason: "missing_argument", + expected: "string", + }), + expect.objectContaining({ + path: ["unexpected"], + reason: "unknown_argument", + }), + ]), }, }, }); diff --git a/src/server/EvidenceMcpServerCore.ts b/src/server/EvidenceMcpServerCore.ts index 185bbca158..f514f275a9 100644 --- a/src/server/EvidenceMcpServerCore.ts +++ b/src/server/EvidenceMcpServerCore.ts @@ -42,8 +42,11 @@ import type { EvidenceWriter } from "../application/investigation/InvestigationR import { AnalysisInputError, AnalysisResourceConstraintError, + type AnalysisInputIssue, } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { projectInputIssueGroupsWithinBudget } from "../domain/inputIssueProjection.js"; +import type { z } from "zod"; import { createEvidence } from "../domain/evidence.js"; import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js"; import { @@ -60,6 +63,46 @@ import { import { RequestTrackingMcpServer } from "./RequestTrackingMcpServer.js"; const currentRequestSignal = new AsyncLocalStorage(); +interface ToolValidationContext { + readonly tool: string; + validation?: { + readonly issues: readonly z.core.$ZodIssue[]; + readonly input: unknown; + }; +} +const currentToolValidation = new AsyncLocalStorage(); + +/** Capture structured issues from the canonical validation the SDK already performs. */ +const captureInputValidation = ( + tool: string, + schema: Schema, +): Schema => { + const standard = schema["~standard"]; + if (standard.vendor !== "zod") return schema; + return { + ...schema, + "~standard": { + ...standard, + validate: (value: unknown) => { + const context = currentToolValidation.getStore(); + const capture = ( + result: Awaited>, + ) => { + if (context?.tool === tool && result.issues !== undefined) + context.validation = { + issues: result.issues as readonly z.core.$ZodIssue[], + input: value, + }; + return result; + }; + const result = standard.validate(value); + return result instanceof Promise + ? result.then(capture) + : capture(result); + }, + }, + } as Schema; +}; /** Signal for the completion request currently being dispatched by the SDK. */ export const currentMcpRequestSignal = (): AbortSignal | undefined => @@ -93,6 +136,73 @@ const sdkInputValidationDiagnostic = ( return diagnostic.length === 0 ? undefined : diagnostic; }; +/** Place an SDK diagnostic at a field only when that same issue produced it. */ +const partitionSdkDiagnostic = ( + validation: ToolValidationContext["validation"], + groups: readonly (readonly AnalysisInputIssue[])[] | undefined, + diagnostic: string, +): + | { issues: readonly AnalysisInputIssue[]; remaining: string | undefined } + | undefined => { + if ( + validation === undefined || + groups === undefined || + validation.issues.length !== groups.length + ) + return undefined; + let offset = 0; + const consume = (text: string): boolean => { + if (!diagnostic.startsWith(text, offset)) return false; + offset += text.length; + return true; + }; + const mapped: AnalysisInputIssue[] = []; + const remaining: string[] = []; + for (let index = 0; index < validation.issues.length; index += 1) { + const raw = validation.issues[index]; + const group = groups[index]; + if (raw === undefined || group === undefined) return undefined; + if (index > 0 && !consume(", ")) return undefined; + const start = offset; + for (let part = 0; part < raw.path.length; part += 1) { + if (part > 0 && !consume(".")) return undefined; + if (!consume(String(raw.path[part]))) return undefined; + } + if (raw.path.length > 0 && !consume(": ")) return undefined; + if (!consume(raw.message)) return undefined; + const issue = group.length === 1 ? group[0] : undefined; + const sameField = + issue !== undefined && + raw.path.length === issue.path.length && + raw.path.every((part, i) => part === issue.path[i]); + const sameMessage = + issue?.message !== undefined && issue.message === raw.message; + const direct = + raw.code !== "invalid_union" && + raw.code !== "invalid_key" && + raw.code !== "unrecognized_keys"; + if ( + sameField && + issue !== undefined && + (sameMessage || (direct && issue.message === undefined)) + ) { + mapped.push( + issue.message === undefined + ? { ...issue, message: raw.message } + : issue, + ); + } else { + for (const issue of group) mapped.push(issue); + remaining.push(diagnostic.slice(start, offset)); + } + } + if (offset !== diagnostic.length) return undefined; + return { + issues: mapped, + remaining: remaining.length === 0 ? undefined : remaining.join(", "), + }; +}; + /** Replace the tool payload after the SDK codec while retaining its envelope. */ const replaceToolResultPayload = ( original: CallToolResult, @@ -295,8 +405,17 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { presentAdvertisedInputSchema(projected, { budgetBytes: budget }), ) : undefined; + const presentedSchema = inputSchema ?? config.inputSchema; const registeredConfig = - inputSchema === undefined ? config : { ...config, inputSchema }; + presentedSchema === undefined + ? config + : { + ...config, + inputSchema: captureInputValidation( + name, + presentedSchema as StandardSchemaWithJSON, + ), + }; const scoped = (input: never, context: never) => this.analysisSettings === undefined ? callback(input, context) @@ -351,6 +470,10 @@ class ErrorEvidenceTransport implements Transport { onmessage: Transport["onmessage"]; readonly hasPerRequestStream?: boolean; private readonly pendingTools = new Map(); + private readonly pendingValidation = new Map< + RequestId, + ToolValidationContext + >(); private readonly pendingToolLists = new Set(); private readonly pendingCompletions = new Map(); @@ -402,12 +525,16 @@ class ErrorEvidenceTransport implements Transport { return; } } + let validation: ToolValidationContext | undefined; if ( isJSONRPCRequest(message) && message.method === "tools/call" && typeof message.params?.["name"] === "string" - ) + ) { this.pendingTools.set(message.id, message.params["name"]); + validation = { tool: message.params["name"] }; + this.pendingValidation.set(message.id, validation); + } if (isJSONRPCRequest(message) && message.method === "tools/list") this.pendingToolLists.add(message.id); if (isJSONRPCRequest(message) && message.method === "completion/complete") @@ -419,19 +546,25 @@ class ErrorEvidenceTransport implements Transport { message.method === "notifications/cancelled" ) { const id = message.params?.["requestId"]; - if (typeof id === "string" || typeof id === "number") + if (typeof id === "string" || typeof id === "number") { + this.pendingValidation.delete(id); forgetPendingMcpRequest( this.pendingTools, this.pendingToolLists, this.pendingCompletions, id, ); + } } const completion = isJSONRPCRequest(message) && message.method === "completion/complete" ? this.pendingCompletions.get(message.id) : undefined; - if (completion === undefined) this.onmessage?.(message, extra); + if (validation !== undefined) + currentToolValidation.run(validation, () => + this.onmessage?.(message, extra), + ); + else if (completion === undefined) this.onmessage?.(message, extra); else currentRequestSignal.run(completion.signal, () => this.onmessage?.(message, extra), @@ -440,6 +573,7 @@ class ErrorEvidenceTransport implements Transport { transport.onerror = (error) => this.onerror?.(error); transport.onclose = () => { this.pendingTools.clear(); + this.pendingValidation.clear(); this.pendingToolLists.clear(); for (const completion of this.pendingCompletions.values()) completion.abort(); @@ -464,6 +598,7 @@ class ErrorEvidenceTransport implements Transport { await this.transport.close(); } finally { this.pendingTools.clear(); + this.pendingValidation.clear(); this.pendingToolLists.clear(); for (const completion of this.pendingCompletions.values()) completion.abort(); @@ -527,6 +662,7 @@ class ErrorEvidenceTransport implements Transport { } else await this.transport.send(advertised, options); } finally { if (!isJSONRPCRequest(message) && id !== undefined) { + this.pendingValidation.delete(id); forgetPendingMcpRequest( this.pendingTools, this.pendingToolLists, @@ -579,7 +715,13 @@ class ErrorEvidenceTransport implements Transport { ? encodeToolResult(structured, budget, "error-text", targetContent) : measureMcpResultWithinBudget(sdkResultWithMarker, budget); const normalized = encoded.ok - ? this.normalizeSdkInputError(message, tool, targetContent, sdkDiagnostic) + ? this.normalizeSdkInputError( + message, + tool, + targetContent, + sdkDiagnostic, + this.pendingValidation.get(message.id)?.validation, + ) : undefined; if ( normalized !== undefined && @@ -742,6 +884,7 @@ class ErrorEvidenceTransport implements Transport { tool: string | undefined, targetContent: boolean, text: string | undefined, + validation: ToolValidationContext["validation"], ): JSONRPCMessage | undefined { if ( tool === undefined || @@ -750,7 +893,6 @@ class ErrorEvidenceTransport implements Transport { !isCallToolResult(message.result) ) return undefined; - let error: AnalysisInputError | AnalysisResourceConstraintError; // The SDK owns counting and refusal before Standard Schema validation. // Recognize its exact wire refusal using our configured fact, without // reparsing the arguments or manufacturing a complete element count. @@ -758,7 +900,7 @@ class ErrorEvidenceTransport implements Transport { text === `Invalid arguments for tool ${tool}: arguments contain more than the maximum of ${String(this.maximumInputElements)} elements` ) { - error = new AnalysisResourceConstraintError( + const error = new AnalysisResourceConstraintError( tool, "transport", "The MCP tool arguments exceed the configured input element allowance; schema validation and tool execution did not start.", @@ -768,17 +910,51 @@ class ErrorEvidenceTransport implements Transport { "Reduce the combined array elements and object members in the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance for this workload.", }, ); - } else { - const diagnostic = sdkInputValidationDiagnostic(text, tool); - if (diagnostic === undefined) return undefined; - error = new AnalysisInputError( - tool, - { - userMessage: `The arguments for MCP tool ${tool} do not match its advertised input schema.`, - }, - [{ path: [], reason: "invalid_value", message: diagnostic }], - ); + const result = this.delivery.toErrorToolResult(error, targetContent); + return { + ...message, + result: replaceToolResultPayload(message.result, result), + }; } + const diagnostic = sdkInputValidationDiagnostic(text, tool); + if (diagnostic === undefined) return undefined; + const groups = + validation === undefined + ? undefined + : projectInputIssueGroupsWithinBudget( + validation.issues, + validation.input, + this.delivery.resultBudgetBytes, + ); + const projectionLimited = validation !== undefined && groups === undefined; + const summary = `The arguments for MCP tool ${tool} do not match its advertised input schema.`; + const partitioned = partitionSdkDiagnostic(validation, groups, diagnostic); + const issues = partitioned?.issues ?? groups?.flat(); + const error = new AnalysisInputError( + tool, + { + userMessage: + issues === undefined + ? projectionLimited + ? `${summary} Field-level issue projection exceeded its response-derived allowance.` + : summary + : partitioned !== undefined + ? partitioned.remaining === undefined + ? summary + : `${summary} ${partitioned.remaining}` + : `${summary} ${diagnostic}`, + }, + issues ?? [ + { + path: [], + reason: "invalid_value", + message: diagnostic, + ...(projectionLimited + ? { observed: { field_details: "omitted" } } + : {}), + }, + ], + ); const result = this.delivery.toErrorToolResult(error, targetContent); return { ...message, diff --git a/src/server/createServer.ts b/src/server/createServer.ts index cfd1cd7197..55f7271f79 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,7 +1,7 @@ +import { registerToolFamilies } from "../contracts/toolFamilyIds.js"; import { AnalysisSettings } from "../application/AnalysisSettings.js"; import { parseMcpInputBudget } from "../config/mcpInputBudget.js"; import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js"; -import type { DevicetreeService } from "../application/binaryDiagnostics/DevicetreeService.js"; import { registerDevicetreeTool } from "./registerDevicetreeTool.js"; import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; import { createGpuCodeObjectsService } from "../composition/gpu.js"; @@ -42,14 +42,6 @@ import { createBinaryLayoutService, pwntoolsHostSupported, } from "../composition/binaryDiagnostics.js"; -import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js"; -import type { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js"; -import type { KernelModuleService } from "../application/binaryDiagnostics/KernelModuleService.js"; -import type { PeLoadConfigService } from "../application/binaryDiagnostics/PeLoadConfigService.js"; -import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; -import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js"; -import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js"; -import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js"; import { registerElfNotesTool } from "./registerElfNotesTool.js"; import { registerAppleIpsTool } from "./registerAppleIpsTool.js"; import { registerKernelModuleTool } from "./registerKernelModuleTool.js"; @@ -198,7 +190,6 @@ import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; import { withAdmittedAnalysis, type ServerAnalysisSource, - type WithAdmittedAnalysis, } from "./analysisAdmission.js"; import { createReverseEngineeringService } from "../composition/reverseEngineering.js"; import { registerReverseEngineeringTools } from "./registerReverseEngineeringTools.js"; @@ -532,7 +523,6 @@ export const createServer = ( environment.REA_MCP_MAX_INPUT_ELEMENTS, ); server.analysisSettings = analysisSettings; - registerAnalysisSettingsTools(server, analysisSettings); if (options.mcpServer !== undefined) { server.attachRuntime( session === undefined @@ -670,124 +660,14 @@ export const createServer = ( recordEvidenceWithUnknown, } = createSessionRecorders(server, session); const analysisAdmission = withAdmittedAnalysis(source); - const toolContext: ServerToolContext = { - server, - session, - options: selectedOptions, - environment, - logger: toolLogger, - evidenceById, - activeTarget, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis: analysisAdmission, - artifactResources, - goBinary, - rustBinary, - erlangBeam, - il2cppMetadata, - peResources, - nativeExports, - devicetree, - elfNotes, - appleIps, - kernelModule, - peLoadConfig, - peCertificates, - peImports, - electronFuses, - pythonPyc, - webNetworkCapture, - }; - registerDesktopApplicationTool( - server, - desktopApplication, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerModelArtifactTools( - server, - modelArtifact, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerGpuCodeObjectsTools( - server, - gpuCodeObjects, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerBinaryAnalysisTools(toolContext); - registerReverseEngineeringTools( - server, - reverseEngineering, - toolLogger, - delivery, + const recordSessionEvidence = session === undefined ? undefined - : (evidence) => session.recordEvidence(evidence), - analysisAdmission, - session === undefined - ? undefined - : () => session.invalidateSnapshotForExternalMutation(), - session === undefined - ? undefined - : (path, signal, operation, rebindFailure) => - session.withAdmittedExternalMutation( - path, - signal, - operation, - rebindFailure, - ), - evidenceById, - ); - registerFridaTools( - server, - fridaInstrumentation, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - evidenceById, - ); - registerCutterTools( - server, - cutter, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - ); - registerGdbTools( - server, - gdbSessions, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - evidenceById, - ); - registerRizinDebugTools( - server, - rizinDebugSessions, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - ); + : ( + evidence: Parameters< + NonNullable["recordEvidence"] + >[0], + ) => session.recordEvidence(evidence); const previousOnclose = server.server.onclose; server.server.onclose = () => { previousOnclose?.(); @@ -808,82 +688,495 @@ export const createServer = ( if (failures.length > 1) throw new AggregateError(failures, "REA server cleanup failed"); }; - registerCaptureTools( - server, - packetCapture, - toolLogger, + const recordUnknown = + session === undefined + ? undefined + : (input: Parameters["recordUnknown"]>[0]) => + session.recordUnknown(input); + const analysisOptions = { + logger: toolLogger, + activeTarget, recordEvidence, - analysisAdmission, - ); - registerSignalTools( - server, - signalRecording, - toolLogger, - recordEvidence, - analysisAdmission, - ); - registerConfiguredAnalysisTools( - toolContext, - android, - wasmArtifact, - apkSignature, - evmInterface, - jeb, - firmwareAnalysis, - webRuntime, - ); - registerAdbTools( - server, - new AdbDeviceAnalysisService(adbDevice), - toolLogger, - recordEvidence, - analysisAdmission, - ); - registerPcapTools( - server, - new PcapAnalysisService(pcap), - toolLogger, - recordEvidence, - analysisAdmission, + recordUnknown, evidenceById, - ); - registerFlutterTools( - server, - new FlutterBuildAnalysisService(flutter), - toolLogger, + withAdmittedAnalysis: analysisAdmission, + }; + const evidenceOptions = { + logger: toolLogger, + activeTarget, recordEvidence, - analysisAdmission, - evidenceById, - ); - registerApktoolTools( - server, - new ApktoolResourceAnalysisService(apktool), - toolLogger, + withAdmittedAnalysis: analysisAdmission, + }; + const common = { + logger: toolLogger, recordEvidence, - analysisAdmission, - ); - registerObservationTools(toolContext); + withAdmittedAnalysis: analysisAdmission, + }; + registerToolFamilies({ + direct: () => registerOfficialTools(server, analysisOptions), + enhanced: () => + registerEnhancedTools(server, { + ...analysisOptions, + analysisProfile: + session === undefined + ? undefined + : (operation) => + session.analysisProfile() ?? session.analysisProfile(operation), + allowsSnapshotReplay: + session === undefined + ? undefined + : (operation) => session.allowsSnapshotReplay(operation), + recordWorkflowSnapshot: + session === undefined + ? undefined + : (input) => session.recordWorkflowSnapshot(input), + }), + native: () => { + registerNativeTools(server, { ...evidenceOptions, evidenceById }); + registerReverseEngineeringTools( + server, + reverseEngineering, + toolLogger, + delivery, + recordSessionEvidence, + analysisAdmission, + session === undefined + ? undefined + : () => session.invalidateSnapshotForExternalMutation(), + session === undefined + ? undefined + : (path, signal, operation, rebindFailure) => + session.withAdmittedExternalMutation( + path, + signal, + operation, + rebindFailure, + ), + evidenceById, + ); + registerCutterTools( + server, + cutter, + toolLogger, + delivery, + recordSessionEvidence, + session, + ); + registerGdbTools( + server, + gdbSessions, + toolLogger, + delivery, + recordSessionEvidence, + session, + evidenceById, + ); + registerRizinDebugTools( + server, + rizinDebugSessions, + toolLogger, + delivery, + recordSessionEvidence, + session, + ); + registerBinaryDiagnosticsTools( + server, + selectedOptions.binaryLayout ?? createBinaryLayoutService(environment), + createByteEntropyService(artifactResources), + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerPeResourcesTool( + server, + peResources, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerNativeExportsTool( + server, + nativeExports, + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerDevicetreeTool( + server, + devicetree, + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerElfNotesTool( + server, + elfNotes, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerAppleIpsTool( + server, + appleIps, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerKernelModuleTool( + server, + kernelModule, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeLoadConfigTool( + server, + peLoadConfig, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeCertificatesTool( + server, + peCertificates, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeImportsTool( + server, + peImports, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerRecordedCrashTools( + server, + selectedOptions.recordedCrash ?? + createRecordedCrashService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + }, + artifact: () => { + registerArtifactTools(server, { ...evidenceOptions, evidenceById }); + registerDesktopApplicationTool( + server, + desktopApplication, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerModelArtifactTools( + server, + modelArtifact, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerGpuCodeObjectsTools( + server, + gpuCodeObjects, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerEvmTools( + server, + evmInterface, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerGoTools( + server, + goBinary, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerIl2cppTools( + server, + il2cppMetadata, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerRustTools( + server, + rustBinary, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerPythonPycTool( + server, + pythonPyc, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerErlangTools( + server, + erlangBeam, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerWasmTools( + server, + wasmArtifact, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerApkSignatureTools( + server, + apkSignature, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerSqliteTools( + server, + selectedOptions.sqliteDatabase ?? + createSqliteDatabaseService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerCaptureTools( + server, + packetCapture, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerSignalTools( + server, + signalRecording, + toolLogger, + recordEvidence, + analysisAdmission, + ); + }, + managed: () => { + registerManagedTools(server, { ...evidenceOptions, evidenceById }); + if (session !== undefined) + registerManagedWorkflowTools(server, { + logger: toolLogger, + recordEvidence, + recordEvidenceWithUnknown, + session, + withAdmittedAnalysis: analysisAdmission, + }); + }, + firmware: () => + registerFirmwareTools( + server, + firmwareAnalysis, + toolLogger, + recordEvidence, + analysisAdmission, + ), + android: () => + registerAndroidTools( + server, + new AndroidAnalysisService(android, recordEvidence), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + pcap: () => + registerPcapTools( + server, + new PcapAnalysisService(pcap), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + flutter: () => + registerFlutterTools( + server, + new FlutterBuildAnalysisService(flutter), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + frida: () => + registerFridaTools( + server, + fridaInstrumentation, + toolLogger, + delivery, + recordSessionEvidence, + evidenceById, + ), + apktool: () => + registerApktoolTools( + server, + new ApktoolResourceAnalysisService(apktool), + toolLogger, + recordEvidence, + analysisAdmission, + ), + jeb: () => + registerJebTools( + server, + new JebAnalysisService(jeb), + toolLogger, + recordEvidence, + analysisAdmission, + ), + adb: () => + registerAdbTools( + server, + new AdbDeviceAnalysisService(adbDevice), + toolLogger, + recordEvidence, + analysisAdmission, + ), + browser: () => { + registerBrowserTools(server, { + ...common, + evidenceById, + browser: selectedOptions.browserObservation, + loadFailure: + selectedOptions.optionalProviderLoadFailures?.browserObservation, + }); + registerBrowserScenarioTool(server, { + ...common, + evidenceById, + provider: selectedOptions.browserScenarioCapture, + loadFailure: + selectedOptions.optionalProviderLoadFailures?.browserScenarioCapture, + }); + registerWebRuntimeTools( + server, + webRuntime, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerWebNetworkCaptureTool( + server, + webNetworkCapture, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + }, + electron: () => + registerElectronTools(server, { + ...common, + artifactResources, + environment, + evidenceById, + electron: selectedOptions.electronObservation, + electronActive: selectedOptions.electronActiveObservation, + electronFuses, + observationLoadFailure: + selectedOptions.optionalProviderLoadFailures?.electronObservation, + activeLoadFailure: + selectedOptions.optionalProviderLoadFailures + ?.electronActiveObservation, + }), + "javascript-runtime": () => + registerJavaScriptRuntimeObservationTools(server, { + ...common, + runtime: selectedOptions.javascriptRuntimeObservation, + loadFailure: + selectedOptions.optionalProviderLoadFailures + ?.javascriptRuntimeObservation, + }), + application: () => { + registerAnalysisSettingsTools(server, analysisSettings); + registerAnalysisViewTool( + server, + toolLogger, + evidenceById, + recordEvidence, + analysisAdmission, + artifactResources, + ); + registerWebScriptTool(server, { ...common, artifactResources }); + registerWebModuleTool( + server, + selectedOptions.webModuleTrace ?? + createWebModuleTraceService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerWebSourceLocationTool( + server, + selectedOptions.webSourceLocation ?? + createWebSourceLocationService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerJavaScriptRecoveryTool( + server, + new JavaScriptRecoveryService( + selectedOptions.javascriptRecovery ?? + createJavaScriptRecoveryProvider(environment), + ), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerApplicationTools(server, { + artifactResources, + evidenceById, + logger: toolLogger, + recordEvidence, + recordEvidenceWithUnknown, + withAdmittedAnalysis: analysisAdmission, + }); + }, + session: () => { + if (session === undefined) return; + registerSessionTools(server, session, toolLogger, { + ...selectedOptions, + artifactResources, + ...(availability === undefined + ? {} + : { availabilityPolicy: availability.policy }), + ...(availability?.evmLimiterAvailability === undefined + ? {} + : { evmLimiterAvailability: availability.evmLimiterAvailability }), + androidAnalysisAvailability: (signal) => + android.inspectAvailability(signal), + adbAnalysisAvailability: (signal) => + adbDevice.inspectAvailability(signal), + apktoolAnalysisAvailability: (signal) => + apktool.inspectAvailability(signal), + jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal), + startedAt, + }); + }, + }); registerGuidedPrompts(server, analysis, session); - if (session !== undefined) { - registerSessionTools(server, session, toolLogger, { - ...selectedOptions, - artifactResources, - ...(availability === undefined - ? {} - : { availabilityPolicy: availability.policy }), - ...(availability?.evmLimiterAvailability === undefined - ? {} - : { evmLimiterAvailability: availability.evmLimiterAvailability }), - androidAnalysisAvailability: (signal) => - android.inspectAvailability(signal), - adbAnalysisAvailability: (signal) => - adbDevice.inspectAvailability(signal), - apktoolAnalysisAvailability: (signal) => - apktool.inspectAvailability(signal), - jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal), - startedAt, - }); - } return server; }; @@ -919,263 +1212,6 @@ const selectCompactInputSchemaPresentation = ( return compactPresentationForMcpInputSchemaProfile(configured.value); }; -const registerConfiguredAnalysisTools = ( - { - server, - options, - environment, - logger: toolLogger, - evidenceById, - recordEvidence, - withAdmittedAnalysis, - goBinary, - rustBinary, - erlangBeam, - il2cppMetadata, - peResources, - nativeExports, - devicetree, - elfNotes, - appleIps, - kernelModule, - peLoadConfig, - peCertificates, - peImports, - pythonPyc, - webNetworkCapture, - artifactResources, - }: ServerToolContext, - android: AndroidAnalysisPort, - wasmArtifact: Pick, - apkSignature: Pick, - evmInterface: EvmInterfaceService, - jeb: JebAnalysisPort, - firmwareAnalysis: FirmwareAnalysisService, - webRuntime: WebRuntimeService, -): void => { - registerAndroidTools( - server, - new AndroidAnalysisService(android, recordEvidence), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerJebTools( - server, - new JebAnalysisService(jeb), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerBinaryDiagnosticsTools( - server, - options.binaryLayout ?? createBinaryLayoutService(environment), - createByteEntropyService(artifactResources), - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerAnalysisViewTool( - server, - toolLogger, - evidenceById, - recordEvidence, - withAdmittedAnalysis, - artifactResources, - ); - registerPeResourcesTool( - server, - peResources, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerNativeExportsTool( - server, - nativeExports, - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerDevicetreeTool( - server, - devicetree, - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerElfNotesTool( - server, - elfNotes, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerAppleIpsTool( - server, - appleIps, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerKernelModuleTool( - server, - kernelModule, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeLoadConfigTool( - server, - peLoadConfig, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeCertificatesTool( - server, - peCertificates, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeImportsTool( - server, - peImports, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerGoTools( - server, - goBinary, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerIl2cppTools( - server, - il2cppMetadata, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerRustTools( - server, - rustBinary, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerPythonPycTool( - server, - pythonPyc, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerErlangTools( - server, - erlangBeam, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerWasmTools( - server, - wasmArtifact, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerApkSignatureTools( - server, - apkSignature, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerEvmTools( - server, - evmInterface, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerSqliteTools( - server, - options.sqliteDatabase ?? createSqliteDatabaseService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerRecordedCrashTools( - server, - options.recordedCrash ?? createRecordedCrashService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerFirmwareTools( - server, - firmwareAnalysis, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebModuleTool( - server, - options.webModuleTrace ?? createWebModuleTraceService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebSourceLocationTool( - server, - options.webSourceLocation ?? createWebSourceLocationService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebRuntimeTools( - server, - webRuntime, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebNetworkCaptureTool( - server, - webNetworkCapture, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerJavaScriptRecoveryTool( - server, - new JavaScriptRecoveryService( - options.javascriptRecovery ?? - createJavaScriptRecoveryProvider(environment), - ), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); -}; - const createSessionRecorders = ( server: EvidenceMcpServer, session: BinarySessionPort | undefined, @@ -1200,146 +1236,6 @@ const createSessionRecorders = ( ) => session.recordEvidenceWithUnknown(evidence, input), }); -interface ServerToolContext extends ReturnType { - readonly goBinary: GoBinaryService; - readonly rustBinary: RustBinaryService; - readonly erlangBeam: BeamService; - readonly il2cppMetadata: Il2cppMetadataService; - readonly peResources: PeResourcesService; - readonly nativeExports: NativeExportsService; - readonly devicetree: DevicetreeService; - readonly elfNotes: ElfNotesService; - readonly appleIps: AppleIpsService; - readonly kernelModule: KernelModuleService; - readonly peLoadConfig: PeLoadConfigService; - readonly peCertificates: PeCertificatesService; - readonly peImports: PeImportsService; - readonly electronFuses: ElectronFuseService; - readonly pythonPyc: PythonPycService; - readonly webNetworkCapture: WebNetworkCaptureService; - readonly artifactResources: ArtifactResourceScope; - readonly server: EvidenceMcpServer; - readonly session: BinarySessionPort | undefined; - readonly options: CreateServerOptions; - readonly environment: Readonly; - readonly logger: Logger; - readonly withAdmittedAnalysis: WithAdmittedAnalysis; -} - -const registerBinaryAnalysisTools = ({ - server, - session, - logger, - activeTarget, - recordEvidence, - recordEvidenceWithUnknown, - evidenceById, - withAdmittedAnalysis, -}: ServerToolContext): void => { - const recordUnknown = - session === undefined - ? undefined - : (input: Parameters[0]) => - session.recordUnknown(input); - const analysisOptions = { - logger, - activeTarget, - recordEvidence, - recordUnknown, - evidenceById, - withAdmittedAnalysis, - }; - registerOfficialTools(server, analysisOptions); - registerEnhancedTools(server, { - ...analysisOptions, - analysisProfile: - session === undefined - ? undefined - : (operation) => - session.analysisProfile() ?? session.analysisProfile(operation), - allowsSnapshotReplay: - session === undefined - ? undefined - : (operation) => session.allowsSnapshotReplay(operation), - recordWorkflowSnapshot: - session === undefined - ? undefined - : (input) => session.recordWorkflowSnapshot(input), - }); - const evidenceOptions = { - logger, - activeTarget, - recordEvidence, - withAdmittedAnalysis, - }; - registerNativeTools(server, { ...evidenceOptions, evidenceById }); - registerArtifactTools(server, { ...evidenceOptions, evidenceById }); - registerManagedTools(server, { ...evidenceOptions, evidenceById }); - if (session !== undefined) - registerManagedWorkflowTools(server, { - logger, - recordEvidence, - recordEvidenceWithUnknown, - session, - withAdmittedAnalysis, - }); -}; - -const registerObservationTools = ({ - electronFuses, - evidenceById, - server, - options, - logger, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis, - artifactResources, - environment, -}: ServerToolContext): void => { - const common = { logger, recordEvidence, withAdmittedAnalysis }; - registerWebScriptTool(server, { ...common, artifactResources }); - registerBrowserTools(server, { - ...common, - evidenceById, - browser: options.browserObservation, - loadFailure: options.optionalProviderLoadFailures?.browserObservation, - }); - registerBrowserScenarioTool(server, { - ...common, - evidenceById, - provider: options.browserScenarioCapture, - loadFailure: options.optionalProviderLoadFailures?.browserScenarioCapture, - }); - registerElectronTools(server, { - ...common, - artifactResources, - environment, - evidenceById, - electron: options.electronObservation, - electronActive: options.electronActiveObservation, - electronFuses, - observationLoadFailure: - options.optionalProviderLoadFailures?.electronObservation, - activeLoadFailure: - options.optionalProviderLoadFailures?.electronActiveObservation, - }); - registerJavaScriptRuntimeObservationTools(server, { - ...common, - runtime: options.javascriptRuntimeObservation, - loadFailure: - options.optionalProviderLoadFailures?.javascriptRuntimeObservation, - }); - registerApplicationTools(server, { - artifactResources, - evidenceById, - logger, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis, - }); -}; - const selectAdvertisedToolFamilies = ( environment: Readonly, ): ReadonlySet | undefined => { diff --git a/src/server/externalMutationAdmission.ts b/src/server/externalMutationAdmission.ts index b4ebd98e66..1fcc40c3d1 100644 --- a/src/server/externalMutationAdmission.ts +++ b/src/server/externalMutationAdmission.ts @@ -1,6 +1,10 @@ import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCancelledError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; +import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { Evidence } from "../domain/evidence.js"; import { err, type Result } from "../domain/result.js"; @@ -10,6 +14,22 @@ export type ExternalMutationAdmission = Pick< "withAdmittedExternalMutation" >; +/** Retain the completed command outcome while reporting a later cancellation. */ +export const retainExternalMutationCancellation = ( + value: Result, + cancellation: AnalysisCancelledError, + evidenceOf?: (value: Value) => Evidence, + retainedDetails?: (value: Value) => Readonly>, +): AnalysisError => { + if (!value.ok) + return analysisErrorWithCancellation(value.error, cancellation); + if (evidenceOf !== undefined) + cancellation.retainPartialObservation(evidenceOf(value.value)); + if (retainedDetails !== undefined) + cancellation.retainDiagnostics(retainedDetails(value.value)); + return cancellation; +}; + /** Shared MCP admission and failure preservation for unrestricted commands. */ export const runWithExternalMutationAdmission = async ( session: ExternalMutationAdmission | undefined, @@ -31,8 +51,16 @@ export const runWithExternalMutationAdmission = async ( undefined, signal, operation, - (value, cause) => - value.ok + (value, cause) => { + if (cause instanceof AnalysisCancelledError) { + return retainExternalMutationCancellation( + value, + cause, + evidenceOf, + retainedDetails, + ); + } + return value.ok ? new AnalysisOutputError( operationName, "The external command may have changed the bound artifact, but the active analysis session could not be retired.", @@ -41,9 +69,6 @@ export const runWithExternalMutationAdmission = async ( ...(evidenceOf === undefined ? {} : { partialObservation: evidenceOf(value.value) }), - ...(cause.cleanup === undefined - ? {} - : { cleanup: cause.cleanup }), ...(cause.capturedOutput === undefined ? {} : { capturedOutput: cause.capturedOutput }), @@ -54,7 +79,8 @@ export const runWithExternalMutationAdmission = async ( ) : value.error.retainSecondaryFailure(cause, { retirement_error: projectAnalysisError(cause), - }), + }); + }, operationName, cleanupRecovery, ); diff --git a/src/server/mcpProgress.test.ts b/src/server/mcpProgress.test.ts new file mode 100644 index 0000000000..17ab8a9807 --- /dev/null +++ b/src/server/mcpProgress.test.ts @@ -0,0 +1,42 @@ +import { expect, it, vi } from "vitest"; +import { mcpProgressReporter, type McpProgressContext } from "./mcpProgress.js"; + +it("emits monotonic MCP progress when a composed workflow starts another phase", async () => { + const notify = vi.fn().mockResolvedValue(undefined); + const reporter = mcpProgressReporter({ + mcpReq: { _meta: { progressToken: "token" }, notify }, + } as unknown as McpProgressContext); + await reporter.report({ + phase: "native", + completed: 1, + total: 1, + message: "complete", + terminal: true, + }); + await reporter.report({ + phase: "reconcile", + completed: 0, + total: null, + message: "examined", + terminal: true, + }); + expect(notify.mock.calls.map(([message]) => message.params.progress)).toEqual( + [1, 2], + ); + expect(notify.mock.calls[1]?.[0].params.message).toBe("reconcile: examined"); +}); + +it("does not emit progress without a negotiated token", async () => { + const notify = vi.fn(); + const reporter = mcpProgressReporter({ + mcpReq: { _meta: undefined, notify }, + } as unknown as McpProgressContext); + await reporter.report({ + phase: "native", + completed: 1, + total: 1, + message: "complete", + terminal: true, + }); + expect(notify).not.toHaveBeenCalled(); +}); diff --git a/src/server/mcpProgress.ts b/src/server/mcpProgress.ts index a3fd4fde0b..41363777c3 100644 --- a/src/server/mcpProgress.ts +++ b/src/server/mcpProgress.ts @@ -15,7 +15,7 @@ export const mcpProgressReporter = ( ): ProgressReporter => { const token = context.mcpReq._meta?.progressToken; if (token === undefined) return silentProgressReporter; - return createProgressReporter( + const reporter = createProgressReporter( async (update) => { try { await context.mcpReq.notify({ @@ -36,4 +36,11 @@ export const mcpProgressReporter = ( }, { minimumIntervalMs: 100 }, ); + return { + // Provider child operations can each complete before their enclosing + // workflow does. MCP progress uses the emitted sequence, not these local + // phase counts, so reset them before the shared monotonic check. + report: (update) => + reporter.report({ ...update, completed: 0, total: null }), + }; }; diff --git a/src/server/registerApplicationTools.ts b/src/server/registerApplicationTools.ts index f9d6450ab3..a68f218579 100644 --- a/src/server/registerApplicationTools.ts +++ b/src/server/registerApplicationTools.ts @@ -10,6 +10,7 @@ import { registerEnvironmentBundleTool } from "./registerApplicationTools/enviro import { registerReconstructionObligationLedgerTool } from "./registerApplicationTools/obligationLedger.js"; import { registerProjectMobileApplicationGraphTools } from "./registerApplicationTools/projectMobileApplicationGraph.js"; import type { ApplicationToolRegistration } from "./registerApplicationTools/types.js"; +import { registerGuidanceTools } from "./registerGuidanceTools.js"; import { registerGuidedWorkflowTool } from "./registerGuidedWorkflowTool.js"; /** Register provider-neutral JavaScript application graph workflows. */ @@ -17,6 +18,7 @@ export const registerApplicationTools = ( server: EvidenceMcpServer, options: ApplicationToolRegistration, ): void => { + registerGuidanceTools(server, options.artifactResources); registerGuidedWorkflowTool(server); registerTraceFeatureTool(server, options); registerTraceJavaScriptSemanticsTool(server, options); diff --git a/src/server/registerApplicationTools/types.ts b/src/server/registerApplicationTools/types.ts index a0979aae58..802900dfa9 100644 --- a/src/server/registerApplicationTools/types.ts +++ b/src/server/registerApplicationTools/types.ts @@ -9,7 +9,7 @@ import type { WithAdmittedAnalysis } from "../analysisAdmission.js"; /** Shared services for registering JavaScript application graph workflows. */ export interface ApplicationToolRegistration { - readonly artifactResources?: ArtifactResourceScope; + readonly artifactResources: ArtifactResourceScope; readonly withAdmittedAnalysis?: WithAdmittedAnalysis; readonly logger: Logger; readonly evidenceById: EvidenceReader["evidenceById"] | undefined; diff --git a/src/server/registerGuidanceTools.ts b/src/server/registerGuidanceTools.ts new file mode 100644 index 0000000000..304da79a6e --- /dev/null +++ b/src/server/registerGuidanceTools.ts @@ -0,0 +1,45 @@ +import { + readGuidanceSection, + searchGuidance, +} from "../application/GuidanceAccess.js"; +import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import { jsonValueSchema } from "../domain/jsonValue.js"; +import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; + +/** Register provider-free search and retrieval over the installed guide bundle. */ +export const registerGuidanceTools = ( + server: EvidenceMcpServer, + resources: ArtifactResourceScope, +): void => { + const search = toolContract("search_guidance"); + server.registerTool( + search.name, + applicationToolRegistrationOptions(search), + async (input) => { + const result = await searchGuidance(input, resources); + return server.delivery.toCallToolResult( + result.ok + ? { ok: true, value: jsonValueSchema.parse(result.value) } + : result, + search, + ); + }, + ); + + const read = toolContract("read_guidance_section"); + server.registerTool( + read.name, + applicationToolRegistrationOptions(read), + async (input) => { + const result = await readGuidanceSection(input, resources); + return server.delivery.toCallToolResult( + result.ok + ? { ok: true, value: jsonValueSchema.parse(result.value) } + : result, + read, + ); + }, + ); +}; diff --git a/src/server/registerReverseEngineeringTools.ts b/src/server/registerReverseEngineeringTools.ts index 4e0ea35ef8..6dc724b648 100644 --- a/src/server/registerReverseEngineeringTools.ts +++ b/src/server/registerReverseEngineeringTools.ts @@ -14,7 +14,11 @@ import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import type { ToolResultDelivery } from "./ToolResultDelivery.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; -import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCancelledError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; +import { retainExternalMutationCancellation } from "./externalMutationAdmission.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { Result } from "../domain/result.js"; @@ -31,7 +35,7 @@ export const registerReverseEngineeringTools = ( path: string | undefined, signal: AbortSignal | undefined, operation: () => Promise>, - retirementFailure: ( + completionFailure: ( value: Result, error: AnalysisError, ) => AnalysisError, @@ -98,6 +102,13 @@ export const registerReverseEngineeringTools = ( ); }, (value, cause) => { + if (cause instanceof AnalysisCancelledError) { + return retainExternalMutationCancellation( + value, + cause, + (evidence) => evidence, + ); + } if (!value.ok) return value.error.retainSecondaryFailure(cause, { retirement_error: projectAnalysisError(cause), @@ -108,9 +119,6 @@ export const registerReverseEngineeringTools = ( "Rizin may have changed the bound artifact, but the active analysis session could not be retired.", { cause, - ...(cause.cleanup === undefined - ? {} - : { cleanup: cause.cleanup }), ...(cause.capturedOutput === undefined ? {} : { capturedOutput: cause.capturedOutput }), diff --git a/src/wasm/WabtAdmissionError.ts b/src/wasm/WabtAdmissionError.ts new file mode 100644 index 0000000000..4a48ad38b8 --- /dev/null +++ b/src/wasm/WabtAdmissionError.ts @@ -0,0 +1,47 @@ +const WABT_ADMISSION_CODES = [ + "incomplete_string", + "unsupported_escape", + "incomplete_comment", + "malformed_form", + "name_missing", + "incomplete_import", + "incomplete_export", + "unsupported_section_header", + "unrecognized_section_row", + "blank_custom_name", + "custom_header_mismatch", + "incomplete_section_row", + "section_ranges", + "custom_inventory_mismatch", + "section_coverage", + "incomplete_name_metadata", + "name_subsection_order", + "incomplete_module_name", + "unfinished_module_name", + "unsupported_module_prefix", + "unexpected_string", + "unexpected_second_module", + "unsupported_custom_name", + "unsupported_name_payload", + "unbalanced_output", + "unexpected_text", + "incomplete_output", + "directory_not_absolute", + "incomplete_tool_profile", +] as const; + +export type WabtAdmissionCode = (typeof WABT_ADMISSION_CODES)[number]; + +/** WABT output or tool-profile admission failure. `code` is the stable identity. */ +export class WabtAdmissionError extends Error { + readonly _tag = "WabtAdmissionError"; + + constructor( + readonly code: WabtAdmissionCode, + readonly detail?: Readonly>, + options?: ErrorOptions, + ) { + super(code, options); + this.name = "WabtAdmissionError"; + } +} diff --git a/src/wasm/WabtArtifactProvider.test.ts b/src/wasm/WabtArtifactProvider.test.ts index 61b9da8665..6efb08d4d3 100644 --- a/src/wasm/WabtArtifactProvider.test.ts +++ b/src/wasm/WabtArtifactProvider.test.ts @@ -436,7 +436,12 @@ it("returns actionable unavailable without creating a workspace when tools are a ok: false, error: { _tag: "AnalysisCapabilityUnavailableError", - userMessage: expect.stringContaining("REA_WABT_BIN_DIRECTORY"), + reason: "directory_not_absolute", + userMessage: "WABT configuration cannot be used", + diagnostics: { + code: "directory_not_absolute", + setting: "REA_WABT_BIN_DIRECTORY", + }, }, }); }); diff --git a/src/wasm/WabtArtifactProvider.ts b/src/wasm/WabtArtifactProvider.ts index 6fde4aa261..0b7abc613f 100644 --- a/src/wasm/WabtArtifactProvider.ts +++ b/src/wasm/WabtArtifactProvider.ts @@ -1,3 +1,4 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; import { randomUUID, createHash } from "node:crypto"; import { access, writeFile } from "node:fs/promises"; import { constants } from "node:fs"; @@ -221,9 +222,9 @@ export class WabtArtifactProvider implements WasmArtifactPort { }); const directory = environment.REA_WABT_BIN_DIRECTORY ?? ""; if (!isAbsolute(directory)) - throw new Error( - "REA_WABT_BIN_DIRECTORY must be an absolute tool directory.", - ); + throw new WabtAdmissionError("directory_not_absolute", { + setting: "REA_WABT_BIN_DIRECTORY", + }); const commands: { tool: "wasm-validate" | "wasm-objdump" | "wasm2wat"; path: string; @@ -466,7 +467,7 @@ export class WabtArtifactProvider implements WasmArtifactPort { objdump === undefined || decode === undefined ) - throw new Error("Incomplete WABT tool profile."); + throw new WabtAdmissionError("incomplete_tool_profile"); const validation = await execute(validate, validate.arguments, true); if (validation.exitCode !== 0) throw new AnalysisInputError( diff --git a/src/wasm/WabtComment.ts b/src/wasm/WabtComment.ts index 0224e25ba3..6170efeb69 100644 --- a/src/wasm/WabtComment.ts +++ b/src/wasm/WabtComment.ts @@ -1,3 +1,5 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; + /** Skip a WAT comment without interpreting its punctuation as forms or strings. */ export const watCommentEnd = ( text: string, @@ -24,5 +26,5 @@ export const watCommentEnd = ( index++; } } - throw new Error("Incomplete WABT WAT comment."); + throw new WabtAdmissionError("incomplete_comment"); }; diff --git a/src/wasm/WabtFailures.ts b/src/wasm/WabtFailures.ts index 7348621b18..27ba86a1bc 100644 --- a/src/wasm/WabtFailures.ts +++ b/src/wasm/WabtFailures.ts @@ -27,6 +27,7 @@ import { RuntimeControlUnavailableError, runtimeControlCapabilityError, } from "../process/RuntimeControlUnavailableError.js"; +import { WabtAdmissionError } from "./WabtAdmissionError.js"; import { DEFAULT_WASM_COMMAND_TIMEOUT_MS, MAX_WASM_COMMAND_TIMEOUT_MS, @@ -237,18 +238,31 @@ const classifyWabtFailure = ( remediationAction: `The WABT tool allowance is ${String(WABT_LIMITS.toolBytes)} bytes. This release has no runtime override. command_timeout_ms and REA_WABT_BIN_DIRECTORY do not raise it. Select tools within the allowance. Do not repeat the unchanged call.`, }, ); - if (phase === "configuration") + if (phase === "configuration") { + const admission = cause instanceof WabtAdmissionError ? cause : undefined; return new AnalysisCapabilityUnavailableError( WABT_PROVIDER_IDENTITY.id, "inspect_wasm_artifact", - reason, + admission?.code ?? reason, { cause, ...artifactOptions, - userMessage: reason, + ...(admission === undefined + ? {} + : { + diagnostics: { + code: admission.code, + ...admission.detail, + }, + }), + userMessage: + admission === undefined + ? reason + : "WABT configuration cannot be used", remediationAction: wabtConfigurationRecoveryAction(cause), }, ); + } if ( phase === "artifact" && cause instanceof ArtifactReaderFailure && diff --git a/src/wasm/WabtInterface.test.ts b/src/wasm/WabtInterface.test.ts index 07cb08ac69..72e3a2253f 100644 --- a/src/wasm/WabtInterface.test.ts +++ b/src/wasm/WabtInterface.test.ts @@ -217,11 +217,19 @@ it("rejects unfinished nested comments rather than projecting a truncated interf "(module (type (func (param (; unfinished ) i32))))", "(module) (; outer (; inner ;) still open", ]) - expect(() => parseWabtWat(source)).toThrow("Incomplete WABT WAT comment"); + expect(() => parseWabtWat(source)).toThrow( + expect.objectContaining({ + _tag: "WabtAdmissionError", + code: "incomplete_comment", + }), + ); }); it("rejects a second module after a CR-terminated line comment", () => { expect(() => parseWabtWat("(module) ;; comment\r(module)")).toThrow( - "Unexpected second WABT WAT module", + expect.objectContaining({ + _tag: "WabtAdmissionError", + code: "unexpected_second_module", + }), ); }); diff --git a/src/wasm/WabtInterface.ts b/src/wasm/WabtInterface.ts index 9ba9260cc4..fc45d50c31 100644 --- a/src/wasm/WabtInterface.ts +++ b/src/wasm/WabtInterface.ts @@ -1,3 +1,4 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { wasmInterfaceEntryRepresentationBytes } from "../domain/wasm/wasmInterface.js"; import { WasmInterfaceBudget } from "./WasmInterfaceBudget.js"; @@ -47,7 +48,7 @@ const parts = (form: string): string[] => { if (items[0] === "func" && items.length > 1 && !atom.startsWith("$")) break; } - if (i <= start) throw new Error("Malformed WABT form"); + if (i <= start) throw new WabtAdmissionError("malformed_form"); budget.reserve(64 + (i - start) * 2); items.push(form.slice(start, i)); } @@ -91,7 +92,7 @@ const functionType = ( }; const name = (text: string | undefined): string => { if (text?.startsWith('"') !== true) - throw new Error("WABT interface name is missing"); + throw new WabtAdmissionError("name_missing"); return new TextDecoder("utf8", { fatal: true, ignoreBOM: true }).decode( watString(text, 1).bytes, ); @@ -252,7 +253,7 @@ const decodeWabtInterface = ( const p = parts(form); if (p[0] !== "import") continue; const body = p[3]; - if (body === undefined) throw new Error("Incomplete WABT import"); + if (body === undefined) throw new WabtAdmissionError("incomplete_import"); const observed = addDefinition(body); const entry: WasmInterfaceEntry = { direction: "import", @@ -280,7 +281,7 @@ const decodeWabtInterface = ( const p = parts(form); if (p[0] !== "export") continue; const body = p[2]; - if (body === undefined) throw new Error("Incomplete WABT export"); + if (body === undefined) throw new WabtAdmissionError("incomplete_export"); const descriptor = parts(body); const rawKind = descriptor[0] ?? "unknown"; const kind = ["func", "table", "memory", "global", "tag"].includes(rawKind) diff --git a/src/wasm/WabtOutput.test.ts b/src/wasm/WabtOutput.test.ts index fe4f0faaeb..fdfb420ed2 100644 --- a/src/wasm/WabtOutput.test.ts +++ b/src/wasm/WabtOutput.test.ts @@ -1,5 +1,8 @@ import { readFile } from "node:fs/promises"; import { expect, it } from "vitest"; + +const refused = (code: string) => + expect.objectContaining({ _tag: "WabtAdmissionError", code }); import { parseWabtOutput } from "./WabtOutput.js"; import { parseWabtWat } from "./WabtWat.js"; import { associateWasmGlue } from "./WasmGlueAdapter.js"; @@ -28,10 +31,10 @@ it("corroborates a blank custom name with bounded invalid UTF-8 bytes", () => { valid[11] = 0x61; valid[12] = 0x62; expect(() => parseWabtOutput(dump, valid.length, [], [], valid)).toThrow( - /not corroborated/u, + refused("blank_custom_name"), ); expect(() => parseWabtOutput(dump, bytes.length, [])).toThrow( - /not corroborated/u, + refused("blank_custom_name"), ); const mixed = Buffer.concat([bytes, Buffer.from("00050174616263", "hex")]); const mixedDump = dump.replace( @@ -57,19 +60,21 @@ it("retains real WABT section ranges, custom section identity and original heade bytes: 12, description: '"test"', }); - expect(() => parseWabtOutput(text, 75, ["test"])).toThrow(/ranges/u); + expect(() => parseWabtOutput(text, 75, ["test"])).toThrow( + refused("section_ranges"), + ); expect(() => parseWabtOutput(text.replace("size=0x0000000c", "size=0x0000000d"), 76, [ "test", ]), - ).toThrow(/ranges/u); + ).toThrow(refused("section_ranges")); expect(() => parseWabtOutput( text.replace("\nSection Details:\n", "unexpected\nSection Details:\n"), 76, ["test"], ), - ).toThrow(/Unrecognized/u); + ).toThrow(refused("unrecognized_section_row")); expect(() => parseWabtOutput( text.replace( @@ -79,7 +84,7 @@ it("retains real WABT section ranges, custom section identity and original heade 76, ["test"], ), - ).toThrow(/Unrecognized/u); + ).toThrow(refused("unrecognized_section_row")); }); const dataCountModuleBytes = 0x3d; const unpaddedDataCountRow = @@ -126,14 +131,14 @@ it("accepts an unpadded DataCount row and still rejects malformed output and bad dataCountModuleBytes, [], ), - ).toThrow(/Unrecognized/u); + ).toThrow(refused("unrecognized_section_row")); expect(() => parseWabtOutput( dataCountDump.replace(unpaddedDataCountRow, "Sections:"), dataCountModuleBytes, [], ), - ).toThrow(/Unrecognized/u); + ).toThrow(refused("unrecognized_section_row")); expect(() => parseWabtOutput( dataCountDump.replace( @@ -143,14 +148,14 @@ it("accepts an unpadded DataCount row and still rejects malformed output and bad dataCountModuleBytes, [], ), - ).toThrow(/Unrecognized/u); + ).toThrow(refused("unrecognized_section_row")); expect(() => parseWabtOutput( dataCountDump.replace("(size=0x00000001)", "(size=0x00000002)"), dataCountModuleBytes, [], ), - ).toThrow(/ranges/u); + ).toThrow(refused("section_ranges")); expect(() => parseWabtOutput( dataCountDump.replace( @@ -160,10 +165,10 @@ it("accepts an unpadded DataCount row and still rejects malformed output and bad dataCountModuleBytes, [], ), - ).toThrow(/ranges/u); + ).toThrow(refused("section_ranges")); expect(() => parseWabtOutput(dataCountDump, dataCountModuleBytes - 1, []), - ).toThrow(/ranges/u); + ).toThrow(refused("section_ranges")); expect(() => parseWabtOutput( dataCountDump.replace( @@ -173,7 +178,7 @@ it("accepts an unpadded DataCount row and still rejects malformed output and bad dataCountModuleBytes, [], ), - ).toThrow(/header/u); + ).toThrow(refused("unsupported_section_header")); }); it("bounds section header bytes while accepting a five-byte size LEB", () => { const paddedCustomDump = [ @@ -209,7 +214,7 @@ it("bounds section header bytes while accepting a five-byte size LEB", () => { "", ].join("\n"); expect(() => parseWabtOutput(malformed, start + 1, [""])).toThrow( - /ranges/u, + refused("section_ranges"), ); } }); @@ -225,7 +230,7 @@ it.each([ `\n${displayed.map((name) => `module name: <${name}>\n`).join("")}\nSections:`, ); expect(() => parseWabtOutput(text, 76, ["test"], decoded)).toThrow( - /header format/u, + refused("unsupported_section_header"), ); }, ); @@ -284,7 +289,7 @@ it("frames custom section names at their NUL-terminated objdump display", async description: '"test"', }); expect(() => parseWabtOutput(text, 76, ["other\0test"])).toThrow( - /custom-section header/u, + refused("custom_header_mismatch"), ); }); it("preserves leading U+FEFF as custom-name data, not a debug-name marker", () => { @@ -327,9 +332,11 @@ it("extracts escaped WAT import/export forms while retaining multiline producer ).exports, ).toEqual(['(export "x" (func 0))']); expect(() => parseWabtWat('(module (export "unfinished"')).toThrow( - /Incomplete/u, + refused("incomplete_output"), + ); + expect(() => parseWabtWat("(module) unexpected")).toThrow( + refused("unexpected_text"), ); - expect(() => parseWabtWat("(module) unexpected")).toThrow(/Unexpected/u); }); it("keeps exact local, ambiguous URL, computed and unparseable glue distinct", () => { const candidates = ["/app/module.wasm", "/other/module.wasm"]; diff --git a/src/wasm/WabtOutput.ts b/src/wasm/WabtOutput.ts index a5265c9748..40a3196b2d 100644 --- a/src/wasm/WabtOutput.ts +++ b/src/wasm/WabtOutput.ts @@ -1,3 +1,5 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; + /** WABT prints names with %.*s, so an embedded NUL ends their display. */ const wabtDisplayedName = (name: string | undefined): string | undefined => name?.split("\0", 1)[0]; @@ -17,7 +19,7 @@ export const parseWabtOutput = ( .join(""); const prefix = `${preamble}\nSections:\n\n`; if (!text.startsWith(prefix)) - throw new Error("Unsupported WABT section header format."); + throw new WabtAdmissionError("unsupported_section_header"); let offset = prefix.length; let customIndex = 0; const sections = []; @@ -27,7 +29,8 @@ export const parseWabtOutput = ( /^[ \t]*(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec( text.slice(offset), ); - if (match === null) throw new Error("Unrecognized WABT section row."); + if (match === null) + throw new WabtAdmissionError("unrecognized_section_row"); offset += match[0].length; let description: string; if (match[1] === "Custom" && !text.startsWith('"', offset)) { @@ -37,7 +40,7 @@ export const parseWabtOutput = ( const end = Number.parseInt(match[3] ?? "", 16); const invalidEnd = invalidCustomNameEnd(artifact, start, end); if (invalidEnd === undefined) - throw new Error("WABT blank custom-section name is not corroborated."); + throw new WabtAdmissionError("blank_custom_name"); invalidNameEnds.push(invalidEnd); description = ""; // WABT does not terminate a blank-name row when another section follows. @@ -49,14 +52,12 @@ export const parseWabtOutput = ( } else if (match[1] === "Custom") { const name = wabtDisplayedName(customSectionNames[customIndex++]); if (name === undefined || !text.startsWith(`"${name}"\n`, offset)) - throw new Error( - "WABT custom-section header does not match decoded WAT.", - ); + throw new WabtAdmissionError("custom_header_mismatch"); description = `"${name}"`; offset += description.length + 1; } else { const end = text.indexOf("\n", offset); - if (end < 0) throw new Error("Incomplete WABT section row."); + if (end < 0) throw new WabtAdmissionError("incomplete_section_row"); description = text.slice(offset, end).trim(); offset = end + 1; } @@ -74,17 +75,13 @@ export const parseWabtOutput = ( sectionHeaderBytes < 2 || sectionHeaderBytes > 6 ) - throw new Error("WABT section ranges do not match selected artifact."); + throw new WabtAdmissionError("section_ranges"); sections.push(section); } if (customIndex !== customSectionNames.length) - throw new Error( - "WABT custom-section inventory does not match decoded WAT.", - ); + throw new WabtAdmissionError("custom_inventory_mismatch"); if ((sections.at(-1)?.end ?? 8) !== artifactBytes) - throw new Error( - "WABT section output does not cover the selected artifact.", - ); + throw new WabtAdmissionError("section_coverage"); const separator = offset + 1; return { sections, diff --git a/src/wasm/WabtString.test.ts b/src/wasm/WabtString.test.ts index a0c2d7cea2..afe7edfa40 100644 --- a/src/wasm/WabtString.test.ts +++ b/src/wasm/WabtString.test.ts @@ -26,8 +26,18 @@ it("frames escaped quotes and trailing backslashes without consuming the next fo it("rejects missing terminators, including a dangling escape or escaped last quote", () => { for (const source of ['"', '"payload', '"payload\\', '"payload\\"']) { - expect(() => watStringEnd(source, 1)).toThrow("Incomplete WABT string"); - expect(() => watString(source, 1)).toThrow("Incomplete WABT string"); + expect(() => watStringEnd(source, 1)).toThrow( + expect.objectContaining({ + _tag: "WabtAdmissionError", + code: "incomplete_string", + }), + ); + expect(() => watString(source, 1)).toThrow( + expect.objectContaining({ + _tag: "WabtAdmissionError", + code: "incomplete_string", + }), + ); expect(() => parseWabtWat(`(module (import ${source}`)).toThrow(); } }); @@ -53,6 +63,9 @@ it("preserves quoted WAT punctuation, nested comments, escaped names and unknown }, ]); expect(() => parseWabtWat('(module (import "env" "\\q" (func)))')).toThrow( - "Unsupported WABT name escape", + expect.objectContaining({ + _tag: "WabtAdmissionError", + code: "unsupported_escape", + }), ); }); diff --git a/src/wasm/WabtString.ts b/src/wasm/WabtString.ts index 715e4cb565..885e8b76ea 100644 --- a/src/wasm/WabtString.ts +++ b/src/wasm/WabtString.ts @@ -1,3 +1,5 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; + /** Locate a WAT string's exclusive end without decoding or copying its payload. */ export const watStringEnd = (form: string, start: number): number => { for ( @@ -9,7 +11,7 @@ export const watStringEnd = (form: string, start: number): number => { while (escapes > start && form[escapes - 1] === "\\") escapes--; if ((end - escapes) % 2 === 0) return end + 1; } - throw new Error("Incomplete WABT string."); + throw new WabtAdmissionError("incomplete_string"); }; /** Decode a producer WAT string only where its bytes are interpreted. */ @@ -45,7 +47,8 @@ export const watString = (form: string, start: number) => { : escape === "\\" ? 92 : undefined; - if (value === undefined) throw new Error("Unsupported WABT name escape."); + if (value === undefined) + throw new WabtAdmissionError("unsupported_escape"); bytes[length++] = value; index += 2; } diff --git a/src/wasm/WabtWat.ts b/src/wasm/WabtWat.ts index cbb0a3edc5..a5305728d8 100644 --- a/src/wasm/WabtWat.ts +++ b/src/wasm/WabtWat.ts @@ -1,3 +1,4 @@ +import { WabtAdmissionError } from "./WabtAdmissionError.js"; import { parseWabtInterface } from "./WabtInterface.js"; import { watString, watStringEnd } from "./WabtString.js"; import { watCommentEnd } from "./WabtComment.js"; @@ -25,24 +26,24 @@ const moduleNames = ( value += (byte & 127) * 2 ** shift; if (byte < 128) return value; } - throw new Error("Incomplete WABT name-section metadata."); + throw new WabtAdmissionError("incomplete_name_metadata"); }; try { let previousType = -1; while (offset < bytes.length) { const type = uint32(bytes.length); if (type <= previousType) - throw new Error("Duplicate or out-of-order WABT name subsection."); + throw new WabtAdmissionError("name_subsection_order"); previousType = type; const size = uint32(bytes.length); const end = offset + size; if (end > bytes.length) - throw new Error("Incomplete WABT name-section metadata."); + throw new WabtAdmissionError("incomplete_name_metadata"); if (type === 0 && size !== 0) { const length = uint32(end); const nameEnd = offset + length; if (nameEnd > end) - throw new Error("Incomplete WABT module-name metadata."); + throw new WabtAdmissionError("incomplete_module_name"); names.push( new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( bytes.subarray(offset, nameEnd), @@ -50,7 +51,7 @@ const moduleNames = ( ); // WABT calls OnModuleName before rejecting trailing subsection bytes. if (nameEnd !== end) - throw new Error("Unfinished WABT module-name subsection."); + throw new WabtAdmissionError("unfinished_module_name"); } offset = end; } @@ -68,7 +69,7 @@ const moduleNames = ( /** Extract exact top-level WABT WAT forms; no raw WASM decoding or import/export name unescaping. */ export const parseWabtWat = (text: string) => { if (!/^\(module(?:\s|\))/u.test(text)) - throw new Error("Unsupported WABT WAT module prefix."); + throw new WabtAdmissionError("unsupported_module_prefix"); const forms: string[] = []; const imports: string[] = []; const exports: string[] = []; @@ -95,13 +96,12 @@ export const parseWabtWat = (text: string) => { continue; } if (char === '"') { - if (depth === 0) - throw new Error("Unexpected string outside WABT WAT module."); + if (depth === 0) throw new WabtAdmissionError("unexpected_string"); index = watStringEnd(text, index + 1) - 1; continue; } if (char === "(") { - if (closed) throw new Error("Unexpected second WABT WAT module."); + if (closed) throw new WabtAdmissionError("unexpected_second_module"); depth++; if (depth === 2) start = index; } else if (char === ")") { @@ -118,7 +118,7 @@ export const parseWabtWat = (text: string) => { else if (/^\(@custom\s/u.test(form)) { const prefix = /^\(@custom\s+"/u.exec(form); if (prefix === null) - throw new Error("Unsupported WABT custom-section name."); + throw new WabtAdmissionError("unsupported_custom_name"); const nameString = watString(form, prefix[0].length); // A WASM name is length-delimited UTF-8; leading U+FEFF is name data. const name = new TextDecoder("utf-8", { @@ -129,7 +129,7 @@ export const parseWabtWat = (text: string) => { if (name === "name") { const payloadPrefix = /^\s+"/u.exec(form.slice(nameString.end)); if (payloadPrefix === null) - throw new Error("Unsupported WABT name-section payload."); + throw new WabtAdmissionError("unsupported_name_payload"); const payload = watString( form, nameString.end + payloadPrefix[0].length, @@ -148,12 +148,12 @@ export const parseWabtWat = (text: string) => { } } } - if (--depth < 0) throw new Error("Unbalanced WABT WAT output."); + if (--depth < 0) throw new WabtAdmissionError("unbalanced_output"); if (depth === 0) closed = true; } else if (depth === 0 && char !== undefined && !/\s/u.test(char)) - throw new Error("Unexpected text outside WABT WAT module."); + throw new WabtAdmissionError("unexpected_text"); } - if (depth !== 0) throw new Error("Incomplete WABT WAT output."); + if (depth !== 0) throw new WabtAdmissionError("incomplete_output"); return { imports, exports, diff --git a/stryker.config.mjs b/stryker.config.mjs index 0fe7f37455..38087bcb05 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -123,10 +123,6 @@ export default { kind: "constructor", owner: "GdbSessionManager", }, - { - kind: "declaration", - name: "DEFAULT_MAX_SESSIONS", - }, { kind: "declaration", owner: "GdbSessionManager", @@ -213,6 +209,12 @@ export default { name: "nextOffset", }, ]), + ...mutationRanges("src/domain/analysisResourceLimits.ts", [ + { + kind: "declaration", + name: "DEFAULT_DEBUGGER_MAX_SESSIONS", + }, + ]), ...mutationRanges("src/cli/reverseEngineeringCommands.ts", [ { kind: "command-handler", diff --git a/tests/acceptance/apkSignatureV1Cli.test.ts b/tests/acceptance/apkSignatureV1Cli.test.ts index 879b8e496a..34da933f51 100644 --- a/tests/acceptance/apkSignatureV1Cli.test.ts +++ b/tests/acceptance/apkSignatureV1Cli.test.ts @@ -81,6 +81,20 @@ cliTest( }, ]), ); + const rotatedWithV2 = join(root, "v1-v2-v3-rotated.apk"); + await writeFile( + rotatedWithV2, + withSigningBlock(v1Zip, [ + { id: APK_SIGNATURE_V2_ID, value: v2SchemeValue(LEAF_CERTIFICATE) }, + { + id: APK_SIGNATURE_V3_ID, + value: v3SchemeValue(ROOT_CERTIFICATE, [ + LEAF_CERTIFICATE, + ROOT_CERTIFICATE, + ]), + }, + ]), + ); // A lineage that does not start at the v1 key is still a mismatch. const misrotated = join(root, "v1-v3-misrotated.apk"); await writeFile( @@ -106,6 +120,7 @@ cliTest( const cliV1 = await viaCli(v1Only); const cliMixed = await viaCli(mixed); const cliRotated = await viaCli(rotated); + const cliRotatedWithV2 = await viaCli(rotatedWithV2); const cliMisrotated = await viaCli(misrotated); expect(cliV1.normalized_result.schemes.v1.signers).toMatchObject([ @@ -127,9 +142,16 @@ cliTest( expect(cliRotated.normalized_result.posture).toMatchObject({ v1_only: false, rotation_present: true, + schemes_agree_on_signer: true, signer_change_declared_by_rotation: false, v1_signers_match_block_signers: true, }); + expect(cliRotatedWithV2.normalized_result.posture).toMatchObject({ + rotation_present: true, + schemes_agree_on_signer: false, + signer_change_declared_by_rotation: true, + v1_signers_match_block_signers: true, + }); expect( cliMisrotated.normalized_result.posture.v1_signers_match_block_signers, ).toBe(false); @@ -157,6 +179,7 @@ cliTest( [v1Only, cliV1], [mixed, cliMixed], [rotated, cliRotated], + [rotatedWithV2, cliRotatedWithV2], ] as const) { const reply = await client.callTool( { name: tool.name, arguments: { path, detail: "complete" } }, diff --git a/tests/acceptance/applications/bundleResourceRootCliMcp.test.ts b/tests/acceptance/applications/bundleResourceRootCliMcp.test.ts index 00539fd2e6..f9aa6a7532 100644 --- a/tests/acceptance/applications/bundleResourceRootCliMcp.test.ts +++ b/tests/acceptance/applications/bundleResourceRootCliMcp.test.ts @@ -1,10 +1,13 @@ import { build } from "plist"; -import { mkdir, writeFile } from "node:fs/promises"; +import { lstat, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; +import { sqliteDatabaseCapability } from "../../../src/application/sqlite/SqliteDatabaseCapability.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { analysisErrorRemediationAction } from "../../../src/domain/analysisErrorPresentation.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ResourceSnapshotMismatchError } from "../../../src/domain/evidenceErrors.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -16,6 +19,74 @@ const resourceOperations = [ "inspect_keyed_archive", ] as const; +const expectUnsupportedArtifactOperations = (content: unknown) => { + const parsed = toolContract("binary_session").outputSchema.parse(content); + for (const name of [ + "inspect_artifact", + "extract_artifact", + "trace_dylib_resolution", + ]) + expect( + parsed.result.tool_availability.groups.find((group) => + group.names.includes(name), + ), + ).toMatchObject({ + available: false, + reason: "target_unsupported", + remediation: expect.stringContaining("open_binary"), + }); +}; + +const expectIndependentResourceWorkflows = async ( + call: Awaited>["call"], + root: string, + firstBundle: string, +): Promise => { + for (const detail of ["summary", "complete"] as const) { + const status = await call("binary_session", { detail }); + expectUnsupportedArtifactOperations(status.structuredContent); + const parsed = toolContract("binary_session").outputSchema.parse( + status.structuredContent, + ); + for (const name of ["inspect_desktop_application"]) + expect( + parsed.result.tool_availability.groups.find((group) => + group.names.includes(name), + ), + ).toMatchObject({ available: true }); + } + const capability = sqliteDatabaseCapability(process.versions.node); + const status = await call("binary_session", { detail: "complete" }); + const parsed = toolContract("binary_session").outputSchema.parse( + status.structuredContent, + ); + expect( + parsed.result.tool_availability.groups.find((group) => + group.names.includes("inspect_sqlite_database"), + ), + ).toMatchObject( + capability.available + ? { available: true } + : { + available: false, + reason: "provider_unavailable", + remediation: capability.reason, + }, + ); + const desktop = await call("inspect_desktop_application", { + path: firstBundle, + }); + expect(desktop.isError, JSON.stringify(desktop)).not.toBe(true); + if (!capability.available) return; + const databasePath = join(root, "fixture.sqlite"); + const { DatabaseSync } = await import("node:sqlite"); + const database = new DatabaseSync(databasePath); + database.exec("CREATE TABLE records (id INTEGER PRIMARY KEY)"); + database.close(); + const result = await call("inspect_sqlite_database", { path: databasePath }); + expect(result.isError, JSON.stringify(result)).not.toBe(true); +}; + cliTest.skipIf(process.platform !== "darwin")( "inspects a keyless app resource root through the compiled CLI", async ({ cli }) => { @@ -52,11 +123,11 @@ cliTest.skipIf(process.platform !== "darwin")( cliTest.skipIf(process.platform !== "darwin")( "retains and switches keyless resource sessions through stdio MCP", async () => { - const temporaryRoot = await createTestTempDirectory( + const root = await createTestTempDirectory( "rea-resource-session-acceptance-", ); - const firstBundle = await createKeylessBundle(temporaryRoot, "First"); - const secondBundle = await createKeylessBundle(temporaryRoot, "Second"); + const firstBundle = await createKeylessBundle(root, "First"); + const secondBundle = await createKeylessBundle(root, "Second"); const { call } = await connectLocalToolsMcp(); const opened = await call("open_bundle_resources", { path: firstBundle }); expect(opened.isError).not.toBe(true); @@ -91,6 +162,37 @@ cliTest.skipIf(process.platform !== "darwin")( }, }); + await expectIndependentResourceWorkflows(call, root, firstBundle); + for (const [name, input] of [ + ["inspect_artifact", {}], + ["extract_artifact", { integrity_policy: "fail" }], + ["trace_dylib_resolution", { roots: ["Absent"] }], + ] as const) { + const refused = await call(name, input); + expect(refused.isError).toBe(true); + expect(errorBody(refused)).toMatchObject({ + error: { code: "capability_unavailable" }, + }); + } + const afterRefusals = await call("binary_session", { detail: "complete" }); + expectUnsupportedArtifactOperations(afterRefusals.structuredContent); + const refusedSnapshot = await call("close_binary", { + snapshot_path: join(root, "snapshot.json"), + }); + expect(refusedSnapshot.isError).toBe(true); + expect(errorBody(refusedSnapshot)).toMatchObject({ + error: { + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + retryable: false, + remediation: { + action: analysisErrorRemediationAction( + new ResourceSnapshotMismatchError("export"), + ), + }, + }, + }); + await expect(lstat(join(root, "snapshot.json"))).rejects.toThrow("ENOENT"); for (const operation of resourceOperations) { const arguments_ = operation === "inspect_keyed_archive" @@ -164,6 +266,14 @@ cliTest.skipIf(process.platform !== "darwin")( }, ); +const errorBody = (response: { + content: ReadonlyArray<{ readonly type: string; readonly text?: string }>; +}): unknown => { + const text = response.content.find((item) => item.type === "text"); + if (typeof text?.text !== "string") throw new Error("Expected error text"); + return JSON.parse(text.text) as unknown; +}; + const createKeylessBundle = async ( root: string, name: string, diff --git a/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts b/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts index 50001cce6e..477160d48b 100644 --- a/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts +++ b/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts @@ -281,8 +281,19 @@ cliTest( const semanticEvidence = toolContract( "trace_javascript_semantics", ).outputSchema.parse(semantic.structuredContent); + const argument = result.semantic_graph.nodes.find( + (node) => + node.kind === "expression" && + node.properties.literal_argument_value === "proc_named" && + node.properties.literal_argument_call_site_node_id === callSite.node_id, + ); + if (argument === undefined) + throw new Error("Expected literal argument node"); expect(semanticEvidence.normalized_result).toMatchObject({ - seed_node_ids: expect.arrayContaining([callSite.node_id]), + seed_node_ids: expect.arrayContaining([argument.node_id]), + nodes: expect.arrayContaining([ + expect.objectContaining({ node_id: callSite.node_id }), + ]), }); }, ); diff --git a/tests/acceptance/bunPathBudgetCli.test.ts b/tests/acceptance/bunPathBudgetCli.test.ts new file mode 100644 index 0000000000..3768082dbc --- /dev/null +++ b/tests/acceptance/bunPathBudgetCli.test.ts @@ -0,0 +1,58 @@ +import { writeFile } from "node:fs/promises"; +import { expect } from "vitest"; +import { workspaceCliTest as test } from "../support/cli/workspaceCliFixture.js"; +import { artifactFixedLimitFailure } from "../../src/artifacts/ArtifactReader.js"; +import { + bunModuleGraphPayload, + elfAppendedBunPayload, +} from "../../src/artifacts/bun/BunStandalone.fixture.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; + +for (const shared of [false, true]) + test(`bounds combined Bun path growth with shared prefixes: ${String(shared)}`, async ({ + cli, + workspace, + }) => { + const modules = Array.from({ length: 1000 }, (_, index) => ({ + name: shared + ? `/${"a/".repeat(254)}${String(index)}` + : `/${String(index)}/${"a/".repeat(254)}end`, + contents: Buffer.alloc(0), + })); + const path = workspace.path("paths"); + await writeFile( + path, + elfAppendedBunPayload(bunModuleGraphPayload({ modules })), + ); + const result = await cli.run({ + arguments: ["inspect-artifact", path, "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + if (!shared) { + expect(result.exitCode).toBe(1); + const limit = artifactFixedLimitFailure( + "Artifact path tree", + 250_000, + "path nodes", + ); + expect(result.json).toMatchObject({ + code: "truncated", + details: { + reason: "limit", + boundary: "capacity", + detail: limit.message, + }, + }); + } else { + expect(result.exitCode, result.stderr).toBe(0); + const inspected = artifactInspectionResultSchema.parse( + parseEvidence(result.json).normalized_result, + ); + expect(inspected.subject.root_format).toBe("bun-standalone"); + const graph = inspected.substeps.find( + (step) => step.operation === "inventory_artifact", + )?.evidence.normalized_result; + expect(graph).toMatchObject({ manifest: { occurrence_count: 1001 } }); + } + }, 120_000); diff --git a/tests/acceptance/bunStandaloneCli.test.ts b/tests/acceptance/bunStandaloneCli.test.ts new file mode 100644 index 0000000000..78d0f8a4fc --- /dev/null +++ b/tests/acceptance/bunStandaloneCli.test.ts @@ -0,0 +1,95 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { cliTest } from "../support/cli/cliFixture.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { + bunModuleGraphPayload, + elfAppendedBunPayload, +} from "../../src/artifacts/bun/BunStandalone.fixture.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; +import { + artifactInventoryResultSchema, + artifactExtractionResultSchema, +} from "../../src/domain/artifactGraph.js"; + +cliTest( + "inventories and extracts Bun source without executing its carrier", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-bun-cli-"); + const source = join(root, "app"); + const contents = "console.log('Bun fixture');\n"; + await writeFile( + source, + elfAppendedBunPayload( + bunModuleGraphPayload({ + modules: [ + { + name: "/$bunfs/root/main.js", + contents: Buffer.from(contents), + sourcemap: Buffer.from("map"), + bytecode: Buffer.from([1, 2]), + }, + ], + }), + ), + ); + const inspected = await cli.run({ + arguments: ["inspect-artifact", source, "--json"], + }); + expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0); + const inspection = artifactInspectionResultSchema.parse( + parseEvidence(inspected.json).normalized_result, + ); + const graph = artifactInventoryResultSchema.parse( + inspection.substeps.find( + (step) => step.operation === "inventory_artifact", + )?.evidence.normalized_result, + ); + expect(graph.manifest.root_format).toBe("bun-standalone"); + expect(inspection.next_probes).toEqual([ + { + operation: "extract_artifact", + authority: "artifact-bytes", + rationale: expect.any(String), + }, + ]); + expect( + graph.occurrences.find((row) => row.logical_path === ".") + ?.format_metadata, + ).toMatchObject({ + kind: "bun-standalone", + entry_point_id: 0, + entry_point_logical_path: "$bunfs/root/main.js", + }); + const module = graph.occurrences.find( + (row) => row.logical_path === "$bunfs/root/main.js", + ); + expect( + graph.nodes.find((node) => node.artifact_id === module?.artifact_id) + ?.sha256, + ).toBe(createHash("sha256").update(contents).digest("hex")); + expect(module).toMatchObject({ + format_metadata: { + entry_point: true, + source_name: "/$bunfs/root/main.js", + sourcemap_bytes: 3, + bytecode_bytes: 2, + }, + }); + const extracted = await cli.run({ + arguments: ["extract-artifact", source, "--json"], + environment: { TMPDIR: root, TMP: root, TEMP: root }, + }); + expect(extracted.exitCode, extracted.stdout + extracted.stderr).toBe(0); + const result = artifactExtractionResultSchema.parse( + parseEvidence(extracted.json).normalized_result, + ); + expect(result.containment_verified).toBe(true); + expect( + await readFile(join(result.output_root, "$bunfs/root/main.js"), "utf8"), + ).toBe(contents); + }, +); diff --git a/tests/acceptance/dotnetBundleJourney.test.ts b/tests/acceptance/dotnetBundleJourney.test.ts new file mode 100644 index 0000000000..85d7768c72 --- /dev/null +++ b/tests/acceptance/dotnetBundleJourney.test.ts @@ -0,0 +1,255 @@ +import { createHash } from "node:crypto"; +import { readFile, rm, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { expect } from "vitest"; + +import { artifactFixedLimitFailure } from "../../src/artifacts/ArtifactReader.js"; +import { dotnetBundleFixture } from "../../src/artifacts/dotnet/DotnetBundle.fixture.js"; +import { cArchiveExecutable } from "../../src/artifacts/pyinstaller/CArchive.fixture.js"; +import { artifactExtractionResultSchema } from "../../src/domain/artifactGraph.js"; +import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { + analysisViewResultSchema, + artifactInventorySummaryViewResultSchema, +} from "../../src/domain/analysisView/analysisView.js"; +import { workspaceCliTest as test } from "../support/cli/workspaceCliFixture.js"; + +const checkProjectedMember = async ( + client: Client, + expectedHostHash: string, + memberPath: string, + expectedHash: string, +) => { + const reply = await client.callTool({ + name: "inspect_artifact", + arguments: { detail: "summary" }, + }); + expect(reply.isError).not.toBe(true); + const summary = artifactInventorySummaryViewResultSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ); + expect(summary.summary.manifest.root_sha256).toBe(expectedHostHash); + const parentId = summary.parent_evidence_id; + expect(parseEvidence(reply.structuredContent).evidence_links).toContain( + parentId, + ); + expect(summary.summary.source_complete).toBe(true); + const source = { kind: "retained-evidence", evidence_id: parentId }; + const read = async (view: Record) => { + const result = await client.callTool({ + name: "inspect_analysis_view", + arguments: { source, view }, + }); + expect(result.isError).not.toBe(true); + const page = analysisViewResultSchema.parse( + parseEvidence(result.structuredContent).normalized_result, + ); + if (page.kind !== "inventory") throw new Error("Unexpected view kind"); + return page; + }; + const occurrences = await read({ + kind: "inventory", + collection: "occurrences", + logical_paths: [memberPath], + fields: ["logical_path", "format_metadata", "source_location"], + limit: 1, + }); + expect(occurrences.parent_evidence_id).toBe(parentId); + expect(occurrences.items).toHaveLength(1); + const member = occurrences.items[0]; + if ( + member === undefined || + !("occurrence_id" in member) || + !("artifact_id" in member) + ) + throw new Error("Expected a projected occurrence"); + expect(member).toMatchObject({ + logical_path: memberPath, + format_metadata: { kind: "dotnet-bundle-entry", major_version: 6 }, + }); + expect(member).not.toHaveProperty("limitations"); + const nodes = await read({ + kind: "inventory", + collection: "nodes", + artifact_ids: [member.artifact_id], + fields: ["sha256"], + limit: 1, + }); + expect(nodes.items).toEqual([ + { artifact_id: member.artifact_id, sha256: expectedHash }, + ]); +}; + +for (const format of ["dotnet-bundle", "pyinstaller"] as const) { + test(`inspects and extracts ${format} through public CLI and stdio MCP`, async ({ + cli, + workspace, + }) => { + const member = + format === "dotnet-bundle" + ? Buffer.from("source-owned compressed resource ".repeat(1024)) + : dotnetBundleFixture().bytes; + const memberPath = + format === "dotnet-bundle" ? "data/resource.txt" : "helper.exe"; + const bytes = + format === "dotnet-bundle" + ? dotnetBundleFixture([ + { path: memberPath, bytes: member, compressed: true, fileType: 0 }, + ]).bytes + : cArchiveExecutable({ + layout: "2.1", + pythonVersion: 313, + members: [ + { + name: memberPath, + typeCode: "b", + payload: member, + compress: false, + }, + ], + }); + const path = workspace.path("renamed.bin"); + await writeFile(path, bytes); + const environment = { + PATH: process.env.PATH ?? "", + HOME: workspace.root, + USERPROFILE: workspace.root, + XDG_CONFIG_HOME: workspace.path("config"), + XDG_CACHE_HOME: workspace.path("cache"), + XDG_STATE_HOME: workspace.path("state"), + REA_LOG_LEVEL: "silent", + }; + const outputs: string[] = []; + const client = new Client({ + name: "rea-dotnet-bundle-acceptance", + version: "1", + }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("scripts/rea.mjs"), "mcp"], + env: environment, + stderr: "pipe", + }); + const checkExtraction = async (value: unknown) => { + const extraction = artifactExtractionResultSchema.parse( + parseEvidence(value).normalized_result, + ); + outputs.push(extraction.output_root); + expect(extraction.containment_verified).toBe(true); + expect( + (await readFile(join(extraction.output_root, memberPath))).equals( + member, + ), + ).toBe(true); + }; + try { + const inspected = await cli.run({ + arguments: ["inspect-artifact", path, "--json"], + environment, + }); + expect(inspected.exitCode, inspected.stderr).toBe(0); + const inventory = artifactInspectionResultSchema.parse( + parseEvidence(inspected.json).normalized_result, + ); + expect(inventory.subject).toMatchObject({ + root_format: format, + root_sha256: createHash("sha256").update(bytes).digest("hex"), + }); + if (format === "dotnet-bundle") { + expect(inventory.next_probes).toHaveLength(1); + expect(inventory.next_probes[0]).toMatchObject({ + operation: "extract_artifact", + authority: "artifact-bytes", + }); + } + expect( + inventory.unexplored_branches.map(({ next_probe }) => next_probe), + ).toEqual(inventory.next_probes); + const extracted = await cli.run({ + arguments: ["extract-artifact", path, "--json"], + environment, + }); + expect(extracted.exitCode, extracted.stderr).toBe(0); + await checkExtraction(extracted.json); + + await client.connect(transport); + await client.listTools(); + const opened = await client.callTool({ + name: "open_binary", + arguments: { path }, + }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const observed = await client.callTool({ + name: "inspect_artifact", + arguments: {}, + }); + expect(observed.isError, JSON.stringify(observed)).not.toBe(true); + expect( + artifactInspectionResultSchema.parse( + parseEvidence(observed.structuredContent).normalized_result, + ), + ).toEqual(inventory); + if (format === "dotnet-bundle") + await checkProjectedMember( + client, + inventory.subject.root_sha256, + memberPath, + createHash("sha256").update(member).digest("hex"), + ); + const materialized = await client.callTool({ + name: "extract_artifact", + arguments: {}, + }); + expect(materialized.isError, JSON.stringify(materialized)).not.toBe(true); + await checkExtraction(materialized.structuredContent); + const closed = await client.callTool({ + name: "close_binary", + arguments: {}, + }); + expect(closed.isError, JSON.stringify(closed)).not.toBe(true); + expect((await readFile(path)).equals(bytes)).toBe(true); + } finally { + await client.close(); + await Promise.all( + outputs.map((output) => rm(output, { recursive: true, force: true })), + ); + } + }, 120_000); +} + +test("reports excessive bundle path depth as a public CLI limit", async ({ + cli, + workspace, +}) => { + const path = workspace.path("deep-host.exe"); + await writeFile( + path, + dotnetBundleFixture([ + { path: `${"a/".repeat(256)}file`, bytes: Buffer.alloc(0) }, + ]).bytes, + ); + + const result = await cli.run({ + arguments: ["inspect-artifact", path, "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(result.exitCode, result.stderr).toBe(1); + const failure = artifactFixedLimitFailure( + "Artifact path depth", + 256, + "path parts", + ); + expect(result.json).toMatchObject({ + message: failure.message, + details: { boundary: "capacity", detail: failure.message }, + }); + expect(result.json).toMatchObject({ + code: "truncated", + category: "truncated", + details: { reason: "limit", truncated: true }, + }); +}); diff --git a/tests/acceptance/modelArtifactCli.test.ts b/tests/acceptance/modelArtifactCli.test.ts index a2eb97524b..70a1707ed8 100644 --- a/tests/acceptance/modelArtifactCli.test.ts +++ b/tests/acceptance/modelArtifactCli.test.ts @@ -1,8 +1,8 @@ import { createHash } from "node:crypto"; import { readFile, writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; -import { expect, it } from "vitest"; -import { toolContract } from "../../src/contracts/toolContracts.js"; +import { afterAll, expect, it } from "vitest"; +import { INSPECT_MODEL_ARTIFACT_CONTRACT } from "../../src/contracts/model/modelToolContracts.js"; import { modelArtifactSchema, type ModelArtifact, @@ -22,7 +22,15 @@ import { safetensorsFixture, } from "../fixtures/model/model.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; -import { cliTest } from "../support/cli/cliFixture.js"; +import { cliTest, createSharedMcpSession } from "../support/cli/cliFixture.js"; + +// Model queries are target-free; share immutable registration and schema setup. +// Portable export keeps its separate post-CLI connection below. +const modelMcp = createSharedMcpSession(async (registerCleanup) => { + const { call } = await connectLocalToolsMcp({}, registerCleanup); + return call; +}); +afterAll(() => modelMcp.close()); cliTest( "preserves lowercase hyphenated GGUF architecture metadata from the published writer", @@ -60,7 +68,7 @@ cliTest( }); expect(response.exitCode, response.stderr).toBe(0); const report = modelArtifactSchema.parse( - toolContract("inspect_model_artifact").outputSchema.parse(response.json) + INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse(response.json) .normalized_result, ); expect(report.metadata.map(({ key, value }) => ({ key, value }))).toEqual( @@ -78,7 +86,7 @@ cliTest( "preserves unresolved Q8_1 layout through CLI, MCP summaries and retained tensor/type pages", async ({ cli }) => { const root = await createTestTempDirectory("rea-model-q8-layout-"); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); for (const blockBytes of [36, 40]) { const path = join(root, `blocks-${String(blockBytes)}.gguf`); const base = ggufFixture(); @@ -305,7 +313,7 @@ cliTest( "preserves released K and shifted integer/IQ uncertainty through CLI and retained MCP views", async ({ cli }) => { const root = await createTestTempDirectory("rea-model-historical-layouts-"); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); for (const layout of ["legacy", "modern"] as const) { const path = join(root, `${layout}.gguf`); const base = ggufFixture(); @@ -454,7 +462,7 @@ cliTest( }); expect(result.exitCode, result.stderr).toBe(0); const report = modelArtifactSchema.parse( - toolContract("inspect_model_artifact").outputSchema.parse(result.json) + INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse(result.json) .normalized_result, ); expect( @@ -487,7 +495,7 @@ cliTest( expect(report.artifact.sha256).toBe( createHash("sha256").update(fixture.bytes).digest("hex"), ); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const response = await call("inspect_model_artifact", { path, detail: "complete", @@ -545,7 +553,7 @@ it.each(["named", "default", "inventory-only", "absent"] as const)( ? [names] : []; await writeFile(path, ggufFixture({ metadata }).bytes); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const response = await call("inspect_model_artifact", { path }); expect(response.isError, JSON.stringify(response)).not.toBe(true); const summary = modelArtifactSummaryViewResultSchema.parse( @@ -591,7 +599,7 @@ it("keeps generated headline keys and imported compound metadata out of compact ], }).bytes, ); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const compact = await call("inspect_model_artifact", { path }); expect(compact.isError, JSON.stringify(compact)).not.toBe(true); const summary = modelArtifactSummaryViewResultSchema.parse( @@ -628,7 +636,7 @@ it("rejects noncanonical imported model indexes and duplicate identities through const root = await createTestTempDirectory("rea-model-import-"); const path = join(root, "model"); await writeFile(path, ggufFixture().bytes); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const response = await call("inspect_model_artifact", { path, detail: "complete", @@ -691,7 +699,7 @@ it("bounds compact model warnings and omits unselected long labels while retaini Buffer.alloc(0), ).bytes, ); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const response = await call("inspect_model_artifact", { path }); expect(response.isError, JSON.stringify(response)).not.toBe(true); expect(JSON.stringify(response).length).toBeLessThan(16_000); @@ -868,7 +876,7 @@ cliTest( "shares scoped model facts through real CLI/MCP and pages retained metadata after source removal", async ({ cli }) => { const root = await createTestTempDirectory("rea-model-cli-"); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); for (const [format, fixture] of [ ["gguf", ggufFixture()], ["safetensors", safetensorsFixture()], @@ -892,9 +900,9 @@ cliTest( ], }); expect(result.exitCode, result.stderr).toBe(0); - const evidence = toolContract( - "inspect_model_artifact", - ).outputSchema.parse(result.json); + const evidence = INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse( + result.json, + ); const report = modelArtifactSchema.parse(evidence.normalized_result); expect(report.format).toBe(format); expect(report.artifact.sha256).toBeNull(); @@ -965,7 +973,7 @@ cliTest( "preserves optional safetensors fields and aligns empty GGUF data in CLI and MCP", async ({ cli }) => { const root = await createTestTempDirectory("rea-model-boundary-compat-"); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const safetensors = safetensorsFixture( '{"__metadata__":null,"weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4],"note":{"source":"local","flags":[null,false,-1,1.25]}}}', Buffer.alloc(4), @@ -993,7 +1001,7 @@ cliTest( }); expect(result.exitCode, result.stderr).toBe(0); const report = modelArtifactSchema.parse( - toolContract("inspect_model_artifact").outputSchema.parse(result.json) + INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse(result.json) .normalized_result, ); if (name.endsWith("safetensors")) { @@ -1041,7 +1049,7 @@ cliTest( }); expect(mapCli.exitCode, mapCli.stderr).toBe(0); const mapReport = modelArtifactSchema.parse( - toolContract("inspect_model_artifact").outputSchema.parse(mapCli.json) + INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse(mapCli.json) .normalized_result, ); expect( @@ -1092,7 +1100,7 @@ cliTest( "inspects unpadded zero-tensor vocab GGUF through CLI and MCP", async ({ cli }) => { const root = await createTestTempDirectory("rea-model-vocab-only-"); - const { call } = await connectLocalToolsMcp(); + const call = await modelMcp.session.call(); const base = ggufFixture(); const vocab = ggufFixture({ tensors: [], @@ -1132,9 +1140,8 @@ cliTest( }); expect(cliResult.exitCode, cliResult.stderr).toBe(0); const report = modelArtifactSchema.parse( - toolContract("inspect_model_artifact").outputSchema.parse( - cliResult.json, - ).normalized_result, + INSPECT_MODEL_ARTIFACT_CONTRACT.outputSchema.parse(cliResult.json) + .normalized_result, ); expect(report.metadata.map(({ key, value }) => ({ key, value }))).toEqual( [ diff --git a/tests/acceptance/nativeMachoParenthesizedCli.test.ts b/tests/acceptance/nativeMachoParenthesizedCli.test.ts new file mode 100644 index 0000000000..388f10e65c --- /dev/null +++ b/tests/acceptance/nativeMachoParenthesizedCli.test.ts @@ -0,0 +1,40 @@ +import { copyFile, mkdir, readFile, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { inspectMachoSchema } from "../../src/domain/native/nativeInspection.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { cliTest } from "../support/cli/cliFixture.js"; + +cliTest.skipIf(process.platform !== "darwin")( + "inspects a Mach-O whose filename ends in parentheses with real otool", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-parenthesized-macho-"); + const target = join(root, "Example (Beta)"); + const temporary = join(root, "temp"); + await mkdir(temporary); + await copyFile("/usr/bin/true", target); + const original = await readFile(target); + const result = await cli.run({ + arguments: ["inspect-macho", target, "--json"], + environment: { + TMPDIR: temporary, + ...(process.env.DEVELOPER_DIR === undefined + ? {} + : { DEVELOPER_DIR: process.env.DEVELOPER_DIR }), + }, + }); + expect(result.exitCode, result.stderr).toBe(0); + const evidence = parseEvidence(result.json); + expect(evidence.caller_input_path).toBe(target); + expect(evidence.subject?.local_path).toBe(target); + const inspected = inspectMachoSchema.parse(evidence.normalized_result); + const operand = inspected.provenance + .flatMap(({ command }) => command) + .find((argument) => argument.includes("rea-macho-operand-")); + expect(operand).toEqual(expect.any(String)); + if (operand === undefined) throw new Error("missing otool operand"); + await expect(stat(operand)).rejects.toMatchObject({ code: "ENOENT" }); + expect((await readFile(target)).equals(original)).toBe(true); + }, +); diff --git a/tests/boundary/browser/cdpCaptureScriptMetadata.test.ts b/tests/boundary/browser/cdpCaptureScriptMetadata.test.ts index de5a210921..3c84010a32 100644 --- a/tests/boundary/browser/cdpCaptureScriptMetadata.test.ts +++ b/tests/boundary/browser/cdpCaptureScriptMetadata.test.ts @@ -5,6 +5,31 @@ import { inspectWebPageInputSchema } from "../../../src/domain/browserObservatio import { CDP_CAPTURE_SCRIPT_METADATA_LIMITS } from "../../../src/browser/CdpCaptureEventTypes.js"; describe("CDP script metadata admission", () => { + it("retains an invalid source-map declaration as unknown", () => { + const events = captureEvents(); + events.beginAuthorizedFrame("frame-1"); + for (const [scriptId, sourceMapURL] of [ + ["bad", null], + ["absent", undefined], + ["empty", ""], + ] as const) { + events.ingest({ + method: "Debugger.scriptParsed", + params: { + scriptId, + url: "https://app.example/app.js", + sourceMapURL, + }, + }); + expect(events.scripts.get(scriptId)?.sourceMapUrl).toBeNull(); + } + expect(events.completeness.snapshot().excluded).toContainEqual({ + section: "source_maps", + reason: "invalid_protocol_value", + count: 1, + }); + }); + it("releases a replaced script ID's prior bytes and resets admission on a new document", () => { const events = captureEvents(); const origin = "https://app.example"; diff --git a/tests/boundary/browser/webCaptureDiff.test.ts b/tests/boundary/browser/webCaptureDiff.test.ts index 719943a8bc..84f6b1a3c2 100644 --- a/tests/boundary/browser/webCaptureDiff.test.ts +++ b/tests/boundary/browser/webCaptureDiff.test.ts @@ -956,6 +956,330 @@ describe("web capture redirect comparison", () => { }); describe("web capture diff incomplete inventories", () => { + it("compares observed script facts without treating excluded map declarations as absent", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const captured = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + if (!captured.ok) throw captured.error; + const known = structuredClone(captured.value); + markSectionsComplete(known, ["scripts", "source_maps"]); + known.completeness.excluded = known.completeness.excluded.filter( + (item) => item.section !== "source_maps", + ); + const script = known.scripts.items[0]; + if (script === undefined) throw new Error("Missing captured script"); + script.source_map_url = "https://cdn.example.test/app.js.map"; + script.cdp_hash = "a".repeat(64); + script.length = 10; + script.is_module = false; + script.language = "JavaScript"; + const excluded = structuredClone(known); + excluded.scripts.items[0]!.source_map_url = null; + excluded.completeness.policy_filtered_sections.push("source_maps"); + excluded.completeness.excluded.push({ + section: "source_maps", + reason: "disallowed_origin", + count: 1, + }); + const absent = structuredClone(known); + absent.scripts.items[0]!.source_map_url = null; + const changed = structuredClone(excluded); + changed.scripts.items[0]!.cdp_hash = "b".repeat(64); + const different = structuredClone(known); + different.scripts.items[0]!.source_map_url = + "https://cdn.example.test/other.map"; + const unavailableBody = structuredClone(known); + unavailableBody.completeness.unavailable_sections.push("source_maps"); + const compare = (before: typeof known, after: typeof known) => + compareWebCaptures( + compareWebCapturesInputSchema.parse({ + before: { inspection: before }, + after: { inspection: after }, + }), + ).dimensions.scripts; + for (const [left, right] of [ + [known, excluded], + [excluded, known], + [excluded, excluded], + [absent, excluded], + ] as const) + expect(compare(left, right)).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + const repeated = structuredClone(excluded); + repeated.scripts.items.push(structuredClone(known.scripts.items[0]!)); + const completeRepeated = structuredClone(different); + completeRepeated.scripts.items.push( + structuredClone(different.scripts.items[0]!), + ); + for (const [left, right] of [ + [repeated, completeRepeated], + [completeRepeated, repeated], + ] as const) + expect(compare(left, right)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + const linkedBefore = structuredClone(repeated); + linkedBefore.scripts.items[0]!.cdp_hash = "b".repeat(64); + const linkedAfter = structuredClone(linkedBefore); + linkedAfter.scripts.items[1]!.source_map_url = + "https://cdn.example.test/new.map"; + for (const [left, right] of [ + [linkedBefore, linkedAfter], + [linkedAfter, linkedBefore], + ] as const) + expect(compare(left, right)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + const ambiguous = structuredClone(linkedAfter); + ambiguous.scripts.items[0]!.cdp_hash = "a".repeat(64); + const ambiguousBefore = structuredClone(linkedBefore); + ambiguousBefore.scripts.items[0]!.cdp_hash = "a".repeat(64); + expect(compare(ambiguousBefore, ambiguous)).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + const incompleteRepeated = structuredClone(repeated); + incompleteRepeated.completeness.truncated_sections.push("scripts"); + expect(compare(incompleteRepeated, completeRepeated)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + const possibleMatch = structuredClone(completeRepeated); + possibleMatch.scripts.items[1]!.source_map_url = null; + possibleMatch.completeness.policy_filtered_sections.push("source_maps"); + expect(compare(repeated, possibleMatch)).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + for (const reason of [ + "unsupported_url", + "invalid_protocol_value", + "resource_budget_exhausted", + ] as const) { + const omission = structuredClone(absent); + omission.completeness.excluded.push({ + section: "source_maps", + reason, + count: 1, + }); + expect(compare(known, omission)).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + } + for (const [left, right] of [ + [known, known], + [absent, absent], + [known, unavailableBody], + ] as const) + expect(compare(left, right)).toMatchObject({ + status: "unchanged", + total_changes: 0, + }); + for (const [left, right] of [ + [known, different], + [absent, known], + [excluded, changed], + ] as const) + expect(compare(left, right)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + }); +}); + +describe("web capture diff partial script metadata", () => { + it("proves a map change that a different hash or declaration cannot explain", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const captured = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + if (!captured.ok) throw captured.error; + const known = structuredClone(captured.value); + markSectionsComplete(known, ["scripts", "source_maps"]); + known.completeness.excluded = known.completeness.excluded.filter( + (item) => item.section !== "source_maps", + ); + const script = known.scripts.items[0]; + if (script === undefined) throw new Error("Missing captured script"); + script.source_map_url = "https://cdn.example.test/app.js.map"; + script.cdp_hash = "a".repeat(64); + script.length = 10; + script.is_module = false; + script.language = "JavaScript"; + const unknownDeclaration = structuredClone(script); + unknownDeclaration.cdp_hash = "b".repeat(64); + unknownDeclaration.source_map_url = null; + const unknownHash = structuredClone(script); + unknownHash.cdp_hash = null; + unknownHash.source_map_url = "https://cdn.example.test/third.js.map"; + const before = structuredClone(known); + before.scripts.items.push(unknownDeclaration, unknownHash); + before.completeness.policy_filtered_sections.push("source_maps"); + const after = structuredClone(before); + after.scripts.items[0]!.source_map_url = + "https://cdn.example.test/new.js.map"; + const compare = (left: typeof known, right: typeof known) => + compareWebCaptures( + compareWebCapturesInputSchema.parse({ + before: { inspection: left }, + after: { inspection: right }, + }), + ).dimensions.scripts; + expect(compare(before, structuredClone(before))).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + expect(compare(before, after)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + }); +}); + +describe("web capture diff repeated source-map declarations", () => { + it("keeps empty and absent declarations distinct during partial matching", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const captured = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + if (!captured.ok) throw captured.error; + const template = captured.value.scripts.items[0]; + if (template === undefined) throw new Error("Missing captured script"); + const script = (hash: string, declaration: string | null) => ({ + ...structuredClone(template), + cdp_hash: hash, + source_map_url: declaration, + }); + const partial = structuredClone(captured.value); + const complete = structuredClone(captured.value); + for (const value of [partial, complete]) + markSectionsComplete(value, ["scripts", "source_maps"]); + partial.completeness.policy_filtered_sections.push("source_maps"); + partial.scripts.items = [ + script("hash-a", ""), + script("hash-b", null), + script("hash-a", null), + ]; + complete.scripts.items = [ + script("hash-a", null), + script("hash-a", ""), + script("hash-b", "https://cdn.example.test/b.js.map"), + ]; + for (const [before, after] of [ + [partial, complete], + [complete, partial], + ] as const) + expect( + compareWebCaptures( + compareWebCapturesInputSchema.parse({ + before: { inspection: before }, + after: { inspection: after }, + }), + ).dimensions.scripts, + ).toMatchObject({ status: "unknown", total_changes: 0 }); + }); + + it("proves differences that declaration counts cannot explain", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const captured = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + if (!captured.ok) throw captured.error; + const template = captured.value.scripts.items[0]; + if (template === undefined) throw new Error("Missing captured script"); + const copies = (count: number, sourceMapUrl: string | null) => + Array.from({ length: count }, () => { + const script = structuredClone(template); + script.source_map_url = sourceMapUrl; + return script; + }); + const inspection = ( + items: typeof captured.value.scripts.items, + omitDeclarations: boolean, + ) => { + const value = structuredClone(captured.value); + markSectionsComplete(value, ["scripts", "source_maps"]); + value.scripts.items = items; + if (omitDeclarations) + value.completeness.policy_filtered_sections.push("source_maps"); + return value; + }; + const compare = ( + left: typeof captured.value, + right: typeof captured.value, + ) => + compareWebCaptures( + compareWebCapturesInputSchema.parse({ + before: { inspection: left }, + after: { inspection: right }, + }), + ).dimensions.scripts; + const map = (name: string) => `https://cdn.example.test/${name}.js.map`; + const nineX = inspection( + [...copies(9, map("x")), ...copies(1, null)], + true, + ); + const tenY = inspection(copies(10, map("y")), false); + const sevenA = inspection( + [...copies(7, map("a")), ...copies(1, map("b")), ...copies(1, null)], + true, + ); + const twelveA = inspection(copies(12, map("a")), false); + + expect(compare(nineX, structuredClone(nineX))).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + expect(compare(nineX, tenY)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + expect(compare(sevenA, twelveA)).toMatchObject({ + status: "changed", + total_changes: 1, + }); + const repeated = inspection( + [...copies(2_000, map("a")), ...copies(1, null)], + true, + ); + expect(compare(repeated, structuredClone(repeated))).toMatchObject({ + status: "unknown", + total_changes: 0, + }); + }); +}); + +describe("web capture diff script inventory coverage", () => { it.each(["truncated_sections", "unavailable_sections"] as const)( "does not infer script additions or removals from %s", async (section) => { diff --git a/tests/boundary/cli/adbCli.test.ts b/tests/boundary/cli/adbCli.test.ts index 877d4e39d3..23fccc5593 100644 --- a/tests/boundary/cli/adbCli.test.ts +++ b/tests/boundary/cli/adbCli.test.ts @@ -1,10 +1,12 @@ -import { chmod, lstat, readFile, unlink } from "node:fs/promises"; +import { chmod, lstat, readFile, unlink, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; +import { crc32 } from "node:zlib"; import { basename, dirname, join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { adbResultSchemas } from "../../../src/domain/adb/adbDeviceAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { execFileOutput, @@ -19,6 +21,93 @@ import { removeTestWorkspace, } from "../../support/workspace/workspaceFixture.js"; +it + .skipIf(process.platform === "win32") + .each([ + "zero width", + "zero height", + "invalid chunk type", + "invalid checksum", + "truncated header", + "valid", + ] as const)( + "validates the screencap PNG header before publishing: %s", + async (kind) => { + const workspace = await createTestWorkspace("rea-adb-screen-cli-"); + onTestFinished(() => removeTestWorkspace(workspace.root)); + const png = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC", + "base64", + ); + if (kind === "zero width") png.writeUInt32BE(0, 16); + if (kind === "zero height") png.writeUInt32BE(0, 20); + if (kind === "invalid chunk type") png[12] = 0xc9; + if (kind !== "invalid checksum") + png.writeUInt32BE(crc32(png.subarray(12, 29)), 29); + else png[29] = png[29]! ^ 0xff; + const frame = kind === "truncated header" ? png.subarray(0, 24) : png; + await writeFile(workspace.path("frame.png"), frame); + const adb = await workspace.write( + "adb", + `#!/bin/sh +if [ "$1" = version ]; then printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-cli\\n'; exit 0; fi +if [ "$1" = -s ] && [ "$3" = exec-out ]; then /bin/cat "$(dirname "$0")/frame.png"; exit 0; fi +exit 1 +`, + ); + await chmod(adb, 0o755); + const outputPath = workspace.path("captures"); + const result = await execFileOutput( + process.execPath, + [ + "scripts/rea.mjs", + "capture-adb-screen", + "device", + "--output-directory", + outputPath, + "--json", + ], + { + env: { ...process.env, REA_ADB_PATH: adb }, + timeout: 20_000, + stopSignal: "SIGTERM", + maxBuffer: 1024 * 1024, + }, + ).then( + (output) => ({ exitCode: 0, stdout: output.stdout }), + (cause: unknown) => { + const failure = execFileOutputFailure(cause); + if (failure === undefined) throw cause; + return { exitCode: failure.code, stdout: failure.stdout }; + }, + ); + if (kind === "valid") { + expect(result.exitCode).toBe(0); + const output = JSON.parse(result.stdout) as { + normalized_result: unknown; + limitations: string[]; + }; + expect( + adbResultSchemas.capture_adb_screen.parse(output.normalized_result), + ).toMatchObject({ width: 1, height: 1 }); + expect(output.limitations).toContain( + "PNG signature, IHDR framing, checksum, and dimensions are validated; encoding and image contents are not validated", + ); + expect( + (await readFile(join(outputPath, "screen.png"))).equals(frame), + ).toBe(true); + } else { + expect(result.exitCode).toBe(1); + expect(JSON.parse(result.stdout)).toMatchObject({ + code: "unreadable_output", + }); + await expect( + readFile(join(outputPath, "screen.png")), + ).rejects.toMatchObject({ code: "ENOENT" }); + } + }, +); + it.skipIf(process.platform === "win32").each([ { exit: 1, stdout: "", stderr: "adb: error: closed", overwrite: false }, { exit: 1, stdout: "", stderr: "adb: error: closed", overwrite: true }, diff --git a/tests/boundary/cli/browserCli.test.ts b/tests/boundary/cli/browserCli.test.ts index 78fe4619f5..99ee7c2e53 100644 --- a/tests/boundary/cli/browserCli.test.ts +++ b/tests/boundary/cli/browserCli.test.ts @@ -36,6 +36,91 @@ afterEach(async () => { await Promise.all(browsers.splice(0).map(async (browser) => browser.close())); }); +cliTest( + "compares source-map declaration omissions through the public CLI", + async ({ cli }) => { + const cdn = "https://cdn.example.test"; + const browser = await startFakeCdpBrowser({ + commandEvents: ({ method }, origin) => + method === "Debugger.enable" + ? [ + { + method: "Debugger.scriptParsed", + sessionId: "session-1", + params: { + scriptId: "map-script", + url: `${origin}/app.js`, + hash: "a".repeat(64), + length: 10, + isModule: false, + scriptLanguage: "JavaScript", + sourceMapURL: `${cdn}/app.map`, + }, + }, + ] + : undefined, + }); + browsers.push(browser); + const inspect = async (broad: boolean) => { + const result = await cli.run({ + arguments: [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--observation-ms", + "0", + "--allowed-origins", + browser.allowedOrigin, + ...(broad ? ["--allowed-origins", cdn] : []), + "--json", + ], + environment: process.env, + }); + expect(result.exitCode, result.stderr).toBe(0); + return normalizedResult(result.json); + }; + const known = await inspect(true); + const omitted = await inspect(false); + expect(known).toMatchObject({ + scripts: { + items: [expect.objectContaining({ source_map_url: `${cdn}/app.map` })], + }, + }); + expect(omitted).toMatchObject({ + scripts: { items: [expect.objectContaining({ source_map_url: null })] }, + completeness: { + excluded: expect.arrayContaining([ + expect.objectContaining({ + section: "source_maps", + reason: "disallowed_origin", + }), + ]), + }, + }); + for (const [before, after] of [ + [known, omitted], + [omitted, known], + [omitted, omitted], + ] as const) { + const result = await cli.run({ + arguments: [ + "compare-web-captures", + JSON.stringify({ inspection: before }), + JSON.stringify({ inspection: after }), + "--json", + ], + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + dimensions: { scripts: { status: "unknown", total_changes: 0 } }, + }, + }); + } + }, + INTEGRATION_TEST_TIMEOUT_MS, +); + cliTest( "browser CLI parity returns the same Evidence discovery and inspection contracts", async ({ cli }) => { diff --git a/tests/boundary/cli/bundleComparisonCli.test.ts b/tests/boundary/cli/bundleComparisonCli.test.ts index f5b97810ab..809ae9b8aa 100644 --- a/tests/boundary/cli/bundleComparisonCli.test.ts +++ b/tests/boundary/cli/bundleComparisonCli.test.ts @@ -12,6 +12,7 @@ import { createEvidenceBundle, serializeEvidenceBundle, } from "../../../src/domain/evidenceBundle.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -219,11 +220,19 @@ cliTest( name: "compare_bundles", arguments: { left_bundle_path: left, right_bundle_path: right }, }); - expect(result.isError).toBe(true); - expect(result.content[0]).toMatchObject({ - text: expect.stringContaining( - `${rejectedField}: must be an absolute local filesystem path`, - ), + expect(parseMcpToolError(result).error).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + operation: "compare_bundles", + issues: expect.arrayContaining([ + expect.objectContaining({ + path: [rejectedField], + reason: "invalid_format", + expected: "regex", + }), + ]), + }, }); } diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index 984f0c4cea..0d0401ef4f 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -6,7 +6,10 @@ import { describe, expect, it } from "vitest"; import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js"; -import { normalizeCliArguments } from "../../../src/cliBuiltinArguments.js"; +import { + normalizeCliArguments, + adaptCliOutputArgumentsForIncur, +} from "../../../src/cliBuiltinArguments.js"; import { renderCliOutputArgumentError, renderCliDocument, @@ -14,7 +17,6 @@ import { renderVersionReport, parseCliOutputArguments, } from "../../../src/cliOutput.js"; -import { adaptCliOutputArgumentsForIncur } from "../../../src/cliBuiltinArguments.js"; const CLI_INTEGRATION_TIMEOUT_MS = 60_000; @@ -113,14 +115,6 @@ describe("CLI output argument boundary", () => { expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value); }); - it("formats arrays of empty objects without column keys as JSON in Markdown", () => { - expect(renderCliDocument([{}], "md", false)).toBe("\\[{}\\]\n"); - expect(renderCliDocument([{}, {}], "md", false)).toBe("\\[{},{}\\]\n"); - expect(renderCliDocument({ empty: [{}] }, "md", false)).toBe( - "## empty\n\n\\[{}\\]\n", - ); - }); - it("rejects token windows that would corrupt structured output", () => { for (const format of ["json", "jsonl", "yaml"] as const) { const validation = parseCliOutputArguments([ @@ -174,6 +168,16 @@ describe("CLI output argument boundary", () => { }); }); +it("formats arrays of empty objects without column keys as JSON in Markdown", () => { + expect(renderCliDocument([{}], "md", false)).toBe("\\[\\{\\}\\]\n"); + expect(renderCliDocument([{}, {}], "md", false)).toBe( + "\\[\\{\\},\\{\\}\\]\n", + ); + expect(renderCliDocument({ empty: [{}] }, "md", false)).toBe( + "## empty\n\n\\[\\{\\}\\]\n", + ); +}); + it("preserves flag-shaped values consumed by valued output options", () => { expect( adaptCliOutputArgumentsForIncur([ diff --git a/tests/boundary/cli/cliOwnedResources.test.ts b/tests/boundary/cli/cliOwnedResources.test.ts new file mode 100644 index 0000000000..fb0639c6e7 --- /dev/null +++ b/tests/boundary/cli/cliOwnedResources.test.ts @@ -0,0 +1,50 @@ +import { afterEach, expect, it, vi } from "vitest"; + +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { CliGuidanceResources } from "../../../src/cli/guidanceCommands.js"; +import { CliJavaScriptAnalysisHost } from "../../../src/cli/javascriptApplicationAnalysis.js"; + +const originalClose = ArtifactResourceScope.prototype.close; + +afterEach(() => { + ArtifactResourceScope.prototype.close = originalClose; + vi.restoreAllMocks(); +}); + +it("keeps guidance descriptors on the host that created them", () => { + const first = new CliGuidanceResources(); + const second = new CliGuidanceResources(); + expect(first.resources()).toBe(first.resources()); + expect(second.resources()).toBe(second.resources()); + expect(first.resources()).not.toBe(second.resources()); +}); + +it("does not share a JavaScript worker scope or its in-flight close", async () => { + let release = (): void => {}; + const gate = new Promise((resolve) => { + release = resolve; + }); + ArtifactResourceScope.prototype.close = function ( + this: ArtifactResourceScope, + ) { + return gate.then(() => originalClose.call(this)); + }; + const first = new CliJavaScriptAnalysisHost(); + const second = new CliJavaScriptAnalysisHost(); + let firstClose: Promise | undefined; + let secondClose: Promise | undefined; + const firstScope = first.scope(); + const secondScope = second.scope(); + try { + expect(firstScope).not.toBe(secondScope); + firstClose = first.close(); + expect(first.close()).toBe(firstClose); + secondClose = second.close(); + expect(secondClose).not.toBe(firstClose); + } finally { + release(); + await Promise.allSettled([firstClose, secondClose]); + } + expect(first.scope()).not.toBe(firstScope); + expect(second.scope()).not.toBe(secondScope); +}); diff --git a/tests/boundary/cli/cliResourceClose.test.ts b/tests/boundary/cli/cliResourceClose.test.ts new file mode 100644 index 0000000000..4a1a9005c7 --- /dev/null +++ b/tests/boundary/cli/cliResourceClose.test.ts @@ -0,0 +1,39 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { closeCliResources } from "../../../src/cli.js"; +import * as javascript from "../../../src/cli/javascriptApplicationAnalysis.js"; +import * as guidance from "../../../src/cli/guidanceCommands.js"; + +afterEach(() => vi.restoreAllMocks()); + +it.each([ + [false, false], + [true, false], + [false, true], + [true, true], +])( + "settles both CLI owners when JavaScript fails=%s and guidance fails=%s", + async (jsFails, guideFails) => { + const jsError = new Error("JavaScript resources retained"); + const guideError = new Error("Guidance descriptor retained"); + const js = vi + .spyOn(javascript, "closeCliJavaScriptResources") + .mockImplementation(async () => { + if (jsFails) throw jsError; + }); + const guides = vi + .spyOn(guidance, "closeCliGuidanceResources") + .mockImplementation(async () => { + if (guideFails) throw guideError; + }); + const closing = closeCliResources(); + if (jsFails && guideFails) + await expect(closing).rejects.toMatchObject({ + errors: [jsError, guideError], + }); + else if (jsFails || guideFails) + await expect(closing).rejects.toBe(jsFails ? jsError : guideError); + else await expect(closing).resolves.toBeUndefined(); + expect(js).toHaveBeenCalledOnce(); + expect(guides).toHaveBeenCalledOnce(); + }, +); diff --git a/tests/boundary/cli/cliToolCoverage.test.ts b/tests/boundary/cli/cliToolCoverage.test.ts index fa138f2cf0..2965c75798 100644 --- a/tests/boundary/cli/cliToolCoverage.test.ts +++ b/tests/boundary/cli/cliToolCoverage.test.ts @@ -8,6 +8,7 @@ import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js"; /** Canonical MCP operations reached by CLI commands with different names. */ const CLI_COMMAND_TOOL_ALIASES = Object.freeze({ "environment-build": ["export_environment_bundle"], + guidance: ["search_guidance", "read_guidance_section"], analyze: ["binary_overview"], inspect: ["binary_overview"], decompile: ["procedure_pseudo_code"], diff --git a/tests/boundary/cli/cliToolFamilyOwners.test.ts b/tests/boundary/cli/cliToolFamilyOwners.test.ts new file mode 100644 index 0000000000..8b7ad988fc --- /dev/null +++ b/tests/boundary/cli/cliToolFamilyOwners.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +import { toolFamilyId } from "../../../src/contracts/toolFamilies.js"; + +const SHARED_APPLICATION_TOOLS = [ + ["export_web_scripts", "registerWebScriptCommands", "registerWebScriptTool"], + [ + "trace_web_module_imports", + "registerWebModuleCommands", + "registerWebModuleTool", + ], + [ + "trace_web_source_location", + "registerWebSourceLocationCommands", + "registerWebSourceLocationTool", + ], +] as const; + +const familyBody = (source: string, family: string): string => { + const marker = `${family}: () =>`; + const start = source.indexOf(marker); + const open = source.indexOf("{", start); + let depth = 0; + for (let index = open; index < source.length; index += 1) { + const character = source[index]; + if (character === "{") depth += 1; + else if (character === "}") { + depth -= 1; + if (depth === 0) return source.slice(open, index + 1); + } + } + throw new Error(`unclosed ${family} registrar`); +}; + +describe("CLI and MCP tool family ownership", () => { + const cli = readFileSync("src/cli.ts", "utf8"); + const server = readFileSync("src/server/createServer.ts", "utf8"); + + it("walks family ids without loading the contract tables", () => { + expect(cli).toContain('from "./contracts/toolFamilyIds.js"'); + expect(cli).not.toContain("toolFamilies.js"); + expect(server).toContain('from "../contracts/toolFamilyIds.js"'); + expect(server).not.toContain("toolFamilies.js"); + }); + + it("keeps web script commands in the application family on both adapters", () => { + const cliApplication = familyBody(cli, "application"); + const cliBrowser = familyBody(cli, "browser"); + const serverApplication = familyBody(server, "application"); + const serverBrowser = familyBody(server, "browser"); + for (const [ + tool, + cliRegistrar, + serverRegistrar, + ] of SHARED_APPLICATION_TOOLS) { + expect(toolFamilyId(tool)).toBe("application"); + expect(cliApplication).toContain(cliRegistrar); + expect(cliBrowser).not.toContain(cliRegistrar); + expect(serverApplication).toContain(serverRegistrar); + expect(serverBrowser).not.toContain(serverRegistrar); + } + }); +}); diff --git a/tests/boundary/cli/dispatcher.test.ts b/tests/boundary/cli/dispatcher.test.ts index 215bf2e36f..c2b1ca2bb9 100644 --- a/tests/boundary/cli/dispatcher.test.ts +++ b/tests/boundary/cli/dispatcher.test.ts @@ -105,7 +105,7 @@ describe("doctor configuration dispatch", () => { const imported = "CLI command runtime imported"; await installCliRuntime( workspace, - `process.stderr.write(${JSON.stringify(`${imported}\n`)}); export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (args, options) => options.stdout(JSON.stringify({ adapter: "cli", args })) });\n`, + `process.stderr.write(${JSON.stringify(`${imported}\n`)}); export const closeCliResources = () => {}; export const createCli = () => ({ serve: (args, options) => options.stdout(JSON.stringify({ adapter: "cli", args })) });\n`, ); // Both token ownership and output/configuration admission use production code. await workspace.write( @@ -194,7 +194,7 @@ describe("executable dispatcher", () => { async ({ processes, workspace }) => { await installCliRuntime( workspace, - 'export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (_args, options) => { options.stdout("first document\\n"); throw new RangeError("Invalid string length"); } });\n', + 'export const closeCliResources = () => {}; export const createCli = () => ({ serve: (_args, options) => { options.stdout("first document\\n"); throw new RangeError("Invalid string length"); } });\n', 'export const createCliResultOutput = () => ({ handled: false, failed: false, captureSdkOutput: () => false, finishSdkOutput: async () => {}, recoverFormattingFailure: () => { process.stdout.write("unexpected second document"); return true; } });\n', ); const result = await processes.run( @@ -213,7 +213,7 @@ describe("executable dispatcher", () => { async ({ processes, workspace }) => { await installCliRuntime( workspace, - 'export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n', + 'export const closeCliResources = () => {}; export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n', ); const scripts = workspace.path("scripts"); await workspace.mkdir("dist/mcp"); @@ -335,7 +335,7 @@ describe("executable dispatcher JavaScript shutdown", () => { async (cleanupFails, { processes, workspace }) => { await installCliRuntime( workspace, - `export const closeCliJavaScriptResources = async () => { ${cleanupFails ? 'throw new Error("worker still owned");' : ""} }; export const createCli = () => ({ serve: () => { throw Object.assign(new Error("broken pipe"), { code: "EPIPE" }); } });\n`, + `export const closeCliResources = async () => { ${cleanupFails ? 'throw new Error("worker still owned");' : ""} }; export const createCli = () => ({ serve: () => { throw Object.assign(new Error("broken pipe"), { code: "EPIPE" }); } });\n`, ); const result = await processes.run( process.execPath, @@ -368,7 +368,7 @@ describe("executable dispatcher JavaScript shutdown", () => { : 'process.stdout.write("complete document\\n");'; await installCliRuntime( workspace, - `export const closeCliJavaScriptResources = () => { globalThis.cleaned = true; ${cleanupFails ? 'return Promise.reject(new Error("worker still owned"));' : ""} }; export const createCli = () => ({ serve: (_args, options) => { options.stdout("pending document"); options.exit(0); } });\n`, + `export const closeCliResources = () => { globalThis.cleaned = true; ${cleanupFails ? 'return Promise.reject(new Error("worker still owned"));' : ""} }; export const createCli = () => ({ serve: (_args, options) => { options.stdout("pending document"); options.exit(0); } });\n`, `export const createCliResultOutput = () => ({ handled: false, failed: false, captureSdkOutput: () => true, finishSdkOutput: async () => { if (!globalThis.cleaned) throw new Error("output preceded cleanup"); await new Promise(resolve => setTimeout(resolve, 10)); ${finish} } });\n`, ); const result = await processes.run( @@ -395,7 +395,7 @@ describe("executable dispatcher JavaScript shutdown", () => { async ({ processes, workspace }) => { await installCliRuntime( workspace, - 'export const closeCliJavaScriptResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { options.exit(0); } });\n', + 'export const closeCliResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { options.exit(0); } });\n', ); const result = await processes.run( process.execPath, @@ -412,7 +412,7 @@ describe("executable dispatcher JavaScript shutdown", () => { async ({ processes, workspace }) => { await installCliRuntime( workspace, - 'export const closeCliJavaScriptResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { process.stdout.emit("error", Object.assign(new Error("broken pipe"), { code: "EPIPE" })); options.exit(0); } });\n', + 'export const closeCliResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { process.stdout.emit("error", Object.assign(new Error("broken pipe"), { code: "EPIPE" })); options.exit(0); } });\n', ); const result = await processes.run( process.execPath, diff --git a/tests/boundary/cli/filteredArrayOutput.test.ts b/tests/boundary/cli/filteredArrayOutput.test.ts index 33fd871f0a..9d2b13aa90 100644 --- a/tests/boundary/cli/filteredArrayOutput.test.ts +++ b/tests/boundary/cli/filteredArrayOutput.test.ts @@ -242,7 +242,7 @@ it("formats arrays of empty objects without columns as JSON in Markdown", async const json = await runFilteredCli(value, "json"); expect(JSON.parse(json)).toEqual(value); const markdown = await runFilteredCli(value, "md"); - expect(markdown).toBe("\\[{},{}\\]\n"); + expect(markdown).toBe("\\[\\{\\},\\{\\}\\]\n"); }); it.each( diff --git a/tests/boundary/cli/gdbStartupCleanupCli.test.ts b/tests/boundary/cli/gdbStartupCleanupCli.test.ts index bbd4a10db7..9a6db771b3 100644 --- a/tests/boundary/cli/gdbStartupCleanupCli.test.ts +++ b/tests/boundary/cli/gdbStartupCleanupCli.test.ts @@ -1,3 +1,6 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; + import pino from "pino"; import { expect, it, vi } from "vitest"; @@ -123,7 +126,12 @@ it("marks the CLI failed when retained GDB cleanup remains incomplete", async () args: { command: "info files" }, options: { inferiorTty: "/dev/pts/42" }, }); - expect(result).toMatchObject({ code: "unreadable_output" }); + expect(result).toMatchObject({ + code: "cleanup_incomplete", + details: { + diagnostics: { primary_error: { code: "unreadable_output" } }, + }, + }); expect(manager.closeAll).toHaveBeenCalledOnce(); expect(process.exitCode).toBe(1); } finally { @@ -199,3 +207,135 @@ it("keeps prelaunch caller cancellation distinct from executable refusal", async await manager.closeAll(); } }); + +class FakeGdb extends EventEmitter { + pid = process.pid + 1; + stdout = new PassThrough(); + stderr = new PassThrough(); + stdin = new PassThrough(); + exitCode: number | null = null; + signalCode: NodeJS.Signals | null = null; + kill(signal: NodeJS.Signals) { + this.exitCode = 0; + this.signalCode = signal; + this.stdout.end(); + this.stderr.end(); + this.stdin.destroy(); + this.emit("exit", 0, signal); + this.emit("close", 0, signal); + return true; + } +} + +const modeledGdbReplies = new Map([ + ["-gdb-set auto-load off", "^done"], + ['-inferior-tty-set "/dev/pts/7"', "^done"], + ["-inferior-tty-show", '^done,inferior_tty_terminal="/dev/pts/7"'], + ['-interpreter-exec console "info files"', "^done"], + ["-list-thread-groups --recurse 1", "^done,groups=[]"], + ["-gdb-exit", "^exit"], +]); + +it.each(["incomplete", "interrupt", "control"] as const)( + "reports final GDB cleanup %s as a typed outcome", + async (mode) => { + const child = new FakeGdb(); + const unmodeled: string[] = []; + let requests = 0; + child.stdout.write("(gdb)\n"); + child.stdin.on("data", (chunk) => { + for (const line of String(chunk).split("\n")) { + const match = /^(\d+)(.*)$/u.exec(line); + if (!match) continue; + requests++; + const token = match[1] ?? ""; + const tail = match[2] ?? ""; + const reply = modeledGdbReplies.get(tail); + if (reply === undefined) unmodeled.push(line); + child.stdout.write( + `${token}${reply ?? '^error,msg="unmodeled request"'}\n`, + ); + } + }); + let calls = 0; + let recovering = false; + let retainedId: string | undefined; + const manager = new GdbSessionManager({ + platform: "linux", + spawnProcess: async (options) => ({ + process: child, + ownership: { + runId: options.runId, + leaderPid: child.pid, + processGroupId: child.pid, + expectedParentPid: process.pid, + }, + }), + stopProcess: async () => { + calls++; + if (mode === "interrupt" && calls === 1) process.emit("SIGINT"); + if (mode === "incomplete" && !recovering) + return { + status: "incomplete", + reason: "synthetic stop remains pending", + }; + child.kill("SIGTERM"); + return { status: "stopped" }; + }, + }); + const originalStart = manager.start.bind(manager); + manager.start = async (...args) => { + const started = await originalStart(...args); + if (started.ok) retainedId = started.value.session_id; + return started; + }; + const command = registerGdbCommand(() => manager); + const previous = process.exitCode; + try { + process.exitCode = 0; + const result = await command.run({ + args: { command: "info files" }, + options: { inferiorTty: "/dev/pts/7" }, + }); + expect(requests).toBeGreaterThan(0); + expect(unmodeled).toEqual([]); + if (mode === "control") { + expect(result).toMatchObject({ + operation: "gdb_console", + parameters: { session_id: retainedId }, + normalized_result: { completion_status: "complete" }, + }); + expect(process.exitCode).toBe(0); + } else if (mode === "interrupt") { + expect(result).toMatchObject({ + code: "cancelled", + details: { + partial_observation: { + operation: "gdb_console", + parameters: { session_id: retainedId }, + }, + }, + }); + expect(process.exitCode).toBe(130); + } else { + expect(result).toMatchObject({ + code: "cleanup_incomplete", + details: { + cleanup: "incomplete", + resources: [retainedId], + partial_observation: { + operation: "gdb_console", + parameters: { session_id: retainedId }, + }, + }, + }); + expect(process.exitCode).toBe(1); + expect(retainedId).toBeDefined(); + } + } finally { + recovering = true; + await manager.closeAll(); + process.exitCode = previous; + } + }, +); diff --git a/tests/boundary/cli/guidanceCli.test.ts b/tests/boundary/cli/guidanceCli.test.ts new file mode 100644 index 0000000000..b80f737330 --- /dev/null +++ b/tests/boundary/cli/guidanceCli.test.ts @@ -0,0 +1,128 @@ +import { afterAll, expect, it } from "vitest"; +import { createCli, closeCliResources } from "../../../src/cli.js"; + +afterAll(async () => closeCliResources()); + +const run = async (arguments_: string[]) => { + const cli = createCli({}); + let stdout = ""; + let exitCode = 0; + const previousExitCode = process.exitCode; + process.exitCode = 0; + try { + await cli.serve([...arguments_, "--json"], { + env: {}, + exit: (code) => { + exitCode = code; + }, + stdout: (chunk) => { + stdout += chunk; + }, + }); + return { exitCode: process.exitCode || exitCode, stdout }; + } finally { + process.exitCode = previousExitCode ?? 0; + } +}; + +it("searches and reads packaged guidance through the provider-free CLI", async () => { + const found = await run(["guidance", "--query", "exact JavaScript module"]); + expect(found.exitCode, found.stdout).toBe(0); + const search = JSON.parse(found.stdout) as { + readonly status: string; + readonly matches: readonly { + readonly heading: string; + readonly locator: string; + }[]; + }; + expect(search.status).toBe("matches"); + const selected = search.matches.find( + ({ heading }) => heading === "Read a module without repeating analysis", + ); + expect(selected).toBeDefined(); + const read = await run([ + "guidance", + "--action", + "read", + "--locator", + selected?.locator ?? "", + ]); + expect(read.exitCode, read.stdout).toBe(0); + expect(JSON.parse(read.stdout)).toMatchObject({ + status: "section", + heading: selected?.heading, + locator: selected?.locator, + source: "installed_rea_guidance", + }); + const ignoredLimit = await run([ + "guidance", + "--action", + "read", + "--locator", + selected?.locator ?? "", + "--limit", + "2", + ]); + expect(ignoredLimit.exitCode).not.toBe(0); +}); + +it("reports no matches and rejects invalid CLI inputs", async () => { + const absent = await run(["guidance", "--query", "qzxv"]); + expect(absent.exitCode).toBe(0); + expect(JSON.parse(absent.stdout)).toMatchObject({ + status: "no_matches", + total_matches: 0, + }); + const empty = await run(["guidance", "--query", " "]); + expect(empty.exitCode).not.toBe(0); + const malformed = await run([ + "guidance", + "--action", + "read", + "--locator", + "../guide.md", + ]); + expect(malformed.exitCode).not.toBe(0); + expect(malformed.stdout).toContain("locator"); + const incompatible = await run([ + "guidance", + "--action", + "read", + "--locator", + "looks-like-a-locator", + "--query", + "module", + ]); + expect(incompatible.exitCode).not.toBe(0); +}); + +it("continues a CLI search with a query- and bundle-bound cursor", async () => { + const first = await run(["guidance", "--query", "module", "--limit", "1"]); + const firstPage = JSON.parse(first.stdout) as { + readonly next_cursor?: string; + readonly matches: readonly { readonly locator: string }[]; + }; + expect(firstPage.next_cursor).toBeDefined(); + const next = await run([ + "guidance", + "--query", + "module", + "--limit", + "1", + "--cursor", + firstPage.next_cursor ?? "", + ]); + expect(next.exitCode, next.stdout).toBe(0); + const nextPage = JSON.parse(next.stdout) as { + readonly matches: readonly { readonly locator: string }[]; + }; + expect(nextPage.matches[0]?.locator).not.toBe(firstPage.matches[0]?.locator); + const wrongQuery = await run([ + "guidance", + "--query", + "different", + "--cursor", + firstPage.next_cursor ?? "", + ]); + expect(wrongQuery.exitCode).not.toBe(0); +}); diff --git a/tests/boundary/cli/markdownLiteralCells.test.ts b/tests/boundary/cli/markdownLiteralCells.test.ts index 559b7e0e32..4942240f23 100644 --- a/tests/boundary/cli/markdownLiteralCells.test.ts +++ b/tests/boundary/cli/markdownLiteralCells.test.ts @@ -95,11 +95,15 @@ cliTest( " leading ": " leading ", leading: "leading", "trailing ": "trailing ", + "\tleading tab": "trailing tab\t", + " mixed\t ": " mixed\t ", " ": " ", " ": " ", "": "", " 中文🙂 ": " 中文🙂 ", " © a|b ": " © a|b ", + "{.hidden}": "literal {.hidden}", + "{name=value}": "{name=value}", }; const row = { [literal]: literal, @@ -168,6 +172,96 @@ cliTest( }, ); +cliTest( + "preserves capability provider identities in rendered Markdown", + async ({ cli }) => { + const json = await cli.run({ arguments: ["capabilities", "--json"] }); + const markdown = await cli.run({ + arguments: ["capabilities", "--format", "md"], + }); + expect(json.exitCode, json.stderr).toBe(0); + expect(markdown.exitCode, markdown.stderr).toBe(0); + const data = JSON.parse(json.stdout) as { + capabilities: { provider: unknown }[]; + }; + const renderer = await createMarkdownRenderer(process.cwd(), { + linkify: false, + }); + expect( + tableColumnByHeader(renderer.render(markdown.stdout), "provider"), + ).toEqual( + data.capabilities.map(({ provider }) => JSON.stringify(provider)), + ); + }, +); + +cliTest( + "keeps scalar markers, edge spaces, and emoji-like names literal in rendered reports", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-markdown-literal-markers-"); + const path = join(root, "bundle.json"); + const result = { + container: "::: details", + trailing: "archive ", + trailingTab: "archive\t", + trailingMixed: "archive \t ", + emoji: ":)", + shortcuts: ":smile: :D :-( ;) ;-() x-) X-) 8-) B-) <3 &", + rows: [{ value: ":)", nested: { path: ";) :smile: 8-)" } }], + after: "still visible", + }; + const record = createEvidence( + undefined, + { id: "fixture", name: "Fixture", version: "1" }, + { operation: "ordinary-json", parameters: {}, result }, + ); + await writeFile(path, JSON.stringify(createEvidenceBundle([record]))); + const base = [ + "inspect-evidence-bundle", + JSON.stringify({ path, detail: "complete" }), + "--filter-output", + "records.0.normalized_result", + ]; + const markdown = await cli.run({ arguments: [...base, "--format", "md"] }); + const json = await cli.run({ arguments: [...base, "--json"] }); + expect(markdown.exitCode, markdown.stderr).toBe(0); + expect(json.exitCode, json.stderr).toBe(0); + expect(JSON.parse(json.stdout)).toEqual({ + records: { "0": { normalized_result: result } }, + }); + const renderer = await createMarkdownRenderer(root, { linkify: false }); + const html = renderer.render(markdown.stdout); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + const children = parseFragment(html).childNodes; + for (const [key, value] of Object.entries(result).filter( + ([, value]) => typeof value === "string", + )) { + const index = children.findIndex( + (node) => + "tagName" in node && + node.tagName === "h2" && + text(node).replaceAll("\u200b", "").trim().endsWith(`.${key}`), + ); + expect(index, key).toBeGreaterThanOrEqual(0); + const body = children.slice(index + 1).find((node) => "tagName" in node); + expect(body && "tagName" in body ? body.tagName : undefined, key).toBe( + "p", + ); + expect(body ? text(body) : undefined, key).toBe(value); + } + expect(html).not.toMatch(/<(?:details|ol|li)(?:\s|>)/u); + expect(renderedCells(html)).toContain( + JSON.stringify(result.rows[0]?.nested), + ); + expect(renderedCells(html)).toContain(":)"); + }, +); + cliTest( "preserves safetensors identifier edge spaces in rendered CLI tables", async ({ cli }) => { @@ -258,9 +352,13 @@ cliTest( renderCliDocument(result, "md", false), ]) { const html = renderer.render(document); - expect(html).toContain( - "**literal** [source](wrong) <T> &copy;", - ); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + expect(text(parseFragment(html))).toContain(literal); expect(html).toContain("**key**
# injected"); expect(html).toContain("literal
- next
# heading"); expect(html).not.toMatch(/<(?:strong|em|ul|ol|li|T|pre|hr)(?:\s|>)/u); diff --git a/tests/boundary/cli/markdownNestedFacts.test.ts b/tests/boundary/cli/markdownNestedFacts.test.ts index 07408de1a2..d5d9daaffc 100644 --- a/tests/boundary/cli/markdownNestedFacts.test.ts +++ b/tests/boundary/cli/markdownNestedFacts.test.ts @@ -2,6 +2,8 @@ import { join } from "node:path"; import { Writable } from "node:stream"; import { Cli } from "incur"; import { expect, it } from "vitest"; +import { createMarkdownRenderer } from "vitepress"; +import { parseFragment, type DefaultTreeAdapterTypes } from "parse5"; import { parseCliOutputArguments, @@ -70,7 +72,7 @@ it("keeps source keys literal in Markdown headings through CLI result output", a expect(JSON.stringify(value)).toBe(original); }); -it("retains nested facts in standalone Markdown table cells and mixed arrays", () => { +it("retains nested facts in standalone Markdown table cells and mixed arrays", async () => { const value = [ { details: { path: "alpha.txt" }, @@ -79,13 +81,32 @@ it("retains nested facts in standalone Markdown table cells and mixed arrays", ( }, ]; const markdown = renderCliDocument(value, "md", false); - expect(markdown).toContain('{"path":"alpha.txt"}'); - expect(markdown).toContain('\\[{"path":"sub/beta.txt"}\\]'); - expect(markdown).toContain("caller\\|value"); - expect(markdown).not.toContain("[object Object]"); - expect( - renderCliDocument({ mixed: [1, { path: "retained" }] }, "md", false), - ).toContain('{"path":"retained"}'); + const renderer = await createMarkdownRenderer(process.cwd(), { + linkify: false, + }); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + const rendered = text(parseFragment(renderer.render(markdown))); + expect(rendered).toContain('{"path":"alpha.txt"}'); + expect(rendered).toContain('[{"path":"sub/beta.txt"}]'); + const literal = "colon: :) ;-) x) #tag :"; + const document = renderCliDocument({ [literal]: literal }, "md", false); + const renderedLiteral = text(parseFragment(renderer.render(document))); + expect(renderedLiteral.split(literal)).toHaveLength(3); + expect(rendered).toContain("caller|value"); + expect(rendered).not.toContain("[object Object]"); + const mixed = renderCliDocument( + { mixed: [1, { path: "retained" }] }, + "md", + false, + ); + expect(text(parseFragment(renderer.render(mixed)))).toContain( + '{"path":"retained"}', + ); expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value); }); diff --git a/tests/boundary/cli/nativeMachoEncryptionCli.test.ts b/tests/boundary/cli/nativeMachoEncryptionCli.test.ts new file mode 100644 index 0000000000..628f68ba88 --- /dev/null +++ b/tests/boundary/cli/nativeMachoEncryptionCli.test.ts @@ -0,0 +1,125 @@ +import { join } from "node:path"; +import { readFile, writeFile } from "node:fs/promises"; + +import { describe, expect } from "vitest"; +import { z } from "zod"; + +import { inspectMachoSchema } from "../../../src/domain/native/nativeInspection.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const evidenceSchema = z.object({ normalized_result: inspectMachoSchema }); +const LC_ENCRYPTION_INFO_64 = 0x2c; + +/** File offset of the thin 64-bit image's LC_ENCRYPTION_INFO_64 cryptid word. */ +const cryptidOffset = (bytes: Buffer): number => { + let offset = 32; + for (let index = 0; index < bytes.readUInt32LE(16); index += 1) { + if (bytes.readUInt32LE(offset) === LC_ENCRYPTION_INFO_64) + return offset + 16; + offset += bytes.readUInt32LE(offset + 4); + } + throw new Error("fixture has no LC_ENCRYPTION_INFO_64"); +}; + +describe.skipIf(process.platform !== "darwin")( + "native Mach-O CLI encryption status", + () => { + cliTest( + "reports every slice's encryption command where otool reports it", + async ({ cli, processes }) => { + const directory = await createTestTempDirectory( + "rea-macho-encryption-", + ); + const source = join(directory, "main.c"); + const arm64 = join(directory, "enc"); + const x86 = join(directory, "plain_x86"); + const fat = join(directory, "fat_enc"); + await writeFile(source, "int main(void) { return 0; }\n"); + const run = async (command: string, arguments_: string[]) => { + const result = await processes.run(command, arguments_, { + timeoutMs: 30_000, + }); + expect(result.exitCode, result.stderr).toBe(0); + return result.stdout; + }; + await run("/usr/bin/xcrun", [ + "clang", + "-arch", + "arm64", + source, + "-o", + arm64, + "-Wl,-encryptable", + ]); + // Mark the range encrypted, as App Store binaries are, and re-sign. + const bytes = await readFile(arm64); + bytes.writeUInt32LE(1, cryptidOffset(bytes)); + await writeFile(arm64, bytes); + await run("/usr/bin/codesign", ["-f", "-s", "-", arm64]); + await run("/usr/bin/xcrun", [ + "clang", + "-arch", + "x86_64", + source, + "-o", + x86, + ]); + await run("/usr/bin/lipo", ["-create", x86, arm64, "-output", fat]); + const otool = await run("/usr/bin/otool", [ + "-arch", + "arm64", + "-l", + fat, + ]); + const field = (name: string) => + Number(new RegExp(`\\n\\s*${name} (\\d+)`, "u").exec(otool)?.[1]); + + const result = await cli.run({ + arguments: ["inspect-macho", fat, "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(result.exitCode, result.stdout + result.stderr).toBe(0); + const inspected = evidenceSchema.parse(result.json).normalized_result; + const [plain, encrypted] = inspected.encryption.items; + expect(inspected.encryption).toMatchObject({ + total: 2, + exhaustive: true, + }); + expect(plain).toMatchObject({ + architecture: "x86_64", + status: "absent", + }); + expect(encrypted).toMatchObject({ + architecture: "arm64", + status: "encrypted", + commands: [ + { + kind: "LC_ENCRYPTION_INFO_64", + cryptoff: field("cryptoff"), + cryptsize: field("cryptsize"), + cryptid: 1, + }, + ], + range: { + content: "ciphertext", + mapping: { + status: "mapped", + sections: expect.arrayContaining([ + { segment: "__TEXT", section: "__text" }, + ]), + }, + }, + }); + const range = encrypted?.range; + if (range == null) throw new Error("expected the encrypted range"); + expect(range.container_start).toBe( + (encrypted?.container_file_offset ?? 0) + field("cryptoff"), + ); + expect(inspected.limitations).toContain( + `Bytes ${range.container_start}-${range.container_end} (input file offsets, end exclusive) of the arm64 slice are ciphertext (cryptid 1); disassembly, decompilation, strings and references derived from them do not describe program code.`, + ); + }, + ); + }, +); diff --git a/tests/boundary/cli/nativeSignatureCli.test.ts b/tests/boundary/cli/nativeSignatureCli.test.ts index 73a57bbe90..cae0929f86 100644 --- a/tests/boundary/cli/nativeSignatureCli.test.ts +++ b/tests/boundary/cli/nativeSignatureCli.test.ts @@ -60,13 +60,13 @@ describe.skipIf(process.platform !== "darwin")( authorities: [], entitlements: null, designated_requirement: expect.stringMatching(/^cdhash H"/u), - limitations: [ + limitations: expect.arrayContaining([ expect.stringContaining("trust validation are not examined."), - ], + ]), }, - limitations: [ + limitations: expect.arrayContaining([ expect.stringContaining("trust validation are not examined."), - ], + ]), }); } const identifier = await cli.run({ diff --git a/tests/boundary/cli/reverseEngineeringCommands.test.ts b/tests/boundary/cli/reverseEngineeringCommands.test.ts index e77aee8f9e..c49d9f9833 100644 --- a/tests/boundary/cli/reverseEngineeringCommands.test.ts +++ b/tests/boundary/cli/reverseEngineeringCommands.test.ts @@ -17,6 +17,7 @@ import { } from "../../../src/domain/evidence.js"; import { err, ok } from "../../../src/domain/result.js"; import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js"; +import { closeOwnedDebuggerSessions } from "../../../src/process/ownedDebuggerSession.js"; import { ReverseEngineeringService as CoreReverseEngineeringService } from "../../../src/application/reverse/ReverseEngineeringService.js"; import { createReverseEngineeringCommandPort } from "../../../src/composition/reverseEngineering.js"; import { OwnedCommandFailure } from "../../../src/process/OwnedCommand.js"; @@ -512,3 +513,141 @@ it.each([false, true])( }); }, ); + +it.each(["incomplete", "interrupt", "control"] as const)( + "reports final Rizin cleanup %s as a typed outcome", + async (mode) => { + const commands = new Map< + string, + { run(input: unknown): Promise } + >(); + const cli = { + command( + name: string, + definition: { run(input: unknown): Promise }, + ) { + commands.set(name, definition); + return cli; + }, + } as unknown as CliInstance; + const previous = process.exitCode; + const evidence = createEvidence( + undefined, + { id: "rizin.debugger", name: "Rizin debugger", version: null }, + { + predicateType: "rea.debugger.command-observation", + operation: "rizin_debug_command", + parameters: { + session_id: "fixture-session", + command: "dr", + inferior_tty: "/dev/pts/7", + }, + result: { output: "registers" }, + rawResult: { output: "registers" }, + }, + ); + const sessions = new Map([["fixture-session", { id: "fixture-session" }]]); + vi.spyOn(RizinDebugSessionManager.prototype, "start").mockResolvedValue( + ok({ session_id: "fixture-session", backend: null }), + ); + const execute = vi + .spyOn(RizinDebugSessionManager.prototype, "execute") + .mockResolvedValue( + ok({ + evidence, + result: { + command: "dr", + output: "registers", + output_scope: "command_and_interleaved_session_output", + output_truncated: false, + completion_status: "complete", + backend: null, + }, + }), + ); + vi.spyOn(RizinDebugSessionManager.prototype, "close").mockImplementation( + async () => { + if (mode === "incomplete") + return err( + new AnalysisOutputError( + "close_rizin_debug_session", + "stop unconfirmed", + { + cleanup: { + reason: "stop unconfirmed", + resources: ["fixture-session"], + }, + }, + ), + ); + sessions.clear(); + return ok({ target_state: "unknown" }); + }, + ); + let abortedAtCleanupEnd = false; + let signal: AbortSignal | undefined; + const closeAll = vi + .spyOn(RizinDebugSessionManager.prototype, "closeAll") + .mockImplementation(async () => { + signal = execute.mock.calls[0]?.[2]; + await new Promise((resolve) => + setImmediate(() => { + if (mode === "interrupt") process.emit("SIGINT"); + resolve(); + }), + ); + abortedAtCleanupEnd = signal?.aborted === true; + await closeOwnedDebuggerSessions( + sessions, + async () => ({ status: "incomplete", reason: "stop unconfirmed" }), + "Rizin", + ); + }); + registerReverseEngineeringCommands(cli, pino({ enabled: false }), {}); + const command = commands.get(CLI_COMMANDS.debugWithRizin); + if (command === undefined) + throw new Error("Rizin CLI command was not registered"); + try { + process.exitCode = 0; + const result = await command.run({ + args: { path: "./sample.bin", command: "dr" }, + options: { inferiorTty: "/dev/pts/7" }, + }); + expect(closeAll).toHaveBeenCalledOnce(); + if (mode === "control") { + expect(result).toBe(evidence); + expect(process.exitCode).toBe(0); + expect(abortedAtCleanupEnd).toBe(false); + } else if (mode === "interrupt") { + expect(result).toMatchObject({ + code: "cancelled", + details: { + partial_observation: { + operation: "rizin_debug_command", + parameters: { session_id: "fixture-session" }, + }, + }, + }); + expect(process.exitCode).toBe(130); + expect(abortedAtCleanupEnd).toBe(true); + } else { + expect(result).toMatchObject({ + code: "cleanup_incomplete", + details: { + cleanup: "incomplete", + resources: ["fixture-session"], + partial_observation: { + operation: "rizin_debug_command", + parameters: { session_id: "fixture-session" }, + }, + }, + }); + expect(sessions.size).toBe(1); + expect(process.exitCode).toBe(1); + } + } finally { + process.exitCode = previous; + vi.restoreAllMocks(); + } + }, +); diff --git a/tests/boundary/evm/evmLimiterAvailability.test.ts b/tests/boundary/evm/evmLimiterAvailability.test.ts index 696afb9338..ec1e1d5655 100644 --- a/tests/boundary/evm/evmLimiterAvailability.test.ts +++ b/tests/boundary/evm/evmLimiterAvailability.test.ts @@ -82,8 +82,12 @@ it("rejects a relative override and non-file without probing an alternate comman ); expect(relative).toMatchObject({ status: "unavailable", - diagnostics: { limiter_path: "prlimit" }, - reason: expect.stringContaining("absolute executable path"), + reason: "prlimit_not_absolute", + diagnostics: { + limiter_path: "prlimit", + code: "prlimit_not_absolute", + setting: "REA_EVM_PRLIMIT_COMMAND", + }, }); const directory = await evmLimiterAvailability( @@ -98,7 +102,8 @@ it("rejects a relative override and non-file without probing an alternate comman ); expect(directory).toMatchObject({ status: "unavailable", - reason: expect.stringContaining("regular executable file"), + reason: "prlimit_not_file", + diagnostics: expect.objectContaining({ code: "prlimit_not_file" }), }); }); diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index b7b0611aa8..1f79921ba6 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -982,7 +982,15 @@ it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])( category: "unsupported_provider", details: { provider_id: "evmole-interface" }, }); - expect(result.error.message).toContain(limiter); + if (scenario === "directory") + expect(result.error).toMatchObject({ + reason: "prlimit_not_file", + diagnostics: { + limiter_path: limiter, + code: "prlimit_not_file", + }, + }); + else expect(result.error.message).toContain(limiter); if (ownedRoot !== undefined) await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); expect(await readFile(path, "utf8")).toBe("0x60006000f3\n"); diff --git a/tests/boundary/evm/solidityMetadataWorker.test.ts b/tests/boundary/evm/solidityMetadataWorker.test.ts index cd68439df8..6b665ec0e3 100644 --- a/tests/boundary/evm/solidityMetadataWorker.test.ts +++ b/tests/boundary/evm/solidityMetadataWorker.test.ts @@ -94,7 +94,17 @@ it.each([ if (kind.startsWith("duplicate nested")) { expect(metadata.entries).toEqual([]); expect(metadata.compiler_version).toBeNull(); - expect(metadata.diagnostic).toContain("repeat map key"); + expect(metadata.diagnostic).toBe("repeated_map_key"); + expect(metadata.admission).toMatchObject({ code: "repeated_map_key" }); + if (kind === "duplicate nested text keys") + expect(metadata.admission?.detail).toEqual({ + key: JSON.stringify({ kind: "text", value: "a" }), + }); + else + expect( + metadata.admission?.detail?.key ?? + metadata.admission?.detail?.key_length, + ).toBeDefined(); expect(metadata.bytecode_offset).toBe(1); expect(metadata.cbor_bytes).toBe(19); } diff --git a/tests/boundary/filesystem/bunMetadataReadBudget.test.ts b/tests/boundary/filesystem/bunMetadataReadBudget.test.ts new file mode 100644 index 0000000000..019f75d9b7 --- /dev/null +++ b/tests/boundary/filesystem/bunMetadataReadBudget.test.ts @@ -0,0 +1,121 @@ +import { open, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { + BUN_TRAILER, + locateBunStandaloneGraph, +} from "../../../src/artifacts/bun/BunModuleGraphLayout.js"; +import { decodeBunModuleGraph } from "../../../src/artifacts/bun/BunModuleGraphMembers.js"; +import { elfAppendedBunPayload } from "../../../src/artifacts/bun/BunStandalone.fixture.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("keeps scattered tiny metadata reads proportional to the graph instead of cache pages", async () => { + const count = 200; + const tableAt = 6 * 65_536; + const data = Buffer.alloc(tableAt + count * 52); + for (let page = 0; page < 6; page++) data[page * 65_536 + 100] = 97; + for (let index = 0; index < count; index++) { + const row = data.subarray(tableAt + index * 52, tableAt + (index + 1) * 52); + for (const [field, page] of [ + [0, (index * 3) % 6], + [8, (index * 3 + 1) % 6], + [40, (index * 3 + 2) % 6], + ] as const) { + row.writeUInt32LE(page * 65_536 + 100, field); + row.writeUInt32LE(1, field + 4); + } + row[48] = 1; + row[49] = 1; + row[50] = 1; + } + const offsets = Buffer.alloc(32); + offsets.writeBigUInt64LE(BigInt(data.length)); + offsets.writeUInt32LE(tableAt, 8); + offsets.writeUInt32LE(count * 52, 12); + const bytes = elfAppendedBunPayload( + Buffer.concat([data, offsets, BUN_TRAILER]), + ); + const path = join( + await createTestTempDirectory("rea-bun-scattered-metadata-"), + "app", + ); + await writeFile(path, bytes); + const handle = await open(path, "r"); + const read = handle.read.bind(handle); + let bytesRead = 0; + const guard = bytes.length * 2; + const counted = new Proxy(handle, { + get: (target, key) => + key === "read" + ? async (...args: Parameters) => { + const result = await read(...args); + bytesRead += result.bytesRead; + if (bytesRead > guard) + throw new Error( + "Metadata read amplification crossed the owned test guard", + ); + return result; + } + : Reflect.get(target, key), + }); + try { + const located = await locateBunStandaloneGraph(counted, bytes.length); + if (located.status !== "present" || located.graph === undefined) + throw new Error("Missing graph"); + const decoded = await decodeBunModuleGraph(located.graph); + expect(decoded.modules).toHaveLength(count); + expect( + decoded.modules.every((module) => module.nameText.text === "a"), + ).toBe(true); + expect(bytesRead).toBeLessThan(guard); + } finally { + await handle.close(); + } +}); + +it("refuses cumulative uncached metadata work before the next file read", async () => { + const rangeLength = 256 * 1024 + 1; + const data = Buffer.alloc(rangeLength); + const offsets = Buffer.alloc(32); + offsets.writeBigUInt64LE(BigInt(data.length)); + const bytes = elfAppendedBunPayload( + Buffer.concat([data, offsets, BUN_TRAILER]), + ); + const path = join( + await createTestTempDirectory("rea-bun-metadata-work-"), + "app", + ); + await writeFile(path, bytes); + const handle = await open(path, "r"); + let reads = 0; + const read = handle.read.bind(handle); + const counted = new Proxy(handle, { + get: (target, key) => + key === "read" + ? async (...args: Parameters) => { + reads++; + return read(...args); + } + : Reflect.get(target, key), + }); + try { + const located = await locateBunStandaloneGraph(counted, bytes.length); + if (located.status !== "present" || located.graph === undefined) + throw new Error("Missing graph"); + const graph = located.graph; + for ( + let index = 0; + index < Math.floor((256 * 1024 * 1024) / rangeLength); + index++ + ) + await graph.data.read(0, rangeLength); + const admittedReads = reads; + await expect(graph.data.read(0, rangeLength)).rejects.toMatchObject({ + reason: "limit", + boundary: "capacity", + }); + expect(reads).toBe(admittedReads); + } finally { + await handle.close(); + } +}); diff --git a/tests/boundary/filesystem/bunStandaloneIntegration.test.ts b/tests/boundary/filesystem/bunStandaloneIntegration.test.ts new file mode 100644 index 0000000000..0302f04acd --- /dev/null +++ b/tests/boundary/filesystem/bunStandaloneIntegration.test.ts @@ -0,0 +1,191 @@ +import type { FileHandle } from "node:fs/promises"; +import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { finished } from "node:stream/promises"; +import { setImmediate } from "node:timers/promises"; +import { expect, it, vi } from "vitest"; +import { BunStandaloneArtifactReader } from "../../../src/artifacts/BunStandaloneArtifactReader.js"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import * as layout from "../../../src/artifacts/bun/BunModuleGraphLayout.js"; +import { decodeBunModuleGraph } from "../../../src/artifacts/bun/BunModuleGraphMembers.js"; +import { + bunModuleGraphPayload, + bunPrefixedPayload, + peBunSection, +} from "../../../src/artifacts/bun/BunStandalone.fixture.js"; +import { extractArtifact } from "../../../src/artifacts/extraction/ArtifactExtraction.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const host = (contents: Buffer) => + peBunSection( + bunPrefixedPayload( + bunModuleGraphPayload({ + modules: [{ name: "/$bunfs/root/main.js", contents }], + }), + ), + ); + +it("reads bounded metadata instead of the stored Bun asset body", async () => { + const root = await createTestTempDirectory("rea-bun-metadata-range-"); + const path = join(root, "host.exe"); + const bytes = host(Buffer.alloc(8 * 1024 * 1024, 97)); + await writeFile(path, bytes); + const handle = await open(path, "r"); + const read = handle.read.bind(handle); + let bytesRead = 0; + const counted = new Proxy(handle, { + get: (target, key) => + key === "read" + ? async (...args: Parameters) => { + const result = await read(...args); + bytesRead += result.bytesRead; + return result; + } + : Reflect.get(target, key), + }); + try { + const located = await layout.locateBunStandaloneGraph( + counted, + bytes.length, + ); + if (located.status !== "present" || located.graph === undefined) + throw new Error("Missing graph"); + const decoded = await decodeBunModuleGraph(located.graph); + expect(decoded.modules[0]?.contents.length).toBe(8 * 1024 * 1024); + expect(decoded.modules[0]?.nameText.text).toBe("/$bunfs/root/main.js"); + expect(bytesRead).toBeLessThan(1024 * 1024); + } finally { + await handle.close(); + } +}); + +it("shares the Bun probe across preflight, inventory and extraction", async () => { + const root = await createTestTempDirectory("rea-bun-shared-probe-"); + const path = join(root, "host.exe"); + const contents = Buffer.from("owned Bun source"); + const bytes = host(contents); + await writeFile(path, bytes); + const pe = bytes.readUInt32LE(60); + const section = pe + 24 + bytes.readUInt16LE(pe + 20); + const prefixOffset = bytes.readUInt32LE(section + 20); + const observed = await open(path, "r"); + const prototype = Object.getPrototypeOf(observed) as FileHandle; + const read = prototype.read; + let probes = 0; + const probe = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + const reply = await read.apply(this, args); + const observedArguments: readonly unknown[] = args; + if (observedArguments[2] === 8 && observedArguments[3] === prefixOffset) + probes++; + return reply; + }); + const scope = new ArtifactResourceScope(); + try { + const output = join(root, "out"); + await extractArtifact({ + inputPath: path, + inputFormat: "pe", + outputRoot: output, + environment: process.env, + resourceScope: scope, + integrityPolicy: "fail", + }); + expect( + (await readFile(join(output, "$bunfs/root/main.js"))).equals(contents), + ).toBe(true); + expect(probes).toBe(1); + } finally { + try { + await scope.close(); + } finally { + probe.mockRestore(); + await observed.close(); + } + } +}); + +it("closes a completed producer whose readable bytes remain buffered", async () => { + const root = await createTestTempDirectory("rea-bun-buffered-reader-"); + const path = join(root, "host.exe"); + await writeFile(path, host(Buffer.from("unconsumed source"))); + const reader = new BunStandaloneArtifactReader(path); + let stream; + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + stream = await reader.open(entries[0]!); + await finished(stream, { readable: false }); + await setImmediate(); + expect(stream.readableEnded).toBe(false); + await reader.close(); + expect(stream.destroyed).toBe(true); + expect(stream.closed).toBe(true); + } finally { + stream?.destroy(); + await reader.close(); + } +}); + +it("rejects replacement of the selected parent while the old descriptor stays valid", async () => { + const root = await createTestTempDirectory("rea-bun-selected-parent-"); + const parent = join(root, "selected"); + await mkdir(parent); + const path = join(parent, "host.exe"); + const bytes = host(Buffer.from("owned source")); + await writeFile(path, bytes); + const reader = new BunStandaloneArtifactReader(path); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + await rename(parent, join(root, "previous")); + await mkdir(parent); + await writeFile(path, bytes); + await expect(reader.open(entries[0]!)).rejects.toMatchObject({ + reason: "integrity", + }); + } finally { + await reader.close(); + } +}); + +it("indexes a deep virtual name while retaining its exact source identity", async () => { + const root = await createTestTempDirectory("rea-bun-deep-virtual-name-"); + const path = join(root, "host.exe"); + const name = "/" + "a/".repeat(256) + "main.js"; + await writeFile( + path, + peBunSection( + bunPrefixedPayload( + bunModuleGraphPayload({ + modules: [{ name, contents: Buffer.from("owned source") }], + }), + ), + ), + ); + const reader = new BunStandaloneArtifactReader(path); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + expect(entries).toHaveLength(1); + expect(entries[0]).toMatchObject({ + path: "bun-modules/0", + sourcePath: name, + formatMetadata: { + kind: "bun-standalone-entry", + index: 0, + source_name: name, + entry_point: true, + }, + }); + expect(entries[0]?.limitations.length).toBeGreaterThan(0); + expect(reader.formatMetadata()).toMatchObject({ + entry_point_logical_path: "bun-modules/0", + entry_point_source_name: name, + }); + } finally { + await reader.close(); + } +}); diff --git a/tests/boundary/filesystem/ciCachePolicy.test.ts b/tests/boundary/filesystem/ciCachePolicy.test.ts index 6b5b22f913..058c47c94b 100644 --- a/tests/boundary/filesystem/ciCachePolicy.test.ts +++ b/tests/boundary/filesystem/ciCachePolicy.test.ts @@ -361,7 +361,7 @@ printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache' ); }); - it("replaces every inline setup-node npm cache immediately and preserves conditions", async () => { + it("pairs npm caches with preceding Node setup and preserves conditions", async () => { const workflows = await readWorkflows(); let npmCacheSteps = 0; @@ -377,12 +377,17 @@ printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache' const step = steps[index]; if (step?.uses !== "./.github/actions/npm-cache") continue; npmCacheSteps++; - const setup = steps[index - 1]; + const setup = steps + .slice(0, index) + .findLast( + (candidate) => + typeof candidate.uses === "string" && + candidate.uses.startsWith("actions/setup-node@"), + ); expect( - typeof setup?.uses === "string" && - setup.uses.startsWith("actions/setup-node@"), - `${file}:${jobName} npm cache action must immediately follow setup-node`, - ).toBe(true); + setup, + `${file}:${jobName} npm cache action requires preceding setup-node`, + ).toBeDefined(); expect(step.if).toBe(setup?.if); expect(setup?.with).not.toHaveProperty("cache", "npm"); const npmCiIndex = steps.findIndex( diff --git a/tests/boundary/filesystem/ciRequiredLanes.test.ts b/tests/boundary/filesystem/ciRequiredLanes.test.ts index 157c3454ee..8e994e4385 100644 --- a/tests/boundary/filesystem/ciRequiredLanes.test.ts +++ b/tests/boundary/filesystem/ciRequiredLanes.test.ts @@ -180,6 +180,33 @@ it("makes the required gate wait for every CI lane, including future additions", ); }); +it("runs architecture guards for fast code feedback before installing dependencies", async () => { + const steps = z + .array( + z + .object({ + run: z.string().optional(), + uses: z.string().optional(), + if: z.string().optional(), + }) + .passthrough(), + ) + .parse((await workflow()).jobs.static?.steps); + const guard = steps.findIndex( + (step) => step.run === "node scripts/verify/architecture-guards.mjs", + ); + const setup = steps.findIndex((step) => + step.uses?.startsWith("actions/setup-node@"), + ); + const install = steps.findIndex((step) => step.run === "npm ci"); + expect(guard).toBeGreaterThanOrEqual(0); + expect(setup).toBeGreaterThanOrEqual(0); + expect(install).toBeGreaterThan(guard); + expect(guard).toBeGreaterThan(setup); + expect(steps[guard]?.if).toBe("needs.changes.outputs.code == 'true'"); + expect(steps[guard]?.["continue-on-error"]).toBeUndefined(); +}); + it("holds native and provider fan-out behind static validation while portable feedback stays independent", async () => { const { jobs } = await workflow(); const earlyJobs = new Set([ diff --git a/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts b/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts index 120b6c0b88..a7a89571ae 100644 --- a/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts +++ b/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts @@ -292,13 +292,29 @@ it.each(["profileless", "uncacheable"] as const)( {}, ); if (!execution.ok) return execution; - expect(execution.value.evidenceId).toBeUndefined(); + if (mode === "profileless") + expect(execution.value.evidenceId).toBeUndefined(); + else { + expect(execution.value.evidenceId).toEqual(expect.any(String)); + expect(execution.value.evidenceId).not.toBe(spoofedEvidenceId); + } return ok(execution.value.result); }, ); if (!composed.ok) throw composed.error; - expect(composed.value.evidenceLinks).toBeUndefined(); - expect(fixture.session.exportEvidenceBundle().records).toEqual([]); + if (mode === "profileless") { + expect(composed.value.evidenceLinks).toBeUndefined(); + expect(fixture.session.exportEvidenceBundle().records).toEqual([]); + } else { + const evidenceId = composed.value.evidenceLinks?.[0]; + expect(evidenceId).toEqual(expect.any(String)); + expect(evidenceId).not.toBe(spoofedEvidenceId); + expect( + fixture.session + .exportEvidenceBundle() + .records.map((record) => record.evidence_id), + ).toEqual([evidenceId]); + } }, ); diff --git a/tests/boundary/filesystem/dotnetBundleArtifacts.test.ts b/tests/boundary/filesystem/dotnetBundleArtifacts.test.ts new file mode 100644 index 0000000000..85ebd9fe96 --- /dev/null +++ b/tests/boundary/filesystem/dotnetBundleArtifacts.test.ts @@ -0,0 +1,736 @@ +import { createHash } from "node:crypto"; +import { access, open, readFile, rename, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { deflateRawSync } from "node:zlib"; + +import { expect, it } from "vitest"; + +import { artifactFixedLimitFailure } from "../../../src/artifacts/ArtifactReader.js"; +import { DotnetBundleArtifactReader } from "../../../src/artifacts/DotnetBundleArtifactReader.js"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { extractArtifact } from "../../../src/artifacts/extraction/ArtifactExtraction.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; +import { + materializeArtifactInventory, + scanArtifactInventory, +} from "../../fixtures/artifactInventory.js"; +import { dotnetBundleFixture } from "../../../src/artifacts/dotnet/DotnetBundle.fixture.js"; +import { cArchiveExecutable } from "../../../src/artifacts/pyinstaller/CArchive.fixture.js"; +import { + findDotnetBundleHeader, + readDotnetBundle, + DOTNET_BUNDLE_SIGNATURE, +} from "../../../src/artifacts/dotnet/DotnetBundle.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it.each([ + { + name: ".NET bundle entry count", + maximum: 100_000, + unit: "entries", + kind: "count", + }, + { + name: ".NET bundle decoded string", + maximum: 65_536, + unit: "UTF-8 bytes", + kind: "string", + }, + { + name: ".NET bundle member stored or expanded size", + maximum: 536_870_912, + unit: "bytes", + kind: "member", + }, + { + name: ".NET bundle exact integer range", + maximum: 9_007_199_254_740_991, + unit: "integer units", + kind: "integer", + }, +])( + "reports the effective fixed $name allowance", + async ({ name, maximum, unit, kind }) => { + const root = await createTestTempDirectory("rea-dotnet-fixed-allowance-"); + const path = join(root, "host.exe"); + const fixture = dotnetBundleFixture([ + { + path: kind === "string" ? "a".repeat(maximum + 1) : "file", + bytes: Buffer.from("x"), + ...(kind === "member" || kind === "integer" + ? { declaredSize: maximum + 1 } + : {}), + }, + ]); + if (kind === "count") + fixture.bytes.writeUInt32LE(maximum + 1, fixture.headerOffset + 8); + await writeFile(path, fixture.bytes); + const handle = await open(path, "r"); + try { + const expected = artifactFixedLimitFailure(name, maximum, unit); + await expect( + readDotnetBundle(handle, fixture.bytes.length, fixture.headerOffset), + ).rejects.toMatchObject({ + reason: "limit", + boundary: "capacity", + message: expected.message, + userMessage: expected.message, + }); + } finally { + await handle.close(); + } + }, +); + +it.each(["pyinstaller", "dotnet-bundle"] as const)( + "keeps the %s outer container when a stored member is another executable container", + async (format) => { + const root = await createTestTempDirectory("rea-dotnet-nested-container-"); + const path = join(root, "renamed.bin"); + const pack = (payload: Buffer) => + cArchiveExecutable({ + layout: "2.1", + pythonVersion: 313, + members: [ + { name: "helper.exe", typeCode: "b", payload, compress: false }, + ], + }); + const helper = + format === "pyinstaller" + ? dotnetBundleFixture().bytes + : pack(Buffer.from("owned helper bytes")); + const bytes = + format === "pyinstaller" + ? pack(helper) + : dotnetBundleFixture([ + { path: "helper.exe", bytes: helper, fileType: 2 }, + ]).bytes; + await writeFile(path, bytes); + const inventory = await inventoryArtifact(path, { + resourceScope: new ArtifactResourceScope(), + }); + expect(inventory.manifest.root_format).toBe(format); + expect(inventory.manifest.root_sha256).toBe(sha(bytes)); + expect( + inventory.occurrences.find((item) => item.logical_path === "helper.exe") + ?.hash_status, + ).toBe("verified"); + const outputRoot = join(root, "output"); + const extracted = await extractArtifact({ + inputPath: path, + inputFormat: format === "pyinstaller" ? "elf" : "pe", + outputRoot, + environment: process.env, + integrityPolicy: "fail", + resourceScope: new ArtifactResourceScope(), + }); + expect(extracted.containment_verified).toBe(true); + expect( + (await readFile(join(outputRoot, "helper.exe"))).equals(helper), + ).toBe(true); + expect((await readFile(path)).equals(bytes)).toBe(true); + }, +); + +it.each([1, 2, 6])( + "inventories and extracts version %s with source and member identity", + async (version) => { + const root = await createTestTempDirectory("rea-dotnet-bundle-"); + const path = join(root, "renamed.bin"); + const plain = Buffer.from("assembly sample"); + const compressed = Buffer.from("resource content ".repeat(1000)); + const fixture = dotnetBundleFixture( + [ + { path: "App.dll", bytes: plain, fileType: 1 }, + { + path: "内容/data.txt", + bytes: compressed, + compressed: version === 6, + fileType: 0, + }, + { path: "empty.txt", bytes: Buffer.alloc(0), fileType: 255 }, + ], + version, + ); + await writeFile(path, fixture.bytes); + const inventory = await inventoryArtifact(path, { + resourceScope: new ArtifactResourceScope(), + }); + expect(inventory.manifest.root_format).toBe("dotnet-bundle"); + expect(inventory.manifest.root_sha256).toBe(sha(fixture.bytes)); + const member = inventory.occurrences.find( + (item) => item.logical_path === "内容/data.txt", + ); + expect(member?.format_metadata).toEqual({ + kind: "dotnet-bundle-entry", + major_version: version, + minor_version: 0, + file_type: 0, + }); + expect(member?.source_location?.length).toBe( + version === 6 ? member?.compressed_size : compressed.length, + ); + expect( + inventory.nodes.find((node) => node.sha256 === sha(compressed))?.size, + ).toBe(compressed.length); + expect( + inventory.occurrences + .find((item) => item.logical_path === "empty.txt") + ?.limitations.join(" "), + ).toContain("Unknown .NET bundle file type 255"); + const outputRoot = join(root, "output"); + const result = await extractArtifact({ + inputPath: path, + inputFormat: "pe", + outputRoot, + environment: process.env, + integrityPolicy: "fail", + resourceScope: new ArtifactResourceScope(), + }); + expect(result.containment_verified).toBe(true); + expect((await readFile(join(outputRoot, "App.dll"))).equals(plain)).toBe( + true, + ); + expect( + (await readFile(join(outputRoot, "内容/data.txt"))).equals(compressed), + ).toBe(true); + expect((await readFile(path)).equals(fixture.bytes)).toBe(true); + }, +); + +it.each([ + [ + "one deeply nested path", + [{ path: `${"a/".repeat(256)}file`, bytes: Buffer.alloc(0) }], + ], + [ + "too many distinct path nodes across members", + Array.from({ length: 1_000 }, (_, index) => ({ + path: `${index}/${"a/".repeat(249)}file`, + bytes: Buffer.alloc(0), + })), + ], +] as const)("rejects %s before building path trees", async (_name, files) => { + const root = await createTestTempDirectory("rea-dotnet-path-budget-"); + const path = join(root, "host.exe"); + const fixture = dotnetBundleFixture(files); + await writeFile(path, fixture.bytes); + const handle = await open(path, "r"); + try { + await expect( + readDotnetBundle(handle, fixture.bytes.length, fixture.headerOffset), + ).rejects.toMatchObject({ reason: "limit" }); + } finally { + await handle.close(); + } +}); + +it("admits many members beneath a shared deep path prefix", async () => { + const root = await createTestTempDirectory("rea-dotnet-shared-prefix-"); + const path = join(root, "host.exe"); + const files = Array.from({ length: 1_000 }, (_, index) => ({ + path: `${"a/".repeat(250)}${index}`, + bytes: Buffer.alloc(0), + })); + const fixture = dotnetBundleFixture(files); + await writeFile(path, fixture.bytes); + const handle = await open(path, "r"); + try { + expect( + ( + await readDotnetBundle( + handle, + fixture.bytes.length, + fixture.headerOffset, + ) + ).files, + ).toHaveLength(files.length); + } finally { + await handle.close(); + } +}); + +it.each([ + [ + "traversal", + () => + dotnetBundleFixture([{ path: "../escape", bytes: Buffer.from("x") }]) + .bytes, + "path", + ], + [ + "overlap", + () => + dotnetBundleFixture([ + { path: "one", bytes: Buffer.from("first") }, + { path: "two", bytes: Buffer.from("second"), declaredOffset: 1025 }, + ]).bytes, + "format", + ], + ["truncated", () => dotnetBundleFixture().bytes.subarray(0, -2), "format"], + [ + "unknown version", + () => dotnetBundleFixture(undefined, 7).bytes, + "unavailable", + ], + [ + "expanded budget", + () => + dotnetBundleFixture([ + { + path: "large", + bytes: Buffer.from("x"), + declaredSize: 600 * 1024 * 1024, + }, + ]).bytes, + "limit", + ], + [ + "false expanded size", + () => + dotnetBundleFixture([ + { + path: "compressed", + bytes: Buffer.from("hello ".repeat(1000)), + compressed: true, + declaredSize: 3, + }, + ]).bytes, + "limit", + ], + [ + "short expanded size", + () => + dotnetBundleFixture([ + { + path: "compressed", + bytes: Buffer.from("hello"), + compressed: true, + declaredSize: 6, + }, + ]).bytes, + "format", + ], + [ + "corrupt deflate", + () => + dotnetBundleFixture([ + { + path: "compressed", + bytes: Buffer.from("hello"), + compressed: true, + storedBytes: Buffer.from([255, 255, 255]), + }, + ]).bytes, + "format", + ], + [ + "trailing deflate bytes", + () => + dotnetBundleFixture([ + { + path: "compressed", + bytes: Buffer.from("hello"), + compressed: true, + storedBytes: Buffer.concat([ + deflateRawSync(Buffer.from("hello")), + Buffer.from("junk"), + ]), + }, + ]).bytes, + "format", + ], + [ + "prefix collision", + () => + dotnetBundleFixture([ + { path: "one", bytes: Buffer.from("x") }, + { path: "one/two", bytes: Buffer.from("y") }, + ]).bytes, + "path", + ], + [ + "invalid UTF-8", + () => { + const bytes = dotnetBundleFixture().bytes; + bytes[bytes.length - 1] = 255; + return bytes; + }, + "format", + ], +] as const)( + "rejects %s without publishing output", + async (_name, make, reason) => { + const root = await createTestTempDirectory("rea-dotnet-invalid-"); + const path = join(root, "host.exe"); + await writeFile(path, make()); + const outputRoot = join(root, "output"); + await expect( + extractArtifact({ + inputPath: path, + inputFormat: "pe", + outputRoot, + environment: process.env, + integrityPolicy: "fail", + resourceScope: new ArtifactResourceScope(), + }), + ).rejects.toMatchObject({ reason }); + await expect(access(outputRoot)).rejects.toThrow(); + }, +); + +it("rejects replacement, forged entry metadata and cancellation without losing cleanup", async () => { + const root = await createTestTempDirectory("rea-dotnet-stable-"); + const path = join(root, "host.exe"); + await writeFile(path, dotnetBundleFixture().bytes); + const reader = new DotnetBundleArtifactReader(path); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries[0]!; + await expect( + reader.open({ ...entry, declaredSize: 1 }), + ).rejects.toMatchObject({ reason: "integrity" }); + const controller = new AbortController(); + controller.abort(); + await expect(reader.open(entry, controller.signal)).rejects.toMatchObject({ + reason: "cancelled", + }); + await writeFile( + path, + dotnetBundleFixture([{ path: "changed", bytes: Buffer.from("other") }]) + .bytes, + ); + await expect(reader.open(entry)).rejects.toMatchObject({ + reason: "integrity", + }); + } finally { + await reader.close(); + } + await reader.close(); +}); + +it("rejects a replaced host even when its inventoried members are unchanged", async () => { + const root = await createTestTempDirectory("rea-dotnet-host-identity-"); + const path = join(root, "host.exe"); + const original = dotnetBundleFixture().bytes; + await writeFile(path, original); + const snapshot = await scanArtifactInventory(path); + const changed = Buffer.from(original); + // Change native host bytes without changing the inventoried member. + changed[400] = changed[400]! ^ 1; + await rename(path, join(root, "original.exe")); + await writeFile(path, changed); + const outputRoot = join(root, "output"); + await expect( + materializeArtifactInventory( + { + inputPath: path, + inputFormat: "pe", + outputRoot, + environment: process.env, + integrityPolicy: "fail", + }, + path, + snapshot, + ), + ).rejects.toMatchObject({ reason: "integrity" }); + await expect(access(outputRoot)).rejects.toThrow(); +}); + +it("detects a renamed host, cancels an active compressed stream, and closes its descriptor", async () => { + const root = await createTestTempDirectory("rea-dotnet-stream-"); + const path = join(root, "host.exe"); + await writeFile( + path, + dotnetBundleFixture([ + { path: "large", bytes: Buffer.alloc(1024 * 1024, 65), compressed: true }, + ]).bytes, + ); + const reader = new DotnetBundleArtifactReader(path); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries[0]!; + const controller = new AbortController(); + const stream = await reader.open(entry, controller.signal); + let received = 0; + await expect( + (async () => { + for await (const chunk of stream) { + received += chunk.length; + controller.abort(); + } + })(), + ).rejects.toMatchObject({ reason: "cancelled" }); + expect(received).toBeGreaterThan(0); + expect(received).toBeLessThan(entry.declaredSize!); + expect(stream.destroyed).toBe(true); + await rename(path, join(root, "moved.exe")); + await expect(reader.open(entry)).rejects.toMatchObject({ + reason: "integrity", + }); + } finally { + await reader.close(); + } + await expect( + (async () => { + for await (const _entry of reader.entries()) { + /* Exhaust the public iterator. */ + } + })(), + ).rejects.toMatchObject({ reason: "unavailable" }); +}); + +it("finds a split marker and uses the first apphost marker, including offset zero", async () => { + const root = await createTestTempDirectory("rea-dotnet-marker-"); + const path = join(root, "host"); + const fixture = dotnetBundleFixture(); + const padded = Buffer.alloc(65536 + fixture.bytes.length); + fixture.bytes.copy(padded, 65536); + const header = 65536 + fixture.headerOffset; + padded.writeBigUInt64LE(BigInt(header), 65536 - 10); + DOTNET_BUNDLE_SIGNATURE.copy(padded, 65536 - 2); + await writeFile(path, padded); + const handle = await open(path, "r"); + try { + expect(await findDotnetBundleHeader(handle, padded.length)).toBe(header); + // Manifest reads may be shorter than requested on a regular file descriptor. + const original = handle.read.bind(handle); + handle.read = (( + buffer: Buffer, + offset: number, + length: number, + position: number, + ) => + original( + buffer, + offset, + Math.min(length, 7), + position, + )) as typeof handle.read; + expect( + (await readDotnetBundle(handle, padded.length, header)).files, + ).toMatchObject([{ path: "App.dll", size: 20 }]); + const controller = new AbortController(); + handle.read = (async () => { + controller.abort(); + throw new Error("injected read failure during cancellation"); + }) as typeof handle.read; + await expect( + findDotnetBundleHeader(handle, padded.length, controller.signal), + ).rejects.toMatchObject({ reason: "cancelled" }); + const metadataController = new AbortController(); + handle.read = (async () => { + metadataController.abort(); + throw new Error("injected manifest read failure during cancellation"); + }) as typeof handle.read; + await expect( + readDotnetBundle( + handle, + padded.length, + header, + metadataController.signal, + ), + ).rejects.toMatchObject({ reason: "cancelled" }); + } finally { + await handle.close(); + } + // A template's zero pointer wins even if a later embedded host has a marker. + padded.writeBigUInt64LE(0n, 65536 - 10); + await writeFile(path, padded); + const template = await open(path, "r"); + try { + expect( + await findDotnetBundleHeader(template, padded.length), + ).toBeUndefined(); + } finally { + await template.close(); + } +}); + +const sha = (bytes: Buffer): string => + createHash("sha256").update(bytes).digest("hex"); + +it("preserves source I/O failures while reading a manifest string across the cache boundary", async () => { + const root = await createTestTempDirectory("rea-dotnet-metadata-io-"); + const path = join(root, "host.exe"); + const name = "a".repeat(64 * 1024); + const fixture = dotnetBundleFixture([ + { path: name, bytes: Buffer.from("x") }, + ]); + await writeFile(path, fixture.bytes); + const handle = await open(path, "r"); + const originalRead = handle.read.bind(handle); + const failure = Object.assign(new Error("manifest source read failed"), { + code: "EIO", + }); + // The first cache fills successfully. Reading the long path then needs a + // new positional read, so this exercises string acquisition, not decoding. + handle.read = (( + buffer: Buffer, + offset: number, + length: number, + position: number, + ) => { + if (position > fixture.headerOffset) return Promise.reject(failure); + return originalRead(buffer, offset, length, position); + }) as typeof handle.read; + try { + await expect( + readDotnetBundle(handle, fixture.bytes.length, fixture.headerOffset), + ).rejects.toBe(failure); + } finally { + await handle.close(); + } +}); + +it.each([3, 4])( + "validates the version 2+ configuration extent for file type %s", + async (fileType) => { + const root = await createTestTempDirectory("rea-dotnet-config-"); + const path = join(root, "host.exe"); + const fixture = dotnetBundleFixture([ + { path: "config.json", bytes: Buffer.from("{}"), fileType }, + ]); + // Three UInt32 fields and the fixture's BinaryWriter string precede the + // deps.json/runtimeconfig.json offset-size pairs, followed by UInt64 flags. + const configuration = + fixture.headerOffset + 12 + 1 + Buffer.byteLength("owned-fixture"); + const selected = configuration + (fileType - 3) * 16; + fixture.bytes.writeBigUInt64LE(BigInt(fixture.offsets[0]!), selected); + fixture.bytes.writeBigUInt64LE(2n, selected + 8); + fixture.bytes.writeBigUInt64LE(1n, configuration + 32); + await writeFile(path, fixture.bytes); + const handle = await open(path, "r+"); + try { + expect( + ( + await readDotnetBundle( + handle, + fixture.bytes.length, + fixture.headerOffset, + ) + ).files, + ).toMatchObject([{ path: "config.json", fileType, size: 2 }]); + fixture.bytes.writeBigUInt64LE(3n, selected + 8); + await handle.write(fixture.bytes, 0, fixture.bytes.length, 0); + await expect( + readDotnetBundle(handle, fixture.bytes.length, fixture.headerOffset), + ).rejects.toMatchObject({ + reason: "format", + message: expect.stringContaining("configuration location disagrees"), + }); + } finally { + await handle.close(); + } + }, +); + +it("retains compressed-source I/O failures and leaves a borrowed descriptor to its owner", async () => { + const root = await createTestTempDirectory("rea-dotnet-io-"); + const path = join(root, "host.exe"); + await writeFile( + path, + dotnetBundleFixture([ + { + path: "compressed", + bytes: Buffer.from("owned".repeat(1024)), + compressed: true, + }, + ]).bytes, + ); + const handle = await open(path, "r"); + const reader = new DotnetBundleArtifactReader(path, { + handle, + initial: await handle.stat(), + }); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + handle.read = (async () => { + throw Object.assign(new Error("injected source read failure"), { + code: "EIO", + }); + }) as typeof handle.read; + const stream = await reader.open(entries[0]!); + await expect( + (async () => { + for await (const _chunk of stream) { + /* Exhaust until the injected failure. */ + } + })(), + ).rejects.toMatchObject({ reason: "io", cause: { code: "EIO" } }); + await reader.close(); + expect((await handle.stat()).isFile()).toBe(true); + } finally { + await reader.close(); + await handle.close(); + } +}); + +it.each(["elf", "mach-o"] as const)( + "routes %s bundle bytes without a filename hint", + async (format) => { + const root = await createTestTempDirectory("rea-dotnet-native-"); + const path = join(root, "renamed.data"); + const fixture = dotnetBundleFixture(); + Buffer.from(format === "elf" ? "7f454c46" : "cffaedfe", "hex").copy( + fixture.bytes, + ); + await writeFile(path, fixture.bytes); + const inventory = await inventoryArtifact(path, { + resourceScope: new ArtifactResourceScope(), + }); + expect(inventory.manifest.root_format).toBe("dotnet-bundle"); + expect( + inventory.nodes.find((node) => node.sha256 === sha(fixture.bytes)) + ?.format, + ).toBe(format); + const outputRoot = join(root, "output"); + await extractArtifact({ + inputPath: path, + inputFormat: format, + outputRoot, + environment: process.env, + integrityPolicy: "fail", + resourceScope: new ArtifactResourceScope(), + }); + expect( + (await readFile(join(outputRoot, "App.dll"))).equals( + Buffer.from("owned assembly bytes"), + ), + ).toBe(true); + }, +); + +it.each(["pe", "elf"] as const)( + "keeps an unbundled %s host native and refuses empty extraction", + async (format) => { + const root = await createTestTempDirectory("rea-dotnet-unbundled-"); + const path = join(root, "template.bin"); + const fixture = dotnetBundleFixture(); + fixture.bytes.writeBigUInt64LE(0n, 536); + if (format === "elf") Buffer.from("7f454c46", "hex").copy(fixture.bytes); + await writeFile(path, fixture.bytes); + const inventory = await inventoryArtifact(path, { + resourceScope: new ArtifactResourceScope(), + }); + expect(inventory.manifest.root_format).toBe(format); + expect(inventory.occurrences.map((entry) => entry.logical_path)).toEqual([ + ".", + ]); + const outputRoot = join(root, "output"); + await expect( + extractArtifact({ + inputPath: path, + inputFormat: format, + outputRoot, + environment: process.env, + integrityPolicy: "fail", + resourceScope: new ArtifactResourceScope(), + }), + ).rejects.toMatchObject({ _tag: "AnalysisUnsupportedTargetError" }); + await expect(access(outputRoot)).rejects.toThrow(); + }, +); diff --git a/tests/boundary/filesystem/dotnetBundleIntegration.test.ts b/tests/boundary/filesystem/dotnetBundleIntegration.test.ts new file mode 100644 index 0000000000..d4aa78da2d --- /dev/null +++ b/tests/boundary/filesystem/dotnetBundleIntegration.test.ts @@ -0,0 +1,43 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, vi } from "vitest"; +import * as dotnetLayout from "../../../src/artifacts/dotnet/DotnetBundle.js"; +import { dotnetBundleFixture } from "../../../src/artifacts/dotnet/DotnetBundle.fixture.js"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { extractArtifact } from "../../../src/artifacts/extraction/ArtifactExtraction.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("shares a single marker scan through extraction preflight, inventory and materialization", async () => { + const root = await createTestTempDirectory("rea-dotnet-shared-scan-"); + const path = join(root, "host"); + const member = Buffer.from("owned .NET resource".repeat(128)); + await writeFile( + path, + dotnetBundleFixture([ + { path: "data/resource.txt", bytes: member, compressed: true }, + ]).bytes, + ); + const locate = vi.spyOn(dotnetLayout, "findDotnetBundleHeader"); + const resources = new ArtifactResourceScope(); + const output = join(root, "out"); + try { + await extractArtifact({ + inputPath: path, + inputFormat: "pe", + outputRoot: output, + environment: process.env, + resourceScope: resources, + integrityPolicy: "fail", + }); + expect( + (await readFile(join(output, "data/resource.txt"))).equals(member), + ).toBe(true); + expect(locate).toHaveBeenCalledTimes(1); + } finally { + try { + await resources.close(); + } finally { + locate.mockRestore(); + } + } +}); diff --git a/tests/boundary/filesystem/ghidraBridgeSources.test.ts b/tests/boundary/filesystem/ghidraBridgeSources.test.ts index 45aa07136d..dc19ac0411 100644 --- a/tests/boundary/filesystem/ghidraBridgeSources.test.ts +++ b/tests/boundary/filesystem/ghidraBridgeSources.test.ts @@ -102,7 +102,7 @@ const admissionCases: readonly AdmissionCase[] = [ { name: "malformed Unicode escape", properties: "application.version=12.1.4\nother=\\u00xx\n", - error: "Malformed Unicode escape in Java properties file", + error: "malformed_unicode_escape", }, ]; diff --git a/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts b/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts index 676a8fe062..5cc068c49f 100644 --- a/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts +++ b/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts @@ -603,7 +603,12 @@ it.each(["seed", "extension"] as const)( ], }, }); - else expect(result.error.diagnostics?.reason).toContain("not a JAR"); + else + expect(result.error.diagnostics).toMatchObject({ + code: "not_jar", + reason: "not_jar", + path, + }); allowClose = true; await scope.retryCleanup(); expect(result.error.cleanupIncomplete).toBe(false); diff --git a/tests/boundary/filesystem/ghidraPspExtension.test.ts b/tests/boundary/filesystem/ghidraPspExtension.test.ts index 97668eb08b..0ac7f3564b 100644 --- a/tests/boundary/filesystem/ghidraPspExtension.test.ts +++ b/tests/boundary/filesystem/ghidraPspExtension.test.ts @@ -241,14 +241,27 @@ describe.skipIf(process.platform === "win32")( if (missing.ok) throw new Error("Missing installation was accepted"); expect(missing.error.message).toContain("PSP_EXTENSION_UNAVAILABLE"); }); + }, +); +describe.skipIf(process.platform === "win32")( + "PSP extension admission codes", + () => { it.each([ - ["name=other\nversion=12.1.3\n", "does not match"], - ["name=ghidra-allegrex\nversion=12.1.4\n", "does not match"], - ["name=ghidra-allegrex\nversion=12.1.\\uZZZZ\n", "Unicode escape"], - ])( + ["name=other\nversion=12.1.3\n", "metadata_mismatch", undefined], + [ + "name=ghidra-allegrex\nversion=12.1.4\n", + "metadata_mismatch", + undefined, + ], + [ + "name=ghidra-allegrex\nversion=12.1.\\uZZZZ\n", + "properties_invalid", + "malformed_unicode_escape", + ], + ] as const)( "rejects mismatched or malformed extension metadata %s", - async (properties, reason) => { + async (properties, code, parserCode) => { const { root } = await installation(); await writeFile(join(root, "extension.properties"), properties); await expect( @@ -257,7 +270,14 @@ describe.skipIf(process.platform === "win32")( "12.1.3", new ArtifactResourceScope(), ), - ).rejects.toThrow(reason); + ).rejects.toMatchObject({ + diagnostics: { + code, + ...(parserCode === undefined + ? { ghidra_version: "12.1.3" } + : { parser_code: parserCode }), + }, + }); }, ); @@ -269,7 +289,12 @@ describe.skipIf(process.platform === "win32")( ); await expect( inspectGhidraPspExtension(root, "12.1.3", new ArtifactResourceScope()), - ).rejects.toThrow("Module.manifest"); + ).rejects.toMatchObject({ + diagnostics: { + code: "component_missing", + component: "Module.manifest", + }, + }); }); it("rejects symlinked code instead of hashing an untracked external producer", async () => { @@ -279,7 +304,9 @@ describe.skipIf(process.platform === "win32")( await symlink(join(directory, "external.jar"), path); await expect( inspectGhidraPspExtension(root, "12.1.3", new ArtifactResourceScope()), - ).rejects.toThrow("regular files"); + ).rejects.toMatchObject({ + diagnostics: { code: "entry_kind", relative: "lib/extension.jar" }, + }); }); it("bounds sparse extension input before reading or allocating its declared extent", async () => { @@ -287,7 +314,12 @@ describe.skipIf(process.platform === "win32")( await truncate(join(root, "lib/extension.jar"), 64 * 1024 * 1024 + 1); await expect( inspectGhidraPspExtension(root, "12.1.3", new ArtifactResourceScope()), - ).rejects.toThrow("inspection limit"); + ).rejects.toMatchObject({ + diagnostics: { + code: "bytes_exceed_limit", + max_bytes: 64 * 1024 * 1024, + }, + }); }); }, ); @@ -314,7 +346,9 @@ describe.skipIf(process.platform === "win32")( "12.1.3", new ArtifactResourceScope(), ), - ).rejects.toThrow("inventory exceeds the inspection limit"); + ).rejects.toMatchObject({ + diagnostics: { code: "inventory_exceeds_limit", max_entries: 1024 }, + }); }, ); }, @@ -571,7 +605,12 @@ describe.skipIf(process.platform === "win32")( const languageOverride = { languageId: "x86:LE:64:default" }; expect(() => ghidraHeadlessArguments({ ...paths, psp: true, languageOverride }), - ).toThrow("PSP ELF import cannot be combined"); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "psp_combined", + }), + ); expect(ghidraHeadlessArguments({ ...paths, languageOverride })).toEqual( expect.arrayContaining(["-processor", "x86:LE:64:default"]), ); @@ -903,11 +942,14 @@ describe.skipIf(process.platform === "win32")( const result = await provider.resolveAnalysisProfile(target); expect(result.ok).toBe(false); if (result.ok) throw new Error("Changed file admitted"); - expect(result.error.message).toContain( - change === "growth" - ? "changed during inspection" - : "Truncated Allegrex component", - ); + expect(result.error).toMatchObject({ + diagnostics: { + code: + change === "growth" ? "component_changed" : "component_truncated", + relative: + change === "growth" ? "Module.manifest" : "extension.properties", + }, + }); expect(closes).toBe(1); expect(result.error.cleanupIncomplete).toBe(false); expect(provider.hasPendingOwnedResources()).toBe(false); diff --git a/tests/boundary/filesystem/jsonOutputOwnership.test.ts b/tests/boundary/filesystem/jsonOutputOwnership.test.ts index 8535836ef9..9bb2ab3e3f 100644 --- a/tests/boundary/filesystem/jsonOutputOwnership.test.ts +++ b/tests/boundary/filesystem/jsonOutputOwnership.test.ts @@ -12,6 +12,7 @@ import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js"; import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { writeTextParts } from "../../../src/application/JsonFiles.js"; +import { StagedOutputError } from "../../../src/filesystem/StagedOutputError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const allocation = vi.hoisted(() => ({ @@ -70,7 +71,10 @@ it("closes the descriptor and retains staged content when its final identity is expect(result.error.cause).toMatchObject({ outcome: { ok: false, - error: { message: "Cannot verify staged file identity" }, + error: { + _tag: "StagedOutputError", + code: "identity_unverified", + }, }, }); const stage = result.error.cleanupResources[0]; @@ -457,7 +461,11 @@ it("refuses to remove unexpected files from an owned staging directory", async ( const unexpected = join(directory, "unexpected"); writeFileSync(unexpected, "unowned"); try { - await expect(owner.close()).rejects.toThrow(/Unexpected staging entries/u); + await expect(owner.close()).rejects.toMatchObject({ + _tag: "StagedOutputError", + code: "unexpected_entries", + detail: { count: 1, entries: "unexpected" }, + }); expect(await fs.readFile(unexpected, "utf8")).toBe("unowned"); } finally { await fs.rm(unexpected, { force: true }); @@ -466,6 +474,37 @@ it("refuses to remove unexpected files from an owned staging directory", async ( } }); +it("keeps unexpected staging names on the publication error", async () => { + const root = await createTestTempDirectory("rea-staged-public-entries-"); + const destination = join(root, "out.json"); + const resources = new ArtifactResourceScope(); + const close = vi + .spyOn(OwnedStagedOutput.prototype, "close") + .mockImplementation(async () => { + throw new StagedOutputError("unexpected_entries", { + count: 1, + entries: "unexpected", + }); + }); + try { + const result = await writeTextParts(["{}"], destination, false, resources); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toMatchObject({ + _tag: "EvidenceFileError", + diagnostics: { + code: "unexpected_entries", + count: 1, + entries: "unexpected", + }, + }); + } finally { + close.mockRestore(); + await resources.close().catch(() => undefined); + await rm(root, { recursive: true, force: true }); + } +}); + it("retains cleanup when its staging directory was renamed", async () => { const root = await createTestTempDirectory("rea-staged-renamed-dir-"); const directory = join(root, ".rea-write-owned"); diff --git a/tests/boundary/filesystem/jsonOutputResourceFailure.test.ts b/tests/boundary/filesystem/jsonOutputResourceFailure.test.ts new file mode 100644 index 0000000000..50b4a95f8e --- /dev/null +++ b/tests/boundary/filesystem/jsonOutputResourceFailure.test.ts @@ -0,0 +1,136 @@ +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, onTestFinished, vi } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const failure = vi.hoisted(() => ({ code: "" })); +vi.mock("node:fs/promises", async (importOriginal) => { + const filesystem = await importOriginal(); + return { + ...filesystem, + mkdtemp: async (...args: Parameters) => { + if (failure.code !== "") + throw Object.assign(new Error("controlled staging refusal"), { + code: failure.code, + }); + return filesystem.mkdtemp(...args); + }, + }; +}); +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { writeTextParts } from "../../../src/application/JsonFiles.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +import { redactExplicitFailure } from "../../../src/domain/explicitSensitiveFailure.js"; + +it.each([ + ["EFBIG", "file-size allowance"], + ["ENOSPC", "space or quota"], + ["EDQUOT", "space or quota"], +] as const)( + "preserves the %s host resource refusal through redaction", + async (code, guidance) => { + const root = await createTestTempDirectory("rea-write-resource-"); + const resources = new ArtifactResourceScope(); + onTestFinished(() => resources.close()); + failure.code = code; + try { + const result = await writeTextParts( + ["bytes"], + join(root, "out.json"), + false, + resources, + ); + expect(result.ok).toBe(false); + if (!result.ok) { + const projected = projectAnalysisError( + redactExplicitFailure(result.error, ["unrelated-secret"]), + ); + expect(projected).toMatchObject({ + code: "resource_constraint", + category: "resource_constraint", + retryable: false, + details: { + operation: "write", + reason: "storage-limit", + system_code: code, + path: join(root, "out.json"), + }, + }); + expect(projected.remediation.action).toContain(guidance); + const excluded = projectAnalysisError( + redactExplicitFailure(result.error, [code]), + ); + expect(excluded.details).not.toHaveProperty("system_code"); + expect(excluded.remediation.action).toContain("file-size allowance"); + } + expect(await readdir(root)).toEqual([]); + } finally { + failure.code = ""; + } + }, +); + +/** Fail after the first staged part has been written, before publication. */ +function* limitedParts(): Generator { + yield "staged bytes"; + throw Object.assign(new Error("file size limit"), { code: "EFBIG" }); +} + +it.each([false, true])( + "retains a staged EFBIG and cleans or retries its owner (cleanup fails: %s)", + async (cleanupFails) => { + const root = await createTestTempDirectory("rea-staged-resource-"); + const output = join(root, "out.json"); + const resources = new ArtifactResourceScope(); + const { OwnedStagedOutput } = + await import("../../../src/filesystem/OwnedStagedOutput.js"); + const close = vi.spyOn(OwnedStagedOutput.prototype, "close"); + if (cleanupFails) + close.mockRejectedValueOnce(new Error("controlled cleanup refusal")); + try { + const result = await writeTextParts( + limitedParts(), + output, + false, + resources, + ); + if (result.ok) throw new Error("Expected resource refusal"); + const redacted = redactExplicitFailure(result.error, [ + "unrelated-secret", + ]); + const projected = projectAnalysisError(redacted); + expect(projected).toMatchObject({ + code: cleanupFails ? "cleanup_incomplete" : "resource_constraint", + details: { + reason: "storage-limit", + system_code: "EFBIG", + path: output, + }, + }); + const staging = (await readdir(root)).filter((name) => + name.startsWith(".rea-write-"), + ); + expect(staging).toHaveLength(cleanupFails ? 1 : 0); + expect((await readdir(root)).includes("out.json")).toBe(false); + if (cleanupFails) { + expect(resources.hasPending).toBe(true); + expect(redacted.cleanupResources).toContain(join(root, staging[0]!)); + await resources.retryCleanup(); + expect(resources.hasPending).toBe(false); + expect(redacted.cleanupIncomplete).toBe(false); + expect(projectAnalysisError(redacted)).toMatchObject({ + code: "resource_constraint", + details: { + reason: "storage-limit", + system_code: "EFBIG", + path: output, + }, + }); + expect(await readdir(root)).toEqual([]); + } + } finally { + close.mockRestore(); + await resources.close(); + } + }, +); diff --git a/tests/boundary/filesystem/keyedArchiveXmlNesting.test.ts b/tests/boundary/filesystem/keyedArchiveXmlNesting.test.ts index bfd729ee76..f81c6637c0 100644 --- a/tests/boundary/filesystem/keyedArchiveXmlNesting.test.ts +++ b/tests/boundary/filesystem/keyedArchiveXmlNesting.test.ts @@ -2,10 +2,11 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; -import { parse } from "plist"; +import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js"; import { inspectBundleKeyedArchive } from "../../../src/artifacts/apple/KeyedArchiveReader.js"; import { plistNestingLimitMessage } from "../../../src/domain/apple/plistValue.js"; +import type { OpenStepPropertyListCode } from "../../../src/domain/propertyListOpenStep.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { deeplyNestedXmlPlist } from "../../fixtures/keyedArchiveXmlNesting.js"; @@ -29,31 +30,34 @@ it.each([129, 3000])( }, ); -it.each(["(}", "{)", "(@", "{key @", "(x @", "(x ", "{key ", "{key = x,"])( - "preserves the pinned OpenStep syntax diagnosis before a deep suffix after %s", - async (prefix) => { - const text = prefix + "(".repeat(130); - let pinned: unknown; - try { - parse(text); - } catch (cause) { - pinned = cause; - } - if (!(pinned instanceof Error) || pinned instanceof RangeError) - throw new Error( - "Malformed control must fail in the pinned syntax decoder", - ); +it.each([ + ["(}", "unexpected_character", 1], + ["{)", "unexpected_character", 1], + ["(@", "unexpected_character", 1], + ["{key @", "expected_equals", 5], + ["(x @", "expected_separator", 3], + ["(x ", "expected_separator", 3], + ["{key ", "expected_equals", 5], + ["{key = x,", "unexpected_character", 8], +] as const)( + "reports OpenStep %s before a deep suffix as %s at %s", + async (prefix, code: OpenStepPropertyListCode, position) => { const root = await createTestTempDirectory("rea-keyed-openstep-malformed-"); - await writeFile(join(root, "Malformed.plist"), text); + await writeFile(join(root, "Malformed.plist"), prefix + "(".repeat(130)); const rejected = await inspectBundleKeyedArchive({ bundlePath: root, targetSha256: "a".repeat(64), parameters: { path: "Malformed.plist" }, platform: "linux", }).catch((cause: unknown) => cause); + expect(rejected).toBeInstanceOf(ArtifactReaderFailure); expect(rejected).toMatchObject({ reason: "format", - message: `Cannot decode selected Foundation keyed archive: ${pinned.message}`, + cause: { + _tag: "OpenStepPropertyListError", + code, + position, + }, }); }, ); diff --git a/tests/boundary/filesystem/ownedArtifactReaderCleanup.test.ts b/tests/boundary/filesystem/ownedArtifactReaderCleanup.test.ts index f33b1d7e2c..17812f8aa2 100644 --- a/tests/boundary/filesystem/ownedArtifactReaderCleanup.test.ts +++ b/tests/boundary/filesystem/ownedArtifactReaderCleanup.test.ts @@ -4,7 +4,10 @@ import { join } from "node:path"; import { expect, it, onTestFinished, vi } from "vitest"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { BunStandaloneArtifactReader } from "../../../src/artifacts/BunStandaloneArtifactReader.js"; +import { DotnetBundleArtifactReader } from "../../../src/artifacts/DotnetBundleArtifactReader.js"; import { MachOSliceArtifactReader } from "../../../src/artifacts/MachOSliceArtifactReader.js"; import { ZipArtifactReader } from "../../../src/artifacts/ZipArtifactReader.js"; import { ok } from "../../../src/domain/result.js"; @@ -47,13 +50,59 @@ const runner: NativeCommandRunner = { }), }; +it("reports cancellation during .NET reader admission and releases the descriptor", async () => { + const root = await createTestTempDirectory("rea-bundle-admission-cancel-"); + const path = join(root, "source"); + await writeFile(path, "unread source"); + const controller = new AbortController(); + let descriptor: FileHandle | undefined; + injected.opened = (openedPath, handle) => { + if (openedPath !== path) return; + descriptor = handle; + controller.abort(); + }; + const reader = new DotnetBundleArtifactReader(path); + onTestFinished(async () => { + injected.opened = undefined; + await reader.close(); + }); + await expect( + reader.entries(controller.signal)[Symbol.asyncIterator]().next(), + ).rejects.toMatchObject({ reason: "cancelled" }); + expect(descriptor?.fd).toBe(-1); +}); + const scenarios = [ + { + reader: "Bun standalone", + createReader: (path: string) => new BunStandaloneArtifactReader(path), + stage: "admission", + failOnStat: 1, + }, + { + reader: "Bun standalone", + createReader: (path: string) => new BunStandaloneArtifactReader(path), + stage: "post-admission-verification", + failOnStat: 2, + }, { reader: "ZIP", createReader: (path: string) => new ZipArtifactReader(path, "zip"), stage: "admission", failOnStat: 1, }, + { + reader: ".NET bundle", + createReader: (path: string) => new DotnetBundleArtifactReader(path), + stage: "admission", + failOnStat: 1, + }, + { + reader: ".NET bundle", + createReader: (path: string) => new DotnetBundleArtifactReader(path), + stage: "post-admission-verification", + failOnStat: 2, + }, { reader: "Mach-O slice", createReader: (path: string) => @@ -160,3 +209,63 @@ const causeChainContains = (cause: unknown, expected: unknown): boolean => { } return false; }; + +it("joins late .NET descriptor admission before reporting reader shutdown", async () => { + const root = await createTestTempDirectory("rea-dotnet-late-admission-"); + const path = join(root, "source"); + await writeFile(path, "unread source"); + let reach = () => {}; + let resume = () => {}; + const reached = new Promise((resolve) => { + reach = resolve; + }); + const gate = new Promise((resolve) => { + resume = resolve; + }); + let descriptor: FileHandle | undefined; + injected.opened = (openedPath, handle) => { + if (openedPath !== path || descriptor !== undefined) return; + descriptor = handle; + const stat = handle.stat.bind(handle); + let held = false; + vi.spyOn(handle, "stat").mockImplementation(async (...args) => { + if (!held) { + held = true; + reach(); + await gate; + } + return stat(...args); + }); + }; + const reader = new DotnetBundleArtifactReader(path); + const launched: Promise[] = []; + try { + const inspection = reader.entries()[Symbol.asyncIterator]().next(); + launched.push(inspection); + await awaitAcknowledgement( + reached, + inspection, + ".NET descriptor admission", + ); + let closed = false; + const closing = reader.close().then(() => { + closed = true; + }); + launched.push(closing); + await Promise.resolve(); + expect(closed).toBe(false); + resume(); + await Promise.allSettled(launched); + expect(closed).toBe(true); + expect(descriptor?.fd).toBe(-1); + } finally { + resume(); + await Promise.allSettled(launched); + try { + await reader.close(); + } finally { + injected.opened = undefined; + vi.restoreAllMocks(); + } + } +}); diff --git a/tests/boundary/filesystem/setupSkill.test.ts b/tests/boundary/filesystem/setupSkill.test.ts index 37832c546f..f4166f17c5 100644 --- a/tests/boundary/filesystem/setupSkill.test.ts +++ b/tests/boundary/filesystem/setupSkill.test.ts @@ -22,6 +22,7 @@ import { canonicalSkillNeedsInstall, existingSkillDestinations, installCanonicalSkill, + readSkillFile, readInstalledSkillIdentity, skillDestinations, type SkillInstallFileOperations, @@ -37,6 +38,21 @@ import { SkillFilePublication } from "../../../src/application/SkillFilePublicat import { z } from "zod"; import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs"; +describe("managed skill file read boundary", () => { + it("rejects linked and oversized managed guide files", async () => { + const root = await createTestTempDirectory("rea-guidance-file-boundary-"); + const target = join(root, "guide.md"); + const link = join(root, "linked-guide.md"); + const oversized = join(root, "oversized-guide.md"); + await writeFile(target, "safe guide content"); + await symlink(target, link); + await writeFile(oversized, "larger than limit"); + + await expect(readSkillFile(link)).rejects.toThrow(); + await expect(readSkillFile(oversized, undefined, 4)).rejects.toThrow(); + }); +}); + const expectPermissionMode = async ( path: string, expected: number, diff --git a/tests/boundary/filesystem/setupSkillCleanupOwnership.test.ts b/tests/boundary/filesystem/setupSkillCleanupOwnership.test.ts index a7ac0e4dbd..f142fe5edc 100644 --- a/tests/boundary/filesystem/setupSkillCleanupOwnership.test.ts +++ b/tests/boundary/filesystem/setupSkillCleanupOwnership.test.ts @@ -12,10 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js"; -import { - runDoctorOwned, - type DoctorHost, -} from "../../../src/application/Doctor.js"; +import { runDoctorOwned } from "../../../src/application/Doctor.js"; import { readSkillFile, skillDestinations, @@ -96,7 +93,7 @@ describe("setup skill backup cleanup ownership", () => { if (stage !== "verification") await writeFile(backup, original); const close = failFirstCloses(backup); const host = systemSetupHost( - { platform: "linux", homeDirectory: home } as DoctorHost, + createDoctorHostFixture({ platform: "linux", homeDirectory: home }), { HOME: home }, ); @@ -127,6 +124,7 @@ describe("setup skill backup cleanup ownership", () => { { approved: true, installHopper: false, structured: true }, { ...host, + nodeVersion: "24.22.1", initialSetupState: async () => { throw primary; }, @@ -188,7 +186,7 @@ describe("skill regular-file cleanup ownership", () => { await writeFile(manifest, "stale managed skill\n"); const close = failFirstCloses(manifest); const host = systemSetupHost( - { platform: "linux", homeDirectory: home } as DoctorHost, + createDoctorHostFixture({ platform: "linux", homeDirectory: home }), { HOME: home }, ); diff --git a/tests/boundary/firmware/firmwareIntegration.test.ts b/tests/boundary/firmware/firmwareIntegration.test.ts index daf7c9796d..0270baa030 100644 --- a/tests/boundary/firmware/firmwareIntegration.test.ts +++ b/tests/boundary/firmware/firmwareIntegration.test.ts @@ -720,6 +720,7 @@ it.each(["depth", "dependency", "link"])( relative_path: "escape", kind: "symlink", link_target: "/etc/passwd", + link_target_hex: Buffer.from("/etc/passwd", "utf8").toString("hex"), }, ]); expect(gapsWithContinuations(normalized)).toEqual([ diff --git a/tests/boundary/firmware/firmwarePublicationPaths.test.ts b/tests/boundary/firmware/firmwarePublicationPaths.test.ts index 6d21371da1..61b26a3c73 100644 --- a/tests/boundary/firmware/firmwarePublicationPaths.test.ts +++ b/tests/boundary/firmware/firmwarePublicationPaths.test.ts @@ -1,16 +1,81 @@ import { createHash } from "node:crypto"; -import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { + access, + lstat, + mkdir, + readFile, + readlink, + symlink, + writeFile, +} from "node:fs/promises"; import { join } from "node:path"; import { expect, it, vi } from "vitest"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import type { AnySchema } from "ajv"; +import { FIRMWARE_TOOL_CONTRACTS } from "../../../src/contracts/firmware/firmwareToolContracts.js"; +import { toolOutputSchemaWithMetadata } from "../../../src/contracts/toolSchemaMetadata.js"; +import { createEvidence } from "../../../src/domain/evidence.js"; +import { RequestTrackingMcpServer } from "../../../src/server/RequestTrackingMcpServer.js"; import { FirmwareProvider } from "../../../src/firmware/FirmwareProvider.js"; import { FirmwareAnalysisService } from "../../../src/application/firmware/FirmwareAnalysisService.js"; -import { FirmwareFileAccess } from "../../../src/firmware/FirmwareFiles.js"; +import { + FirmwareFileAccess, + inventoryFirmwareOutput, +} from "../../../src/firmware/FirmwareFiles.js"; import { publishFirmwareExtraction } from "../../../src/firmware/FirmwarePublication.js"; import { normalizeUnblobReport } from "../../../src/firmware/FirmwareReports.js"; import { startFirmwareDeadline } from "../../../src/firmware/FirmwareDeadline.js"; import { SafeOutputTree } from "../../../src/artifacts/SafeOutputTree.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +const validateAdvertisedFirmwareOutput = async ( + result: Awaited>, +): Promise => { + const contract = FIRMWARE_TOOL_CONTRACTS.find( + (item) => item.name === "extract_firmware", + ); + if (contract === undefined) + throw new Error("Missing extract_firmware contract"); + const server = new RequestTrackingMcpServer({ + name: "schema", + version: "1", + }); + const client = new Client({ name: "schema-client", version: "1" }); + server.registerTool( + contract.name, + { outputSchema: toolOutputSchemaWithMetadata(contract) }, + () => ({ content: [] }), + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const schema = (await client.listTools()).tools.find( + (tool) => tool.name === contract.name, + )?.outputSchema; + if (schema === undefined) throw new Error("Missing firmware output schema"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + expect(ajv.validateSchema(schema as AnySchema)).toBe(true); + const validate = ajv.compile(schema as AnySchema); + const evidence = createEvidence( + undefined, + { id: "fixture", name: "fixture", version: null }, + { + operation: "extract_firmware", + parameters: {}, + result, + confidence: "observed", + }, + ); + expect(validate(evidence), JSON.stringify(validate.errors)).toBe(true); + } finally { + await client.close(); + await server.close(); + } +}; + it.each(["existing", "missing-parent", "relative"] as const)( "refuses %s destinations before selecting or launching a firmware engine", async (kind) => { @@ -70,6 +135,14 @@ it("publishes the writer's normalized path while retaining source spelling and u const bytes = Buffer.from("child"); const path = join(staging, name); await writeFile(path, bytes); + if (process.platform !== "win32") { + await symlink( + Buffer.from([0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0xff]), + join(staging, "raw-link"), + ); + await symlink("sample\ufffd", join(staging, "unicode-link")); + await symlink("sample", join(staging, "control-link")); + } const sha256 = createHash("sha256").update(bytes).digest("hex"); const inputPath = join(root, "input"); const inputHash = "a".repeat(64); @@ -111,15 +184,12 @@ it("publishes the writer's normalized path while retaining source spelling and u deadline: startFirmwareDeadline("publication", 120_000), analysisTimeoutMs: 120_000, outputDirectory: output, - entries: [ - { - path, - relativePath: name, - size: bytes.length, - kind: "file", - linkTarget: null, - }, - ], + entries: await inventoryFirmwareOutput( + staging, + 1024, + 10, + "extract_firmware", + ), normalized, selection: { offset: 7, length: 1, sha256: inputHash }, maxDepth: 3, @@ -144,18 +214,46 @@ it("publishes the writer's normalized path while retaining source spelling and u const published = result.files[0]; if (published === undefined) throw new Error("Expected published child"); expect(Buffer.compare(await readFile(published.path), bytes)).toBe(0); + if (process.platform !== "win32") { + expect(result.unpublished_entries).toEqual([ + { + relative_path: "control-link", + kind: "symlink", + link_target: "sample", + link_target_hex: "73616d706c65", + }, + { + relative_path: "raw-link", + kind: "symlink", + link_target: null, + link_target_hex: "73616d706c65ff", + }, + { + relative_path: "unicode-link", + kind: "symlink", + link_target: "sample\ufffd", + link_target_hex: "73616d706c65efbfbd", + }, + ]); + const rawLink = join(output, "raw-link"); + await expect(lstat(rawLink)).rejects.toMatchObject({ code: "ENOENT" }); + await expect(readlink(rawLink)).rejects.toMatchObject({ + code: "ENOENT", + }); + await validateAdvertisedFirmwareOutput(result); + } expect(result).toMatchObject({ coverage: "partial", - extraction_gaps: [ - { + extraction_gaps: expect.arrayContaining([ + expect.objectContaining({ kind: "unclassified-range", input_path: "$input", provider_id: null, handler: null, range: { offset: 0, length: 1 }, root_file_range: { offset: 7, length: 1 }, - }, - ], + }), + ]), }); expect(retainCleanup).not.toHaveBeenCalled(); } finally { diff --git a/tests/boundary/guidanceSearch.test.ts b/tests/boundary/guidanceSearch.test.ts new file mode 100644 index 0000000000..6a4148621c --- /dev/null +++ b/tests/boundary/guidanceSearch.test.ts @@ -0,0 +1,53 @@ +import { expect, it } from "vitest"; +import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; +import { + readGuidanceSection, + searchGuidance, +} from "../../src/application/GuidanceAccess.js"; + +it("finds and retrieves the selected section from the generated package bundle", async () => { + const resources = new ArtifactResourceScope(); + try { + const result = await searchGuidance( + { query: "exact JavaScript module" }, + resources, + ); + if (!result.ok) throw result.error; + expect(result.value).toMatchObject({ + status: "matches", + source: "installed_rea_guidance", + package: expect.stringMatching(/^rea-agents@/u), + }); + if (result.value.status !== "matches") + throw new Error("Expected guidance matches"); + const match = result.value.matches.find( + ({ heading }) => heading === "Read a module without repeating analysis", + ); + expect(match).toBeDefined(); + expect(result.value.matches[0]?.heading).toBe( + "Read a module without repeating analysis", + ); + expect(match).toMatchObject({ + guide: "references/javascript-applications.md", + matched_terms: expect.arrayContaining(["exact", "javascript", "module"]), + context: expect.stringContaining("javascript-module"), + }); + if (match === undefined) throw new Error("Expected selected guidance"); + + const retrieved = await readGuidanceSection( + { locator: match.locator }, + resources, + ); + if (!retrieved.ok) throw retrieved.error; + expect(retrieved.value).toMatchObject({ + status: "section", + source: "installed_rea_guidance", + guide: match.guide, + heading: match.heading, + locator: match.locator, + content: expect.stringContaining("node_id"), + }); + } finally { + await resources.close(); + } +}); diff --git a/tests/boundary/javascriptSemanticTraceEvidence.test.ts b/tests/boundary/javascriptSemanticTraceEvidence.test.ts index 7cb46c65f1..33a464a14b 100644 --- a/tests/boundary/javascriptSemanticTraceEvidence.test.ts +++ b/tests/boundary/javascriptSemanticTraceEvidence.test.ts @@ -7,7 +7,7 @@ import { Ajv2020 } from "ajv/dist/2020.js"; import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js"; import { parseMcpToolError } from "../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; -import { closeCliJavaScriptResources, createCli } from "../../src/cli.js"; +import { closeCliResources, createCli } from "../../src/cli.js"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import type { JsonValue } from "../../src/domain/jsonValue.js"; import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js"; @@ -20,7 +20,7 @@ const SEMANTIC_GRAPH_ID = `jsrg_${"0".repeat(64)}`; // In-process serve keeps the CLI worker for this file. Stop it before the // worker process exits so the private runtime root is not left behind. -afterAll(closeCliJavaScriptResources); +afterAll(closeCliResources); it("reports invalid nested semantic graph Evidence as caller input through CLI and SDK MCP", async () => { const root = await createTestTempDirectory("rea-semantic-trace-input-"); @@ -273,6 +273,90 @@ it("traces exact empty property names through CLI and MCP", async () => { } }); +it("traces transformed operands through CLI and advertised MCP output", async () => { + const root = await createTestTempDirectory("rea-semantic-expression-trace-"); + await writeFile( + join(root, "main.js"), + 'const base = "https://example.test"; const url = base + "/x"; fetch(url); consume("only-direct"); const nested = "nested-token"; const box = { url: wrap(nested + "/x") };', + ); + const cli = createCli({}); + const analyzed = await serveCli(cli, [ + "analyze-javascript-application", + root, + "--json", + ]); + expect(analyzed.exitCode, analyzed.stdout).toBe(0); + const application = parseEvidence(JSON.parse(analyzed.stdout)); + const harness = await createApplicationMcpHarness(); + try { + const result = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "literal", value: "https://example.test" }, + direction: "forward-influence", + }, + ); + expect(result.nodes.map((node) => node.label)).toContain("argument 0"); + expect( + result.relations.some((edge) => edge.relation === "influences"), + ).toBe(true); + const direct = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "literal", value: "only-direct" }, + direction: "forward-influence", + }, + ); + expect(direct.summary.total_seed_matches).toBe(1); + expect( + direct.nodes.find((node) => node.node_id === direct.seed_node_ids[0]) + ?.label, + ).toBe("argument 0"); + const nested = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "literal", value: "nested-token" }, + direction: "forward-influence", + }, + ); + expect(nested.status).not.toBe("found"); + expect(nested.unknowns.map((unknown) => unknown.detail)).toContain( + "The dynamic call's return dependency on this argument is unknown.", + ); + } finally { + await harness.close(); + } +}); + +it("reports offered semantic capacity when a large template exceeds it", async () => { + const root = await createTestTempDirectory("rea-semantic-expression-budget-"); + const expressions = Array.from({ length: 500 }, () => "${base}").join(""); + await writeFile( + join(root, "main.js"), + `const base = "token"; const value = \`${expressions}\`;`, + ); + const analyzed = await serveCli(createCli({}), [ + "analyze-javascript-application", + root, + "--max-semantic-nodes-per-file", + "4", + "--json", + ]); + expect(analyzed.exitCode, analyzed.stdout).toBe(0); + const application = parseEvidence(JSON.parse(analyzed.stdout)); + const result = javascriptApplicationAnalysisResultSchema.parse( + application.normalized_result, + ); + expect(result.semantic_graph.coverage.truncated).toBe(true); + expect(result.semantic_graph.coverage.status).toBe("partial"); +}); + const assertSuccessfulTraceParity = async ( cli: ReturnType, harness: Awaited>, diff --git a/tests/boundary/mcp/adbMcp.test.ts b/tests/boundary/mcp/adbMcp.test.ts index c0f08cae21..461503e45f 100644 --- a/tests/boundary/mcp/adbMcp.test.ts +++ b/tests/boundary/mcp/adbMcp.test.ts @@ -407,10 +407,9 @@ it("rejects invalid tool input before reaching the provider", async () => { operation: "inspect_adb_device", issues: [ { - path: [], - reason: "invalid_value", - message: - "serial: Device serial cannot contain whitespace or quotes", + path: ["serial"], + reason: "invalid_format", + expected: "regex", }, ], }, diff --git a/tests/boundary/mcp/addressContextBookmarks.test.ts b/tests/boundary/mcp/addressContextBookmarks.test.ts new file mode 100644 index 0000000000..ed97c4a055 --- /dev/null +++ b/tests/boundary/mcp/addressContextBookmarks.test.ts @@ -0,0 +1,87 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { expect, it } from "vitest"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import { AnalysisProtocolError } from "../../../src/domain/analysisErrorCore.js"; +import { err, ok } from "../../../src/domain/result.js"; +import { createServer } from "../../../src/server/createServer.js"; + +it.each([ + ["resolved padded", true, "0x00401000", "0x401000", "available"], + ["provider padded", true, "0x401000", "0x00401000", "available"], + ["failed padded", false, "0x00401000", "0x401000", "available"], + ["failed canonical", false, "0x401000", "0x401000", "available"], + ["unknown identity", false, "entry", "0x401000", "unavailable"], + ["unknown default space", false, "0x401000", "ram:0x401000", "unavailable"], + [ + "different named spaces", + false, + "rom:0x401000", + "ram:0x401000", + "unavailable", + ], +] as const)( + "preserves bookmark identity: %s", + async (_label, resolves, address, bookmarkAddress, state) => { + const identity = { + id: "fixture", + name: "Fixture analysis provider", + version: "1", + }; + const bookmark = { address: bookmarkAddress, name: "review" }; + const directory = await createTestTempDirectory("rea-address-bookmark-"); + const target = join(directory, "fixture.com"); + await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21])); + const session = createTestBinarySession(() => ({ + execute: async (operation) => { + if (operation === "resolve_containing_procedure" && !resolves) + return err(new AnalysisProtocolError("containment read failed")); + const value = + operation === "resolve_containing_procedure" + ? { + query_address: "0x401000", + found: false, + procedure: null, + reason: "not_in_procedure", + } + : operation === "list_bookmarks" + ? [bookmark, { address: "0x402000", name: "other" }] + : operation === "address_name" + ? "entry" + : null; + return ok(createAnalysisExecution(value, identity)); + }, + close: async () => ok(null), + })); + const server = createServer({ kind: "session", session }); + const client = new Client({ name: "bookmark-verifier", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const opened = await client.callTool({ + name: "open_binary", + arguments: { path: target, format: "dos-com" }, + }); + expect(opened.isError).not.toBe(true); + const response = await client.callTool({ + name: "inspect_address_context", + arguments: { address, document: "fixture" }, + }); + expect(response.isError).not.toBe(true); + const body = response.structuredContent as { + result: { bookmarks: { state: string; value?: unknown } }; + }; + expect(body.result.bookmarks.state).toBe(state); + if (state === "available") + expect(body.result.bookmarks.value).toEqual([bookmark]); + } finally { + await client.close(); + await server.close(); + } + }, +); diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index f09c776a75..47a50ac94f 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -1596,6 +1596,19 @@ it("advertises Mach-O pages whose selected facet owns their exact rows", async ( ]), exports: coverage([symbol]), segments: coverage([]), + encryption: coverage([ + { + architecture: "arm64", + cpu_type: 16777228, + cpu_subtype: 0, + container_file_offset: 0, + size: 4096, + status: "absent", + reason: null, + commands: [], + range: null, + }, + ]), provenance: [], limitations: [], }); @@ -1611,6 +1624,7 @@ it("advertises Mach-O pages whose selected facet owns their exact rows", async ( "imports", "exports", "segments", + "encryption", ] as const) { for (const offset of [0, 999]) { const response = await client.callTool({ diff --git a/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts b/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts index 59a2274eff..bf6098c6a2 100644 --- a/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts @@ -175,13 +175,7 @@ cliTest.for(["full", "compact"] as const)( recovery: { kind: "inspect_input_contract", operation: tool.name }, details: { operation: tool.name, - issues: [ - { - message: expect.stringContaining( - `${scenario.issue.path.join(".")}: Too big: expected number to be <=2147483647`, - ), - }, - ], + issues: [scenario.issue], }, }); } diff --git a/tests/boundary/mcp/bunStandaloneMcp.test.ts b/tests/boundary/mcp/bunStandaloneMcp.test.ts new file mode 100644 index 0000000000..892250b021 --- /dev/null +++ b/tests/boundary/mcp/bunStandaloneMcp.test.ts @@ -0,0 +1,59 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { + bunModuleGraphPayload, + elfAppendedBunPayload, +} from "../../../src/artifacts/bun/BunStandalone.fixture.js"; + +it("exposes Bun graph metadata through the existing MCP artifact tool", async () => { + const root = await createTestTempDirectory("rea-bun-mcp-"); + const path = join(root, "app"); + await writeFile( + path, + elfAppendedBunPayload( + bunModuleGraphPayload({ + modules: [ + { name: "/$bunfs/root/main.js", contents: Buffer.from("hello") }, + ], + }), + ), + ); + const session = createTestBinarySession(new ArtifactProvider({})); + const server = createServer({ kind: "session", session }); + const client = new Client({ name: "bun-contract-test", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + expect( + (await client.callTool({ name: "open_binary", arguments: { path } })) + .isError, + ).not.toBe(true); + const reply = await client.callTool({ + name: "inspect_artifact", + arguments: { detail: "summary" }, + }); + expect(reply.isError).not.toBe(true); + const summary = artifactInventorySummaryViewResultSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ); + expect(summary.summary.root_format_metadata).toMatchObject({ + kind: "bun-standalone", + module_count: 1, + entry_point_source_name: "/$bunfs/root/main.js", + }); + } finally { + await client.close(); + await server.close(); + await session.close(); + } +}); diff --git a/tests/boundary/mcp/bundleComparisonMcp.test.ts b/tests/boundary/mcp/bundleComparisonMcp.test.ts index 9020ff9320..7128d196ef 100644 --- a/tests/boundary/mcp/bundleComparisonMcp.test.ts +++ b/tests/boundary/mcp/bundleComparisonMcp.test.ts @@ -165,10 +165,19 @@ it("requires absolute bundle paths without changing server-cwd semantics", async expect(error).toMatchObject({ code: "invalid_request", category: "invalid_input", - details: { operation: "compare_bundles" }, + details: { + operation: "compare_bundles", + issues: expect.arrayContaining( + rejectedFields.map((field) => + expect.objectContaining({ + path: [field], + reason: "invalid_format", + expected: "regex", + }), + ), + ), + }, }); - const details = JSON.stringify(error.details); - for (const field of rejectedFields) expect(details).toContain(field); } expect(connected.session.exportEvidenceBundle().records).toEqual([]); expect( @@ -195,9 +204,15 @@ it("requires absolute bundle paths without changing server-cwd semantics", async expect(parseMcpToolError(relativeImport).error).toMatchObject({ code: "invalid_request", category: "invalid_input", - }); - expect(relativeImport.content[0]).toMatchObject({ - text: expect.stringContaining("absolute local filesystem path"), + details: { + operation: "import_evidence_bundle", + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ["path"], + reason: "invalid_format", + }), + ]), + }, }); const absolute = await compare( join(root, "left.json"), diff --git a/tests/boundary/mcp/dylibResolutionMcp.test.ts b/tests/boundary/mcp/dylibResolutionMcp.test.ts index f7b0990d65..e36fba4af0 100644 --- a/tests/boundary/mcp/dylibResolutionMcp.test.ts +++ b/tests/boundary/mcp/dylibResolutionMcp.test.ts @@ -373,6 +373,38 @@ it("follows relative symlinks only while they stay inside the root", async () => expect(JSON.stringify(trace)).not.toContain(outsideSha256); }); +it.skipIf(process.platform === "win32")( + "does not resolve a non-UTF-8 link target as a replacement-character path", + async () => { + const { trace } = await traceBesideOutside( + [dylibCommand(LC.LOAD_DYLIB, "@loader_path/raw-link")], + async (analyzed) => { + await symlink( + Buffer.from([0x6c, 0x69, 0x62, 0xff]), + join(analyzed, "raw-link"), + ); + await writeFile( + join(analyzed, "lib\ufffd"), + machoImage({ + fileType: FILE_TYPE.dylib, + commands: [buildVersionCommand(1)], + }), + ); + }, + ); + expect(trace.edges[0]?.resolution).toEqual({ + status: "undetermined", + image: null, + }); + expect(trace.coverage.status).toBe("partial"); + expect(trace.limitations).toEqual( + expect.arrayContaining([ + expect.stringContaining('"raw-link":"6c6962ff"'), + ]), + ); + }, +); + it.skipIf(process.platform === "win32")( "resolves a POSIX file name that contains backslashes", async () => { diff --git a/tests/boundary/mcp/flutterMcp.test.ts b/tests/boundary/mcp/flutterMcp.test.ts index bc9b6a078c..1d005b0be2 100644 --- a/tests/boundary/mcp/flutterMcp.test.ts +++ b/tests/boundary/mcp/flutterMcp.test.ts @@ -10,7 +10,11 @@ import { ok } from "../../../src/domain/result.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createFlutterBuildAnalysisProvider } from "../../../src/composition/flutter.js"; -import { flutterResultSchemas } from "../../../src/domain/flutter/flutterBuildAnalysis.js"; +import { + ABI_SELECTOR_MESSAGE, + flutterResultSchemas, +} from "../../../src/domain/flutter/flutterBuildAnalysis.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { aotFixtureImage } from "../../../src/flutter/FlutterBuild.fixture.js"; import { createFlutterApk } from "../../fixtures/flutterApk.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; @@ -255,12 +259,18 @@ it("advertises and enforces the same Unicode selector grammar through MCP", asyn name: "inspect_dart_aot", arguments: arguments_, }); - expect(refused.isError).toBe(true); - expect(refused.content).toContainEqual({ - type: "text", - text: expect.stringContaining( - "abi: ABI must be the exact lib/ directory label", - ), + expect(parseMcpToolError(refused).error).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + operation: "inspect_dart_aot", + issues: [ + { + path: ["abi"], + message: ABI_SELECTOR_MESSAGE, + }, + ], + }, }); } }); diff --git a/tests/boundary/mcp/gdbRetirementCancellation.test.ts b/tests/boundary/mcp/gdbRetirementCancellation.test.ts new file mode 100644 index 0000000000..54e0e41c43 --- /dev/null +++ b/tests/boundary/mcp/gdbRetirementCancellation.test.ts @@ -0,0 +1,390 @@ +import type { CallToolResult, McpServer } from "@modelcontextprotocol/server"; +import { expect, it, vi } from "vitest"; +import { + registerGdbTools, + type GdbToolManager, +} from "../../../src/server/registerGdbTools.js"; +import { createToolResultDelivery } from "../../../src/server/toolResult.js"; +import { silentLogger } from "../../../src/logger.js"; +import { createEvidence } from "../../../src/domain/evidence.js"; +import { ok, err } from "../../../src/domain/result.js"; +import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js"; +import { primaryAnalysisError } from "../../../src/domain/analysisErrorBase.js"; +import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; +import { + createAnalysisExecution, + type AnalysisClient, + type AnalysisProvider, +} from "../../../src/application/AnalysisProvider.js"; +import { CompositeProvider } from "../../../src/application/binary/CompositeProvider.js"; +import { runWithExternalMutationAdmission } from "../../../src/server/externalMutationAdmission.js"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; +import { + createBinarySessionTargets, + createDeferred, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; + +it( + "compares cancellation before dispatch, during retirement, and an uncancelled control", + { timeout: 8000 }, + async () => { + for (const mode of [ + "pre-aborted", + "during-retirement", + "control", + ] as const) { + const [target] = await createBinarySessionTargets(); + const entered = createDeferred(); + const released = createDeferred(); + let closes = 0; + const session = createTestBinarySession( + () => + ({ + execute: async () => ok({ operation: "health", value: null }), + close: async () => { + closes++; + entered.resolve(); + await released.promise; + return ok(null); + }, + }) as never, + ); + expect((await session.open(target)).ok).toBe(true); + const id = "00000000-0000-4000-8000-000000000001"; + const evidence = createEvidence( + undefined, + { id: "gdb", name: "GDB", version: null }, + { + predicateType: "rea.debugger.command-observation", + operation: "gdb_console", + parameters: { session_id: id, command: "info files" }, + result: { command: "info files", completion_status: "complete" }, + rawResult: { command: "info files" }, + }, + ); + let handler!: ( + input: unknown, + context: unknown, + ) => Promise; + const server = { + registerTool(name: string, _options: unknown, fn: typeof handler) { + if (name === "gdb_console") handler = fn; + }, + } as unknown as McpServer; + const execute = vi.fn(async () => ok({ evidence })); + const manager = { + execute, + retryCleanupFailures: async () => ok(null), + } as unknown as GdbToolManager; + const record = vi.fn((value: typeof evidence) => + session.recordEvidence(value), + ); + registerGdbTools( + server, + manager, + silentLogger, + createToolResultDelivery(undefined), + record, + session, + ); + const controller = new AbortController(); + const reason = { phase: mode }; + if (mode === "pre-aborted") controller.abort(reason); + try { + const pending = handler( + { session_id: id, command: "info files", detail: "complete" }, + { mcpReq: { signal: controller.signal } }, + ); + if (mode !== "pre-aborted") { + await awaitAcknowledgement( + entered.promise, + pending, + "session retirement", + ); + expect(execute).toHaveBeenCalledTimes(1); + expect(record).not.toHaveBeenCalled(); + if (mode === "during-retirement") controller.abort(reason); + } + released.resolve(); + const result = await pending; + const body = result.structuredContent as any; + const observation = { + mode, + aborted: controller.signal.aborted, + executions: execute.mock.calls.length, + retirementCloses: closes, + recordings: record.mock.calls.length, + isError: result.isError === true, + code: body.error?.code ?? null, + hasEvidence: typeof body.evidence_id === "string", + stored: session.hasEvidence(evidence.evidence_id), + }; + expect(observation).toEqual({ + mode, + aborted: mode !== "control", + executions: mode === "pre-aborted" ? 0 : 1, + retirementCloses: mode === "pre-aborted" ? 0 : 1, + recordings: mode === "pre-aborted" ? 0 : 1, + isError: mode !== "control", + code: mode === "control" ? null : "cancelled", + hasEvidence: mode === "control", + stored: mode !== "pre-aborted", + }); + + if (mode === "during-retirement") + expect(record.mock.calls[0]?.[0]).toEqual(evidence); + } finally { + released.resolve(); + await session.close(); + } + } + }, +); + +it.each(["completed", "failed"] as const)( + "preserves a %s command and cleanup ownership when cancellation overlaps failed retirement", + async (mode) => { + const [target] = await createBinarySessionTargets(); + const entered = createDeferred(); + const released = createDeferred(); + const cleanup = new ProviderCleanupError("fixture", ["document"], {}); + let closes = 0; + const session = createTestBinarySession( + () => + ({ + execute: async () => ok({ operation: "health", value: null }), + close: async () => { + if (++closes === 1) { + entered.resolve(); + await released.promise; + return err(cleanup); + } + return ok(null); + }, + }) as never, + ); + expect((await session.open(target)).ok).toBe(true); + const commandFailure = new AnalysisInputError("command"); + const evidence = createEvidence( + undefined, + { id: "gdb", name: "GDB", version: null }, + { + operation: "gdb_console", + parameters: {}, + result: { completed: true }, + }, + ); + const controller = new AbortController(); + const reason = { stage: "retirement" }; + try { + const pending = runWithExternalMutationAdmission( + session, + "gdb_console", + controller.signal, + async () => (mode === "completed" ? ok(evidence) : err(commandFailure)), + (value) => value, + ); + await awaitAcknowledgement( + entered.promise, + pending, + "failed session retirement", + ); + controller.abort(reason); + released.resolve(); + const result = await pending; + if (result.ok) throw new Error("Expected a failure"); + expect(result.error.cleanupIncomplete).toBe(true); + const primary = primaryAnalysisError(result.error); + expect( + primary.secondaryFailures.some((failure) => failure.cause === reason), + ).toBe(true); + if (mode === "completed") + expect(result.error.partialObservation).toEqual(evidence); + else expect(primary).toBe(commandFailure); + expect(await session.status()).toMatchObject({ cleanup_pending: true }); + expect((await session.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + released.resolve(); + await session.close(); + } + }, +); + +it.each(["started", "failed"] as const)( + "keeps a %s mutation outcome when successful retirement is cancelled", + async (mode) => { + const [target] = await createBinarySessionTargets(); + const entered = createDeferred(); + const released = createDeferred(); + const session = createTestBinarySession( + () => + ({ + execute: async () => ok({ operation: "health", value: null }), + close: async () => { + entered.resolve(); + await released.promise; + return ok(null); + }, + }) as never, + ); + expect((await session.open(target)).ok).toBe(true); + const failure = new AnalysisInputError("start_rizin_debug_session"); + const reason = { stage: "retirement" }; + const controller = new AbortController(); + try { + const pending = runWithExternalMutationAdmission( + session, + "start_rizin_debug_session", + controller.signal, + async () => + mode === "started" ? ok({ session_id: "retained" }) : err(failure), + undefined, + undefined, + (value) => ({ session_id: value.session_id }), + ); + await awaitAcknowledgement( + entered.promise, + pending, + "session retirement", + ); + controller.abort(reason); + released.resolve(); + const result = await pending; + if (result.ok) throw new Error("Expected a failure"); + if (mode === "started") { + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + expect(result.error.diagnostics).toEqual({ session_id: "retained" }); + } else { + expect(result.error).toBe(failure); + expect( + result.error.secondaryFailures.some((item) => item.cause === reason), + ).toBe(true); + } + } finally { + released.resolve(); + await session.close(); + } + }, +); + +it("follows the original owner of a completed mutation's retirement failure", async () => { + const [target] = await createBinarySessionTargets(); + let closed = false; + const cleanup = new ProviderCleanupError( + "ida", + ["workspace"], + {}, + ).bindCleanupResourceOwner({ + get cleanupResources() { + return closed ? [] : ["workspace"]; + }, + }); + const session = createTestBinarySession( + () => + ({ + execute: async () => ok({ operation: "health", value: null }), + close: async () => { + if (!closed) return err(cleanup); + cleanup.resolveCleanup(); + return ok(null); + }, + }) as never, + ); + expect((await session.open(target)).ok).toBe(true); + try { + const result = await runWithExternalMutationAdmission( + session, + "gdb_console", + undefined, + async () => ok(null), + undefined, + ); + if (result.ok) throw new Error("Expected cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + closed = true; + expect((await session.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + closed = true; + await session.close(); + } +}); + +it("resolves nested unowned child cleanup errors after the client confirms retry", async () => { + const [target] = await createBinarySessionTargets(); + const first = new ProviderCleanupError("hopper", ["hopper"], {}); + const second = new ProviderCleanupError("auxiliary", ["file"], {}); + const child = ( + id: string, + operation: "address_name" | "procedure_info", + failure: ProviderCleanupError, + ): AnalysisProvider => { + const identity = { id, name: id, version: "1" }; + let attempts = 0; + return { + identity: () => identity, + capabilities: () => [ + { + provider: identity, + operation, + available: true, + reason: null, + effects: { + mutatesArtifact: false, + launchesProcess: false, + mayShowUi: false, + mayAccessNetwork: false, + mayWriteFilesystem: false, + changesPermissions: false, + requiresRoot: false, + }, + limitations: [], + }, + ], + createClient: (): AnalysisClient => ({ + execute: async (name) => ok(createAnalysisExecution(name, identity)), + close: async () => (++attempts === 1 ? err(failure) : ok(null)), + }), + }; + }; + const composite = new CompositeProvider([ + child("hopper", "address_name", first), + child("auxiliary", "procedure_info", second), + ]); + const session = createTestBinarySession((target) => { + const client = composite.createClient(target); + let initialized = false; + return { + ...client, + execute: async (operation, parameters, options) => { + if (!initialized && operation === "health") { + initialized = true; + await client.execute("address_name", {}); + await client.execute("procedure_info", {}); + } + return client.execute(operation, parameters, options); + }, + }; + }); + expect((await session.open(target)).ok).toBe(true); + try { + const result = await runWithExternalMutationAdmission( + session, + "gdb_console", + undefined, + async () => ok(null), + undefined, + ); + if (result.ok) throw new Error("Expected cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect((await session.close()).ok).toBe(true); + expect(first.cleanupIncomplete).toBe(false); + expect(second.cleanupIncomplete).toBe(false); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + await session.close(); + } +}); diff --git a/tests/boundary/mcp/javascriptLiteralPayload.test.ts b/tests/boundary/mcp/javascriptLiteralPayload.test.ts index b35cdbd3bd..fb8442f5db 100644 --- a/tests/boundary/mcp/javascriptLiteralPayload.test.ts +++ b/tests/boundary/mcp/javascriptLiteralPayload.test.ts @@ -8,6 +8,7 @@ import { expect } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js"; +import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -112,8 +113,8 @@ cliTest( node.kind === "literal" && (node.properties.value === 1 || node.properties.value === "1"), ); - expect(choices.length).toBe(2); - expect(new Set(choices.map((node) => node.node_id)).size).toBe(2); + expect(choices.length).toBe(4); + expect(new Set(choices.map((node) => node.node_id)).size).toBe(4); expect( graph.nodes.some( (node) => node.kind === "literal" && node.properties.value === "", @@ -169,7 +170,8 @@ cliTest( expect(valueTrace.seed_node_ids).toEqual( choices .filter((node) => node.properties.value === "1") - .map((node) => node.node_id), + .map((node) => node.node_id) + .sort(compareUnicodeCodePoints), ); await client.ping(); } finally { diff --git a/tests/boundary/mcp/nativeCallObservationMcp.test.ts b/tests/boundary/mcp/nativeCallObservationMcp.test.ts index 12a8f647b7..fe81631688 100644 --- a/tests/boundary/mcp/nativeCallObservationMcp.test.ts +++ b/tests/boundary/mcp/nativeCallObservationMcp.test.ts @@ -156,11 +156,9 @@ it("routes observe_native_calls through MCP with schema-checked input and output details: { issues: [ expect.objectContaining({ - path: [], - reason: "invalid_value", - message: expect.stringMatching( - /environment\.__proto__:.*expected string, received number/u, - ), + path: ["environment", "__proto__"], + reason: "invalid_type", + expected: "string", }), ], }, diff --git a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts index b7a87c3f53..5d2337d070 100644 --- a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts +++ b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts @@ -132,6 +132,7 @@ const binaryUncertainFixture = () => { imports: empty, exports: empty, segments: incomplete(segment), + encryption: empty, provenance: [], limitations: [], }); @@ -298,6 +299,7 @@ mcpTest( imports: { ...empty, items: [imported], total: 1 }, exports: { ...empty, items: [exported], total: 1 }, segments: empty, + encryption: empty, provenance: [], limitations: [], }); @@ -399,7 +401,12 @@ describe.skipIf(process.platform !== "darwin")( const report = full.normalized_result; expect(summary.summary.uuid).toBe(report.uuid); expect(summary.summary.provenance_count).toBe(report.provenance.length); - for (const name of ["imports", "exports", "dependencies"] as const) + for (const name of [ + "imports", + "exports", + "dependencies", + "encryption", + ] as const) expect(summary.summary.collections[name]).toEqual({ returned: report[name].items.length, total: report[name].total, @@ -422,6 +429,7 @@ describe.skipIf(process.platform !== "darwin")( "imports", "exports", "segments", + "encryption", "provenance", ] as const; for (const facet of facets) { diff --git a/tests/boundary/mcp/promptMcp.test.ts b/tests/boundary/mcp/promptMcp.test.ts index 5e69332415..7b732a78d0 100644 --- a/tests/boundary/mcp/promptMcp.test.ts +++ b/tests/boundary/mcp/promptMcp.test.ts @@ -19,14 +19,12 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createAnalysisExecution, type AnalysisClient, + type AnalysisProvider, + type AnalysisProviderCandidate, + type CapabilityDescriptor, + type ProviderIdentity, } from "../../../src/application/AnalysisProvider.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; -import type { - AnalysisProvider, - AnalysisProviderCandidate, - CapabilityDescriptor, - ProviderIdentity, -} from "../../../src/application/AnalysisProvider.js"; import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; diff --git a/tests/boundary/mcp/registerGuidanceTools.test.ts b/tests/boundary/mcp/registerGuidanceTools.test.ts new file mode 100644 index 0000000000..174f6b3372 --- /dev/null +++ b/tests/boundary/mcp/registerGuidanceTools.test.ts @@ -0,0 +1,127 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { afterEach, expect, it } from "vitest"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { + readGuidanceSectionOutputSchema, + searchGuidanceInputSchema, + searchGuidanceOutputSchema, +} from "../../../src/contracts/guidanceContracts.js"; +import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; +import { registerGuidanceTools } from "../../../src/server/registerGuidanceTools.js"; +import { ToolResultDelivery } from "../../../src/server/ToolResultDelivery.js"; + +const open: { + client: Client; + server: EvidenceMcpServer; + resources: ArtifactResourceScope; +}[] = []; +const guidanceOutput = ( + schema: { parse(value: unknown): T }, + value: unknown, +): T => { + // MCP's legacy output-schema projection wraps root-level union schemas. + // Newer transports pass the object unchanged. + if (typeof value === "object" && value !== null && "result" in value) + return schema.parse(value.result); + return schema.parse(value); +}; +afterEach(async () => { + await Promise.all( + open + .splice(0) + .flatMap(({ client, server, resources }) => [ + client.close(), + server.close(), + resources.close(), + ]), + ); +}); + +it("advertises strict provider-free contracts and returns packaged guide results over MCP", async () => { + const server = new EvidenceMcpServer( + { name: "guidance-test", version: "1" }, + { capabilities: { tools: { listChanged: true } } }, + undefined, + new ToolResultDelivery(1024 * 1024), + ); + const resources = new ArtifactResourceScope(); + registerGuidanceTools(server, resources); + const client = new Client({ name: "guidance-client", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + open.push({ client, server, resources }); + + const listed = await client.listTools(); + expect(listed.tools.map(({ name }) => name)).toEqual( + expect.arrayContaining(["search_guidance", "read_guidance_section"]), + ); + expect( + searchGuidanceInputSchema.safeParse({ + query: "module", + unexpected: true, + }).success, + ).toBe(false); + const invalidLocator = await client.callTool({ + name: "read_guidance_section", + arguments: { locator: "../../outside.md" }, + }); + expect(invalidLocator.isError).toBe(true); + const invalidLimit = await client.callTool({ + name: "search_guidance", + arguments: { query: "module", limit: 21 }, + }); + expect(invalidLimit.isError).toBe(true); + const response = await client.callTool({ + name: "search_guidance", + arguments: { query: "exact JavaScript module" }, + }); + expect(response.isError).not.toBe(true); + const parsed = guidanceOutput( + searchGuidanceOutputSchema, + response.structuredContent, + ); + expect(parsed).toMatchObject({ + status: "matches", + source: "installed_rea_guidance", + }); + if (parsed.status !== "matches") throw new Error("Expected guidance matches"); + const selected = parsed.matches.find( + ({ heading }) => heading === "Read a module without repeating analysis", + ); + expect(selected).toBeDefined(); + const section = await client.callTool({ + name: "read_guidance_section", + arguments: { locator: selected?.locator }, + }); + expect(section.isError).not.toBe(true); + expect( + guidanceOutput(readGuidanceSectionOutputSchema, section.structuredContent), + ).toMatchObject({ + status: "section", + heading: selected?.heading, + source: "installed_rea_guidance", + }); + const firstPage = await client.callTool({ + name: "search_guidance", + arguments: { query: "module", limit: 1 }, + }); + const firstResult = guidanceOutput( + searchGuidanceOutputSchema, + firstPage.structuredContent, + ); + if (firstResult.status !== "matches" || firstResult.next_cursor === undefined) + throw new Error("Expected a guidance continuation token"); + const nextPage = await client.callTool({ + name: "search_guidance", + arguments: { + query: "module", + limit: 1, + cursor: firstResult.next_cursor, + }, + }); + expect( + guidanceOutput(searchGuidanceOutputSchema, nextPage.structuredContent), + ).toMatchObject({ status: "matches", offset: 1 }); +}); diff --git a/tests/boundary/mcp/sdkInputIssueAttribution.test.ts b/tests/boundary/mcp/sdkInputIssueAttribution.test.ts new file mode 100644 index 0000000000..e554827518 --- /dev/null +++ b/tests/boundary/mcp/sdkInputIssueAttribution.test.ts @@ -0,0 +1,381 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { expect, it } from "vitest"; +import { z } from "zod"; +import { createServer } from "../../../src/server/createServer.js"; +import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; +import { createToolResultDelivery } from "../../../src/server/toolResult.js"; +import { observed } from "../../fixtures/analysisExecution.js"; +import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; + +it("keeps concurrent SDK validation issues attached to their own tool call", async () => { + let calls = 0; + const server = createServer({ + kind: "fixed", + analysis: { + execute: () => { + calls += 1; + return Promise.resolve(observed(null)); + }, + }, + }); + const client = new Client({ name: "input-issue-attribution", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const [read, search, missing] = await Promise.all([ + client.callTool({ + name: "read_bytes", + arguments: { address: 123, length: 0, document: false }, + }), + client.callTool({ + name: "search_bytes", + arguments: { pattern: 123, limit: 0, cursor: 23 }, + }), + client.callTool({ name: "read_bytes", arguments: {} }), + ]); + expect(parseMcpToolError(read)).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { path: ["address"], reason: "invalid_type", expected: "string" }, + { path: ["length"], reason: "out_of_range", minimum: 1 }, + { path: ["document"], reason: "invalid_type", expected: "string" }, + ], + }, + }, + }); + expect(parseMcpToolError(search)).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { path: ["pattern"], reason: "invalid_type", expected: "string" }, + { path: ["limit"], reason: "out_of_range", minimum: 1 }, + { path: ["cursor"], reason: "invalid_type", expected: "string" }, + ], + }, + }, + }); + expect(parseMcpToolError(missing)).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { + path: ["address"], + reason: "missing_argument", + expected: "string", + }, + ], + }, + }, + }); + expect(calls).toBe(0); + await expect(client.ping()).resolves.toBeDefined(); + } finally { + await client.close(); + await server.close(); + } +}); + +it("preserves field diagnostics over production stdio and keeps serving", async () => { + const { call, client } = await connectLocalToolsMcp(); + const result = await call("read_bytes", { + address: 123, + length: 0, + document: false, + }); + expect(parseMcpToolError(result)).toMatchObject({ + error: { + code: "invalid_request", + recovery: { kind: "inspect_input_contract", operation: "read_bytes" }, + details: { + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ["address"], + reason: "invalid_type", + expected: "string", + }), + expect.objectContaining({ + path: ["length"], + reason: "out_of_range", + minimum: 1, + }), + expect.objectContaining({ + path: ["document"], + reason: "invalid_type", + expected: "string", + }), + ]), + }, + }, + }); + await expect(client.ping()).resolves.toBeDefined(); +}); + +it("retains mixed SDK field diagnostics without repeating custom guidance", async () => { + const info = { name: "custom-issues", version: "1" }; + const server = new EvidenceMcpServer( + info, + { capabilities: {} }, + undefined, + createToolResultDelivery(4096), + ); + const inputSchema = z.strictObject({ + first: z.string().regex(/^a$/u, "first correction"), + second: z.string().regex(/^b$/u, "second correction"), + count: z.number(), + }); + server.registerTool("custom_issues", { inputSchema }, async () => ({ + content: [{ type: "text", text: "unexpected dispatch" }], + })); + const client = new Client(info); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const argumentsValue = { + first: "wrong", + second: "wrong", + count: "wrong", + extra: true, + }; + const parsed = inputSchema.safeParse(argumentsValue); + if (parsed.success) throw new Error("expected invalid input"); + const countIssue = parsed.error.issues.find( + (issue) => issue.path[0] === "count", + ); + if (countIssue === undefined) throw new Error("missing count issue"); + const unknownIssue = parsed.error.issues.find( + (issue) => issue.code === "unrecognized_keys", + ); + if (unknownIssue === undefined) + throw new Error("missing unknown-key issue"); + const result = await client.callTool({ + name: "custom_issues", + arguments: argumentsValue, + }); + const error = parseMcpToolError(result).error; + expect(error.details).toMatchObject({ + issues: [ + { + path: ["first"], + reason: "invalid_format", + message: "first correction", + }, + { + path: ["second"], + reason: "invalid_format", + message: "second correction", + }, + { + path: ["count"], + reason: "invalid_type", + expected: "number", + message: countIssue.message, + }, + { path: ["extra"], reason: "unknown_argument" }, + ], + }); + const text = JSON.stringify(error); + expect(text.split("first correction")).toHaveLength(2); + expect(text.split("second correction")).toHaveLength(2); + expect(error.message.split(unknownIssue.message)).toHaveLength(2); + } finally { + await client.close(); + await server.close(); + } +}); + +it("keeps same-path union and independent issues attributed to their producers", async () => { + const info = { name: "issue-producer", version: "1" }; + const server = new EvidenceMcpServer( + info, + { capabilities: {} }, + undefined, + createToolResultDelivery(4096), + ); + const inputSchema = z.intersection( + z.object({ + a: z.union([z.object({ x: z.string() }), z.object({ x: z.number() })]), + }), + z.object({ a: z.object({ x: z.array(z.string()) }) }), + ); + server.registerTool("overlapping_issues", { inputSchema }, async () => ({ + content: [{ type: "text", text: "unexpected dispatch" }], + })); + const client = new Client(info); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const input = { a: { x: false } }; + const parsed = inputSchema.safeParse(input); + if (parsed.success) throw new Error("expected invalid input"); + const rawArrayIssue = parsed.error.issues.find( + (issue) => issue.code === "invalid_type", + ); + if (rawArrayIssue === undefined) throw new Error("missing array issue"); + const result = await client.callTool({ + name: "overlapping_issues", + arguments: input, + }); + const error = parseMcpToolError(result).error; + const issues = z + .array( + z + .object({ + expected: z.unknown().optional(), + message: z.string().optional(), + }) + .passthrough(), + ) + .parse(error.details?.issues); + expect(issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: ["a", "x"], + reason: "invalid_type", + expected: "array", + message: rawArrayIssue.message, + }), + expect.objectContaining({ + path: ["a", "x"], + reason: "invalid_type", + expected: "string", + }), + expect.objectContaining({ + path: ["a", "x"], + reason: "invalid_type", + expected: "number", + }), + ]), + ); + const stringIssue = issues.find((issue) => issue.expected === "string"); + expect(stringIssue?.message).not.toBe(rawArrayIssue.message); + } finally { + await client.close(); + await server.close(); + } +}); + +it("discloses when bounded projection cannot retain union branch details", async () => { + const info = { name: "bounded-issues", version: "1" }; + const server = new EvidenceMcpServer( + info, + { capabilities: {} }, + undefined, + createToolResultDelivery(4096), + ); + const branches = Array.from({ length: 256 }, (_, index) => + z.object({ [`field_${String(index)}`]: z.string() }), + ); + const first = branches[0]; + const second = branches[1]; + if (first === undefined || second === undefined) + throw new Error("missing branches"); + const inputSchema = z.object({ + value: z.union([first, second, ...branches.slice(2)]), + }); + server.registerTool("bounded_issues", { inputSchema }, async () => ({ + content: [{ type: "text", text: "unexpected dispatch" }], + })); + const sdkServer = new McpServer(info); + sdkServer.registerTool("bounded_issues", { inputSchema }, async () => ({ + content: [{ type: "text", text: "unexpected dispatch" }], + })); + const keySchema = z.strictObject({}); + const unreachable = async () => ({ + content: [{ type: "text" as const, text: "unexpected dispatch" }], + }); + server.registerTool("long_key", { inputSchema: keySchema }, unreachable); + sdkServer.registerTool("long_key", { inputSchema: keySchema }, unreachable); + const client = new Client(info); + const sdkClient = new Client(info); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + const [sdkClientTransport, sdkServerTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await sdkServer.connect(sdkServerTransport); + await sdkClient.connect(sdkClientTransport); + const sdkResult = await sdkClient.callTool({ + name: "bounded_issues", + arguments: { value: {} }, + }); + const sdkText = sdkResult.content.find( + (item) => item.type === "text", + )?.text; + const prefix = + "Input validation error: Invalid arguments for tool bounded_issues: "; + expect(sdkText?.startsWith(prefix)).toBe(true); + const diagnostic = sdkText?.slice(prefix.length); + expect(diagnostic).toBeTruthy(); + const result = await client.callTool({ + name: "bounded_issues", + arguments: { value: {} }, + }); + expect(parseMcpToolError(result)).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { + path: [], + reason: "invalid_value", + message: diagnostic, + observed: { field_details: "omitted" }, + }, + ], + }, + }, + }); + const argumentsWithLongKey = { [`key_${"x".repeat(1600)}`]: true }; + const sdkLongKey = await sdkClient.callTool({ + name: "long_key", + arguments: argumentsWithLongKey, + }); + const sdkLongText = sdkLongKey.content.find( + (item) => item.type === "text", + )?.text; + const keyPrefix = + "Input validation error: Invalid arguments for tool long_key: "; + expect(sdkLongText?.startsWith(keyPrefix)).toBe(true); + const keyDiagnostic = sdkLongText?.slice(keyPrefix.length); + expect(keyDiagnostic).toBeTruthy(); + const longKey = await client.callTool({ + name: "long_key", + arguments: argumentsWithLongKey, + }); + const longKeyError = parseMcpToolError(longKey).error; + expect(longKeyError).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: [], + reason: "invalid_value", + message: keyDiagnostic, + observed: { field_details: "omitted" }, + }, + ], + }, + }); + expect(longKeyError.message).not.toContain(keyDiagnostic); + await expect(client.ping()).resolves.toBeDefined(); + } finally { + await client.close(); + await server.close(); + await sdkClient.close(); + await sdkServer.close(); + } +}); diff --git a/tests/boundary/mcp/sessionMcp.test.ts b/tests/boundary/mcp/sessionMcp.test.ts index 2ee8dd5bd7..74fab27725 100644 --- a/tests/boundary/mcp/sessionMcp.test.ts +++ b/tests/boundary/mcp/sessionMcp.test.ts @@ -752,9 +752,9 @@ describe("json depth bounds over MCP", () => { operation: "compare_process_captures", issues: [ { - path: [], + path: ["left", "parameters"], reason: "invalid_value", - message: `left.parameters: JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}. This release has no runtime override.`, + message: expect.stringContaining(String(MAX_JSON_DEPTH)), }, ], }, diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index eb9d5fa467..83e04f6cc0 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -141,7 +141,11 @@ it.each(["bin", "hop"])( code: "capability_unavailable", details: { operation: "inspect_wasm_artifact", - reason: expect.stringContaining("REA_WABT_BIN_DIRECTORY"), + reason: "directory_not_absolute", + diagnostics: { + code: "directory_not_absolute", + setting: "REA_WABT_BIN_DIRECTORY", + }, }, }, }); @@ -152,7 +156,13 @@ it.each(["bin", "hop"])( expect(parseMcpToolError(summary)).toMatchObject({ error: { code: "capability_unavailable", - details: { reason: expect.stringContaining("REA_WABT_BIN_DIRECTORY") }, + details: { + reason: "directory_not_absolute", + diagnostics: { + code: "directory_not_absolute", + setting: "REA_WABT_BIN_DIRECTORY", + }, + }, }, }); await client.ping(); diff --git a/tests/boundary/process/androidMappingMcp.test.ts b/tests/boundary/process/androidMappingMcp.test.ts index 4e08889b25..1f40065c36 100644 --- a/tests/boundary/process/androidMappingMcp.test.ts +++ b/tests/boundary/process/androidMappingMcp.test.ts @@ -227,7 +227,7 @@ it("annotates class inspection with a caller-selected mapping and reuses one eng it.each([ ["missing", "invalid_request", "invalid_value"], - ["relative", "invalid_request", undefined], + ["relative", "invalid_request", "invalid_format"], ["not UTF-8", "invalid_request", "invalid_format"], ["no class records", "invalid_request", "invalid_format"], ["over budget", "resource_constraint", undefined], @@ -258,9 +258,15 @@ it.each([ }); // The advertised schema refuses a relative path before Android admission. if (scenario === "relative") - expect(JSON.stringify(error.details)).toContain( - "mapping_path: must be an absolute local filesystem path", - ); + expect(error.details).toMatchObject({ + issues: [ + { + path: ["mapping_path"], + reason: "invalid_format", + expected: "regex", + }, + ], + }); if (scenario === "not UTF-8" || scenario === "no class records") expect(JSON.stringify(error)).toContain( createHash("sha256") diff --git a/tests/boundary/process/darwinProcessRunTokenReader.test.ts b/tests/boundary/process/darwinProcessRunTokenReader.test.ts index 64d224a3b0..e68bc04008 100644 --- a/tests/boundary/process/darwinProcessRunTokenReader.test.ts +++ b/tests/boundary/process/darwinProcessRunTokenReader.test.ts @@ -1040,7 +1040,10 @@ it.skipIf(process.platform === "win32")( throw new Error("closed reader compilation unexpectedly succeeded"); expect(outcome.cause).toMatchObject({ name: "AbortError" }); await expect(access(state.root)).rejects.toThrow(); - await expect(reader.prepare()).rejects.toThrow(/reader is closed/u); + await expect(reader.prepare()).rejects.toMatchObject({ + _tag: "ProcessAdmissionError", + code: "reader_closed", + }); await expect(reader.close()).resolves.toBeUndefined(); } finally { await Promise.allSettled([preparation]); diff --git a/tests/boundary/process/providerProcess.test.ts b/tests/boundary/process/providerProcess.test.ts index 6e3b709956..6a6103f2a6 100644 --- a/tests/boundary/process/providerProcess.test.ts +++ b/tests/boundary/process/providerProcess.test.ts @@ -506,7 +506,10 @@ describe("provider process memory limits", () => { platform: "linux", memoryLimitBytes: 704 * 1024 * 1024, }), - ).rejects.toThrow("require the Windows Job Object launcher"); + ).rejects.toMatchObject({ + _tag: "ProcessAdmissionError", + code: "memory_limit_unsupported", + }); }); }); diff --git a/tests/boundary/providers/GdbSessionManagerCoverage.test.ts b/tests/boundary/providers/GdbSessionManagerCoverage.test.ts index bcd11c4754..1e48af6df0 100644 --- a/tests/boundary/providers/GdbSessionManagerCoverage.test.ts +++ b/tests/boundary/providers/GdbSessionManagerCoverage.test.ts @@ -25,6 +25,11 @@ import type { ProviderProcessHandle, SpawnedOwnedProviderProcess, } from "../../../src/process/ProviderProcess.js"; +import { + DEBUGGER_COMMAND_DEADLINE_SETTING, + DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, +} from "../../../src/config/debuggerSessionLimits.js"; +import { DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES } from "../../../src/domain/analysisResourceLimits.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js"; import { parseLinuxProcessStat } from "../../../src/process/LinuxProcessStat.js"; @@ -1509,7 +1514,7 @@ it("rejects a truncated inferior TTY readback before publishing the session", as ); }); const manager = managerWithFakeSpawner(fake.spawnProcess, { - maxCommandOutputBytes: 80, + controlReplyBytes: 80, }); const started = await manager.start(undefined, { inferiorTty: tty }); @@ -1521,6 +1526,28 @@ it("rejects a truncated inferior TTY readback before publishing the session", as expect(fake.children[0]?.killSignals).toEqual(["SIGTERM"]); }); +it("keeps inferior TTY readback when the caller output cap is smaller than that reply", async () => { + const tty = `/dev/pts/${"x".repeat(200)}`; + const fake = createFakeSpawner((child, token, command) => { + child.stdout.write( + command.includes("inferior-tty-show") + ? `${token}^done,inferior_tty_terminal=${JSON.stringify(tty)}\n` + : `${token}^done\n`, + ); + }); + const manager = managerWithFakeSpawner(fake.spawnProcess, { + maxCommandOutputBytes: 80, + }); + + const started = await manager.start(undefined, { inferiorTty: tty }); + + if (!started.ok) throw started.error; + expect(started.value.mi_version).toMatch(/^mi[23]$/u); + expect(started.value.session_id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u, + ); +}); + it.each([ ["trailing garbage", 'inferior_tty_terminal="/dev/pts/42"junk'], [ @@ -1871,13 +1898,22 @@ it("preserves a timeout while startup waits for the automatic-loading response", const started = await manager.start(); - expect(started.ok).toBe(false); - if (!started.ok) { - expect(started.error._tag).toBe("AnalysisTimeoutError"); - expect(started.error).toBeInstanceOf(AnalysisTimeoutError); - if (started.error instanceof AnalysisTimeoutError) - expect(started.error.operation).toBe("start_gdb_session"); - } + if (started.ok) throw new Error("expected startup to time out"); + expect(started.error._tag).toBe("AnalysisTimeoutError"); + if (!(started.error instanceof AnalysisTimeoutError)) throw started.error; + expect(started.error.operation).toBe("start_gdb_session"); + expect(projectAnalysisError(started.error)).toMatchObject({ + code: "provider_timeout", + details: { + diagnostics: { + setting: DEBUGGER_COMMAND_DEADLINE_SETTING, + effective_value: 25, + source: "runtime", + unit: "ms", + restart_required: true, + }, + }, + }); expect(fake.children[0]?.killSignals).toEqual(["SIGTERM"]); }); @@ -3424,10 +3460,15 @@ process.stdin.on('data', (chunk) => { completion_status: "unknown", }, }); - expect(exited.error.partialObservation).toMatchObject({ - limitations: expect.arrayContaining([ - expect.stringContaining("16777216 byte or 16384 record cap"), - ]), + expect(projectAnalysisError(exited.error)).toMatchObject({ + code: "cleanup_incomplete", + details: { + diagnostics: { + setting: DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING, + effective_value: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES, + source: "default", + }, + }, }); } }); diff --git a/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts b/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts index 312237c2c3..c65ffbfdf2 100644 --- a/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts +++ b/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts @@ -4,6 +4,12 @@ import { AnalysisInputError, } from "../../../src/domain/analysisErrorCore.js"; import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js"; +import { + applyDebuggerAdmissionOverride, + DEBUGGER_COMMAND_DEADLINE_SETTING, + resolveDebuggerSessionLimits, +} from "../../../src/config/debuggerSessionLimits.js"; +import { rizinCommandTimeoutLimitation } from "../../../src/rizin/RizinDebugSessionManager.js"; import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js"; import { createRizinCoverageHarness, @@ -22,26 +28,33 @@ it.skipIf(process.platform === "win32")( frameTimeoutMs: 10, stopProcess: async () => ({ status: "stopped" }), }); - expect(started.ok).toBe(true); - if (!started.ok) return; + if (!started.ok) throw started.error; + const deadline = applyDebuggerAdmissionOverride( + resolveDebuggerSessionLimits({}).commandDeadline, + 10, + ); + expect(deadline).toMatchObject({ + setting: DEBUGGER_COMMAND_DEADLINE_SETTING, + value: 10, + source: "runtime", + unit: "ms", + }); const result = await manager.execute(started.value.session_id, "wait"); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.value.result.completion_status).toBe("unknown"); - expect(result.value.evidence.raw_result).toEqual(result.value.result); - expect(result.value.evidence.parameters.launch_provenance).toEqual({ - caller_selected_path: "/tmp/injected", - resolved_path: "/tmp/injected", - }); - expect(result.value.evidence.limitations).toEqual([ - "The current debugger target identity is not established by the Rizin session identifier.", - "The command timed out before its completion marker at 10 ms (deadlineExpired). This release has no runtime override. Retained output is partial and completion is unknown.", - "The owned debugger session was stopped because output correlation could no longer be guaranteed.", - "The command is unrestricted and may access local files, the shell, network, or mutate the target.", - "Interrupted output may include an incomplete completion-marker prefix; it is retained because it cannot be distinguished from command diagnostics.", - "Launch provenance records the original and resolved startup paths; it does not establish the current target after unrestricted debugger commands.", - ]); - } + if (!result.ok) throw result.error; + expect(result.value.result.completion_status).toBe("unknown"); + expect(result.value.evidence.raw_result).toEqual(result.value.result); + expect(result.value.evidence.parameters.launch_provenance).toEqual({ + caller_selected_path: "/tmp/injected", + resolved_path: "/tmp/injected", + }); + expect(result.value.evidence.limitations).toEqual([ + "The current debugger target identity is not established by the Rizin session identifier.", + rizinCommandTimeoutLimitation(deadline), + "The owned debugger session was stopped because output correlation could no longer be guaranteed.", + "The command is unrestricted and may access local files, the shell, network, or mutate the target.", + "Interrupted output may include an incomplete completion-marker prefix; it is retained because it cannot be distinguished from command diagnostics.", + "Launch provenance records the original and resolved startup paths; it does not establish the current target after unrestricted debugger commands.", + ]); }, ); @@ -499,6 +512,43 @@ it.skipIf(process.platform === "win32").each([ }, ); +it.skipIf(process.platform === "win32")( + "accepts cleanup readback larger than the caller command-output cap", + async () => { + const cleanupReadback = `${" ".repeat(32)}[]`; + const injected = createInjectedProcess({ + onWrite: (text, control) => { + const marker = /!echo (.+)\n/u.exec(text)?.[1]; + if (marker === undefined) return; + if (text.startsWith("ok\n")) control.output(`ok\0${marker}\0`); + else if (text.startsWith("olj\n")) + control.output(`${cleanupReadback}\0${marker}\0`); + else control.output(`${marker}\0`); + }, + }); + const { manager, started } = await startInjected(injected.spawnProcess, { + maxCommandOutputBytes: 8, + stopProcess: async (session) => { + try { + await session.detachInferior?.(); + injected.control.exit(); + return { status: "stopped" }; + } catch (cause) { + return { status: "incomplete", reason: String(cause) }; + } + }, + }); + if (!started.ok) throw started.error; + const executed = await manager.execute(started.value.session_id, "ok"); + if (!executed.ok) throw executed.error; + expect(executed.value.result.completion_status).toBe("complete"); + expect(Buffer.byteLength(cleanupReadback, "utf8")).toBeGreaterThan(8); + const closed = await manager.close(started.value.session_id); + if (!closed.ok) throw closed.error; + expect(closed.value).toEqual({ target_state: "unknown" }); + }, +); + it.skipIf(process.platform === "win32")( "retains a startup protocol diagnosis across successful cleanup synchronization", async () => { diff --git a/tests/boundary/providers/RizinTimeoutEvidence.test.ts b/tests/boundary/providers/RizinTimeoutEvidence.test.ts index 2f383b8b16..8c9d4484b6 100644 --- a/tests/boundary/providers/RizinTimeoutEvidence.test.ts +++ b/tests/boundary/providers/RizinTimeoutEvidence.test.ts @@ -1,6 +1,14 @@ import { expect, it } from "vitest"; -import { createRizinCommandTimeoutEvidence } from "../../../src/rizin/RizinDebugSessionManager.js"; +import { + DEBUGGER_COMMAND_DEADLINE_SETTING, + resolveDebuggerSessionLimits, +} from "../../../src/config/debuggerSessionLimits.js"; +import { DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS } from "../../../src/domain/analysisResourceLimits.js"; +import { + createRizinCommandTimeoutEvidence, + rizinCommandTimeoutLimitation, +} from "../../../src/rizin/RizinDebugSessionManager.js"; it("preserves every field of partial Rizin timeout Evidence", () => { const result = { @@ -35,9 +43,16 @@ it("preserves every field of partial Rizin timeout Evidence", () => { }); expect(evidence.raw_result).toEqual(result); expect(evidence.normalized_result).toEqual(result); + const deadline = resolveDebuggerSessionLimits({}).commandDeadline; + expect(deadline).toMatchObject({ + setting: DEBUGGER_COMMAND_DEADLINE_SETTING, + value: DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS, + source: "default", + unit: "ms", + }); expect(evidence.limitations).toEqual([ "The current debugger target identity is not established by the Rizin session identifier.", - "The command timed out before its completion marker at 120000 ms (deadlineExpired). This release has no runtime override. Retained output is partial and completion is unknown.", + rizinCommandTimeoutLimitation(deadline), "The owned debugger session was stopped because output correlation could no longer be guaranteed.", "The command is unrestricted and may access local files, the shell, network, or mutate the target.", "Interrupted output may include an incomplete completion-marker prefix; it is retained because it cannot be distinguished from command diagnostics.", diff --git a/tests/boundary/providers/adb/adbScreenPublicationCancellation.test.ts b/tests/boundary/providers/adb/adbScreenPublicationCancellation.test.ts index f8e018cb6e..e3b87cfa02 100644 --- a/tests/boundary/providers/adb/adbScreenPublicationCancellation.test.ts +++ b/tests/boundary/providers/adb/adbScreenPublicationCancellation.test.ts @@ -50,7 +50,7 @@ const fixture = async () => { root, provider, png: Buffer.from( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv4sAAAAASUVORK5CYII=", + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC", "base64", ), }; diff --git a/tests/boundary/providers/adb/adbSelectedLocalPaths.test.ts b/tests/boundary/providers/adb/adbSelectedLocalPaths.test.ts index 4aa51dfad9..79c453e2b3 100644 --- a/tests/boundary/providers/adb/adbSelectedLocalPaths.test.ts +++ b/tests/boundary/providers/adb/adbSelectedLocalPaths.test.ts @@ -20,8 +20,8 @@ const sourceBytes = "selected native bytes"; const pulledBytes = "pulled fixture"; const bugreportBytes = "bugreport fixture"; const png = Buffer.from( - "89504e470d0a1a0a0000000d494844520000000400000003", - "hex", + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC", + "base64", ); const digest = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex"); @@ -97,6 +97,7 @@ const fixture = async () => { await mkdir(join(physical, "nested"), { recursive: true }); await symlink(join(physical, "nested"), join(root, "alias")); const binary = join(root, "adb"); + await writeFile(join(root, "screen-fixture.png"), png); const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`; await writeFile( binary, @@ -110,7 +111,7 @@ if [ "$3" = push ] || [ "$3" = install ]; then printf 'Success\\n'; exit 0 fi if [ "$3" = pull ]; then printf '${pulledBytes}' > "$5"; exit 0; fi -if [ "$3" = exec-out ]; then printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003'; exit 0; fi +if [ "$3" = exec-out ]; then /bin/cat ${quote(join(root, "screen-fixture.png"))}; exit 0; fi if [ "$3" = bugreport ]; then printf '${bugreportBytes}' > "$4/bugreport.zip" printf 'Bug report copied to %s/bugreport.zip\\n' "$4"; exit 0 diff --git a/tests/boundary/providers/ghidra/ghidraJavaLaunch.test.ts b/tests/boundary/providers/ghidra/ghidraJavaLaunch.test.ts index b402432d38..ca7d30603d 100644 --- a/tests/boundary/providers/ghidra/ghidraJavaLaunch.test.ts +++ b/tests/boundary/providers/ghidra/ghidraJavaLaunch.test.ts @@ -452,6 +452,13 @@ describe.skipIf(process.platform === "win32")( cause: { code: 3 }, }, }); + if (mode === "env-malformed") + expect(cause).toMatchObject({ + cause: { + code: "invalid_environment_entry", + detail: { check: "separator", line: "invalid environment" }, + }, + }); if (mode === "vm-malformed") expect(cause).toMatchObject({ launch_support: expect.arrayContaining([ @@ -474,9 +481,8 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected a JVM platform override failure"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining( - "may not override os.arch or os.name", - ), + _tag: "GhidraAdmissionError", + code: "platform_override", }); }, ); @@ -499,9 +505,8 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected a JVM platform override failure"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining( - "may not override os.arch or os.name", - ), + _tag: "GhidraAdmissionError", + code: "platform_override", }); }, ); @@ -578,9 +583,8 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected an effective platform override refusal"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining( - "may not override os.arch or os.name", - ), + _tag: "GhidraAdmissionError", + code: "platform_override", }); }, ); @@ -600,7 +604,8 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected a JVM platform override failure"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining("may not override os.arch or os.name"), + _tag: "GhidraAdmissionError", + code: "platform_override", }); }); @@ -634,7 +639,9 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected an unreadable argument-file failure"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining("argument file is unreadable"), + _tag: "GhidraAdmissionError", + code: "argument_file_unreadable", + detail: { path: optionsFile }, }); }); @@ -657,7 +664,8 @@ describe.skipIf(process.platform === "win32")( if (launched.ok) throw new Error("Expected a nested argument-file failure"); expect(launched.error.cause).toMatchObject({ - message: expect.stringContaining("may not nest @ references"), + _tag: "GhidraAdmissionError", + code: "nested_argument_file", }); }); }, @@ -823,9 +831,8 @@ describe.skipIf(process.platform === "win32")( ); if (cleanup.fileCleanupFailure.outcome.kind === "failed") expect(cleanup.fileCleanupFailure.outcome.cause).toMatchObject({ - message: expect.stringContaining( - "may not override os.arch or os.name", - ), + _tag: "GhidraAdmissionError", + code: "platform_override", }); await resources.retryCleanup(); expect(resources.hasPending).toBe(false); diff --git a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts index 4c5508813d..273fd3df95 100644 --- a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts +++ b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts @@ -260,7 +260,12 @@ describe("Ghidra GameCube DOL loader", () => { sectionFilePath: "/tmp/gamecube-dol-sections.txt", }, }), - ).toThrow(/cannot be combined with another loader, language override/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "gamecube_dol_combined", + }), + ); }); }); @@ -346,7 +351,12 @@ describe("Ghidra headless launcher", () => { ]) expect(() => ghidraHeadlessArguments({ ...base, mipsSoftFloat: true, ...conflict }), - ).toThrow(/generic MIPS ELF/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "mips_soft_float_scope", + }), + ); }); }); @@ -395,7 +405,12 @@ describe("Ghidra configured import language and seeds", () => { dosMz: true, languageOverride: { languageId: "x86:LE:32:default" }, }), - ).toThrow(/cannot be combined with a DOS import/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "language_override_dos", + }), + ); }); it("never combines a configured language with a raw-binary import", () => { @@ -409,7 +424,12 @@ describe("Ghidra configured import language and seeds", () => { baseAddr: "0x8000000", }, }), - ).toThrow(/cannot be combined with a raw-binary import/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "language_override_raw_binary", + }), + ); }); it.each(["darwin", "win32"] as const)( @@ -579,30 +599,41 @@ describe("Ghidra existing project", () => { }); it.each([ - { dosMz: true as const }, - { dosCom: true as const }, - { - rawBinary: { - languageId: "ARM:LE:32:v7", - compilerSpecId: "default", - baseAddr: "0x8000000", + [{ dosMz: true as const }, "existing_project_dos"], + [{ dosCom: true as const }, "existing_project_dos"], + [ + { + rawBinary: { + languageId: "ARM:LE:32:v7", + compilerSpecId: "default", + baseAddr: "0x8000000", + }, }, - }, - { languageOverride: { languageId: "x86:LE:32:default" } }, - { psp: true as const }, - { mipsSoftFloat: true as const }, - { seedPath: "/tmp/runtime/seeds/seeds.tsv" }, - { disabledAnalyzers: ["Decompiler Parameter ID"] }, - ])( + "existing_project_raw_binary", + ], + [ + { languageOverride: { languageId: "x86:LE:32:default" } }, + "existing_project_language_override", + ], + [{ psp: true as const }, "existing_project_psp"], + [{ mipsSoftFloat: true as const }, "existing_project_mips_soft_float"], + [{ seedPath: "/tmp/runtime/seeds/seeds.tsv" }, "existing_project_analysis"], + [ + { disabledAnalyzers: ["Decompiler Parameter ID"] }, + "existing_project_analysis", + ], + ] as const)( "rejects the import-only option %o with an existing project", - (conflict) => { + (conflict, code) => { expect(() => ghidraHeadlessArguments({ ...base, existingProject: { programName: "game.exe" }, ...conflict, }), - ).toThrow(/existing (Ghidra )?project|fresh import/u); + ).toThrow( + expect.objectContaining({ _tag: "GhidraAdmissionError", code }), + ); }, ); @@ -618,7 +649,12 @@ describe("Ghidra existing project", () => { ), existingProject: { programName: "game.exe" }, }), - ).toThrow(/Windows/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "existing_project_windows", + }), + ); }); it("requires a program name for -process", () => { @@ -627,7 +663,12 @@ describe("Ghidra existing project", () => { ...base, existingProject: { programName: " " }, }), - ).toThrow(/program name/u); + ).toThrow( + expect.objectContaining({ + _tag: "GhidraAdmissionError", + code: "existing_project_program_name", + }), + ); }); }); diff --git a/tests/boundary/providers/hopper/hopperCallerReconciliation.test.ts b/tests/boundary/providers/hopper/hopperCallerReconciliation.test.ts new file mode 100644 index 0000000000..28594ef03e --- /dev/null +++ b/tests/boundary/providers/hopper/hopperCallerReconciliation.test.ts @@ -0,0 +1,59 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +it("pages forward caller facts across segments without retaining stale results", async () => { + const bridge = fileURLToPath( + new URL("../../../../bridge/hopper_bridge.py", import.meta.url), + ); + const fixture = fileURLToPath( + new URL("../../../fixtures/hopperCallerProbe.py", import.meta.url), + ); + const { stdout } = await promisify(execFile)("python3", [fixture, bridge], { + encoding: "utf8", + timeout: 3000, + }); + const result = JSON.parse(stdout) as Record; + expect(result.first).toMatchObject({ + start: 0, + next: 64, + total: 130, + items: 64, + }); + expect(result.second).toMatchObject({ + start: 64, + next: 128, + total: 130, + items: 64, + caller: { + caller: { address: "0x3410" }, + callees: ["0x2000"], + references: [ + { + source_address: "0x3414", + target_address: "0x2000", + call: { classification: "direct" }, + }, + ], + }, + }); + expect(result.third).toMatchObject({ + start: 128, + next: 130, + total: 130, + items: 2, + }); + expect(result.changed).toMatchObject({ callees: [], references: [] }); + expect(result.oversized).toMatchObject({ + start: 0, + next: 0, + total: 130, + items: [], + oversized: true, + }); + expect(result.target_limited).toBe(true); + expect(result.target_count).toBeGreaterThan(0); + expect(result.target_count).toBeLessThan(0x10000); + expect(result.invalid).toBe(true); +}); diff --git a/tests/boundary/providers/kuna/kunaReferenceFiles.test.ts b/tests/boundary/providers/kuna/kunaReferenceFiles.test.ts index 12eee495b7..44eeb9ddb2 100644 --- a/tests/boundary/providers/kuna/kunaReferenceFiles.test.ts +++ b/tests/boundary/providers/kuna/kunaReferenceFiles.test.ts @@ -176,7 +176,10 @@ it("rejects unrequested bodies and a document for a different input", async () = }; expect(await parseKunaCallGraph(wire(body), input)).toMatchObject({ ok: false, - error: { reason: "functions row codeC must be null" }, + error: { + reason: "row_must_be_null", + diagnostics: { member: "functions", field: "codeC" }, + }, }); }); @@ -319,6 +322,21 @@ it.each([ }, ); +it.each([ + ["not-an-address", "invalid_address"], + ["@18446744073709551616", "address_exceeds_u64"], +] as const)( + "keeps the failed address field for incoming reference %s", + (token, code) => { + const value = xrefs(); + value.xrefs[0]!.from_address = token; + expect(parseKunaIncomingReferences(wire(value), input, "0x2000")).toEqual({ + ok: false, + error: { code, field: "from_address" }, + }); + }, +); + it("bounds u64 input parsing before BigInt allocation", () => { expect(normalizeKunaAddress(`0X${"0".repeat(1000)}ABC`)).toBe("0xabc"); expect(normalizeKunaAddress("0xffffffffffffffff")).toBe("0xffffffffffffffff"); diff --git a/tests/boundary/providers/kuna/kunaSpanCloseOwnership.test.ts b/tests/boundary/providers/kuna/kunaSpanCloseOwnership.test.ts index c0a8a17de8..52e570eefd 100644 --- a/tests/boundary/providers/kuna/kunaSpanCloseOwnership.test.ts +++ b/tests/boundary/providers/kuna/kunaSpanCloseOwnership.test.ts @@ -143,9 +143,10 @@ it.each([ const handle = fault.handles[0]; if (handle === undefined) throw new Error("missing span descriptor"); if (short) { - expect(result.error.cleanupPrimary?.userMessage).toContain( - "shorter than its recorded span", - ); + expect(result.error.cleanupPrimary?.cause).toMatchObject({ + _tag: "KunaDocumentError", + code: "span_short", + }); expect(result.error.diagnostics?.span_read).toBeUndefined(); } else { expect(result.error).toBeInstanceOf(ProviderCleanupError); diff --git a/tests/boundary/providers/kuna/kunaStringInventory.test.ts b/tests/boundary/providers/kuna/kunaStringInventory.test.ts index 813b6d3e1e..6fc83e0067 100644 --- a/tests/boundary/providers/kuna/kunaStringInventory.test.ts +++ b/tests/boundary/providers/kuna/kunaStringInventory.test.ts @@ -281,7 +281,11 @@ it("bounds keys and numbers: overlong keys match no field, overlong kept numbers const kept = wire({ ...document(), count: "@1" + "0".repeat(64) }); expect((await read(kept)).result).toMatchObject({ ok: false, - error: { reason: "count is not a number Kuna writes" }, + error: { + _tag: "AnalysisOutputError", + reason: "not_kuna_number", + diagnostics: { code: "not_kuna_number", field: "count" }, + }, }); }); diff --git a/tests/boundary/providers/native/nativeDispatchFormatFailures.test.ts b/tests/boundary/providers/native/nativeDispatchFormatFailures.test.ts new file mode 100644 index 0000000000..b9def76798 --- /dev/null +++ b/tests/boundary/providers/native/nativeDispatchFormatFailures.test.ts @@ -0,0 +1,103 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; + +import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; +import { + NativeFixtureRunner, + nativeMachoTargetForFile, +} from "../../../fixtures/nativeCommands.js"; +import { objcRecordBudgetMacho } from "../../../fixtures/objcRecordBudgetMacho.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +const sectionCases = [ + { + section: "__objc_classlist", + size: 12, + reason: "Misaligned Objective-C class-list size", + }, + { + section: "__objc_protolist", + size: 12, + reason: "Misaligned Objective-C protocol list", + }, + { + section: "__objc_catlist", + size: 12, + reason: "Misaligned Objective-C category list", + }, + { + section: "__swift5_types", + size: 6, + reason: "Misaligned Swift type section", + }, + { + section: "__swift5_proto", + size: 6, + reason: "Misaligned Swift conformance section", + }, +]; + +const inspect = async (bytes: Buffer) => { + const root = await createTestTempDirectory("rea-dispatch-format-"); + const path = join(root, "fixture.macho"); + await writeFile(path, bytes); + const client = new NativeMacOSProvider( + {}, + new NativeFixtureRunner(), + "darwin", + ).createClient(await nativeMachoTargetForFile(path)); + try { + return { + path, + result: await client.execute("inspect_native_dispatch_metadata", {}), + }; + } finally { + expect((await client.close()).ok).toBe(true); + } +}; + +it.each(sectionCases)( + "reports malformed $section bytes as a target refusal with the producing reason", + async ({ section, size, reason }) => { + const bytes = objcRecordBudgetMacho("class"); + bytes.fill(0, 104, 120); + bytes.write(section, 104); + bytes.writeBigUInt64LE(BigInt(size), 144); + const { path, result } = await inspect(bytes); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected malformed target refusal"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unsupported_target", + category: "unsupported_target", + details: { operation: "inspect_native_dispatch_metadata", path, reason }, + }); + }, +); + +it("keeps an unsupported big-endian CPU declaration distinct from provider failure", async () => { + const bytes = Buffer.alloc(32); + bytes.writeUInt32BE(0xfeedfacf); + bytes.writeUInt32BE(0x0100000c, 4); + bytes.writeUInt32BE(2, 12); + const { path, result } = await inspect(bytes); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected unsupported target refusal"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unsupported_target", + category: "unsupported_target", + details: { path, reason: "Unsupported big-endian Mach-O CPU type" }, + }); +}); + +it("continues decoding the well-formed eight-byte class-list control", async () => { + const { result } = await inspect(objcRecordBudgetMacho("class")); + expect(result.ok).toBe(true); + if (!result.ok) throw result.error; + expect(result.value.result).toMatchObject({ + result: { + objc_classes: [expect.objectContaining({ name: "FixtureClass" })], + }, + }); +}); diff --git a/tests/boundary/providers/native/nativeInventory.golden.test.ts b/tests/boundary/providers/native/nativeInventory.golden.test.ts index 1d0cf5d2ec..7d42727f2e 100644 --- a/tests/boundary/providers/native/nativeInventory.golden.test.ts +++ b/tests/boundary/providers/native/nativeInventory.golden.test.ts @@ -1,5 +1,5 @@ import { inspectSignatureSchema } from "../../../../src/domain/native/nativeInspection.js"; -import { realpath, writeFile } from "node:fs/promises"; +import { readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; import { @@ -8,6 +8,7 @@ import { machoImage, } from "../../../../src/artifacts/apple/MachoImage.fixture.js"; import { err, ok } from "../../../../src/domain/result.js"; +import { parseExecutableHeader } from "../../../../src/domain/binaryTarget.js"; import { NativeCommandFailure, type NativeCommandCapture, @@ -75,7 +76,11 @@ const signatureTarget = async ( })), ), ); - return nativeMachoTargetForFile(path); + const target = await nativeMachoTargetForFile(path); + const header = parseExecutableHeader(await readFile(path), "arm64"); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected a universal Mach-O fixture"); + return { ...target, ...header.value }; }; it("distinguishes a codesign execution failure from an observed unsigned artifact", async () => { @@ -171,6 +176,189 @@ it("reports unsigned slices when a universal Mach-O has mixed signatures", async } }); +it("uses the admitted universal target when codesign describes only its selected slice", async () => { + const existing = await signatureTarget(); + const header = parseExecutableHeader(await readFile(existing.path), "arm64"); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected a universal Mach-O fixture"); + const signed = (await fixture("codesign.txt")).replace( + /^Format=.*$/mu, + "Format=Mach-O thin (arm64)", + ); + const client = new NativeMacOSProvider( + {}, + signatureRunner({ + signed, + unsigned: await fixture("dyld-inventory/codesign-unsigned.txt"), + entitlements: await fixture("entitlements.xml"), + unsignedHeader: true, + }), + "darwin", + ).createClient({ ...existing, ...header.value }); + const execution = await client.execute("inspect_signature", {}); + if (!execution.ok) throw execution.error; + expect(execution.value.result).toMatchObject({ + signed: true, + format: "Mach-O thin (arm64)", + limitations: expect.arrayContaining([ + expect.stringContaining("Partial signature coverage"), + "Unsigned Mach-O slices: x86_64.", + ]), + provenance: expect.arrayContaining([ + expect.objectContaining({ + command: expect.arrayContaining(["-a", "x86_64"]), + exit: { code: 1, signal: null }, + }), + ]), + }); +}); + +it("retains distinct same-family FAT slices for signature coverage", async () => { + const path = join( + await createTestTempDirectory("rea-native-same-family-"), + "fixture", + ); + await writeFile( + path, + fatImage( + [CPU.arm64, CPU.arm64e].map((cpu) => ({ + cpu, + bytes: machoImage({ cpu }), + })), + ), + ); + const header = parseExecutableHeader(await readFile(path), "arm64"); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected universal fixture"); + const replay = signatureRunner({ + signed: (await fixture("codesign.txt")).replace( + /^Format=.*$/mu, + "Format=Mach-O thin (arm64)", + ), + unsigned: await fixture("dyld-inventory/codesign-unsigned.txt"), + entitlements: await fixture("entitlements.xml"), + unsignedHeader: true, + }); + const runner: NativeCommandRunner = { + async run(tool, args, options) { + const result = await replay.run( + tool, + args.map((value) => (value === "arm64e" ? "x86_64" : value)), + options, + ); + return result.ok ? ok({ ...result.value, arguments: [...args] }) : result; + }, + }; + const client = new NativeMacOSProvider({}, runner, "darwin").createClient({ + ...(await nativeMachoTargetForFile(path)), + ...header.value, + }); + const execution = await client.execute("inspect_signature", {}); + if (!execution.ok) throw execution.error; + expect(execution.value.result).toMatchObject({ + limitations: expect.arrayContaining([ + expect.stringContaining("Partial signature coverage"), + "Unsigned Mach-O slices: arm64e.", + ]), + provenance: expect.arrayContaining([ + expect.objectContaining({ + command: expect.arrayContaining(["-a", "arm64e"]), + exit: { code: 1, signal: null }, + }), + ]), + }); +}); + +it.each([42, 0x81000002])( + "reports unknown signature coverage for unmapped FAT subtype %i", + async (subtype) => { + const path = join( + await createTestTempDirectory("rea-native-unknown-subtype-"), + "fixture", + ); + const unknown = { type: CPU.arm64.type, subtype }; + await writeFile( + path, + fatImage( + [CPU.arm64, unknown].map((cpu) => ({ + cpu, + bytes: machoImage({ cpu }), + })), + ), + ); + const header = parseExecutableHeader(await readFile(path), "arm64"); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected universal fixture"); + const client = new NativeMacOSProvider( + {}, + signatureRunner({ + signed: await fixture("codesign.txt"), + unsigned: await fixture("dyld-inventory/codesign-unsigned.txt"), + entitlements: await fixture("entitlements.xml"), + }), + "darwin", + ).createClient({ + ...(await nativeMachoTargetForFile(path)), + ...header.value, + }); + const execution = await client.execute("inspect_signature", {}); + if (!execution.ok) throw execution.error; + expect(execution.value.result).toMatchObject({ + limitations: expect.arrayContaining([ + expect.stringContaining("signature states are unknown"), + expect.stringContaining("Partial signature coverage"), + ]), + }); + }, +); + +it.each(["x64", "arm64"] as const)( + "inspects an x86_64 LIB64 slice when %s is selected", + async (host) => { + const path = join( + await createTestTempDirectory("rea-native-lib64-"), + "fixture", + ); + const lib64 = { type: CPU.x86_64.type, subtype: 0x80000003 }; + await writeFile( + path, + fatImage( + [lib64, CPU.arm64].map((cpu) => ({ cpu, bytes: machoImage({ cpu }) })), + ), + ); + const header = parseExecutableHeader(await readFile(path), host); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected universal fixture"); + const replay = signatureRunner({ + signed: await fixture("codesign.txt"), + unsigned: await fixture("dyld-inventory/codesign-unsigned.txt"), + entitlements: await fixture("entitlements.xml"), + unsignedHeader: true, + }); + const calls: (readonly string[])[] = []; + const runner: NativeCommandRunner = { + run(tool, args, options) { + calls.push(args); + return replay.run(tool, args, options); + }, + }; + const client = new NativeMacOSProvider({}, runner, "darwin").createClient({ + ...(await nativeMachoTargetForFile(path)), + ...header.value, + }); + const result = await client.execute("inspect_signature", {}); + if (!result.ok) throw result.error; + expect(calls.some((args) => args.includes("x86_64"))).toBe(true); + expect(result.value.result).toMatchObject({ + limitations: expect.arrayContaining(["Unsigned Mach-O slices: x86_64."]), + }); + if (host === "x64") + expect(calls.slice(0, 3).every((args) => args.includes("x86_64"))).toBe( + true, + ); + }, +); + it("rejects nonzero supplemental codesign output that is not an unsigned observation", async () => { const signed = await fixture("codesign.txt"); const entitlements = await fixture("entitlements.xml"); @@ -1036,10 +1224,44 @@ it("reruns nm with its dyld-info pass when a dyld_info export row is unreadable" ]); }); +it("lets codesign select the only slice of a thin arm64e target", async () => { + const path = join( + await createTestTempDirectory("rea-thin-signature-"), + "fixture", + ); + await writeFile(path, machoImage({ cpu: CPU.arm64e })); + const calls: (readonly string[])[] = []; + const display = (await fixture("codesign.txt")).replace( + /^Format=.*$/mu, + "Format=Mach-O thin (arm64e)", + ); + const runner: NativeCommandRunner = { + run(tool, args, options) { + calls.push(args); + return new FixtureRunner({ codesign: display }).run(tool, args, options); + }, + }; + const client = new NativeMacOSProvider({}, runner, "darwin").createClient( + await nativeMachoTargetForFile(path), + ); + const result = await client.execute("inspect_signature", {}); + if (!result.ok) throw result.error; + expect(result.value.result).toMatchObject({ signed: true }); + expect(calls).toHaveLength(3); + expect(calls.every((args) => !args.includes("-a"))).toBe(true); + await client.close(); +}); + it.each(["arm64", "x86_64"] as const)( "binds every signature facet to the requested %s slice when all slices are signed", async (architecture) => { const registered = await signatureTarget(); + const header = parseExecutableHeader( + await readFile(registered.path), + architecture === "x86_64" ? "x64" : "arm64", + ); + if (!header.ok || header.value.format !== "mach-o-universal") + throw new Error("expected universal fixture"); const display = (await fixture("codesign.txt")).replace( /^Format=.*$/mu, "Format=Mach-O universal (x86_64 arm64)", @@ -1061,7 +1283,7 @@ it.each(["arm64", "x86_64"] as const)( }, }; const result = await new NativeMacOSProvider({}, runner, "darwin") - .createClient({ ...registered, architecture }) + .createClient({ ...registered, ...header.value }) .execute("inspect_signature", {}); if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ diff --git a/tests/boundary/providers/native/nativeMachoEncryptionBinding.test.ts b/tests/boundary/providers/native/nativeMachoEncryptionBinding.test.ts new file mode 100644 index 0000000000..d49418b883 --- /dev/null +++ b/tests/boundary/providers/native/nativeMachoEncryptionBinding.test.ts @@ -0,0 +1,200 @@ +import { appendFile, open, writeFile, type FileHandle } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, onTestFinished, vi } from "vitest"; +import { machoImage } from "../../../../src/artifacts/apple/MachoImage.fixture.js"; +import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js"; +import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { inspectMachoSchema } from "../../../../src/domain/native/nativeInspection.js"; +import { ProviderCleanupError } from "../../../../src/domain/providerCleanupError.js"; +import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; +import { + NativeFixtureRunner, + nativeMachoTargetForFile, +} from "../../../fixtures/nativeCommands.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +/** A thin arm64 image whose LC_ENCRYPTION_INFO_64 marks 0x4000-0x8000 as ciphertext. */ +const encryptedImage = (): Uint8Array => { + const command = new Uint8Array(24); + const view = new DataView(command.buffer); + view.setUint32(0, 0x2c, true); + view.setUint32(4, 24, true); + view.setUint32(8, 0x4000, true); + view.setUint32(12, 0x4000, true); + view.setUint32(16, 1, true); + const bytes = new Uint8Array(0x10000); + bytes.set(machoImage({ commands: [command] })); + return bytes; +}; + +const writeTarget = async () => { + const root = await createTestTempDirectory("rea-native-macho-encryption-"); + const path = join(root, "fixture.macho"); + await writeFile(path, encryptedImage()); + return path; +}; + +const thinLipo = { lipo: "Non-fat file: fixture is architecture: arm64\n" }; + +const inspect = async ( + path: string, + runner: NativeFixtureRunner = new NativeFixtureRunner(thinLipo), +) => { + const client = new NativeMacOSProvider({}, runner, "darwin").createClient( + await nativeMachoTargetForFile(path), + ); + onTestFinished(async () => { + await client.close(); + }); + return { client, result: await client.execute("inspect_macho", {}) }; +}; + +it("reads encryption from the bound file and names the ciphertext bytes", async () => { + const { result } = await inspect(await writeTarget()); + if (!result.ok) throw result.error; + const report = inspectMachoSchema.parse(result.value.result); + expect(report.encryption).toMatchObject({ + total: 1, + exhaustive: true, + items: [ + { + status: "encrypted", + container_file_offset: 0, + range: { content: "ciphertext", container_start: 0x4000 }, + }, + ], + }); + expect(result.value.limitations).toContain( + "Bytes 16384-32768 (input file offsets, end exclusive) of the arm64 slice are ciphertext (cryptid 1); disassembly, decompilation, strings and references derived from them do not describe program code.", + ); +}); + +/** Replay fixture output while appending to the target once lipo reads it. */ +class MutatingRunner extends NativeFixtureRunner { + constructor(private readonly path: string) { + super(thinLipo); + } + + override async run(tool: string, arguments_: readonly string[]) { + if (tool === "lipo") await appendFile(this.path, "changed"); + return super.run(tool, arguments_); + } +} + +it("refuses facts when the file changes while the tools inspect it", async () => { + const path = await writeTarget(); + const { result } = await inspect(path, new MutatingRunner(path)); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected a changed-artifact refusal"); + expect(result.error).toMatchObject({ + _tag: "AnalysisArtifactChangedError", + cleanupIncomplete: false, + }); +}); + +/** Make only the admitted descriptor's close reject until released. */ +const failingClose = async (onClosed?: () => void) => { + const probe = await open(await writeTarget(), "r"); + const prototype = Object.getPrototypeOf(probe) as FileHandle; + await probe.close(); + const state = { failing: onClosed === undefined, attempts: 0 }; + const original = prototype.stat; + const stat = vi + .spyOn(prototype, "stat") + .mockImplementationOnce(async function ( + this: FileHandle, + ...args: Parameters + ) { + const release = this.close.bind(this); + this.close = async () => { + state.attempts += 1; + if (state.failing) throw new Error("macho target close failed"); + await release(); + onClosed?.(); + }; + return Reflect.apply(original, this, args); + }); + onTestFinished(() => { + stat.mockRestore(); + state.failing = false; + }); + return state; +}; + +/** The finished report survives a failure that arrives after the read completed. */ +const expectRetainedReport = (error: AnalysisError) => { + const report = inspectMachoSchema.parse( + (error.partialObservation as { normalized_result?: unknown } | undefined) + ?.normalized_result, + ); + expect(report.encryption.items).toMatchObject([{ status: "encrypted" }]); + expect(projectAnalysisError(error).details?.partial_observation).toEqual( + error.partialObservation, + ); +}; + +it("retains the target descriptor when close rejects and retries it on client close", async () => { + const path = await writeTarget(); + const state = await failingClose(); + const { client, result } = await inspect(path); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected the close failure"); + expect(result.error).toBeInstanceOf(ProviderCleanupError); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.cleanupResources).toEqual([path]); + expect(state.attempts).toBe(1); + expectRetainedReport(result.error); + + expect((await client.close()).ok).toBe(false); + expect(state.attempts).toBe(2); + state.failing = false; + expect((await client.close()).ok).toBe(true); + expect(state.attempts).toBe(3); + expect(result.error.cleanupIncomplete).toBe(false); +}); + +it("keeps the inspection failure primary when the descriptor close also rejects", async () => { + const path = await writeTarget(); + const state = await failingClose(); + const { client, result } = await inspect(path, new MutatingRunner(path)); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected change and close failures"); + expect(result.error).toBeInstanceOf(ProviderCleanupError); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.cleanupResources).toEqual([path]); + expect(result.error.cleanupPrimary?._tag).toBe( + "AnalysisArtifactChangedError", + ); + expect(result.error.partialObservation).toBeUndefined(); + + state.failing = false; + expect((await client.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); +}); + +it("reports cancellation that arrives during the final close without retaining the descriptor", async () => { + const path = await writeTarget(); + const controller = new AbortController(); + const reason = { caller: "macho-final-close" }; + await failingClose(() => controller.abort(reason)); + const client = new NativeMacOSProvider( + {}, + new NativeFixtureRunner(thinLipo), + "darwin", + ).createClient(await nativeMachoTargetForFile(path)); + onTestFinished(async () => { + await client.close(); + }); + const result = await client.execute( + "inspect_macho", + {}, + { signal: controller.signal }, + ); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected final-close cancellation"); + expect(result.error).toMatchObject({ + _tag: "AnalysisCancelledError", + cleanupIncomplete: false, + }); + expectRetainedReport(result.error); +}); diff --git a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts index 816747dbf2..8b5343ab17 100644 --- a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts +++ b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts @@ -39,6 +39,13 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js const CONTENT = "registered executable bytes"; const runFile = promisify(execFile); +// Respect the caller's selected developer tools without passing the ambient environment. +const realXcrunRunner = () => + new XcrunCommandRunner( + process.env.DEVELOPER_DIR === undefined + ? {} + : { DEVELOPER_DIR: process.env.DEVELOPER_DIR }, + ); const fixture = async () => { const directory = await createTestTempDirectory("rea-signature-binding-"); const path = join(directory, "program"); @@ -78,12 +85,12 @@ const registerSignatureObservationCases = () => { .createClient(target) .execute("inspect_signature", {}); if (!result.ok) throw result.error; - expect(captures.map((args) => args[args.indexOf("-a") + 1])).toEqual([ - target.architecture, - target.architecture, - target.architecture, - ...target.availableArchitectures, - ]); + expect(captures.slice(0, 3).every((args) => !args.includes("-a"))).toBe( + true, + ); + expect( + captures.slice(3).map((args) => args[args.indexOf("-a") + 1]), + ).toEqual(target.availableArchitectures); expect(result.value.result).toMatchObject({ signed: true, identifier: "com.example.fixture", @@ -453,7 +460,7 @@ it.skipIf(process.platform !== "darwin")( if (target.kind !== "executable") throw new Error("Expected the copied system executable"); const originalBytes = await readFile(path); - const real = new XcrunCommandRunner({}); + const real = realXcrunRunner(); let snapshotPath: string | undefined; const runner: NativeCommandRunner = { async run(tool, args, options) { @@ -502,7 +509,7 @@ it.skipIf(process.platform !== "darwin")( throw new Error("Expected the copied system executable"); const result = await new NativeMacOSProvider( {}, - new XcrunCommandRunner({}), + realXcrunRunner(), "darwin", ) .createClient(target) @@ -510,20 +517,43 @@ it.skipIf(process.platform !== "darwin")( if (!result.ok) throw result.error; const parsed = inspectSignatureSchema.parse(result.value.result); expect(parsed).toMatchObject({ signed: false }); - const selectedArchitecture = - target.architecture === "x86" ? "i386" : target.architecture; + const { stdout: archOutput } = await runFile("/usr/bin/lipo", [ + "-archs", + path, + ]); + const names = archOutput.trim().split(/\s+/u); + const toolName = (architecture: string) => { + const family = architecture === "x86" ? "i386" : architecture; + const name = + names.find((candidate) => candidate === family) ?? + names.find((candidate) => candidate.startsWith(family)); + if (name === undefined) throw new Error(`Missing ${family} slice`); + return name; + }; + const selectedArchitecture = toolName(target.architecture); + const universal = target.format === "mach-o-universal"; + if (!universal && target.format !== "mach-o") + throw new Error(`Unexpected Mach-O format ${target.format}`); + const selector = (command: readonly string[], required: boolean) => { + const index = command.indexOf("-a"); + if (!required) { + expect(index).toBe(-1); + return null; + } + if (index < 0) + throw new Error("codesign omitted -a for a universal slice"); + return command[index + 1]; + }; expect( - parsed.provenance.map( - ({ command }) => command[command.indexOf("-a") + 1], + parsed.provenance.map(({ command }, index) => + selector(command, universal || index >= 3), ), ).toEqual([ - selectedArchitecture, - selectedArchitecture, - selectedArchitecture, - ...(target.availableArchitectures.length > 1 - ? target.availableArchitectures.map((architecture) => - architecture === "x86" ? "i386" : architecture, - ) + ...(universal + ? [selectedArchitecture, selectedArchitecture, selectedArchitecture] + : [null, null, null]), + ...(universal && target.availableArchitectures.length > 1 + ? target.availableArchitectures.map(toolName) : []), ]); expect(parsed.provenance.every(({ exit }) => exit.code === 1)).toBe(true); @@ -654,7 +684,7 @@ it.skipIf(process.platform !== "darwin")( const target = admitted.value; if (target.kind !== "executable") throw new Error("Expected the copied system executable"); - const real = new XcrunCommandRunner({}); + const real = realXcrunRunner(); let capturedPath: string | undefined; const runner: NativeCommandRunner = { async run(tool, args, options) { @@ -687,7 +717,19 @@ it.skipIf(process.platform !== "darwin")( inspectSignatureSchema .parse(result.value.result) .provenance.map(({ command }) => command.at(-1)), - ).toEqual(["$ARTIFACT", "$ARTIFACT", "$ARTIFACT"]); + ).toEqual( + Array.from( + { + length: + 3 + + (target.format === "mach-o-universal" && + target.availableArchitectures.length > 1 + ? target.availableArchitectures.length + : 0), + }, + () => "$ARTIFACT", + ), + ); if (capturedPath === undefined) throw new Error("Expected codesign invocation"); await expect(lstat(dirname(capturedPath))).rejects.toMatchObject({ diff --git a/tests/boundary/providers/native/objcClassMetadataFallback.test.ts b/tests/boundary/providers/native/objcClassMetadataFallback.test.ts index afa8c31ac3..d3df378ae6 100644 --- a/tests/boundary/providers/native/objcClassMetadataFallback.test.ts +++ b/tests/boundary/providers/native/objcClassMetadataFallback.test.ts @@ -15,6 +15,7 @@ import { parseMachoLayout, } from "../../../../src/native/AppleMachoSelection.js"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; +import { BinaryFormatFailure } from "../../../../src/native/metadata/BinaryContainer.js"; import { NativeFixtureRunner } from "../../../fixtures/nativeCommands.js"; import { ok } from "../../../../src/domain/result.js"; @@ -171,7 +172,8 @@ describe("native Objective-C class metadata capability fallback", () => { } catch (cause: unknown) { mismatchedCpuFailure = cause; } - expect(mismatchedCpuFailure).toBeInstanceOf(TypeError); + expect(mismatchedCpuFailure).toBeInstanceOf(BinaryFormatFailure); + expect(mismatchedCpuFailure).toMatchObject({ kind: "unsupported" }); expect(mismatchedCpuFailure).not.toBeInstanceOf( UnsupportedMachoMetadataFormatError, ); diff --git a/tests/composition/analysis-sessions/backwardCallerEvidence.test.ts b/tests/composition/analysis-sessions/backwardCallerEvidence.test.ts new file mode 100644 index 0000000000..86ffa90992 --- /dev/null +++ b/tests/composition/analysis-sessions/backwardCallerEvidence.test.ts @@ -0,0 +1,140 @@ +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import type { AnalysisProvider } from "../../../src/application/AnalysisProvider.js"; +import { + executeComposedWorkflow, + createWorkflowEvidence, +} from "../../../src/application/WorkflowEvidence.js"; +import { EnhancedTools } from "../../../src/application/EnhancedTools.js"; +import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; +import { ok } from "../../../src/domain/result.js"; +import { + createBinarySessionTargets, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; +import { expect, it } from "vitest"; + +const identity = { + id: "fixture", + name: "Fixture analysis provider", + version: "1", +} as const; + +const effects = { + mutatesArtifact: false, + launchesProcess: false, + mayShowUi: false, + mayAccessNetwork: false, + mayWriteFilesystem: false, + changesPermissions: false, + requiresRoot: false, +} as const; + +it("links backward caller observations into workflow Evidence without a document", async () => { + const forward = { + native_result: { callers: ["0x2000"], callers_complete: true }, + forward_observations: { + call_references: [ + { + caller_address: "0x2000", + source_address: "0x2004", + target_address: "0x1000", + source_procedure: "0x2000", + call: "direct", + }, + ], + }, + }; + const provider: AnalysisProvider = { + identity: () => identity, + resolveAnalysisProfile: () => + Promise.resolve( + ok({ profile: createAnalysisProfile(identity, { fixture: true }) }), + ), + capabilities: () => + (["procedure_address", "procedure_callers"] as const).map( + (operation) => ({ + provider: identity, + operation, + available: true, + reason: null, + effects, + limitations: [], + }), + ), + createClient: () => ({ + execute: (operation, parameters) => { + if (operation === "procedure_address") + return Promise.resolve( + ok(createAnalysisExecution(parameters.procedure, identity)), + ); + const callers = parameters.procedure === "0x1000" ? ["0x2000"] : []; + return Promise.resolve( + ok( + createAnalysisExecution(callers, identity, { + rawResult: + parameters.procedure === "0x1000" + ? forward + : { + native_result: callers, + forward_observations: { call_references: [] }, + }, + }), + ), + ); + }, + close: () => Promise.resolve(ok(null)), + }), + }; + const session = createTestBinarySession(provider); + const [target] = await createBinarySessionTargets(); + if (target === undefined) throw new Error("Missing session target"); + try { + const opened = await session.open(target); + if (!opened.ok) throw opened.error; + const composed = await executeComposedWorkflow(session, (observed) => + new EnhancedTools(observed).execute("get_call_graph", { + address: "0x1000", + direction: "backward", + }), + ); + if (!composed.ok) throw composed.error; + const workflow = createWorkflowEvidence({ + target: opened.value, + operation: "get_call_graph", + parameters: { address: "0x1000", direction: "backward" }, + result: composed.value.result, + upstreamProfile: session.analysisProfile(), + upstreamLimitations: composed.value.limitations, + ...(composed.value.evidenceLinks === undefined + ? {} + : { upstreamEvidenceLinks: composed.value.evidenceLinks }), + }); + const caller = composed.value.evidenceLinks + ?.map((id) => session.evidenceById(id)) + .find( + (evidence) => + evidence?.operation === "procedure_callers" && + evidence.parameters.procedure === "0x1000", + ); + expect(caller).toMatchObject({ + parameters: { procedure: "0x1000" }, + raw_result: forward, + }); + expect(caller?.raw_result).toMatchObject({ + forward_observations: { + call_references: [ + { + caller_address: "0x2000", + source_address: "0x2004", + target_address: "0x1000", + }, + ], + }, + }); + expect(caller?.raw_result).not.toHaveProperty("native_result.instructions"); + expect(caller?.parameters).not.toHaveProperty("document"); + expect(workflow.evidence_links).toContain(caller?.evidence_id); + } finally { + await session.close(); + } +}); diff --git a/tests/composition/analysis-sessions/binarySession.test.ts b/tests/composition/analysis-sessions/binarySession.test.ts index 535144585b..c1e56cac2f 100644 --- a/tests/composition/analysis-sessions/binarySession.test.ts +++ b/tests/composition/analysis-sessions/binarySession.test.ts @@ -1076,12 +1076,22 @@ describe("snapshot cache eligibility", () => { const parameters: Readonly> = live ? { address: "0x1000", document: "main" } : {}; + const evidenceIds: Array = []; for (let attempt = 0; attempt < 2; attempt += 1) { const result = await session.execute("address_name", parameters); - expect(result).toMatchObject({ ok: true }); - if (result.ok) expect(result.value.evidenceId).toBeUndefined(); + if (!result.ok) throw result.error; + evidenceIds.push(result.value.evidenceId); + } + const records = session.exportEvidenceBundle().records; + if (live) { + expect(evidenceIds).toEqual([expect.any(String), evidenceIds[0]]); + expect(records.map((record) => record.evidence_id)).toEqual( + expect.arrayContaining([source.evidence_id, evidenceIds[0]]), + ); + } else { + expect(evidenceIds).toEqual([undefined, undefined]); + expect(records).toEqual([source]); } - expect(session.exportEvidenceBundle().records).toEqual([source]); expect(session.exportAnalysisSnapshot()).toMatchObject({ ok: true, value: { entries: [] }, @@ -1122,20 +1132,20 @@ describe("snapshot cache eligibility", () => { await session.close(); } }); +}); - it("does not snapshot reads that depend on the provider cursor", async () => { - const [first] = await createBinarySessionTargets(); - const calls: string[] = []; - const session = createTestBinarySession(createCacheProvider(calls)); - await openAndExecuteAddressName(session, first, {}); - expect((await session.execute("address_name", {})).ok).toBe(true); - expect(calls).toEqual(["address_name", "address_name"]); - expect(session.exportAnalysisSnapshot()).toMatchObject({ - ok: true, - value: { entries: [] }, - }); - await session.close(); +it("does not snapshot reads that depend on the provider cursor", async () => { + const [first] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + await openAndExecuteAddressName(session, first, {}); + expect((await session.execute("address_name", {})).ok).toBe(true); + expect(calls).toEqual(["address_name", "address_name"]); + expect(session.exportAnalysisSnapshot()).toMatchObject({ + ok: true, + value: { entries: [] }, }); + await session.close(); }); describe("provider health and availability", () => { diff --git a/tests/conformance/contracts/targetContentProvenance.test.ts b/tests/conformance/contracts/targetContentProvenance.test.ts index c41ebcef58..3041892013 100644 --- a/tests/conformance/contracts/targetContentProvenance.test.ts +++ b/tests/conformance/contracts/targetContentProvenance.test.ts @@ -21,7 +21,9 @@ describe("target-content provenance audit", () => { "export_evidence_bundle", "guided_workflows", "inspect_analysis_settings", + "read_guidance_section", "record_unknown", + "search_guidance", "update_analysis_settings", ]); }); diff --git a/tests/evaluation/importSpecifiers.test.ts b/tests/evaluation/importSpecifiers.test.ts new file mode 100644 index 0000000000..db7225d73b --- /dev/null +++ b/tests/evaluation/importSpecifiers.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { + importSpecifier, + readImportSpecifiers, +} from "../../scripts/lib/import-specifiers.mjs"; + +describe("import specifier reader", () => { + it("reads side-effect imports and ignores import-like text", () => { + expect( + readImportSpecifiers(` + // from "./commented.js" + /* import "./block-comment.js"; */ + const text = "from './string.js'"; + import "./side-effect.js"; + import { value } from "./named.js"; + export { value as again } from "./reexported.js"; + const loaded = import("./dynamic.js"); + `).map(({ specifier }) => specifier), + ).toEqual([ + "./side-effect.js", + "./named.js", + "./reexported.js", + "./dynamic.js", + ]); + }); + + it("reads type, namespace, and empty-template specifiers", () => { + const facts = readImportSpecifiers(`import type { Fact } from "./types.js"; +export * from "./all.js"; +import name = require("./equals.js"); +type Query = import("./query.js").Query; +const dynamic = import(\`./template.js\`); +const computed = import(name); +import "node:fs"; +`); + expect(facts).toEqual([ + { specifier: "./types.js", line: 1 }, + { specifier: "./all.js", line: 2 }, + { specifier: "./equals.js", line: 3 }, + { specifier: "./query.js", line: 4 }, + { specifier: "./template.js", line: 5 }, + { specifier: "node:fs", line: 7 }, + ]); + }); + + it("ignores nodes that are not static imports", () => { + expect(importSpecifier({ type: "Identifier" })).toBeUndefined(); + }); +}); diff --git a/tests/fixtures/browserScenarioCapture.ts b/tests/fixtures/browserScenarioCapture.ts index c5dfb24d56..7c7f9300ca 100644 --- a/tests/fixtures/browserScenarioCapture.ts +++ b/tests/fixtures/browserScenarioCapture.ts @@ -27,6 +27,10 @@ export interface ScenarioExchange { readonly body?: string | Buffer; readonly requestMediaType?: string; readonly requestBody?: string; + /** A request body Playwright did not expose. */ + readonly requestBodyHidden?: true; + /** The request event's header names. */ + readonly requestHeaderNames?: readonly string[]; /** Header values as `header_values` retains them; a null value is one the capture excluded. */ readonly requestHeaders?: readonly ScenarioHeader[]; readonly responseHeaders?: readonly ScenarioHeader[]; @@ -145,13 +149,15 @@ const exchangeEvents = ( method: exchange.method, url: exchange.redactedUrl === true ? { ...url, redacted: true } : url, resource_type: exchange.resourceType ?? "fetch", - header_names: [], + header_names: [...(exchange.requestHeaderNames ?? [])], }; const content = ( sequence: number, source: number, phase: "request" | "response", - body: ReturnType | { readonly state: "not_requested" }, + body: + | ReturnType + | { readonly state: "not_requested" | "not_exposed" }, ) => ({ sequence, step_index: 1, @@ -176,6 +182,7 @@ const exchangeEvents = ( let next = first + 1; if ( exchange.requestBody !== undefined || + exchange.requestBodyHidden === true || exchange.requestHeaders !== undefined ) { events.push( @@ -183,13 +190,15 @@ const exchangeEvents = ( next, first, "request", - exchange.requestBody === undefined - ? { state: "not_requested" } - : capturedBody( - exchange.requestBody, - "browser-exposed-request-bytes", - exchange.requestMediaType ?? null, - ), + exchange.requestBodyHidden === true + ? { state: "not_exposed" } + : exchange.requestBody === undefined + ? { state: "not_requested" } + : capturedBody( + exchange.requestBody, + "browser-exposed-request-bytes", + exchange.requestMediaType ?? null, + ), ), ); next += 1; diff --git a/tests/fixtures/environmentStateItemWrites.ts b/tests/fixtures/environmentStateItemWrites.ts new file mode 100644 index 0000000000..203cb03a30 --- /dev/null +++ b/tests/fixtures/environmentStateItemWrites.ts @@ -0,0 +1,297 @@ +import type { TimedCaptureRow } from "./analysisView.js"; +import type { JsonValue } from "../../src/domain/jsonValue.js"; + +type Entity = { readonly [key: string]: JsonValue }; + +const folder = (id: number, title: string): Entity => ({ + id, + title, + version: 1, +}); + +/** One seeded folder, a create a list shows after it, then one a list shows before that create by title, then a delete. */ +export const CREATES_BY_TITLE_ROWS: readonly TimedCaptureRow[] = (() => { + const rota = folder(1, "Lighthouse Rota"); + const pier = folder(2, "Scratch Pier 3"); + const meadow = folder(3, "Scratch Meadow 4"); + return [ + ["GET", "/api/folders", [rota]], + ["GET", "/api/folders/1", rota], + ["POST", "/api/folders", pier, 200, { title: "Scratch Pier 3" }], + ["GET", "/api/folders/2", pier], + ["GET", "/api/folders", [rota, pier]], + ["POST", "/api/folders", meadow, 200, { title: "Scratch Meadow 4" }], + ["GET", "/api/folders/3", meadow], + ["GET", "/api/folders", [rota, meadow, pier]], + ["DELETE", "/api/folders/2", { message: "Folder deleted" }, 200], + ["GET", "/api/folders/2", { message: "folder not found" }, 404], + ["GET", "/api/folders", [rota, meadow]], + ]; +})(); + +const LOCATIONS = [ + { + id: "7d1c0b6e-0001-4000-8000-000000000001", + name: "Garage", + kind: "location", + }, + { + id: "7d1c0b6e-0002-4000-8000-000000000002", + name: "Attic", + kind: "location", + }, +]; +const TAGS = [ + { + id: "3b9f2a10-0001-4000-8000-000000000001", + name: "Tools", + color: "#aa0000", + }, + { + id: "3b9f2a10-0002-4000-8000-000000000002", + name: "Winter", + color: "#0000aa", + }, +]; + +/** An item as the application answers it: its parent location and tags spelled out from the ids a call sent. */ +export const inventoryItem = ( + id: string, + name: string, + parent: Entity | null, + tags: readonly Entity[], +): Entity => ({ id, name, parent, tags: [...tags] }); + +/** Item creates naming a parent location and tags by id, the first with none, the second with one of each. */ +export const ITEM_REFERENCES_ROWS: readonly TimedCaptureRow[] = (() => { + const seeded = inventoryItem( + "5e5e0000-0001-4000-8000-000000000001", + "Ladder", + LOCATIONS[0]!, + [], + ); + const bare = inventoryItem( + "5e5e0000-0002-4000-8000-000000000002", + "Scratch box", + null, + [], + ); + const filed = inventoryItem( + "5e5e0000-0003-4000-8000-000000000003", + "Scratch drill", + LOCATIONS[1]!, + [TAGS[0]!], + ); + return [ + ["GET", "/api/locations", LOCATIONS], + ["GET", `/api/locations/${LOCATIONS[0]!["id"] as string}`, LOCATIONS[0]!], + ["GET", `/api/locations/${LOCATIONS[1]!["id"] as string}`, LOCATIONS[1]!], + ["GET", "/api/tags", TAGS], + ["GET", `/api/tags/${TAGS[0]!["id"] as string}`, TAGS[0]!], + ["GET", `/api/tags/${TAGS[1]!["id"] as string}`, TAGS[1]!], + ["GET", "/api/items", [seeded]], + ["GET", `/api/items/${seeded["id"] as string}`, seeded], + [ + "POST", + "/api/items", + bare, + 201, + { name: "Scratch box", parentId: null, tagIds: [] }, + ], + ["GET", `/api/items/${bare["id"] as string}`, bare], + ["GET", "/api/items", [seeded, bare]], + [ + "POST", + "/api/items", + filed, + 201, + { + name: "Scratch drill", + parentId: LOCATIONS[1]!["id"]!, + tagIds: [TAGS[0]!["id"]!], + }, + ], + ["GET", `/api/items/${filed["id"] as string}`, filed], + ["GET", "/api/items", [seeded, bare, filed]], + ]; +})(); + +export const ITEM_LOCATIONS = LOCATIONS; +export const ITEM_TAGS = TAGS; + +/** A feed entry with a `starred` flag a body-less action flips, and the instant it last changed. */ +export const feedEntry = ( + id: number, + starred: boolean, + changedAt: string, +): Entity => ({ + id, + title: `Entry ${String(id)}`, + starred, + changed_at: changedAt, +}); + +/** One entry starred, then unstarred, by the same body-less call, each read back after it. */ +export const BOOKMARK_TOGGLE_ROWS: readonly TimedCaptureRow[] = (() => { + const seeded = "2026-10-17T17:00:00.000000Z"; + const first = feedEntry(1, false, seeded); + const second = feedEntry(2, false, seeded); + const starred = feedEntry(1, true, "2026-10-17T18:00:03.120000Z"); + const unstarred = feedEntry(1, false, "2026-10-17T18:00:05.090000Z"); + return [ + ["GET", "/api/entries", [first, second]], + ["GET", "/api/entries/1", first], + ["GET", "/api/entries/2", second], + ["PUT", "/api/entries/1/bookmark", "", 204], + ["GET", "/api/entries/1", starred], + ["PUT", "/api/entries/1/bookmark", "", 204], + ["GET", "/api/entries/1", unstarred], + ["GET", "/api/entries", [unstarred, second]], + ]; +})(); + +const ranked = ( + id: number, + title: string, + position: number | null, +): Entity => ({ + id, + title, + position, +}); + +/** Folders a list shows by `position`, the seed holding none: the first create lists after the seed's higher title, and once the seed is deleted later lists alone would also fit a sort by title. */ +export const RANKED_AFTER_DELETE_ROWS: readonly TimedCaptureRow[] = (() => { + const zulu = ranked(1, "Zulu Base", null); + const bravo = ranked(2, "Bravo", 2); + const charlie = ranked(3, "Charlie", 3); + const able = ranked(4, "Able", 0); + return [ + ["GET", "/api/folders", [zulu]], + ["GET", "/api/folders/1", zulu], + ["POST", "/api/folders", bravo, 200, { title: "Bravo", position: 2 }], + ["GET", "/api/folders/2", bravo], + ["GET", "/api/folders", [zulu, bravo]], + ["DELETE", "/api/folders/1", { message: "Folder deleted" }, 200], + ["GET", "/api/folders/1", { message: "folder not found" }, 404], + ["GET", "/api/folders", [bravo]], + ["POST", "/api/folders", charlie, 200, { title: "Charlie", position: 3 }], + ["GET", "/api/folders/3", charlie], + ["GET", "/api/folders", [bravo, charlie]], + ["POST", "/api/folders", able, 200, { title: "Able", position: 0 }], + ["GET", "/api/folders/4", able], + ["GET", "/api/folders", [able, bravo, charlie]], + ]; +})(); + +/** Item creates: the first shows no parent at all, the second names one, the third sends `parentId: null` and stores none; lists show each item's id and name. */ +export const ADDED_EMPTY_REFERENCE_ROWS: readonly TimedCaptureRow[] = (() => { + const seeded = { id: "5e5e0000-0001-4000-8000-000000000001", name: "Ladder" }; + const loose = { + id: "5e5e0000-0002-4000-8000-000000000002", + name: "Scratch box", + }; + const filed = { + id: "5e5e0000-0003-4000-8000-000000000003", + name: "Scratch drill", + parent: LOCATIONS[1]!, + }; + const cleared = { + id: "5e5e0000-0004-4000-8000-000000000004", + name: "Scratch lamp", + parent: null, + }; + const summary = (item: Entity): Entity => ({ + id: item["id"]!, + name: item["name"]!, + }); + return [ + ["GET", "/api/locations", LOCATIONS], + ["GET", `/api/locations/${LOCATIONS[0]!["id"] as string}`, LOCATIONS[0]!], + ["GET", `/api/locations/${LOCATIONS[1]!["id"] as string}`, LOCATIONS[1]!], + ["GET", "/api/items", [seeded]], + ["GET", `/api/items/${seeded.id}`, seeded], + ["POST", "/api/items", loose, 201, { name: "Scratch box" }], + ["GET", `/api/items/${loose.id}`, loose], + ["GET", "/api/items", [seeded, loose].map(summary)], + [ + "POST", + "/api/items", + filed, + 201, + { name: "Scratch drill", parentId: LOCATIONS[1]!["id"]! }, + ], + ["GET", `/api/items/${filed.id}`, filed], + ["GET", "/api/items", [seeded, loose, filed].map(summary)], + [ + "POST", + "/api/items", + cleared, + 201, + { name: "Scratch lamp", parentId: null }, + ], + ["GET", `/api/items/${cleared.id}`, cleared], + ["GET", "/api/items", [seeded, loose, filed, cleared].map(summary)], + ]; +})(); + +/** Renames that keep collection order, which an ascending title sort would not replay, then a create a whole list could place by title. */ +export const REFUTED_TITLE_SORT_ROWS: readonly TimedCaptureRow[] = (() => { + const zulu = folder(1, "Zulu"); + const bravo = folder(2, "Bravo"); + const alpha = folder(3, "Alpha"); + const echo = folder(3, "Echo"); + const able = folder(1, "Able"); + const delta = folder(4, "Delta"); + return [ + ["GET", "/api/folders", [zulu, bravo, alpha]], + ["GET", "/api/folders/1", zulu], + ["GET", "/api/folders/2", bravo], + ["GET", "/api/folders/3", alpha], + ["PATCH", "/api/folders/3", echo, 200, { title: "Echo" }], + ["GET", "/api/folders/3", echo], + ["GET", "/api/folders", [zulu, bravo, echo]], + ["PATCH", "/api/folders/1", able, 200, { title: "Able" }], + ["GET", "/api/folders/1", able], + ["GET", "/api/folders", [able, bravo, echo]], + ["POST", "/api/folders", delta, 200, { title: "Delta" }], + ["GET", "/api/folders/4", delta], + ["GET", "/api/folders", [able, bravo, delta, echo]], + ]; +})(); + +/** A body-less bookmark action whose first call finds `starred` null, then flips the boolean it stored. */ +export const NULL_THEN_STARRED_ROWS: readonly TimedCaptureRow[] = (() => { + const seeded = { + id: 1, + title: "Entry 1", + starred: null, + changed_at: "2026-10-17T17:00:00.000000Z", + }; + const other = { + id: 2, + title: "Entry 2", + starred: false, + changed_at: "2026-10-17T17:00:00.000000Z", + }; + const starred = { + ...seeded, + starred: true, + changed_at: "2026-10-17T18:00:03.120000Z", + }; + const cleared = { + ...seeded, + starred: false, + changed_at: "2026-10-17T18:00:05.090000Z", + }; + return [ + ["GET", "/api/entries", [seeded, other]], + ["GET", "/api/entries/1", seeded], + ["GET", "/api/entries/2", other], + ["PUT", "/api/entries/1/bookmark", "", 204], + ["GET", "/api/entries/1", starred], + ["PUT", "/api/entries/1/bookmark", "", 204], + ["GET", "/api/entries/1", cleared], + ["GET", "/api/entries", [cleared, other]], + ]; +})(); diff --git a/tests/fixtures/environmentStateVariedCalls.ts b/tests/fixtures/environmentStateVariedCalls.ts new file mode 100644 index 0000000000..fc0baadde4 --- /dev/null +++ b/tests/fixtures/environmentStateVariedCalls.ts @@ -0,0 +1,466 @@ +import type { TimedCaptureRow } from "./analysisView.js"; +import { bookClock } from "./environmentStateWrites.js"; +import type { JsonValue } from "../../src/domain/jsonValue.js"; + +type Entity = { readonly [key: string]: JsonValue }; + +/** Two contact updates, the second sending only the email the first sent with a name and a city. */ +export const FEWER_MEMBERS_ROWS: readonly TimedCaptureRow[] = (() => { + const contact = ( + id: number, + name: string, + city: string, + email: string, + ): Entity => ({ id, name, city, email }); + const [ada, mina] = [ + contact(1, "Ada Finch", "Porto", "ada@example.test"), + contact(2, "Mina Okafor", "Accra", "mina@example.test"), + ]; + const adaMoved = contact( + 1, + "Ada Finch-Lowe", + "Lisbon", + "ada.lowe@example.test", + ); + const minaMail = { ...mina, email: "mina.o@example.test" }; + return [ + ["GET", "/api/contacts", [ada, mina]], + ["GET", "/api/contacts/1", ada], + ["GET", "/api/contacts/2", mina], + [ + "PUT", + "/api/contacts/1", + adaMoved, + 200, + { + name: "Ada Finch-Lowe", + city: "Lisbon", + email: "ada.lowe@example.test", + }, + ], + ["GET", "/api/contacts/1", adaMoved], + ["PUT", "/api/contacts/2", minaMail, 200, { email: "mina.o@example.test" }], + ["GET", "/api/contacts/2", minaMail], + ["GET", "/api/contacts", [adaMoved, minaMail]], + ]; +})(); + +/** Bundle creates: the first sends its order, the second leaves it to the application, which numbers it next. */ +export const NUMBERED_UNLESS_SENT_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)]; + const [charlie, delta] = [bundle(3, "Charlie", 5), bundle(4, "Delta", 6)]; + return [ + ["GET", "/api/bundles", [alpha, bravo]], + ["GET", "/api/bundles/1", alpha], + ["GET", "/api/bundles/2", bravo], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie", order: 5 }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [alpha, bravo, charlie]], + ["POST", "/api/bundles", delta, 201, { name: "Delta" }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [alpha, bravo, charlie, delta]], + ]; +})(); + +/** Tasks marked done: each answer stamps `done_at` to the nanosecond, the read after it to the second. */ +export const DONE_CLOCK_ROWS: readonly TimedCaptureRow[] = (() => { + const task = (id: number, title: string): Entity => ({ + id, + title, + done: false, + done_at: "0001-01-01T00:00:00Z", + }); + const [trim, oil] = [task(1, "Trim the wicks"), task(2, "Order lamp oil")]; + const done = (entity: Entity, at: number, fraction: string): Entity => ({ + ...entity, + done: true, + done_at: bookClock(at).replace("Z", fraction), + }); + return [ + ["GET", "/api/tasks", [trim, oil]], + ["GET", "/api/tasks/1", trim], + ["GET", "/api/tasks/2", oil], + [ + "PATCH", + "/api/tasks/1", + done(trim, 3, ".001234567Z"), + 200, + { done: true }, + ], + ["GET", "/api/tasks/1", done(trim, 3, "Z")], + ["PATCH", "/api/tasks/2", done(oil, 5, ".002345678Z"), 200, { done: true }], + ["GET", "/api/tasks/2", done(oil, 5, "Z")], + ]; +})(); + +/** Tasks marked done, each answer to the nanosecond: the first read spells `done_at` to the millisecond, the second to the second. */ +export const DONE_MIXED_ROWS: readonly TimedCaptureRow[] = DONE_CLOCK_ROWS.map( + (row, index) => + index === 4 && row[0] === "GET" + ? [ + row[0], + row[1], + { + ...(row[2] as Entity), + done_at: String((row[2] as Entity)["done_at"]).replace( + "Z", + ".001Z", + ), + }, + ] + : row, +); + +/** Bundles numbered unless an order is sent: 2 left out, 9 and 4 sent, the 9 deleted, then one left out the application numbers 5. */ +export const SENT_THEN_DELETED_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const seeds = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)]; + const [charlie, delta, echo, foxtrot] = [ + bundle(3, "Charlie", 2), + bundle(4, "Delta", 9), + bundle(5, "Echo", 4), + bundle(6, "Foxtrot", 5), + ]; + return [ + ["GET", "/api/bundles", seeds], + ["GET", "/api/bundles/1", seeds[0]!], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie" }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [...seeds, charlie]], + ["POST", "/api/bundles", delta, 201, { name: "Delta", order: 9 }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [...seeds, charlie, delta]], + ["POST", "/api/bundles", echo, 201, { name: "Echo", order: 4 }], + ["GET", "/api/bundles/5", echo], + ["GET", "/api/bundles", [...seeds, charlie, delta, echo]], + ["DELETE", "/api/bundles/4", null, 204], + ["GET", "/api/bundles", [...seeds, charlie, echo]], + ["POST", "/api/bundles", foxtrot, 201, { name: "Foxtrot" }], + ["GET", "/api/bundles/6", foxtrot], + ["GET", "/api/bundles", [...seeds, charlie, echo, foxtrot]], + ]; +})(); + +/** Tasks marked done: each answer stamps `done_at` to the millisecond in `Z`, the read after it to the second in `+00:00`. */ +export const DONE_ZONE_ROWS: readonly TimedCaptureRow[] = (() => { + const task = (id: number, title: string): Entity => ({ + id, + title, + done: false, + done_at: "0001-01-01T00:00:00Z", + }); + const [trim, oil] = [task(1, "Trim the wicks"), task(2, "Order lamp oil")]; + const done = (entity: Entity, at: number, suffix: string): Entity => ({ + ...entity, + done: true, + done_at: bookClock(at).replace("Z", suffix), + }); + return [ + ["GET", "/api/tasks", [trim, oil]], + ["GET", "/api/tasks/1", trim], + ["GET", "/api/tasks/2", oil], + ["PATCH", "/api/tasks/1", done(trim, 3, ".123Z"), 200, { done: true }], + ["GET", "/api/tasks/1", done(trim, 3, "+00:00")], + ["PATCH", "/api/tasks/2", done(oil, 5, ".234Z"), 200, { done: true }], + ["GET", "/api/tasks/2", done(oil, 5, "+00:00")], + ]; +})(); + +/** Tag PUTs: one leaves the owner out and the application clears it, the other sends it and keeps it. */ +export const PUT_CLEARS_ROWS: readonly TimedCaptureRow[] = (() => { + const tag = (id: number, name: string, owner: number | null): Entity => ({ + id, + name, + owner, + }); + const [inbox, bills] = [tag(1, "Inbox", 3), tag(2, "Bills", 3)]; + const [triage, paid] = [tag(1, "Triage", null), tag(2, "Paid", 3)]; + return [ + ["GET", "/api/tags", [inbox, bills]], + ["GET", "/api/tags/1", inbox], + ["GET", "/api/tags/2", bills], + ["PUT", "/api/tags/1", triage, 200, { name: "Triage" }], + ["GET", "/api/tags/1", triage], + ["PUT", "/api/tags/2", paid, 200, { name: "Paid", owner: 3 }], + ["GET", "/api/tags/2", paid], + ["GET", "/api/tags", [triage, paid]], + ]; +})(); + +/** Bundle creates the other way round: the first leaves its order to the application, the second sends one. */ +export const NUMBERED_THEN_SENT_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)]; + const [charlie, delta] = [bundle(3, "Charlie", 2), bundle(4, "Delta", 9)]; + return [ + ["GET", "/api/bundles", [alpha, bravo]], + ["GET", "/api/bundles/1", alpha], + ["GET", "/api/bundles/2", bravo], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie" }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [alpha, bravo, charlie]], + ["POST", "/api/bundles", delta, 201, { name: "Delta", order: 9 }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [alpha, bravo, charlie, delta]], + ]; +})(); + +/** Two name-only tag PUTs, one keeping the owner and one clearing it, in the given order. */ +const ownerPuts = (clearsFirst: boolean): readonly TimedCaptureRow[] => { + const tag = (id: number, name: string, owner: number | null): Entity => ({ + id, + name, + owner, + }); + const [inbox, bills] = [tag(1, "Inbox", 3), tag(2, "Bills", 3)]; + const [triage, paid] = [ + tag(1, "Triage", clearsFirst ? null : 3), + tag(2, "Paid", clearsFirst ? 3 : null), + ]; + return [ + ["GET", "/api/tags", [inbox, bills]], + ["GET", "/api/tags/1", inbox], + ["GET", "/api/tags/2", bills], + ["PUT", "/api/tags/1", triage, 200, { name: "Triage" }], + ["GET", "/api/tags/1", triage], + ["PUT", "/api/tags/2", paid, 200, { name: "Paid" }], + ["GET", "/api/tags/2", paid], + ["GET", "/api/tags", [triage, paid]], + ]; +}; + +/** A name-only PUT that keeps the owner, then one that clears it. */ +export const KEPT_THEN_CLEARED_ROWS = ownerPuts(false); + +/** Two name-only tag PUTs where one answers only `id` and `name` and its read shows the owner kept, and the other clears it. */ +const omittedOwnerPuts = (omitsFirst: boolean): readonly TimedCaptureRow[] => { + const tag = (id: number, name: string, owner: number | null): Entity => ({ + id, + name, + owner, + }); + const [inbox, bills] = [tag(1, "Inbox", 3), tag(2, "Bills", 3)]; + const kept = (entity: Entity, name: string) => ({ ...entity, name }); + const answered = (entity: Entity) => ({ + id: entity["id"]!, + name: entity["name"]!, + }); + const [triage, paid] = omitsFirst + ? [kept(inbox, "Triage"), tag(2, "Paid", null)] + : [tag(1, "Triage", null), kept(bills, "Paid")]; + const answer = (entity: Entity, omits: boolean) => + omits ? answered(entity) : entity; + return [ + ["GET", "/api/tags", [inbox, bills]], + ["GET", "/api/tags/1", inbox], + ["GET", "/api/tags/2", bills], + ["PUT", "/api/tags/1", answer(triage, omitsFirst), 200, { name: "Triage" }], + ["GET", "/api/tags/1", triage], + ["PUT", "/api/tags/2", answer(paid, !omitsFirst), 200, { name: "Paid" }], + ["GET", "/api/tags/2", paid], + ["GET", "/api/tags", [triage, paid]], + ]; +}; + +/** A name-only PUT answered without the owner its read shows kept, then one that clears it. */ +export const OMITTED_KEPT_THEN_CLEARED_ROWS = omittedOwnerPuts(true); + +/** A name-only PUT that clears the owner, then one answered without the owner its read shows kept. */ +export const CLEARED_THEN_OMITTED_KEPT_ROWS = omittedOwnerPuts(false); + +/** A name-only PUT that clears the owner, then one that keeps it. */ +export const CLEARED_THEN_KEPT_ROWS = ownerPuts(true); + +/** Bundle creates: the first sends order 5, the second leaves it out and the application stores 5, the third leaves it out and gets 6. */ +export const DEFAULT_THEN_NUMBERED_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)]; + const [charlie, delta, echo] = [ + bundle(3, "Charlie", 5), + bundle(4, "Delta", 5), + bundle(5, "Echo", 6), + ]; + return [ + ["GET", "/api/bundles", [alpha, bravo]], + ["GET", "/api/bundles/1", alpha], + ["GET", "/api/bundles/2", bravo], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie", order: 5 }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [alpha, bravo, charlie]], + ["POST", "/api/bundles", delta, 201, { name: "Delta" }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [alpha, bravo, charlie, delta]], + ["POST", "/api/bundles", echo, 201, { name: "Echo" }], + ["GET", "/api/bundles/5", echo], + ["GET", "/api/bundles", [alpha, bravo, charlie, delta, echo]], + ]; +})(); + +/** Tasks marked done, each answer to the nanosecond and each read after it to the microsecond. */ +export const DONE_MICRO_ROWS: readonly TimedCaptureRow[] = DONE_CLOCK_ROWS.map( + (row, index) => { + const answer = DONE_CLOCK_ROWS[index - 1]?.[2] as Entity | undefined; + return row[0] === "GET" && (index === 4 || index === 6) + ? [ + row[0], + row[1], + { + ...(row[2] as Entity), + done_at: String(answer!["done_at"]).replace( + /(\.\d{6})\d{3}Z$/u, + "$1Z", + ), + }, + ] + : row; + }, +); + +/** Tag PUTs whose owner is a user object, in the given order: one names the owner by `ownerId`, the other leaves it out and the application clears it. */ +const ownerReferencePuts = ( + clearsFirst: boolean, +): readonly TimedCaptureRow[] => { + const [ada, bo] = [ + { id: 3, name: "Ada" }, + { id: 4, name: "Bo" }, + ]; + const tag = (id: number, name: string, owner: Entity | null): Entity => ({ + id, + name, + owner, + }); + const [inbox, bills] = [tag(1, "Inbox", bo), tag(2, "Bills", bo)]; + const named = tag(clearsFirst ? 2 : 1, clearsFirst ? "Paid" : "Triage", ada); + const cleared = tag( + clearsFirst ? 1 : 2, + clearsFirst ? "Triage" : "Paid", + null, + ); + const put = (entity: Entity): TimedCaptureRow[] => [ + [ + "PUT", + `/api/tags/${String(entity["id"])}`, + entity, + 200, + entity["owner"] === null + ? { name: entity["name"]! } + : { name: entity["name"]!, ownerId: 3 }, + ], + ["GET", `/api/tags/${String(entity["id"])}`, entity], + ]; + const [first, second] = clearsFirst ? [cleared, named] : [named, cleared]; + return [ + ["GET", "/api/users", [ada, bo]], + ["GET", "/api/users/3", ada], + ["GET", "/api/users/4", bo], + ["GET", "/api/tags", [inbox, bills]], + ["GET", "/api/tags/1", inbox], + ["GET", "/api/tags/2", bills], + ...put(first), + ...put(second), + [ + "GET", + "/api/tags", + [first, second].sort((a, b) => Number(a["id"]) - Number(b["id"])), + ], + ]; +}; + +/** A PUT naming the owner by `ownerId`, then one that leaves it out and clears it. */ +export const OWNER_NAMED_THEN_CLEARED_ROWS = ownerReferencePuts(false); + +/** A PUT that leaves the owner out and clears it, then one naming it by `ownerId`. */ +export const OWNER_CLEARED_THEN_NAMED_ROWS = ownerReferencePuts(true); + +/** Bundle creates with a string order: the first leaves it out and the application numbers it "2", the second sends "" and stores it. */ +export const NUMBERED_THEN_BLANK_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: string): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", "0"), bundle(2, "Bravo", "1")]; + const [charlie, delta] = [bundle(3, "Charlie", "2"), bundle(4, "Delta", "")]; + return [ + ["GET", "/api/bundles", [alpha, bravo]], + ["GET", "/api/bundles/1", alpha], + ["GET", "/api/bundles/2", bravo], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie" }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [alpha, bravo, charlie]], + ["POST", "/api/bundles", delta, 201, { name: "Delta", order: "" }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [alpha, bravo, charlie, delta]], + ]; +})(); + +/** Bundle creates: the first sends order 9 and is deleted, the second leaves it out and the application numbers it 2. */ +export const SENT_DELETED_THEN_NUMBERED_ROWS: readonly TimedCaptureRow[] = + (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 1)]; + const [charlie, delta] = [bundle(3, "Charlie", 9), bundle(4, "Delta", 2)]; + return [ + ["GET", "/api/bundles", [alpha, bravo]], + ["GET", "/api/bundles/1", alpha], + ["GET", "/api/bundles/2", bravo], + ["POST", "/api/bundles", charlie, 201, { name: "Charlie", order: 9 }], + ["GET", "/api/bundles/3", charlie], + ["GET", "/api/bundles", [alpha, bravo, charlie]], + ["DELETE", "/api/bundles/3", null, 204], + ["GET", "/api/bundles", [alpha, bravo]], + ["POST", "/api/bundles", delta, 201, { name: "Delta" }], + ["GET", "/api/bundles/4", delta], + ["GET", "/api/bundles", [alpha, bravo, delta]], + ]; + })(); + +/** Bundles numbered per project: a create in project 1 sends order 9, one in project 2 leaves it out and gets 1. */ +export const SENT_IN_ANOTHER_PROJECT_ROWS: readonly TimedCaptureRow[] = (() => { + const bundle = (id: number, name: string, order: number): Entity => ({ + id, + name, + order, + }); + const [alpha, bravo] = [bundle(1, "Alpha", 0), bundle(2, "Bravo", 0)]; + const [charlie, delta] = [bundle(3, "Charlie", 9), bundle(4, "Delta", 1)]; + return [ + ["GET", "/api/projects/1/bundles", [alpha]], + ["GET", "/api/projects/1/bundles/1", alpha], + ["GET", "/api/projects/2/bundles", [bravo]], + ["GET", "/api/projects/2/bundles/2", bravo], + [ + "POST", + "/api/projects/1/bundles", + charlie, + 201, + { name: "Charlie", order: 9 }, + ], + ["GET", "/api/projects/1/bundles/3", charlie], + ["GET", "/api/projects/1/bundles", [alpha, charlie]], + ["POST", "/api/projects/2/bundles", delta, 201, { name: "Delta" }], + ["GET", "/api/projects/2/bundles/4", delta], + ["GET", "/api/projects/2/bundles", [bravo, delta]], + ]; +})(); diff --git a/tests/fixtures/fakeHopper.mjs b/tests/fixtures/fakeHopper.mjs index f6b650b785..9c18c8acbe 100644 --- a/tests/fixtures/fakeHopper.mjs +++ b/tests/fixtures/fakeHopper.mjs @@ -112,6 +112,46 @@ const enhancedFixtureResult = (method) => { return [{ address: "0x1000", value: "fixture" }]; case "procedure_pseudo_code": return "return 0;"; + case "rea_caller_scan_target": + return { + target: { + address: "0x1000", + name: "fixture", + symbol: null, + classification: null, + body: { + available: false, + reason: "The fixture has no function body-extent observation.", + }, + }, + instructions: ["0x1000"], + instructions_complete: true, + }; + case "rea_caller_base": + return { + target: { + address: "0x1000", + name: "fixture", + symbol: null, + classification: null, + body: { + available: false, + reason: "The fixture has no function body-extent observation.", + }, + }, + instructions: ["0x1000"], + instructions_complete: true, + callers: [], + callers_complete: true, + }; + case "rea_caller_scan_page": + return { + start: 0, + next: 0, + total: 0, + oversized: false, + items: [], + }; case "analyze_function": { return { procedure: { diff --git a/tests/fixtures/hopperCallerProbe.py b/tests/fixtures/hopperCallerProbe.py new file mode 100644 index 0000000000..eb98d039e8 --- /dev/null +++ b/tests/fixtures/hopperCallerProbe.py @@ -0,0 +1,68 @@ +"""Exercise the real paging owner using Hopper-shaped native objects.""" +import json +from pathlib import Path +import sys + +class Procedure: + def __init__(self, address): + self.address = address + self.callees = [] + self.calls = {} + def getEntryPoint(self): + return self.address + def getAllCalleeProcedures(self): + return self.callees + +class Segment: + def __init__(self, procedures): + self.procedures = procedures + def getProcedureCount(self): + return len(self.procedures) + def getProcedureAtIndex(self, index): + return self.procedures[index] + +class Document: + def __init__(self, segments): + self.segments = segments + def getSegmentsList(self): + return self.segments + +path = sys.argv[1] +bridge = {"__file__": path, "__name__": "rea_hopper_bridge"} +exec(compile(Path(path).read_text(encoding="utf-8"), path, "exec"), bridge) +target = Procedure(0x2000) +procedures = [Procedure(0x3000 + 0x10 * i) for i in range(130)] +procedures[65].callees = [target] +procedures[65].calls = {(procedures[65].address + 4, target.address): {"classification": "direct", "provider_type": 2, "provenance": "fixture"}} +document = Document([Segment(procedures[:32]), Segment(procedures[32:])]) +bridge["_procedure"] = lambda document, value: target +bridge["_instruction_addresses"] = lambda procedure: [procedure.address] +bridge["_procedure_identity"] = lambda procedure: {"address": hex(procedure.address)} +bridge["_iter_native_calls"] = lambda procedure, direction: ((source, destination, call) for (source, destination), call in procedure.calls.items()) +bridge["_containing_procedure"] = lambda document, address: (target if address == target.address else procedures[65], None) +read = lambda start: bridge["_caller_scan_page"](document, {"start": start}) +first, second, third = read(0), read(64), read(128) +procedures[65].callees = [] +procedures[65].calls = {} +changed = read(64) +try: + read(131) +except bridge["InvalidRequestError"]: + invalid = True +else: + invalid = False +original_identity = bridge["_procedure_identity"] +bridge["_procedure_identity"] = lambda procedure: {"address": hex(procedure.address), "name": "x" * 600000} +oversized = read(0) +bridge["_procedure_identity"] = original_identity +target.basicBlockIterator = lambda: [None] +bridge["_block_instruction_addresses"] = lambda procedure, block: range(0x100000000, 0x100010000) +limited_target = bridge["_caller_target_data"](target) +print(json.dumps({ + "first": {"start": first["start"], "next": first["next"], "total": first["total"], "items": len(first["items"])}, + "second": {"start": second["start"], "next": second["next"], "total": second["total"], "items": len(second["items"]), "caller": second["items"][1]}, + "third": {"start": third["start"], "next": third["next"], "total": third["total"], "items": len(third["items"])}, + "changed": changed["items"][1], "invalid": invalid, "oversized": oversized, + "target_limited": not limited_target["instructions_complete"], + "target_count": len(limited_target["instructions"]), +})) diff --git a/tests/support/cli/cliCommandSession.ts b/tests/support/cli/cliCommandSession.ts index 96ed81b521..21b890b886 100644 --- a/tests/support/cli/cliCommandSession.ts +++ b/tests/support/cli/cliCommandSession.ts @@ -129,7 +129,7 @@ const stopChild = async ( graceMs: number, ): Promise => { if (child.exitCode !== null || child.signalCode !== null) return; - // EOF lets the worker await closeCliJavaScriptResources before exiting. + // EOF lets the worker await closeCliResources before exiting. if (!child.stdin.writableEnded) child.stdin.end(); if (await waitForExit(child, graceMs)) return; signalChild(child, "SIGTERM"); diff --git a/tests/support/cli/cliCommandSessionWorker.mjs b/tests/support/cli/cliCommandSessionWorker.mjs index 88742a5900..c98d0f6ba1 100644 --- a/tests/support/cli/cliCommandSessionWorker.mjs +++ b/tests/support/cli/cliCommandSessionWorker.mjs @@ -40,7 +40,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { process.exit(1); } -const { closeCliJavaScriptResources, createCli } = +const { closeCliResources, createCli } = await import("../../../dist/cli-runtime/cli.js"); const { createCliResultOutput } = await import("../../../dist/cli-runtime/cliResultOutput.js"); @@ -106,7 +106,7 @@ try { await inputStopped; await chain; if (completion.kind === "failed") throw completion.cause; - }, [closeCliJavaScriptResources]); + }, [closeCliResources]); } catch (cause) { console.error(cause); process.exit(1); diff --git a/website/locales/ar/messages.po b/website/locales/ar/messages.po index b6d37ddde9..deeb0d4be9 100644 --- a/website/locales/ar/messages.po +++ b/website/locales/ar/messages.po @@ -71,13 +71,13 @@ msgstr "بواسطة N0zoM1z0 * 10 أكتوبر 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3205,21 +3205,21 @@ msgstr "الطرفية" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "نسخ" @@ -3443,12 +3443,12 @@ msgid "" msgstr "{ph0}أي نظام:{ph1} اسحب المجلد إلى نافذة طرفية ويظهر مساره." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "في المحطة الطرفية ، ضع علامات اقتباس حول مسار يحتوي على مسافات." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3456,14 +3456,14 @@ msgid "" msgstr "" "تحليل JavaScript أو Electron مجلد التطبيق أو ASAR الأرشيف دون تشغيل التطبيق:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "تحليل التطبيق الثابت" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3473,13 +3473,13 @@ msgstr "" "استبدل المسار بدليل التطبيق المستخرج أو {ph0} ملف. على Windows، مسار مثل " "{ph1} يعمل هنا." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "أين أجد ملف .asar للتطبيق؟" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3490,7 +3490,7 @@ msgstr "Electron تطبيقات عادة يبقيه في {ph0} المجلد:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3502,7 +3502,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3515,20 +3515,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} في كثير من الأحيان {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "يتم تثبيت بعض التطبيقات في مكان آخر. ابحث في مجلد التطبيق عن {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3539,36 +3539,36 @@ msgstr "" "اتصالات REA لا يمكن حل يتم وضع علامة في النتيجة." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "إذا أردت أن يكون أمر {ph0} متاحًا دائمًا في بيئة الأوامر لديك:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "تثبيت CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}تواصل مع JavaScript دليل التطبيق{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "ربط الخاص بك Ghidra تركيب" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "الاتصال Ghidra على Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3579,7 +3579,7 @@ msgstr "" "الأصلية بمعمارية x86 وx64 على أقراص NTFS المحلية الثابتة. استخدم نسخة مثبتة " "من Ghidra 12.1.x وJDK بمعمارية 64 بت وإصدار 21 أو أحدث:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3594,18 +3594,18 @@ msgstr "" "الاتصال Hopper على macOS أو Linux. قم بتشغيل أمر الإعداد أعلاه ؛ يكتشف " "تثبيتا موجودا أو يقدم خطة تثبيت للموافقة عليها." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "الحصول على المساعدة" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "إذا كان وكيلك لا يرى REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3615,22 +3615,22 @@ msgstr "" "أعد تشغيل العميل أو أعد توصيله بعد الإعداد. تحقق لها REA اتصال مع {ph0}؛ إلى" " عن على Codex:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "إذا احتاج مقدم الخدمة إلى الاهتمام" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "تحقق من الموفر الذي تستخدمه. ل Ghidra:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "تحقق من مزود واحد" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "يعطي التقرير الاختيار الفاشل والخطوة التالية المقترحة." @@ -3639,7 +3639,7 @@ msgstr "يعطي التقرير الاختيار الفاشل والخطوة ا #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3652,7 +3652,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3666,7 +3666,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3678,7 +3678,7 @@ msgstr "{ph0}GitHub القضايا{ph1}{ph2}الإبلاغ عن مشكلة في #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3741,22 +3741,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "نسخ REA موجه التثبيت" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "نسخ JavaScript أمر التحليل" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "نسخ CLI أمر التثبيت" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "نسخ أمر تشخيص أداة التحليل" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "العودة للأعلى" @@ -5087,21 +5087,7 @@ msgstr "REA يحتاج إلى واحد من هؤلاء لقراءة البرام #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1} (macOS أو Linux): REAيكتشف الإعداد أو يعرض تثبيته. على " -"macOS, افتح Hopper مرة واحدة واختيار طريقة العرض أو تفعيل ترخيص قبل التحليل " -"الأول." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5113,7 +5099,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5127,14 +5113,14 @@ msgstr "انظر {ph0}إعداد تحليل برامج رمز الجهاز{ph1} #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}مساعدك البرمجي" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "نسخ" @@ -5145,7 +5131,7 @@ msgstr "نسخ" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5155,7 +5141,7 @@ msgstr "" "{ph0}›{ph1}{ph2}استخدام REA لمعرفة كيف DX-Ball يحسب بالغسل الصوت. شرح الحساب" " وإظهار التعليمات البرمجية.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5166,7 +5152,7 @@ msgstr "" "للتنفيذ. REA لا يشمل DX-Ball، لذا استخدم نسختك الخاصة ، أو اطرح نفس النوع من" " الأسئلة حول صغيرة أخرى Windows برنامج لديك." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "كيف يمكنني الحصول على المسار الكامل للملف?" @@ -5175,7 +5161,7 @@ msgstr "كيف يمكنني الحصول على المسار الكامل للم #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5190,7 +5176,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5202,7 +5188,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5210,17 +5196,17 @@ msgid "" "appears." msgstr "{ph0}أي نظام:{ph1} اسحب الملف إلى نافذة طرفية ويظهر مساره." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "قراءة REA النتائج" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "قراءة المدخلات" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5229,13 +5215,13 @@ msgstr "" "تقوم التعليمات بتحميل وسيطة عدد صحيح وتحويلها لحساب الفاصلة العائمة. هذا هو " "الإدخال الغائب عن عرض الكود الكاذب الأول." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "اتبع الدالة المستدعية" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5245,12 +5231,12 @@ msgstr "" "REA يحدد الدالة المستدعية ضرب الطوب. تعليماته السابقة تضاعف إحداثيات البلاط " "في 30 ؛ مضيفا {ph0} (20) يعطي موقف شاشة الطوب." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "قراءة الثوابت المشار إليها" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5259,12 +5245,12 @@ msgstr "" "يشير الحساب إلى هذه العناوين. قراءة وحدات البايت تزود القيم الفعلية ؛ يخبر " "ترتيب التعليمات الوكيل كيف يتم استخدامها." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "اكتب وتحقق من الوظيفة المستردة" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5274,7 +5260,7 @@ msgstr "" "بمقياس 1.0 ، مواضع الشاشة 0 و 320 و 640 خريطة إلى -500 و 0 و 500 قبل تحويل " "عدد صحيح. يضع الحساب وسط الشاشة في وسط حقل الاستريو." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5286,20 +5272,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}انظر التحقيق الكامل ومقارنة التجميع إلى ج{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "تحاول ذلك مع ثنائي الخاصة بك" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5312,7 +5298,7 @@ msgstr "" "مكتبة وميزة للتحقيق فيها. تتضمن أسئلة البداية المفيدة \"كيف يتم تحليل هذا " "الملف?\"أو\" ما هي الوظيفة التي تتعامل مع إجراء القائمة هذا?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5321,13 +5307,13 @@ msgstr "" "إذا كنت تعرف بالفعل اسم دالة أو عنوانا ، فيمكنك الاستعلام عنه من الجهاز. " "للحصول على تكوين Ghidra تركيب:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "تحليل الوظيفة" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5340,7 +5326,7 @@ msgstr "" "يستورد الاحتجاج ويحلل الثنائي ، ثم يغلق جلسته. وكيلك MCP استعلامات إعادة " "استخدام ثنائي المستوردة في حين أن الدورة مفتوحة." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5373,17 +5359,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "تحليل برامج كود الآلة" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "نسخة مثال موجه" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "نسخ تحليل وظيفة" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "العودة للأعلى" @@ -6346,7 +6332,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}مساعدك البرمجي" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "نسخ" @@ -6752,21 +6738,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "فحص APK نفسك" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}إعداد REA{ph1} على macOS أو Linux، ثم قم بتنزيل هذين الملفين في نفس " -"المجلد. على Windows، استخدم أ Linux البيئة مثل وسل." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6776,18 +6750,18 @@ msgstr "" "تحليل Android يحتاج جدك الكامل 17 أو في وقت لاحق. حدده مع {ph0} أو {ph1}. " "هذه الحالة تستخدم جدك 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "نقطة REA في ملف الموفر وفحص APK:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6799,7 +6773,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6809,7 +6783,7 @@ msgstr "" "السؤال التالي: {ph0}ماذا يحدث عندما يستخدم الحساب ثمانية أرقام أو فاصل زمني " "مختلف?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "المصادر" @@ -6825,7 +6799,7 @@ msgstr "المصادر" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6839,14 +6813,14 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" "{ph0}رك 6238 الحالات المرجعية{ph1} · {ph2}بيانات الاختبار التجريبي{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6891,12 +6865,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "اتبع حساب رمز تسجيل الدخول" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "نسخ APK قيادة التفتيش" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "العودة للأعلى" @@ -10613,13 +10587,13 @@ msgstr "الأدلة" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10636,13 +10610,13 @@ msgstr "الكود المصدري" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10659,13 +10633,13 @@ msgstr "رخصة MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10682,13 +10656,13 @@ msgstr "انضم إلى المجتمع" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10773,22 +10747,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA النتيجة * ملخص.مقتطفات التصنيف الدولي للبراءات" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA مقتطفات التعليمات * 0 * 406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA مقتطف الدالة المستدعية · 0 * 411 أف 5 ب" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA نتائج البايت * تفسر على أنها زوجي صغير" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "تعافى ج * مقتطفات" @@ -10849,12 +10823,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * جينيراتوتب ، كامل الجسم" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "تحميل Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "تحميل مزود تحليل Android" @@ -13449,11 +13423,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "غير متاح" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "التوزيعات المدعومة" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13490,17 +13459,7 @@ msgstr "" "التجريبي أو فعّل ترخيصك عندما يفتح Hopper لأول مرة. على Linux، يجهّز الإعداد" " النسخة التجريبية المدعومة وشاشتها الخاصة." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"أنظمة Linux المضيفة: Ubuntu 24.04+ وFedora 41+ وNobara 44+ وArch Linux " -"وCachyOS أو Omarchy. Hopper برنامج تجاري؛ للنسخة التجريبية حدود تحددها " -"الشركة المنتجة." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13513,18 +13472,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}إعداد Hopper وحل المشكلات{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · ثبّت أداة التحليل المجانية وJava" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13534,7 +13493,7 @@ msgstr "" "المطابقة لمعمارية جهازك." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13547,7 +13506,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13563,7 +13522,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13571,7 +13530,7 @@ msgstr "{ph0}تنزيل JDK{ph1} · {ph2}تنزيل Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13581,7 +13540,7 @@ msgstr "" "تحقق من Java. يعرض الأمر الأول {ph0}؛ استخدم ذلك المجلد لـ {ph1}. ويتحقق " "الأمر الثاني من تثبيت المترجم:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13598,7 +13557,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13608,12 +13567,12 @@ msgstr "" "{ph0}إعداد Ghidra ومتطلبات المعمارية{ph1} · {ph2}دليل Ghidra على " "Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · اربط نسخة مثبتة لديك" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13624,7 +13583,7 @@ msgstr "" " يكون الملف الثنائي الأصلي مفتوحًا فيها، أو مشرفًا دون واجهة رسومية يفتح " "قاعدة بيانات خاصة." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13636,7 +13595,7 @@ msgstr "" "Professional 9.1. لجسر الواجهة الرسومية الأصلي الاختياري متطلبات إصدار ومنصة" " خاصة به." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13648,13 +13607,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}أنماط IDA وأمثلة التسجيل والمنصات المتحقق منها{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13665,18 +13624,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}اتبع مثالًا للتحليل الأصلي{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "مواقع الويب وAndroid و.NET والبرامج الثابتة وغيرها" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13685,110 +13644,110 @@ msgstr "" "افتح دليل النوع الذي تريد فحصه. يوضح كل دليل الأدوات الإضافية والأنظمة " "المضيفة المدعومة والأمر الأول." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "الهدف" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "أدوات ينبغي تجهيزها" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "الدليل" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}افتح الدليل{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "موقع ويب" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / الحزم" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "جهاز Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "موارد Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "مشروع JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "البرامج الثابتة" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / تعطل Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "الإشارات المنطقية" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "سلوك العمليات" @@ -13804,7 +13763,7 @@ msgstr "سلوك العمليات" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13814,18 +13773,18 @@ msgstr "" "{ph0}دليل Flutter{ph1} · {ph2}الحزم والموارد{ph3} · {ph4}بايت كود EVM{ph5} ·" " {ph6}جميع أدلة الفحص{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "استخدم REA من الطرفية" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "أوامر CLI والتثبيت العام وcurl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13835,14 +13794,14 @@ msgstr "" "استبدل {ph0} باسم دالة أو عنوانها من قائمة دوال الهدف. يجب إعداد Ghidra " "أولًا." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " "Node.js:" msgstr "على macOS أو Linux، يمكنك أيضًا استخدام مثبت curl بعد تجهيز Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -13851,12 +13810,12 @@ msgstr "" "استخدم فحص التطبيق في الخطوة 3 مع معرّف إعدادك. في المحررات، فعّل REA في " "إعدادات MCP واسمح باستخدام أدواته في محادثة المساعد." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows وWSL والمسارات التي تحتوي على مسافات" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -13867,17 +13826,17 @@ msgstr "" "يعمل داخل WSL، فثبّت Node.js وREA داخل بيئة Linux تلك واستخدم مسارات Linux " "فيها. تعمل موفرات التحليل دون واجهة رسومية على الجهاز نفسه الذي يشغّل REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "أمر إعداد Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "تحديث REA أو إزالته" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -13888,7 +13847,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13902,7 +13861,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -13925,18 +13884,18 @@ msgstr "نسخ أمر إعداد REA على Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "نسخ الأمر" @@ -13992,14 +13951,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · اختر نموذج ذكاء اصطناعي" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "جهّز ملف تسجيل IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14014,7 +13973,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14027,7 +13986,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14039,12 +13998,12 @@ msgstr "" "و{ph2} الاختيارية من تسجيله العامل إلى الملف. افتح الملف الثنائي الأصلي في " "IDA وابدأ الملحق الأصلي قبل أن تطلب من REA فحصه." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "قاعدة بيانات SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "بايت كود EVM" @@ -14099,7 +14058,7 @@ msgstr "" "راجع الخطة ووافق عليها، ثم أعد فتح Claude و{ph0}تحقق من الاتصال في الخطوة " "3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14109,12 +14068,12 @@ msgstr "" "استبدل المسارين بمساري نسختيك المثبتتين. عند تثبيت نسخة جديدة على macOS أو " "Linux arm64، ابنِ أداة فك الترجمة الأصلية لجهازك قبل المتابعة." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "ابنِ أداة فك الترجمة الأصلية لـ Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14127,7 +14086,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14137,7 +14096,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14147,13 +14106,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · تعليمات البناء الأصلي{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "ثم افحص Ghidra واحفظ مساراته لتطبيق المساعد:" @@ -14537,7 +14496,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "خيارات تثبيت أخرى: {ph0}{ph1} ←{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14550,7 +14509,7 @@ msgstr "اتبع تعليمات الإعداد لربط وكيلك. إذا لم #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14564,7 +14523,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14575,7 +14534,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14586,7 +14545,7 @@ msgstr "" "Linux، أمر الإعداد الافتراضي هو {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14604,13 +14563,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15494,7 +15453,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15503,6 +15462,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1} (macOS أو Linux): REAيكتشف الإعداد أو يعرض تثبيته. على " +#~ "macOS, افتح Hopper مرة واحدة واختيار طريقة العرض أو تفعيل ترخيص قبل التحليل " +#~ "الأول." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}إعداد REA{ph1} على macOS أو Linux، ثم قم بتنزيل هذين الملفين في نفس " +#~ "المجلد. على Windows، استخدم أ Linux البيئة مثل وسل." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "التوزيعات المدعومة" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "أنظمة Linux المضيفة: Ubuntu 24.04+ وFedora 41+ وNobara 44+ وArch Linux " +#~ "وCachyOS أو Omarchy. Hopper برنامج تجاري؛ للنسخة التجريبية حدود تحددها " +#~ "الشركة المنتجة." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/de/messages.po b/website/locales/de/messages.po index e4b5f6e5ca..a2b9194c8b 100644 --- a/website/locales/de/messages.po +++ b/website/locales/de/messages.po @@ -72,13 +72,13 @@ msgstr "Von N0zoM1z0 · 10. Oktober 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3340,21 +3340,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Kopieren" @@ -3589,14 +3589,14 @@ msgstr "" "sein Pfad wird angezeigt." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Setzen Sie in einem Terminal Anführungszeichen um einen Pfad, der " "Leerzeichen enthält." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3605,14 +3605,14 @@ msgstr "" "Analysieren Sie einen JavaScript- oder Electron-Anwendungsordner oder ein " "ASAR-Archiv, ohne die App auszuführen:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Statische Anwendungsanalyse" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3622,13 +3622,13 @@ msgstr "" "Ersetzen Sie den Pfad durch Ihr extrahiertes App-Verzeichnis oder {ph0} " "Datei. Auf Windows ein Pfad wie {ph1} arbeitet hier." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Wo finde ich eine App .asar-Datei?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3639,7 +3639,7 @@ msgstr "Electron Apps halten es normalerweise in einem {ph0} Ordner:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3651,7 +3651,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3664,14 +3664,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} häufig {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3679,7 +3679,7 @@ msgstr "" "Einige Apps werden an anderer Stelle installiert. Durchsuchen Sie den Ordner" " der App nach {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3691,36 +3691,36 @@ msgstr "" "Ergebnis markiert." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Wenn Sie eine dauerhafte {ph0} befehl in Ihrer Shell:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Installieren Sie den CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Fahren Sie mit dem JavaScript-Anwendungshandbuch fort{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Verbinden Sie Ihre Ghidra-Installation" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Verbinden Sie Ghidra mit Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3732,7 +3732,7 @@ msgstr "" "Verwenden Sie einen vorhandenen Ghidra 12.1.x-Installation und ein 64-Bit-" "JDK 21 oder neuer:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3750,18 +3750,18 @@ msgstr "" "Befehl aus. Es erkennt eine vorhandene Installation oder bietet einen " "Installationsplan zur Genehmigung an." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Hilfe bekommen" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Wenn Ihr Agent REA nicht sieht" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3771,22 +3771,22 @@ msgstr "" "Starten Sie den Client nach dem Setup neu oder verbinden Sie ihn erneut. " "Überprüfen Sie die REA-Verbindung mit {ph0}; für Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Wenn ein Anbieter Aufmerksamkeit braucht" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Überprüfen Sie den Anbieter, den Sie verwenden. Für Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Überprüfen Sie einen Anbieter" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3797,7 +3797,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3810,7 +3810,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3824,7 +3824,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3838,7 +3838,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3904,22 +3904,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Installationsaufforderung für REA kopieren" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Kopieren Sie den JavaScript-Analysebefehl" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Kopieren Sie den Installationsbefehl CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Provider-Diagnosebefehl kopieren" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Nach oben" @@ -5301,22 +5301,7 @@ msgstr "REA benötigt eines davon, um native Programme zu lesen:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} MODELL: Hopper {ph1} (macOS oder Linux): Das Setup von REA erkennt es " -"oder bietet an, es zu installieren. Öffnen Sie macOS auf Hopper einmal und " -"wählen Sie den Demo-Modus oder aktivieren Sie eine Lizenz vor Ihrer ersten " -"Analyse." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5329,7 +5314,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5345,14 +5330,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ihr Codierungsagent" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Kopieren" @@ -5363,7 +5348,7 @@ msgstr "Kopieren" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5374,7 +5359,7 @@ msgstr "" "Stereoklangpositionierung berechnet. Erklären Sie die Berechnung und zeigen " "Sie den Code.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5386,7 +5371,7 @@ msgstr "" "Verwenden Sie also Ihre eigene Kopie oder stellen Sie die gleiche Frage zu " "einem anderen kleinen Windows-Programm, das Sie haben." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Wie erhalte ich den vollständigen Pfad einer Datei?" @@ -5395,7 +5380,7 @@ msgstr "Wie erhalte ich den vollständigen Pfad einer Datei?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5411,7 +5396,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5424,7 +5409,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5434,17 +5419,17 @@ msgstr "" "{ph0}Jedes System:{ph1} ziehen Sie die Datei in ein Terminalfenster und ihr " "Pfad wird angezeigt." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Lesen Sie die REA-Ergebnisse" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Lesen Sie die Eingabe" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5455,14 +5440,14 @@ msgstr "" "Ansicht fehlt." # Reviewed for grammatical function-call terminology. -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Die aufrufende Funktion verfolgen" # Reviewed for grammatical function-call terminology. #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5473,12 +5458,12 @@ msgstr "" "multipliziert sie die Gitterkoordinate mit 30 und addiert {ph0} (20). So " "ergibt sich die Bildschirmposition des Ziegels." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Lesen Sie die referenzierten Konstanten" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5488,12 +5473,12 @@ msgstr "" "tatsächlichen Werte; Die Befehlsreihenfolge teilt dem Agenten mit, wie sie " "verwendet werden." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Schreiben und überprüfen Sie die wiederhergestellte Funktion" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5504,7 +5489,7 @@ msgstr "" " der Ganzzahlumwandlung auf -500, 0 und 500 abgebildet. Bei der Berechnung " "wird die Bildschirmmitte in die Mitte des Stereofelds gesetzt." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5517,7 +5502,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" @@ -5525,14 +5510,14 @@ msgstr "" "{ph0}Sehen Sie sich die vollständige Untersuchung und den Assembly-to-C-" "Vergleich an{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Probieren Sie es mit Ihrer eigenen Binärdatei aus" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5547,7 +5532,7 @@ msgstr "" " “Wie wird diese Datei analysiert?\" oder \"Welche Funktion behandelt diese " "Menüaktion?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5556,13 +5541,13 @@ msgstr "" "Wenn Sie bereits einen Funktionsnamen oder eine Adresse kennen, können Sie " "diese vom Terminal abfragen. Für eine konfigurierte Ghidra-Installation:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Funktionsanalyse" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5577,7 +5562,7 @@ msgstr "" "verwenden die importierte Binärdatei wieder, während diese Sitzung geöffnet " "ist." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5612,17 +5597,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Analyse von Maschinencode-Programmen" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Beispiel-Eingabeaufforderung kopieren" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Analyse der Kopierfunktion" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Nach oben" @@ -6620,7 +6605,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ihr Codierungsagent" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Kopieren" @@ -7037,22 +7022,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Überprüfen Sie den APK selbst" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Einrichten von REA {ph1} laden Sie diese beiden Dateien auf macOS oder " -"Linux in denselben Ordner herunter. Verwenden Sie auf Windows eine Linux-" -"Umgebung wie WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7062,18 +7034,18 @@ msgstr "" "Android Analysis benötigt ein vollständiges JDK 17 oder höher. Wählen Sie es" " mit {ph0} oder {ph1}. In diesem Fall wurde JDK 21 verwendet." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Zeigen Sie mit REA auf die Providerdatei und überprüfen Sie den APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "MODELL: macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7086,7 +7058,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7096,7 +7068,7 @@ msgstr "" "Eine nächste Frage: {ph0}was passiert, wenn ein Konto acht Ziffern oder ein " "anderes Zeitintervall verwendet?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Hauptquellen" @@ -7112,7 +7084,7 @@ msgstr "Hauptquellen" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7126,13 +7098,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}Referenzfälle zu RFC 6238{ph1} · {ph2}Demo-Testdaten{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7178,12 +7150,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Folgen Sie der Login-Code-Berechnung" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Inspektionsbefehl APK kopieren" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Nach oben" @@ -11032,13 +11004,13 @@ msgstr "Anleitungen" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -11055,13 +11027,13 @@ msgstr "Quellcode" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -11078,13 +11050,13 @@ msgstr "MIT-Lizenz" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -11101,13 +11073,13 @@ msgstr "Zur Community" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11193,23 +11165,23 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA Ergebnis * Zusammenfassung.ipc Auszug" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA Auszug aus der Anleitung · 0x406409" # Reviewed for grammatical function-call terminology. -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA · Aufrufende Funktion bei 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA Byte-Ergebnisse * interpretiert als Little-Endian-Doubles" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Wiederhergestellter C · -Auszug" @@ -11271,12 +11243,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, kompletter Körper" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Laden Sie Aegis APK herunter" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Android-Analyse-Anbieter herunterladen" @@ -14007,11 +13979,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Nicht verfügbar" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Unterstützte Distributionen" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -14049,17 +14016,7 @@ msgstr "" "aktivieren Sie Ihre Lizenz. Unter Linux richtet das Setup die unterstützte " "Demo und deren separate Anzeige ein." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linux-Hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS oder" -" Omarchy. Hopper ist kommerzielle Software; für die Demo gelten die vom " -"Anbieter festgelegten Einschränkungen." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -14073,18 +14030,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopper einrichten und Probleme beheben{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · das kostenlose Analysewerkzeug und Java installieren" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -14094,7 +14051,7 @@ msgstr "" "Compilers. Wählen Sie das Paket passend zur Architektur Ihres Computers." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14108,7 +14065,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14124,7 +14081,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -14133,7 +14090,7 @@ msgstr "" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14143,7 +14100,7 @@ msgstr "" "Prüfen Sie Java. Der erste Befehl gibt {ph0} aus; verwenden Sie diesen " "Ordner für {ph1}. Der zweite bestätigt, dass der Compiler installiert ist:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14161,7 +14118,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14171,12 +14128,12 @@ msgstr "" "{ph0}Ghidra einrichten und Architekturvoraussetzungen prüfen{ph1} · " "{ph2}Ghidra-Anleitung für Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · eine vorhandene Installation verbinden" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14188,7 +14145,7 @@ msgstr "" " mit geöffnetem Originalprogramm oder einen Supervisor ohne grafische " "Oberfläche, der eine private Datenbank öffnet." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14200,7 +14157,7 @@ msgstr "" "Professional 9.1 unter macOS arm64. Die optionale native GUI-Brücke hat " "eigene Versions- und Plattformvoraussetzungen." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14213,14 +14170,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}IDA-Profile, Registrierungsbeispiele und geprüfte Plattformen{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14231,18 +14188,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Einem Beispiel zur nativen Analyse folgen{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Websites, Android, .NET, Firmware und weitere Ziele" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14252,110 +14209,110 @@ msgstr "" " zusätzlich benötigten Werkzeuge, die unterstützten Host-Systeme und den " "ersten Befehl." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Ziel" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Benötigte Werkzeuge" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Anleitung" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Anleitung öffnen{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Website" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / Pakete" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android-APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android-Gerät" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Android-Ressourcen" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEB-Projekt" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linux-Absturz" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Logiksignale" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Prozessverhalten" @@ -14371,7 +14328,7 @@ msgstr "Prozessverhalten" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14381,18 +14338,18 @@ msgstr "" "{ph0}Flutter-Anleitung{ph1} · {ph2}Pakete und Ressourcen{ph3} · {ph4}EVM-" "Bytecode{ph5} · {ph6}Alle Untersuchungsanleitungen{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "REA im Terminal verwenden" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLI-Befehle, globale Installation und curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14402,7 +14359,7 @@ msgstr "" "Ersetzen Sie {ph0} durch einen Funktionsnamen oder eine Adresse aus der " "Funktionsliste des Ziels. Ghidra muss vorher eingerichtet sein." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14411,7 +14368,7 @@ msgstr "" "Unter macOS oder Linux können Sie nach der Einrichtung von Node.js auch den " "curl-Installer verwenden:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14421,12 +14378,12 @@ msgstr "" "Aktivieren Sie REA bei Editor-Clients in den MCP-Einstellungen und erlauben " "Sie seine Werkzeuge im Agenten-Chat." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL und Pfade mit Leerzeichen" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14438,17 +14395,17 @@ msgstr "" "Linux-Umgebung und verwenden Sie dort Linux-Pfade. Anbieter ohne grafische " "Oberfläche laufen auf demselben Host wie REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Setup-Befehl für Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "REA aktualisieren oder entfernen" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14460,7 +14417,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14474,7 +14431,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14497,18 +14454,18 @@ msgstr "REA-Setup-Befehl für Windows kopieren" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Befehl kopieren" @@ -14568,14 +14525,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · ein KI-Modell auswählen" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Die IDA-Registrierungsdatei vorbereiten" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14591,7 +14548,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14604,7 +14561,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14617,12 +14574,12 @@ msgstr "" "Datei. Öffnen Sie das Originalprogramm in IDA und starten Sie dessen " "vorgeschaltetes Plugin, bevor Sie REA mit der Untersuchung beauftragen." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLite-Datenbank" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVM-Bytecode" @@ -14682,7 +14639,7 @@ msgstr "" "Prüfen und genehmigen Sie den Plan, öffnen Sie Claude erneut und {ph0}prüfen" " Sie die Verbindung in Schritt 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14693,12 +14650,12 @@ msgstr "" "Kompilieren Sie bei einer neuen Installation unter macOS oder Linux arm64 " "zuerst den nativen Decompiler für Ihren Computer." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Den nativen Decompiler von Ghidra kompilieren" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14711,7 +14668,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14721,7 +14678,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14731,13 +14688,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · Anleitung zum nativen Build{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" @@ -15146,7 +15103,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Weitere Installationsoptionen: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15161,7 +15118,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15176,7 +15133,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15188,7 +15145,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15200,7 +15157,7 @@ msgstr "" "Setup-Befehl {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15219,13 +15176,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -16127,7 +16084,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -16136,6 +16093,102 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} MODELL: Hopper {ph1} (macOS oder Linux): Das Setup von REA erkennt es " +#~ "oder bietet an, es zu installieren. Öffnen Sie macOS auf Hopper einmal und " +#~ "wählen Sie den Demo-Modus oder aktivieren Sie eine Lizenz vor Ihrer ersten " +#~ "Analyse." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Einrichten von REA {ph1} laden Sie diese beiden Dateien auf macOS oder " +#~ "Linux in denselben Ordner herunter. Verwenden Sie auf Windows eine Linux-" +#~ "Umgebung wie WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Unterstützte Distributionen" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linux-Hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS oder" +#~ " Omarchy. Hopper ist kommerzielle Software; für die Demo gelten die vom " +#~ "Anbieter festgelegten Einschränkungen." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/el/messages.po b/website/locales/el/messages.po index b82a2dd600..46ae3fc2a9 100644 --- a/website/locales/el/messages.po +++ b/website/locales/el/messages.po @@ -188,13 +188,13 @@ msgstr "Του N0zoM1z0 · 10 Οκτωβρίου 2026" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -211,13 +211,13 @@ msgstr "Πηγαίος κώδικας" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -234,13 +234,13 @@ msgstr "Άδεια MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -257,13 +257,13 @@ msgstr "Ελάτε στην κοινότητα" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -282,13 +282,13 @@ msgstr "Ακολουθήστε μας στο X" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -5148,21 +5148,21 @@ msgstr "Τερματικό" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Αντιγραφή" @@ -5428,13 +5428,13 @@ msgstr "" "θα εμφανιστεί η διαδρομή του." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Στο τερματικό, βάλτε μέσα σε εισαγωγικά μια διαδρομή που περιέχει κενά." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -5443,14 +5443,14 @@ msgstr "" "Αναλύστε έναν φάκελο εφαρμογής JavaScript ή Electron ή ένα αρχείο ASAR χωρίς" " να εκτελέσετε την εφαρμογή:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Στατική ανάλυση εφαρμογής" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5460,13 +5460,13 @@ msgstr "" "Αντικαταστήστε τη διαδρομή με τον κατάλογο της αποσυμπιεσμένης εφαρμογής ή " "το αρχείο {ph0} σας. Στα Windows, μια διαδρομή όπως {ph1} λειτουργεί εδώ." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Πού βρίσκω το αρχείο .asar μιας εφαρμογής;" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -5477,7 +5477,7 @@ msgstr "Οι εφαρμογές Electron συνήθως το διατηρούν #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -5489,7 +5489,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5502,14 +5502,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} συχνά {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -5517,7 +5517,7 @@ msgstr "" "Ορισμένες εφαρμογές εγκαθίστανται αλλού. Αναζητήστε το {ph0} στον φάκελο της" " εφαρμογής." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -5529,36 +5529,36 @@ msgstr "" "μπορεί να επιλύσει επισημαίνονται στο αποτέλεσμα." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Αν θέλετε μια μόνιμη εντολή {ph0} στο κέλυφός σας:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Εγκατάσταση του CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Συνεχίστε με τον οδηγό εφαρμογών JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Συνδέστε την εγκατάσταση Ghidra σας" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Συνδέστε το Ghidra στα Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -5570,7 +5570,7 @@ msgstr "" "Χρησιμοποιήστε μια υπάρχουσα εγκατάσταση Ghidra 12.1.x και JDK 21 ή νεότερο," " 64 bit:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -5588,18 +5588,18 @@ msgstr "" " εντοπίζει μια υπάρχουσα εγκατάσταση ή προτείνει ένα σχέδιο εγκατάστασης για" " έγκρισή σας." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Βρείτε βοήθεια" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Αν ο βοηθός σας δεν βλέπει το REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5609,22 +5609,22 @@ msgstr "" "Επανεκκινήστε ή επανασυνδέστε την εφαρμογή μετά τη ρύθμιση. Ελέγξτε τη " "σύνδεσή της με το REA χρησιμοποιώντας το {ph0}· για το Codex:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Αν ένας πάροχος ανάλυσης χρειάζεται έλεγχο" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Ελέγξτε τον πάροχο ανάλυσης που χρησιμοποιείτε. Για το Ghidra:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Έλεγχος ενός παρόχου ανάλυσης" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -5634,7 +5634,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5647,7 +5647,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5661,7 +5661,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5675,7 +5675,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5741,22 +5741,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Αντιγραφή προτροπής εγκατάστασης του REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Αντιγραφή εντολής ανάλυσης JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Αντιγραφή εντολής εγκατάστασης CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Αντιγραφή εντολής διάγνωσης παρόχου ανάλυσης" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Επιστροφή στην κορυφή" @@ -7255,21 +7255,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1} (macOS ή Linux): η ρύθμιση του REA το εντοπίζει ή προτείνει" -" την εγκατάστασή του. Στο macOS, ανοίξτε το Hopper μία φορά και επιλέξτε " -"λειτουργία επίδειξης ή ενεργοποιήστε άδεια πριν από την πρώτη σας ανάλυση." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7281,7 +7267,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -7295,14 +7281,14 @@ msgstr "Δείτε τη {ph0}ρύθμιση ανάλυσης εγγενούς κ #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ο βοηθός προγραμματισμού σας" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Αντιγραφή" @@ -7313,7 +7299,7 @@ msgstr "Αντιγραφή" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7324,7 +7310,7 @@ msgstr "" "υπολογίζει τη στερεοφωνική θέση του ήχου. Εξηγήστε τον υπολογισμό και δείξτε" " τον κώδικα.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -7336,7 +7322,7 @@ msgstr "" "χρησιμοποιήστε το δικό σας αντίγραφο ή κάντε την ίδιας μορφής ερώτηση για " "ένα άλλο μικρό πρόγραμμα Windows που διαθέτετε." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Πώς βρίσκω την πλήρη διαδρομή ενός αρχείου;" @@ -7345,7 +7331,7 @@ msgstr "Πώς βρίσκω την πλήρη διαδρομή ενός αρχε #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7360,7 +7346,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7372,7 +7358,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7382,22 +7368,22 @@ msgstr "" "{ph0}Σε κάθε σύστημα:{ph1} σύρετε το αρχείο σε ένα παράθυρο τερματικού και " "θα εμφανιστεί η διαδρομή του." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Διαβάστε τα αποτελέσματα του REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Διαβάστε την είσοδο" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Απόσπασμα εντολών του REA · 0x406409" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -7407,18 +7393,18 @@ msgstr "" "κινητής υποδιαστολής. Αυτή είναι η είσοδος που λείπει από την πρώτη προβολή " "ψευδοκώδικα." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Ακολουθήστε την καλούσα συνάρτηση" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "Απόσπασμα καλούσας συνάρτησης του REA · 0x411f5b" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7430,19 +7416,19 @@ msgstr "" "πλακιδίου επί 30· προσθέτοντας {ph0} (20), προκύπτει η θέση του τούβλου στην" " οθόνη." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Διαβάστε τις σταθερές στις οποίες γίνεται αναφορά" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" "Αποτελέσματα bytes του REA · ερμηνεία ως αριθμοί διπλής ακρίβειας little-" "endian" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -7452,17 +7438,17 @@ msgstr "" "bytes παρέχει τις πραγματικές τιμές· η σειρά των εντολών δείχνει στον βοηθό " "πώς χρησιμοποιούνται." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Γράψτε και ελέγξτε την ανακτημένη συνάρτηση" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Ανακτημένος κώδικας C · απόσπασμα" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -7473,7 +7459,7 @@ msgstr "" "500 πριν από τη μετατροπή σε ακέραιο. Ο υπολογισμός τοποθετεί το κέντρο της " "οθόνης στο κέντρο του στερεοφωνικού πεδίου." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -7486,20 +7472,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Δείτε την πλήρη διερεύνηση και τη σύγκριση assembly με C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Δοκιμάστε το με το δικό σας δυαδικό αρχείο" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7513,7 +7499,7 @@ msgstr "" "διερεύνηση. Χρήσιμες αρχικές ερωτήσεις είναι «Πώς αναλύεται αυτό το αρχείο;»" " ή «Ποια συνάρτηση χειρίζεται αυτή την ενέργεια του μενού;»" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -7522,13 +7508,13 @@ msgstr "" "Αν γνωρίζετε ήδη ένα όνομα ή μια διεύθυνση συνάρτησης, μπορείτε να υποβάλετε" " ερώτημα από το τερματικό. Για μια ρυθμισμένη εγκατάσταση Ghidra:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Ανάλυση συνάρτησης" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7543,7 +7529,7 @@ msgstr "" "επαναχρησιμοποιούν το εισαγμένο δυαδικό αρχείο όσο αυτή η συνεδρία παραμένει" " ανοιχτή." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -7578,17 +7564,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Ανάλυση εγγενούς κώδικα" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Αντιγραφή παραδείγματος προτροπής" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Αντιγραφή εντολής ανάλυσης συνάρτησης" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Επιστροφή στην κορυφή" @@ -8787,7 +8773,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ο βοηθός προγραμματισμού σας" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Αντιγραφή" @@ -9257,22 +9243,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Εξετάστε το APK μόνοι σας" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Ρυθμίστε το REA{ph1} σε macOS ή Linux και έπειτα κατεβάστε αυτά τα δύο " -"αρχεία στον ίδιο φάκελο. Στα Windows, χρησιμοποιήστε περιβάλλον Linux όπως " -"το WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9282,28 +9255,28 @@ msgstr "" "Η ανάλυση Android απαιτεί πλήρες JDK 17 ή νεότερο. Επιλέξτε το με το {ph0} ή" " το {ph1}. Αυτή η μελέτη χρησιμοποίησε JDK 21." -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Λήψη APK του Aegis" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Λήψη παρόχου ανάλυσης Android" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Ορίστε στο REA το αρχείο του παρόχου ανάλυσης και εξετάστε το APK:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9316,7 +9289,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9326,7 +9299,7 @@ msgstr "" "Μια επόμενη ερώτηση: {ph0}τι συμβαίνει όταν ένας λογαριασμός χρησιμοποιεί " "οκτώ ψηφία ή διαφορετικό χρονικό διάστημα;{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Πηγές" @@ -9342,7 +9315,7 @@ msgstr "Πηγές" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9356,7 +9329,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -9364,7 +9337,7 @@ msgstr "" "{ph0}Περιπτώσεις αναφοράς RFC 6238{ph1} · {ph2}Δεδομένα δοκιμής της " "επίδειξης{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -9410,12 +9383,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Ακολουθήστε τον υπολογισμό του κωδικού σύνδεσης" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Αντιγραφή εντολής εξέτασης APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Επιστροφή στην κορυφή" @@ -14102,11 +14075,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Μη διαθέσιμο" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Υποστηριζόμενες διανομές" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -14144,17 +14112,7 @@ msgstr "" "όταν ανοίξει για πρώτη φορά το Hopper. Στο Linux, η ρύθμιση προετοιμάζει την" " υποστηριζόμενη έκδοση επίδειξης και το ιδιωτικό περιβάλλον οθόνης της." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Συστήματα Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS " -"ή Omarchy. Το Hopper είναι εμπορικό λογισμικό· η έκδοση επίδειξής του έχει " -"περιορισμούς που ορίζει ο κατασκευαστής." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -14167,18 +14125,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Ρύθμιση Hopper και αντιμετώπιση προβλημάτων{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · εγκαταστήστε το δωρεάν εργαλείο ανάλυσης και τη Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -14188,7 +14146,7 @@ msgstr "" "Επιλέξτε το πακέτο που αντιστοιχεί στην αρχιτεκτονική του υπολογιστή σας." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14201,7 +14159,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14217,7 +14175,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -14225,7 +14183,7 @@ msgstr "{ph0}Λήψη JDK{ph1} · {ph2}Λήψη Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14236,7 +14194,7 @@ msgstr "" " φάκελο για το {ph1}. Η δεύτερη επιβεβαιώνει ότι ο μεταγλωττιστής είναι " "εγκατεστημένος:" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14247,12 +14205,12 @@ msgstr "" "Για μια νέα εγκατάσταση σε macOS ή Linux arm64, μεταγλωττίστε τον " "απομεταγλωττιστή εγγενούς κώδικα για τον υπολογιστή σας πριν συνεχίσετε." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Μεταγλωττίστε τον απομεταγλωττιστή εγγενούς κώδικα του Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14265,7 +14223,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14275,7 +14233,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14285,20 +14243,20 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · οδηγίες μεταγλώττισης εγγενών στοιχείων{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" "Έπειτα ελέγξτε το Ghidra και αποθηκεύστε τις διαδρομές του για την εφαρμογή " "σας:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14316,7 +14274,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14326,12 +14284,12 @@ msgstr "" "{ph0}Ρύθμιση Ghidra και απαιτήσεις αρχιτεκτονικής{ph1} · {ph2}Οδηγός Ghidra " "για Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · συνδέστε μια υπάρχουσα εγκατάσταση" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14343,7 +14301,7 @@ msgstr "" "περιβάλλον με ανοιχτό το αρχικό δυαδικό αρχείο ή έναν επόπτη χωρίς γραφικό " "περιβάλλον που ανοίγει ιδιωτική βάση δεδομένων." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14356,14 +14314,14 @@ msgstr "" "εγγενής γέφυρα γραφικού περιβάλλοντος έχει δικές της απαιτήσεις έκδοσης και " "πλατφόρμας." -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Προετοιμάστε το αρχείο καταχώρισης IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14379,7 +14337,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14392,7 +14350,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14405,7 +14363,7 @@ msgstr "" "αρχείο στο IDA και ξεκινήστε το πρόσθετο του υποκείμενου διακομιστή πριν " "ζητήσετε από το REA να το εξετάσει." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14417,14 +14375,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Προφίλ IDA, παραδείγματα καταχώρισης και επαληθευμένες πλατφόρμες{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14435,18 +14393,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Ακολουθήστε ένα παράδειγμα ανάλυσης εγγενούς κώδικα{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Ιστότοποι, Android, .NET, υλικολογισμικό και άλλοι στόχοι" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14455,120 +14413,120 @@ msgstr "" "Ανοίξτε τον οδηγό για τον στόχο που διαθέτετε. Κάθε οδηγός δίνει τα πρόσθετα" " εργαλεία, τα υποστηριζόμενα συστήματα φιλοξενίας και την πρώτη εντολή." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Στόχος" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Εργαλεία προς προετοιμασία" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Οδηγός" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Ανοίξτε τον οδηγό{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Ιστότοπος" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / πακέτα" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Συσκευή Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Πόροι Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Έργο JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Υλικολογισμικό" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Βάση δεδομένων SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Κώδικας bytecode EVM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / κατάρρευση Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Λογικά σήματα" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Συμπεριφορά διεργασίας" @@ -14584,7 +14542,7 @@ msgstr "Συμπεριφορά διεργασίας" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14594,18 +14552,18 @@ msgstr "" "{ph0}Οδηγός Flutter{ph1} · {ph2}Πακέτα και πόροι{ph3} · {ph4}Κώδικας " "bytecode EVM{ph5} · {ph6}Όλοι οι οδηγοί διερεύνησης{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Χρησιμοποιήστε το REA από τερματικό" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Εντολές CLI, καθολική εγκατάσταση και curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14615,7 +14573,7 @@ msgstr "" "Αντικαταστήστε το {ph0} με ένα όνομα ή μια διεύθυνση συνάρτησης από τη λίστα" " συναρτήσεων του στόχου. Το Ghidra πρέπει να έχει ρυθμιστεί πρώτα." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14625,7 +14583,7 @@ msgstr "" "εγκατάστασης μέσω curl αφού προετοιμάσετε το Node.js:" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14635,7 +14593,7 @@ msgstr "" "Ακολουθήστε τις οδηγίες ρύθμισης για να συνδέσετε τον βοηθό σας. Αν δεν " "εμφανιστούν, εκτελέστε {ph0}." -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14645,12 +14603,12 @@ msgstr "" "ρύθμισής σας. Σε εφαρμογές επεξεργασίας κώδικα, ενεργοποιήστε το REA στις " "ρυθμίσεις MCP και επιτρέψτε τα εργαλεία του στη συνομιλία του βοηθού." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL και διαδρομές με κενά" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14663,17 +14621,17 @@ msgstr "" "Οι πάροχοι ανάλυσης χωρίς γραφικό περιβάλλον εκτελούνται στο ίδιο σύστημα με" " το REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Εντολή ρύθμισης για Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Ενημερώστε ή αφαιρέστε το REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14684,7 +14642,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14698,7 +14656,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14721,18 +14679,18 @@ msgstr "Αντιγραφή εντολής ρύθμισης REA για Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Αντιγραφή εντολής" @@ -15111,7 +15069,7 @@ msgstr "Επιλογές ρύθμισης" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15127,7 +15085,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15139,7 +15097,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15151,7 +15109,7 @@ msgstr "" "προεπιλεγμένη εντολή ρύθμισης είναι {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15170,13 +15128,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -16081,7 +16039,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -16090,6 +16048,101 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1} (macOS ή Linux): η ρύθμιση του REA το εντοπίζει ή προτείνει" +#~ " την εγκατάστασή του. Στο macOS, ανοίξτε το Hopper μία φορά και επιλέξτε " +#~ "λειτουργία επίδειξης ή ενεργοποιήστε άδεια πριν από την πρώτη σας ανάλυση." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Ρυθμίστε το REA{ph1} σε macOS ή Linux και έπειτα κατεβάστε αυτά τα δύο " +#~ "αρχεία στον ίδιο φάκελο. Στα Windows, χρησιμοποιήστε περιβάλλον Linux όπως " +#~ "το WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Υποστηριζόμενες διανομές" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Συστήματα Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS " +#~ "ή Omarchy. Το Hopper είναι εμπορικό λογισμικό· η έκδοση επίδειξής του έχει " +#~ "περιορισμούς που ορίζει ο κατασκευαστής." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/es/messages.po b/website/locales/es/messages.po index 3f6300cdf5..fade1aec29 100644 --- a/website/locales/es/messages.po +++ b/website/locales/es/messages.po @@ -72,13 +72,13 @@ msgstr "Por N0zoM1z0 * 10 octubre 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3323,21 +3323,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Copiar" @@ -3569,14 +3569,14 @@ msgstr "" " aparecerá su ruta." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "En una terminal, coloque comillas alrededor de una ruta que contenga " "espacios." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3585,14 +3585,14 @@ msgstr "" "Analice una carpeta de aplicación JavaScript o Electron o un archivo ASAR " "sin ejecutar la aplicación:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Análisis estático de aplicaciones" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3602,13 +3602,13 @@ msgstr "" "Reemplace la ruta con el directorio de la aplicación extraída o {ph0} " "archivo. En Windows, una ruta como {ph1} trabaja aquí." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "¿Dónde encuentro el archivo .asar de una aplicación?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3620,7 +3620,7 @@ msgstr "" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3632,7 +3632,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3645,14 +3645,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} a menudo {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3660,7 +3660,7 @@ msgstr "" "Algunas aplicaciones se instalan en otro lugar. Busque en la carpeta de la " "aplicación {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3672,36 +3672,36 @@ msgstr "" "se pueden resolver están marcadas en el resultado." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Si desea un permanente {ph0} comando en tu shell:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Instale el CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Continúe con la guía de aplicación del JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Conecte su instalación de Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Conecte Ghidra en Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3712,7 +3712,7 @@ msgstr "" "DLL nativas x86 y x64 PE en unidades NTFS locales fijas. Usa una instalación" " existente de Ghidra 12.1.x y un JDK de 64 bits, versión 21 o posterior:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3730,18 +3730,18 @@ msgstr "" "anterior; detecta una instalación existente u ofrece un plan de instalación " "para su aprobación." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Cómo obtener ayuda" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Si su agente no ve REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3751,22 +3751,22 @@ msgstr "" "Reinicie o vuelva a conectar el cliente después de la configuración. " "Compruebe su conexión REA con {ph0}; para Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Si un proveedor necesita atención" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Verifique el proveedor que está usando. Para Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Consulte a un proveedor" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3776,7 +3776,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3789,7 +3789,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3803,7 +3803,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3817,7 +3817,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3883,22 +3883,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Copie el mensaje de instalación de REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Copie el comando de análisis JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Copie el comando de instalación de CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Copie el comando de diagnóstico del proveedor" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Volver arriba" @@ -5264,21 +5264,7 @@ msgstr "REA necesita uno de estos para leer programas nativos:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} ( macOS o Linux): La configuración de REA lo detecta u " -"ofrece instalarlo. En macOS , abra Hopper una vez y elija el modo de " -"demostración o active una licencia antes de su primer análisis." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5291,7 +5277,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5307,14 +5293,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Su agente de codificación" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Copiar" @@ -5325,7 +5311,7 @@ msgstr "Copiar" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5336,7 +5322,7 @@ msgstr "" "posicionamiento del sonido estéreo. Explique el cálculo y muestre el " "código.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5347,7 +5333,7 @@ msgstr "" "DX-Ball 1.07. REA no incluye DX-Ball, así que use su propia copia o haga el" " mismo tipo de pregunta sobre otro pequeño programa Windows que tenga." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "¿Cómo obtengo la ruta completa de un archivo?" @@ -5356,7 +5342,7 @@ msgstr "¿Cómo obtengo la ruta completa de un archivo?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5371,7 +5357,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5384,7 +5370,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5394,17 +5380,17 @@ msgstr "" "{ph0}Cualquier sistema:{ph1} arrastre el archivo a una ventana de terminal y" " aparecerá su ruta." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Lea los resultados del REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Lea la entrada" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5414,13 +5400,13 @@ msgstr "" " de coma flotante. Esta es la entrada ausente en la primera vista de " "pseudocódigo." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Siga a la persona que llama" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5431,12 +5417,12 @@ msgstr "" "instrucciones anteriores multiplican la coordenada del mosaico por 30; " "sumando {ph0} (20) da la posición de la pantalla del ladrillo." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Lee las constantes a las que se hace referencia" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5446,12 +5432,12 @@ msgstr "" "proporciona los valores reales; el orden de instrucción le dice al agente " "cómo se usan." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Escriba y verifique la función recuperada" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5462,7 +5448,7 @@ msgstr "" "-500, 0 y 500 antes de la conversión de enteros. El cálculo coloca el centro" " de la pantalla en el centro del campo estéreo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5475,21 +5461,21 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" "{ph0}Vea la investigación completa y la comparación de assembly-to-C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Pruébalo con tu propio binario" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5503,7 +5489,7 @@ msgstr "" " para investigar . Las preguntas iniciales útiles incluyen \"¿ Cómo se " "analiza este archivo?\"o\" ¿Qué función maneja esta acción de menú?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5512,13 +5498,13 @@ msgstr "" "Si ya conoce el nombre o la dirección de una función, puede consultarla " "desde el terminal. Para una instalación Ghidra configurada:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Análisis de funciones" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5532,7 +5518,7 @@ msgstr "" " sesión. Las consultas MCP de su agente reutilizan el binario importado " "mientras esa sesión está abierta." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5566,17 +5552,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Análisis de programas de código máquina" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Copie el mensaje de ejemplo" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Análisis de la función de copia" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Volver arriba" @@ -6563,7 +6549,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Su agente de codificación" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Copiar" @@ -6979,21 +6965,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Inspeccione el APK usted mismo" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Configurar REA {ph1} en macOS o Linux , descargue estos dos archivos en" -" la misma carpeta. En Windows, use un entorno Linux como WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7003,18 +6977,18 @@ msgstr "" "El análisis de Android necesita un JDK 17 completo o posterior. Selecciónelo" " con {ph0} o {ph1}. Este caso usó JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Apunte REA al archivo del proveedor e inspeccione el APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7027,7 +7001,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7037,7 +7011,7 @@ msgstr "" "Una pregunta siguiente: {ph0}¿qué sucede cuando una cuenta usa ocho dígitos " "o un intervalo de tiempo diferente?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Fuentes" @@ -7053,7 +7027,7 @@ msgstr "Fuentes" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7067,7 +7041,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -7075,7 +7049,7 @@ msgstr "" "{ph0}Casos de referencia RFC 6238{ph1} · {ph2}Datos de prueba de " "demostración{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7121,12 +7095,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Siga el cálculo del código de inicio de sesión" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Copie el comando de inspección APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Volver arriba" @@ -10957,13 +10931,13 @@ msgstr "Guías" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10980,13 +10954,13 @@ msgstr "Código fuente" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -11003,13 +10977,13 @@ msgstr "Licencia MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -11026,13 +11000,13 @@ msgstr "Únete a la comunidad" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11117,22 +11091,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA resultado · resumen.extracto de ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Extracto de instrucciones REA * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "Extracto de la persona que llama REA * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Resultados de bytes REA * interpretados como dobles little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Extracto recuperado de C *" @@ -11193,12 +11167,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, cuerpo completo" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Descargar Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Descargar proveedor de análisis de Android" @@ -13903,11 +13877,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "No disponible" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Distribuciones compatibles" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13945,17 +13914,7 @@ msgstr "" "cuando Hopper se abra por primera vez. En Linux, la configuración prepara la" " demostración compatible y su pantalla privada." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Hosts Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS u " -"Omarchy. Hopper es software comercial; su demostración tiene límites " -"definidos por el fabricante." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13969,18 +13928,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Configuración de Hopper y solución de problemas{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · instala la herramienta de análisis gratuita y Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13991,7 +13950,7 @@ msgstr "" "ordenador." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14004,7 +13963,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14020,7 +13979,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -14028,7 +13987,7 @@ msgstr "{ph0}Descargar un JDK{ph1} · {ph2}Descargar Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14038,7 +13997,7 @@ msgstr "" "Comprueba Java. El primer comando muestra {ph0}; usa esa carpeta para {ph1}." " El segundo confirma que el compilador está instalado:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14056,7 +14015,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14066,12 +14025,12 @@ msgstr "" "{ph0}Configuración de Ghidra y requisitos de arquitectura{ph1} · {ph2}Guía " "de Ghidra en Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · conecta una instalación existente" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14083,7 +14042,7 @@ msgstr "" "original abierto o un supervisor sin interfaz gráfica que abra una base de " "datos privada." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14096,7 +14055,7 @@ msgstr "" "opcional de la interfaz gráfica tiene sus propios requisitos de versión y " "plataforma." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14108,14 +14067,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Perfiles de IDA, ejemplos de registro y plataformas verificadas{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14126,18 +14085,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Sigue un ejemplo de análisis nativo{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Sitios web, Android, .NET, firmware y otros objetivos" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14146,110 +14105,110 @@ msgstr "" "Abre la guía del tipo de objetivo que tienes. Cada guía indica las " "herramientas adicionales, los hosts compatibles y el primer comando." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Objetivo" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Herramientas necesarias" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Guía" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Abrir guía{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Sitio web" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / paquetes" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "APK de Android" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Dispositivo Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Recursos de Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Proyecto JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / fallo de Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Señales lógicas" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Comportamiento de procesos" @@ -14265,7 +14224,7 @@ msgstr "Comportamiento de procesos" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14275,18 +14234,18 @@ msgstr "" "{ph0}Guía de Flutter{ph1} · {ph2}Paquetes y recursos{ph3} · {ph4}Bytecode de" " EVM{ph5} · {ph6}Todas las guías de investigación{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Usa REA desde una terminal" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Comandos CLI, instalación global y curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14296,7 +14255,7 @@ msgstr "" "Sustituye {ph0} por un nombre o una dirección de función de la lista de " "funciones del objetivo. Ghidra debe estar configurado previamente." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14305,7 +14264,7 @@ msgstr "" "En macOS o Linux también puedes usar el instalador curl después de preparar " "Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14315,12 +14274,12 @@ msgstr "" "configuración. En los clientes que son editores, activa REA en los ajustes " "MCP y autoriza sus herramientas en el chat del agente." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL y rutas con espacios" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14332,17 +14291,17 @@ msgstr "" " entorno Linux y usa allí rutas de Linux. Los proveedores sin interfaz " "gráfica se ejecutan en el mismo host que REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Comando de configuración para Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Actualiza o elimina REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14353,7 +14312,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14367,7 +14326,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14390,18 +14349,18 @@ msgstr "Copiar el comando de configuración de REA para Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Copiar comando" @@ -14457,14 +14416,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · elige un modelo de IA" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Prepara el archivo de registro de IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14480,7 +14439,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14493,7 +14452,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14506,12 +14465,12 @@ msgstr "" " binario original en IDA e inicia el complemento de ese servidor antes de " "pedir a REA que lo inspeccione." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Base de datos SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode de EVM" @@ -14570,7 +14529,7 @@ msgstr "" "Revisa y aprueba el plan, vuelve a abrir Claude y {ph0}comprueba la conexión" " en el paso 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14581,12 +14540,12 @@ msgstr "" "instalación nueva de macOS o Linux arm64, compila el descompilador nativo " "para tu ordenador antes de continuar." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Compila el descompilador nativo de Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14599,7 +14558,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14609,7 +14568,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14619,13 +14578,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · instrucciones de compilación nativa{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Después, comprueba Ghidra y guarda sus rutas para tu cliente:" @@ -15029,7 +14988,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Otras formas de instalación: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15044,7 +15003,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15059,7 +15018,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15071,7 +15030,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15082,7 +15041,7 @@ msgstr "" "instalador. En Linux, el comando de configuración predeterminado es {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15101,13 +15060,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -16006,7 +15965,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -16015,6 +15974,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} ( macOS o Linux): La configuración de REA lo detecta u " +#~ "ofrece instalarlo. En macOS , abra Hopper una vez y elija el modo de " +#~ "demostración o active una licencia antes de su primer análisis." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Configurar REA {ph1} en macOS o Linux , descargue estos dos archivos en" +#~ " la misma carpeta. En Windows, use un entorno Linux como WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Distribuciones compatibles" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Hosts Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS u " +#~ "Omarchy. Hopper es software comercial; su demostración tiene límites " +#~ "definidos por el fabricante." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/fa/messages.po b/website/locales/fa/messages.po index 8560c939e3..775705e877 100644 --- a/website/locales/fa/messages.po +++ b/website/locales/fa/messages.po @@ -71,13 +71,13 @@ msgstr "توسط N0zoM1z0 * 10 اکتبر 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3223,21 +3223,21 @@ msgstr "ترمینال" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "کپی" @@ -3468,13 +3468,13 @@ msgstr "" "{ph0}هر سیستم:{ph1} پوشه را به یک پنجره ترمینال بکشید و مسیر آن ظاهر می شود." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "در یک ترمینال ، نقل قول ها را در اطراف مسیری قرار دهید که شامل فضاها است." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3483,14 +3483,14 @@ msgstr "" "یک پوشه برنامه JavaScript یا Electron یا آرشیو ASAR را بدون اجرای برنامه " "تجزیه و تحلیل کنید:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "تجزیه و تحلیل کاربرد استاتیک" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3500,13 +3500,13 @@ msgstr "" "مسیر را با دایرکتوری برنامه استخراج شده خود جایگزین کنید یا {ph0} فایل. در " "Windows ، مسیری مانند {ph1} اینجا کار میکنه" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "فایل .asar برنامه را کجا پیدا کنم؟" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3517,7 +3517,7 @@ msgstr "برنامه های Electron معمولا آن را در یک {ph0} پو #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3529,7 +3529,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3542,21 +3542,21 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} اغلب {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" "برخی از برنامه ها در جای دیگری نصب می شوند. پوشه برنامه را جستجو کنید {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3567,36 +3567,36 @@ msgstr "" "منبع آنها را می دهد. اتصالات REA نمی تواند حل شود در نتیجه مشخص شده است." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "اگر می‌خواهید فرمان {ph0} همیشه در پوستهٔ شما در دسترس باشد:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "CLI را نصب کنید" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}ادامه با راهنمای برنامه JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "نصب Ghidra خود را وصل کنید" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "اتصال Ghidra در Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3607,7 +3607,7 @@ msgstr "" "وx64 با قالب PE روی درایوهای ثابت محلی NTFS دارد. از نصب موجود Ghidra 12.1.x" " وJDK ‏64 بیتی نسخهٔ 21 یا جدیدتر استفاده کنید:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3625,18 +3625,18 @@ msgstr "" "این دستور یک نصب موجود را تشخیص می دهد یا یک برنامه نصب برای تأیید شما ارائه" " می دهد." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "کمک گرفتن" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "اگر نماینده شما REA را نمی بیند" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3646,22 +3646,22 @@ msgstr "" "پس از راه‌اندازی، برنامهٔ دستیار را دوباره اجرا یا متصل کنید. اتصال REA آن " "را با {ph0} بررسی کنید؛ برای Codex:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "اگر یک ارائه دهنده نیاز به توجه داشته باشد" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "ارائه دهنده ای که استفاده می کنید را بررسی کنید. برای Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "یک ارائه دهنده را بررسی کنید" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "گزارش چک شکست خورده و گام بعدی پیشنهاد شده را ارائه می دهد." @@ -3670,7 +3670,7 @@ msgstr "گزارش چک شکست خورده و گام بعدی پیشنهاد ش #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3683,7 +3683,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3697,7 +3697,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3709,7 +3709,7 @@ msgstr "{ph0} مسائل GitHub{ph1}{ph2}گزارش یک مشکل با دستو #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3774,22 +3774,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "کپی REA نصب و راه اندازی prompt" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "دستور تجزیه و تحلیل JavaScript را کپی کنید" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "دستور نصب CLI را کپی کنید" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "کپی فرمان تشخیص ابزار تحلیل" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "بازگشت به بالا" @@ -5132,21 +5132,7 @@ msgstr "REA برای خواندن برنامه های بومی به یکی از #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS یا Linux ): راه اندازی REA آن را تشخیص می دهد یا " -"پیشنهاد نصب آن را می دهد. در macOS ، Hopper را یک بار باز کنید و حالت نمایشی" -" را انتخاب کنید یا مجوز را قبل از اولین تجزیه و تحلیل خود فعال کنید." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5158,7 +5144,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5173,14 +5159,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}عامل برنامه نویسی شما" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "کپی" @@ -5191,7 +5177,7 @@ msgstr "کپی" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5201,7 +5187,7 @@ msgstr "" "{ph0}›{ph1}{ph2}استفاده از REA برای پیدا کردن چگونه DX-Ball محاسبه موقعیت " "صدا استریو. محاسبه را توضیح دهید و کد را نشان دهید.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5212,7 +5198,7 @@ msgstr "" "REA شامل DX-Ball نیست ، بنابراین از نسخه خود استفاده کنید ، یا همان نوع سوال" " را در مورد یک برنامه کوچک Windows که دارید بپرسید." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "چطور می توانم مسیر کامل یک فایل را بدست آورم ؟" @@ -5221,7 +5207,7 @@ msgstr "چطور می توانم مسیر کامل یک فایل را بدست #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5236,7 +5222,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5248,7 +5234,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5257,17 +5243,17 @@ msgid "" msgstr "" "{ph0}هر سیستم:{ph1} فایل را به یک پنجره ترمینال بکشید و مسیر آن ظاهر می شود." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "نتایج REA را بخوانید" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "ورودی را بخوانید" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5276,13 +5262,13 @@ msgstr "" "دستورالعمل ها یک استدلال عدد صحیح را بارگذاری می کنند و آن را برای حساب نقطه" " شناور تبدیل می کنند. این ورودی غایب از اولین نمای شبه کد است." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "تماس گیرنده را دنبال کنید" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5293,12 +5279,12 @@ msgstr "" "کاشی را با 30 ضرب می کند ؛ اضافه کردن {ph0} (20) موقعیت صفحه نمایش آجر را می" " دهد." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "ثابت های مرجع را بخوانید" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5307,12 +5293,12 @@ msgstr "" "حساب ها به این آدرس ها اشاره می کنند. خواندن بایت ها ارزش های واقعی را فراهم" " می کند ؛ دستور دستورالعمل به عامل می گوید که چگونه از آنها استفاده می شود." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "عملکرد بازیابی شده را بنویسید و بررسی کنید" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5322,7 +5308,7 @@ msgstr "" "با مقیاس 1.0 ، موقعیت صفحه نمایش 0 ، 320 و 640 نقشه به -500 ، 0 و 500 قبل از" " تبدیل عدد صحیح. محاسبه مرکز صفحه را در مرکز میدان استریو قرار می دهد." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5335,20 +5321,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}بررسی کامل و مقایسه مونتاژ به C را ببینید{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "آن را با باینری خود امتحان کنید" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5362,7 +5348,7 @@ msgstr "" "سوالات مفید شروع شامل \" این فایل چگونه تجزیه می شود ؟ \"یا\" کدام تابع این " "عمل منو را مدیریت می کند ؟ ”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5371,13 +5357,13 @@ msgstr "" "اگر قبلا یک نام یا آدرس تابع را می دانید ، می توانید آن را از ترمینال پرس و " "جو کنید. برای نصب Ghidra پیکربندی شده:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "تجزیه و تحلیل عملکرد" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5391,7 +5377,7 @@ msgstr "" "جوهای MCP نماینده شما در حالی که جلسه باز است ، از باینری وارد شده استفاده " "می کند." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5424,17 +5410,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "تجزیه و تحلیل برنامه های کد ماشین" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "کپی مثال prompt" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "تجزیه و تحلیل تابع کپی" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "بازگشت به بالا" @@ -6409,7 +6395,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}عامل برنامه نویسی شما" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "کپی" @@ -6817,21 +6803,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "APK را خودتان بازرسی کنید" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}راه اندازی REA {ph1} در macOS یا Linux ، سپس این دو فایل را در یک پوشه " -"دانلود کنید. در Windows ، از یک محیط Linux مانند WSL استفاده کنید." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6841,18 +6815,18 @@ msgstr "" "Analysis Android به JDK 17 کامل یا بالاتر نیاز دارد. انتخاب کنید با {ph0} یا" " {ph1}. این مورد از JDK 21 استفاده کرد." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "نقطه REA در فایل ارائه دهنده و بازرسی APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6864,7 +6838,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6874,7 +6848,7 @@ msgstr "" "سوال بعدی: {ph0}چه اتفاقی می افتد وقتی یک حساب کاربری از هشت رقم یا یک فاصله" " زمانی متفاوت استفاده می کند ؟ {ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "منابع" @@ -6890,7 +6864,7 @@ msgstr "منابع" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6904,13 +6878,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}پرونده های مرجع rfc 6238{ph1} · {ph2}داده های تست دمو{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6955,12 +6929,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "محاسبه کد ورود را دنبال کنید" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "کپی APK دستور بازرسی" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "بازگشت به بالا" @@ -10704,13 +10678,13 @@ msgstr "راهنماها" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10727,13 +10701,13 @@ msgstr "کد منبع" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10750,13 +10724,13 @@ msgstr "مجوز MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10773,13 +10747,13 @@ msgstr "به جامعه بپیوندید" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10864,22 +10838,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA نتیجه * خلاصه.گزیده ای از ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA گزیده دستورالعمل * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA گزیده تماس گیرنده * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "نتایج بایت REA * به عنوان دو برابر اندک تفسیر می شود" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "بازیابی C * گزیده" @@ -10940,12 +10914,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP ، بدن کامل" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "دانلود Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "دانلود analysis provider اندروید" @@ -13572,11 +13546,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "در دسترس نیست" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "توزیع‌های پشتیبانی‌شده" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13614,17 +13583,7 @@ msgstr "" "فعال کنید. در Linux، راه‌اندازی نسخهٔ آزمایشی پشتیبانی‌شده و نمایشگر اختصاصی" " آن را آماده می‌کند." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"میزبان‌های Linux: ‏Ubuntu 24.04+، Fedora 41+، Nobara 44+، Arch Linux، " -"CachyOS یا Omarchy. Hopper نرم‌افزار تجاری است؛ نسخهٔ آزمایشی محدودیت‌هایی " -"دارد که فروشنده تعیین می‌کند." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13638,18 +13597,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}راه‌اندازی Hopper و رفع اشکال{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · ابزار تحلیل رایگان و Java را نصب کنید" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13659,7 +13618,7 @@ msgstr "" " انتخاب کنید که با معماری رایانه‌تان مطابقت دارد." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13672,7 +13631,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13688,7 +13647,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13696,7 +13655,7 @@ msgstr "{ph0}دانلود JDK{ph1} · {ph2}دانلود Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13706,7 +13665,7 @@ msgstr "" "Java را بررسی کنید. فرمان اول {ph0} را چاپ می‌کند؛ از آن پوشه برای {ph1} " "استفاده کنید. فرمان دوم وجود کامپایلر را تأیید می‌کند:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13724,7 +13683,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13734,12 +13693,12 @@ msgstr "" "{ph0}راه‌اندازی Ghidra و نیازهای معماری{ph1} · {ph2}راهنمای Ghidra در " "Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · نصب موجود را متصل کنید" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13750,7 +13709,7 @@ msgstr "" "رابط گرافیکی متصل با فایل باینری اصلی باز، یا یک ناظر بدون رابط گرافیکی که " "پایگاه دادهٔ اختصاصی باز می‌کند انتخاب کنید." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13761,7 +13720,7 @@ msgstr "" "Windows، و ناظر بدون رابط گرافیکی macOS arm64 با IDA Professional 9.1 هستند." " پل اختیاری رابط گرافیکی بومی، نیازهای نسخه و پلتفرم خودش را دارد." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13773,13 +13732,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}پروفایل‌های IDA، نمونه‌های ثبت و پلتفرم‌های بررسی‌شده{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13790,18 +13749,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}یک نمونهٔ تحلیل بومی را دنبال کنید{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "وب‌سایت‌ها، Android، ‏.NET، میان‌افزار و هدف‌های دیگر" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13810,110 +13769,110 @@ msgstr "" "راهنمای هدف خود را باز کنید. هر راهنما ابزارهای اضافی، میزبان‌های " "پشتیبانی‌شده و فرمان اول را مشخص می‌کند." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "هدف" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "ابزارهای لازم برای آماده‌سازی" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "راهنما" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}باز کردن راهنما{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "وب‌سایت" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / بسته‌ها" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "دستگاه Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "منابع Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "پروژهٔ JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "میان‌افزار" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / از کار افتادن Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "سیگنال‌های منطقی" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "رفتار فرایند" @@ -13929,7 +13888,7 @@ msgstr "رفتار فرایند" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13939,18 +13898,18 @@ msgstr "" "{ph0}راهنمای Flutter{ph1} · {ph2}بسته‌ها و منابع{ph3} · {ph4}بایت‌کد " "EVM{ph5} · {ph6}همهٔ راهنماهای بررسی{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "REA را از ترمینال استفاده کنید" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "فرمان‌های CLI، نصب سراسری و curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13960,7 +13919,7 @@ msgstr "" "به‌جای {ph0} نام یا نشانی تابع را از فهرست توابع هدف قرار دهید. Ghidra باید " "از قبل پیکربندی شده باشد." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -13969,7 +13928,7 @@ msgstr "" "در macOS یا Linux می‌توانید پس از آماده‌سازی Node.js از نصب‌کنندهٔ curl هم " "استفاده کنید:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -13979,12 +13938,12 @@ msgstr "" "ویرایشگرها، REA را در تنظیمات MCP فعال کنید و در گفت‌وگوی دستیار به " "ابزارهایش اجازه بدهید." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows، ‏WSL و مسیرهای دارای فاصله" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -13996,17 +13955,17 @@ msgstr "" "و از مسیرهای Linux استفاده کنید. ارائه‌دهنده‌های بدون رابط گرافیکی روی همان " "میزبان REA اجرا می‌شوند." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "فرمان راه‌اندازی Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "به‌روزرسانی یا حذف REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14017,7 +13976,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14031,7 +13990,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14054,18 +14013,18 @@ msgstr "کپی فرمان راه‌اندازی REA در Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "کپی فرمان" @@ -14122,14 +14081,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · یک مدل هوش مصنوعی انتخاب کنید" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "فایل ثبت IDA را آماده کنید" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14144,7 +14103,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14157,7 +14116,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14169,12 +14128,12 @@ msgstr "" "و در صورت وجود {ph2} را در فایل کپی کنید. فایل باینری اصلی را در IDA باز " "کنید و پیش از درخواست بررسی از REA، افزونهٔ بالادستی را اجرا کنید." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "پایگاه دادهٔ SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "بایت‌کد EVM" @@ -14233,7 +14192,7 @@ msgstr "" "طرح را بررسی و تأیید کنید، Claude را دوباره باز کنید و {ph0}اتصال را در " "مرحلهٔ 3 بررسی کنید{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14243,12 +14202,12 @@ msgstr "" "هر دو مسیر را با مسیر نصب‌های موجودتان جایگزین کنید. برای نصب تازه در macOS " "یا Linux arm64، پیش از ادامه دیکامپایلر بومی را برای رایانه‌تان بسازید." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "دیکامپایلر بومی Ghidra را بسازید" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14261,7 +14220,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14271,7 +14230,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14281,13 +14240,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · راهنمای ساخت بومی{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" @@ -14682,7 +14641,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "روش‌های دیگر نصب: {ph0}{ph1} ←{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14697,7 +14656,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14712,7 +14671,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14724,7 +14683,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14735,7 +14694,7 @@ msgstr "" "استفاده کنید. در Linux، فرمان پیش‌فرض راه‌اندازی {ph1} است." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14753,13 +14712,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15654,7 +15613,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15663,6 +15622,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS یا Linux ): راه اندازی REA آن را تشخیص می دهد یا " +#~ "پیشنهاد نصب آن را می دهد. در macOS ، Hopper را یک بار باز کنید و حالت نمایشی" +#~ " را انتخاب کنید یا مجوز را قبل از اولین تجزیه و تحلیل خود فعال کنید." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}راه اندازی REA {ph1} در macOS یا Linux ، سپس این دو فایل را در یک پوشه " +#~ "دانلود کنید. در Windows ، از یک محیط Linux مانند WSL استفاده کنید." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "توزیع‌های پشتیبانی‌شده" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "میزبان‌های Linux: ‏Ubuntu 24.04+، Fedora 41+، Nobara 44+، Arch Linux، " +#~ "CachyOS یا Omarchy. Hopper نرم‌افزار تجاری است؛ نسخهٔ آزمایشی محدودیت‌هایی " +#~ "دارد که فروشنده تعیین می‌کند." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/fr/messages.po b/website/locales/fr/messages.po index ed8ae4e606..7e814b61b7 100644 --- a/website/locales/fr/messages.po +++ b/website/locales/fr/messages.po @@ -72,13 +72,13 @@ msgstr "Par N0zoM1z0 · 10 octobre 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3322,21 +3322,21 @@ msgstr "Borne" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Copier" @@ -3571,14 +3571,14 @@ msgstr "" "terminal et son chemin apparaît." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Dans un terminal, mettez des guillemets autour d'un chemin qui contient des " "espaces." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3587,14 +3587,14 @@ msgstr "" "Analysez un dossier d'application JavaScript ou Electron ou une archive ASAR" " sans exécuter l'application:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Analyse statique des applications" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3604,13 +3604,13 @@ msgstr "" "Remplacez le chemin par le répertoire de votre application extraite ou {ph0}" " dossier. Sur Windows, un chemin tel que {ph1} travaille ici." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Où trouver le fichier .asar d’une application ?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3622,7 +3622,7 @@ msgstr "" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3634,7 +3634,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3647,14 +3647,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} souvent {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3662,7 +3662,7 @@ msgstr "" "Certaines applications s'installent ailleurs. Recherchez le dossier de " "l'application pour {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3674,36 +3674,36 @@ msgstr "" "résoudre sont marquées dans le résultat." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Si vous souhaitez un permanent {ph0} commande dans votre shell:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Installez le CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Continuez avec le guide d'application JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Connectez votre installation de Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Connectez le modèle Ghidra sur le modèle Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3715,7 +3715,7 @@ msgstr "" "locaux fixes. Utilisez une installation existante de Ghidra 12.1.x et un JDK" " 64 bits, version 21 ou ultérieure :" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3733,18 +3733,18 @@ msgstr "" "dessus; il détecte une installation existante ou propose un plan " "d'installation pour votre approbation." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Obtenir de l'aide" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Si votre agent ne voit pas REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3754,22 +3754,22 @@ msgstr "" "Redémarrez ou reconnectez le client après l'installation. Vérifiez sa " "connexion REA avec {ph0}; pour le modèle Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Si un fournisseur a besoin d'attention" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Vérifiez le fournisseur que vous utilisez. Pour Ghidra:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Vérifiez un fournisseur" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3780,7 +3780,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3793,7 +3793,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3807,7 +3807,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3821,7 +3821,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3887,22 +3887,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Copier l'invite d'installation de REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Copier la commande d'analyse JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Copier la commande d'installation de CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Commande de diagnostic du fournisseur de copie" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Haut de page" @@ -5269,21 +5269,7 @@ msgstr "REA a besoin de l'un d'entre eux pour lire des programmes natifs:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1} (macOS ou Linux): REAle programme d'installation le détecte" -" ou propose de l'installer. Sur macOS, ouvert Hopper une fois et choisissez " -"le mode démo ou activez une licence avant votre première analyse." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5296,7 +5282,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5312,14 +5298,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Votre agent de codage" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Copier" @@ -5330,7 +5316,7 @@ msgstr "Copier" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5341,7 +5327,7 @@ msgstr "" "positionnement du son stéréo. Expliquez le calcul et montrez le " "code.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5353,7 +5339,7 @@ msgstr "" "copie ou posez le même genre de question à propos d'un autre petit programme" " Windows que vous avez." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Comment puis-je obtenir le chemin complet d'un fichier?" @@ -5362,7 +5348,7 @@ msgstr "Comment puis-je obtenir le chemin complet d'un fichier?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5378,7 +5364,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5391,7 +5377,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5401,17 +5387,17 @@ msgstr "" "{ph0}Tout système:{ph1} faites glisser le fichier dans une fenêtre de " "terminal et son chemin apparaît." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Lire les résultats du REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Lire l'entrée" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5421,13 +5407,13 @@ msgstr "" "arithmétique à virgule flottante. C'est l'entrée absente de la première vue " "pseudocode." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Suivez l'appelant" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5438,12 +5424,12 @@ msgstr "" "multiplient la coordonnée de la tuile par 30; en ajoutant {ph0} (20) donne " "la position de l'écran de la brique." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Lire les constantes référencées" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5453,12 +5439,12 @@ msgstr "" "les valeurs réelles; l'ordre des instructions indique à l'agent comment " "elles sont utilisées." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Écrivez et vérifiez la fonction récupérée" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5469,7 +5455,7 @@ msgstr "" " -500, 0 et 500 avant la conversion d'entiers. Le calcul place le centre de " "l'écran au centre du champ stéréo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5482,20 +5468,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Voir l'enquête complète et la comparaison assemblage-C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Essayez-le avec votre propre binaire" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5509,7 +5495,7 @@ msgstr "" "fonctionnalité à étudier. Les questions de départ utiles incluent “Comment " "ce fichier est-il analysé?“ou \" Quelle fonction gère cette action de menu?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5518,13 +5504,13 @@ msgstr "" "Si vous connaissez déjà un nom ou une adresse de fonction, vous pouvez " "l'interroger à partir du terminal. Pour une installation Ghidra configurée:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Analyse de fonction" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5538,7 +5524,7 @@ msgstr "" "session. Les requêtes MCP de votre agent réutilisent le binaire importé " "pendant que cette session est ouverte." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5572,17 +5558,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Analyse de programmes en code machine" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Copier l'exemple d'invite" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Analyse de la fonction de copie" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Haut de page" @@ -6571,7 +6557,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Votre agent de codage" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Copier" @@ -6986,22 +6972,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Inspectez vous-même le APK" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Mettre en place REA{ph1} sur macOS ou Linux, puis téléchargez ces deux " -"fichiers dans le même dossier. Sur Windows, utilisez un Linux environnement " -"tel que WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7011,18 +6984,18 @@ msgstr "" "L'analyse Android nécessite un JDK 17 complet ou une version ultérieure. " "Sélectionnez-le avec {ph0} ou {ph1}. Cette affaire utilisait JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Pointez REA sur le fichier fournisseur et inspectez le APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "Modèle: macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7035,7 +7008,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7045,7 +7018,7 @@ msgstr "" "Une question suivante: {ph0}que se passe-t-il lorsqu'un compte utilise huit " "chiffres ou un intervalle de temps différent?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Sources" @@ -7061,7 +7034,7 @@ msgstr "Sources" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7075,7 +7048,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -7083,7 +7056,7 @@ msgstr "" "{ph0}Cas de référence RFC 6238{ph1} · {ph2}Données de test de " "démonstration{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7129,12 +7102,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Suivez le calcul du code de connexion" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Copie de la commande d'inspection APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Haut de page" @@ -10966,13 +10939,13 @@ msgstr "Guides" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10989,13 +10962,13 @@ msgstr "Code source" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -11012,13 +10985,13 @@ msgstr "Licence MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -11035,13 +11008,13 @@ msgstr "Rejoindre la communauté" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11126,22 +11099,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "Résultat de REA * résumé.extrait de la CIB" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Extrait d'instruction REA · 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "Extrait de l'appelant REA · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Résultats d'octets REA * interprétés comme des doubles little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Extrait C · récupéré" @@ -11202,12 +11175,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * Générez OTP, corps complet" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Télécharger Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Télécharger le fournisseur d'analyse Android" @@ -13938,11 +13911,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Non disponible" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Distributions prises en charge" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13980,17 +13948,7 @@ msgstr "" " licence à la première ouverture de Hopper. Sur Linux, la configuration " "prépare la démo prise en charge et son affichage privé." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Hôtes Linux : Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS ou " -"Omarchy. Hopper est un logiciel commercial ; sa démo a des limites définies " -"par l’éditeur." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -14004,18 +13962,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Configuration de Hopper et dépannage{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · installer l’outil d’analyse gratuit et Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -14026,7 +13984,7 @@ msgstr "" "votre ordinateur." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14040,7 +13998,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14056,7 +14014,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -14064,7 +14022,7 @@ msgstr "{ph0}Télécharger un JDK{ph1} · {ph2}Télécharger Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14074,7 +14032,7 @@ msgstr "" "Vérifiez Java. La première commande affiche {ph0} ; utilisez ce dossier pour" " {ph1}. La seconde confirme que le compilateur est installé :" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14092,7 +14050,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14102,12 +14060,12 @@ msgstr "" "{ph0}Configuration de Ghidra et exigences d’architecture{ph1} · {ph2}Guide " "de Ghidra sur Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · connecter une installation existante" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14119,7 +14077,7 @@ msgstr "" "où le binaire original est ouvert, ou un superviseur sans interface " "graphique qui ouvre une base de données privée." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14132,7 +14090,7 @@ msgstr "" "natif facultatif vers l’interface graphique a ses propres exigences de " "version et de plateforme." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14144,14 +14102,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Profils IDA, exemples d’enregistrement et plateformes vérifiées{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14162,18 +14120,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Suivre un exemple d’analyse native{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Sites web, Android, .NET, firmware et autres cibles" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14182,110 +14140,110 @@ msgstr "" "Ouvrez le guide correspondant à votre cible. Chaque guide indique les outils" " supplémentaires, les hôtes pris en charge et la première commande." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Cible" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Outils à préparer" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Guide" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Ouvrir le guide{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Site web" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / paquets" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "APK Android" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Appareil Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Ressources Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Projet JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / plantage Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Signaux logiques" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Comportement des processus" @@ -14301,7 +14259,7 @@ msgstr "Comportement des processus" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14311,18 +14269,18 @@ msgstr "" "{ph0}Guide Flutter{ph1} · {ph2}Paquets et ressources{ph3} · {ph4}Bytecode " "EVM{ph5} · {ph6}Tous les guides d’investigation{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Utiliser REA depuis un terminal" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Commandes CLI, installation globale et curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14332,7 +14290,7 @@ msgstr "" "Remplacez {ph0} par un nom ou une adresse de fonction figurant dans la liste" " des fonctions de la cible. Ghidra doit être configuré au préalable." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14341,7 +14299,7 @@ msgstr "" "Sur macOS ou Linux, vous pouvez aussi utiliser l’installateur curl après " "avoir préparé Node.js :" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14351,12 +14309,12 @@ msgstr "" "configuration. Dans les clients éditeurs, activez REA dans les paramètres " "MCP et autorisez ses outils dans le chat de l’agent." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL et chemins contenant des espaces" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14368,17 +14326,17 @@ msgstr "" "cet environnement Linux et utilisez-y des chemins Linux. Les fournisseurs " "sans interface graphique s’exécutent sur le même hôte que REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Commande de configuration pour Windows :" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Mettre à jour ou supprimer REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14389,7 +14347,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14403,7 +14361,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14426,18 +14384,18 @@ msgstr "Copier la commande de configuration de REA pour Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Copier la commande" @@ -14495,14 +14453,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · choisir un modèle d’IA" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Préparer le fichier d’enregistrement d’IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14519,7 +14477,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14532,7 +14490,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14545,12 +14503,12 @@ msgstr "" "nécessaire, {ph2}. Ouvrez le binaire original dans IDA et lancez le plugin " "de ce serveur avant de demander à REA de l’inspecter." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Base de données SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode EVM" @@ -14611,7 +14569,7 @@ msgstr "" "Examinez et approuvez le plan, rouvrez Claude, puis {ph0}vérifiez la " "connexion à l’étape 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14622,12 +14580,12 @@ msgstr "" "une nouvelle installation sur macOS ou Linux arm64, compilez le " "décompilateur natif pour votre ordinateur avant de continuer." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Compiler le décompilateur natif de Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14640,7 +14598,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14650,7 +14608,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14660,13 +14618,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · instructions de compilation native{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" @@ -15073,7 +15031,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Autres options d'installation : {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15088,7 +15046,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15103,7 +15061,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15115,7 +15073,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15127,7 +15085,7 @@ msgstr "" "défaut est {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15146,13 +15104,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -16058,7 +16016,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -16067,6 +16025,101 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1} (macOS ou Linux): REAle programme d'installation le détecte" +#~ " ou propose de l'installer. Sur macOS, ouvert Hopper une fois et choisissez " +#~ "le mode démo ou activez une licence avant votre première analyse." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Mettre en place REA{ph1} sur macOS ou Linux, puis téléchargez ces deux " +#~ "fichiers dans le même dossier. Sur Windows, utilisez un Linux environnement " +#~ "tel que WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Distributions prises en charge" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Hôtes Linux : Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS ou " +#~ "Omarchy. Hopper est un logiciel commercial ; sa démo a des limites définies " +#~ "par l’éditeur." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/id/messages.po b/website/locales/id/messages.po index 5378e7577a..3340dc1007 100644 --- a/website/locales/id/messages.po +++ b/website/locales/id/messages.po @@ -72,13 +72,13 @@ msgstr "Oleh N0zoM1z0 · 10 Oktober 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3299,21 +3299,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Salin" @@ -3544,12 +3544,12 @@ msgstr "" "muncul." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "Di terminal, beri tanda kutip di sekitar jalur yang berisi spasi." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3558,14 +3558,14 @@ msgstr "" "Analisis folder aplikasi JavaScript atau Electron atau arsip ASAR tanpa " "menjalankan aplikasi:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Analisis aplikasi statis" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3575,13 +3575,13 @@ msgstr "" "Ganti jalur dengan direktori aplikasi yang Anda ekstrak atau {ph0} berkas. " "Pada Windows, jalur seperti {ph1} bekerja di sini." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Di mana berkas .asar suatu aplikasi?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3592,7 +3592,7 @@ msgstr "Aplikasi Electron biasanya menyimpannya dalam {ph0} folder:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3604,7 +3604,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3617,21 +3617,21 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} sering {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" "Beberapa aplikasi dipasang di tempat lain. Cari folder aplikasi untuk {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3643,36 +3643,36 @@ msgstr "" "ditandai dalam hasil." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Jika Anda ingin permanen {ph0} perintah di shell Anda:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Instal CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Lanjutkan dengan panduan aplikasi JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Hubungkan instalasi Ghidra Anda" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Hubungkan Ghidra di Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3684,7 +3684,7 @@ msgstr "" "instalasi Ghidra 12.1.x yang sudah ada dan JDK 64-bit versi 21 atau lebih " "baru:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3701,18 +3701,18 @@ msgstr "" " itu mendeteksi instalasi yang ada atau menawarkan rencana instalasi untuk " "persetujuan Anda." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Mendapatkan bantuan" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Jika agen Anda tidak melihat REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3722,22 +3722,22 @@ msgstr "" "Mulai ulang atau sambungkan kembali klien setelah pengaturan. Periksa " "koneksi REA-nya dengan {ph0}; untuk Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Jika penyedia membutuhkan perhatian" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Periksa penyedia yang Anda gunakan. Untuk Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Periksa satu penyedia" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3748,7 +3748,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3761,7 +3761,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3775,7 +3775,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3789,7 +3789,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3855,22 +3855,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Salin prompt instalasi REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Salin perintah analisis JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Salin perintah instalasi CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Salin perintah diagnosis penyedia" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Kembali ke atas" @@ -5219,21 +5219,7 @@ msgstr "REA membutuhkan salah satunya untuk membaca program asli:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS atau Linux ): Pengaturan REA mendeteksi atau " -"menawarkan untuk menginstalnya. Di macOS, buka Hopper sekali dan pilih mode " -"demo atau aktifkan lisensi sebelum analisis pertama Anda." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5246,7 +5232,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5262,14 +5248,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Agen pengkodean Anda" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Salin" @@ -5280,7 +5266,7 @@ msgstr "Salin" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5291,7 +5277,7 @@ msgstr "" "pemosisian suara stereo. Jelaskan perhitungannya dan tunjukkan " "kodenya.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5303,7 +5289,7 @@ msgstr "" "sendiri, atau ajukan pertanyaan yang sama tentang program Windows kecil " "lainnya yang Anda miliki." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Bagaimana cara mendapatkan path lengkap file?" @@ -5312,7 +5298,7 @@ msgstr "Bagaimana cara mendapatkan path lengkap file?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5327,7 +5313,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5339,7 +5325,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5349,17 +5335,17 @@ msgstr "" "{ph0}Sistem apa pun:{ph1} seret file ke jendela terminal dan jalurnya " "muncul." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Baca hasil REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Baca inputnya" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5369,13 +5355,13 @@ msgstr "" "floating-point. Ini adalah input yang tidak ada dari tampilan pseudocode " "pertama." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Ikuti fungsi pemanggil" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5386,12 +5372,12 @@ msgstr "" "sebelumnya mengalikan koordinat ubin dengan 30; menambahkan {ph0} (20) " "memberikan posisi layar bata." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Baca konstanta yang dirujuk" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5400,12 +5386,12 @@ msgstr "" "Aritmatika mereferensikan alamat-alamat ini. Membaca byte memberikan nilai " "aktual; urutan instruksi memberi tahu agen bagaimana mereka digunakan." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Tulis dan periksa fungsi yang dipulihkan" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5416,7 +5402,7 @@ msgstr "" " sebelum konversi bilangan bulat. Perhitungan menempatkan bagian tengah " "layar di tengah bidang stereo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5429,20 +5415,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Lihat investigasi lengkap dan perbandingan assembly-to-C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Cobalah dengan biner Anda sendiri" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5456,7 +5442,7 @@ msgstr "" "awal yang berguna termasuk \"Bagaimana file ini diuraikan?\"atau\" Fungsi " "mana yang menangani tindakan menu ini?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5465,13 +5451,13 @@ msgstr "" "Jika Anda sudah mengetahui nama atau alamat fungsi, Anda dapat menanyakannya" " dari terminal. Untuk instalasi Ghidra yang dikonfigurasi:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Analisis fungsi" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5485,7 +5471,7 @@ msgstr "" "sesinya. Kueri MCP agen Anda menggunakan kembali biner yang diimpor saat " "sesi tersebut dibuka." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5519,17 +5505,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Analisis program kode mesin" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Salin contoh prompt" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Salin analisis fungsi" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Kembali ke atas" @@ -6512,7 +6498,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Agen pengkodean Anda" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Salin" @@ -6925,21 +6911,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Periksa sendiri APK" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Siapkan REA {ph1} di macOS atau Linux, lalu unduh kedua file ini ke " -"dalam folder yang sama. Pada Windows, gunakan lingkungan Linux seperti WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6949,18 +6923,18 @@ msgstr "" "Analisis Android membutuhkan JDK 17 lengkap atau lebih baru. Pilih dengan " "{ph0} atau {ph1}. Kasus ini menggunakan JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Arahkan REA ke file penyedia dan periksa APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6972,7 +6946,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6982,7 +6956,7 @@ msgstr "" "Pertanyaan selanjutnya: {ph0}apa yang terjadi jika akun menggunakan delapan " "digit atau interval waktu yang berbeda?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Sumber" @@ -6998,7 +6972,7 @@ msgstr "Sumber" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7012,13 +6986,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}Kasus referensi RFC 6238{ph1} · {ph2}Data uji demo{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7063,12 +7037,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Ikuti perhitungan kode masuk" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Salin perintah inspeksi APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Kembali ke atas" @@ -10860,13 +10834,13 @@ msgstr "Panduan" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10883,13 +10857,13 @@ msgstr "Kode sumber" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10906,13 +10880,13 @@ msgstr "Lisensi MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10929,13 +10903,13 @@ msgstr "Gabung komunitas" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11020,22 +10994,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "Hasil REA * ringkasan.kutipan ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Kutipan instruksi REA · 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "Kutipan fungsi pemanggil REA · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Hasil byte REA * ditafsirkan sebagai ganda little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Kutipan C · yang dipulihkan" @@ -11096,12 +11070,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, isi lengkap" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Unduh Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Unduh penyedia analisis Android" @@ -13790,11 +13764,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Tidak tersedia" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Distribusi yang didukung" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13832,17 +13801,7 @@ msgstr "" "dibuka. Di Linux, penyiapan menyiapkan demo yang didukung beserta tampilan " "privatnya." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, atau" -" Omarchy. Hopper adalah perangkat lunak komersial; demonya memiliki batas " -"yang ditentukan vendor." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13855,18 +13814,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Penyiapan Hopper dan pemecahan masalah{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · instal alat analisis gratis dan Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13876,7 +13835,7 @@ msgstr "" "paket yang sesuai dengan arsitektur komputermu." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13889,7 +13848,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13905,7 +13864,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13913,7 +13872,7 @@ msgstr "{ph0}Unduh JDK{ph1} · {ph2}Unduh Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13923,7 +13882,7 @@ msgstr "" "Periksa Java. Perintah pertama menampilkan {ph0}; gunakan folder itu untuk " "{ph1}. Perintah kedua memastikan kompiler sudah terinstal:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13941,7 +13900,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13951,12 +13910,12 @@ msgstr "" "{ph0}Penyiapan Ghidra dan kebutuhan arsitektur{ph1} · {ph2}Panduan Ghidra " "untuk Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · hubungkan instalasi yang sudah ada" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13967,7 +13926,7 @@ msgstr "" "upstream. Pilih GUI terhubung dengan berkas biner asli terbuka, atau " "supervisor tanpa antarmuka grafis yang membuka basis data privat." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13979,7 +13938,7 @@ msgstr "" "macOS arm64 dengan IDA Professional 9.1. Bridge GUI native yang opsional " "memiliki kebutuhan versi dan platform sendiri." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13991,7 +13950,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" @@ -13999,7 +13958,7 @@ msgstr "" "{ph0}Profil IDA, contoh registrasi, dan platform yang sudah " "diverifikasi{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14010,18 +13969,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Ikuti contoh analisis native{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Situs web, Android, .NET, firmware, dan target lain" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14030,110 +13989,110 @@ msgstr "" "Buka panduan untuk targetmu. Setiap panduan menjelaskan alat tambahan, host " "yang didukung, dan perintah pertama." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Target" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Alat yang perlu disiapkan" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Panduan" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Buka panduan{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Situs web" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / paket" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "APK Android" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Perangkat Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Sumber daya Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Proyek JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / crash Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Sinyal logika" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Perilaku proses" @@ -14149,7 +14108,7 @@ msgstr "Perilaku proses" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14159,18 +14118,18 @@ msgstr "" "{ph0}Panduan Flutter{ph1} · {ph2}Paket dan sumber daya{ph3} · {ph4}Bytecode " "EVM{ph5} · {ph6}Semua panduan penyelidikan{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Gunakan REA dari terminal" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Perintah CLI, instalasi global, dan curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14180,7 +14139,7 @@ msgstr "" "Ganti {ph0} dengan nama atau alamat fungsi dari daftar fungsi target. Ghidra" " harus dikonfigurasi terlebih dahulu." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14189,7 +14148,7 @@ msgstr "" "Di macOS atau Linux, kamu juga bisa menggunakan penginstal curl setelah " "menyiapkan Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14199,12 +14158,12 @@ msgstr "" "editor, aktifkan REA di pengaturan MCP dan izinkan alatnya dalam percakapan " "agen." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL, dan jalur yang mengandung spasi" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14216,17 +14175,17 @@ msgstr "" "gunakan jalur Linux di sana. Penyedia tanpa antarmuka grafis berjalan pada " "host yang sama dengan REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Perintah penyiapan Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Perbarui atau hapus REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14237,7 +14196,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14251,7 +14210,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14274,18 +14233,18 @@ msgstr "Salin perintah penyiapan REA untuk Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Salin perintah" @@ -14341,14 +14300,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · pilih model AI" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Siapkan berkas registrasi IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14364,7 +14323,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14377,7 +14336,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14390,12 +14349,12 @@ msgstr "" "biner asli di IDA dan mulai plugin upstream-nya sebelum meminta REA " "memeriksanya." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Basis data SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode EVM" @@ -14453,7 +14412,7 @@ msgstr "" "Tinjau dan setujui rencana, buka kembali Claude, lalu {ph0}periksa koneksi " "pada langkah 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14464,12 +14423,12 @@ msgstr "" "baru di macOS atau Linux arm64, build dekompiler native untuk komputermu " "sebelum melanjutkan." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Build dekompiler native Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14482,7 +14441,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14492,7 +14451,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14502,13 +14461,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · petunjuk build native{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Lalu periksa Ghidra dan simpan jalurnya untuk aplikasimu:" @@ -14911,7 +14870,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Pilihan instalasi lain: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14926,7 +14885,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14941,7 +14900,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14953,7 +14912,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14964,7 +14923,7 @@ msgstr "" "ditampilkan installer. Di Linux, perintah penyiapan default adalah {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14983,13 +14942,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15883,7 +15842,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15892,6 +15851,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS atau Linux ): Pengaturan REA mendeteksi atau " +#~ "menawarkan untuk menginstalnya. Di macOS, buka Hopper sekali dan pilih mode " +#~ "demo atau aktifkan lisensi sebelum analisis pertama Anda." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Siapkan REA {ph1} di macOS atau Linux, lalu unduh kedua file ini ke " +#~ "dalam folder yang sama. Pada Windows, gunakan lingkungan Linux seperti WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Distribusi yang didukung" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, atau" +#~ " Omarchy. Hopper adalah perangkat lunak komersial; demonya memiliki batas " +#~ "yang ditentukan vendor." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/it/messages.po b/website/locales/it/messages.po index 6afd297e10..777566dee3 100644 --- a/website/locales/it/messages.po +++ b/website/locales/it/messages.po @@ -71,13 +71,13 @@ msgstr "Di N0zoM1z0 * 10 ottobre 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3307,21 +3307,21 @@ msgstr "Terminale" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Copia" @@ -3555,14 +3555,14 @@ msgstr "" "terminale e il suo percorso appare." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "In un terminale, metti le virgolette attorno a un percorso che contiene " "spazi." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3571,14 +3571,14 @@ msgstr "" "Analizzare una cartella dell'applicazione JavaScript o Electron o un " "archivio ASAR senza eseguire l'app:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Analisi statica delle applicazioni" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3588,13 +3588,13 @@ msgstr "" "Sostituire il percorso con la directory dell'app estratta o {ph0} file. Su " "Windows, un percorso come {ph1} funziona qui." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Dove trovo il file .asar di un'app?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3605,7 +3605,7 @@ msgstr "Electron apps di solito tenerlo in un {ph0} cartella:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3617,7 +3617,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3630,21 +3630,21 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} spesso {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" "Alcune app si installano altrove. Cerca nella cartella dell'app per {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3656,36 +3656,36 @@ msgstr "" " contrassegnate nel risultato." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Se vuoi un permanente {ph0} comando nella tua shell:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Installare il CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Continuare con la guida all'applicazione JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Collegare l'installazione Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Collegare Ghidra su Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3697,7 +3697,7 @@ msgstr "" "un'installazione esistente di Ghidra 12.1.x e un JDK a 64 bit, versione 21 o" " successiva:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3715,18 +3715,18 @@ msgstr "" "un'installazione esistente o offre un piano di installazione per " "l'approvazione." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Ottenere aiuto" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Se il tuo agente non vede REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3736,22 +3736,22 @@ msgstr "" "Riavviare o ricollegare il client dopo l'installazione. Controlla la sua " "connessione REA con {ph0}; per Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Se un fornitore ha bisogno di attenzione" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Controlla il provider che stai utilizzando. Per Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Controllare un provider" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3762,7 +3762,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3775,7 +3775,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3789,7 +3789,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3803,7 +3803,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3867,22 +3867,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Copia REA prompt di installazione" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Copia il comando di analisi JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Copia il comando di installazione CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Copia il comando di diagnosi del provider" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Torna in alto" @@ -5244,21 +5244,7 @@ msgstr "REA ha bisogno di uno di questi per leggere i programmi nativi:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} ( macOS o Linux): la configurazione di REA lo rileva o " -"offre di installarlo. Su macOS, apri Hopper una volta e scegli la modalità " -"demo o attiva una licenza prima della tua prima analisi." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5271,7 +5257,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5287,14 +5273,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Il tuo agente di codifica" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Copia" @@ -5305,7 +5291,7 @@ msgstr "Copia" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5316,7 +5302,7 @@ msgstr "" "posizionamento del suono stereo. Spiega il calcolo e mostra il " "codice.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5327,7 +5313,7 @@ msgstr "" "1.07 locale. REA non include DX-Ball, quindi usa la tua copia o fai lo " "stesso tipo di domanda su un altro piccolo programma Windows che hai." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Come ottengo il percorso completo di un file?" @@ -5336,7 +5322,7 @@ msgstr "Come ottengo il percorso completo di un file?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5351,7 +5337,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5363,7 +5349,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5373,17 +5359,17 @@ msgstr "" "{ph0}Qualsiasi sistema:{ph1} trascinare il file in una finestra di terminale" " e il suo percorso appare." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Leggi i risultati di REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Leggi l'input" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5392,13 +5378,13 @@ msgstr "" "Le istruzioni caricano un argomento intero e lo convertono per l'aritmetica " "in virgola mobile. Questo è l'input assente dalla prima vista pseudocodice." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Segui il chiamante" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5409,12 +5395,12 @@ msgstr "" "moltiplicano la coordinata tile per 30; aggiunta {ph0} (20) dà la posizione " "dello schermo del mattone." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Leggi le costanti di riferimento" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5424,12 +5410,12 @@ msgstr "" " i valori effettivi; l'ordine di istruzioni indica all'agente come vengono " "utilizzati." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Scrivi e controlla la funzione recuperata" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5440,7 +5426,7 @@ msgstr "" "-500, 0 e 500 prima della conversione intera. Il calcolo mette il centro " "dello schermo al centro del campo stereo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5453,20 +5439,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Vedi l'indagine completa e il confronto assembly-to-C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Provalo con il tuo binario" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5481,7 +5467,7 @@ msgstr "" "Come viene analizzato questo file?\"o\" Quale funzione gestisce questa " "azione del menu?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5490,13 +5476,13 @@ msgstr "" "Se si conosce già il nome o l'indirizzo di una funzione, è possibile " "interrogarlo dal terminale. Per un'installazione Ghidra configurata:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Analisi delle funzioni" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5510,7 +5496,7 @@ msgstr "" "binario, quindi chiude la sua sessione. Le query MCP dell'agente " "riutilizzano il binario importato mentre la sessione è aperta." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5544,17 +5530,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Analisi di programmi a codice macchina" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Copia esempio prompt" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Analisi delle funzioni di copia" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Torna in alto" @@ -6537,7 +6523,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Il tuo agente di codifica" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Copia" @@ -6951,21 +6937,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Ispezionare il APK da soli" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Impostare REA {ph1} su macOS o Linux, quindi scaricare questi due file " -"nella stessa cartella. Su Windows, utilizzare un ambiente Linux come WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6975,18 +6949,18 @@ msgstr "" "Android analysis ha bisogno di un JDK completo 17 o successivo. Selezionalo " "con {ph0} o {ph1}. Questo caso ha usato JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Puntare REA sul file del provider e ispezionare APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6998,7 +6972,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7008,7 +6982,7 @@ msgstr "" "Una prossima domanda: {ph0}cosa succede quando un account utilizza otto " "cifre o un intervallo di tempo diverso?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Fonte" @@ -7024,7 +6998,7 @@ msgstr "Fonte" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7038,13 +7012,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC 6238 casi di riferimento{ph1} · {ph2}Dati di prova demo{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7089,12 +7063,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Segui il calcolo del codice di accesso" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Copia il comando di ispezione APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Torna in alto" @@ -10910,13 +10884,13 @@ msgstr "Guide" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10933,13 +10907,13 @@ msgstr "Codice sorgente" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10956,13 +10930,13 @@ msgstr "Licenza MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10979,13 +10953,13 @@ msgstr "Unisciti alla community" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11070,22 +11044,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA risultato * riepilogo.estratto ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Estratto di istruzioni REA * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA chiamante estratto * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Risultati byte REA * interpretati come doppi little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Recuperato C * estratto" @@ -11146,12 +11120,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, corpo completo" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Scarica Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Scarica Android analysis provider" @@ -13854,11 +13828,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Non disponibile" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Distribuzioni supportate" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13896,17 +13865,7 @@ msgstr "" "licenza al primo avvio di Hopper. Su Linux, la configurazione prepara la " "demo supportata e il suo display privato." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS o " -"Omarchy. Hopper è un software commerciale; la demo ha limiti definiti dal " -"produttore." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13919,18 +13878,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Configurazione di Hopper e risoluzione dei problemi{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · installa lo strumento di analisi gratuito e Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13940,7 +13899,7 @@ msgstr "" "Java. Scegli il pacchetto adatto all'architettura del tuo computer." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13953,7 +13912,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13969,7 +13928,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13977,7 +13936,7 @@ msgstr "{ph0}Scarica un JDK{ph1} · {ph2}Scarica Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13987,7 +13946,7 @@ msgstr "" "Controlla Java. Il primo comando stampa {ph0}; usa quella cartella per " "{ph1}. Il secondo conferma che il compilatore è installato:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14005,7 +13964,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14015,12 +13974,12 @@ msgstr "" "{ph0}Configurazione di Ghidra e requisiti di architettura{ph1} · {ph2}Guida " "a Ghidra su Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · collega un'installazione esistente" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14031,7 +13990,7 @@ msgstr "" "upstream. Scegli una GUI collegata con il binario originale aperto, oppure " "un supervisore senza interfaccia grafica che apre un database privato." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14043,7 +14002,7 @@ msgstr "" "grafica su macOS arm64 con IDA Professional 9.1. Il bridge nativo " "facoltativo per la GUI ha requisiti propri di versione e piattaforma." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14055,14 +14014,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Profili IDA, esempi di registrazione e piattaforme verificate{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14073,18 +14032,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Segui un esempio di analisi nativa{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Siti web, Android, .NET, firmware e altri obiettivi" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14093,110 +14052,110 @@ msgstr "" "Apri la guida per ciò che vuoi analizzare. Ogni guida indica gli strumenti " "aggiuntivi, gli host supportati e il primo comando." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Target" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Strumenti da preparare" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Guida" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Apri la guida{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Sito web" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / pacchetti" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "APK Android" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Dispositivo Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Risorse Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Progetto JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / crash di Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Segnali logici" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Comportamento dei processi" @@ -14212,7 +14171,7 @@ msgstr "Comportamento dei processi" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14222,18 +14181,18 @@ msgstr "" "{ph0}Guida Flutter{ph1} · {ph2}Pacchetti e risorse{ph3} · {ph4}Bytecode " "EVM{ph5} · {ph6}Tutte le guide di analisi{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Usa REA dal terminale" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Comandi CLI, installazione globale e curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14243,7 +14202,7 @@ msgstr "" "Sostituisci {ph0} con il nome o l'indirizzo di una funzione dall'elenco " "delle funzioni del target. Ghidra deve essere già configurato." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14252,7 +14211,7 @@ msgstr "" "Su macOS o Linux puoi anche usare il programma di installazione curl dopo " "aver preparato Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14262,12 +14221,12 @@ msgstr "" "configurazione. Negli editor, abilita REA nelle impostazioni MCP e consenti " "i suoi strumenti nella chat dell'agente." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL e percorsi con spazi" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14279,17 +14238,17 @@ msgstr "" "quell'ambiente Linux e usa lì i percorsi Linux. I provider senza interfaccia" " grafica vengono eseguiti sullo stesso host di REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Comando di configurazione per Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Aggiorna o rimuovi REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14301,7 +14260,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14315,7 +14274,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14338,18 +14297,18 @@ msgstr "Copia il comando di configurazione REA per Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Copia il comando" @@ -14405,14 +14364,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · scegli un modello IA" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Prepara il file di registrazione IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14428,7 +14387,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14441,7 +14400,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14454,12 +14413,12 @@ msgstr "" "in IDA e avvia il suo plugin upstream prima di chiedere a REA di " "analizzarlo." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Database SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode EVM" @@ -14517,7 +14476,7 @@ msgstr "" "Controlla e approva il piano, riapri Claude, poi {ph0}verifica la " "connessione al passaggio 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14528,12 +14487,12 @@ msgstr "" "Per una nuova installazione su macOS o Linux arm64, compila il decompilatore" " nativo per il tuo computer prima di continuare." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Compila il decompilatore nativo di Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14546,7 +14505,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14556,7 +14515,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14566,13 +14525,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · istruzioni di compilazione nativa{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Poi controlla Ghidra e salva i suoi percorsi per l'applicazione:" @@ -14975,7 +14934,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Altre opzioni di installazione: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14990,7 +14949,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15005,7 +14964,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15017,7 +14976,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15029,7 +14988,7 @@ msgstr "" "predefinito è {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15048,13 +15007,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15957,7 +15916,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15966,6 +15925,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} ( macOS o Linux): la configurazione di REA lo rileva o " +#~ "offre di installarlo. Su macOS, apri Hopper una volta e scegli la modalità " +#~ "demo o attiva una licenza prima della tua prima analisi." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Impostare REA {ph1} su macOS o Linux, quindi scaricare questi due file " +#~ "nella stessa cartella. Su Windows, utilizzare un ambiente Linux come WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Distribuzioni supportate" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS o " +#~ "Omarchy. Hopper è un software commerciale; la demo ha limiti definiti dal " +#~ "produttore." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/ja/messages.po b/website/locales/ja/messages.po index 17a28619f3..b63bbcca26 100644 --- a/website/locales/ja/messages.po +++ b/website/locales/ja/messages.po @@ -70,13 +70,13 @@ msgstr "によってN0zoM1z0•10 10月2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3047,21 +3047,21 @@ msgstr "ターミナル" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "コピー" @@ -3278,26 +3278,26 @@ msgid "" msgstr "{ph0}任意のシステム:{ph1} フォルダをターミナルウィンドウにドラッグすると、そのパスが表示されます。" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "ターミナルでは、スペースを含むパスの周りに引用符を付けます。" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "アプリを実行せずに、JavaScriptまたはElectronアプリケーションフォルダーまたはASARアーカイブを分析します:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "静的アプリケーション分析" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3305,13 +3305,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "パスを、展開したアプリのディレクトリーまたは{ph0}ファイルに置き換えてください。Windowsでは、{ph1}のようなパスを使えます。" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "どこでアプリを見つけるのですか。asarファイル?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3322,7 +3322,7 @@ msgstr "Electronアプリは、通常、それを維持します {ph0} フォル #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3334,7 +3334,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3345,20 +3345,20 @@ msgstr "{ph0} macOS :{ph1} {ph2} (アプリを右クリックして選択し #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} 多くの場合 {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "一部のアプリは他の場所にインストールされます。 アプリのフォルダを検索します {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3367,36 +3367,36 @@ msgid "" msgstr "結果はアプリを識別し、モジュール間の接続を示し、ソースの場所を示します。 REAが解決できない接続は、結果にマークされています。" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "パーマネントをご希望の場合は {ph0} シェル内のコマンド:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "CLIをインストールする" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}JavaScriptアプリケーションガイドの続きを読む{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Ghidraインストールを接続します" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "GhidraをWindowsに接続する" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3406,7 +3406,7 @@ msgstr "" "Windows x64では、固定ローカルNTFSドライブ上のネイティブx86/x64 " "PEアプリとDLLに実験的な読み取り専用対応があります。既存のGhidra 12.1.xと64ビットのJDK 21以降を使ってください。" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3421,18 +3421,18 @@ msgstr "" "HopperをmacOSまたはLinuxに接続します。 " "上記のsetupコマンドを実行します。既存のインストールを検出するか、承認のためのインストール計画を提供します。" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "助けを得る" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "エージェントにREAが表示されない場合" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3440,22 +3440,22 @@ msgid "" "{ph0}; for Codex:" msgstr "セットアップ後にクライアントを再起動または再接続します。 とのREA接続を確認してください {ph0};Codexのため :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "プロバイダが注意を必要とする場合" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "使用しているプロバイダーを確認してください。 Ghidraのため :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "一つのプロバイダを確認してください" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "レポートには、失敗したチェックと推奨される次のステップが表示されます。" @@ -3464,7 +3464,7 @@ msgstr "レポートには、失敗したチェックと推奨される次のス #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3475,7 +3475,7 @@ msgstr "{ph0} REAよくある質問{ph1}{ph2}更新、エージェント、分 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3487,7 +3487,7 @@ msgstr "{ph0}調査ガイドを選ぶ{ph1}{ph2}バイナリ、Electronアプリ #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3499,7 +3499,7 @@ msgstr "{ph0} GitHubの問題{ph1}{ph2}コマンドと診断に関する問題 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3562,22 +3562,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "コピー REAインストールプロンプト" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Copy JavaScript解析コマンド" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Copy CLIインストールコマンド" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "プロバイダ診断コマンドのコピー" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "ページ上部へ" @@ -4851,20 +4851,7 @@ msgstr "REAは、ネイティブプログラムを読むためにこれらのい #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOSまたはLinux):REAのセットアップがそれを検出するか、それをインストールすることを提案します。 " -"macOSでは、Hopperを一度開き、デモモードを選択するか、最初の分析の前にライセンスをアクティブにします。" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4876,7 +4863,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -4890,14 +4877,14 @@ msgstr "見る {ph0}マシンコードプログラムの解析の設定{ph1} ス #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}あなたのコーディングエージェント" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "コピー" @@ -4908,7 +4895,7 @@ msgstr "コピー" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4918,7 +4905,7 @@ msgstr "" "{ph0}›{ph1}{ph2}REAを使用して、DX-Ballがステレオサウンドの位置を計算する方法を見つけます。 " "計算を説明し、コードを表示します。{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -4928,7 +4915,7 @@ msgstr "" "プロンプトの例。 エージェントにローカルのDX-Ball1.07実行可能ファイルへのパスを指定します。 REAにはDX-" "Ballが含まれていないため、独自のコピーを使用するか、持っている別の小さなWindowsプログラムについて同じ種類の質問をしてください。" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "ファイルのフルパスを取得するにはどうすればよいですか?" @@ -4937,7 +4924,7 @@ msgstr "ファイルのフルパスを取得するにはどうすればよいで #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4951,7 +4938,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4963,7 +4950,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4971,30 +4958,30 @@ msgid "" "appears." msgstr "{ph0}任意のシステム:{ph1} ファイルをターミナルウィンドウにドラッグすると、そのパスが表示されます。" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "REAの結果を読む" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "入力を読む" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "命令は整数引数をロードし、浮動小数点演算用に変換します。 これは、最初の擬似コードビューに存在しない入力です。" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "発信者に従ってください" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5003,24 +4990,24 @@ msgid "" msgstr "" "REAは、ブリックヒット呼び出し元を識別します。 以前の手順では、タイル座標に30を掛けます。 {ph0} (20)は、レンガの画面位置を与えます。" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "参照される定数の読み取り" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "算術はこれらのアドレスを参照します。 命令の順序は、それらがどのように使用されているかをエージェントに伝えます。" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "復元された機能の書き込みと確認" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5030,7 +5017,7 @@ msgstr "" "縮尺が1.0の場合、画面の位置0、320、640は整数変換の前に-500、0、500にマップされます。 " "この計算では、画面の中心がステレオフィールドの中心に置かれます。" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5042,20 +5029,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}完全な調査とアセンブリとCの比較を参照してください{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "独自のバイナリで試してみてください" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5068,20 +5055,20 @@ msgstr "" "次に、エージェントにローカルの実行可能ファイルまたはライブラリと調査する機能を提供します。 " "有用な開始質問には、\"このファイルはどのように解析されますか?\"または\"このメニューアクションを処理する関数はどれですか?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "関数名またはアドレスがすでにわかっている場合は、端末からクエリできます。 構成されたGhidraインストールの場合:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "関数解析" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5094,7 +5081,7 @@ msgstr "" "各CLI呼び出しは、バイナリをインポートして分析し、そのセッションを閉じます。 " "エージェントのMCPクエリは、セッションが開いている間にインポートされたバイナリを再利用します。" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5123,17 +5110,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "機械コードプログラムの解析" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "プロンプトの例をコピーする" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "コピー関数解析" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "ページ上部へ" @@ -6067,7 +6054,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}あなたのコーディングエージェント" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "コピー" @@ -6454,21 +6441,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "APKを自分で調べてください" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}REAのセットアップ {ph1} macOSまたはLinuxで、これら2つのファイルを同じフォルダーにダウンロードします。 " -"Windowsでは、WSLなどのLinux環境を使用します。" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6478,18 +6453,18 @@ msgstr "" "Android analysisには完全なJDK17以降が必要です。 で選択します。 {ph0} または {ph1}. " "このケースはJDK21を使用していました。" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "プロバイダーファイルでREAをポイントし、APKを検査します :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS/Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6501,7 +6476,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6509,7 +6484,7 @@ msgid "" "different time interval?{ph1}" msgstr "次の質問: {ph0}アカウントが8桁または異なる時間間隔を使用するとどうなりますか?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "ソース" @@ -6525,7 +6500,7 @@ msgstr "ソース" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6538,13 +6513,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC6238参照ケース{ph1} · {ph2}デモテストデータ{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6586,12 +6561,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "ログインコードの計算に従ってください" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "コピー APK検査コマンド" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "ページ上部へ" @@ -10153,13 +10128,13 @@ msgstr "ガイド" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10176,13 +10151,13 @@ msgstr "ソースコード" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10199,13 +10174,13 @@ msgstr "MIT ライセンス" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10222,13 +10197,13 @@ msgstr "コミュニティに参加" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10313,22 +10288,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA結果·概要。ipc抜粋" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA命令の抜粋·0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA発信者の抜粋*0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REAバイトの結果·リトルエンディアンの倍精度として解釈されます" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "回収されたC*抜粋" @@ -10389,12 +10364,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA*generateOTP、完全なボディ" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "ダウンロードAegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Android分析プロバイダーをダウンロード" @@ -12801,11 +12776,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "利用不可" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "対応ディストリビューション" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -12840,16 +12810,7 @@ msgid "" msgstr "" "セットアップでHopperを選び、別途提示されるインストール計画を確認してください。macOSでは、Hopperを初めて開いたときにデモモードを選ぶか、ライセンスを有効化します。Linuxでは、セットアップが対応するデモと専用のディスプレイを準備します。" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linuxホスト:Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch " -"Linux、CachyOS、Omarchy。Hopperは商用ソフトウェアで、デモには提供元が定めた制限があります。" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -12862,18 +12823,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopperの設定とトラブル対処{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · 無料の分析ツールとJavaをインストールする" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -12882,7 +12843,7 @@ msgstr "" "Javaコンパイラーを含む64ビットのJDK 21以降をインストールしてください。コンピューターのアーキテクチャに合うパッケージを選びます。" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12895,7 +12856,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12909,7 +12870,7 @@ msgstr "展開したフォルダーのうち、{ph0}または{ph1}が入って #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -12917,7 +12878,7 @@ msgstr "{ph0}JDKをダウンロード{ph1} · {ph2}Ghidra 12.1.4をダウンロ #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12926,7 +12887,7 @@ msgid "" msgstr "" "Javaを確認します。最初のコマンドに表示される{ph0}のフォルダーを{ph1}に指定してください。2つ目のコマンドで、コンパイラーがインストールされているか確認します。" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -12942,7 +12903,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12950,12 +12911,12 @@ msgid "" "guide{ph3}" msgstr "{ph0}Ghidraの設定とアーキテクチャ要件{ph1} · {ph2}Windows向けGhidraガイド{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · 既存のインストールを接続する" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -12964,7 +12925,7 @@ msgid "" msgstr "" "IDAプロバイダーガイドに従い、接続先MCPサーバーの登録設定を用意してください。元のバイナリーを開いたGUIに接続するか、専用のデータベースを開くヘッドレススーパーバイザーを選びます。" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -12974,7 +12935,7 @@ msgstr "" "検証済みの構成には、Windowsの従来型GUIとヘッドレススーパーバイザー、およびIDA Professional 9.1を使うmacOS " "arm64のヘッドレススーパーバイザーがあります。任意のネイティブGUIブリッジには、独自のバージョンとプラットフォーム要件があります。" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -12984,13 +12945,13 @@ msgstr "プロバイダーガイドで用意したJSON登録ファイルを使 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}IDAの構成、登録例、検証済みプラットフォーム{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -12999,128 +12960,128 @@ msgstr "接続したら、元の実行ファイルのフルパスと、使う分 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}ネイティブ分析の例を進める{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Webサイト、Android、.NET、ファームウェアなど" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "お持ちの対象に対応するガイドを開いてください。各ガイドで、追加のツール、対応ホスト、最初のコマンドを説明しています。" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "対象" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "準備するツール" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "ガイド" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}ガイドを開く{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Webサイト" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / パッケージ" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android端末" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Androidリソース" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEBプロジェクト" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "ファームウェア" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linuxクラッシュ" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "ロジック信号" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "プロセスの動作" @@ -13136,7 +13097,7 @@ msgstr "プロセスの動作" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13146,18 +13107,18 @@ msgstr "" "{ph0}Flutterガイド{ph1} · {ph2}パッケージとリソース{ph3} · {ph4}EVMバイトコード{ph5} · " "{ph6}すべての調査ガイド{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "ターミナルからREAを使う" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLIコマンド、グローバルインストール、curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13165,14 +13126,14 @@ msgid "" "list. Ghidra must be configured first." msgstr "{ph0}を、対象の関数一覧にある関数名またはアドレスに置き換えてください。先にGhidraの設定が必要です。" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " "Node.js:" msgstr "macOSまたはLinuxでは、Node.jsを準備した後にcurlインストーラーも使えます。" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -13180,12 +13141,12 @@ msgid "" msgstr "" "手順3のクライアントチェックに、お使いのセットアップIDを指定してください。エディター型のクライアントでは、MCP設定でREAを有効にし、エージェントのチャットでツールの使用を許可します。" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows、WSL、スペースを含むパス" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -13195,17 +13156,17 @@ msgstr "" "Windowsのクライアントや分析ツールにはWindows " "PowerShellを使ってください。エージェントがWSL内で動く場合は、そのLinux環境内にNode.jsとREAをインストールし、Linuxのパスを使います。ヘッドレスプロバイダーはREAと同じホストで動作します。" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Windows用のセットアップコマンド:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "REAを更新または削除する" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -13214,7 +13175,7 @@ msgstr "エージェントの接続を更新するには、最新のセットア #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13227,7 +13188,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -13250,18 +13211,18 @@ msgstr "Windows用REAセットアップコマンドをコピー" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "コマンドをコピー" @@ -13316,14 +13277,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · AIモデルを選ぶ" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "IDAの登録ファイルを用意する" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13336,7 +13297,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13347,7 +13308,7 @@ msgstr "{ph0}、{ph1}、{ph2}を提供するヘッドレススーパーバイザ #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13358,12 +13319,12 @@ msgstr "" "接続済みの従来型1.4.0 " "GUIサーバーでは、動作確認済みの{ph0}、{ph1}、任意の{ph2}をファイルにコピーしてください。REAに調査を頼む前に、IDAで元のバイナリーを開き、接続先のプラグインを起動します。" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLiteデータベース" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVMバイトコード" @@ -13416,7 +13377,7 @@ msgid "" "in step 3{ph1}." msgstr "計画を確認して承認し、Claudeを開き直したら、{ph0}手順3で接続を確認{ph1}してください。" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -13426,12 +13387,12 @@ msgstr "" "両方のパスを、既存のインストール先に置き換えてください。macOSまたはLinux " "arm64に新しくインストールした場合は、先にそのコンピューター用のネイティブデコンパイラーをビルドします。" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Ghidraのネイティブデコンパイラーをビルドする" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -13444,7 +13405,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -13454,7 +13415,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -13464,13 +13425,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · ネイティブビルドの手順{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "その後、Ghidraをチェックし、クライアント用にパスを保存します。" @@ -13834,7 +13795,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "その他のインストール方法:{ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13847,7 +13808,7 @@ msgstr "セットアップの案内に従ってエージェントを接続して #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13860,7 +13821,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13870,7 +13831,7 @@ msgstr "ビルド済みのソースディレクトリで、下の{ph2}コマン #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13880,7 +13841,7 @@ msgstr "" "シェルが{ph0}を見つけられない場合は、インストーラーが表示した実行ファイルのパスを使ってください。Linuxでの標準のセットアップコマンドは{ph1}です。" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13897,13 +13858,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -14774,7 +14735,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -14783,6 +14744,98 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOSまたはLinux):REAのセットアップがそれを検出するか、それをインストールすることを提案します。 " +#~ "macOSでは、Hopperを一度開き、デモモードを選択するか、最初の分析の前にライセンスをアクティブにします。" + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}REAのセットアップ {ph1} macOSまたはLinuxで、これら2つのファイルを同じフォルダーにダウンロードします。 " +#~ "Windowsでは、WSLなどのLinux環境を使用します。" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "対応ディストリビューション" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linuxホスト:Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch " +#~ "Linux、CachyOS、Omarchy。Hopperは商用ソフトウェアで、デモには提供元が定めた制限があります。" + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/ko/messages.po b/website/locales/ko/messages.po index 92ae40790e..ce852f3368 100644 --- a/website/locales/ko/messages.po +++ b/website/locales/ko/messages.po @@ -71,13 +71,13 @@ msgstr "작성자: N0zoM1z0 · 2026년 10월 10일" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3046,21 +3046,21 @@ msgstr "터미널" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "복사" @@ -3274,26 +3274,26 @@ msgid "" msgstr "{ph0}모든 시스템:{ph1} 폴더를 터미널 창으로 드래그하면 해당 경로가 나타납니다." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "터미널에서는 공백이 있는 경로를 따옴표로 감싸세요." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "앱을 실행하지 않고 JavaScript 또는 Electron 앱 폴더나 ASAR 파일을 분석합니다:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "정적 애플리케이션 분석" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3301,13 +3301,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "경로를 압축을 푼 앱 폴더나 {ph0} 파일로 바꾸세요. Windows에서는 {ph1} 같은 경로를 사용할 수 있습니다." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "앱의 .asar 파일은 어디에 있나요?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3318,7 +3318,7 @@ msgstr "Electron 앱은 보통 {ph0} 폴더에 이 파일을 둡니다:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3330,7 +3330,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3341,20 +3341,20 @@ msgstr "{ph0}macOS:{ph1} {ph2}(앱을 마우스 오른쪽 버튼으로 클릭한 #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} 대개 {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "다른 위치에 설치되는 앱도 있습니다. 앱 폴더에서 {ph0}을 찾아보세요." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3363,36 +3363,36 @@ msgid "" msgstr "결과에는 앱의 식별 정보, 모듈 간 연결과 소스 위치가 나옵니다. REA가 연결을 확인하지 못한 부분도 표시됩니다." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "셸에서 {ph0} 명령을 항상 사용할 수 있도록 설치하려면:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "CLI 설치" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}JavaScript 앱 가이드 이어서 보기{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "기존 Ghidra 설치 연결" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Windows에서 Ghidra 연결" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3402,7 +3402,7 @@ msgstr "" "Windows x64에서는 고정 로컬 NTFS 드라이브의 네이티브 x86 및 x64 PE 앱과 DLL을 실험적으로 읽기 전용 분석할 수 " "있습니다. 기존 Ghidra 12.1.x 설치와 64비트 JDK 21 이상을 사용하세요." -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3416,18 +3416,18 @@ msgid "" msgstr "" "macOS 또는 Linux에서 Hopper를 연결하세요. 위 설정 명령은 기존 설치를 찾거나, 승인할 수 있는 설치 계획을 제안합니다." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "도움 받기" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "에이전트에서 REA가 보이지 않을 때" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3435,22 +3435,22 @@ msgid "" "{ph0}; for Codex:" msgstr "설정 후 클라이언트를 다시 시작하거나 연결하세요. {ph0}로 REA 연결을 확인할 수 있습니다. Codex의 예:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "분석 도구에 문제가 있을 때" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "사용 중인 분석 도구를 확인하세요. Ghidra의 예:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "분석 도구 하나 진단" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "보고서에 실패한 검사와 권장 후속 조치가 나옵니다." @@ -3459,7 +3459,7 @@ msgstr "보고서에 실패한 검사와 권장 후속 조치가 나옵니다." #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3470,7 +3470,7 @@ msgstr "{ph0}REA FAQ{ph1}{ph2}업데이트, 에이전트, 분석에 관한 자 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3482,7 +3482,7 @@ msgstr "{ph0}분석 가이드 선택{ph1}{ph2}바이너리, Electron 앱, 브라 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3494,7 +3494,7 @@ msgstr "{ph0}GitHub 이슈{ph1}{ph2}실행한 명령과 진단 결과를 첨부 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3557,22 +3557,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "REA 설치 프롬프트 복사" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "JavaScript 분석 명령 복사" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "CLI 설치 명령 복사" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "분석 도구 진단 명령 복사" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "맨 위로" @@ -4850,20 +4850,7 @@ msgstr "REA로 네이티브 프로그램을 읽으려면 다음 중 하나가 #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1}(macOS 또는 Linux): REA 설정이 기존 설치를 찾거나 설치를 제안합니다. macOS에서는 첫 " -"분석 전에 Hopper를 한 번 열어 데모 모드를 선택하거나 라이선스를 활성화하세요." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4875,7 +4862,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -4889,14 +4876,14 @@ msgstr "절차는 {ph0}네이티브 분석 설정{ph1}을 참고하세요." #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}코딩 에이전트" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "복사" @@ -4907,7 +4894,7 @@ msgstr "복사" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4917,7 +4904,7 @@ msgstr "" "{ph0}›{ph1}{ph2}REA로 DX-Ball의 사운드 패닝 계산 방식을 찾아줘. 계산을 설명하고 코드도 " "보여줘.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -4927,7 +4914,7 @@ msgstr "" "예제 프롬프트입니다. 에이전트에게 로컬 DX-Ball 1.07 실행 파일의 경로를 알려주세요. REA에는 DX-Ball이 포함되어 있지 " "않으므로 보유한 복사본을 사용하거나, 가지고 있는 다른 작은 Windows 프로그램에 같은 종류의 질문을 해 보세요." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "파일의 전체 경로는 어떻게 얻나요?" @@ -4936,7 +4923,7 @@ msgstr "파일의 전체 경로는 어떻게 얻나요?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4950,7 +4937,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4962,7 +4949,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4970,30 +4957,30 @@ msgid "" "appears." msgstr "{ph0}모든 시스템:{ph1} 파일을 터미널 창으로 드래그하면 해당 경로가 나타납니다." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "REA 결과 읽기" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "입력 읽기" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "명령어는 정수 인수를 읽어 부동소수점 연산용으로 변환합니다. 첫 의사 코드에서 빠졌던 입력이 바로 이 값입니다." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "호출 함수 추적" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5003,12 +4990,12 @@ msgstr "" "REA가 벽돌 충돌을 처리하는 호출 함수를 식별합니다. 앞선 명령어에서 타일 좌표에 30을 곱하고 {ph0}(20)을 더해 벽돌의 화면 " "위치를 구합니다." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "참조된 상수 읽기" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5016,12 +5003,12 @@ msgid "" msgstr "" "연산은 이 주소들을 참조합니다. 바이트를 읽으면 실제 값을 알 수 있고, 명령어 순서는 그 값을 어떻게 사용하는지 알려줍니다." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "복구한 함수 작성 및 검증" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5031,7 +5018,7 @@ msgstr "" "배율이 1.0이면 화면 위치 0, 320, 640은 정수 변환 전에 각각 −500, 0, 500이 됩니다. 화면 중앙이 스테레오 음장의 " "중앙에 대응하는 계산입니다." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5043,20 +5030,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}전체 조사와 어셈블리·C 비교 보기{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "직접 고른 바이너리로 시도하기" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5068,20 +5055,20 @@ msgstr "" "{ph0}네이티브 분석 도구를 연결{ph1}한 뒤, 로컬 실행 파일이나 라이브러리와 조사할 기능을 에이전트에게 전달하세요. “이 파일은 " "어떻게 파싱해?” 또는 “이 메뉴 동작은 어떤 함수가 처리해?” 같은 질문으로 시작할 수 있습니다." -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "함수 이름이나 주소를 이미 안다면 터미널에서 조회할 수 있습니다. Ghidra가 설정된 환경의 예:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "함수 분석" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5093,7 +5080,7 @@ msgstr "" "대상 경로와 {ph0}를 실제 함수 이름이나 주소로 바꾸세요. CLI는 호출할 때마다 바이너리를 가져와 분석한 뒤 세션을 닫습니다. " "에이전트의 MCP 질의는 세션이 열려 있는 동안 가져온 바이너리를 재사용합니다." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5122,17 +5109,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "기계 코드 프로그램 분석" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "예제 프롬프트 복사" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "함수 분석 명령 복사" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "맨 위로" @@ -6070,7 +6057,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}코딩 에이전트" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "복사" @@ -6461,21 +6448,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "APK 직접 살펴보기" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"macOS나 Linux에서 {ph0}REA를 설정{ph1}한 뒤 아래 두 파일을 같은 폴더에 내려받으세요. Windows에서는 WSL " -"같은 Linux 환경을 사용하세요." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6485,18 +6460,18 @@ msgstr "" "Android 분석에는 전체 JDK 17 이상이 필요합니다. {ph0}이나 {ph1}로 사용할 JDK를 지정하세요. 이 사례에서는 JDK" " 21을 사용했습니다." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "REA가 사용할 분석 도구 파일의 경로를 지정한 뒤 APK를 살펴보세요:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6508,7 +6483,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6516,7 +6491,7 @@ msgid "" "different time interval?{ph1}" msgstr "다음 질문: {ph0}계정이 8자리 코드나 다른 시간 간격을 사용하면 어떻게 될까요?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "출처" @@ -6532,7 +6507,7 @@ msgstr "출처" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6546,13 +6521,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC 6238 기준 사례{ph1} · {ph2}데모 테스트 데이터{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6595,12 +6570,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "로그인 코드 계산 따라가기" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "APK 분석 명령 복사" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "맨 위로" @@ -10178,13 +10153,13 @@ msgstr "가이드" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10201,13 +10176,13 @@ msgstr "소스 코드" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10224,13 +10199,13 @@ msgstr "MIT 라이선스" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10247,13 +10222,13 @@ msgstr "커뮤니티 참여" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10338,22 +10313,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA 결과 · summary.ipc 발췌" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA 명령어 발췌 · 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA 호출 함수 발췌 · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA 바이트 결과 · 리틀 엔디언 double 값으로 해석" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "복구된 C · 발췌" @@ -10414,12 +10389,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA · generateOTP, 함수 본문 전체" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Aegis APK 다운로드" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Android 분석 도구 다운로드" @@ -12861,11 +12836,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "사용 불가" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "지원 배포판" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -12901,16 +12871,7 @@ msgstr "" "설정에서 Hopper를 선택하고 별도로 제시되는 설치 계획을 확인하세요. macOS에서는 Hopper를 처음 열 때 데모 모드를 " "선택하거나 라이선스를 활성화하세요. Linux에서는 설정 과정에서 지원되는 데모와 전용 디스플레이를 준비합니다." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linux 호스트: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, " -"Omarchy. Hopper는 상용 소프트웨어이며 데모에는 공급업체가 정한 제한이 있습니다." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -12923,18 +12884,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopper 설정과 문제 해결{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · 무료 분석 도구와 Java 설치하기" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -12942,7 +12903,7 @@ msgid "" msgstr "Java 컴파일러를 포함하는 64비트 JDK 21 이상을 설치하세요. 컴퓨터 아키텍처에 맞는 패키지를 선택하세요." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12955,7 +12916,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12969,7 +12930,7 @@ msgstr "압축을 푼 폴더 중 {ph0} 또는 {ph1}가 들어 있는 폴더를 #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -12977,7 +12938,7 @@ msgstr "{ph0}JDK 다운로드{ph1} · {ph2}Ghidra 12.1.4 다운로드{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12987,7 +12948,7 @@ msgstr "" "Java를 확인하세요. 첫 번째 명령어가 출력하는 {ph0} 폴더를 {ph1}로 지정하세요. 두 번째 명령어로 컴파일러가 설치되어 있는지" " 확인합니다." -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13004,7 +12965,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13012,12 +12973,12 @@ msgid "" "guide{ph3}" msgstr "{ph0}Ghidra 설정과 아키텍처 요구사항{ph1} · {ph2}Windows Ghidra 가이드{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · 기존 설치 연결하기" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13027,7 +12988,7 @@ msgstr "" "IDA 분석 도구 가이드를 따라 연동할 MCP 서버의 등록 설정을 준비하세요. 원본 바이너리를 연 GUI에 연결하거나, 전용 " "데이터베이스를 여는 헤드리스 감독 프로세스를 선택하세요." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13037,7 +12998,7 @@ msgstr "" "검증된 구성에는 Windows의 레거시 GUI와 헤드리스 감독 프로세스, IDA Professional 9.1을 사용하는 macOS " "arm64 헤드리스 감독 프로세스가 포함됩니다. 선택 사항인 네이티브 GUI 브리지에는 별도의 버전 및 플랫폼 요구사항이 있습니다." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13047,13 +13008,13 @@ msgstr "분석 도구 가이드에서 준비한 JSON 등록 파일을 사용한 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}IDA 구성, 등록 예제, 검증된 플랫폼{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13062,128 +13023,128 @@ msgstr "연결한 뒤 원본 실행 파일의 전체 경로와 사용할 분석 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}네이티브 분석 예제 따라 하기{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "웹사이트, Android, .NET, 펌웨어 등 다른 대상" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "분석할 대상에 맞는 가이드를 여세요. 각 가이드에서 추가 도구, 지원 호스트, 첫 명령어를 안내합니다." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "대상" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "준비할 도구" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "가이드" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}가이드 열기{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "웹사이트" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / 패키지" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android 기기" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Android 리소스" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEB 프로젝트" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "펌웨어" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linux 충돌" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "논리 신호" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "프로세스 동작" @@ -13199,7 +13160,7 @@ msgstr "프로세스 동작" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13209,18 +13170,18 @@ msgstr "" "{ph0}Flutter 가이드{ph1} · {ph2}패키지와 리소스{ph3} · {ph4}EVM 바이트코드{ph5} · {ph6}모든 " "조사 가이드{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "터미널에서 REA 사용하기" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLI 명령어, 전역 설치, curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13228,14 +13189,14 @@ msgid "" "list. Ghidra must be configured first." msgstr "{ph0}을 대상의 함수 목록에 있는 함수 이름이나 주소로 바꾸세요. 먼저 Ghidra를 설정해야 합니다." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " "Node.js:" msgstr "macOS나 Linux에서는 Node.js를 준비한 뒤 curl 설치 프로그램을 사용할 수도 있습니다." -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -13244,12 +13205,12 @@ msgstr "" "3단계의 클라이언트 검사에 사용하는 설정 ID를 지정하세요. 편집기 클라이언트에서는 MCP 설정에서 REA를 활성화하고 에이전트 채팅에서" " 도구 사용을 허용하세요." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL, 공백이 포함된 경로" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -13260,17 +13221,17 @@ msgstr "" "Linux 환경 안에 Node.js와 REA를 설치하고 Linux 경로를 사용하세요. 헤드리스 분석 도구는 REA와 같은 호스트에서 " "실행됩니다." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Windows 설정 명령어:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "REA 업데이트 또는 제거하기" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -13279,7 +13240,7 @@ msgstr "에이전트 연결을 갱신하려면 최신 설정 명령어를 다시 #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13291,7 +13252,7 @@ msgstr "전역 CLI를 설치했다면 {ph0}로 업데이트하세요. REA의 클 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -13314,18 +13275,18 @@ msgstr "Windows REA 설정 명령어 복사" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "명령어 복사" @@ -13381,14 +13342,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · AI 모델 선택하기" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "IDA 등록 파일 준비하기" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13402,7 +13363,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13413,7 +13374,7 @@ msgstr "{ph0}, {ph1}, {ph2}를 제공하는 헤드리스 감독 프로세스에 #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13424,12 +13385,12 @@ msgstr "" "연결된 레거시 1.4.0 GUI 서버의 경우 동작이 확인된 {ph0}, {ph1}, 선택 사항인 {ph2}를 파일에 복사하세요. REA에" " 조사를 요청하기 전에 IDA에서 원본 바이너리를 열고 연동할 플러그인을 시작하세요." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLite 데이터베이스" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVM 바이트코드" @@ -13482,7 +13443,7 @@ msgid "" "in step 3{ph1}." msgstr "계획을 확인하고 승인한 뒤 Claude를 다시 열고 {ph0}3단계에서 연결을 확인{ph1}하세요." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -13492,12 +13453,12 @@ msgstr "" "두 경로를 기존 설치 경로로 바꾸세요. macOS나 Linux arm64에 새로 설치했다면 먼저 컴퓨터에 맞는 네이티브 디컴파일러를 " "빌드하세요." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Ghidra의 네이티브 디컴파일러 빌드하기" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -13510,7 +13471,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -13520,7 +13481,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -13530,13 +13491,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · 네이티브 빌드 안내{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "그런 다음 Ghidra를 검사하고 클라이언트용으로 경로를 저장하세요." @@ -13900,7 +13861,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "다른 설치 방법: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13913,7 +13874,7 @@ msgstr "설정 안내에 따라 에이전트를 연결하세요. 안내가 나 #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13927,7 +13888,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13937,7 +13898,7 @@ msgstr "빌드한 소스 디렉터리에서 아래 {ph2} 명령 대신 {ph0}와 #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13947,7 +13908,7 @@ msgstr "" "셸에서 {ph0}를 찾지 못하면 설치 프로그램이 출력한 실행 파일 경로를 사용하세요. Linux의 기본 설정 명령은 {ph1}입니다." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13965,13 +13926,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -14842,7 +14803,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -14851,6 +14812,98 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1}(macOS 또는 Linux): REA 설정이 기존 설치를 찾거나 설치를 제안합니다. macOS에서는 첫 " +#~ "분석 전에 Hopper를 한 번 열어 데모 모드를 선택하거나 라이선스를 활성화하세요." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "macOS나 Linux에서 {ph0}REA를 설정{ph1}한 뒤 아래 두 파일을 같은 폴더에 내려받으세요. Windows에서는 WSL " +#~ "같은 Linux 환경을 사용하세요." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "지원 배포판" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linux 호스트: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, " +#~ "Omarchy. Hopper는 상용 소프트웨어이며 데모에는 공급업체가 정한 제한이 있습니다." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/messages.pot b/website/locales/messages.pot index ac20dacd50..2117345285 100644 --- a/website/locales/messages.pot +++ b/website/locales/messages.pot @@ -168,12 +168,12 @@ msgstr "" #: website/public/contributors/index.html:1131 #: website/public/examples/dino-lab/index.html:475 website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 website/public/index.html:1303 -#: website/public/showcase/aegis/index.html:808 +#: website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 website/public/showcase/index.html:337 #: website/public/showcase/notion/index.html:631 @@ -188,12 +188,12 @@ msgstr "" #: website/public/contributors/index.html:1132 #: website/public/examples/dino-lab/index.html:476 website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 website/public/index.html:1304 -#: website/public/showcase/aegis/index.html:809 +#: website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 website/public/showcase/index.html:338 #: website/public/showcase/notion/index.html:632 @@ -208,12 +208,12 @@ msgstr "" #: website/public/contributors/index.html:1133 #: website/public/examples/dino-lab/index.html:477 website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 website/public/index.html:1305 -#: website/public/showcase/aegis/index.html:810 +#: website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 website/public/showcase/index.html:339 #: website/public/showcase/notion/index.html:633 @@ -228,12 +228,12 @@ msgstr "" #: website/public/contributors/index.html:1134 #: website/public/examples/dino-lab/index.html:478 website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 website/public/index.html:1306 -#: website/public/showcase/aegis/index.html:811 +#: website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 website/public/showcase/index.html:340 #: website/public/showcase/notion/index.html:634 @@ -248,12 +248,12 @@ msgstr "" #: website/public/contributors/index.html:1135 #: website/public/examples/dino-lab/index.html:479 website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 website/public/index.html:1307 -#: website/public/showcase/aegis/index.html:812 +#: website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 website/public/showcase/index.html:341 #: website/public/showcase/notion/index.html:635 @@ -270,12 +270,12 @@ msgstr "" #: website/public/contributors/index.html:1140 #: website/public/examples/dino-lab/index.html:484 website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 website/public/index.html:1312 -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 website/public/showcase/index.html:346 #: website/public/showcase/notion/index.html:640 @@ -4337,14 +4337,14 @@ msgstr "" #: website/public/get-started/index.html:777 website/public/get-started/index.html:807 #: website/public/get-started/index.html:832 website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "" @@ -4947,7 +4947,7 @@ msgid "" "{ph0}Any system:{ph1} drag the folder into a terminal window and its path appears." msgstr "" -#: website/public/get-started/index.html:1006 website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1006 website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" @@ -4988,7 +4988,7 @@ msgstr "" #: website/public/get-started/index.html:1042 msgctxt "page:get-started/index.html" -msgid "Supported distributions" +msgid "x86_64 installer on supported distributions" msgstr "" #: website/public/get-started/index.html:1046 @@ -5034,11 +5034,13 @@ msgstr "" #: website/public/get-started/index.html:1072 msgctxt "page:get-started/index.html" msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. " -"Hopper is commercial software; its demo has vendor-defined limits." +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora 41+, " +"Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher can also be " +"configured on other supported Linux distributions. On Linux arm64, use Ghidra. Hopper " +"is commercial software; its demo has vendor-defined limits." msgstr "" -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in System " @@ -5048,18 +5050,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the package " @@ -5067,7 +5069,7 @@ msgid "" msgstr "" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5078,7 +5080,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Find the extracted folder containing {ph0} or {ph1}. Use that folder for {ph2}." @@ -5090,7 +5092,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -5098,7 +5100,7 @@ msgstr "" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5106,12 +5108,12 @@ msgid "" "confirms the compiler is installed:" msgstr "" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE applications" @@ -5119,29 +5121,29 @@ msgid "" " 64-bit JDK 21 or newer:" msgstr "" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "Replace the two paths with your installations, then review setup's plan." msgstr "" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or Linux arm64 " "installation, build the native decompiler for your computer before continuing." msgstr "" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the guide for " @@ -5152,7 +5154,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing-macos- #. native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -5161,7 +5163,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -5171,18 +5173,18 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting the paths," @@ -5194,19 +5196,19 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" "{ph0}Ghidra setup and architecture requirements{ph1} · {ph2}Windows Ghidra guide{ph3}" msgstr "" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose an attached" @@ -5214,7 +5216,7 @@ msgid "" "database." msgstr "" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, plus a macOS" @@ -5226,7 +5228,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5237,21 +5239,21 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" "From the built checkout, run {ph0} and {ph1} instead of the {ph2} commands below." msgstr "" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5263,7 +5265,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5274,7 +5276,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5283,7 +5285,7 @@ msgid "" "before asking REA to inspect it." msgstr "" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then rerun setup " @@ -5292,13 +5294,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name your " @@ -5306,7 +5308,7 @@ msgid "" msgstr "" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5316,130 +5318,130 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional tools, " "supported hosts and first command." msgstr "" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr "" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "" @@ -5453,7 +5455,7 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5461,31 +5463,31 @@ msgid "" "{ph6}All investigation guides{ph7}" msgstr "" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without running " "the app:" msgstr "" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5493,13 +5495,13 @@ msgid "" "such as {ph1} works here." msgstr "" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -5509,7 +5511,7 @@ msgstr "" #. {ph1}: #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -5520,7 +5522,7 @@ msgstr "" #. {ph2}: /Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5531,20 +5533,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives their " @@ -5552,27 +5554,27 @@ msgid "" msgstr "" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5580,13 +5582,13 @@ msgid "" "must be configured first." msgstr "" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "On macOS or Linux, you can also use the curl installer after preparing Node.js:" msgstr "" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Follow the setup prompts to connect your agent. If they don't appear, run {ph0}." @@ -5594,7 +5596,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5602,18 +5604,18 @@ msgid "" "Linux, the default setup command is {ph1}." msgstr "" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5621,39 +5623,39 @@ msgid "" "Codex:" msgstr "" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable REA in " "MCP settings and allow its tools in the agent chat." msgstr "" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent runs " @@ -5661,17 +5663,17 @@ msgid "" "there. Headless providers run on the same host as REA." msgstr "" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review its plan " @@ -5680,7 +5682,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5692,7 +5694,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose-update- #. and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -5702,7 +5704,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}REA FAQ{ph1}{ph2}Quick answers on updates, agents and analysis.{ph3}" @@ -5712,7 +5714,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5724,7 +5726,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5735,7 +5737,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}REA community{ph1}{ph2}Questions, investigations and work in progress.{ph3}" @@ -5794,12 +5796,12 @@ msgstr "" #: website/public/get-started/index.html:582 website/public/get-started/index.html:611 #: website/public/get-started/index.html:777 website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "" @@ -5814,22 +5816,22 @@ msgctxt "page:get-started/index.html" msgid "Copy Notes investigation prompt" msgstr "" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "" @@ -7201,12 +7203,12 @@ msgctxt "page:guides/native/index.html" msgid "" "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to install it. On " "macOS, open Hopper once and choose demo mode or activate a license before your first " -"analysis." +"analysis. Automated Linux setup requires x86_64; use Ghidra on Linux arm64." msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7216,7 +7218,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7227,7 +7229,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -7241,14 +7243,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "" @@ -7259,7 +7261,7 @@ msgstr "" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7267,7 +7269,7 @@ msgid "" "calculation and show the code.{ph3}{ph4}{ph5}" msgstr "" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 executable. REA " @@ -7275,7 +7277,7 @@ msgid "" "another small Windows program you have." msgstr "" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "" @@ -7284,7 +7286,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7296,7 +7298,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7306,46 +7308,46 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}Any system:{ph1} drag the file into a terminal window and its path appears." msgstr "" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7353,34 +7355,34 @@ msgid "" "coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the actual " "values; the instruction order tells the agent how they are used." msgstr "" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 before " @@ -7388,7 +7390,7 @@ msgid "" " stereo field." msgstr "" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205 inputs " @@ -7398,20 +7400,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7420,20 +7422,20 @@ msgid "" "this file parsed?” or “Which function handles this menu action?”" msgstr "" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the terminal. " "For a configured Ghidra installation:" msgstr "" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7442,7 +7444,7 @@ msgid "" "queries reuse the imported binary while that session is open." msgstr "" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result leaves a call" @@ -7471,17 +7473,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "" @@ -9463,7 +9465,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "" @@ -9872,17 +9874,21 @@ msgstr "" #. {ph0}: #. {ph1}: +#. {ph2}: +#. {ph3}: #: website/public/showcase/aegis/index.html:714 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into the same " -"folder. On Windows, use a Linux environment such as WSL." +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On Windows " +"x64, use a package with matching Windows native controls and owned protocol stdin " +"support; see the {ph2}Android provider guide{ph3} for prerequisites and PowerShell " +"setup. A Linux environment such as WSL is another option." msgstr "" #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9890,28 +9896,28 @@ msgid "" "case used JDK 21." msgstr "" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9921,7 +9927,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9929,7 +9935,7 @@ msgid "" "time interval?{ph1}" msgstr "" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "" @@ -9944,7 +9950,7 @@ msgstr "" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9956,13 +9962,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited to Aegis;" @@ -10001,12 +10007,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "" diff --git a/website/locales/ml/messages.po b/website/locales/ml/messages.po index a20e70fa3f..8fe64ae7e9 100644 --- a/website/locales/ml/messages.po +++ b/website/locales/ml/messages.po @@ -111,13 +111,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -205,13 +205,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -228,13 +228,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -251,13 +251,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -274,13 +274,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -299,13 +299,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -4465,21 +4465,21 @@ msgstr "" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "" @@ -5074,7 +5074,7 @@ msgid "" msgstr "" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" @@ -5113,11 +5113,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -5158,14 +5153,7 @@ msgid "" " setup prepares the supported demo and its private display." msgstr "" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -5176,18 +5164,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -5195,7 +5183,7 @@ msgid "" msgstr "" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5206,7 +5194,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5220,7 +5208,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -5228,7 +5216,7 @@ msgstr "" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5236,12 +5224,12 @@ msgid "" "second confirms the compiler is installed:" msgstr "" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -5249,18 +5237,18 @@ msgid "" "12.1.x installation and a 64-bit JDK 21 or newer:" msgstr "" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." msgstr "" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -5268,12 +5256,12 @@ msgid "" "before continuing." msgstr "" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -5284,7 +5272,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -5294,7 +5282,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -5304,18 +5292,18 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -5330,7 +5318,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5338,12 +5326,12 @@ msgid "" "guide{ph3}" msgstr "" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -5351,7 +5339,7 @@ msgid "" " opens a private database." msgstr "" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -5364,7 +5352,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5376,7 +5364,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5384,14 +5372,14 @@ msgid "" "below." msgstr "" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5403,7 +5391,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5414,7 +5402,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5423,7 +5411,7 @@ msgid "" "upstream plugin before asking REA to inspect it." msgstr "" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -5433,13 +5421,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -5447,7 +5435,7 @@ msgid "" msgstr "" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5458,138 +5446,138 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr "" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "" @@ -5605,7 +5593,7 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5613,31 +5601,31 @@ msgid "" "bytecode{ph5} · {ph6}All investigation guides{ph7}" msgstr "" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5645,13 +5633,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -5662,7 +5650,7 @@ msgstr "" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -5674,7 +5662,7 @@ msgstr "" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5685,20 +5673,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -5707,27 +5695,27 @@ msgid "" msgstr "" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5735,7 +5723,7 @@ msgid "" "list. Ghidra must be configured first." msgstr "" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -5743,7 +5731,7 @@ msgid "" msgstr "" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5753,7 +5741,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5761,18 +5749,18 @@ msgid "" "installer. On Linux, the default setup command is {ph1}." msgstr "" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5780,39 +5768,39 @@ msgid "" "{ph0}; for Codex:" msgstr "" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" " REA in MCP settings and allow its tools in the agent chat." msgstr "" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -5820,17 +5808,17 @@ msgid "" "use Linux paths there. Headless providers run on the same host as REA." msgstr "" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -5839,7 +5827,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5851,7 +5839,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -5861,7 +5849,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5872,7 +5860,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5884,7 +5872,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5896,7 +5884,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5963,18 +5951,18 @@ msgstr "" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "" @@ -5989,22 +5977,22 @@ msgctxt "page:get-started/index.html" msgid "Copy Notes investigation prompt" msgstr "" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "" @@ -7337,18 +7325,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7358,7 +7335,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -7372,14 +7349,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "" @@ -7390,7 +7367,7 @@ msgstr "" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7398,7 +7375,7 @@ msgid "" "Explain the calculation and show the code.{ph3}{ph4}{ph5}" msgstr "" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -7406,7 +7383,7 @@ msgid "" "same kind of question about another small Windows program you have." msgstr "" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "" @@ -7415,7 +7392,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7427,7 +7404,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7437,7 +7414,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7445,40 +7422,40 @@ msgid "" "appears." msgstr "" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7486,34 +7463,34 @@ msgid "" "tile coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -7521,7 +7498,7 @@ msgid "" "the center of the stereo field." msgstr "" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -7531,20 +7508,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7554,20 +7531,20 @@ msgid "" " menu action?”" msgstr "" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7577,7 +7554,7 @@ msgid "" "session is open." msgstr "" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -7606,17 +7583,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "" @@ -9025,7 +9002,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "" @@ -9437,19 +9414,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9457,28 +9424,28 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9488,7 +9455,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9496,7 +9463,7 @@ msgid "" "different time interval?{ph1}" msgstr "" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "" @@ -9512,7 +9479,7 @@ msgstr "" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9524,13 +9491,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -9570,12 +9537,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "" @@ -13930,7 +13897,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -13939,6 +13906,92 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" + #, python-brace-format #~ msgctxt "page:get-started/index.html" #~ msgid "" diff --git a/website/locales/pl/messages.po b/website/locales/pl/messages.po index 4bbd08861e..2f7089dc9b 100644 --- a/website/locales/pl/messages.po +++ b/website/locales/pl/messages.po @@ -71,13 +71,13 @@ msgstr "Przez N0zoM1z0 * 10 października 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3270,21 +3270,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Kopiuj" @@ -3515,12 +3515,12 @@ msgstr "" " jego ścieżka." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "W terminalu umieść cudzysłowy wokół ścieżki zawierającej spacje." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3529,14 +3529,14 @@ msgstr "" "Analizuj folder aplikacji JavaScript lub Electron lub archiwum ASAR bez " "uruchamiania aplikacji:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Statyczna analiza aplikacji" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3546,13 +3546,13 @@ msgstr "" "Zastąp ścieżkę wyodrębnionym katalogiem aplikacji lub {ph0} akta. Na Windows" " ścieżka taka jak {ph1} działa tutaj." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Gdzie znajdę aplikację .plik asar?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3563,7 +3563,7 @@ msgstr "Aplikacje Electron zwykle przechowują go w {ph0} folder:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3575,7 +3575,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3588,14 +3588,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} często {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3603,7 +3603,7 @@ msgstr "" "Niektóre aplikacje instalują się gdzie indziej. Wyszukaj w folderze " "aplikacji {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3615,37 +3615,37 @@ msgstr "" "wyniku." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Jeśli chcesz mieć stałe {ph0} polecenie w Twojej powłoce:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Zainstaluj CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "" "{ph0}Kontynuuj korzystanie z Przewodnika po aplikacjach JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Podłącz instalację Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Połącz Ghidra na Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3656,7 +3656,7 @@ msgstr "" "aplikacji x86 i x64 PE oraz bibliotek DLL na stałych lokalnych dyskach NTFS." " Użyj istniejącego Ghidra 12.1.instalacja x i 64-bitowy JDK 21 lub nowszy:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3672,18 +3672,18 @@ msgstr "" "Podłącz Hopper na macOS lub Linux . Uruchom powyższe polecenie konfiguracji;" " wykrywa istniejącą instalację lub oferuje plan instalacji do zatwierdzenia." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Uzyskiwanie pomocy" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Jeśli twój agent nie widzi REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3693,22 +3693,22 @@ msgstr "" "Uruchom ponownie lub podłącz ponownie Klienta po konfiguracji. Sprawdź " "połączenie REA z {ph0}; dla Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Jeśli dostawca wymaga uwagi" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Sprawdź dostawcę, którego używasz. Dla Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Sprawdź jednego dostawcę" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "Raport zawiera nieudaną kontrolę i sugerowany następny krok." @@ -3717,7 +3717,7 @@ msgstr "Raport zawiera nieudaną kontrolę i sugerowany następny krok." #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3730,7 +3730,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3744,7 +3744,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3758,7 +3758,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3823,22 +3823,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Skopiuj monit instalacji REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Skopiuj polecenie analizy JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Skopiuj polecenie instalacji CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Kopiuj polecenie diagnozy dostawcy" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Do góry" @@ -5191,21 +5191,7 @@ msgstr "REA potrzebuje jednego z nich do odczytu programów natywnych:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS lub Linux ): konfiguracja REA wykrywa ją lub " -"oferuje jej instalację. Na macOS otwórz Hopper raz i wybierz tryb demo lub " -"Aktywuj licencję przed pierwszą analizą." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5218,7 +5204,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5234,14 +5220,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Twój agent kodowania" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Kopiuj" @@ -5252,7 +5238,7 @@ msgstr "Kopiuj" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5263,7 +5249,7 @@ msgstr "" "pozycjonowanie dźwięku stereo. Wyjaśnij obliczenia i pokaż " "kod.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5275,7 +5261,7 @@ msgstr "" "kopii lub zadaj to samo pytanie dotyczące innego małego programu Windows, " "który masz." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Jak uzyskać pełną ścieżkę pliku?" @@ -5284,7 +5270,7 @@ msgstr "Jak uzyskać pełną ścieżkę pliku?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5299,7 +5285,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5311,7 +5297,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5321,17 +5307,17 @@ msgstr "" "{ph0}Dowolny system:{ph1} przeciągnij plik do okna terminala, a pojawi się " "jego ścieżka." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Przeczytaj wyniki REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Przeczytaj dane wejściowe" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5341,13 +5327,13 @@ msgstr "" "zmiennoprzecinkową. Jest to dane wejściowe nieobecne w pierwszym widoku " "pseudokodu." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Podążaj za dzwoniącym" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5358,12 +5344,12 @@ msgstr "" "współrzędną płytki przez 30; dodawanie {ph0} (20) podaje położenie ekranu " "Cegły." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Przeczytaj przywoływane stałe" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5373,12 +5359,12 @@ msgstr "" "rzeczywistych wartości; kolejność instrukcji informuje agenta, w jaki sposób" " są używane." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Napisz i sprawdź odzyskaną funkcję" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5389,7 +5375,7 @@ msgstr "" "konwersją liczb całkowitych. Obliczenia umieszczają środek ekranu na środku " "pola stereo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5402,20 +5388,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Zobacz pełne porównanie badań i montażu do C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Spróbuj z własnym binarnym" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5429,7 +5415,7 @@ msgstr "" "Przydatne pytania początkowe obejmują \"Jak jest analizowany ten " "plik?\"lub\" która funkcja obsługuje tę akcję menu?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5438,13 +5424,13 @@ msgstr "" "Jeśli znasz już nazwę lub adres funkcji, możesz wysłać zapytanie z " "terminala. Dla skonfigurowanej instalacji Ghidra:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Analiza funkcji" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5458,7 +5444,7 @@ msgstr "" "sesję. Zapytania MCP twojego agenta ponownie wykorzystują zaimportowany plik" " binarny, gdy ta sesja jest otwarta." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5492,17 +5478,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Analiza programów kodu maszynowego" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Skopiuj przykładowy monit" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Analiza funkcji kopiowania" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Do góry" @@ -6480,7 +6466,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Twój agent kodowania" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Kopiuj" @@ -6888,21 +6874,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Sprawdź sam APK" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Skonfiguruj REA {ph1} na macOS lub Linux pobierz te dwa pliki do tego " -"samego folderu. W Windows Użyj środowiska Linux, takiego jak WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6912,18 +6886,18 @@ msgstr "" "Analiza Androida wymaga pełnego JDK 17 lub nowszego. Wybierz go za pomocą " "{ph0} lub {ph1}. W tym przypadku zastosowano JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Wskaż REA w pliku dostawcy i sprawdź APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6935,7 +6909,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6945,7 +6919,7 @@ msgstr "" "Następne pytanie: {ph0}co się dzieje, gdy konto używa ośmiu cyfr lub innego " "przedziału czasu?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Źródła" @@ -6961,7 +6935,7 @@ msgstr "Źródła" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6975,7 +6949,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -6983,7 +6957,7 @@ msgstr "" "{ph0}RFC 6238 przypadki referencyjne{ph1} · {ph2}Dane z testów " "demonstracyjnych{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7028,12 +7002,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Postępuj zgodnie z obliczeniem kodu logowania" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Skopiuj polecenie kontroli APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Do góry" @@ -10820,13 +10794,13 @@ msgstr "Poradniki" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10843,13 +10817,13 @@ msgstr "Kod źródłowy" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10866,13 +10840,13 @@ msgstr "Licencja MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10889,13 +10863,13 @@ msgstr "Dołącz do społeczności" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10980,22 +10954,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA wynik * podsumowanie.fragment ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA Instrukcja fragment * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA fragment dzwoniącego * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Wyniki bajtów REA * interpretowane jako podwaja się little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Odzyskany fragment C ·" @@ -11056,12 +11030,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, kompletne ciało" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Pobierz Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Pobierz Android analysis provider" @@ -13745,11 +13719,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Niedostępne" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Obsługiwane dystrybucje" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13787,17 +13756,7 @@ msgstr "" "demonstracyjny lub aktywuj licencję. W Linux konfigurator przygotowuje " "obsługiwaną wersję demonstracyjną i osobny ekran dla niej." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Hosty Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS lub " -"Omarchy. Hopper to oprogramowanie komercyjne; ograniczenia wersji " -"demonstracyjnej określa producent." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13811,18 +13770,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Konfiguracja Hopper i rozwiązywanie problemów{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · zainstaluj bezpłatne narzędzie analizy i Javę" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13832,7 +13791,7 @@ msgstr "" " pakiet pasujący do architektury Twojego komputera." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13845,7 +13804,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13860,7 +13819,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13868,7 +13827,7 @@ msgstr "{ph0}Pobierz JDK{ph1} · {ph2}Pobierz Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13878,7 +13837,7 @@ msgstr "" "Sprawdź Javę. Pierwsza komenda wyświetla {ph0}; użyj tego folderu jako " "{ph1}. Druga potwierdza, że kompilator jest zainstalowany:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13896,7 +13855,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13906,12 +13865,12 @@ msgstr "" "{ph0}Konfiguracja Ghidra i wymagania architektury{ph1} · {ph2}Przewodnik po " "Ghidra w Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · połącz istniejącą instalację" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13923,7 +13882,7 @@ msgstr "" "jest oryginalny plik binarny, lub z nadzorcą bez interfejsu graficznego, " "który otwiera osobną bazę danych." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13935,7 +13894,7 @@ msgstr "" "macOS arm64 z IDA Professional 9.1. Opcjonalny natywny most do GUI ma własne" " wymagania dotyczące wersji i platformy." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13947,14 +13906,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Profile IDA, przykłady rejestracji i zweryfikowane platformy{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13965,18 +13924,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Przejdź przez przykład analizy kodu maszynowego{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Witryny, Android, .NET, firmware i inne cele" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13985,110 +13944,110 @@ msgstr "" "Otwórz przewodnik dla swojego celu. Każdy podaje dodatkowe narzędzia, " "obsługiwane hosty i pierwszą komendę." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Cel" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Narzędzia do przygotowania" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Przewodnik" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Otwórz przewodnik{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Witryna" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / pakiety" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Urządzenie Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Zasoby Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Projekt JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / awaria Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Sygnały logiczne" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Zachowanie procesu" @@ -14104,7 +14063,7 @@ msgstr "Zachowanie procesu" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14114,18 +14073,18 @@ msgstr "" "{ph0}Przewodnik po Flutter{ph1} · {ph2}Pakiety i zasoby{ph3} · {ph4}Bajtkod " "EVM{ph5} · {ph6}Wszystkie przewodniki badania{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Używaj REA z terminala" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Komendy CLI, instalacja globalna i curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14135,7 +14094,7 @@ msgstr "" "Zastąp {ph0} nazwą lub adresem funkcji z listy funkcji celu. Najpierw trzeba" " skonfigurować Ghidra." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14144,7 +14103,7 @@ msgstr "" "W macOS lub Linux po przygotowaniu Node.js możesz również użyć instalatora " "przez curl:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14154,12 +14113,12 @@ msgstr "" " W edytorach włącz REA w ustawieniach MCP i zezwól na jego narzędzia w " "czacie agenta." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL i ścieżki ze spacjami" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14171,17 +14130,17 @@ msgstr "" " w nim ścieżek Linux. Dostawcy bez interfejsu graficznego działają na tym " "samym hoście co REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Komenda konfiguracji dla Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Zaktualizuj lub usuń REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14192,7 +14151,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14206,7 +14165,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14229,18 +14188,18 @@ msgstr "Kopiuj komendę konfiguracji REA dla Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Kopiuj komendę" @@ -14296,14 +14255,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · wybierz model AI" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Przygotuj plik rejestracji IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14319,7 +14278,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14332,7 +14291,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14345,12 +14304,12 @@ msgstr "" " binarny w IDA i uruchom wtyczkę po stronie IDA, zanim poprosisz REA o jego " "zbadanie." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Baza danych SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bajtkod EVM" @@ -14409,7 +14368,7 @@ msgstr "" "Przejrzyj i zatwierdź plan, ponownie otwórz Claude, a następnie {ph0}sprawdź" " połączenie w kroku 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14420,12 +14379,12 @@ msgstr "" "instalacji w macOS lub Linux arm64 przed kontynuowaniem skompiluj natywny " "dekompilator dla swojego komputera." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Skompiluj natywny dekompilator Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14438,7 +14397,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14448,7 +14407,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14458,13 +14417,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · instrukcje natywnej kompilacji{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Następnie sprawdź Ghidra i zapisz jego ścieżki dla swojego klienta:" @@ -14866,7 +14825,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Inne sposoby instalacji: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14881,7 +14840,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14896,7 +14855,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14908,7 +14867,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14920,7 +14879,7 @@ msgstr "" "konfiguracji to {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14939,13 +14898,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15842,7 +15801,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15851,6 +15810,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS lub Linux ): konfiguracja REA wykrywa ją lub " +#~ "oferuje jej instalację. Na macOS otwórz Hopper raz i wybierz tryb demo lub " +#~ "Aktywuj licencję przed pierwszą analizą." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Skonfiguruj REA {ph1} na macOS lub Linux pobierz te dwa pliki do tego " +#~ "samego folderu. W Windows Użyj środowiska Linux, takiego jak WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Obsługiwane dystrybucje" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Hosty Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS lub " +#~ "Omarchy. Hopper to oprogramowanie komercyjne; ograniczenia wersji " +#~ "demonstracyjnej określa producent." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/pt-BR/messages.po b/website/locales/pt-BR/messages.po index 399b983d86..f11cfeba10 100644 --- a/website/locales/pt-BR/messages.po +++ b/website/locales/pt-BR/messages.po @@ -71,13 +71,13 @@ msgstr "Por N0zoM1z0 * 10 de outubro de 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3300,21 +3300,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Copiar" @@ -3546,13 +3546,13 @@ msgstr "" "seu caminho será exibido." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Em um terminal, coloque aspas em torno de um caminho que contém espaços." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3561,14 +3561,14 @@ msgstr "" "Analisar uma pasta de Aplicação JavaScript ou Electron ou um arquivo ASAR " "sem executar a aplicação:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Análise estática da aplicação" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3578,13 +3578,13 @@ msgstr "" "Substitua o caminho pelo diretório do aplicativo extraído ou {ph0} ficheiro." " Em Windows, um caminho como {ph1} trabalha aqui." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Onde encontro o arquivo .asar de um aplicativo?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3595,7 +3595,7 @@ msgstr "Os aplicativos Electron geralmente o mantêm em um {ph0} pasta:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3607,7 +3607,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3620,14 +3620,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} muitas vezes {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3635,7 +3635,7 @@ msgstr "" "Alguns aplicativos são instalados em outro lugar. Pesquise na pasta do " "aplicativo {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3647,36 +3647,36 @@ msgstr "" "resolvidas estão marcadas no resultado." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Se quiser ter o comando {ph0} sempre disponível no seu shell:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Instalar o CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Continue com o Guia de Aplicação JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Conecte sua instalação Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Ligar Ghidra em Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3687,7 +3687,7 @@ msgstr "" "DLLs nativos x86 e x64 PE em unidades NTFS locais fixas. Use uma instalação " "existente do Ghidra 12.1.x e um JDK de 64 bits, versão 21 ou posterior:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3705,18 +3705,18 @@ msgstr "" "detecta uma instalação existente ou oferece um plano de instalação para sua " "aprovação." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Obter ajuda" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Se o seu agente não vir REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3726,22 +3726,22 @@ msgstr "" "Reinicie ou volte a ligar o cliente após a configuração. Verificar a ligação" " REA com {ph0}; para Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Se um fornecedor precisar de atenção" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Verifique o fornecedor que está a utilizar. Para Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Verificar um fornecedor" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "O relatório apresenta a verificação falhada e sugere o próximo passo." @@ -3750,7 +3750,7 @@ msgstr "O relatório apresenta a verificação falhada e sugere o próximo passo #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3763,7 +3763,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3777,7 +3777,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3791,7 +3791,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3857,22 +3857,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Copiar o aviso de instalação do REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Copiar comando de análise JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Copiar o comando de instalação do CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Comando de diagnóstico do Fornecedor de cópia" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Voltar ao topo" @@ -5230,21 +5230,7 @@ msgstr "REA precisa de um destes para ler programas nativos:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS ou Linux): a configuração do REA detecta-o ou " -"oferece-se para instalá-lo. No macOS, abra o Hopper uma vez e escolha o modo" -" de demonstração ou ative uma licença antes da sua primeira análise." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5257,7 +5243,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5273,14 +5259,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}O seu agente de codificação" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Copiar" @@ -5291,7 +5277,7 @@ msgstr "Copiar" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5302,7 +5288,7 @@ msgstr "" "posicionamento do som estéreo. Explique o cálculo e mostre o " "código.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5313,7 +5299,7 @@ msgstr "" "Ball 1.07. REA não inclui DX-Ball, então use sua própria cópia ou Faça o " "mesmo tipo de pergunta sobre outro pequeno programa Windows que você possui." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Como obtenho o caminho completo de um ficheiro?" @@ -5322,7 +5308,7 @@ msgstr "Como obtenho o caminho completo de um ficheiro?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5338,7 +5324,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5351,7 +5337,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5361,17 +5347,17 @@ msgstr "" "{ph0}Qualquer sistema:{ph1} arraste o arquivo para uma janela de terminal e " "seu caminho será exibido." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Leia os resultados do REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Leia a entrada" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5381,13 +5367,13 @@ msgstr "" " ponto flutuante. Esta é a entrada ausente da primeira visualização de " "pseudocódigo." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Siga o chamador" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5398,12 +5384,12 @@ msgstr "" "multiplicam a coordenada do bloco por 30; adicionando {ph0} (20) indica a " "posição do ecrã do tijolo." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Leia as constantes referenciadas" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5413,12 +5399,12 @@ msgstr "" "os valores reais; a ordem de instrução informa ao agente como eles são " "usados." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Escrever e verificar a função recuperada" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5429,7 +5415,7 @@ msgstr "" "-500, 0 e 500 antes da conversão de inteiros. O cálculo coloca o centro da " "tela no centro do campo estéreo." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5442,21 +5428,21 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" "{ph0}Ver o inquérito completo e a comparação entre a montagem e a c{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Experimente com seu próprio binário" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5470,7 +5456,7 @@ msgstr "" " para investigar. Perguntas iniciais úteis incluem \"como este arquivo é " "analisado?\"ou\" qual função lida com esta ação do menu?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5479,13 +5465,13 @@ msgstr "" "Se já souber o nome ou endereço de uma função, pode consultá-la a partir do " "terminal. Para uma instalação Ghidra configurada:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Análise de funções" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5499,7 +5485,7 @@ msgstr "" "fecha sua sessão. As consultas MCP do seu agente reutilizam o binário " "importado enquanto essa sessão está aberta." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5533,17 +5519,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Análise de programas de código de máquina" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Copiar exemplo de pedido" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Análise da função de cópia" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Voltar ao topo" @@ -6525,7 +6511,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}O seu agente de codificação" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Copiar" @@ -6940,21 +6926,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Inspecione você mesmo o APK" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Configurar REA {ph1} em macOS ou Linux, faça o download desses dois " -"arquivos na mesma pasta. Em Windows, use um ambiente Linux, como WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6964,18 +6938,18 @@ msgstr "" "A análise do Android precisa de um JDK 17 completo ou posterior. Selecione-o" " com {ph0} ou {ph1}. Este caso utilizou o JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Aponte REA para o ficheiro do fornecedor e inspecione o APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6987,7 +6961,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6997,7 +6971,7 @@ msgstr "" "Uma próxima pergunta: {ph0}o que acontece quando uma conta utiliza oito " "dígitos ou um intervalo de tempo diferente?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Fontes" @@ -7013,7 +6987,7 @@ msgstr "Fontes" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7027,7 +7001,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -7035,7 +7009,7 @@ msgstr "" "{ph0}RFC 6238 casos de referência{ph1} · {ph2}Dados do teste de " "demonstração{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7080,12 +7054,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Siga o cálculo do código de login" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Copiar comando de inspecção APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Voltar ao topo" @@ -10896,13 +10870,13 @@ msgstr "Guias" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10919,13 +10893,13 @@ msgstr "Código-fonte" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10942,13 +10916,13 @@ msgstr "Licença MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10965,13 +10939,13 @@ msgstr "Participe da comunidade" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11056,22 +11030,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA resultado * resumo.excerto do ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "Trecho de instrução REA * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "Trecho do chamador REA * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "Resultados do byte REA * interpretados como duplos little-endian" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Excerto c * recuperado" @@ -11132,12 +11106,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * gerateotp, corpo completo" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Baixar Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Baixar Android analysis provider" @@ -13823,11 +13797,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Indisponível" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Distribuições compatíveis" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13865,17 +13834,7 @@ msgstr "" "abrir pela primeira vez. No Linux, a configuração prepara a demonstração " "compatível e sua tela privada." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Hosts Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS ou " -"Omarchy. O Hopper é um software comercial; sua demonstração tem limites " -"definidos pelo fabricante." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13889,18 +13848,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Configuração do Hopper e solução de problemas{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · instale a ferramenta de análise gratuita e o Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13910,7 +13869,7 @@ msgstr "" "Java. Escolha o pacote correspondente à arquitetura do seu computador." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13923,7 +13882,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13939,7 +13898,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13947,7 +13906,7 @@ msgstr "{ph0}Baixar um JDK{ph1} · {ph2}Baixar o Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13957,7 +13916,7 @@ msgstr "" "Verifique o Java. O primeiro comando mostra {ph0}; use essa pasta para " "{ph1}. O segundo confirma que o compilador está instalado:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13975,7 +13934,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13985,12 +13944,12 @@ msgstr "" "{ph0}Configuração do Ghidra e requisitos de arquitetura{ph1} · {ph2}Guia do " "Ghidra no Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · conecte uma instalação existente" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14002,7 +13961,7 @@ msgstr "" "original aberto ou um supervisor sem interface gráfica que abra um banco de " "dados privado." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14014,7 +13973,7 @@ msgstr "" " macOS arm64 com IDA Professional 9.1. A ponte nativa opcional para a " "interface gráfica tem seus próprios requisitos de versão e plataforma." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14026,14 +13985,14 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" "{ph0}Perfis do IDA, exemplos de registro e plataformas verificadas{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14044,18 +14003,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Siga um exemplo de análise nativa{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Sites, Android, .NET, firmware e outros alvos" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14064,110 +14023,110 @@ msgstr "" "Abra o guia correspondente ao seu alvo. Cada guia informa as ferramentas " "adicionais, os hosts compatíveis e o primeiro comando." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Alvo" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Ferramentas a preparar" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Guia" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Abrir guia{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Site" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / pacotes" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "APK Android" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Dispositivo Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Recursos Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Projeto JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / falha no Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Sinais lógicos" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Comportamento de processos" @@ -14183,7 +14142,7 @@ msgstr "Comportamento de processos" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14193,18 +14152,18 @@ msgstr "" "{ph0}Guia de Flutter{ph1} · {ph2}Pacotes e recursos{ph3} · {ph4}Bytecode " "EVM{ph5} · {ph6}Todos os guias de investigação{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Use o REA pelo terminal" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Comandos CLI, instalação global e curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14214,7 +14173,7 @@ msgstr "" "Substitua {ph0} por um nome ou endereço de função da lista de funções do " "alvo. O Ghidra precisa estar configurado antes." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14223,7 +14182,7 @@ msgstr "" "No macOS ou Linux, você também pode usar o instalador curl depois de " "preparar o Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14233,12 +14192,12 @@ msgstr "" "clientes que são editores, habilite o REA nas configurações MCP e permita " "suas ferramentas no chat do agente." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL e caminhos com espaços" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14250,17 +14209,17 @@ msgstr "" "Linux e use caminhos Linux ali. Os provedores sem interface gráfica rodam no" " mesmo host que o REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Comando de configuração para Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Atualize ou remova o REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14271,7 +14230,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14285,7 +14244,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14308,18 +14267,18 @@ msgstr "Copiar o comando de configuração do REA para Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Copiar comando" @@ -14375,14 +14334,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · escolha um modelo de IA" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Prepare o arquivo de registro do IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14398,7 +14357,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14411,7 +14370,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14424,12 +14383,12 @@ msgstr "" "binário original no IDA e inicie o plugin desse servidor antes de pedir ao " "REA para inspecioná-lo." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Banco de dados SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode EVM" @@ -14488,7 +14447,7 @@ msgstr "" "Revise e aprove o plano, reabra o Claude e {ph0}verifique a conexão no passo" " 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14499,12 +14458,12 @@ msgstr "" "instalação nova no macOS ou Linux arm64, compile o descompilador nativo para" " seu computador antes de continuar." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Compile o descompilador nativo do Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14517,7 +14476,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14527,7 +14486,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14537,13 +14496,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · instruções de compilação nativa{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Depois, verifique o Ghidra e salve seus caminhos para o cliente:" @@ -14946,7 +14905,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Outras opções de instalação: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14961,7 +14920,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14976,7 +14935,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14988,7 +14947,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14999,7 +14958,7 @@ msgstr "" "instalador. No Linux, o comando de configuração padrão é {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15018,13 +14977,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15921,7 +15880,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15930,6 +15889,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS ou Linux): a configuração do REA detecta-o ou " +#~ "oferece-se para instalá-lo. No macOS, abra o Hopper uma vez e escolha o modo" +#~ " de demonstração ou ative uma licença antes da sua primeira análise." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Configurar REA {ph1} em macOS ou Linux, faça o download desses dois " +#~ "arquivos na mesma pasta. Em Windows, use um ambiente Linux, como WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Distribuições compatíveis" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Hosts Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS ou " +#~ "Omarchy. O Hopper é um software comercial; sua demonstração tem limites " +#~ "definidos pelo fabricante." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/ro/messages.po b/website/locales/ro/messages.po index 3c0d95ee2f..37f75587cd 100644 --- a/website/locales/ro/messages.po +++ b/website/locales/ro/messages.po @@ -111,13 +111,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -205,13 +205,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -228,13 +228,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -251,13 +251,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -274,13 +274,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -299,13 +299,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -4465,21 +4465,21 @@ msgstr "" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "" @@ -5074,7 +5074,7 @@ msgid "" msgstr "" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" @@ -5113,11 +5113,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -5158,14 +5153,7 @@ msgid "" " setup prepares the supported demo and its private display." msgstr "" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -5176,18 +5164,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -5195,7 +5183,7 @@ msgid "" msgstr "" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5206,7 +5194,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5220,7 +5208,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -5228,7 +5216,7 @@ msgstr "" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5236,12 +5224,12 @@ msgid "" "second confirms the compiler is installed:" msgstr "" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -5249,18 +5237,18 @@ msgid "" "12.1.x installation and a 64-bit JDK 21 or newer:" msgstr "" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." msgstr "" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -5268,12 +5256,12 @@ msgid "" "before continuing." msgstr "" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -5284,7 +5272,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -5294,7 +5282,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -5304,18 +5292,18 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -5330,7 +5318,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5338,12 +5326,12 @@ msgid "" "guide{ph3}" msgstr "" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -5351,7 +5339,7 @@ msgid "" " opens a private database." msgstr "" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -5364,7 +5352,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5376,7 +5364,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5384,14 +5372,14 @@ msgid "" "below." msgstr "" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5403,7 +5391,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5414,7 +5402,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5423,7 +5411,7 @@ msgid "" "upstream plugin before asking REA to inspect it." msgstr "" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -5433,13 +5421,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -5447,7 +5435,7 @@ msgid "" msgstr "" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5458,138 +5446,138 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr "" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "" @@ -5605,7 +5593,7 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5613,31 +5601,31 @@ msgid "" "bytecode{ph5} · {ph6}All investigation guides{ph7}" msgstr "" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5645,13 +5633,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -5662,7 +5650,7 @@ msgstr "" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -5674,7 +5662,7 @@ msgstr "" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5685,20 +5673,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -5707,27 +5695,27 @@ msgid "" msgstr "" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5735,7 +5723,7 @@ msgid "" "list. Ghidra must be configured first." msgstr "" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -5743,7 +5731,7 @@ msgid "" msgstr "" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5753,7 +5741,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5761,18 +5749,18 @@ msgid "" "installer. On Linux, the default setup command is {ph1}." msgstr "" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5780,39 +5768,39 @@ msgid "" "{ph0}; for Codex:" msgstr "" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" " REA in MCP settings and allow its tools in the agent chat." msgstr "" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -5820,17 +5808,17 @@ msgid "" "use Linux paths there. Headless providers run on the same host as REA." msgstr "" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -5839,7 +5827,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5851,7 +5839,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -5861,7 +5849,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5872,7 +5860,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5884,7 +5872,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5896,7 +5884,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5963,18 +5951,18 @@ msgstr "" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "" @@ -5989,22 +5977,22 @@ msgctxt "page:get-started/index.html" msgid "Copy Notes investigation prompt" msgstr "" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "" @@ -7337,18 +7325,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7358,7 +7335,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -7372,14 +7349,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "" @@ -7390,7 +7367,7 @@ msgstr "" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7398,7 +7375,7 @@ msgid "" "Explain the calculation and show the code.{ph3}{ph4}{ph5}" msgstr "" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -7406,7 +7383,7 @@ msgid "" "same kind of question about another small Windows program you have." msgstr "" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "" @@ -7415,7 +7392,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7427,7 +7404,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7437,7 +7414,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7445,40 +7422,40 @@ msgid "" "appears." msgstr "" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7486,34 +7463,34 @@ msgid "" "tile coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -7521,7 +7498,7 @@ msgid "" "the center of the stereo field." msgstr "" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -7531,20 +7508,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7554,20 +7531,20 @@ msgid "" " menu action?”" msgstr "" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7577,7 +7554,7 @@ msgid "" "session is open." msgstr "" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -7606,17 +7583,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "" @@ -9025,7 +9002,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "" @@ -9437,19 +9414,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9457,28 +9424,28 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9488,7 +9455,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9496,7 +9463,7 @@ msgid "" "different time interval?{ph1}" msgstr "" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "" @@ -9512,7 +9479,7 @@ msgstr "" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9524,13 +9491,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -9570,12 +9537,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "" @@ -13935,7 +13902,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -13944,6 +13911,92 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" + #, python-brace-format #~ msgctxt "page:get-started/index.html" #~ msgid "" diff --git a/website/locales/ru/messages.po b/website/locales/ru/messages.po index 89c41ec9ab..ef538affd8 100644 --- a/website/locales/ru/messages.po +++ b/website/locales/ru/messages.po @@ -71,13 +71,13 @@ msgstr "Автор: N0zoM1z0 · 10 октября 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3306,21 +3306,21 @@ msgstr "Терминал" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Копировать" @@ -3554,12 +3554,12 @@ msgstr "" "к ней." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "В терминале заключите путь, содержащий пробелы, в кавычки." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3568,14 +3568,14 @@ msgstr "" "Проанализируйте папку приложения JavaScript или Electron или архив ASAR, не " "запуская приложение:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Статический анализ приложений" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3585,13 +3585,13 @@ msgstr "" "Замените путь на каталог вашего извлеченного приложения или файл {ph0}. В " "Windows здесь работает путь, например {ph1}." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Где мне найти asar-файл приложения?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3602,7 +3602,7 @@ msgstr "Приложения Electron обычно хранят его в пап #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3614,7 +3614,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3627,14 +3627,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} часто {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3642,7 +3642,7 @@ msgstr "" "Некоторые приложения устанавливаются в другом месте. Найдите в папке " "приложения {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3654,36 +3654,36 @@ msgstr "" "которые REA не может разрешить." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Если вы хотите, чтобы в вашей оболочке была постоянная команда {ph0}:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Установите CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Продолжайте читать руководство по применению JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Подключите вашу установку Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Подключите Ghidra к Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3695,7 +3695,7 @@ msgstr "" "NTFS. Используйте существующую установку Ghidra 12.1.x и 64-разрядную версию" " JDK 21 или новее:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3712,18 +3712,18 @@ msgstr "" "установки; она обнаружит существующую установку или предложит план установки" " для вашего одобрения." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Получение помощи" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Если ваш агент не видит REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3733,22 +3733,22 @@ msgstr "" "Перезапустите или повторно подключите клиент после установки. Проверьте его " "REA-соединение с {ph0}; для Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Если поставщик услуг нуждается во внимании" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Проверьте, каким провайдером вы пользуетесь. Для Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Проверьте одного поставщика" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3759,7 +3759,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3772,7 +3772,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3786,7 +3786,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3800,7 +3800,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3865,22 +3865,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Скопировать запрос на установку REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Скопировать команду анализа JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Скопировать команду установки CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Команда диагностики поставщика копирования" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Наверх" @@ -5244,21 +5244,7 @@ msgstr "REA нужен один из них для чтения собствен #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper{ph1} ( macOS или Linux ): Программа установки REA обнаруживает " -"его или предлагает установить. На macOS откройте Hopper один раз и выберите " -"демонстрационный режим или активируйте лицензию перед первым анализом." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5271,7 +5257,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5287,14 +5273,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ваш кодирующий агент" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Копировать" @@ -5305,7 +5291,7 @@ msgstr "Копировать" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5315,7 +5301,7 @@ msgstr "" "{ph0}\"{ph1}{ph2}Используйте REA, чтобы узнать, как DX-Ball рассчитывает " "расположение стереозвука. Объясните расчет и покажите код.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5327,7 +5313,7 @@ msgstr "" " копию или задайте такой же вопрос о другой небольшой программе Windows, " "которая у вас есть." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Как мне получить полный путь к файлу?" @@ -5336,7 +5322,7 @@ msgstr "Как мне получить полный путь к файлу?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5351,7 +5337,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5363,7 +5349,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5373,17 +5359,17 @@ msgstr "" "{ph0}Любая система:{ph1} перетащите файл в окно терминала, и появится путь к" " нему." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Ознакомьтесь с результатами REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Прочитайте входные данные" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5393,13 +5379,13 @@ msgstr "" " плавающей запятой. Это входные данные, отсутствующие в первом представлении" " псевдокода." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Следуйте за звонящим" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5410,12 +5396,12 @@ msgstr "" "координата плитки умножалась на 30; добавление {ph0} (20) дает положение " "кирпича на экране." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Прочитайте указанные константы" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5424,12 +5410,12 @@ msgstr "" "Арифметика ссылается на эти адреса. Считывание байтов позволяет получить " "фактические значения; порядок команд сообщает агенту, как они используются." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Запишите и проверьте восстановленную функцию" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5440,7 +5426,7 @@ msgstr "" "до преобразования в целое число. При вычислении центр экрана оказывается в " "центре стереополя." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5453,20 +5439,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Смотрите полное исследование и сравнение сборки с C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Попробуйте это с вашим собственным двоичным кодом" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5481,7 +5467,7 @@ msgstr "" "обрабатывается этот файл?” или “Какая функция выполняет это действие в " "меню?”." -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5490,13 +5476,13 @@ msgstr "" "Если вы уже знаете название функции или адрес, вы можете запросить его из " "терминала. Для настроенной установки Ghidra:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Функциональный анализ" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5510,7 +5496,7 @@ msgstr "" "свой сеанс. Запросы вашего агента MCP повторно используют импортированный " "двоичный файл, пока этот сеанс открыт." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5545,17 +5531,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Анализ программ с машинным кодом" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Запрос на копирование примера" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Анализ функции копирования" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Наверх" @@ -6538,7 +6524,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ваш кодирующий агент" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Копировать" @@ -6950,21 +6936,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Проверьте APK самостоятельно" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Настройте REA{ph1} на macOS или Linux , затем загрузите эти два файла в" -" одну папку. В Windows используйте среду Linux, такую как WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6975,18 +6949,18 @@ msgstr "" "Выберите его с помощью {ph0} или {ph1}. В данном случае использовался JDK " "21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Наведите курсор REA на файл поставщика и проверьте APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6999,7 +6973,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7009,7 +6983,7 @@ msgstr "" "Следующий вопрос: {ph0}что происходит, когда в учетной записи используются " "восемь цифр или другой интервал времени?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Источники" @@ -7025,7 +6999,7 @@ msgstr "Источники" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7039,7 +7013,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -7047,7 +7021,7 @@ msgstr "" "{ph0}Справочные примеры RFC 6238{ph1} · {ph2}Демонстрационные тестовые " "данные{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7092,12 +7066,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Следуйте инструкциям по вводу регистрационного кода" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Скопировать команду проверки APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Наверх" @@ -10929,13 +10903,13 @@ msgstr "Руководства" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10952,13 +10926,13 @@ msgstr "Исходный код" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10975,13 +10949,13 @@ msgstr "Лицензия MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10998,13 +10972,13 @@ msgstr "Вступить в сообщество" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11089,23 +11063,23 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA результат · сводка.выдержка из ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA выдержка из инструкции · 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA запрос вызывающего абонента · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" "Результаты в байтах REA · интерпретируются как удвоения в порядке убывания" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Восстановленный отрывок из C ·" @@ -11166,12 +11140,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA · generateOTP, полный корпус" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Скачать Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Скачать Android analysis provider" @@ -13863,11 +13837,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Недоступно" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Поддерживаемые дистрибутивы" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13905,17 +13874,7 @@ msgstr "" "лицензию. В Linux установщик подготавливает поддерживаемую демоверсию и " "отдельный дисплей для неё." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Хосты Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS или " -"Omarchy. Hopper — коммерческая программа; ограничения демоверсии определяет " -"разработчик." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13929,18 +13888,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Настройка Hopper и устранение неполадок{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · установите бесплатный инструмент анализа и Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13950,7 +13909,7 @@ msgstr "" "пакет для архитектуры своего компьютера." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13963,7 +13922,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13979,7 +13938,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13987,7 +13946,7 @@ msgstr "{ph0}Скачать JDK{ph1} · {ph2}Скачать Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13997,7 +13956,7 @@ msgstr "" "Проверьте Java. Первая команда выводит {ph0}; используйте эту папку для " "{ph1}. Вторая подтверждает, что компилятор установлен:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -14015,7 +13974,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14025,12 +13984,12 @@ msgstr "" "{ph0}Настройка Ghidra и требования к архитектуре{ph1} · {ph2}Руководство по " "Ghidra в Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · подключите существующую установку" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -14042,7 +14001,7 @@ msgstr "" "бинарным файлом или к супервизору без графического интерфейса, который " "открывает отдельную базу данных." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -14054,7 +14013,7 @@ msgstr "" "интерфейса в macOS arm64 с IDA Professional 9.1. У необязательного нативного" " моста к GUI свои требования к версиям и платформам." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14066,13 +14025,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}Профили IDA, примеры регистрации и проверенные платформы{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14083,18 +14042,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Пройдите пример анализа машинного кода{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Сайты, Android, .NET, прошивки и другие цели" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14103,110 +14062,110 @@ msgstr "" "Откройте руководство для своей цели. В каждом указаны дополнительные " "инструменты, поддерживаемые хосты и первая команда." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Цель" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Необходимые инструменты" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Руководство" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Открыть руководство{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Сайт" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / пакеты" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Устройство Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Ресурсы Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Проект JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Прошивка" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / сбой Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Логические сигналы" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Поведение процесса" @@ -14222,7 +14181,7 @@ msgstr "Поведение процесса" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14232,18 +14191,18 @@ msgstr "" "{ph0}Руководство по Flutter{ph1} · {ph2}Пакеты и ресурсы{ph3} · {ph4}Байткод" " EVM{ph5} · {ph6}Все руководства по исследованию{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Используйте REA из терминала" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Команды CLI, глобальная установка и curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14253,7 +14212,7 @@ msgstr "" "Замените {ph0} именем или адресом функции из списка функций цели. Сначала " "нужно настроить Ghidra." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14262,7 +14221,7 @@ msgstr "" "В macOS или Linux после подготовки Node.js можно также воспользоваться " "установщиком через curl:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14272,12 +14231,12 @@ msgstr "" " В редакторах включите REA в настройках MCP и разрешите его инструменты в " "чате агента." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL и пути с пробелами" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14289,17 +14248,17 @@ msgstr "" " используйте там пути Linux. Провайдеры без графического интерфейса работают" " на том же хосте, что и REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Команда настройки для Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Обновите или удалите REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14310,7 +14269,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14324,7 +14283,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14347,18 +14306,18 @@ msgstr "Копировать команду настройки REA для Window #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Копировать команду" @@ -14414,14 +14373,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · выберите модель ИИ" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Подготовьте файл регистрации IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14436,7 +14395,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14449,7 +14408,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14462,12 +14421,12 @@ msgstr "" "бинарный файл в IDA и запустите плагин на стороне IDA, прежде чем просить " "REA его исследовать." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "База данных SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Байткод EVM" @@ -14526,7 +14485,7 @@ msgstr "" "Просмотрите и одобрите план, снова откройте Claude, затем {ph0}проверьте " "подключение по шагу 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14537,12 +14496,12 @@ msgstr "" "macOS или Linux arm64 перед продолжением соберите нативный декомпилятор для " "своего компьютера." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Соберите нативный декомпилятор Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14555,7 +14514,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14565,7 +14524,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14575,13 +14534,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · инструкции по нативной сборке{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Затем проверьте Ghidra и сохраните пути к нему для своего клиента:" @@ -14983,7 +14942,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Другие способы установки: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14998,7 +14957,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15012,7 +14971,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15024,7 +14983,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15035,7 +14994,7 @@ msgstr "" "который вывел установщик. В Linux команда настройки по умолчанию — {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15054,13 +15013,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15954,7 +15913,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15963,6 +15922,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper{ph1} ( macOS или Linux ): Программа установки REA обнаруживает " +#~ "его или предлагает установить. На macOS откройте Hopper один раз и выберите " +#~ "демонстрационный режим или активируйте лицензию перед первым анализом." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Настройте REA{ph1} на macOS или Linux , затем загрузите эти два файла в" +#~ " одну папку. В Windows используйте среду Linux, такую как WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Поддерживаемые дистрибутивы" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Хосты Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS или " +#~ "Omarchy. Hopper — коммерческая программа; ограничения демоверсии определяет " +#~ "разработчик." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/th/messages.po b/website/locales/th/messages.po index 5fdc8fe4e4..3af6bad931 100644 --- a/website/locales/th/messages.po +++ b/website/locales/th/messages.po @@ -71,13 +71,13 @@ msgstr "โดย N0zoM1z0 ·10 ตุลาคม 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3221,21 +3221,21 @@ msgstr "เทอร์มินัล" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "คัดลอก" @@ -3459,12 +3459,12 @@ msgstr "" "ลากโฟลเดอร์ลงในหน้าต่างเทอร์มินัลและเส้นทางของมันปรากฏขึ้น" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "ในเทอร์มินัล ให้ใส่เครื่องหมายคำพูดครอบพาธที่มีช่องว่าง" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3472,14 +3472,14 @@ msgid "" msgstr "" "วิเคราะห์เป็นJavaScriptหรือElectronองโปรแกรมหรือโฟลเดอร์ASARแฟ้มจัดเก็บโดยไม่มีกำลังแอพฯ:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "การวิเคราะห์แอปพลิเคชันแบบคงที่" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3489,13 +3489,13 @@ msgstr "" "แทนที่พาธด้วยไดเรกทอรีแอปที่คุณแยกออกหรือ {ph0} แฟ้ม บน Windows เส้นทางเช่น " "{ph1} ทำงานที่นี่" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "หาไฟล์ .asar ของแอปได้ที่ไหน?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3506,7 +3506,7 @@ msgstr "Electron ปพลิเคชันมักจะเก็บไว้ #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3518,7 +3518,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3531,20 +3531,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1} มักจะ {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "บางแอปติดตั้งที่อื่น ค้นหาโฟลเดอร์ของแอป {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3555,36 +3555,36 @@ msgstr "" " การเชื่อมต่อ REA ไม่สามารถแก้ไขได้ถูกทำเครื่องหมายในผลลัพธ์" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "ถ้าคุณต้องการถาวร {ph0} คำสั่งในเปลือกของคุณ:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "ติดตั้ง CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}อ่านต่อในคู่มือแอป JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "เชื่อมต่อของคุณ Ghidra การติดตั้ง" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "เชื่อมต่อ Ghidra บน Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3595,7 +3595,7 @@ msgstr "" "เนทีฟ x86 และ x64 บนไดรฟ์ NTFS ภายในเครื่องแบบคงที่ ใช้ Ghidra 12.1.x " "ที่ติดตั้งไว้แล้วและ JDK 21 ขึ้นไปแบบ 64 บิต:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3610,18 +3610,18 @@ msgstr "" "เชื่อมต่อHopperบmacOSหรือLinux. " "รันคำสั่งการติดตั้งข้างต้นจะตรวจพบการติดตั้งที่มีอยู่หรือเสนอแผนการติดตั้งเพื่อขออนุมัติ" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "การรับความช่วยเหลือ" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "ถ้าตัวแทนของคุณไม่เห็น REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3631,22 +3631,22 @@ msgstr "" "รีสตาร์ทหรือเชื่อมต่อไคลเอ็นต์หลังจากการตั้งค่า ตรวจสอบ REA การเชื่อมต่อกับ " "{ph0};สำหรับ Codex:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "หากผู้ให้บริการต้องการความสนใจ" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "ตรวจสอบผู้ให้บริการที่คุณใช้ สำหรับ Ghidra:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "ตรวจสอบผู้ให้บริการหนึ่งราย" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "รายงานให้ตรวจสอบล้มเหลวและแนะนำขั้นตอนถัดไป." @@ -3655,7 +3655,7 @@ msgstr "รายงานให้ตรวจสอบล้มเหลวแ #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3668,7 +3668,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3682,7 +3682,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3695,7 +3695,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3759,22 +3759,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "คัดลอก REA พร้อมท์การติดตั้ง" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "คัดลอกJavaScriptคำสั่งการวิเคราะห์" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "คัดลอกCLIคำสั่งการติดตั้ง" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "คัดลอกคำสั่งการวินิจฉัยผู้ให้บริการ" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "กลับขึ้นด้านบน" @@ -5098,21 +5098,7 @@ msgstr "REA การศึกษาและการติว,อังกฤ #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1} (macOS หรือ Linux): REAการติดตั้งของตรวจพบหรือมีการติดตั้ง " -"บน macOS,เปิด Hopper " -"ครั้งเดียวและเลือกโหมดการสาธิตหรือเปิดใช้งานใบอนุญาตก่อนการวิเคราะห์ครั้งแรกข" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5124,7 +5110,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5139,14 +5125,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}ตัวแทนการเข้ารหัสของคุณ" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "คัดลอก" @@ -5157,7 +5143,7 @@ msgstr "คัดลอก" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5167,7 +5153,7 @@ msgstr "" "{ph0}›{ph1}{ph2}ใช้ REA เพื่อหาวิธีการ DX-Ball คำนวณเสียงแพน " "อธิบายการคำนวณและแสดงรหัส{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5179,7 +5165,7 @@ msgstr "" "เพื่อใช้สำเนาของคุณเองหรือถามชนิดเดียวกันของคำถามเกี่ยวกับขนาดเล็กอื่น " "Windows โปรแกรมที่คุณมี" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "ฉันจะได้รับเส้นทางเต็มของแฟ้มได้อย่างไร" @@ -5188,7 +5174,7 @@ msgstr "ฉันจะได้รับเส้นทางเต็มขอ #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5202,7 +5188,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5214,7 +5200,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5223,17 +5209,17 @@ msgid "" msgstr "" "{ph0}ระบบใดๆ:{ph1} ลากไฟล์ลงในหน้าต่างเทอร์มินัลและเส้นทางของมันจะปรากฏขึ้น" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "อ่าน REA ผลลัพธ์" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "อ่านข้อมูล" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5242,13 +5228,13 @@ msgstr "" "คำแนะนำโหลดอาร์กิวเมนต์จำนวนเต็มและแปลงสำหรับเลขคณิตจุดลอยตัว " "นี่คือการป้อนข้อมูลที่ขาดจากมุมมองหลอกแรก" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "ติดตามฟังก์ชันที่เรียก" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5258,12 +5244,12 @@ msgstr "" "REA ระบุตัวฟังก์ชันที่เรียกหาอิฐ คำแนะนำก่อนหน้านี้คูณกระเบื้องประสานงานด้วย" " 30;เพิ่ม {ph0} (20)ให้ตำแหน่งหน้าจอของอิฐ" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "อ่านค่าคงที่อ้างอิง" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5272,12 +5258,12 @@ msgstr "" "คณิตศาสตร์อ้างอิงที่อยู่เหล่านี้ " "การอ่านไบต์เป็นค่าจริงลำดับคำสั่งจะบอกเอเจนต์ว่ามีการใช้งานอย่างไร" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "เขียนและตรวจสอบฟังก์ชั่นการกู้คืน" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5288,7 +5274,7 @@ msgstr "" "ก่อนที่จะแปลงจำนวนเต็ม " "การคำนวณทำให้ศูนย์กลางของหน้าจอที่ศูนย์กลางของสนามสเตอริโอ" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5300,21 +5286,21 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" "{ph0}เครื่องชั่งสำหรับธุรกิจค้าปลีกอาหารเครื่องชั่งคิดราคาเครื่องชั่งติดป้ายราคา{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "ลองใช้ไบนารีของคุณเอง" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5327,7 +5313,7 @@ msgstr "" "แล้วให้ตัวแทนของคุณปฏิบัติการในท้องถิ่นหรือห้องสมุดและคุณลักษณะในการตรวจสอบ " "คำถามเริ่มต้นที่มีประโยชน์รวมถึง\"วิธีการคือไฟล์นี้แยก?\"หรือ\"ฟังก์ชั่นใดที่จัดการกับการกระทำของเมนูนี้”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5336,13 +5322,13 @@ msgstr "" "ถ้าคุณรู้อยู่แล้วว่าชื่อฟังก์ชั่นหรือที่อยู่คุณสามารถสอบถามได้จากขั้ว " "สำหรับการกำหนดค่า Ghidra การติดตั้ง:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "การวิเคราะห์ฟังก์ชัน" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5355,7 +5341,7 @@ msgstr "" "CLI การอิมพอร์ตการเรียกใช้และการวิเคราะห์ไบนารีแล้วปิดเซสชัน ตัวแทนของคุณ " "MCP การสืบค้นข้อมูลใช้ไบนารีที่นำเข้าอีกครั้งในขณะที่เซสชันนั้นเปิดอยู่" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5388,17 +5374,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "การวิเคราะห์โปรแกรมเครื่องรหัส" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "คัดลอกตัวอย่างพรอมต์" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "คัดลอกการวิเคราะห์ฟังก์ชัน" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "กลับขึ้นด้านบน" @@ -6355,7 +6341,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}ตัวแทนการเข้ารหัสของคุณ" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "คัดลอก" @@ -6759,22 +6745,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "ตรวจสอบ APK ตัวคุณเอง" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}ตั้งค่า REA{ph1} บน macOS หรือ Linux " -"แล้วดาวน์โหลดทั้งสองไฟล์ลงในโฟลเดอร์เดียวกัน บน Windows ใช้ Linux " -"วุฒิการศึกษาระดับปริญญาตรีขึ้นไป" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6782,18 +6755,18 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "17 หรือสูงกว่า เลือกด้วย {ph0} หรือ {ph1}. คดีนี้ใช้เจดีเค 21" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "จุด REA ที่ไฟล์ผู้ให้บริการและตรวจสอบ APK:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6805,7 +6778,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6815,7 +6788,7 @@ msgstr "" "คำถามต่อไป: " "{ph0}จะเกิดอะไรขึ้นเมื่อบัญชีใช้ตัวเลขแปดหลักหรือช่วงเวลาที่แตกต่างกัน{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "แหล่งที่มา" @@ -6831,7 +6804,7 @@ msgstr "แหล่งที่มา" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6845,13 +6818,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}กรณีอ้างอิง 6238{ph1} · {ph2}ข้อมูลการทดสอบการสาธิต{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6896,12 +6869,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "ติดตามการคำนวณการเข้าสู่ระบบรหัส" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "คัดลอก APK คำสั่งการตรวจสอบ" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "กลับขึ้นด้านบน" @@ -10605,13 +10578,13 @@ msgstr "คู่มือ" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10628,13 +10601,13 @@ msgstr "ซอร์สโค้ด" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10651,13 +10624,13 @@ msgstr "สัญญาอนุญาต MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10674,13 +10647,13 @@ msgstr "เข้าร่วมชุมชน" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10765,22 +10738,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REAผล·สรุปข้อความที่ตัดตอนมาจากไอพีซี" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA คำแนะนำ*0*406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA ข้อความที่ตัดตอนมา*0*411เอฟ5 บาท" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA ผลลัพธ์ไบต์*ตีความว่าเป็นคู่เล็กๆน้อยๆเอนเดียน" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "กู้คืนค*ตัดตอนมา" @@ -10841,12 +10814,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA *เครื่องกำเนิดไฟฟ้า,ร่างกายที่สมบูรณ์" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "ดาวน์โหลด Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "ดาวน์โหลดผู้ให้บริการวิเคราะห์Android" @@ -13493,11 +13466,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "ไม่รองรับ" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "ดิสทริบิวชันที่รองรับ" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13534,16 +13502,7 @@ msgstr "" "ให้เลือกโหมดเดโมหรือเปิดใช้งานใบอนุญาตเมื่อเปิด Hopper ครั้งแรก บน Linux " "ขั้นตอนตั้งค่าจะเตรียมเดโมที่รองรับและจอแสดงผลเฉพาะของเดโม" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"โฮสต์ Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS หรือ" -" Omarchy Hopper เป็นซอฟต์แวร์เชิงพาณิชย์ และเดโมมีข้อจำกัดตามที่ผู้ผลิตกำหนด" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13556,18 +13515,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}ตั้งค่า Hopper และแก้ปัญหา{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · ติดตั้งเครื่องมือวิเคราะห์ฟรีและ Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13577,7 +13536,7 @@ msgstr "" "เลือกแพ็กเกจที่ตรงกับสถาปัตยกรรมของคอมพิวเตอร์" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13590,7 +13549,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13605,7 +13564,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13613,7 +13572,7 @@ msgstr "{ph0}ดาวน์โหลด JDK{ph1} · {ph2}ดาวน์โห #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13623,7 +13582,7 @@ msgstr "" "ตรวจสอบ Java คำสั่งแรกจะแสดง {ph0} ให้ใช้โฟลเดอร์นั้นเป็นค่า {ph1} " "คำสั่งที่สองยืนยันว่าติดตั้งคอมไพเลอร์แล้ว:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13641,7 +13600,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13651,12 +13610,12 @@ msgstr "" "{ph0}ตั้งค่า Ghidra และข้อกำหนดด้านสถาปัตยกรรม{ph1} · {ph2}คู่มือ Ghidra บน " "Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · เชื่อมต่อกับที่ติดตั้งไว้แล้ว" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13667,7 +13626,7 @@ msgstr "" "เลือกเชื่อมต่อกับ GUI ที่เปิดไบนารีต้นฉบับอยู่ หรือใช้โพรเซสควบคุมแบบไม่มี " "GUI ที่เปิดฐานข้อมูลเฉพาะ" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13678,7 +13637,7 @@ msgstr "" "รวมถึงโพรเซสควบคุมแบบไม่มี GUI บน macOS arm64 ที่ใช้ IDA Professional 9.1 " "บริดจ์ GUI เนทีฟที่เป็นตัวเลือกเสริมมีข้อกำหนดด้านรุ่นและแพลตฟอร์มของตัวเอง" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13690,13 +13649,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}รูปแบบ IDA ตัวอย่างการลงทะเบียน และแพลตฟอร์มที่ตรวจสอบแล้ว{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13708,18 +13667,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}ทำตามตัวอย่างการวิเคราะห์เนทีฟ{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "เว็บไซต์ Android, .NET, เฟิร์มแวร์ และเป้าหมายอื่น" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13728,110 +13687,110 @@ msgstr "" "เปิดคู่มือสำหรับเป้าหมายที่คุณมี แต่ละคู่มือระบุเครื่องมือเพิ่มเติม " "โฮสต์ที่รองรับ และคำสั่งแรก" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "เป้าหมาย" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "เครื่องมือที่ต้องเตรียม" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "คู่มือ" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}เปิดคู่มือ{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "เว็บไซต์" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / แพ็กเกจ" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "อุปกรณ์ Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "ทรัพยากร Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "โปรเจกต์ JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "เฟิร์มแวร์" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / โปรแกรมขัดข้องบน Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "สัญญาณลอจิก" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "พฤติกรรมของโพรเซส" @@ -13847,7 +13806,7 @@ msgstr "พฤติกรรมของโพรเซส" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13857,18 +13816,18 @@ msgstr "" "{ph0}คู่มือ Flutter{ph1} · {ph2}แพ็กเกจและทรัพยากร{ph3} · {ph4}ไบต์โค้ด " "EVM{ph5} · {ph6}คู่มือการสำรวจทั้งหมด{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "ใช้ REA จากเทอร์มินัล" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "คำสั่ง CLI การติดตั้งแบบโกลบอล และ curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13878,7 +13837,7 @@ msgstr "" "แทนที่ {ph0} ด้วยชื่อฟังก์ชันหรือที่อยู่จากรายการฟังก์ชันของเป้าหมาย " "ต้องตั้งค่า Ghidra ก่อน" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -13887,7 +13846,7 @@ msgstr "" "บน macOS หรือ Linux คุณใช้ตัวติดตั้งผ่าน curl ได้เช่นกัน หลังเตรียม Node.js " "แล้ว:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -13897,12 +13856,12 @@ msgstr "" "สำหรับโปรแกรมแก้ไขโค้ด ให้เปิดใช้ REA ในการตั้งค่า MCP " "และอนุญาตเครื่องมือในแชตของผู้ช่วย AI" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL และพาธที่มีช่องว่าง" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -13913,17 +13872,17 @@ msgstr "" "AI ทำงานใน WSL ให้ติดตั้ง Node.js และ REA ในสภาพแวดล้อม Linux นั้น และใช้พาธ" " Linux เครื่องมือวิเคราะห์แบบไม่มี GUI ทำงานบนโฮสต์เดียวกับ REA" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "คำสั่งตั้งค่าบน Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "อัปเดตหรือลบ REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -13935,7 +13894,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13949,7 +13908,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -13972,18 +13931,18 @@ msgstr "คัดลอกคำสั่งตั้งค่า REA บน Win #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "คัดลอกคำสั่ง" @@ -14039,14 +13998,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · เลือกโมเดล AI" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "เตรียมไฟล์ลงทะเบียน IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14061,7 +14020,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14074,7 +14033,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14086,12 +14045,12 @@ msgstr "" "{ph1} ที่ใช้งานได้ และ {ph2} หากมี ลงในไฟล์ เปิดไบนารีต้นฉบับใน IDA " "และเริ่มปลั๊กอินฝั่ง IDA ก่อนขอให้ REA สำรวจ" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "ฐานข้อมูล SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "ไบต์โค้ด EVM" @@ -14147,7 +14106,7 @@ msgstr "" "ตรวจสอบและอนุมัติแผน เปิด Claude ใหม่ " "แล้ว{ph0}ตรวจสอบการเชื่อมต่อในขั้นตอนที่ 3{ph1}" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14157,12 +14116,12 @@ msgstr "" "แทนที่ทั้งสองพาธด้วยตำแหน่งที่ติดตั้งไว้ หากเพิ่งติดตั้งบน macOS หรือ Linux " "arm64 ให้สร้างดีคอมไพเลอร์เนทีฟสำหรับคอมพิวเตอร์ของคุณก่อนดำเนินการต่อ" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "สร้างดีคอมไพเลอร์เนทีฟของ Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14175,7 +14134,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14185,7 +14144,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14195,13 +14154,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · ขั้นตอนการสร้างเนทีฟ{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "จากนั้นตรวจสอบ Ghidra และบันทึกพาธสำหรับแอปที่เชื่อมต่อของคุณ:" @@ -14586,7 +14545,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "วิธีติดตั้งอื่น: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14601,7 +14560,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14616,7 +14575,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14627,7 +14586,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14638,7 +14597,7 @@ msgstr "" "คำสั่งตั้งค่าเริ่มต้นคือ {ph1}" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14657,13 +14616,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15549,7 +15508,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15558,6 +15517,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1} (macOS หรือ Linux): REAการติดตั้งของตรวจพบหรือมีการติดตั้ง " +#~ "บน macOS,เปิด Hopper " +#~ "ครั้งเดียวและเลือกโหมดการสาธิตหรือเปิดใช้งานใบอนุญาตก่อนการวิเคราะห์ครั้งแรกข" + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}ตั้งค่า REA{ph1} บน macOS หรือ Linux " +#~ "แล้วดาวน์โหลดทั้งสองไฟล์ลงในโฟลเดอร์เดียวกัน บน Windows ใช้ Linux " +#~ "วุฒิการศึกษาระดับปริญญาตรีขึ้นไป" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "ดิสทริบิวชันที่รองรับ" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "โฮสต์ Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS หรือ" +#~ " Omarchy Hopper เป็นซอฟต์แวร์เชิงพาณิชย์ และเดโมมีข้อจำกัดตามที่ผู้ผลิตกำหนด" + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/tr/messages.po b/website/locales/tr/messages.po index a0779023c7..30e848312e 100644 --- a/website/locales/tr/messages.po +++ b/website/locales/tr/messages.po @@ -71,13 +71,13 @@ msgstr "Yazan N0zoM1z0 · 10 Ekim 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3270,21 +3270,21 @@ msgstr "Terminal" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Kopyala" @@ -3514,13 +3514,13 @@ msgstr "" "ve yolu görünür." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Bir terminalde, boşluk içeren bir yolun etrafına tırnak işaretleri koyun." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3529,14 +3529,14 @@ msgstr "" "Uygulamayı çalıştırmadan bir JavaScript veya Electron uygulama klasörünü " "veya ASAR arşivini analiz edin:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Statik uygulama analizi" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3546,13 +3546,13 @@ msgstr "" "Yolu çıkarılan uygulama dizininizle değiştirin veya {ph0} dosyası. " "Windows’ta {ph1} burada çalışıyor." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Bir uygulamanın .asar dosyasını nerede bulurum?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3563,7 +3563,7 @@ msgstr "Electron uygulamaları genellikle bir {ph0} klasöründe saklar:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3575,7 +3575,7 @@ msgstr "{ph0}Windows:{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3588,21 +3588,21 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux:{ph1} genellikle {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" "Bazı uygulamalar başka bir yere yüklenir. Uygulama klasöründe {ph0} arayın." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3614,36 +3614,36 @@ msgstr "" "işaretlenir." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Kabuğunuzda kalıcı bir {ph0} komutu istiyorsanız:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "CLI’yı kurun" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}JavaScript uygulama kılavuzu ile devam edin{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Ghidra kurulumunuzu bağlayın" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Ghidra’yı Windows’a bağlayın" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3655,7 +3655,7 @@ msgstr "" "bir Ghidra 12.1.x kurulumu ve 64 bit JDK 21 veya daha yeni bir sürüm " "kullanın:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3673,18 +3673,18 @@ msgstr "" "çalıştırın; Mevcut bir kurulumu algılar veya onayınız için bir kurulum planı" " sunar." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Yardım alma" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Temsilciniz REA’yı görmüyorsa" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3694,22 +3694,22 @@ msgstr "" "Kurulumdan sonra istemciyi yeniden başlatın veya yeniden bağlayın. REA " "bağlantısını {ph0} kontrol edin; Codex için:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Bir sağlayıcının dikkat etmesi gerekiyorsa" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Kullandığınız sağlayıcıyı kontrol edin. Ghidra için:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Bir sağlayıcıyı kontrol edin" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "Rapor, başarısız kontrolü ve önerilen bir sonraki adımı verir." @@ -3718,7 +3718,7 @@ msgstr "Rapor, başarısız kontrolü ve önerilen bir sonraki adımı verir." #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3731,7 +3731,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3745,7 +3745,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3759,7 +3759,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3825,22 +3825,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "REA kurulum istemini kopyalayın" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "JavaScript analiz komutunu kopyalayın" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "CLI kurulum komutunu kopyalayın" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Sağlayıcı tanılama komutunu kopyala" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Başa dön" @@ -5182,21 +5182,7 @@ msgstr "REA, yerel programları okumak için bunlardan birine ihtiyaç duyar:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper{ph1} (macOS veya Linux): REA’nın kurulumu onu algılar veya " -"kurmayı teklif eder. macOS’ta Hopper’ı bir kez açın ve ilk analizinizden " -"önce demo modunu seçin veya bir lisansı etkinleştirin." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5208,7 +5194,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5224,14 +5210,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Kodlama aracınız" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Kopyala" @@ -5242,7 +5228,7 @@ msgstr "Kopyala" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5253,7 +5239,7 @@ msgstr "" "bulmak için REA kullanın. Hesaplamayı açıklayın ve kodu " "gösterin.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5265,7 +5251,7 @@ msgstr "" " olduğunuz başka bir küçük Windows programı hakkında aynı tür soruları " "sorun." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Bir dosyanın tam yolunu nasıl alabilirim?" @@ -5274,7 +5260,7 @@ msgstr "Bir dosyanın tam yolunu nasıl alabilirim?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5289,7 +5275,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5301,7 +5287,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5311,17 +5297,17 @@ msgstr "" "{ph0}Herhangi bir sistem:{ph1} dosyayı bir terminal penceresine sürükleyin " "ve yolu görünür." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "REA sonuçlarını okuyun" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Girişi oku" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5330,13 +5316,13 @@ msgstr "" "Talimatlar bir tamsayı argümanı yükler ve kayan nokta aritmetiği için " "dönüştürür. Bu, ilk sözde kod görünümünde bulunmayan girdidir." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Çağıranı takip et" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5347,12 +5333,12 @@ msgstr "" "koordinatını 30 ile çarpar; {ph0} (20) eklemek tuğlanın ekran konumunu " "verir." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Başvurulan sabitleri okuyun" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5361,12 +5347,12 @@ msgstr "" "Aritmetik bu adreslere başvurur. Baytları okumak gerçek değerleri sağlar; " "talimatların sırası ajana bunların nasıl kullanıldığını gösterir." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Kurtarılan işlevi yazın ve kontrol edin" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5377,7 +5363,7 @@ msgstr "" "-500, 0 ve 500 ile eşleşir. Hesaplama, ekranın merkezini stereo alanın " "ortasına yerleştirir." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5390,20 +5376,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Tüm inceleme ve assembly-to-C karşılaştırmasına bakın{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Kendi ikili dosyanızla deneyin" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5417,7 +5403,7 @@ msgstr "" "araştırılacak bir özellik verin. Faydalı başlangıç soruları arasında \"Bu " "dosya nasıl ayrıştırılır?\" veya \"Bu menü eylemini hangi işlev işler?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5426,13 +5412,13 @@ msgstr "" "Bir işlev adını veya adresini zaten biliyorsanız, bunu terminalden " "sorgulayabilirsiniz. Yapılandırılmış bir Ghidra kurulumu için:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Fonksiyon analizi" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5446,7 +5432,7 @@ msgstr "" "kapatır. Aracınızın MCP sorguları, oturum açıkken içe aktarılan ikili " "dosyayı yeniden kullanır." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5480,17 +5466,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Yerel ikili dosyaların analizi" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Örnek istemi kopyala" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Fonksiyon analizini kopyala" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Başa dön" @@ -6473,7 +6459,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Kodlama aracınız" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Kopyala" @@ -6885,21 +6871,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "APK’yı kendiniz inceleyin" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}REA’yı macOS veya Linux’ta ayarlayın{ph1}, ardından bu iki dosyayı aynı" -" klasöre indirin. Windows’ta WSL gibi bir Linux ortamı kullanın." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6909,19 +6883,19 @@ msgstr "" "Android analizinin tam bir JDK 17 veya daha yenisine ihtiyacı var. {ph0} " "veya {ph1} ile seçin. Bu durumda JDK 21 kullanıldı." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "" "REA dosyasını sağlayıcı dosyasına yönlendirin ve APK dosyasını inceleyin :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux'NIN sohbeti" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6934,7 +6908,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6944,7 +6918,7 @@ msgstr "" "Sıradaki soru: {ph0}Bir hesap sekiz basamak veya farklı bir zaman aralığı " "kullandığında ne olur?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Kaynaklar" @@ -6960,7 +6934,7 @@ msgstr "Kaynaklar" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6974,13 +6948,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC 6238 referans durumları{ph1} * {ph2}Demo test verileri{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7026,12 +7000,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Giriş kodu hesaplamasını takip edin" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "APK denetim komutunu kopyalayın" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Başa dön" @@ -10811,13 +10785,13 @@ msgstr "Rehberler" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10834,13 +10808,13 @@ msgstr "Kaynak kod" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10857,13 +10831,13 @@ msgstr "MIT lisansı" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10880,13 +10854,13 @@ msgstr "Topluluğa katıl" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10971,22 +10945,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA sonuç * özet.ıpc'den alıntı" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA talimat alıntısı * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA çağıran alıntı * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA bayt sonuçları * küçük endian çiftleri olarak yorumlanır" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Kurtarılan C * alıntı" @@ -11047,12 +11021,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, komple gövde" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "İndir Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Android analiz sağlayıcısını indirin" @@ -13728,11 +13702,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Kullanılamıyor" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Desteklenen dağıtımlar" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13769,17 +13738,7 @@ msgstr "" "ilk açıldığında demo modunu seçin veya lisansınızı etkinleştirin. Linux’ta " "kurulum, desteklenen demoyu ve ona özel ekran ortamını hazırlar." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linux sistemleri: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS" -" veya Omarchy. Hopper ticari bir yazılımdır; demosunun sınırlarını üretici " -"belirler." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13793,18 +13752,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopper kurulumu ve sorun giderme{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · ücretsiz analiz aracını ve Java’yı kurun" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13814,7 +13773,7 @@ msgstr "" "Bilgisayarınızın mimarisine uygun paketi seçin." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13827,7 +13786,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13843,7 +13802,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13851,7 +13810,7 @@ msgstr "{ph0}JDK indirin{ph1} · {ph2}Ghidra 12.1.4’ü indirin{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13861,7 +13820,7 @@ msgstr "" "Java’yı kontrol edin. İlk komut {ph0} değerini gösterir; {ph1} için bu " "klasörü kullanın. İkinci komut derleyicinin kurulu olduğunu doğrular:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13879,7 +13838,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13889,12 +13848,12 @@ msgstr "" "{ph0}Ghidra kurulumu ve mimari gereksinimleri{ph1} · {ph2}Windows Ghidra " "kılavuzu{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · mevcut bir kurulumu bağlayın" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13906,7 +13865,7 @@ msgstr "" "bağlanmayı veya özel bir veritabanı açan grafik arayüzsüz denetleyiciyi " "seçin." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13918,7 +13877,7 @@ msgstr "" "grafik arayüzsüz denetleyici bulunur. İsteğe bağlı yerel GUI köprüsünün " "ayrıca sürüm ve platform gereksinimleri vardır." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13930,7 +13889,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" @@ -13938,7 +13897,7 @@ msgstr "" "{ph0}IDA profilleri, bağlantı kaydı örnekleri ve doğrulanmış " "platformlar{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13949,18 +13908,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Makine kodu analiz örneğini izleyin{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Web siteleri, Android, .NET, donanım yazılımları ve diğer hedefler" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13969,110 +13928,110 @@ msgstr "" "Elinizdeki hedefin kılavuzunu açın. Her kılavuz ek araçları, desteklenen " "sistemleri ve ilk komutu gösterir." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Hedef" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Hazırlanacak araçlar" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Kılavuz" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Kılavuzu açın{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Web sitesi" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / paketler" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android cihazı" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Android kaynakları" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEB projesi" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Donanım yazılımı" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linux çökmesi" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Mantık sinyalleri" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Süreç davranışı" @@ -14088,7 +14047,7 @@ msgstr "Süreç davranışı" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14098,18 +14057,18 @@ msgstr "" "{ph0}Flutter kılavuzu{ph1} · {ph2}Paketler ve kaynaklar{ph3} · {ph4}EVM bayt" " kodu{ph5} · {ph6}Tüm inceleme kılavuzları{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "REA’yı terminalden kullanın" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLI komutları, genel kurulum ve curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14119,7 +14078,7 @@ msgstr "" "{ph0} yerine hedefin fonksiyon listesinden bir fonksiyon adı veya adresi " "yazın. Önce Ghidra’nın yapılandırılması gerekir." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14128,7 +14087,7 @@ msgstr "" "macOS veya Linux’ta Node.js’i hazırladıktan sonra curl yükleyicisini de " "kullanabilirsiniz:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14138,12 +14097,12 @@ msgstr "" "istemcilerinde MCP ayarlarında REA’yı etkinleştirin ve ajan sohbetinde " "araçlarına izin verin." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL ve boşluk içeren yollar" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14155,17 +14114,17 @@ msgstr "" " orada Linux yollarını kullanın. Grafik arayüzsüz sağlayıcılar REA ile aynı " "sistemde çalışır." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Windows kurulum komutu:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "REA’yı güncelleyin veya kaldırın" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14176,7 +14135,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14190,7 +14149,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14213,18 +14172,18 @@ msgstr "Windows için REA kurulum komutunu kopyala" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Komutu kopyala" @@ -14281,14 +14240,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · bir yapay zekâ modeli seçin" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "IDA bağlantı kayıt dosyasını hazırlayın" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14304,7 +14263,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14317,7 +14276,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14329,12 +14288,12 @@ msgstr "" "değerlerini dosyaya kopyalayın. REA’dan inceleme istemeden önce özgün ikili " "dosyayı IDA’da açın ve IDA tarafındaki eklentiyi başlatın." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLite veritabanı" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVM bayt kodu" @@ -14393,7 +14352,7 @@ msgstr "" "Planı inceleyip onaylayın, Claude’u yeniden açın, ardından {ph0}3. adımdaki " "bağlantı kontrolünü yapın{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14404,12 +14363,12 @@ msgstr "" "macOS veya Linux arm64 kurulumunda, devam etmeden önce bilgisayarınız için " "yerel decompiler’ı derleyin." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Ghidra’nın yerel decompiler’ını derleyin" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14422,7 +14381,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14432,7 +14391,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14442,13 +14401,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · yerel derleme talimatları{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" @@ -14848,7 +14807,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Diğer kurulum seçenekleri: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14863,7 +14822,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14878,7 +14837,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14890,7 +14849,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14901,7 +14860,7 @@ msgstr "" " yolunu kullanın. Linux'ta varsayılan kurulum komutu {ph1} şeklindedir." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14920,13 +14879,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15818,7 +15777,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15827,6 +15786,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper{ph1} (macOS veya Linux): REA’nın kurulumu onu algılar veya " +#~ "kurmayı teklif eder. macOS’ta Hopper’ı bir kez açın ve ilk analizinizden " +#~ "önce demo modunu seçin veya bir lisansı etkinleştirin." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}REA’yı macOS veya Linux’ta ayarlayın{ph1}, ardından bu iki dosyayı aynı" +#~ " klasöre indirin. Windows’ta WSL gibi bir Linux ortamı kullanın." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Desteklenen dağıtımlar" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linux sistemleri: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS" +#~ " veya Omarchy. Hopper ticari bir yazılımdır; demosunun sınırlarını üretici " +#~ "belirler." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/uk/messages.po b/website/locales/uk/messages.po index 667fe694d0..a83f498d89 100644 --- a/website/locales/uk/messages.po +++ b/website/locales/uk/messages.po @@ -71,13 +71,13 @@ msgstr "Автор: N0zoM1z0 · 10 жовтня 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3287,21 +3287,21 @@ msgstr "Термінал" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Копіювати" @@ -3534,12 +3534,12 @@ msgstr "" " шлях до неї." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "У терміналі вкладіть шлях, що містить пробіли, у лапки." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3548,14 +3548,14 @@ msgstr "" "Проаналізуйте папку Програми JavaScript або Electron або архів ASAR, не " "запускаючи програму:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Статичний аналіз додатків" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3565,13 +3565,13 @@ msgstr "" "Замініть шлях на каталог витягнутої Програми або файл {ph0}. У Windows тут " "працює шлях, наприклад {ph1}." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Де я можу знайти файл програми asar?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3582,7 +3582,7 @@ msgstr "Програми Electron зазвичай зберігають його #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3594,7 +3594,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3607,14 +3607,14 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} часто {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." @@ -3622,7 +3622,7 @@ msgstr "" "Деякі додатки встановлюються в іншому місці. Знайдіть у папці програми " "{ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3634,36 +3634,36 @@ msgstr "" "може вирішити." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Якщо ви хочете, щоб у вашій оболонці була постійна команда {ph0}:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Встановіть CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Продовжуйте читати посібник із застосування JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Підключіть вашу установку Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Підключіть Ghidra до Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3675,7 +3675,7 @@ msgstr "" "Використовуйте існуючу установку Ghidra 12.1.x і 64-розрядну версію JDK 21 " "або новіше:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3693,18 +3693,18 @@ msgstr "" "встановлення; вона виявить існуючу установку або запропонує план " "встановлення для вашого схвалення." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Отримання допомоги" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Якщо ваш агент не бачить REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3714,22 +3714,22 @@ msgstr "" "Перезапустіть або підключіть клієнт після встановлення. Перевірте його " "REA-з'єднання з {ph0}; для Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Якщо постачальник послуг потребує уваги" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Перевірте, яким провайдером ви користуєтесь. Для Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Перевірте одного постачальника" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" @@ -3740,7 +3740,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3752,7 +3752,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3766,7 +3766,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3780,7 +3780,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3845,22 +3845,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Скопіюйте запит на встановлення REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Скопіювати команду аналізу JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Скопіювати команду установки CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Команда діагностики Постачальника копіювання" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Нагору" @@ -5222,21 +5222,7 @@ msgstr "REA потрібен один з них для читання власн #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper{ph1} (macOS або Linux ): програма встановлення REA виявляє його" -" або пропонує встановити. На macOS відкрийте Hopper один раз і виберіть " -"демонстраційний режим або активуйте ліцензію перед першим аналізом." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5249,7 +5235,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5265,14 +5251,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ваш кодуючий агент" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Копіювати" @@ -5283,7 +5269,7 @@ msgstr "Копіювати" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5293,7 +5279,7 @@ msgstr "" "{ph0}\"{ph1}{ph2}Використовуйте REA, щоб дізнатися, як DX-Ball обчислює " "розташування стереозвуку. Поясніть розрахунок і покажіть код.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5304,7 +5290,7 @@ msgstr "" "DX-Ball 1.07. REA не включає DX-Ball, тому використовуйте власну копію або " "задайте те саме питання про іншу невелику програму Windows, яку ви маєте." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Як отримати повний шлях до файлу?" @@ -5313,7 +5299,7 @@ msgstr "Як отримати повний шлях до файлу?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5328,7 +5314,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5340,7 +5326,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5350,17 +5336,17 @@ msgstr "" "{ph0}Будь-яка система:{ph1} перетягніть файл у вікно терміналу, і з'явиться " "шлях до нього." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Ознайомтеся з результатами REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Прочитайте вхідні дані" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5369,13 +5355,13 @@ msgstr "" "Інструкції завантажують цілий аргумент і перетворюють його в арифметику з " "плаваючою комою. Це вхідні дані, відсутні в першому поданні псевдокоду." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Слідуйте за абонентом" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5386,12 +5372,12 @@ msgstr "" "координату плитки множили на 30; додавання {ph0} (20) дає положення цегли на" " екрані." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Прочитайте зазначені константи" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5401,12 +5387,12 @@ msgstr "" "фактичні значення; порядок команд повідомляє агенту, як вони " "використовуються." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Запишіть і перевірте відновлену функцію" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5417,7 +5403,7 @@ msgstr "" "до перетворення в ціле число. При обчисленні центр екрану виявляється в " "центрі стереополя." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5430,20 +5416,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Дивіться повне дослідження та порівняння збірки з C{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Спробуйте це за допомогою власного двійкового коду" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5457,7 +5443,7 @@ msgstr "" "для вивчення. Корисні початкові запитання включають “як обробляється цей " "файл?\"або\" яка функція виконує цю дію в меню?”." -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5466,13 +5452,13 @@ msgstr "" "Якщо ви вже знаєте назву функції або адресу, ви можете запитати її з " "терміналу. Для налаштованої установки Ghidra:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Функціональний аналіз" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5486,7 +5472,7 @@ msgstr "" "сеанс. Запити вашого агента MCP повторно використовують імпортований " "двійковий файл, поки цей сеанс відкритий." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5520,17 +5506,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Аналіз програм з машинним кодом" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Запит на копіювання прикладу" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Аналіз функції копіювання" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Нагору" @@ -6512,7 +6498,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Ваш кодуючий агент" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Копіювати" @@ -6924,21 +6910,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Перевірте APK самостійно" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Налаштуйте REA{ph1} на macOS або Linux , потім завантажте ці два файли " -"в одну папку. У Windows використовуйте середовище Linux, таке як WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6948,18 +6922,18 @@ msgstr "" "Для аналізу Android потрібен повний JDK 17 або новішої версії. Виберіть його" " за допомогою {ph0} або {ph1}. У цьому випадку використовувався JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Наведіть курсор REA на файл постачальника та перевірте APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6972,7 +6946,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6982,7 +6956,7 @@ msgstr "" "Наступне питання: {ph0}що відбувається, коли в обліковому записі " "використовуються вісім цифр або інший інтервал часу?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Джерело" @@ -6998,7 +6972,7 @@ msgstr "Джерело" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -7012,7 +6986,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" @@ -7020,7 +6994,7 @@ msgstr "" "{ph0}Довідкові приклади RFC 6238{ph1} · {ph2}Демонстраційні тестові " "дані{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7065,12 +7039,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Дотримуйтесь інструкцій щодо введення реєстраційного коду" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Скопіювати команду перевірки APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Нагору" @@ -10888,13 +10862,13 @@ msgstr "Посібники" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10911,13 +10885,13 @@ msgstr "Вихідний код" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10934,13 +10908,13 @@ msgstr "Ліцензія MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10957,13 +10931,13 @@ msgstr "Приєднатися до спільноти" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -11048,23 +11022,23 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA результат * підсумок.витяг з ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA витяг з інструкції * 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA запит абонента * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" "Результати в байтах REA * інтерпретуються як подвоєння в порядку зменшення" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Відновлений уривок з C ·" @@ -11125,12 +11099,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP, повний корпус" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Завантажити Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Завантажити Android analysis provider" @@ -13807,11 +13781,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Недоступно" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Підтримувані дистрибутиви" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13849,17 +13818,7 @@ msgstr "" "або активуйте ліцензію. У Linux налаштування готує підтримувану демоверсію " "та окремий дисплей для неї." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Хости Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS або " -"Omarchy. Hopper — комерційна програма; обмеження демоверсії визначає " -"розробник." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13873,18 +13832,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Налаштування Hopper та усунення неполадок{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · встановіть безкоштовний інструмент аналізу та Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13894,7 +13853,7 @@ msgstr "" "Виберіть пакет для архітектури свого комп’ютера." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13907,7 +13866,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13923,7 +13882,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13931,7 +13890,7 @@ msgstr "{ph0}Завантажити JDK{ph1} · {ph2}Завантажити Ghid #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13941,7 +13900,7 @@ msgstr "" "Перевірте Java. Перша команда виводить {ph0}; використовуйте цю папку для " "{ph1}. Друга підтверджує, що компілятор встановлено:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13959,7 +13918,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13969,12 +13928,12 @@ msgstr "" "{ph0}Налаштування Ghidra та вимоги до архітектури{ph1} · {ph2}Посібник із " "Ghidra у Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · підключіть наявне встановлення" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13986,7 +13945,7 @@ msgstr "" "бінарним файлом або до супервізора без графічного інтерфейсу, який відкриває" " окрему базу даних." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13998,7 +13957,7 @@ msgstr "" "інтерфейсу в macOS arm64 з IDA Professional 9.1. Необов’язковий нативний " "міст до GUI має власні вимоги до версій і платформ." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -14010,13 +13969,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}Профілі IDA, приклади реєстрації та перевірені платформи{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -14027,18 +13986,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Пройдіть приклад аналізу машинного коду{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Сайти, Android, .NET, прошивки та інші цілі" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -14047,110 +14006,110 @@ msgstr "" "Відкрийте посібник для своєї цілі. Кожен містить додаткові інструменти, " "підтримувані хости та першу команду." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Ціль" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Потрібні інструменти" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Посібник" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Відкрити посібник{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Сайт" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / пакети" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Пристрій Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Ресурси Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Проєкт JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Прошивка" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / збій Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Логічні сигнали" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Поведінка процесу" @@ -14166,7 +14125,7 @@ msgstr "Поведінка процесу" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14176,18 +14135,18 @@ msgstr "" "{ph0}Посібник із Flutter{ph1} · {ph2}Пакети та ресурси{ph3} · {ph4}Байткод " "EVM{ph5} · {ph6}Усі посібники з дослідження{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Використовуйте REA з термінала" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Команди CLI, глобальне встановлення та curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14197,7 +14156,7 @@ msgstr "" "Замініть {ph0} назвою або адресою функції зі списку функцій цілі. Спочатку " "потрібно налаштувати Ghidra." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14206,7 +14165,7 @@ msgstr "" "У macOS або Linux після підготовки Node.js можна також скористатися " "інсталятором через curl:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14216,12 +14175,12 @@ msgstr "" "налаштування. У редакторах увімкніть REA в параметрах MCP і дозвольте його " "інструменти в чаті агента." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL і шляхи з пробілами" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14233,17 +14192,17 @@ msgstr "" "середовище Linux та використовуйте там шляхи Linux. Провайдери без " "графічного інтерфейсу працюють на тому самому хості, що й REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Команда налаштування для Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Оновіть або видаліть REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14254,7 +14213,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14268,7 +14227,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14291,18 +14250,18 @@ msgstr "Копіювати команду налаштування REA для Wi #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Копіювати команду" @@ -14360,14 +14319,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · виберіть модель ШІ" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Підготуйте файл реєстрації IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14382,7 +14341,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14395,7 +14354,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14408,12 +14367,12 @@ msgstr "" " бінарний файл у IDA та запустіть плагін на боці IDA, перш ніж просити REA " "його дослідити." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "База даних SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Байткод EVM" @@ -14472,7 +14431,7 @@ msgstr "" "Перегляньте й схваліть план, знову відкрийте Claude, а потім {ph0}перевірте " "підключення за кроком 3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14483,12 +14442,12 @@ msgstr "" "встановлення в macOS або Linux arm64 перед продовженням зберіть нативний " "декомпілятор для свого комп’ютера." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Зберіть нативний декомпілятор Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14501,7 +14460,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14511,7 +14470,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14521,13 +14480,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · інструкції з нативного збирання{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Потім перевірте Ghidra та збережіть шляхи до нього для свого клієнта:" @@ -14929,7 +14888,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Інші способи встановлення: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14944,7 +14903,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14958,7 +14917,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14970,7 +14929,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14981,7 +14940,7 @@ msgstr "" "який вивів інсталятор. У Linux стандартна команда налаштування — {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -15000,13 +14959,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15903,7 +15862,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15912,6 +15871,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper{ph1} (macOS або Linux ): програма встановлення REA виявляє його" +#~ " або пропонує встановити. На macOS відкрийте Hopper один раз і виберіть " +#~ "демонстраційний режим або активуйте ліцензію перед першим аналізом." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Налаштуйте REA{ph1} на macOS або Linux , потім завантажте ці два файли " +#~ "в одну папку. У Windows використовуйте середовище Linux, таке як WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Підтримувані дистрибутиви" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Хости Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS або " +#~ "Omarchy. Hopper — комерційна програма; обмеження демоверсії визначає " +#~ "розробник." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/ur-Latn/messages.po b/website/locales/ur-Latn/messages.po index 7c55293a3b..4d47425eda 100644 --- a/website/locales/ur-Latn/messages.po +++ b/website/locales/ur-Latn/messages.po @@ -111,13 +111,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -205,13 +205,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -228,13 +228,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -251,13 +251,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -274,13 +274,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -299,13 +299,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -4465,21 +4465,21 @@ msgstr "" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "" @@ -5074,7 +5074,7 @@ msgid "" msgstr "" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" @@ -5113,11 +5113,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -5158,14 +5153,7 @@ msgid "" " setup prepares the supported demo and its private display." msgstr "" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -5176,18 +5164,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -5195,7 +5183,7 @@ msgid "" msgstr "" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5206,7 +5194,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5220,7 +5208,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -5228,7 +5216,7 @@ msgstr "" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5236,12 +5224,12 @@ msgid "" "second confirms the compiler is installed:" msgstr "" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -5249,18 +5237,18 @@ msgid "" "12.1.x installation and a 64-bit JDK 21 or newer:" msgstr "" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." msgstr "" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -5268,12 +5256,12 @@ msgid "" "before continuing." msgstr "" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -5284,7 +5272,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -5294,7 +5282,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -5304,18 +5292,18 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -5330,7 +5318,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5338,12 +5326,12 @@ msgid "" "guide{ph3}" msgstr "" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -5351,7 +5339,7 @@ msgid "" " opens a private database." msgstr "" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -5364,7 +5352,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5376,7 +5364,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5384,14 +5372,14 @@ msgid "" "below." msgstr "" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5403,7 +5391,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5414,7 +5402,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5423,7 +5411,7 @@ msgid "" "upstream plugin before asking REA to inspect it." msgstr "" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -5433,13 +5421,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -5447,7 +5435,7 @@ msgid "" msgstr "" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5458,138 +5446,138 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr "" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "" @@ -5605,7 +5593,7 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5613,31 +5601,31 @@ msgid "" "bytecode{ph5} · {ph6}All investigation guides{ph7}" msgstr "" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5645,13 +5633,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -5662,7 +5650,7 @@ msgstr "" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -5674,7 +5662,7 @@ msgstr "" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5685,20 +5673,20 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -5707,27 +5695,27 @@ msgid "" msgstr "" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5735,7 +5723,7 @@ msgid "" "list. Ghidra must be configured first." msgstr "" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -5743,7 +5731,7 @@ msgid "" msgstr "" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5753,7 +5741,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5761,18 +5749,18 @@ msgid "" "installer. On Linux, the default setup command is {ph1}." msgstr "" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5780,39 +5768,39 @@ msgid "" "{ph0}; for Codex:" msgstr "" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" " REA in MCP settings and allow its tools in the agent chat." msgstr "" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -5820,17 +5808,17 @@ msgid "" "use Linux paths there. Headless providers run on the same host as REA." msgstr "" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -5839,7 +5827,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5851,7 +5839,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -5861,7 +5849,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5872,7 +5860,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5884,7 +5872,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5896,7 +5884,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -5963,18 +5951,18 @@ msgstr "" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "" @@ -5989,22 +5977,22 @@ msgctxt "page:get-started/index.html" msgid "Copy Notes investigation prompt" msgstr "" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "" @@ -7337,18 +7325,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7358,7 +7335,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -7372,14 +7349,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "" @@ -7390,7 +7367,7 @@ msgstr "" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7398,7 +7375,7 @@ msgid "" "Explain the calculation and show the code.{ph3}{ph4}{ph5}" msgstr "" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -7406,7 +7383,7 @@ msgid "" "same kind of question about another small Windows program you have." msgstr "" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "" @@ -7415,7 +7392,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7427,7 +7404,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7437,7 +7414,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7445,40 +7422,40 @@ msgid "" "appears." msgstr "" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7486,34 +7463,34 @@ msgid "" "tile coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -7521,7 +7498,7 @@ msgid "" "the center of the stereo field." msgstr "" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -7531,20 +7508,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7554,20 +7531,20 @@ msgid "" " menu action?”" msgstr "" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -7577,7 +7554,7 @@ msgid "" "session is open." msgstr "" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -7606,17 +7583,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "" @@ -9025,7 +9002,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "" @@ -9437,19 +9414,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9457,28 +9424,28 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9488,7 +9455,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9496,7 +9463,7 @@ msgid "" "different time interval?{ph1}" msgstr "" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "" @@ -9512,7 +9479,7 @@ msgstr "" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -9524,13 +9491,13 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -9570,12 +9537,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "" @@ -13927,7 +13894,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -13936,6 +13903,92 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" + #, python-brace-format #~ msgctxt "page:get-started/index.html" #~ msgid "" diff --git a/website/locales/vi/messages.po b/website/locales/vi/messages.po index 399f1456b1..e67edcf69a 100644 --- a/website/locales/vi/messages.po +++ b/website/locales/vi/messages.po @@ -71,13 +71,13 @@ msgstr "Bởi N0zoM1z0 * 10 tháng mười 2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -3278,21 +3278,21 @@ msgstr "Thiết bị đầu cuối" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "Sao chép" @@ -3521,14 +3521,14 @@ msgstr "" "nó xuất hiện." #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "" "Trong một thiết bị đầu cuối, đặt dấu ngoặc kép xung quanh một đường dẫn có " "chứa khoảng trắng." -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " @@ -3537,14 +3537,14 @@ msgstr "" "Phân tích THƯ mục ỨNG DỤNG JavaScript hoặc Electron hoặc KHO LƯU TRỮ ASAR mà" " không cần chạy ứng dụng:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "Phân tích ứng dụng tĩnh" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3554,13 +3554,13 @@ msgstr "" "Thay thế đường dẫn bằng thư mục ứng dụng được trích xuất của bạn hoặc {ph0} " "tập tin. TRÊN Windows, một đường dẫn như {ph1} hoạt động ở đây." -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "Tìm tệp .asar của ứng dụng ở đâu?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3571,7 +3571,7 @@ msgstr "Electron ứng dụng thường giữ nó trong một {ph0} thư mục:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3583,7 +3583,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3596,21 +3596,21 @@ msgstr "" #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0} Linux :{ph1} thường xuyên {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "" "Một số ứng dụng cài đặt ở nơi khác. Tìm kiếm thư mục của ứng dụng cho {ph0}." -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3622,36 +3622,36 @@ msgstr "" "kết quả." #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "Nếu bạn muốn một vĩnh viễn {ph0} lệnh trong shell của bạn:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "Cài ĐẶT CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}Tiếp tục VỚI hướng dẫn ứng DỤNG JavaScript{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "Kết nối CÀI ĐẶT Ghidra của bạn" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "Kết NỐI Ghidra TRÊN Windows" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3662,7 +3662,7 @@ msgstr "" " pe gốc trên các ổ đĩa ntfs cục bộ cố định. Sử DỤNG Ghidra 12.1 hiện có.x " "cài đặt VÀ 64-bit JDK 21 hoặc mới hơn:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3679,18 +3679,18 @@ msgstr "" "Kết nối Hopper TRÊN macOS hoặc Linux . Chạy lệnh thiết lập ở trên; nó phát " "hiện cài đặt hiện có hoặc cung cấp kế hoạch cài đặt để bạn phê duyệt." -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "Nhận trợ giúp" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "Nếu agent của bạn không THẤY REA" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3700,22 +3700,22 @@ msgstr "" "Khởi động lại hoặc kết nối lại máy khách sau khi thiết lập. Kiểm tra KẾT nối" " REA của NÓ với {ph0}; CHO Codex :" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "Nếu một nhà cung cấp cần chú ý" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "Kiểm tra nhà cung cấp bạn đang sử dụng. CHO Ghidra :" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "Kiểm tra một nhà cung cấp" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "Báo cáo đưa ra kiểm tra thất bại và một bước tiếp theo được đề xuất." @@ -3724,7 +3724,7 @@ msgstr "Báo cáo đưa ra kiểm tra thất bại và một bước tiếp theo #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3737,7 +3737,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3751,7 +3751,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3764,7 +3764,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3830,22 +3830,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "Sao CHÉP lời nhắc cài ĐẶT REA" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "Sao CHÉP lệnh PHÂN tích JavaScript" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "Sao CHÉP lệnh cài ĐẶT CLI" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "Sao chép lệnh chẩn đoán nhà cung cấp" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "Về đầu trang" @@ -5195,21 +5195,7 @@ msgstr "REA cần một trong số này để đọc các chương trình gốc: #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS hoặc Linux ): THIẾT lập CỦA REA phát hiện nó hoặc " -"đề nghị cài đặt nó. TRÊN macOS, mở Hopper một lần và chọn chế độ demo hoặc " -"kích hoạt giấy phép trước khi phân tích đầu tiên của bạn." - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5221,7 +5207,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -5236,14 +5222,14 @@ msgstr "" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Agent mã hóa của bạn" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "Sao chép" @@ -5254,7 +5240,7 @@ msgstr "Sao chép" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5264,7 +5250,7 @@ msgstr "" "{ph0}›{ph1}{ph2}SỬ dụng REA để tìm CÁCH DX-Ball tính toán định vị âm thanh " "nổi. Giải thích tính toán và hiển thị mã.{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -5276,7 +5262,7 @@ msgstr "" "bản sao của riêng bạn hoặc hỏi cùng một loại câu hỏi về một chương trình " "Windows nhỏ khác mà bạn có." -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "Làm cách nào để có được đường dẫn đầy đủ của tệp?" @@ -5285,7 +5271,7 @@ msgstr "Làm cách nào để có được đường dẫn đầy đủ của t #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5300,7 +5286,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5312,7 +5298,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5322,17 +5308,17 @@ msgstr "" "{ph0}Bất kỳ hệ thống:{ph1} kéo tệp vào cửa sổ đầu cuối và đường dẫn của nó " "xuất hiện." -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "Đọc KẾT QUẢ REA" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "Đọc đầu vào" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " @@ -5341,13 +5327,13 @@ msgstr "" "Các hướng dẫn tải một đối số nguyên và chuyển đổi nó cho số học dấu phẩy " "động. Đây là đầu vào vắng mặt từ chế độ xem mã giả đầu tiên." -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "Theo dõi hàm gọi" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5357,12 +5343,12 @@ msgstr "" "REA xác định hàm gọi gạch-hit. Các hướng dẫn trước đó của nó nhân tọa độ " "gạch với 30; thêm {ph0} (20) cho vị trí màn hình của gạch." -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "Đọc các hằng số được tham chiếu" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " @@ -5371,12 +5357,12 @@ msgstr "" "Số học tham chiếu các địa chỉ này. Đọc các byte cung cấp các giá trị thực " "tế; thứ tự hướng dẫn cho tác nhân biết cách chúng được sử dụng." -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "Viết và kiểm tra chức năng đã khôi phục" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -5387,7 +5373,7 @@ msgstr "" " khi chuyển đổi số nguyên. Tính toán đặt trung tâm của màn hình ở trung tâm " "của trường âm thanh nổi." -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -5400,20 +5386,20 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}Xem điều tra đầy đủ và so sánh assembly-To-c{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "Hãy thử nó với nhị phân của riêng bạn" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5427,7 +5413,7 @@ msgstr "" " để điều tra. Các câu hỏi bắt đầu hữu ích bao gồm \" tệp này được phân tích " "cú pháp Như thế nào?\"hoặc\" chức năng nào xử lý hành động menu này?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " @@ -5436,13 +5422,13 @@ msgstr "" "Nếu bạn đã biết tên hoặc địa chỉ chức năng, bạn có thể truy vấn nó từ thiết " "bị đầu cuối. Để CÀI ĐẶT Ghidra được định cấu HÌNH:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "Phân tích chức năng" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -5456,7 +5442,7 @@ msgstr "" "phiên của NÓ. Các truy vấn MCP của agent của bạn sử dụng lại nhị phân đã " "nhập trong khi phiên đó đang mở." -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -5490,17 +5476,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "Phân tích các chương trình mã máy" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "Sao chép ví dụ dấu nhắc" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "Sao chép phân tích chức năng" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "Về đầu trang" @@ -6478,7 +6464,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Agent mã hóa của bạn" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "Sao chép" @@ -6886,21 +6872,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "Tự kiểm tra APK" -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"{ph0}Thiết LẬP REA {ph1} TRÊN macOS hoặc Linux, sau đó tải hai tệp này vào " -"cùng một thư mục. TRÊN Windows, sử dụng MÔI trường Linux như WSL." - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6910,18 +6884,18 @@ msgstr "" "Phân tích Android cần MỘT jdk 17 đầy đủ hoặc mới hơn. Chọn nó với {ph0} hoặc" " {ph1}. Trường hợp này sử DỤNG JDK 21." -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "Điểm REA tại tệp nhà cung cấp và kiểm TRA APK :" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS / Linux" #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6934,7 +6908,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6944,7 +6918,7 @@ msgstr "" "Một câu hỏi tiếp theo: {ph0}điều gì xảy ra khi một tài khoản sử dụng tám chữ" " số hoặc một khoảng thời gian khác nhau?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "Nguồn" @@ -6960,7 +6934,7 @@ msgstr "Nguồn" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6974,14 +6948,14 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "" "{ph0}Rfc 6238 trường hợp tham khảo{ph1} · {ph2}Dữ liệu thử nghiệm Demo{ph3}" -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -7026,12 +7000,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "Thực hiện theo tính toán mã đăng nhập" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "Sao CHÉP lệnh kiểm TRA APK" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "Về đầu trang" @@ -10802,13 +10776,13 @@ msgstr "Hướng dẫn" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -10825,13 +10799,13 @@ msgstr "Mã nguồn" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -10848,13 +10822,13 @@ msgstr "Giấy phép MIT" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -10871,13 +10845,13 @@ msgstr "Tham gia cộng đồng" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10962,22 +10936,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "KẾT quả REA · tóm tắt.trích đoạn ipc" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "TRÍCH ĐOẠN HƯỚNG dẫn REA · 0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA hàm gọi trích đoạn * 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "KẾT quả byte REA * được hiểu là đôi endian nhỏ" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "Phục Hồi C · trích Đoạn" @@ -11038,12 +11012,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA * generateOTP · cơ thể hoàn chỉnh" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "Tải XUỐNG Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "Tải Xuống Nhà cung Cấp Phân Tích Android" @@ -13709,11 +13683,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "Không khả dụng" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "Các bản phân phối được hỗ trợ" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -13751,17 +13720,7 @@ msgstr "" " Trên Linux, trình thiết lập chuẩn bị bản demo được hỗ trợ cùng màn hình " "riêng của nó." -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS hoặc " -"Omarchy. Hopper là phần mềm thương mại; bản demo có các giới hạn do nhà cung" -" cấp quy định." - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -13774,18 +13733,18 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Thiết lập Hopper và xử lý sự cố{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · cài công cụ phân tích miễn phí và Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -13795,7 +13754,7 @@ msgstr "" "với kiến trúc máy tính của bạn." #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13808,7 +13767,7 @@ msgstr "" #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13823,7 +13782,7 @@ msgstr "" #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -13831,7 +13790,7 @@ msgstr "{ph0}Tải JDK{ph1} · {ph2}Tải Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13841,7 +13800,7 @@ msgstr "" "Kiểm tra Java. Lệnh đầu hiển thị {ph0}; dùng thư mục đó cho {ph1}. Lệnh thứ " "hai xác nhận đã cài trình biên dịch:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -13859,7 +13818,7 @@ msgstr "" #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13869,12 +13828,12 @@ msgstr "" "{ph0}Thiết lập Ghidra và yêu cầu kiến trúc{ph1} · {ph2}Hướng dẫn Ghidra trên" " Windows{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · kết nối bản cài đặt sẵn có" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -13885,7 +13844,7 @@ msgstr "" "IDA. Chọn kết nối với GUI đang mở tệp nhị phân gốc, hoặc dùng tiến trình " "giám sát không có giao diện để mở cơ sở dữ liệu riêng." -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -13897,7 +13856,7 @@ msgstr "" "arm64 với IDA Professional 9.1. Cầu nối GUI native tùy chọn có yêu cầu riêng" " về phiên bản và nền tảng." -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -13909,13 +13868,13 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}Cấu hình IDA, ví dụ đăng ký và nền tảng đã kiểm chứng{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -13926,18 +13885,18 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}Làm theo ví dụ phân tích mã máy{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "Website, Android, .NET, firmware và các đối tượng khác" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " @@ -13946,110 +13905,110 @@ msgstr "" "Mở hướng dẫn cho đối tượng bạn có. Mỗi hướng dẫn nêu công cụ bổ sung, host " "được hỗ trợ và lệnh đầu tiên." -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "Đối tượng" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "Công cụ cần chuẩn bị" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "Hướng dẫn" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}Mở hướng dẫn{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "Website" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / gói phần mềm" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Thiết bị Android" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Tài nguyên Android" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "Dự án JEB" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "Firmware" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / sự cố Linux" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "Tín hiệu logic" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "Hành vi tiến trình" @@ -14065,7 +14024,7 @@ msgstr "Hành vi tiến trình" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14075,18 +14034,18 @@ msgstr "" "{ph0}Hướng dẫn Flutter{ph1} · {ph2}Gói và tài nguyên{ph3} · {ph4}Bytecode " "EVM{ph5} · {ph6}Tất cả hướng dẫn điều tra{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "Dùng REA từ terminal" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "Lệnh CLI, cài đặt toàn cục và curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14096,7 +14055,7 @@ msgstr "" "Thay {ph0} bằng tên hàm hoặc địa chỉ trong danh sách hàm của đối tượng. Bạn " "phải cấu hình Ghidra trước." -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " @@ -14105,7 +14064,7 @@ msgstr "" "Trên macOS hoặc Linux, bạn cũng có thể dùng bộ cài qua curl sau khi chuẩn bị" " Node.js:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" @@ -14115,12 +14074,12 @@ msgstr "" "trình soạn thảo, bật REA trong cài đặt MCP và cho phép dùng công cụ trong " "cuộc trò chuyện với trợ lý AI." -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows, WSL và đường dẫn có khoảng trắng" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -14132,17 +14091,17 @@ msgstr "" "đó và dùng đường dẫn Linux. Các công cụ phân tích không có giao diện chạy " "trên cùng host với REA." -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Lệnh thiết lập trên Windows:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "Cập nhật hoặc gỡ REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -14153,7 +14112,7 @@ msgstr "" #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14167,7 +14126,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -14190,18 +14149,18 @@ msgstr "Sao chép lệnh thiết lập REA trên Windows" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "Sao chép lệnh" @@ -14258,14 +14217,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · chọn mô hình AI" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "Chuẩn bị tệp đăng ký IDA" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14280,7 +14239,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14293,7 +14252,7 @@ msgstr "" #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14305,12 +14264,12 @@ msgstr "" "động và {ph2} nếu có vào tệp. Mở tệp nhị phân gốc trong IDA và khởi chạy " "plugin phía IDA trước khi yêu cầu REA kiểm tra." -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "Cơ sở dữ liệu SQLite" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "Bytecode EVM" @@ -14367,7 +14326,7 @@ msgstr "" "Xem và chấp thuận kế hoạch, mở lại Claude rồi {ph0}kiểm tra kết nối ở bước " "3{ph1}." -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -14378,12 +14337,12 @@ msgstr "" "macOS hoặc Linux arm64, hãy biên dịch bộ dịch ngược native cho máy tính của " "bạn trước khi tiếp tục." -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "Biên dịch bộ dịch ngược native của Ghidra" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -14396,7 +14355,7 @@ msgstr "" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -14406,7 +14365,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -14416,13 +14375,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · hướng dẫn biên dịch native{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "Sau đó kiểm tra Ghidra và lưu đường dẫn cho ứng dụng kết nối của bạn:" @@ -14818,7 +14777,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "Cách cài đặt khác: {ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14833,7 +14792,7 @@ msgstr "" #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14848,7 +14807,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14860,7 +14819,7 @@ msgstr "" #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14871,7 +14830,7 @@ msgstr "" " đặt in ra. Trên Linux, lệnh thiết lập mặc định là {ph1}." #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -14890,13 +14849,13 @@ msgstr "" #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -15790,7 +15749,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -15799,6 +15758,100 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS hoặc Linux ): THIẾT lập CỦA REA phát hiện nó hoặc " +#~ "đề nghị cài đặt nó. TRÊN macOS, mở Hopper một lần và chọn chế độ demo hoặc " +#~ "kích hoạt giấy phép trước khi phân tích đầu tiên của bạn." + +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "{ph0}Thiết LẬP REA {ph1} TRÊN macOS hoặc Linux, sau đó tải hai tệp này vào " +#~ "cùng một thư mục. TRÊN Windows, sử dụng MÔI trường Linux như WSL." + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "Các bản phân phối được hỗ trợ" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Host Linux: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS hoặc " +#~ "Omarchy. Hopper là phần mềm thương mại; bản demo có các giới hạn do nhà cung" +#~ " cấp quy định." + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/zh-CN/messages.po b/website/locales/zh-CN/messages.po index 89d0095bb7..ed3fedc6d5 100644 --- a/website/locales/zh-CN/messages.po +++ b/website/locales/zh-CN/messages.po @@ -69,13 +69,13 @@ msgstr "作者 N0zoM1z0 · 2026 年 10 月 10 日" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -2888,21 +2888,21 @@ msgstr "终端机" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "复制" @@ -3109,26 +3109,26 @@ msgid "" msgstr "{ph0}所有系统:{ph1}把文件夹拖进终端窗口,也能显示路径。" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "在终端里,包含空格的路径需要加引号。" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "静态分析 JavaScript 或 Electron 应用目录、ASAR 文件,无需启动应用:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "静态应用分析" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3136,13 +3136,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "把路径换成解包后的应用目录或 {ph0} 文件。Windows 上也可以使用 {ph1} 这样的路径。" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "在哪里找到应用的 .asar 文件?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3153,7 +3153,7 @@ msgstr "Electron 应用通常把它放在 {ph0} 文件夹中:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3165,7 +3165,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3176,20 +3176,20 @@ msgstr "{ph0}macOS:{ph1}{ph2}(右键应用,选择{ph3}“显示包内容 #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1}通常位于 {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "有些应用安装在其他位置。在应用目录里搜索 {ph0}。" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3198,36 +3198,36 @@ msgid "" msgstr "结果会识别应用,展示模块之间的关系,并给出源码位置。REA 无法解析的关系会在结果中注明。" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "如果希望在 shell 中直接使用 {ph0} 命令:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "安装CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}继续看 JavaScript 应用指南{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "连接已有的 Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "在 Windows 上连接 Ghidra" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3237,7 +3237,7 @@ msgstr "" "Windows x64 提供实验性的只读支持:分析固定本地 NTFS 驱动器上的原生 x86、x64 PE 应用和 DLL。使用已有的 Ghidra " "12.1.x 和 64 位 JDK 21 或更新版本:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3250,18 +3250,18 @@ msgid "" " existing installation or offers an installation plan for your approval." msgstr "在 macOS 或 Linux 上连接 Hopper。运行上面的 setup 命令,它会检测已有安装,或提出安装计划供你确认。" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "寻求帮助" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "助手看不到 REA 工具时" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3269,22 +3269,22 @@ msgid "" "{ph0}; for Codex:" msgstr "配置后重启或重新连接客户端。用 {ph0} 检查 REA 连接。以 Codex 为例:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "分析后端出现问题时" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "检查正在使用的分析后端。以 Ghidra 为例:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "检查一个分析后端" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "报告会列出失败的检查和建议的下一步。" @@ -3293,7 +3293,7 @@ msgstr "报告会列出失败的检查和建议的下一步。" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3304,7 +3304,7 @@ msgstr "{ph0}REA FAQ{ph1}{ph2}关于更新、助手和分析的常见解答。{p #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3316,7 +3316,7 @@ msgstr "{ph0}选择调查指南{ph1}{ph2}二进制、Electron 应用和浏览器 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3328,7 +3328,7 @@ msgstr "{ph0}GitHub issues{ph1}{ph2}附上命令与诊断结果,反馈问题 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3389,22 +3389,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "复制REA安装提示" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "复制JavaScript分析命令" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "复制CLI安装命令" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "复制提供者诊断命令" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "回到顶部" @@ -4634,20 +4634,7 @@ msgstr "REA需要其中一个来阅读原生程序:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0}Hopper{ph1}(macOS 或 Linux):REA 的安装与配置流程会检测到它,或提示你安装。在 macOS 上,先打开一次 " -"Hopper,选择演示模式或在首次分析前激活许可证。" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4659,7 +4646,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -4673,14 +4660,14 @@ msgstr "具体步骤参见{ph0}配置原生分析{ph1}。" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}你的编程助手" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "复制" @@ -4691,7 +4678,7 @@ msgstr "复制" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4700,7 +4687,7 @@ msgid "" msgstr "" "{ph0}›{ph1}{ph2}用 REA 找出 DX-Ball 如何计算声音的左右位置。解释计算过程,并展示代码。{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -4710,7 +4697,7 @@ msgstr "" "示例提示词。把本地 DX-Ball 1.07 可执行文件的路径交给编程助手。REA 不附带 DX-" "Ball,请使用你自己的副本;也可以就你手上的另一个小型 Windows 程序问同样的问题。" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "如何获取文件的完整路径?" @@ -4719,7 +4706,7 @@ msgstr "如何获取文件的完整路径?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4733,7 +4720,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4743,7 +4730,7 @@ msgstr "{ph0} macOS :{ph1} 在Finder中,右键单击文件,按住选项并 #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4751,30 +4738,30 @@ msgid "" "appears." msgstr "{ph0}任何系统:{ph1} 将文件拖到终端窗口中并显示其路径。" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "阅读REA结果" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "阅读输入" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "指令加载一个整数参数并将其转换为浮点算术。 这是第一个伪代码视图中没有的输入。" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "跟随调用方" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4782,24 +4769,24 @@ msgid "" "tile coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "REA 找到砖块碰撞处理函数。它先把砖块横坐标乘以 30,再加上 {ph0}(20),得到砖块的屏幕位置。" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "读取引用的常量" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "算术引用这些地址。 读取字节提供实际值;指令顺序告诉编程助手如何使用它们。" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "写入并检查恢复的函数" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -4807,7 +4794,7 @@ msgid "" "the center of the stereo field." msgstr "比例为 1.0 时,屏幕坐标 0、320 和 640 在整数转换前映射到 -500、0 和 500。这一计算把屏幕正中放在立体声场的中心。" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -4817,20 +4804,20 @@ msgstr "编程助手使用REA的发现来编写C.单独的检查将3,205个输 #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}查看完整调查过程和汇编与 C 的对比{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "用你自己的二进制文件试试" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4841,20 +4828,20 @@ msgid "" msgstr "" "{ph0}连接原生分析提供程序{ph1},然后给编程助手一个本地可执行文件或库,并指定要调查的功能。有用的起始问题包括“如何解析这个文件?”或“哪个函数处理这个菜单操作?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "如果你已经知道函数名称或地址,则可以从终端查询它。 对于已配置的Ghidra安装:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "函数分析" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4866,7 +4853,7 @@ msgstr "" "替换目标路径和 {ph0} 使用你自己的函数名称或地址。 每个 CLI 调用导入并分析二进制文件,然后关闭其会话。 你的编程助手 MCP " "查询在该会话打开时重用导入的二进制文件。" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -4895,17 +4882,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "机器代码程序分析" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "复制示例提示" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "复制函数分析" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "回到顶部" @@ -5831,7 +5818,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}你的编程助手" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "复制" @@ -6215,22 +6202,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "亲自检查APK" -# Reviewed for clear case-study reasoning and evidence. -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"先在 macOS 或 Linux 上{ph0}设置 REA{ph1},再将这两个文件下载到同一文件夹。Windows 上可使用 WSL 等 Linux " -"环境。" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6238,19 +6212,19 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "Android 分析需要完整的 JDK 17 或更高版本。通过设置 {ph0} 或 {ph1} 来选择它。本案例使用了 JDK 21。" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "让 REA 使用分析后端配置文件,再检查 APK:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS/Linux" # Reviewed for clear case-study reasoning and evidence. #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6260,7 +6234,7 @@ msgstr "助手可以通过 REA CLI 继续使用上方的 prompt。如果使用 M #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6268,7 +6242,7 @@ msgid "" "different time interval?{ph1}" msgstr "下一个问题: {ph0}当帐户使用八位数字或不同的时间间隔时会发生什么?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "资料来源" @@ -6284,7 +6258,7 @@ msgstr "资料来源" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6296,14 +6270,14 @@ msgstr "{ph0}官方Aegis3.4.3发布{ph1} · {ph2}匹配源提交{ph3} · {ph4}GP #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC6238参考案例{ph1} · {ph2}演示测试数据{ph3}" # Reviewed for clear case-study reasoning and evidence. -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6344,12 +6318,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "按照登录代码计算" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "复制APK检查命令" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "回到顶部" @@ -9915,13 +9889,13 @@ msgstr "指南" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -9938,13 +9912,13 @@ msgstr "源代码" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -9961,13 +9935,13 @@ msgstr "MIT 许可证" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -9984,13 +9958,13 @@ msgstr "加入社区" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10075,22 +10049,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA 结果 · summary.ipc 摘录" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA指令摘录·0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA调用方摘录 · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA字节结果 · 解释为little-endian双精度浮点数" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "恢复C · 摘录" @@ -10151,12 +10125,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA · generateOTP 完整函数体" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "下载Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "下载 Android 分析提供程序" @@ -12508,11 +12482,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "不支持" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "支持的发行版" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -12548,16 +12517,7 @@ msgstr "" "在 setup 中选择 Hopper,查看它的独立安装计划。macOS 首次打开 Hopper 时选择试用模式或激活许可证;Linux 的 setup " "会准备支持的试用版及其专用显示环境。" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linux 支持 Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch Linux、CachyOS 和 " -"Omarchy。Hopper 是商业软件,试用版有厂商规定的限制。" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -12568,18 +12528,18 @@ msgstr "如果 macOS 请求自动化权限,在“系统设置 → 隐私与安 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopper 安装与故障排查{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · 安装免费的分析工具和 Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -12587,7 +12547,7 @@ msgid "" msgstr "安装包含 Java 编译器的 64 位 JDK 21 或更新版本。下载时选择符合电脑架构的安装包。" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12598,7 +12558,7 @@ msgstr "从 Ghidra 12.1.4 发布页的 Assets 列表下载并解压 {ph0}。选 #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12612,7 +12572,7 @@ msgstr "找到解压后包含 {ph0} 或 {ph1} 的文件夹,把该文件夹的 #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -12620,7 +12580,7 @@ msgstr "{ph0}下载 JDK{ph1} · {ph2}下载 Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12628,7 +12588,7 @@ msgid "" "second confirms the compiler is installed:" msgstr "检查 Java。第一条命令会显示 {ph0},将该文件夹用于 {ph1};第二条确认编译器已安装:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -12643,7 +12603,7 @@ msgstr "在同一个终端会话中运行这些命令。设置路径后重新运 #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12651,12 +12611,12 @@ msgid "" "guide{ph3}" msgstr "{ph0}Ghidra 配置与架构要求{ph1} · {ph2}Windows Ghidra 指南{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · 连接已有安装" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -12665,7 +12625,7 @@ msgid "" msgstr "" "按 IDA provider 指南准备上游 MCP 配置。可以连接已打开原始程序的 IDA 界面,或使用创建独立数据库的无界面 supervisor。" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -12675,7 +12635,7 @@ msgstr "" "已验证的配置包括 Windows 旧版 GUI 与无界面 supervisor,以及 macOS arm64 上的 IDA Professional " "9.1 无界面 supervisor。可选的原生 GUI 桥接另有版本和平台要求。" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -12685,13 +12645,13 @@ msgstr "使用按 provider 指南准备好的 JSON 配置文件,再运行 setu #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}IDA 配置、注册示例与已验证平台{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -12700,128 +12660,128 @@ msgstr "连接后,把原始可执行文件的完整路径和分析工具名称 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}跟着示例分析原生程序{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "网站、Android、.NET、固件及其他目标" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "打开对应目标的指南,其中会列出额外工具、支持的平台和第一条分析命令。" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "目标" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "需要准备的工具" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "指南" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}查看指南{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "网站" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / 软件包" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android 设备" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Android 资源" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEB 项目" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "固件" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linux 崩溃记录" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "逻辑信号" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "进程行为" @@ -12837,7 +12797,7 @@ msgstr "进程行为" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12847,18 +12807,18 @@ msgstr "" "{ph0}Flutter 指南{ph1} · {ph2}软件包与资源{ph3} · {ph4}EVM 字节码{ph5} · " "{ph6}全部分析指南{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "在终端中使用 REA" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLI 命令、全局安装与 curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12866,26 +12826,26 @@ msgid "" "list. Ghidra must be configured first." msgstr "把 {ph0} 换成目标函数列表中的名称或地址。先完成 Ghidra 配置再运行。" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " "Node.js:" msgstr "macOS 或 Linux 准备好 Node.js 后,也可以使用 curl 安装:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" " REA in MCP settings and allow its tools in the agent chat." msgstr "在第 3 步的检查命令中填写你的 setup ID。使用编辑器时,在 MCP 设置中启用 REA,并在助手聊天中允许使用它的工具。" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows、WSL 与含空格的路径" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -12895,17 +12855,17 @@ msgstr "" "Windows 客户端和分析工具使用 Windows PowerShell。如果编程助手在 WSL 内运行,就在那个 Linux 环境中安装 " "Node.js 和 REA,并使用 Linux 路径。无界面 provider 与 REA 在同一个系统环境中运行。" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Windows setup 命令:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "更新或移除 REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -12914,7 +12874,7 @@ msgstr "要更新编程助手的连接,重新运行最新版 setup,查看并 #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12926,7 +12886,7 @@ msgstr "全局安装的 CLI 用 {ph0} 更新。使用 {ph1} 移除 REA 的客户 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -12949,18 +12909,18 @@ msgstr "复制 Windows REA setup 命令" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "复制命令" @@ -13014,14 +12974,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · 选择 AI 模型" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "准备 IDA 配置文件" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13035,7 +12995,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13046,7 +13006,7 @@ msgstr "如果使用提供 {ph0}、{ph1} 和 {ph2} 的无界面 supervisor,文 #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13057,12 +13017,12 @@ msgstr "" "如果连接旧版 1.4.0 GUI 服务,把可用的 {ph0}、{ph1} 和可选的 {ph2} 复制到文件中。先在 IDA " "中打开原始程序并启动上游插件,再让 REA 分析。" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLite 数据库" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVM 字节码" @@ -13113,7 +13073,7 @@ msgid "" "in step 3{ph1}." msgstr "查看并确认计划,重新打开 Claude,再{ph0}按第 3 步检查连接{ph1}。" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -13122,12 +13082,12 @@ msgid "" msgstr "" "把两个路径换成你已有的安装位置。如果是全新安装在 macOS 或 Linux arm64 上,请先为这台电脑编译原生反编译器,再继续下面的步骤。" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "编译 Ghidra 的原生反编译器" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -13138,7 +13098,7 @@ msgstr "macOS 编译需要 Xcode Command Line Tools、make 和 bison。选择符 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -13148,7 +13108,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -13158,13 +13118,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · 原生组件编译说明{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "然后检查 Ghidra,并为客户端保存这些路径:" @@ -13521,7 +13481,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "其他安装方式:{ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13534,7 +13494,7 @@ msgstr "按设置提示连接你的助手。如果没有出现提示,运行 {p #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13547,7 +13507,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13557,7 +13517,7 @@ msgstr "在已构建的源码目录中,运行 {ph0} 和 {ph1},不要使用 #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13566,7 +13526,7 @@ msgid "" msgstr "如果终端找不到 {ph0},请使用安装器打印的可执行文件路径。在 Linux 上,默认的设置命令是 {ph1}。" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13582,13 +13542,13 @@ msgstr "用 REA 通过我已配置的分析引擎检查 {ph0}。列出它的函 #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -14457,7 +14417,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -14466,6 +14426,99 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0}Hopper{ph1}(macOS 或 Linux):REA 的安装与配置流程会检测到它,或提示你安装。在 macOS 上,先打开一次 " +#~ "Hopper,选择演示模式或在首次分析前激活许可证。" + +# Reviewed for clear case-study reasoning and evidence. +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "先在 macOS 或 Linux 上{ph0}设置 REA{ph1},再将这两个文件下载到同一文件夹。Windows 上可使用 WSL 等 Linux " +#~ "环境。" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "支持的发行版" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linux 支持 Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch Linux、CachyOS 和 " +#~ "Omarchy。Hopper 是商业软件,试用版有厂商规定的限制。" + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/locales/zh-TW/messages.po b/website/locales/zh-TW/messages.po index 3ef503a00e..28937dfd0d 100644 --- a/website/locales/zh-TW/messages.po +++ b/website/locales/zh-TW/messages.po @@ -69,13 +69,13 @@ msgstr "透過N0zoM1z0*10十月2026" #: website/public/examples/dino-lab/index.html:484 #: website/public/faq/index.html:582 #: website/public/first-investigation/index.html:543 -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 #: website/public/guides/browser/index.html:516 #: website/public/guides/index.html:310 #: website/public/guides/javascript/index.html:520 -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 #: website/public/guides/rl-environments/index.html:507 -#: website/public/index.html:1312 website/public/showcase/aegis/index.html:817 +#: website/public/index.html:1312 website/public/showcase/aegis/index.html:824 #: website/public/showcase/ctf/index.html:786 #: website/public/showcase/dx-ball/index.html:794 #: website/public/showcase/index.html:346 @@ -2891,21 +2891,21 @@ msgstr "終端機" #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 #: website/public/get-started/index.html:928 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1598 -#: website/public/get-started/index.html:1657 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1741 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1601 +#: website/public/get-started/index.html:1660 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1744 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy" msgstr "複製" @@ -3117,26 +3117,26 @@ msgid "" msgstr "{ph0}所有系統:{ph1}把資料夾拖進終端視窗,也能顯示路徑。" #: website/public/get-started/index.html:1006 -#: website/public/get-started/index.html:1764 +#: website/public/get-started/index.html:1767 msgctxt "page:get-started/index.html" msgid "In a terminal, put quotes around a path that contains spaces." msgstr "在終端裡,包含空格的路徑需要加引號。" -#: website/public/get-started/index.html:1591 +#: website/public/get-started/index.html:1594 msgctxt "page:get-started/index.html" msgid "" "Analyze a JavaScript or Electron application folder or ASAR archive without " "running the app:" msgstr "靜態分析 JavaScript 或 Electron 應用目錄、ASAR 檔案,無需啟動應用:" -#: website/public/get-started/index.html:1597 +#: website/public/get-started/index.html:1600 msgctxt "page:get-started/index.html" msgid "Static application analysis" msgstr "靜態應用分析" #. {ph0}: .asar #. {ph1}: "D:/apps/example" -#: website/public/get-started/index.html:1609 +#: website/public/get-started/index.html:1612 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3144,13 +3144,13 @@ msgid "" "Windows, a path such as {ph1} works here." msgstr "把路徑換成解包後的應用目錄或 {ph0} 檔案。Windows 上也可以使用 {ph1} 這樣的路徑。" -#: website/public/get-started/index.html:1615 +#: website/public/get-started/index.html:1618 msgctxt "page:get-started/index.html" msgid "Where do I find an app's .asar file?" msgstr "在哪裡找到應用的 .asar 檔案?" #. {ph0}: resources -#: website/public/get-started/index.html:1616 +#: website/public/get-started/index.html:1619 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Electron apps usually keep it in a {ph0} folder:" @@ -3161,7 +3161,7 @@ msgstr "Electron 應用通常把它放在 {ph0} 資料夾中:" #. {ph2}: C:\Users\<you>\AppData\Local\Programs\<App>\resources\app.asar -#: website/public/get-started/index.html:1621 +#: website/public/get-started/index.html:1624 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Windows:{ph1} {ph2}" @@ -3173,7 +3173,7 @@ msgstr "{ph0} Windows :{ph1} {ph2}" #. >/Applications/<App>.app/Contents/Resources/app.asar #. {ph3}: #. {ph4}: -#: website/public/get-started/index.html:1627 +#: website/public/get-started/index.html:1630 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3184,20 +3184,20 @@ msgstr "{ph0}macOS:{ph1}{ph2}(右鍵應用,選擇{ph3}“顯示包內容 #. {ph0}: #. {ph1}: #. {ph2}: /opt/<App>/resources/app.asar -#: website/public/get-started/index.html:1635 +#: website/public/get-started/index.html:1638 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux:{ph1} often {ph2}" msgstr "{ph0}Linux:{ph1}通常位於 {ph2}" #. {ph0}: app.asar -#: website/public/get-started/index.html:1640 +#: website/public/get-started/index.html:1643 #, python-brace-format msgctxt "page:get-started/index.html" msgid "Some apps install elsewhere. Search the app's folder for {ph0}." msgstr "有些應用安裝在其他位置。在應用目錄裡搜尋 {ph0}。" -#: website/public/get-started/index.html:1645 +#: website/public/get-started/index.html:1648 msgctxt "page:get-started/index.html" msgid "" "The result identifies the app, shows connections between modules and gives " @@ -3206,36 +3206,36 @@ msgid "" msgstr "結果會識別應用,展示模組之間的關係,並給出原始碼位置。REA 無法解析的關係會在結果中註明。" #. {ph0}: rea -#: website/public/get-started/index.html:1650 +#: website/public/get-started/index.html:1653 #, python-brace-format msgctxt "page:get-started/index.html" msgid "If you'd like a permanent {ph0} command in your shell:" msgstr "如果希望在 shell 中直接使用 {ph0} 命令:" -#: website/public/get-started/index.html:1656 +#: website/public/get-started/index.html:1659 msgctxt "page:get-started/index.html" msgid "Install the CLI" msgstr "安裝CLI" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1668 +#: website/public/get-started/index.html:1671 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Continue with the JavaScript application guide{ph1}" msgstr "{ph0}繼續看 JavaScript 應用指南{ph1}" -#: website/public/get-started/index.html:1185 +#: website/public/get-started/index.html:1188 msgctxt "page:get-started/index.html" msgid "Connect your Ghidra installation" msgstr "連線已有的 Ghidra" -#: website/public/get-started/index.html:1155 +#: website/public/get-started/index.html:1158 msgctxt "page:get-started/index.html" msgid "Connect Ghidra on Windows" msgstr "在 Windows 上連線 Ghidra" -#: website/public/get-started/index.html:1156 +#: website/public/get-started/index.html:1159 msgctxt "page:get-started/index.html" msgid "" "Windows x64 has experimental, read-only support for native x86 and x64 PE " @@ -3245,7 +3245,7 @@ msgstr "" "Windows x64 提供實驗性的只讀支援:分析固定本地 NTFS 驅動器上的原生 x86、x64 PE 應用和 DLL。使用已有的 Ghidra " "12.1.x 和 64 位 JDK 21 或更新版本:" -#: website/public/get-started/index.html:1179 +#: website/public/get-started/index.html:1182 msgctxt "page:get-started/index.html" msgid "" "Replace the two paths with your installations, then review setup's plan." @@ -3258,18 +3258,18 @@ msgid "" " existing installation or offers an installation plan for your approval." msgstr "在 macOS 或 Linux 上連線 Hopper。執行上面的 setup 命令,它會檢測已有安裝,或提出安裝計劃供你確認。" -#: website/public/get-started/index.html:1722 +#: website/public/get-started/index.html:1725 msgctxt "page:get-started/index.html" msgid "Getting help" msgstr "尋求幫助" -#: website/public/get-started/index.html:1724 +#: website/public/get-started/index.html:1727 msgctxt "page:get-started/index.html" msgid "If your agent doesn't see REA" msgstr "助手看不到 REA 工具時" #. {ph0}: doctor -#: website/public/get-started/index.html:1725 +#: website/public/get-started/index.html:1728 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3277,22 +3277,22 @@ msgid "" "{ph0}; for Codex:" msgstr "配置後重啟或重新連線客戶端。用 {ph0} 檢查 REA 連線。以 Codex 為例:" -#: website/public/get-started/index.html:1736 +#: website/public/get-started/index.html:1739 msgctxt "page:get-started/index.html" msgid "If a provider needs attention" msgstr "分析後端出現問題時" -#: website/public/get-started/index.html:1737 +#: website/public/get-started/index.html:1740 msgctxt "page:get-started/index.html" msgid "Check the provider you're using. For Ghidra:" msgstr "檢查正在使用的分析後端。以 Ghidra 為例:" -#: website/public/get-started/index.html:1740 +#: website/public/get-started/index.html:1743 msgctxt "page:get-started/index.html" msgid "Check one provider" msgstr "檢查一個分析後端" -#: website/public/get-started/index.html:1752 +#: website/public/get-started/index.html:1755 msgctxt "page:get-started/index.html" msgid "The report gives the failed check and a suggested next step." msgstr "報告會列出失敗的檢查和建議的下一步。" @@ -3301,7 +3301,7 @@ msgstr "報告會列出失敗的檢查和建議的下一步。" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1817 +#: website/public/get-started/index.html:1820 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3312,7 +3312,7 @@ msgstr "{ph0}REA FAQ{ph1}{ph2}關於更新、助手和分析的常見解答。{p #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1821 +#: website/public/get-started/index.html:1824 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3324,7 +3324,7 @@ msgstr "{ph0}選擇調查指南{ph1}{ph2}二進位制、Electron 應用和瀏覽 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1828 +#: website/public/get-started/index.html:1831 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3336,7 +3336,7 @@ msgstr "{ph0}GitHub issues{ph1}{ph2}附上命令與診斷結果,反饋問題 #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1832 +#: website/public/get-started/index.html:1835 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -3399,22 +3399,22 @@ msgctxt "page:get-started/index.html" msgid "Copy REA installation prompt" msgstr "複製REA安裝提示" -#: website/public/get-started/index.html:1598 +#: website/public/get-started/index.html:1601 msgctxt "page:get-started/index.html" msgid "Copy JavaScript analysis command" msgstr "複製JavaScript分析命令" -#: website/public/get-started/index.html:1657 +#: website/public/get-started/index.html:1660 msgctxt "page:get-started/index.html" msgid "Copy CLI installation command" msgstr "複製CLI安裝命令" -#: website/public/get-started/index.html:1741 +#: website/public/get-started/index.html:1744 msgctxt "page:get-started/index.html" msgid "Copy provider diagnosis command" msgstr "複製提供者診斷命令" -#: website/public/get-started/index.html:1857 +#: website/public/get-started/index.html:1860 msgctxt "page:get-started/index.html" msgid "Back to top" msgstr "回到頂部" @@ -4642,20 +4642,7 @@ msgstr "REA需要其中一個來閱讀原生程式:" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:316 -#, python-brace-format -msgctxt "page:guides/native/index.html" -msgid "" -"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " -"install it. On macOS, open Hopper once and choose demo mode or activate a " -"license before your first analysis." -msgstr "" -"{ph0} Hopper {ph1} (macOS或Linux):REA的安裝程式檢測到它或提供安裝它。 " -"在macOS上,開啟一次Hopper,然後選擇演示模式或在第一次分析之前啟用許可證。" - -#. {ph0}: -#. {ph1}: -#: website/public/guides/native/index.html:322 +#: website/public/guides/native/index.html:323 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4667,7 +4654,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:334 +#: website/public/guides/native/index.html:335 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "See {ph0}Set up native analysis{ph1} for the steps." @@ -4681,14 +4668,14 @@ msgstr "見 {ph0}設定原生分析{ph1} 為步驟。" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/guides/native/index.html:342 +#: website/public/guides/native/index.html:343 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}你的程式設計助手" -#: website/public/guides/native/index.html:346 -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:347 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy" msgstr "複製" @@ -4699,7 +4686,7 @@ msgstr "複製" #. {ph3}: #. {ph4}: -#: website/public/guides/native/index.html:355 +#: website/public/guides/native/index.html:356 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4708,7 +4695,7 @@ msgid "" msgstr "" "{ph0}›{ph1}{ph2}用 REA 找出 DX-Ball 如何計算聲音的左右位置。解釋計算過程,並展示程式碼。{ph3}{ph4}{ph5}" -#: website/public/guides/native/index.html:363 +#: website/public/guides/native/index.html:364 msgctxt "page:guides/native/index.html" msgid "" "Example prompt. Give your agent the path to your local DX-Ball 1.07 " @@ -4718,7 +4705,7 @@ msgstr "" "示例 prompt。把本地 DX-Ball 1.07 可執行檔案的路徑交給助手。REA 不附帶遊戲,需要用自己的副本,也可以換成手裡的其他小型 " "Windows 程式,問同類問題。" -#: website/public/guides/native/index.html:370 +#: website/public/guides/native/index.html:371 msgctxt "page:guides/native/index.html" msgid "How do I get a file's full path?" msgstr "如何獲取檔案的完整路徑?" @@ -4727,7 +4714,7 @@ msgstr "如何獲取檔案的完整路徑?" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:372 +#: website/public/guides/native/index.html:373 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4741,7 +4728,7 @@ msgstr "" #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/guides/native/index.html:377 +#: website/public/guides/native/index.html:378 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4751,7 +4738,7 @@ msgstr "{ph0} macOS :{ph1} 在Finder中,右鍵單擊檔案,按住選項並 #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:381 +#: website/public/guides/native/index.html:382 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4759,30 +4746,30 @@ msgid "" "appears." msgstr "{ph0}任何系統:{ph1} 將檔案拖到終端視窗中並顯示其路徑。" -#: website/public/guides/native/index.html:389 +#: website/public/guides/native/index.html:390 msgctxt "page:guides/native/index.html" msgid "Read the REA results" msgstr "閱讀REA結果" -#: website/public/guides/native/index.html:390 +#: website/public/guides/native/index.html:391 msgctxt "page:guides/native/index.html" msgid "Read the input" msgstr "閱讀輸入" -#: website/public/guides/native/index.html:399 +#: website/public/guides/native/index.html:400 msgctxt "page:guides/native/index.html" msgid "" "The instructions load an integer argument and convert it for floating-point " "arithmetic. This is the input absent from the first pseudocode view." msgstr "指令載入一個整數引數並將其轉換為浮點算術。 這是第一個虛擬碼檢視中沒有的輸入。" -#: website/public/guides/native/index.html:404 +#: website/public/guides/native/index.html:405 msgctxt "page:guides/native/index.html" msgid "Follow the caller" msgstr "跟隨呼叫方" #. {ph0}: 0x14 -#: website/public/guides/native/index.html:413 +#: website/public/guides/native/index.html:414 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4790,24 +4777,24 @@ msgid "" "tile coordinate by 30; adding {ph0} (20) gives the brick's screen position." msgstr "REA 找到磚塊碰撞處理函式。它先把磚塊橫座標乘以 30,再加上 {ph0}(20),得到磚塊的螢幕位置。" -#: website/public/guides/native/index.html:418 +#: website/public/guides/native/index.html:419 msgctxt "page:guides/native/index.html" msgid "Read the referenced constants" msgstr "讀取引用的常量" -#: website/public/guides/native/index.html:429 +#: website/public/guides/native/index.html:430 msgctxt "page:guides/native/index.html" msgid "" "The arithmetic references these addresses. Reading the bytes supplies the " "actual values; the instruction order tells the agent how they are used." msgstr "算術引用這些地址。 讀取位元組提供實際值;指令順序告訴程式設計助手如何使用它們。" -#: website/public/guides/native/index.html:436 +#: website/public/guides/native/index.html:437 msgctxt "page:guides/native/index.html" msgid "Write and check the recovered function" msgstr "寫入並檢查恢復的函式" -#: website/public/guides/native/index.html:445 +#: website/public/guides/native/index.html:446 msgctxt "page:guides/native/index.html" msgid "" "With a scale of 1.0, screen positions 0, 320 and 640 map to −500, 0 and 500 " @@ -4815,7 +4802,7 @@ msgid "" "the center of the stereo field." msgstr "比例為1.0時,螢幕位置0、320和640在整數轉換之前對映到-500、0和500。 計算將螢幕的中心置於立體場的中心。" -#: website/public/guides/native/index.html:450 +#: website/public/guides/native/index.html:451 msgctxt "page:guides/native/index.html" msgid "" "The agent used REA's findings to write the C. Separate checks compared 3,205" @@ -4825,20 +4812,20 @@ msgstr "程式設計助手使用REA的發現來編寫C.單獨的檢查將3,205 #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:455 +#: website/public/guides/native/index.html:456 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "{ph0}See the full investigation and assembly-to-C comparison{ph1}" msgstr "{ph0}檢視完整的調查和組裝到C的比較{ph1}" -#: website/public/guides/native/index.html:462 +#: website/public/guides/native/index.html:463 msgctxt "page:guides/native/index.html" msgid "Try it with your own binary" msgstr "用你自己的二進位制檔案試試" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:463 +#: website/public/guides/native/index.html:464 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4850,20 +4837,20 @@ msgstr "" "{ph0}連線原生分析提供程式{ph1},然後給你的程式設計助手一個本地可執行檔案或庫和一個要調查的函式。 " "有用的起始問題包括\"如何解析此檔案?\"或\"哪個函式處理此選單操作?”" -#: website/public/guides/native/index.html:470 +#: website/public/guides/native/index.html:471 msgctxt "page:guides/native/index.html" msgid "" "If you already know a function name or address, you can query it from the " "terminal. For a configured Ghidra installation:" msgstr "如果你已經知道函式名稱或地址,則可以從終端查詢它。 對於已配置的Ghidra安裝:" -#: website/public/guides/native/index.html:476 +#: website/public/guides/native/index.html:477 msgctxt "page:guides/native/index.html" msgid "Function analysis" msgstr "函式分析" #. {ph0}: main -#: website/public/guides/native/index.html:489 +#: website/public/guides/native/index.html:490 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -4875,7 +4862,7 @@ msgstr "" "替換目標路徑和 {ph0} 使用你自己的函式名稱或地址。 每個 CLI 呼叫匯入並分析二進位制檔案,然後關閉其會話。 你的程式設計助手 MCP " "查詢在該會話開啟時重用匯入的二進位制檔案。" -#: website/public/guides/native/index.html:495 +#: website/public/guides/native/index.html:496 msgctxt "page:guides/native/index.html" msgid "" "Read the pseudocode alongside instructions and references. When a result " @@ -4904,17 +4891,17 @@ msgctxt "page:guides/native/index.html" msgid "Native analysis" msgstr "機器程式碼程式分析" -#: website/public/guides/native/index.html:346 +#: website/public/guides/native/index.html:347 msgctxt "page:guides/native/index.html" msgid "Copy example prompt" msgstr "複製示例提示" -#: website/public/guides/native/index.html:477 +#: website/public/guides/native/index.html:478 msgctxt "page:guides/native/index.html" msgid "Copy function analysis" msgstr "複製函式分析" -#: website/public/guides/native/index.html:520 +#: website/public/guides/native/index.html:521 msgctxt "page:guides/native/index.html" msgid "Back to top" msgstr "回到頂部" @@ -5841,7 +5828,7 @@ msgid "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}Your coding agent" msgstr "{ph0}{ph1}{ph2}{ph3}{ph4}{ph5}{ph6}{ph7}你的程式設計助手" #: website/public/showcase/aegis/index.html:276 -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy" msgstr "複製" @@ -6225,22 +6212,9 @@ msgctxt "page:showcase/aegis/index.html" msgid "Inspect the APK yourself" msgstr "親自檢查APK" -# Reviewed for clear case-study reasoning and evidence. -#. {ph0}: -#. {ph1}: -#: website/public/showcase/aegis/index.html:714 -#, python-brace-format -msgctxt "page:showcase/aegis/index.html" -msgid "" -"{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " -"the same folder. On Windows, use a Linux environment such as WSL." -msgstr "" -"先在 macOS 或 Linux 上{ph0}設定 REA{ph1},再將這兩個檔案下載到同一資料夾。Windows 上可使用 WSL 等 Linux " -"環境。" - #. {ph0}: JAVA_HOME #. {ph1}: PATH -#: website/public/showcase/aegis/index.html:719 +#: website/public/showcase/aegis/index.html:726 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6248,19 +6222,19 @@ msgid "" "{ph1}. This case used JDK 21." msgstr "Android分析需要完整的JDK17或更高版本。 選擇它與 {ph0} 或 {ph1}. 本案例使用了JDK21。" -#: website/public/showcase/aegis/index.html:735 +#: website/public/showcase/aegis/index.html:742 msgctxt "page:showcase/aegis/index.html" msgid "Point REA at the provider file and inspect the APK:" msgstr "讓 REA 使用分析後端配置檔案,再檢查 APK:" -#: website/public/showcase/aegis/index.html:738 +#: website/public/showcase/aegis/index.html:745 msgctxt "page:showcase/aegis/index.html" msgid "macOS / Linux" msgstr "macOS/Linux" # Reviewed for clear case-study reasoning and evidence. #. {ph0}: REA_JADX_MCP_JAR -#: website/public/showcase/aegis/index.html:752 +#: website/public/showcase/aegis/index.html:759 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6270,7 +6244,7 @@ msgstr "助手可以透過 REA CLI 繼續使用上方的 prompt。如果使用 M #. {ph0}: #. {ph1}: -#: website/public/showcase/aegis/index.html:757 +#: website/public/showcase/aegis/index.html:764 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6278,7 +6252,7 @@ msgid "" "different time interval?{ph1}" msgstr "下一個問題: {ph0}當帳戶使用八位數字或不同的時間間隔時會發生什麼?{ph1}" -#: website/public/showcase/aegis/index.html:767 +#: website/public/showcase/aegis/index.html:774 msgctxt "page:showcase/aegis/index.html" msgid "Sources" msgstr "資料來源" @@ -6294,7 +6268,7 @@ msgstr "資料來源" #. href="https://github.com/beemdevelopment/Aegis/blob/d6f4e5925a97e4e91593f1542085eae03432a759/LICENSE" #. > #. {ph5}: -#: website/public/showcase/aegis/index.html:769 +#: website/public/showcase/aegis/index.html:776 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "" @@ -6306,14 +6280,14 @@ msgstr "{ph0}官方Aegis3.4.3釋出{ph1} · {ph2}匹配源提交{ph3} · {ph4}GP #. {ph1}: #. {ph2}: #. {ph3}: -#: website/public/showcase/aegis/index.html:785 +#: website/public/showcase/aegis/index.html:792 #, python-brace-format msgctxt "page:showcase/aegis/index.html" msgid "{ph0}RFC 6238 reference cases{ph1} · {ph2}Demo test data{ph3}" msgstr "{ph0}RFC6238參考案例{ph1} · {ph2}演示測試資料{ph3}" # Reviewed for clear case-study reasoning and evidence. -#: website/public/showcase/aegis/index.html:792 +#: website/public/showcase/aegis/index.html:799 msgctxt "page:showcase/aegis/index.html" msgid "" "Inspected with REA 6.1.0 on 9 October 2026. Decompiled excerpts are credited" @@ -6354,12 +6328,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "Follow the login-code calculation" msgstr "按照登入程式碼計算" -#: website/public/showcase/aegis/index.html:739 +#: website/public/showcase/aegis/index.html:746 msgctxt "page:showcase/aegis/index.html" msgid "Copy APK inspection command" msgstr "複製APK檢查命令" -#: website/public/showcase/aegis/index.html:817 +#: website/public/showcase/aegis/index.html:824 msgctxt "page:showcase/aegis/index.html" msgid "Back to top" msgstr "回到頂部" @@ -9922,13 +9896,13 @@ msgstr "指南" #: website/public/examples/dino-lab/index.html:476 #: website/public/faq/index.html:574 #: website/public/first-investigation/index.html:535 -#: website/public/get-started/index.html:1849 +#: website/public/get-started/index.html:1852 #: website/public/guides/browser/index.html:508 #: website/public/guides/index.html:302 #: website/public/guides/javascript/index.html:512 -#: website/public/guides/native/index.html:512 +#: website/public/guides/native/index.html:513 #: website/public/guides/rl-environments/index.html:499 -#: website/public/index.html:1304 website/public/showcase/aegis/index.html:809 +#: website/public/index.html:1304 website/public/showcase/aegis/index.html:816 #: website/public/showcase/ctf/index.html:778 #: website/public/showcase/dx-ball/index.html:786 #: website/public/showcase/index.html:338 @@ -9945,13 +9919,13 @@ msgstr "原始碼" #: website/public/examples/dino-lab/index.html:477 #: website/public/faq/index.html:575 #: website/public/first-investigation/index.html:536 -#: website/public/get-started/index.html:1850 +#: website/public/get-started/index.html:1853 #: website/public/guides/browser/index.html:509 #: website/public/guides/index.html:303 #: website/public/guides/javascript/index.html:513 -#: website/public/guides/native/index.html:513 +#: website/public/guides/native/index.html:514 #: website/public/guides/rl-environments/index.html:500 -#: website/public/index.html:1305 website/public/showcase/aegis/index.html:810 +#: website/public/index.html:1305 website/public/showcase/aegis/index.html:817 #: website/public/showcase/ctf/index.html:779 #: website/public/showcase/dx-ball/index.html:787 #: website/public/showcase/index.html:339 @@ -9968,13 +9942,13 @@ msgstr "MIT 許可證" #: website/public/examples/dino-lab/index.html:478 #: website/public/faq/index.html:576 #: website/public/first-investigation/index.html:537 -#: website/public/get-started/index.html:1851 +#: website/public/get-started/index.html:1854 #: website/public/guides/browser/index.html:510 #: website/public/guides/index.html:304 #: website/public/guides/javascript/index.html:514 -#: website/public/guides/native/index.html:514 +#: website/public/guides/native/index.html:515 #: website/public/guides/rl-environments/index.html:501 -#: website/public/index.html:1306 website/public/showcase/aegis/index.html:811 +#: website/public/index.html:1306 website/public/showcase/aegis/index.html:818 #: website/public/showcase/ctf/index.html:780 #: website/public/showcase/dx-ball/index.html:788 #: website/public/showcase/index.html:340 @@ -9991,13 +9965,13 @@ msgstr "加入社群" #: website/public/examples/dino-lab/index.html:479 #: website/public/faq/index.html:577 #: website/public/first-investigation/index.html:538 -#: website/public/get-started/index.html:1852 +#: website/public/get-started/index.html:1855 #: website/public/guides/browser/index.html:511 #: website/public/guides/index.html:305 #: website/public/guides/javascript/index.html:515 -#: website/public/guides/native/index.html:515 +#: website/public/guides/native/index.html:516 #: website/public/guides/rl-environments/index.html:502 -#: website/public/index.html:1307 website/public/showcase/aegis/index.html:812 +#: website/public/index.html:1307 website/public/showcase/aegis/index.html:819 #: website/public/showcase/ctf/index.html:781 #: website/public/showcase/dx-ball/index.html:789 #: website/public/showcase/index.html:341 @@ -10082,22 +10056,22 @@ msgctxt "page:guides/javascript/index.html" msgid "REA result · summary.ipc excerpt" msgstr "REA 結果 · summary.ipc 摘錄" -#: website/public/guides/native/index.html:393 +#: website/public/guides/native/index.html:394 msgctxt "page:guides/native/index.html" msgid "REA instruction excerpt · 0x406409" msgstr "REA指令摘錄·0x406409" -#: website/public/guides/native/index.html:407 +#: website/public/guides/native/index.html:408 msgctxt "page:guides/native/index.html" msgid "REA caller excerpt · 0x411f5b" msgstr "REA呼叫方摘錄 · 0x411f5b" -#: website/public/guides/native/index.html:421 +#: website/public/guides/native/index.html:422 msgctxt "page:guides/native/index.html" msgid "REA byte results · interpreted as little-endian doubles" msgstr "REA位元組結果 · 解釋為little-endian雙精度浮點數" -#: website/public/guides/native/index.html:438 +#: website/public/guides/native/index.html:439 msgctxt "page:guides/native/index.html" msgid "Recovered C · excerpt" msgstr "恢復C · 摘錄" @@ -10158,12 +10132,12 @@ msgctxt "page:showcase/aegis/index.html" msgid "REA · generateOTP, complete body" msgstr "REA · generateOTP 完整函式體" -#: website/public/showcase/aegis/index.html:725 +#: website/public/showcase/aegis/index.html:732 msgctxt "page:showcase/aegis/index.html" msgid "Download Aegis APK" msgstr "下載Aegis APK" -#: website/public/showcase/aegis/index.html:729 +#: website/public/showcase/aegis/index.html:736 msgctxt "page:showcase/aegis/index.html" msgid "Download Android analysis provider" msgstr "下載 Android 分析後端" @@ -12524,11 +12498,6 @@ msgctxt "page:get-started/index.html" msgid "Unavailable" msgstr "不支援" -#: website/public/get-started/index.html:1042 -msgctxt "page:get-started/index.html" -msgid "Supported distributions" -msgstr "支援的發行版" - #: website/public/get-started/index.html:1046 msgctxt "page:get-started/index.html" msgid "x64 · experimental" @@ -12564,16 +12533,7 @@ msgstr "" "在 setup 中選擇 Hopper,查看它的獨立安裝計畫。macOS 首次打開 Hopper 時選擇試用模式或啟用授權;Linux 的 setup " "會準備支援的試用版及其專用顯示環境。" -#: website/public/get-started/index.html:1072 -msgctxt "page:get-started/index.html" -msgid "" -"Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " -"Omarchy. Hopper is commercial software; its demo has vendor-defined limits." -msgstr "" -"Linux 支援 Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch Linux、CachyOS 和 " -"Omarchy。Hopper 是商業軟體,試用版有廠商規定的限制。" - -#: website/public/get-started/index.html:1091 +#: website/public/get-started/index.html:1094 msgctxt "page:get-started/index.html" msgid "" "If macOS asks for Automation permission, allow the app that starts REA in " @@ -12584,18 +12544,18 @@ msgstr "如果 macOS 請求自動化權限,在「系統設定 → 隱私權與 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#hopper" #. > #. {ph1}: -#: website/public/get-started/index.html:1096 +#: website/public/get-started/index.html:1099 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Hopper setup and troubleshooting{ph1}" msgstr "{ph0}Hopper 安裝與疑難排解{ph1}" -#: website/public/get-started/index.html:1104 +#: website/public/get-started/index.html:1107 msgctxt "page:get-started/index.html" msgid "Ghidra · install the free analysis tool and Java" msgstr "Ghidra · 安裝免費的分析工具和 Java" -#: website/public/get-started/index.html:1108 +#: website/public/get-started/index.html:1111 msgctxt "page:get-started/index.html" msgid "" "Install a 64-bit JDK 21 or newer, including the Java compiler. Choose the " @@ -12603,7 +12563,7 @@ msgid "" msgstr "安裝包含 Java 編譯器的 64 位元 JDK 21 或更新版本。下載時選擇符合電腦架構的安裝套件。" #. {ph0}: ghidra_…_PUBLIC_….zip -#: website/public/get-started/index.html:1113 +#: website/public/get-started/index.html:1116 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12614,7 +12574,7 @@ msgstr "從 Ghidra 12.1.4 發布頁的 Assets 清單下載並解壓縮 {ph0}。 #. {ph0}: ghidraRun #. {ph1}: ghidraRun.bat #. {ph2}: GHIDRA_INSTALL_DIR -#: website/public/get-started/index.html:1118 +#: website/public/get-started/index.html:1121 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12628,7 +12588,7 @@ msgstr "找到解壓縮後包含 {ph0} 或 {ph1} 的資料夾,把該資料夾 #. href="https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build" #. > #. {ph3}: -#: website/public/get-started/index.html:1124 +#: website/public/get-started/index.html:1127 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Download a JDK{ph1} · {ph2}Download Ghidra 12.1.4{ph3}" @@ -12636,7 +12596,7 @@ msgstr "{ph0}下載 JDK{ph1} · {ph2}下載 Ghidra 12.1.4{ph3}" #. {ph0}: java.home #. {ph1}: JAVA_HOME -#: website/public/get-started/index.html:1134 +#: website/public/get-started/index.html:1137 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12644,7 +12604,7 @@ msgid "" "second confirms the compiler is installed:" msgstr "檢查 Java。第一條指令會顯示 {ph0},將該資料夾用於 {ph1};第二條確認編譯器已安裝:" -#: website/public/get-started/index.html:1250 +#: website/public/get-started/index.html:1253 msgctxt "page:get-started/index.html" msgid "" "Keep these commands in the same terminal session. Rerun setup after setting " @@ -12659,7 +12619,7 @@ msgstr "在同一個終端機工作階段中執行這些指令。設定路徑後 #. {ph2}: #. {ph3}: -#: website/public/get-started/index.html:1255 +#: website/public/get-started/index.html:1258 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12667,12 +12627,12 @@ msgid "" "guide{ph3}" msgstr "{ph0}Ghidra 設定與架構要求{ph1} · {ph2}Windows Ghidra 指南{ph3}" -#: website/public/get-started/index.html:1268 +#: website/public/get-started/index.html:1271 msgctxt "page:get-started/index.html" msgid "IDA · connect an existing installation" msgstr "IDA · 連接既有安裝" -#: website/public/get-started/index.html:1269 +#: website/public/get-started/index.html:1272 msgctxt "page:get-started/index.html" msgid "" "Use the IDA provider guide to prepare an upstream MCP registration. Choose " @@ -12681,7 +12641,7 @@ msgid "" msgstr "" "按 IDA provider 指南準備上游 MCP 設定。可以連接已打開原始程式的 IDA 介面,或使用建立獨立資料庫的無介面 supervisor。" -#: website/public/get-started/index.html:1274 +#: website/public/get-started/index.html:1277 msgctxt "page:get-started/index.html" msgid "" "Verified profiles include the Windows legacy GUI and headless supervisor, " @@ -12691,7 +12651,7 @@ msgstr "" "已驗證的設定包括 Windows 舊版 GUI 與無介面 supervisor,以及 macOS arm64 上的 IDA Professional " "9.1 無介面 supervisor。可選的原生 GUI 橋接另有版本和平台要求。" -#: website/public/get-started/index.html:1369 +#: website/public/get-started/index.html:1372 msgctxt "page:get-started/index.html" msgid "" "Use the JSON registration file you prepared with the provider guide, then " @@ -12701,13 +12661,13 @@ msgstr "使用按 provider 指南準備好的 JSON 設定檔,再執行 setup #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1373 +#: website/public/get-started/index.html:1376 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}IDA profiles, registration examples and verified platforms{ph1}" msgstr "{ph0}IDA 設定、註冊範例與已驗證平台{ph1}" -#: website/public/get-started/index.html:1381 +#: website/public/get-started/index.html:1384 msgctxt "page:get-started/index.html" msgid "" "Once connected, give your agent the original executable's full path and name" @@ -12716,128 +12676,128 @@ msgstr "連接後,把原始執行檔的完整路徑和分析工具名稱告訴 #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1393 +#: website/public/get-started/index.html:1396 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Follow a native analysis example{ph1}" msgstr "{ph0}跟著範例分析原生程式{ph1}" -#: website/public/get-started/index.html:1398 +#: website/public/get-started/index.html:1401 msgctxt "page:get-started/index.html" msgid "Websites, Android, .NET, firmware and other targets" msgstr "網站、Android、.NET、韌體及其他目標" -#: website/public/get-started/index.html:1401 +#: website/public/get-started/index.html:1404 msgctxt "page:get-started/index.html" msgid "" "Open the guide for the target you have. Each guide gives the additional " "tools, supported hosts and first command." msgstr "打開對應目標的指南,其中會列出額外工具、支援的平台和第一條分析指令。" -#: website/public/get-started/index.html:1409 +#: website/public/get-started/index.html:1412 msgctxt "page:get-started/index.html" msgid "Target" msgstr "目標" -#: website/public/get-started/index.html:1410 +#: website/public/get-started/index.html:1413 msgctxt "page:get-started/index.html" msgid "Tools to prepare" msgstr "需要準備的工具" -#: website/public/get-started/index.html:1411 +#: website/public/get-started/index.html:1414 msgctxt "page:get-started/index.html" msgid "Guide" msgstr "指南" -#: website/public/get-started/index.html:1416 +#: website/public/get-started/index.html:1419 msgctxt "page:get-started/index.html" msgid "JavaScript / Electron" msgstr "JavaScript / Electron" #. {ph0}: #. {ph1}: -#: website/public/get-started/index.html:1418 -#: website/public/get-started/index.html:1423 -#: website/public/get-started/index.html:1428 -#: website/public/get-started/index.html:1438 -#: website/public/get-started/index.html:1448 -#: website/public/get-started/index.html:1458 -#: website/public/get-started/index.html:1468 -#: website/public/get-started/index.html:1478 -#: website/public/get-started/index.html:1488 -#: website/public/get-started/index.html:1498 -#: website/public/get-started/index.html:1508 -#: website/public/get-started/index.html:1518 -#: website/public/get-started/index.html:1528 -#: website/public/get-started/index.html:1538 -#: website/public/get-started/index.html:1548 -#: website/public/get-started/index.html:1558 +#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1431 +#: website/public/get-started/index.html:1441 +#: website/public/get-started/index.html:1451 +#: website/public/get-started/index.html:1461 +#: website/public/get-started/index.html:1471 +#: website/public/get-started/index.html:1481 +#: website/public/get-started/index.html:1491 +#: website/public/get-started/index.html:1501 +#: website/public/get-started/index.html:1511 +#: website/public/get-started/index.html:1521 +#: website/public/get-started/index.html:1531 +#: website/public/get-started/index.html:1541 +#: website/public/get-started/index.html:1551 +#: website/public/get-started/index.html:1561 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Open guide{ph1}" msgstr "{ph0}查看指南{ph1}" -#: website/public/get-started/index.html:1421 +#: website/public/get-started/index.html:1424 msgctxt "page:get-started/index.html" msgid "Website" msgstr "網站" -#: website/public/get-started/index.html:1426 +#: website/public/get-started/index.html:1429 msgctxt "page:get-started/index.html" msgid ".NET / Flutter / packages" msgstr ".NET / Flutter / 軟體套件" -#: website/public/get-started/index.html:1436 +#: website/public/get-started/index.html:1439 msgctxt "page:get-started/index.html" msgid "Android APK" msgstr "Android APK" -#: website/public/get-started/index.html:1446 +#: website/public/get-started/index.html:1449 msgctxt "page:get-started/index.html" msgid "Android device" msgstr "Android 裝置" -#: website/public/get-started/index.html:1456 +#: website/public/get-started/index.html:1459 msgctxt "page:get-started/index.html" msgid "Android resources" msgstr "Android 資源" -#: website/public/get-started/index.html:1466 +#: website/public/get-started/index.html:1469 msgctxt "page:get-started/index.html" msgid "JEB project" msgstr "JEB 專案" -#: website/public/get-started/index.html:1476 +#: website/public/get-started/index.html:1479 msgctxt "page:get-started/index.html" msgid "Firmware" msgstr "韌體" -#: website/public/get-started/index.html:1486 +#: website/public/get-started/index.html:1489 msgctxt "page:get-started/index.html" msgid "WASM" msgstr "WASM" -#: website/public/get-started/index.html:1516 +#: website/public/get-started/index.html:1519 msgctxt "page:get-started/index.html" msgid "ELF / Linux crash" msgstr "ELF / Linux 當機記錄" -#: website/public/get-started/index.html:1526 +#: website/public/get-started/index.html:1529 msgctxt "page:get-started/index.html" msgid "HAR / SAZ / mitmproxy" msgstr "HAR / SAZ / mitmproxy" -#: website/public/get-started/index.html:1536 +#: website/public/get-started/index.html:1539 msgctxt "page:get-started/index.html" msgid "PCAP / PCAPNG" msgstr "PCAP / PCAPNG" -#: website/public/get-started/index.html:1546 +#: website/public/get-started/index.html:1549 msgctxt "page:get-started/index.html" msgid "Logic signals" msgstr "邏輯訊號" -#: website/public/get-started/index.html:1556 +#: website/public/get-started/index.html:1559 msgctxt "page:get-started/index.html" msgid "Process behavior" msgstr "程序行為" @@ -12853,7 +12813,7 @@ msgstr "程序行為" #. {ph5}: #. {ph6}: #. {ph7}: -#: website/public/get-started/index.html:1568 +#: website/public/get-started/index.html:1571 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12863,18 +12823,18 @@ msgstr "" "{ph0}Flutter 指南{ph1} · {ph2}軟體套件與資源{ph3} · {ph4}EVM 位元碼{ph5} · " "{ph6}全部分析指南{ph7}" -#: website/public/get-started/index.html:1588 +#: website/public/get-started/index.html:1591 msgctxt "page:get-started/index.html" msgid "Use REA from a terminal" msgstr "在終端機中使用 REA" -#: website/public/get-started/index.html:1590 +#: website/public/get-started/index.html:1593 msgctxt "page:get-started/index.html" msgid "CLI commands, global installation and curl" msgstr "CLI 指令、全域安裝與 curl" #. {ph0}: main -#: website/public/get-started/index.html:1687 +#: website/public/get-started/index.html:1690 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12882,26 +12842,26 @@ msgid "" "list. Ghidra must be configured first." msgstr "把 {ph0} 換成目標函式清單中的名稱或位址。先完成 Ghidra 設定再執行。" -#: website/public/get-started/index.html:1691 +#: website/public/get-started/index.html:1694 msgctxt "page:get-started/index.html" msgid "" "On macOS or Linux, you can also use the curl installer after preparing " "Node.js:" msgstr "macOS 或 Linux 準備好 Node.js 後,也可以使用 curl 安裝:" -#: website/public/get-started/index.html:1729 +#: website/public/get-started/index.html:1732 msgctxt "page:get-started/index.html" msgid "" "Use the client check in step 3 with your setup ID. In editor clients, enable" " REA in MCP settings and allow its tools in the agent chat." msgstr "在第 3 步的檢查指令中填寫你的 setup ID。使用編輯器時,在 MCP 設定中啟用 REA,並在助手聊天中允許使用它的工具。" -#: website/public/get-started/index.html:1757 +#: website/public/get-started/index.html:1760 msgctxt "page:get-started/index.html" msgid "Windows, WSL and paths with spaces" msgstr "Windows、WSL 與含空白的路徑" -#: website/public/get-started/index.html:1758 +#: website/public/get-started/index.html:1761 msgctxt "page:get-started/index.html" msgid "" "Use Windows PowerShell for Windows clients and analysis tools. If your agent" @@ -12911,17 +12871,17 @@ msgstr "" "Windows 用戶端和分析工具使用 Windows PowerShell。如果程式開發助手在 WSL 內執行,就在那個 Linux 環境中安裝 " "Node.js 和 REA,並使用 Linux 路徑。無介面 provider 與 REA 在同一個系統環境中執行。" -#: website/public/get-started/index.html:1767 +#: website/public/get-started/index.html:1770 msgctxt "page:get-started/index.html" msgid "Windows setup command:" msgstr "Windows setup 指令:" -#: website/public/get-started/index.html:1784 +#: website/public/get-started/index.html:1787 msgctxt "page:get-started/index.html" msgid "Update or remove REA" msgstr "更新或移除 REA" -#: website/public/get-started/index.html:1785 +#: website/public/get-started/index.html:1788 msgctxt "page:get-started/index.html" msgid "" "To refresh your agent connection, run the latest setup command again, review" @@ -12930,7 +12890,7 @@ msgstr "要更新程式開發助手的連接,重新執行最新版 setup,查 #. {ph0}: rea update #. {ph1}: npx rea-agents@latest uninstall -#: website/public/get-started/index.html:1803 +#: website/public/get-started/index.html:1806 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -12942,7 +12902,7 @@ msgstr "全域安裝的 CLI 用 {ph0} 更新。使用 {ph1} 移除 REA 的用戶 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#diagnose- #. update-and-remove" > #. {ph1}: -#: website/public/get-started/index.html:1809 +#: website/public/get-started/index.html:1812 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Update and uninstall details{ph1}" @@ -12965,18 +12925,18 @@ msgstr "複製 Windows REA setup 指令" #: website/public/get-started/index.html:832 #: website/public/get-started/index.html:847 #: website/public/get-started/index.html:895 -#: website/public/get-started/index.html:1080 -#: website/public/get-started/index.html:1142 -#: website/public/get-started/index.html:1165 -#: website/public/get-started/index.html:1189 -#: website/public/get-started/index.html:1237 -#: website/public/get-started/index.html:1312 -#: website/public/get-started/index.html:1338 -#: website/public/get-started/index.html:1355 -#: website/public/get-started/index.html:1676 -#: website/public/get-started/index.html:1698 -#: website/public/get-started/index.html:1771 -#: website/public/get-started/index.html:1792 +#: website/public/get-started/index.html:1083 +#: website/public/get-started/index.html:1145 +#: website/public/get-started/index.html:1168 +#: website/public/get-started/index.html:1192 +#: website/public/get-started/index.html:1240 +#: website/public/get-started/index.html:1315 +#: website/public/get-started/index.html:1341 +#: website/public/get-started/index.html:1358 +#: website/public/get-started/index.html:1679 +#: website/public/get-started/index.html:1701 +#: website/public/get-started/index.html:1774 +#: website/public/get-started/index.html:1795 msgctxt "page:get-started/index.html" msgid "Copy command" msgstr "複製指令" @@ -13030,14 +12990,14 @@ msgctxt "page:get-started/index.html" msgid "OpenCode · choose an AI model" msgstr "OpenCode · 選擇 AI 模型" -#: website/public/get-started/index.html:1296 +#: website/public/get-started/index.html:1299 msgctxt "page:get-started/index.html" msgid "Prepare the IDA registration file" msgstr "準備 IDA 設定檔" #. {ph0}: ida-mcp.json #. {ph1}: .txt -#: website/public/get-started/index.html:1297 +#: website/public/get-started/index.html:1300 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13051,7 +13011,7 @@ msgstr "" #. {ph0}: idb_open #. {ph1}: idb_list #. {ph2}: idb_close -#: website/public/get-started/index.html:1304 +#: website/public/get-started/index.html:1307 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13062,7 +13022,7 @@ msgstr "如果使用提供 {ph0}、{ph1} 和 {ph2} 的無介面 supervisor,檔 #. {ph0}: command #. {ph1}: args #. {ph2}: env -#: website/public/get-started/index.html:1327 +#: website/public/get-started/index.html:1330 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13073,12 +13033,12 @@ msgstr "" "如果連接舊版 1.4.0 GUI 服務,把可用的 {ph0}、{ph1} 和可選的 {ph2} 複製到檔案中。先在 IDA " "中打開原始程式並啟動上游外掛,再讓 REA 分析。" -#: website/public/get-started/index.html:1496 +#: website/public/get-started/index.html:1499 msgctxt "page:get-started/index.html" msgid "SQLite database" msgstr "SQLite 資料庫" -#: website/public/get-started/index.html:1506 +#: website/public/get-started/index.html:1509 msgctxt "page:get-started/index.html" msgid "EVM bytecode" msgstr "EVM 位元碼" @@ -13129,7 +13089,7 @@ msgid "" "in step 3{ph1}." msgstr "查看並確認計畫,重新打開 Claude,再{ph0}按第 3 步檢查連接{ph1}。" -#: website/public/get-started/index.html:1201 +#: website/public/get-started/index.html:1204 msgctxt "page:get-started/index.html" msgid "" "Replace both paths with your existing installations. For a fresh macOS or " @@ -13139,12 +13099,12 @@ msgstr "" "把兩個路徑換成你已有的安裝位置。如果剛在 macOS 或 Linux arm64 上解壓縮 " "Ghidra,先為這台電腦編譯原生反編譯器,再繼續下面的步驟。" -#: website/public/get-started/index.html:1207 +#: website/public/get-started/index.html:1210 msgctxt "page:get-started/index.html" msgid "Build Ghidra's native decompiler" msgstr "編譯 Ghidra 的原生反編譯器" -#: website/public/get-started/index.html:1208 +#: website/public/get-started/index.html:1211 msgctxt "page:get-started/index.html" msgid "" "On macOS, the build needs Xcode Command Line Tools, make and bison. Use the " @@ -13155,7 +13115,7 @@ msgstr "macOS 編譯需要 Xcode Command Line Tools、make 和 bison。選擇符 #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#missing- #. macos-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1213 +#: website/public/get-started/index.html:1216 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Intel{ph1}" @@ -13165,7 +13125,7 @@ msgstr "{ph0}macOS · Intel{ph1}" #. href="https://github.com/morluto/rea/blob/main/docs/installation.md#macos- #. arm64-native-decompiler" > #. {ph1}: -#: website/public/get-started/index.html:1219 +#: website/public/get-started/index.html:1222 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}macOS · Apple Silicon{ph1}" @@ -13175,13 +13135,13 @@ msgstr "{ph0}macOS · Apple Silicon{ph1}" #. href="https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md#building- #. native-components" > #. {ph1}: -#: website/public/get-started/index.html:1225 +#: website/public/get-started/index.html:1228 #, python-brace-format msgctxt "page:get-started/index.html" msgid "{ph0}Linux arm64 · native build instructions{ph1}" msgstr "{ph0}Linux arm64 · 原生元件編譯說明{ph1}" -#: website/public/get-started/index.html:1233 +#: website/public/get-started/index.html:1236 msgctxt "page:get-started/index.html" msgid "Then check Ghidra and save its paths for your client:" msgstr "然後檢查 Ghidra,並為用戶端儲存這些路徑:" @@ -13538,7 +13498,7 @@ msgid "Other install options: {ph0}{ph1} →{ph2}" msgstr "其他安裝方式:{ph0}{ph1} →{ph2}" #. {ph0}: rea setup -#: website/public/get-started/index.html:1709 +#: website/public/get-started/index.html:1712 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13551,7 +13511,7 @@ msgstr "依設定提示連接你的助手。如果沒有出現提示,執行 {p #. setup" > #. {ph1}: #. {ph2}: 6.4.0 -#: website/public/get-started/index.html:1280 +#: website/public/get-started/index.html:1283 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13564,7 +13524,7 @@ msgstr "" #. {ph0}: node scripts/rea.mjs doctor --provider ida --json #. {ph1}: node scripts/rea.mjs setup #. {ph2}: npx -#: website/public/get-started/index.html:1289 +#: website/public/get-started/index.html:1292 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13574,7 +13534,7 @@ msgstr "在已建置的原始碼目錄中,執行 {ph0} 和 {ph1},不要使 #. {ph0}: rea #. {ph1}: "$HOME/.local/bin/rea" setup -#: website/public/get-started/index.html:1713 +#: website/public/get-started/index.html:1716 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13583,7 +13543,7 @@ msgid "" msgstr "如果終端找不到 {ph0},請使用安裝器印出的執行檔路徑。在 Linux 上,預設的設定命令是 {ph1}。" #. {ph0}: /absolute/path/to/program -#: website/public/get-started/index.html:1386 +#: website/public/get-started/index.html:1389 #, python-brace-format msgctxt "page:get-started/index.html" msgid "" @@ -13599,13 +13559,13 @@ msgstr "使用 REA 和我已設定的分析工具檢查 {ph0}。列出其中的 #: website/public/examples/dino-lab/index.html:475 #: website/public/faq/index.html:573 #: website/public/first-investigation/index.html:534 -#: website/public/get-started/index.html:1848 +#: website/public/get-started/index.html:1851 #: website/public/guides/browser/index.html:507 #: website/public/guides/index.html:301 #: website/public/guides/javascript/index.html:511 -#: website/public/guides/native/index.html:511 +#: website/public/guides/native/index.html:512 #: website/public/guides/rl-environments/index.html:498 -#: website/public/index.html:1303 website/public/showcase/aegis/index.html:808 +#: website/public/index.html:1303 website/public/showcase/aegis/index.html:815 #: website/public/showcase/ctf/index.html:777 #: website/public/showcase/dx-ball/index.html:785 #: website/public/showcase/index.html:337 @@ -14474,7 +14434,7 @@ msgstr "" #. {ph0}: #. {ph1}: -#: website/public/guides/native/index.html:327 +#: website/public/guides/native/index.html:328 #, python-brace-format msgctxt "page:guides/native/index.html" msgid "" @@ -14483,6 +14443,99 @@ msgid "" " or a release that includes it." msgstr "" +#: website/public/get-started/index.html:1042 +msgctxt "page:get-started/index.html" +msgid "x86_64 installer on supported distributions" +msgstr "" + +#. {ph0}: +#. {ph1}: +#. {ph2}: +#. {ph3}: +#: website/public/showcase/aegis/index.html:714 +#, python-brace-format +msgctxt "page:showcase/aegis/index.html" +msgid "" +"{ph0}Set up REA{ph1}, then download these two files into the same folder. On" +" Windows x64, use a package with matching Windows native controls and owned " +"protocol stdin support; see the {ph2}Android provider guide{ph3} for " +"prerequisites and PowerShell setup. A Linux environment such as WSL is " +"another option." +msgstr "" + +#: website/public/get-started/index.html:1072 +msgctxt "page:get-started/index.html" +msgid "" +"Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +"41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +" can also be configured on other supported Linux distributions. On Linux " +"arm64, use Ghidra. Hopper is commercial software; its demo has vendor-" +"defined limits." +msgstr "" + +#. {ph0}: +#. {ph1}: +#: website/public/guides/native/index.html:316 +#, python-brace-format +msgctxt "page:guides/native/index.html" +msgid "" +"{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +"install it. On macOS, open Hopper once and choose demo mode or activate a " +"license before your first analysis. Automated Linux setup requires x86_64; " +"use Ghidra on Linux arm64." +msgstr "" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Automated Linux setup requires an x86_64 host running Ubuntu 24.04+, Fedora " +#~ "41+, Nobara 44+, Arch Linux, CachyOS or Omarchy. An existing Hopper launcher" +#~ " can also be configured on other supported Linux distributions. On other " +#~ "Linux architectures, use Ghidra. Hopper is commercial software; its demo has" +#~ " vendor-defined limits." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis. Automated Linux setup requires x86_64; " +#~ "use Ghidra on other Linux architectures." +#~ msgstr "" + +#, python-brace-format +#~ msgctxt "page:guides/native/index.html" +#~ msgid "" +#~ "{ph0}Hopper{ph1} (macOS or Linux): REA's setup detects it or offers to " +#~ "install it. On macOS, open Hopper once and choose demo mode or activate a " +#~ "license before your first analysis." +#~ msgstr "" +#~ "{ph0} Hopper {ph1} (macOS或Linux):REA的安裝程式檢測到它或提供安裝它。 " +#~ "在macOS上,開啟一次Hopper,然後選擇演示模式或在第一次分析之前啟用許可證。" + +# Reviewed for clear case-study reasoning and evidence. +#, python-brace-format +#~ msgctxt "page:showcase/aegis/index.html" +#~ msgid "" +#~ "{ph0}Set up REA{ph1} on macOS or Linux, then download these two files into " +#~ "the same folder. On Windows, use a Linux environment such as WSL." +#~ msgstr "" +#~ "先在 macOS 或 Linux 上{ph0}設定 REA{ph1},再將這兩個檔案下載到同一資料夾。Windows 上可使用 WSL 等 Linux " +#~ "環境。" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "Supported distributions" +#~ msgstr "支援的發行版" + +#~ msgctxt "page:get-started/index.html" +#~ msgid "" +#~ "Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or " +#~ "Omarchy. Hopper is commercial software; its demo has vendor-defined limits." +#~ msgstr "" +#~ "Linux 支援 Ubuntu 24.04+、Fedora 41+、Nobara 44+、Arch Linux、CachyOS 和 " +#~ "Omarchy。Hopper 是商業軟體,試用版有廠商規定的限制。" + #, python-brace-format #~ msgctxt "page:guides/native/index.html" #~ msgid "" diff --git a/website/public/get-started/index.html b/website/public/get-started/index.html index f5d9a9687d..9eeb335552 100644 --- a/website/public/get-started/index.html +++ b/website/public/get-started/index.html @@ -1039,7 +1039,7 @@ cd rea-work Hopper Unavailable 12+ - Supported distributions + x86_64 installer on supported distributions Ghidra @@ -1070,9 +1070,12 @@ cd rea-work demo and its private display.

- Linux hosts: Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch - Linux, CachyOS or Omarchy. Hopper is commercial software; its - demo has vendor-defined limits. + Automated Linux setup requires an x86_64 host running Ubuntu + 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS or + Omarchy. An existing Hopper launcher can also be configured on + other supported Linux distributions. On Linux arm64, use + Ghidra. Hopper is commercial software; its demo has + vendor-defined limits.

diff --git a/website/public/guides/native/index.html b/website/public/guides/native/index.html index 9ae3a8ce8b..3fa0bf5d96 100644 --- a/website/public/guides/native/index.html +++ b/website/public/guides/native/index.html @@ -317,7 +317,8 @@ Hopper (macOS or Linux): REA's setup detects it or offers to install it. On macOS, open Hopper once and choose demo mode or activate a license before your first - analysis. + analysis. Automated Linux setup requires x86_64; use Ghidra on + Linux arm64.
  • Ghidra (macOS or Linux; experimental on diff --git a/website/public/showcase/aegis/index.html b/website/public/showcase/aegis/index.html index 2dfaeb4504..2fcb634131 100644 --- a/website/public/showcase/aegis/index.html +++ b/website/public/showcase/aegis/index.html @@ -712,9 +712,16 @@ code = pad_with_zeroes(code, 6)

    Inspect the APK yourself

    - Set up REA on macOS or Linux, - then download these two files into the same folder. On Windows, - use a Linux environment such as WSL. + Set up REA, then download these + two files into the same folder. On Windows x64, use a package with + matching Windows native controls and owned protocol stdin support; + see the + Android provider guide + for prerequisites and PowerShell setup. A Linux environment such + as WSL is another option.

    Android analysis needs a full JDK 17 or later. Select it with