From e103a89889d4fe5291d8f9125c62fbf1485b621c Mon Sep 17 00:00:00 2001 From: xin zhuang <65798732+1052326311@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:36:06 +0800 Subject: [PATCH] fix(artifacts): preserve NIB key and class name identity (#2109) Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/artifacts/apple/NibArchive.test.ts | 16 +++++++ .../filesystem/interfaceBuilderBundle.test.ts | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/artifacts/apple/NibArchive.test.ts b/src/artifacts/apple/NibArchive.test.ts index 10579d011..665617d28 100644 --- a/src/artifacts/apple/NibArchive.test.ts +++ b/src/artifacts/apple/NibArchive.test.ts @@ -36,6 +36,22 @@ const archiveWithOverlappingObjectRanges = (objectCount: number): Buffer => { }; describe("NIBArchive decoder", () => { + it("keeps leading U+FEFF in length-prefixed keys and class names", () => { + const archive = encodeArchive({ + classes: ["\uFEFFWidget"], + objects: [{ classIndex: 0, values: { count: 1, "\uFEFFcount": 2 } }], + }); + expect(decodeNibArchive(archive).objects).toEqual([ + { + id: 0, + class_name: "\uFEFFWidget", + values: { count: 1, "\uFEFFcount": 2 }, + }, + ]); + const invalid = Buffer.from(archive); + invalid[invalid.indexOf(Buffer.from("\uFEFFcount"))] = 0xff; + expect(() => decodeNibArchive(invalid)).toThrow(/not valid UTF-8/u); + }); it("decodes bounded object, key, class, and reference tables", () => { const archive = encodeArchive({ classes: ["NSView\0", "NSString\0"], diff --git a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts index d584aaf3c..21ec633c5 100644 --- a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts +++ b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts @@ -696,6 +696,49 @@ describe("NIB projection byte amplification", () => { }); }); +cliTest( + "compiled CLI preserves distinct NIB keys and class identity", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-nib-name-identity-"); + const bundle = join(root, "Example.app"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + await mkdir(join(bundle, "Contents", "MacOS"), { recursive: true }); + await writeFile( + join(bundle, "Contents", "MacOS", "App"), + thinMach(0xfeedfacf, 0x0100000c), + ); + await writeFile( + join(bundle, "Contents", "Info.plist"), + "CFBundleExecutableApp", + ); + await writeFile( + join(resources, "Identity.nib"), + encodeNibArchiveFixture({ + classes: ["\uFEFFWidget"], + objects: [{ classIndex: 0, values: { count: 1, "\uFEFFcount": 2 } }], + }), + ); + const output = await cli.run({ + arguments: ["decode-interface-builder", bundle, "--json"], + environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" }, + }); + expect(output.exitCode, output.stderr).toBe(0); + const result = interfaceBuilderAnalysisSchema.parse( + parseEvidence(output.json).normalized_result, + ); + expect(result.graph.nodes).toContainEqual( + expect.objectContaining({ + attributes: expect.objectContaining({ + class_name: "\uFEFFWidget", + nibValues: { count: 1, "\uFEFFcount": 2 }, + }), + }), + ); + expect(JSON.stringify(result)).toContain("\uFEFFWidget"); + }, +); + cliTest( "compiled CLI preserves a good archive and reports repeated key and data omissions", async ({ cli }) => {