[PR #4285] feat(native): decode Delphi/C++Builder TPF0 form payloads in PE resources #6157

Open
opened 2026-10-31 01:12:18 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/4285
Author: @Henglie
Created: 10/15/2026
Status: 🔄 Open

Base: main ← Head: feat/delphi-tpf0-forms


📝 Commits (9)

  • 73996a6 feat(native): decode Delphi/C++Builder TPF0 form payloads in PE resources
  • a8afb88 fix(native): bound Delphi form decoding and report its real limits
  • ec63efd fix(contracts): advertise the recursive form projections nonrecursively
  • 1ba5dec fix(native): keep form fields discoverable and decode gaps explicit
  • 23c156b Merge remote-tracking branch 'origin/main' into feat/delphi-tpf0-forms
  • 9acd7c9 Merge remote-tracking branch 'upstream/main' into feat/delphi-tpf0-forms
  • 7d4d724 fix(native): keep partial collections and advertised child fields
  • 7cc298d fix(native): bound form recursion and keep partial decode evidence
  • 2ac1a91 test(native): validate a decoded Delphi form against the SDK-converted schema

📊 Changes

12 files changed (+2792 additions, -41 deletions)

View changed files

📝 docs/binary-diagnostics.md (+16 -1)
📝 src/application/binaryDiagnostics/PeResourcesService.test.ts (+125 -0)
📝 src/application/binaryDiagnostics/PeResourcesService.ts (+138 -38)
📝 src/contracts/native/binaryDiagnosticsToolContracts.ts (+1 -1)
➕ src/domain/native/delphiForm.test.ts (+124 -0)
➕ src/domain/native/delphiForm.ts (+356 -0)
📝 src/domain/native/peResources.ts (+2 -0)
➕ src/native/pe/DelphiForm.fixture.ts (+156 -0)
➕ src/native/pe/DelphiFormStream.test.ts (+766 -0)
➕ src/native/pe/DelphiFormStream.ts (+932 -0)
📝 src/native/pe/PeResourceParser.test.ts (+98 -0)
📝 src/native/pe/PeResourceParser.ts (+78 -1)

📄 Description

Summary

inspect_pe_resources returned a complete PE32/PE32+ resource inventory and
decoded RT_GROUP_ICON, but every other payload stayed an opaque byte range
with a hash. Delphi/C++Builder VCL forms (TPF0 RCDATA) were therefore
unreadable without an external tool, which is what #2810 asks for.

This adds a static, provider-free decoder for the Object Pascal filer stream
over the resource payload that inspect_pe_resources has already located,
reported as a new delphi_forms array next to icon_groups.

Refs #2810.

Problem and expected behavior

For a Delphi target, each form lives in an RT_RCDATA resource whose payload
starts with the filer signature TPF0 (or TPF1) followed by a component tree:
class name, component name, filer flags, properties with declared value types,
and nested components. Before this change the tool reported the payload's RVA,
file range and SHA-256 and nothing else, and the first limitations entry said
payloads other than RT_GROUP_ICON remain opaque.

After the change a caller gets the decoded component tree with byte offsets back
into the payload for every object, property and value, with the original value
bytes retained next to each decode.

Change and scope

  • src/native/pe/DelphiFormStream.ts (new): bounded TPF0 reader. Signature
    detection within a 16-byte window (bytes before the signature are kept
    verbatim, not interpreted), BeginComponent filer flags with child position,
    ReadStr short strings, and the TValueType payloads from
    classesh.inc/reader.inc: vaInt8/16/32/64, vaQWord, vaSingle,
    vaDouble, vaExtended (ten-byte x87 reduced to a double, with the
    precision loss stated), vaCurrency, vaDate, vaString, vaIdent,
    vaLString, vaUTF8String, vaWString, vaUString, vaBinary, vaList,
    vaSet and vaCollection.
  • src/domain/native/delphiForm.ts (new): the result types and their zod
    schemas, recursive through z.lazy.
  • src/domain/native/peResources.ts: delphi_forms added to the schema.
  • src/native/pe/PeResourceParser.ts: readDelphiForms(), shaped after
    readIconGroups() — same 8 MiB metadata accounting (ensureCapacity before
    allocating, account per form), same synchronous pass over resources with
    type id 10. The first limitations entry is updated and the decoder's own
    limitations are reported with it.
  • src/contracts/native/binaryDiagnosticsToolContracts.ts and
    docs/binary-diagnostics.md: tool description and owning doc updated.

Intentionally not included: no text projection equivalent to
ObjectBinaryToText, no inspect_delphi_forms operation, no class semantics or
event-handler binding, and no runtime or provider involvement. The decoder reads
bytes already located by the resource inventory and never executes or mutates
anything, so it stays inside the documented "format inspection" boundary.

Scope question for maintainers: #2810 offers (A) decoding RCDATA payloads inside
inspect_pe_resources and (B) a separate operation. This implements (A) because
it matches how icon_groups already decodes one resource type's payload. If (B)
is preferred, the decoder and its tests move unchanged; only the wiring and the
contract change.

Contract and boundary impact

  • Semantic owner and earliest changed stage: inspect_pe_resources; the change
    starts at payload decoding, after the resource tree is complete.
  • Contribution scope fit and linked maintainer decision: static format
    inspection; scope decision requested in #2810 and left pending in this draft.
  • CLI and MCP/tool-catalog contract: peResourcesSchema gains a
    delphi_forms array. It is additive, always present, and empty when the image
    has no payload with a filer signature, so existing callers parsing a
    strictObject continue to validate. The tool description is updated; no
    input contract, tool name, or effect metadata changes.
  • Provider, bridge, target-format, or platform compatibility: provider-free and
    portable; no Ghidra, Windows SDK, or engine installation.
  • Evidence, artifact, provenance, or reconstruction contract: every decoded
    object, property and value carries {offset, bytes} into the resource payload,
    and the payload SHA-256 is already reported per resource, so a decoded claim
    can be traced to bytes and their hash.
  • Target selection, authority, upstream behavior, and cleanup details: not
    applicable; no process, no instrumentation, no writes.
  • Generated metadata: no new tool and no skills/ changes, so the generated
    catalogs are unaffected; docs/binary-diagnostics.md is the owning authored
    doc and is updated.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: src/native/pe/DelphiFormStream.test.ts (new, 37
    tests), src/native/pe/PeResourceParser.test.ts (+3 tests, 24 to 27) and
    src/application/binaryDiagnostics/PeResourcesService.test.ts (one rejection
    case for a value range outside the payload).
  • The fixtures are hand-authored TPF0 bytes written from the filer layout in
    src/native/pe/DelphiForm.fixture.ts, independent of the decoder, so a
    decoder regression cannot hide behind the fixture. Coverage includes each
    value type, nested components, filer flags with child position, cp1252 and
    cp1251 decodes with raw bytes retained, hex capping, and the failure surface:
    unsupported value type, truncated payload, payload ending mid-value, and a
    child position that is not an integer value type.
  • Parser coverage asserts the decoded form against the resource it came from:
    signature_location.offset equals the payload offset, root.location.offset
    equals payload offset + 4, the declared code page is used when the resource
    declares one (code_page_source: "resource") and recorded as a default when it
    does not ("default"), and non-form RCDATA payloads are left out. The result
    is parsed with peResourcesSchema.parse, so the schema and the decoder are
    checked against each other rather than only in isolation.
  • Base reproduction: on the base revision the same payload yields
    delphi_forms absent and the limitation "payloads other than RT_GROUP_ICON
    remain opaque"; the new tests fail there with a missing field rather than a
    wrong value.
  • Remaining proof gaps: no real Delphi binary was decoded end to end — the
    public sample from #2810 is on FTP, so coverage uses self-owned synthetic
    payloads. The acceptance suite through the compiled CLI and stdio MCP was not
    run in this environment (see Validation performed).

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run test:local -- src/native/pe/DelphiFormStream.test.ts src/application/binaryDiagnostics/PeResourcesService.test.ts -> 68 passed.
  • Reverting the source change makes all 12 new regressions fail, so they exercise the fixes instead of the fixtures: object nesting raises, the extended value underflows to 0, the list entries disappear, and the out-of-payload value range is accepted.
  • npm run lint -> no errors (existing function-length and complexity warnings remain in readValue/readObject).
  • npm run typecheck -> no errors in the changed files. Unrelated pre-existing errors remain in browser test fixtures in this environment.
  • npx oxfmt was applied by the pre-commit hook; git diff --check is clean.
  • Previously on this branch: PeResourceParser.test.ts 27 passed.

Not run, and not claimed as passing: npm run check:fast (its deps:check
pre-step fails in this environment), npm run test:acceptance (requires a
build), and npm run docs:check. These are expected to be covered by CI.

Compatibility, safety, and release

  • Breaking changes or migration steps: none; the new array is additive.
  • See Hopper/Ghidra, browser, or OS coverage: none required; the path is
    portable Node and provider-free.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: the decoder is a bounded
    read of bytes already in the artifact snapshot. It never executes the target,
    never resolves classes against a process, and never writes. Decode budgets
    (1024 objects, 8192 properties, 32768 values, 8 MiB decoded text per payload) and the existing
    8 MiB metadata budget bound the output; exhausting a budget is reported as an
    unknown rather than returning partial data.

Author readiness

Human owner: @Henglie

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/4285 **Author:** [@Henglie](https://github.com/Henglie) **Created:** 10/15/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/delphi-tpf0-forms` --- ### 📝 Commits (9) - [`73996a6`](https://github.com/morluto/rea/commit/73996a663619c7d4e19e5825769ad833651d2850) feat(native): decode Delphi/C++Builder TPF0 form payloads in PE resources - [`a8afb88`](https://github.com/morluto/rea/commit/a8afb885d974b0cc2793ab0ad483f597dba390e1) fix(native): bound Delphi form decoding and report its real limits - [`ec63efd`](https://github.com/morluto/rea/commit/ec63efdc13db9ceb2aa21860ef2cb567d6608951) fix(contracts): advertise the recursive form projections nonrecursively - [`1ba5dec`](https://github.com/morluto/rea/commit/1ba5decc3343c65ca0439903b5165948d149a77c) fix(native): keep form fields discoverable and decode gaps explicit - [`23c156b`](https://github.com/morluto/rea/commit/23c156b89a3aa9a4d517691cc6b984130dd89138) Merge remote-tracking branch 'origin/main' into feat/delphi-tpf0-forms - [`9acd7c9`](https://github.com/morluto/rea/commit/9acd7c9ee733771e452d7a3edf6d36f451244f27) Merge remote-tracking branch 'upstream/main' into feat/delphi-tpf0-forms - [`7d4d724`](https://github.com/morluto/rea/commit/7d4d724a7927fe38ca4d765b37262d45d6ad258e) fix(native): keep partial collections and advertised child fields - [`7cc298d`](https://github.com/morluto/rea/commit/7cc298de88ca7475f3a44470e1676927271e4197) fix(native): bound form recursion and keep partial decode evidence - [`2ac1a91`](https://github.com/morluto/rea/commit/2ac1a915e378cbb5084e50b37bf2cb0d030a4142) test(native): validate a decoded Delphi form against the SDK-converted schema ### 📊 Changes **12 files changed** (+2792 additions, -41 deletions) <details> <summary>View changed files</summary> 📝 `docs/binary-diagnostics.md` (+16 -1) 📝 `src/application/binaryDiagnostics/PeResourcesService.test.ts` (+125 -0) 📝 `src/application/binaryDiagnostics/PeResourcesService.ts` (+138 -38) 📝 `src/contracts/native/binaryDiagnosticsToolContracts.ts` (+1 -1) ➕ `src/domain/native/delphiForm.test.ts` (+124 -0) ➕ `src/domain/native/delphiForm.ts` (+356 -0) 📝 `src/domain/native/peResources.ts` (+2 -0) ➕ `src/native/pe/DelphiForm.fixture.ts` (+156 -0) ➕ `src/native/pe/DelphiFormStream.test.ts` (+766 -0) ➕ `src/native/pe/DelphiFormStream.ts` (+932 -0) 📝 `src/native/pe/PeResourceParser.test.ts` (+98 -0) 📝 `src/native/pe/PeResourceParser.ts` (+78 -1) </details> ### 📄 Description ## Summary `inspect_pe_resources` returned a complete PE32/PE32+ resource inventory and decoded `RT_GROUP_ICON`, but every other payload stayed an opaque byte range with a hash. Delphi/C++Builder VCL forms (`TPF0` RCDATA) were therefore unreadable without an external tool, which is what #2810 asks for. This adds a static, provider-free decoder for the Object Pascal filer stream over the resource payload that `inspect_pe_resources` has already located, reported as a new `delphi_forms` array next to `icon_groups`. Refs #2810. ## Problem and expected behavior For a Delphi target, each form lives in an `RT_RCDATA` resource whose payload starts with the filer signature `TPF0` (or `TPF1`) followed by a component tree: class name, component name, filer flags, properties with declared value types, and nested components. Before this change the tool reported the payload's RVA, file range and SHA-256 and nothing else, and the first `limitations` entry said payloads other than `RT_GROUP_ICON` remain opaque. After the change a caller gets the decoded component tree with byte offsets back into the payload for every object, property and value, with the original value bytes retained next to each decode. ## Change and scope - `src/native/pe/DelphiFormStream.ts` (new): bounded `TPF0` reader. Signature detection within a 16-byte window (bytes before the signature are kept verbatim, not interpreted), `BeginComponent` filer flags with child position, `ReadStr` short strings, and the `TValueType` payloads from `classesh.inc`/`reader.inc`: `vaInt8/16/32/64`, `vaQWord`, `vaSingle`, `vaDouble`, `vaExtended` (ten-byte x87 reduced to a double, with the precision loss stated), `vaCurrency`, `vaDate`, `vaString`, `vaIdent`, `vaLString`, `vaUTF8String`, `vaWString`, `vaUString`, `vaBinary`, `vaList`, `vaSet` and `vaCollection`. - `src/domain/native/delphiForm.ts` (new): the result types and their zod schemas, recursive through `z.lazy`. - `src/domain/native/peResources.ts`: `delphi_forms` added to the schema. - `src/native/pe/PeResourceParser.ts`: `readDelphiForms()`, shaped after `readIconGroups()` — same 8 MiB metadata accounting (`ensureCapacity` before allocating, `account` per form), same synchronous pass over resources with type id 10. The first `limitations` entry is updated and the decoder's own limitations are reported with it. - `src/contracts/native/binaryDiagnosticsToolContracts.ts` and `docs/binary-diagnostics.md`: tool description and owning doc updated. Intentionally not included: no text projection equivalent to `ObjectBinaryToText`, no `inspect_delphi_forms` operation, no class semantics or event-handler binding, and no runtime or provider involvement. The decoder reads bytes already located by the resource inventory and never executes or mutates anything, so it stays inside the documented "format inspection" boundary. Scope question for maintainers: #2810 offers (A) decoding RCDATA payloads inside `inspect_pe_resources` and (B) a separate operation. This implements (A) because it matches how `icon_groups` already decodes one resource type's payload. If (B) is preferred, the decoder and its tests move unchanged; only the wiring and the contract change. ## Contract and boundary impact - Semantic owner and earliest changed stage: `inspect_pe_resources`; the change starts at payload decoding, after the resource tree is complete. - Contribution scope fit and linked maintainer decision: static format inspection; scope decision requested in #2810 and left pending in this draft. - CLI and MCP/tool-catalog contract: `peResourcesSchema` gains a `delphi_forms` array. It is additive, always present, and empty when the image has no payload with a filer signature, so existing callers parsing a `strictObject` continue to validate. The tool `description` is updated; no input contract, tool name, or effect metadata changes. - Provider, bridge, target-format, or platform compatibility: provider-free and portable; no Ghidra, Windows SDK, or engine installation. - Evidence, artifact, provenance, or reconstruction contract: every decoded object, property and value carries `{offset, bytes}` into the resource payload, and the payload SHA-256 is already reported per resource, so a decoded claim can be traced to bytes and their hash. - Target selection, authority, upstream behavior, and cleanup details: not applicable; no process, no instrumentation, no writes. - Generated metadata: no new tool and no `skills/` changes, so the generated catalogs are unaffected; `docs/binary-diagnostics.md` is the owning authored doc and is updated. Expected effects: - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: src/native/pe/DelphiFormStream.test.ts (new, 37 tests), src/native/pe/PeResourceParser.test.ts (+3 tests, 24 to 27) and src/application/binaryDiagnostics/PeResourcesService.test.ts (one rejection case for a value range outside the payload). - The fixtures are hand-authored `TPF0` bytes written from the filer layout in `src/native/pe/DelphiForm.fixture.ts`, independent of the decoder, so a decoder regression cannot hide behind the fixture. Coverage includes each value type, nested components, filer flags with child position, cp1252 and cp1251 decodes with raw bytes retained, hex capping, and the failure surface: unsupported value type, truncated payload, payload ending mid-value, and a child position that is not an integer value type. - Parser coverage asserts the decoded form against the resource it came from: `signature_location.offset` equals the payload offset, `root.location.offset` equals payload offset + 4, the declared code page is used when the resource declares one (`code_page_source: "resource"`) and recorded as a default when it does not (`"default"`), and non-form RCDATA payloads are left out. The result is parsed with `peResourcesSchema.parse`, so the schema and the decoder are checked against each other rather than only in isolation. - Base reproduction: on the base revision the same payload yields `delphi_forms` absent and the limitation "payloads other than RT_GROUP_ICON remain opaque"; the new tests fail there with a missing field rather than a wrong value. - Remaining proof gaps: no real Delphi binary was decoded end to end — the public sample from #2810 is on FTP, so coverage uses self-owned synthetic payloads. The acceptance suite through the compiled CLI and stdio MCP was not run in this environment (see Validation performed). For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npm run test:local -- src/native/pe/DelphiFormStream.test.ts src/application/binaryDiagnostics/PeResourcesService.test.ts` -> 68 passed. - Reverting the source change makes all 12 new regressions fail, so they exercise the fixes instead of the fixtures: object nesting raises, the extended value underflows to 0, the list entries disappear, and the out-of-payload value range is accepted. - `npm run lint` -> no errors (existing function-length and complexity warnings remain in `readValue`/`readObject`). - `npm run typecheck` -> no errors in the changed files. Unrelated pre-existing errors remain in browser test fixtures in this environment. - `npx oxfmt` was applied by the pre-commit hook; `git diff --check` is clean. - Previously on this branch: `PeResourceParser.test.ts` 27 passed. Not run, and not claimed as passing: `npm run check:fast` (its `deps:check` pre-step fails in this environment), `npm run test:acceptance` (requires a build), and `npm run docs:check`. These are expected to be covered by CI. ## Compatibility, safety, and release - Breaking changes or migration steps: none; the new array is additive. - See Hopper/Ghidra, browser, or OS coverage: none required; the path is portable Node and provider-free. - Package or release metadata impact: none. - Security, privacy, process, or containment review: the decoder is a bounded read of bytes already in the artifact snapshot. It never executes the target, never resolves classes against a process, and never writes. Decode budgets (1024 objects, 8192 properties, 32768 values, 8 MiB decoded text per payload) and the existing 8 MiB metadata budget bound the output; exhausting a budget is reported as an unknown rather than returning partial data. ## Author readiness Human owner: @Henglie - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#6157
No description provided.