diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 5f533d4cb..766182713 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -324,6 +324,9 @@ complete observation. shared/cyclic references, class descriptors, raw serialized fields and malformed/unresolved/nil references. Missing fields stay absent; unknown classes are preserved. Select `root`, `offset` and `limit` explicitly. + Object `kind` describes the decoded archive value: data, dates and numbers + remain `scalar` when their JSON value uses a `$plist_type` wrapper. An actual + dictionary with the same fields remains `dictionary`. UID 0 cannot distinguish conditional nil from ordinary nil. Limits are 64 MiB of input, 200,000 objects/references and 128 nesting levels. XML dictionary entries decoded as `__proto__` are omitted with an explicit diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts index 0da53002f..e2340f3ad 100644 --- a/src/artifacts/apple/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -6,10 +6,7 @@ import { AnalysisInputError, AnalysisUnsupportedTargetError, } from "../../domain/analysisErrorCore.js"; -import { - projectPlistValue, - unknownDateLimitation, -} from "../../domain/apple/plistValue.js"; +import { unknownDateLimitation } from "../../domain/apple/plistValue.js"; import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js"; import { decodeBinaryPlistNumbers, @@ -85,12 +82,10 @@ export const decodeKeyedArchiveBytes = ( : 0, } : parseXmlPropertyList(xmlText ?? ""); - const { - value: plistValue, - unknownRealCount, - unknownDateCount, - } = projectPlistValue(parsed.value); - const graph = projectKeyedArchive(plistValue, selection); + const { unknownRealCount, unknownDateCount, ...graph } = projectKeyedArchive( + parsed.value, + selection, + ); const numbers = createPlistNumberProjection( binary ? binaryNumberMetadata!.literals diff --git a/src/domain/apple/keyedArchive.test.ts b/src/domain/apple/keyedArchive.test.ts index 30881efbf..2610ff911 100644 --- a/src/domain/apple/keyedArchive.test.ts +++ b/src/domain/apple/keyedArchive.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; -import { z } from "zod"; - -import { MAX_JSON_DEPTH } from "../jsonValue.js"; +import { MAX_PLIST_DEPTH } from "./plistValue.js"; import { projectKeyedArchive } from "./keyedArchive.js"; const deepValue = (depth: number): unknown => { @@ -11,18 +9,13 @@ const deepValue = (depth: number): unknown => { }; describe("projectKeyedArchive input bound", () => { - it("rejects an over-deep archive value with a validation error instead of crashing", () => { - let error: unknown; - try { - projectKeyedArchive(deepValue(MAX_JSON_DEPTH + 1), { + it("rejects over-deep decoded input at the plist projection limit", () => { + expect(() => + projectKeyedArchive(deepValue(MAX_PLIST_DEPTH + 1), { offset: 0, limit: 1, - }); - } catch (caught) { - error = caught; - } - expect(error).toBeInstanceOf(z.ZodError); - expect(error).not.toBeInstanceOf(RangeError); + }), + ).toThrow(`Plist exceeds ${String(MAX_PLIST_DEPTH)} nesting levels`); }); }); diff --git a/src/domain/apple/keyedArchive.ts b/src/domain/apple/keyedArchive.ts index a4b335956..65bcaa3e5 100644 --- a/src/domain/apple/keyedArchive.ts +++ b/src/domain/apple/keyedArchive.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { jsonValueSchema, type JsonValue } from "../jsonValue.js"; import { localPathStringSchema } from "../localPath.js"; +import { projectPlistValue } from "./plistValue.js"; /** * The selected file is intact but is another kind of file, so another tool @@ -68,8 +69,12 @@ export const keyedArchiveResultSchema = z.strictObject({ const record = ( value: JsonValue | undefined, + wrappedScalars: WeakSet, ): Record | undefined => - value !== null && typeof value === "object" && !Array.isArray(value) + value !== null && + typeof value === "object" && + !Array.isArray(value) && + !wrappedScalars.has(value) ? value : undefined; /** @@ -77,8 +82,11 @@ const record = ( * a numeric value. Any other value means an ordinary dictionary, such as a * `$top` that holds a root encoded under the key `UID`. */ -const uid = (value: JsonValue | undefined): number | undefined => { - const object = record(value); +const uid = ( + value: JsonValue | undefined, + wrappedScalars: WeakSet, +): number | undefined => { + const object = record(value, wrappedScalars); if (object === undefined || Object.keys(object).length !== 1) return undefined; const marker = Object.hasOwn(object, "UID") ? object.UID : object["CF$UID"]; @@ -95,12 +103,18 @@ const archiverMismatch = ( : "its $archiver is not a string"; }; -/** Interpret serialized Foundation references without instantiating archive classes. */ +/** Interpret decoded Foundation values without instantiating archive classes. */ export const projectKeyedArchive = ( value: unknown, selection: { root?: string | undefined; offset: number; limit: number }, ) => { - const archive = record(jsonValueSchema.parse(value)); + // Classify generated scalar wrappers by their producer identity, never by + // a caller-controlled $plist_type field in an ordinary dictionary. + const wrappedScalars = new WeakSet(); + const projected = projectPlistValue(value, (scalar) => + wrappedScalars.add(scalar), + ); + const archive = record(projected.value, wrappedScalars); if (archive?.$archiver !== "NSKeyedArchiver") throw new KeyedArchiveKindError( `Selected property list is not an NSKeyedArchiver archive: ${archiverMismatch(archive)}`, @@ -111,7 +125,7 @@ export const projectKeyedArchive = ( const table = archive.$objects; if (table.length > 200_000) throw new RangeError("Archive exceeds 200000 object-table entries"); - const top = record(archive.$top); + const top = record(archive.$top, wrappedScalars); if (top === undefined || Object.keys(top).length === 0) throw new TypeError("Keyed archive has no $top roots"); if (selection.root !== undefined && !Object.hasOwn(top, selection.root)) @@ -140,7 +154,7 @@ export const projectKeyedArchive = ( throw new RangeError( "Archive exceeds 200000 values per object or 128 nesting levels", ); - const target = uid(item.value); + const target = uid(item.value, wrappedScalars); if (target !== undefined) { const index = Number.isSafeInteger(target) && target >= 0 ? target : null; @@ -165,7 +179,7 @@ export const projectKeyedArchive = ( pending.push({ value: child, path: [...item.path, index] }); } } else { - const fields = record(item.value); + const fields = record(item.value, wrappedScalars); if (fields !== undefined) for (const key of Object.keys(fields).sort().reverse()) { const child = fields[key]; @@ -193,13 +207,14 @@ export const projectKeyedArchive = ( ); const objects = table.map((value, id) => { scan(id, [{ value, path: [] }]); - const fields = record(value); - const classUid = uid(fields?.$class); + const fields = record(value, wrappedScalars); + const classUid = uid(fields?.$class, wrappedScalars); const classId = classUid !== undefined && Number.isSafeInteger(classUid) && classUid >= 0 ? classUid : null; - const descriptor = classId === null ? undefined : record(table[classId]); + const descriptor = + classId === null ? undefined : record(table[classId], wrappedScalars); const className = typeof descriptor?.$classname === "string" ? descriptor.$classname : null; const isNil = id === 0 && value === "$null"; @@ -232,6 +247,8 @@ export const projectKeyedArchive = ( const page = objects.slice(selection.offset, end); const ids = new Set(page.map(({ id }) => id)); return { + unknownRealCount: projected.unknownRealCount, + unknownDateCount: projected.unknownDateCount, roots, objects: page, references: references.filter( diff --git a/src/domain/apple/plistValue.test.ts b/src/domain/apple/plistValue.test.ts index 65ce87ba9..dadada73f 100644 --- a/src/domain/apple/plistValue.test.ts +++ b/src/domain/apple/plistValue.test.ts @@ -26,3 +26,63 @@ it("rejects excessive plist nesting without exhausting the stack", () => { } expect(() => projectPlistValue(deep)).toThrow(RangeError); }); + +it("reports only generated scalar wrappers and keeps lookalike dictionaries distinct", () => { + const observed: object[] = []; + const value = projectPlistValue( + [ + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + new Date(0), + new Date(Number.NaN), + Number.POSITIVE_INFINITY, + { $plist_type: "data", base64: "3q2+7w==" }, + ], + (wrapper) => observed.push(wrapper), + ).value; + expect(Array.isArray(value)).toBe(true); + if (!Array.isArray(value)) throw new Error("Expected projected array"); + expect(observed).toEqual(value.slice(0, 4)); + expect(observed[0]).toBe(value[0]); + expect(value[0]).toEqual(value[4]); + expect(observed).not.toContain(value[4]); +}); + +it("rejects accessors, proxies, holes and symbol keys without invoking user code", () => { + let accessed = 0; + const dictionary = Object.defineProperty({}, "value", { + enumerable: true, + get() { + accessed++; + return "unexpected"; + }, + }); + const array = Object.defineProperty([], "0", { + enumerable: true, + get() { + accessed++; + return "unexpected"; + }, + }); + const proxy = new Proxy( + {}, + { + ownKeys() { + accessed++; + return []; + }, + getPrototypeOf() { + accessed++; + return Object.prototype; + }, + }, + ); + for (const value of [ + dictionary, + array, + proxy, + Array(1), + { [Symbol("key")]: "value" }, + ]) + expect(() => projectPlistValue(value)).toThrow(TypeError); + expect(accessed).toBe(0); +}); diff --git a/src/domain/apple/plistValue.ts b/src/domain/apple/plistValue.ts index 3ea0c8733..41138e868 100644 --- a/src/domain/apple/plistValue.ts +++ b/src/domain/apple/plistValue.ts @@ -1,3 +1,4 @@ +import { types } from "node:util"; import { z } from "zod"; import type { JsonValue } from "../jsonValue.js"; @@ -27,38 +28,67 @@ export const unknownDateLimitation = (count: number): string => * `$plist_type` objects; a non-finite real becomes an explicit unknown * because the XML decoder reports NaN and ±infinity alike. */ -export const projectPlistValue = (value: unknown): ProjectedPlistValue => { +export const projectPlistValue = ( + value: unknown, + /** Observe generated wrappers before consumers classify the JSON value tree. */ + onWrappedScalar?: (value: Record) => void, +): ProjectedPlistValue => { let unknownRealCount = 0; let unknownDateCount = 0; + const scalar = (value: Record): JsonValue => { + onWrappedScalar?.(value); + return value; + }; const project = (item: unknown, depth: number): JsonValue => { if (depth > MAX_PLIST_DEPTH) throw new RangeError( `Plist exceeds ${String(MAX_PLIST_DEPTH)} nesting levels`, ); + if (item !== null && typeof item === "object" && types.isProxy(item)) + throw new TypeError("Plist values cannot contain proxies"); if (item instanceof Uint8Array) - return { + return scalar({ $plist_type: "data", base64: Buffer.from(item).toString("base64"), - }; + }); if (item instanceof Date) { - if (!Number.isNaN(item.getTime())) - return { $plist_type: "date", iso8601: item.toISOString() }; + if (!Number.isNaN(Date.prototype.getTime.call(item))) + return scalar({ + $plist_type: "date", + iso8601: Date.prototype.toISOString.call(item), + }); unknownDateCount += 1; - return { $plist_type: "date", iso8601: null }; + return scalar({ $plist_type: "date", iso8601: null }); } if (typeof item === "number" && !Number.isFinite(item)) { unknownRealCount += 1; - return { $plist_type: "real", value: null }; + return scalar({ $plist_type: "real", value: null }); + } + if (Array.isArray(item)) { + const result: JsonValue[] = []; + for (let index = 0; index < item.length; index++) { + const descriptor = Object.getOwnPropertyDescriptor(item, index); + if (descriptor === undefined || !Object.hasOwn(descriptor, "value")) + throw new TypeError( + "Plist arrays require dense data properties without accessors", + ); + result.push(project(descriptor.value, depth + 1)); + } + return result; + } + if (item !== null && typeof item === "object") { + const entries: [string, JsonValue][] = []; + for (const key of Reflect.ownKeys(item)) { + const descriptor = Object.getOwnPropertyDescriptor(item, key); + if (descriptor?.enumerable !== true) continue; + if (typeof key !== "string" || !Object.hasOwn(descriptor, "value")) + throw new TypeError( + "Plist dictionary entries require string keys and data properties", + ); + entries.push([key, project(descriptor.value, depth + 1)]); + } + return Object.fromEntries(entries); } - if (Array.isArray(item)) - return item.map((entry: unknown) => project(entry, depth + 1)); - if (item !== null && typeof item === "object") - return Object.fromEntries( - Object.entries(item).map(([key, entry]) => [ - key, - project(entry, depth + 1), - ]), - ); return z.union([z.string(), z.number(), z.boolean(), z.null()]).parse(item); }; const projected = project(value, 0); diff --git a/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts new file mode 100644 index 000000000..c9921bc44 --- /dev/null +++ b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts @@ -0,0 +1,199 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { build, buildBinary } from "plist"; +import { expect } from "vitest"; + +import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for(["xml", "binary"] as const)( + "distinguishes serialized scalar objects from lookalike dictionaries in a %s keyed archive", + async (format, { cli }) => { + const root = await createTestTempDirectory("rea-keyed-scalar-kinds-"); + const archive = { + $archiver: "NSKeyedArchiver", + $version: 100000, + $top: { root: { CF$UID: 12 }, data: { CF$UID: 1 } }, + $objects: [ + "$null", + Buffer.from("deadbeef", "hex"), + new Date("2001-01-01T00:00:00.000Z"), + 9007199254740992, + "hello", + 42, + 1.5, + Number.POSITIVE_INFINITY, + { $plist_type: "data", base64: "3q2+7w==" }, + { $plist_type: "date", iso8601: "2001-01-01T00:00:00.000Z" }, + { $plist_type: "real", value: 0 }, + [{ CF$UID: 1 }], + { $class: { CF$UID: 13 }, payload: { CF$UID: 1 } }, + { $classname: "ScalarHolder", $classes: ["ScalarHolder", "NSObject"] }, + ], + }; + const bytes = Buffer.from( + format === "binary" ? buildBinary(archive) : build(archive), + ); + const path = join(root, "archive.plist"); + await writeFile(path, bytes); + const digest = createHash("sha256").update(bytes).digest("hex"); + const run = async (arguments_: readonly string[] = []) => { + const output = await cli.run({ + arguments: ["inspect-keyed-archive", path, ...arguments_, "--json"], + environment: { + HOME: root, + USERPROFILE: root, + REA_LOG_LEVEL: "silent", + REA_ANALYSIS_PROVIDER: "auto", + }, + }); + expect(output.exitCode, output.stderr).toBe(0); + return keyedArchiveResultSchema.parse( + parseEvidence(output.json).normalized_result, + ); + }; + const result = await run(); + const { call } = await connectLocalToolsMcp({ + HOME: root, + USERPROFILE: root, + }); + const opened = await call("open_binary", { path }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const mcp = await call("inspect_keyed_archive", {}); + expect(mcp.isError, JSON.stringify(mcp)).not.toBe(true); + expect( + keyedArchiveResultSchema.parse( + parseEvidence(mcp.structuredContent).normalized_result, + ), + ).toEqual(result); + expect(result.archive_sha256).toBe(digest); + expect(result.archive_format).toBe( + format === "binary" ? "binary-plist" : "xml-plist", + ); + expect(result.objects.map(({ kind }) => kind)).toEqual([ + "nil", + "scalar", + "scalar", + "scalar", + "scalar", + "scalar", + "scalar", + "scalar", + "dictionary", + "dictionary", + "dictionary", + "array", + "archived-object", + "class-descriptor", + ]); + for (const [scalar, dictionary] of [ + [1, 8], + [2, 9], + ] as const) { + expect(result.objects[scalar]?.value).toEqual( + result.objects[dictionary]?.value, + ); + expect(result.objects[scalar]?.status).toBe("observed"); + } + expect(result.objects[1]?.value).toEqual({ + $plist_type: "data", + base64: "3q2+7w==", + }); + expect(result.objects[2]?.value).toEqual({ + $plist_type: "date", + iso8601: "2001-01-01T00:00:00.000Z", + }); + expect(result.objects[7]?.value).toEqual({ + $plist_type: "real", + value: null, + }); + expect(result.objects[10]?.value).toEqual({ + $plist_type: "real", + value: 0, + }); + expect(result.objects[12]).toMatchObject({ + class_id: 13, + class_name: "ScalarHolder", + status: "unknown-class", + }); + expect(result.references).toContainEqual( + expect.objectContaining({ + source: 12, + path: ["payload"], + target: 1, + status: "resolved", + }), + ); + expect(result.limitations).toContainEqual( + expect.stringContaining("1 non-finite real value(s)"), + ); + + const page = await run(["--root", "data", "--offset", "1", "--limit", "2"]); + expect(page.objects).toEqual(result.objects.slice(1, 3)); + expect(page).toMatchObject({ + archive_sha256: digest, + total_objects: 14, + offset: 1, + next_offset: 3, + truncated: true, + }); + expect(Object.keys(page.roots)).toEqual(["data"]); + expect(page.references).toEqual([ + expect.objectContaining({ + source: null, + path: ["data"], + target: 1, + status: "resolved", + }), + ]); + expect(await readFile(path)).toEqual(bytes); + }, +); + +cliTest( + "rejects scalar top dictionaries through the public CLI and MCP", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-keyed-scalar-top-"); + const { call } = await connectLocalToolsMcp({ + HOME: root, + USERPROFILE: root, + }); + for (const format of ["xml", "binary"]) { + const archive = { + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null"], + $top: new Uint8Array([0]), + }; + const path = join(root, format + ".plist"); + await writeFile( + path, + Buffer.from(format === "xml" ? build(archive) : buildBinary(archive)), + ); + const output = await cli.run({ + arguments: ["inspect-keyed-archive", path, "--json"], + environment: { HOME: root, USERPROFILE: root }, + }); + expect(output.exitCode).toBe(1); + expect(output.json).toMatchObject({ + code: "artifact_operation_failed", + details: { + reason: "format", + detail: expect.stringContaining("Keyed archive has no $top roots"), + }, + }); + const opened = await call("open_binary", { path }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const reply = await call("inspect_keyed_archive", {}); + expect(reply.isError).toBe(true); + expect(JSON.stringify(reply)).toContain( + "Keyed archive has no $top roots", + ); + } + }, +); diff --git a/tests/boundary/filesystem/keyedArchiveDateRange.test.ts b/tests/boundary/filesystem/keyedArchiveDateRange.test.ts index 9a45bbaa6..235b402d6 100644 --- a/tests/boundary/filesystem/keyedArchiveDateRange.test.ts +++ b/tests/boundary/filesystem/keyedArchiveDateRange.test.ts @@ -33,11 +33,14 @@ cliTest( const result = keyedArchiveResultSchema.parse( parseEvidence(output.json).normalized_result, ); - expect(result.objects.map(({ id, value }) => ({ id, value }))).toEqual([ - { id: 0, value: "$null" }, - { id: 1, value: { $plist_type: "date", iso8601: null } }, + expect( + result.objects.map(({ id, kind, value }) => ({ id, kind, value })), + ).toEqual([ + { id: 0, kind: "nil", value: "$null" }, + { id: 1, kind: "scalar", value: { $plist_type: "date", iso8601: null } }, { id: 2, + kind: "scalar", value: { $plist_type: "date", iso8601: "2020-01-01T00:00:00.000Z" }, }, ]); diff --git a/tests/boundary/filesystem/keyedArchiveScalarStructure.test.ts b/tests/boundary/filesystem/keyedArchiveScalarStructure.test.ts new file mode 100644 index 000000000..f7e6c5adc --- /dev/null +++ b/tests/boundary/filesystem/keyedArchiveScalarStructure.test.ts @@ -0,0 +1,70 @@ +import { build, buildBinary } from "plist"; +import { expect, it } from "vitest"; + +import { decodeKeyedArchiveBytes } from "../../../src/artifacts/apple/KeyedArchiveReader.js"; + +it.each([ + { name: "data", value: new Uint8Array([0]) }, + { name: "date", value: new Date(0) }, + { name: "real", value: Number.POSITIVE_INFINITY }, +])( + "rejects a $name scalar used as the top-level root dictionary in either encoding", + ({ value }) => { + const archive = { + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null"], + $top: value, + }; + for (const encode of [build, buildBinary]) { + const bytes = Buffer.from(encode(archive)); + expect(() => + decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 10 }), + ).toThrow("Keyed archive has no $top roots"); + } + }, +); + +it("keeps literal wrapper-shaped dictionaries as real root maps", () => { + const top = { $plist_type: "data", base64: "AA==" }; + const archive = { + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null"], + $top: top, + }; + for (const encode of [build, buildBinary]) { + const graph = decodeKeyedArchiveBytes(Buffer.from(encode(archive)), { + offset: 0, + limit: 10, + }); + expect(graph.roots).toEqual(top); + expect(graph.references).toEqual([]); + } +}); + +it("retains own binary dictionary facts when a prototype key changes the decoder prototype", () => { + const object = Object.fromEntries([ + ["__proto__", { inherited: "not an own fact" }], + ["payload", "retained"], + ]); + const graph = decodeKeyedArchiveBytes( + Buffer.from( + buildBinary({ + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null", object], + $top: { root: { UID: 1 } }, + }), + ), + { offset: 0, limit: 10 }, + ); + expect(graph.objects[1]).toMatchObject({ + kind: "dictionary", + value: { payload: "retained" }, + }); + expect(graph.objects[1]?.value).toEqual({ payload: "retained" }); + expect(graph.limitations).toContainEqual( + expect.stringContaining("__proto__"), + ); +}); diff --git a/tests/fixtures/golden/README.md b/tests/fixtures/golden/README.md index f75e79807..6bd799dff 100644 --- a/tests/fixtures/golden/README.md +++ b/tests/fixtures/golden/README.md @@ -19,6 +19,12 @@ its nil conditional reference and the payload `00 01 02 ff`. The reader reports serialized classes as unknown and does not instantiate them. The synthetic malformed-reference tests remain separate. +Object 3 is a plist `` value in the captured XML, produced by the Swift +fixture's `Data([0, 1, 2, 255])`. Its reviewed graph kind is `scalar`; the JSON +`$plist_type` wrapper does not make it a serialized dictionary. This expectation +correction updates only the graph digest in `provenance.json`, without +recapturing or changing the original XML or its producer metadata. + To recapture the input on an approved macOS host: ```sh diff --git a/tests/fixtures/golden/keyed-archive/graph.json b/tests/fixtures/golden/keyed-archive/graph.json index b3336cc83..00543bd29 100644 --- a/tests/fixtures/golden/keyed-archive/graph.json +++ b/tests/fixtures/golden/keyed-archive/graph.json @@ -57,7 +57,7 @@ }, { "id": 3, - "kind": "dictionary", + "kind": "scalar", "class_id": null, "class_name": null, "status": "observed", diff --git a/tests/fixtures/golden/provenance.json b/tests/fixtures/golden/provenance.json index 6ba7a0d59..8a2b9b5d2 100644 --- a/tests/fixtures/golden/provenance.json +++ b/tests/fixtures/golden/provenance.json @@ -16,7 +16,7 @@ "tests/conformance/native/Assets.xcassets/Contents.json": "67328f1f20917d61b0904d1eeadd3a9af5ebdfeb20bdd8c6f585f97f7192d873", "tests/conformance/native/Assets.xcassets/FixtureColor.colorset/Contents.json": "26c6400ce2082ddc155175e80130daddded1a863bd68fe441bee413cc2786731", "tests/fixtures/golden/keyed-archive/foundation.xml": "93c2bf33ad960be6fba6d45c28462c4bda73ff371fe16725d087087f044479ec", - "tests/fixtures/golden/keyed-archive/graph.json": "eaadd9fca83052e81abd0dac797d2423b0bb750bea386f790586273eb3a3172e", + "tests/fixtures/golden/keyed-archive/graph.json": "d761330d5a3ca89a51eaaacaa5d43b92cda2ee544931b8b67849b2e937cf848a", "tests/fixtures/golden/assetutil.json": "ff50e24963447584e3398dddf4b0426274ffd2ea2d3ae6fac8295f449f91feb3", "tests/fixtures/golden/mcp-initialize.json": "8f590db5143106aa43dfa96123178484d98268874eff7fabbd708c240c5975bc" }