[GH-ISSUE #2810] [Feature] Decode Delphi/C++Builder VCL form resources (TPF0 RCDATA) statically #1001

Open
opened 2026-10-15 18:30:15 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @matbrgz on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2810

Type: Feature Domain: Reverse Engineering Status: Proposed Ecosystem: REA

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal (searched DFM, TPF0, Delphi, RCDATA; related but different: #2373, #2375, #1254).
  • I understand the contribution scope must be reviewed before integration.
  • This request does not include credentials, private samples, or sensitive target data (the sample is a public Brazilian Ministry of Health installer).

Area

Evidence, artifacts, or reconstruction

Current limitation

Delphi/C++Builder VCL applications keep every form (.dfm) as a binary RT_RCDATA resource whose payload starts with the TPF0 signature. The form stream holds the component tree, published event-handler names (OnClick = bitsalvarClick), field bindings and, for database apps, the literal SQL (SQL.Strings, CommandText). For legacy line-of-business software this is often the single richest source of business rules.

inspect-pe-resources (6.4.0) inventories these resources correctly but keeps them opaque:

$ rea --version
6.4.0
$ rea inspect-pe-resources /abs/AUTORIZADOR.exe --json
# normalized_result.resources: 272 entries, 99 with type id 10 (RT_RCDATA)
# limitations: "payloads other than RT_GROUP_ICON remain opaque."

Example entry (index 172): type 10, name TFCADAPAC, payload offset 3884040, 93,212 bytes, sha256 12b168ad…0bfe; first bytes 54 50 46 30 09 54 46 43 61 64 41 50 41 43 = TPF0 + TFCadAPAC.

Environment: macOS 27.0.1 arm64, Node 24.13.0, rea-agents@6.4.0 installed locally (no agent setup), no native provider configured.

Proposed outcome

A provider-free, static decoder for Delphi TPF0 form streams, either as a payload decoder inside inspect_pe_resources or as a separate inspect_delphi_forms operation:

  • Detect TPF0 (and the FF 0A 00 RCDATA prefix variant) and decode the object tree: class name, component name, flags (inherited/inline), properties with the original value type (vaInt8/16/32, vaString, vaLString, vaWString/vaUTF8String, vaIdent, vaSet, vaList, vaCollection, vaBinary, vaExtended, vaDate…).
  • Keep byte offsets for every object and property, and the payload sha256, so each decoded fact links back to the resource evidence.
  • Report undecodable value types or truncated streams as explicit unknowns instead of failing the whole inventory.
  • Optional text projection equivalent to Delphi's ObjectBinaryToText.
  • String values are bytes in the form's code page (Delphi 7 = ANSI, here cp1252); keep raw bytes plus a decoded display, never guess silently.

Use cases

  • Recover event-handler → component wiring and the SQL embedded in data modules (TSQLQuery.SQL.Strings, TClientDataSet, TQuery) of legacy Delphi 2–2007 apps.
  • Diff forms across two builds of the same app (here 6.6.2.0 → 6.6.3.0 changed SQL in TDmCMD and a combo box in TFCadAPAC).
  • Name seeds for native analysis: published handler names map to addresses through the VMT method table (see the related VMT proposal).

Minimal public repro

  • Installer: ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe (sha256 0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44), extract with innoextract -e; target app/AUTORIZADOR.exe (sha256 1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363, PE32, Delphi 7).
  • Stable alternative: AUTORIZADOR_SETUP.exe (sha256 226e68e4e5c2ef3d8749bdc788481f80eb79d5ac13dc51c89ecdc1cf96082aaa), app/AUTORIZADOR.exe sha256 708b9749…22f4f, 97 RCDATA entries.
  • A ~300-line reference decoder used for cross-checking exists in a private research repo; its output for these 99 resources can be shared as expected fixtures on request.

Suggested approach

Pure TypeScript reader over the already-owned resource snapshot; the format is small and documented by the Classes.pas reader (TReader.ReadValue). Bound recursion depth and per-payload size, and keep it static (no VCL, no execution).

Originally created by @matbrgz on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2810 [![Type: Feature](https://img.shields.io/badge/type-feature-blue.svg?style=flat-square)](https://github.com/morluto/rea) [![Domain: Reverse Engineering](https://img.shields.io/badge/domain-reverse_engineering-blueviolet.svg?style=flat-square)](https://github.com/morluto/rea) [![Status: Proposed](https://img.shields.io/badge/status-proposed-blue.svg?style=flat-square)](https://github.com/morluto/rea) [![Ecosystem: REA](https://img.shields.io/badge/ecosystem-morluto%2Frea-8A2BE2.svg?style=flat-square)](https://github.com/morluto/rea) ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal (searched `DFM`, `TPF0`, `Delphi`, `RCDATA`; related but different: #2373, #2375, #1254). - [x] I understand the contribution scope must be reviewed before integration. - [x] This request does not include credentials, private samples, or sensitive target data (the sample is a public Brazilian Ministry of Health installer). ### Area Evidence, artifacts, or reconstruction ### Current limitation Delphi/C++Builder VCL applications keep every form (`.dfm`) as a binary `RT_RCDATA` resource whose payload starts with the `TPF0` signature. The form stream holds the component tree, published event-handler names (`OnClick = bitsalvarClick`), field bindings and, for database apps, the literal SQL (`SQL.Strings`, `CommandText`). For legacy line-of-business software this is often the single richest source of business rules. `inspect-pe-resources` (6.4.0) inventories these resources correctly but keeps them opaque: ```text $ rea --version 6.4.0 $ rea inspect-pe-resources /abs/AUTORIZADOR.exe --json # normalized_result.resources: 272 entries, 99 with type id 10 (RT_RCDATA) # limitations: "payloads other than RT_GROUP_ICON remain opaque." ``` Example entry (index 172): type 10, name `TFCADAPAC`, payload offset `3884040`, 93,212 bytes, sha256 `12b168ad…0bfe`; first bytes `54 50 46 30 09 54 46 43 61 64 41 50 41 43` = `TPF0` + `TFCadAPAC`. Environment: macOS 27.0.1 arm64, Node 24.13.0, `rea-agents@6.4.0` installed locally (no agent setup), no native provider configured. ### Proposed outcome A provider-free, static decoder for Delphi `TPF0` form streams, either as a payload decoder inside `inspect_pe_resources` or as a separate `inspect_delphi_forms` operation: - Detect `TPF0` (and the `FF 0A 00` RCDATA prefix variant) and decode the object tree: class name, component name, flags (inherited/inline), properties with the original value type (`vaInt8/16/32`, `vaString`, `vaLString`, `vaWString`/`vaUTF8String`, `vaIdent`, `vaSet`, `vaList`, `vaCollection`, `vaBinary`, `vaExtended`, `vaDate`…). - Keep byte offsets for every object and property, and the payload sha256, so each decoded fact links back to the resource evidence. - Report undecodable value types or truncated streams as explicit unknowns instead of failing the whole inventory. - Optional text projection equivalent to Delphi's `ObjectBinaryToText`. - String values are bytes in the form's code page (Delphi 7 = ANSI, here cp1252); keep raw bytes plus a decoded display, never guess silently. ### Use cases - Recover event-handler → component wiring and the SQL embedded in data modules (`TSQLQuery.SQL.Strings`, `TClientDataSet`, `TQuery`) of legacy Delphi 2–2007 apps. - Diff forms across two builds of the same app (here 6.6.2.0 → 6.6.3.0 changed SQL in `TDmCMD` and a combo box in `TFCadAPAC`). - Name seeds for native analysis: published handler names map to addresses through the VMT method table (see the related VMT proposal). ### Minimal public repro - Installer: `ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe` (sha256 `0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44`), extract with `innoextract -e`; target `app/AUTORIZADOR.exe` (sha256 `1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363`, PE32, Delphi 7). - Stable alternative: `AUTORIZADOR_SETUP.exe` (sha256 `226e68e4e5c2ef3d8749bdc788481f80eb79d5ac13dc51c89ecdc1cf96082aaa`), `app/AUTORIZADOR.exe` sha256 `708b9749…22f4f`, 97 RCDATA entries. - A ~300-line reference decoder used for cross-checking exists in a private research repo; its output for these 99 resources can be shared as expected fixtures on request. ### Suggested approach Pure TypeScript reader over the already-owned resource snapshot; the format is small and documented by the `Classes.pas` reader (`TReader.ReadValue`). Bound recursion depth and per-payload size, and keep it static (no VCL, no execution).
Author
Owner

@matbrgz commented on GitHub (Oct 13, 2026):

Additional use case from the same target: the newer build 6.6.3.0 (inner AUTORIZADOR.exe sha256 1398d55a…8fa363) replaced 6.6.2.0 on the public FTP. Comparing decoded forms between the two builds was the fastest way to find behavior changes: 7 of 99 RCDATA forms differ, and after filtering geometry (Left/Top/Width/Height) the real changes are a new TSQLQuery in TDMCMD, a sex combo box whose item texts change the stored value ('1-Masculino' → 'Masculino'), a new OnExit handler in TFCADPROFSOL, and a new OnPrint in TFRELATORIOS. A decoder that keeps per-property offsets would also make a geometry-insensitive form diff possible inside REA (compare_artifacts).

<!-- gh-comment-id:6147255968 --> @matbrgz commented on GitHub (Oct 13, 2026): Additional use case from the same target: the newer build 6.6.3.0 (inner `AUTORIZADOR.exe` sha256 `1398d55a…8fa363`) replaced 6.6.2.0 on the public FTP. Comparing decoded forms between the two builds was the fastest way to find behavior changes: 7 of 99 RCDATA forms differ, and after filtering geometry (Left/Top/Width/Height) the real changes are a new `TSQLQuery` in `TDMCMD`, a sex combo box whose item texts change the stored value (`'1-Masculino'` → `'Masculino'`), a new `OnExit` handler in `TFCADPROFSOL`, and a new `OnPrint` in `TFRELATORIOS`. A decoder that keeps per-property offsets would also make a geometry-insensitive form diff possible inside REA (`compare_artifacts`).
Author
Owner

@Henglie commented on GitHub (Oct 15, 2026):

Happy to take this one on.

Before this grows further I'd like to confirm the intended shape, since it decides the
contract:

  • (A) decode RCDATA payloads inside inspect_pe_resources, the way icon_groups already
    decodes RT_GROUP_ICON — a new delphi_forms array alongside icon_groups, with the
    limitations wording updated; or
  • (B) a separate inspect_delphi_forms operation with its own input/output contract.

I implemented (A) in draft PR #4285, because it matches how icon_groups already decodes one
resource type's payload. Reshaping it as (B) keeps the decoder and its tests and only changes
the wiring and the contract, so say the word if that is what you prefer.

My reading of the contribution scope is that a provider-free, static decoder over the
already-owned resource snapshot stays inside the supported "format inspection" boundary — it
reads bytes that inspect_pe_resources has already located, and never executes or mutates
anything — so I have treated this comment as the scope confirmation rather than opening a
separate design discussion. If you would rather treat it as a new capability that needs a
formal scope decision, I will start that discussion instead.

What the draft does today:

  • a pure TypeScript reader for the TPF0 stream: object tree, class and component names,
    filer flags (ffInherited/ffChildPos/ffInline), and properties with their original
    value types, each with byte offsets back into the resource payload;
  • undecodable value types and truncated streams reported as explicit unknowns rather than
    failing the whole inventory, and a value type whose length cannot be derived stops decoding
    instead of resynchronising on a guess;
  • raw string bytes retained alongside a decode, with the code page recorded rather than
    guessed (windows-1252 when the resource declares none, and recorded as a default);
  • synthetic self-owned fixtures for regression coverage, since the public sample is on FTP.

On your second comment: the per-property offsets are kept, so a geometry-insensitive form diff
between two builds is a follow-up this could feed.

<!-- gh-comment-id:6204452679 --> @Henglie commented on GitHub (Oct 15, 2026): Happy to take this one on. Before this grows further I'd like to confirm the intended shape, since it decides the contract: - **(A)** decode RCDATA payloads inside `inspect_pe_resources`, the way `icon_groups` already decodes `RT_GROUP_ICON` — a new `delphi_forms` array alongside `icon_groups`, with the `limitations` wording updated; or - **(B)** a separate `inspect_delphi_forms` operation with its own input/output contract. I implemented (A) in draft PR #4285, because it matches how `icon_groups` already decodes one resource type's payload. Reshaping it as (B) keeps the decoder and its tests and only changes the wiring and the contract, so say the word if that is what you prefer. My reading of the contribution scope is that a provider-free, static decoder over the already-owned resource snapshot stays inside the supported "format inspection" boundary — it reads bytes that `inspect_pe_resources` has already located, and never executes or mutates anything — so I have treated this comment as the scope confirmation rather than opening a separate design discussion. If you would rather treat it as a new capability that needs a formal scope decision, I will start that discussion instead. What the draft does today: - a pure TypeScript reader for the `TPF0` stream: object tree, class and component names, filer flags (`ffInherited`/`ffChildPos`/`ffInline`), and properties with their original value types, each with byte offsets back into the resource payload; - undecodable value types and truncated streams reported as explicit unknowns rather than failing the whole inventory, and a value type whose length cannot be derived stops decoding instead of resynchronising on a guess; - raw string bytes retained alongside a decode, with the code page recorded rather than guessed (`windows-1252` when the resource declares none, and recorded as a default); - synthetic self-owned fixtures for regression coverage, since the public sample is on FTP. On your second comment: the per-property offsets are kept, so a geometry-insensitive form diff between two builds is a follow-up this could feed.
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#1001
No description provided.