mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): preserve leading U+FEFF in Apple metadata names (#4730)
* fix(native): preserve leading U+FEFF in Apple metadata names * test(native): run property metadata regressions on macOS
This commit is contained in:
@@ -653,6 +653,7 @@ jobs:
|
||||
tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts
|
||||
tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
tests/boundary/cli/nativeDemangleCli.test.ts
|
||||
tests/boundary/cli/nativeDispatchPropertyAttributes.test.ts
|
||||
tests/boundary/mcp/nativeDemangleMcp.test.ts
|
||||
tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts
|
||||
tests/boundary/cli/nativeSignatureCli.test.ts
|
||||
|
||||
@@ -77,6 +77,7 @@ export const SCOPES = {
|
||||
"tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts",
|
||||
"tests/boundary/cli/nativePlistSelectionFailureCli.test.ts",
|
||||
"tests/boundary/cli/nativeDemangleCli.test.ts",
|
||||
"tests/boundary/cli/nativeDispatchPropertyAttributes.test.ts",
|
||||
"tests/boundary/mcp/nativeDemangleMcp.test.ts",
|
||||
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
|
||||
"tests/boundary/cli/nativeSignatureCli.test.ts",
|
||||
|
||||
@@ -87,7 +87,8 @@ export const decodeAppleDispatchMetadata = (
|
||||
if (end === bytes.length || end - start >= 4096)
|
||||
throw new RangeError("Unterminated or oversized metadata string");
|
||||
offset(address, end - start + 1);
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
// A metadata C string owns every code point, including an initial U+FEFF.
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes.subarray(start, end),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,35 @@ import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native dispatch property attributes",
|
||||
() => {
|
||||
cliTest(
|
||||
"preserves a leading U+FEFF in a binary property name",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-objc-property-");
|
||||
const path = join(directory, "fixture.bin");
|
||||
await writeFile(
|
||||
path,
|
||||
machoWithPropertyAttributes("Tq,N", { firstName: "\ufefffirst" }),
|
||||
);
|
||||
const output = await cli.run({
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(output.exitCode, `${output.stdout}\n${output.stderr}`).toBe(0);
|
||||
expect(output.json).toMatchObject({
|
||||
normalized_result: {
|
||||
result: {
|
||||
objc_classes: [
|
||||
{
|
||||
name: "Fixture",
|
||||
properties: [{ name: "\ufefffirst" }, { name: "second" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for([
|
||||
["complete nonatomic", "Tq,N", "nonatomic", false, "complete"],
|
||||
["complete default", 'T@"NSString",C', "atomic", false, "complete"],
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { inspectAppleDispatchMetadata } from "../../../src/native/AppleDispatchMetadata.js";
|
||||
import { withArtifactResourceScope } from "../../fixtures/artifactInventory.js";
|
||||
import { machoWithPropertyAttributes } from "../../fixtures/objcPropertyMacho.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.each([
|
||||
["plain names", "Fixture", "first", "perform:"],
|
||||
["leading U+FEFF", "\ufeffFixture", "\ufefffirst", "\ufeffperform:"],
|
||||
["U+FEFF-only names", "\ufeff", "\ufeff", "\ufeff"],
|
||||
] as const)(
|
||||
"preserves %s in file-backed Apple metadata identities",
|
||||
async (_, className, propertyName, selector) => {
|
||||
const directory = await createTestTempDirectory("rea-dispatch-strings-");
|
||||
const path = join(directory, "fixture.bin");
|
||||
const bytes = machoWithPropertyAttributes("Tq,N", {
|
||||
firstName: propertyName,
|
||||
});
|
||||
bytes.write(`${className}\0`, 0x400);
|
||||
const base = 0x100000000n;
|
||||
bytes.writeBigUInt64LE(base + 0x700n, 0x320);
|
||||
bytes.writeUInt32LE(24, 0x700);
|
||||
bytes.writeUInt32LE(1, 0x704);
|
||||
bytes.writeBigUInt64LE(base + 0x750n, 0x708);
|
||||
bytes.writeBigUInt64LE(base + 0x780n, 0x710);
|
||||
bytes.writeBigUInt64LE(base + 0x800n, 0x718);
|
||||
bytes.write(`${selector}\0`, 0x750);
|
||||
bytes.write("v@:\0", 0x780);
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const metadata = await withArtifactResourceScope((resources) =>
|
||||
resources.run(() =>
|
||||
inspectAppleDispatchMetadata(target.value, 100, resources),
|
||||
),
|
||||
);
|
||||
expect(metadata.target_sha256).toBe(
|
||||
createHash("sha256").update(bytes).digest("hex"),
|
||||
);
|
||||
expect(metadata.result.objc_classes).toMatchObject([
|
||||
{
|
||||
name: className,
|
||||
methods: [{ selector }],
|
||||
properties: [{ name: propertyName }, { name: "second" }],
|
||||
location: { address: "0x100000200", file_offset: 0x200 },
|
||||
evidence: [
|
||||
{
|
||||
artifact_path: target.value.path,
|
||||
artifact_sha256: target.value.sha256,
|
||||
location: { address: "0x100000200", file_offset: 0x200 },
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(metadata.result.objc_dispatch_implementations).toMatchObject([
|
||||
{
|
||||
class_name: className,
|
||||
selector,
|
||||
implementation_address: "0x100000800",
|
||||
location: { address: "0x100000708", file_offset: 0x708 },
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
metadata.result.coverage.find(
|
||||
({ facet }) => facet === "objc_properties_categories",
|
||||
),
|
||||
).toMatchObject({ status: "complete", reason: null });
|
||||
expect(await readFile(path)).toEqual(bytes);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["malformed UTF-8", [0xef, 0xbb, 0]],
|
||||
["an empty name", [0]],
|
||||
] as const)(
|
||||
"keeps %s partial beside a valid Apple metadata identity",
|
||||
async (_, name) => {
|
||||
const directory = await createTestTempDirectory("rea-dispatch-strings-");
|
||||
const path = join(directory, "fixture.bin");
|
||||
const bytes = machoWithPropertyAttributes("Tq,N");
|
||||
// Invalid UTF-8 and a NUL-only name remain distinct from EF BB BF payload.
|
||||
bytes.set(name, 0x600);
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const metadata = await withArtifactResourceScope((resources) =>
|
||||
resources.run(() =>
|
||||
inspectAppleDispatchMetadata(target.value, 100, resources),
|
||||
),
|
||||
);
|
||||
expect(metadata.result.objc_classes).toMatchObject([
|
||||
{ name: "Fixture", properties: [{ name: "second" }] },
|
||||
]);
|
||||
expect(
|
||||
metadata.result.coverage.find(
|
||||
({ facet }) => facet === "objc_properties_categories",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("property_decode_failed"),
|
||||
});
|
||||
expect(await readFile(path)).toEqual(bytes);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user