[PR #899] [MERGED] fix(artifacts): decode flat keyed-archive NIBs with data and shared references #2331

Closed
opened 2026-10-16 21:17:24 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/899
Author: @kaoru0822-kitauji
Created: 10/7/2026
Status: ✅ Merged
Merged: 10/7/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: fix/interface-builder-plist-data


📝 Commits (4)

  • 2a4667b fix(artifacts): decode flat keyed-archive NIBs with data and shared references
  • 96a9f32 chore: merge main and resolve PR #899 conflicts
  • 071cfd1 chore: merge main and preserve plist and AppKit regressions
  • be121d8 chore: merge main and preserve keyed archive regressions

📊 Changes

4 files changed (+250 additions, -52 deletions)

View changed files

📝 src/application/InterfaceBuilderAnalysis.ts (+3 -1)
📝 src/domain/interfaceBuilderGraph.test.ts (+85 -0)
📝 src/domain/interfaceBuilderKeyedArchive.ts (+118 -51)
📝 tests/boundary/filesystem/interfaceBuilderBundle.test.ts (+44 -0)

📄 Description

Summary

decode_interface_builder / rea decode-interface-builder could not decode flat (keyed-archive) NIBs, the format older Interface Builder and many AppKit apps and frameworks ship. There were two failures:

1. Plist data and dates failed validation. The decoder ran jsonValueSchema.parse on the decoded plist, but <data> becomes a Uint8Array and <date> a Date. Archived colors keep their components as data (NSWhite, NSRGB), so nearly every AppKit NIB failed. The whole document was dropped with a raw invalid_union dump in limitations. The decoder now uses the shared projectPlistValue, which inspect_keyed_archive and inspect_plist already use and which keeps these values as explicit {"$plist_type":"data","base64":…} / {"$plist_type":"date",…}.

2. Exponential reference expansion. Fixing (1) exposed this one. For every object in the table, parseKeyedArchive inlined each UID reference recursively, up to 16 levels. AppKit objects reference each other densely and cyclically: a view points to its window, superview, next key view, and subviews. The archive therefore expanded into a tree exponential in that depth, in both time and attribute size. A reference now resolves to:

  • the string, when it points to a string or an NSString/NSMutableString object;
  • null for UID 0, the archived nil (previously the literal "$null");
  • otherwise a stub { archiveUID, objectID?, className }, where objectID is the authored ID the referenced node is published under.

Connection endpoints and labels resolve through the same stubs. The hierarchy walk dereferences the object table directly and visits each object once.

While rewriting the resolver, I made it read XML references too. A UID is a one-key UID (binary) or CF$UID (XML) dictionary, matching keyedArchive.ts. Before, XML keyed archives resolved no references.

App (real, /Applications) main this PR
XLD 20250302 0 of 64 archives decoded (one_or_more_archives_invalid), 109 omitted by the limit 64 decoded, 5851 nodes, 1.2 s
Transmission 4.1.2 28 of 33 (Sparkle's 5 flat NIBs invalid) 33 of 33, archive_decode complete
XLD MainMenu.nib (158 KB) alone, with only fix (1) did not finish 258 ms
XLD XLDAacOutput.nib (9 KB) alone, with only fix (1) 3.1 s 29 ms
KeepingYouAwake, Chess, Shadowrocket (NIBArchive / storyboard) — byte-identical result

No schema or catalog change. Object attributes now hold stubs where they held deep copies of referenced objects. Every referenced object is still its own node with its own attributes.

Not changed here: flat AppKit NIBs still produce no outlet/action edges. Their connectors use NSSource/NSDestination/NSLabel, and the keyed-archive parser reads only source/destination/label (and from/to, …). That is a separate follow-up.

Verification

  • src/domain/interfaceBuilderGraph.test.ts:
    • a 40-view archive with window, superview, next-key-view, and subview references yields exact stub attributes and the window → view hierarchy;
    • XML CF$UID references resolve an outlet's source, NSMutableString label, and nil destination.
      Both fail on main. The first takes 2 s there.
  • tests/boundary/filesystem/interfaceBuilderBundle.test.ts: binary and XML flat NIBs holding data and date values decode with archive_decode complete. Fails on main.
  • Real apps as in the table above (main and branch builds side by side).
  • npm run check:changed (29 files, 248 tests) passes. The ibtool-compiled NIB tests (nibNativeHierarchy, the compiled storyboard case) fail the same way on main on this host because it has only the Command Line Tools and no ibtool. They decode NIBArchive output, which does not use the keyed-archive parser.

No Hopper/Ghidra/IDA verification is needed.

🤖 Generated with Claude Code


🔄 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/899 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/7/2026 **Status:** ✅ Merged **Merged:** 10/7/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `fix/interface-builder-plist-data` --- ### 📝 Commits (4) - [`2a4667b`](https://github.com/morluto/rea/commit/2a4667bb79117b202abb009c0bb9ebfdfde57158) fix(artifacts): decode flat keyed-archive NIBs with data and shared references - [`96a9f32`](https://github.com/morluto/rea/commit/96a9f32dc1f04befad5c3864c39ad7ff9a9cf8c5) chore: merge main and resolve PR #899 conflicts - [`071cfd1`](https://github.com/morluto/rea/commit/071cfd1bb6a78dfa3e704e61cd3972c50de0c88c) chore: merge main and preserve plist and AppKit regressions - [`be121d8`](https://github.com/morluto/rea/commit/be121d87eb86980d57be9e7d1254e2c236f568c2) chore: merge main and preserve keyed archive regressions ### 📊 Changes **4 files changed** (+250 additions, -52 deletions) <details> <summary>View changed files</summary> 📝 `src/application/InterfaceBuilderAnalysis.ts` (+3 -1) 📝 `src/domain/interfaceBuilderGraph.test.ts` (+85 -0) 📝 `src/domain/interfaceBuilderKeyedArchive.ts` (+118 -51) 📝 `tests/boundary/filesystem/interfaceBuilderBundle.test.ts` (+44 -0) </details> ### 📄 Description ## Summary `decode_interface_builder` / `rea decode-interface-builder` could not decode flat (keyed-archive) NIBs, the format older Interface Builder and many AppKit apps and frameworks ship. There were two failures: **1. Plist data and dates failed validation.** The decoder ran `jsonValueSchema.parse` on the decoded plist, but `<data>` becomes a `Uint8Array` and `<date>` a `Date`. Archived colors keep their components as data (`NSWhite`, `NSRGB`), so nearly every AppKit NIB failed. The whole document was dropped with a raw `invalid_union` dump in `limitations`. The decoder now uses the shared `projectPlistValue`, which `inspect_keyed_archive` and `inspect_plist` already use and which keeps these values as explicit `{"$plist_type":"data","base64":…}` / `{"$plist_type":"date",…}`. **2. Exponential reference expansion.** Fixing (1) exposed this one. For every object in the table, `parseKeyedArchive` inlined each UID reference recursively, up to 16 levels. AppKit objects reference each other densely and cyclically: a view points to its window, superview, next key view, and subviews. The archive therefore expanded into a tree exponential in that depth, in both time and attribute size. A reference now resolves to: - the string, when it points to a string or an `NSString`/`NSMutableString` object; - `null` for UID 0, the archived nil (previously the literal `"$null"`); - otherwise a stub `{ archiveUID, objectID?, className }`, where `objectID` is the authored ID the referenced node is published under. Connection endpoints and labels resolve through the same stubs. The hierarchy walk dereferences the object table directly and visits each object once. While rewriting the resolver, I made it read XML references too. A UID is a one-key `UID` (binary) or `CF$UID` (XML) dictionary, matching `keyedArchive.ts`. Before, XML keyed archives resolved no references. | App (real, `/Applications`) | `main` | this PR | | --- | --- | --- | | XLD 20250302 | 0 of 64 archives decoded (`one_or_more_archives_invalid`), 109 omitted by the limit | 64 decoded, 5851 nodes, 1.2 s | | Transmission 4.1.2 | 28 of 33 (Sparkle's 5 flat NIBs invalid) | 33 of 33, `archive_decode` complete | | XLD `MainMenu.nib` (158 KB) alone, with only fix (1) | did not finish | 258 ms | | XLD `XLDAacOutput.nib` (9 KB) alone, with only fix (1) | 3.1 s | 29 ms | | KeepingYouAwake, Chess, Shadowrocket (NIBArchive / storyboard) | — | byte-identical result | No schema or catalog change. Object `attributes` now hold stubs where they held deep copies of referenced objects. Every referenced object is still its own node with its own attributes. **Not changed here:** flat AppKit NIBs still produce no outlet/action edges. Their connectors use `NSSource`/`NSDestination`/`NSLabel`, and the keyed-archive parser reads only `source`/`destination`/`label` (and `from`/`to`, …). That is a separate follow-up. ## Verification - `src/domain/interfaceBuilderGraph.test.ts`: - a 40-view archive with window, superview, next-key-view, and subview references yields exact stub attributes and the window → view hierarchy; - XML `CF$UID` references resolve an outlet's source, `NSMutableString` label, and nil destination. Both fail on `main`. The first takes 2 s there. - `tests/boundary/filesystem/interfaceBuilderBundle.test.ts`: binary and XML flat NIBs holding data and date values decode with `archive_decode` complete. Fails on `main`. - Real apps as in the table above (main and branch builds side by side). - `npm run check:changed` (29 files, 248 tests) passes. The ibtool-compiled NIB tests (`nibNativeHierarchy`, the compiled storyboard case) fail the same way on `main` on this host because it has only the Command Line Tools and no `ibtool`. They decode NIBArchive output, which does not use the keyed-archive parser. No Hopper/Ghidra/IDA verification is needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-16 21:17:24 +00:00
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#2331
No description provided.