From 17f2bc7b0d1682eb22ee1d38fbe44133bb066046 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:46:43 +0800 Subject: [PATCH] fix(apple): discover cache companions from canonical main file --- src/artifacts/apple/DyldSharedCacheReader.ts | 43 ++++++++++++------- .../filesystem/dyldSharedCache.test.ts | 23 +++++++++- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/artifacts/apple/DyldSharedCacheReader.ts b/src/artifacts/apple/DyldSharedCacheReader.ts index 94412898e..cce259ad0 100644 --- a/src/artifacts/apple/DyldSharedCacheReader.ts +++ b/src/artifacts/apple/DyldSharedCacheReader.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { open, type FileHandle } from "node:fs/promises"; +import { open, realpath, type FileHandle } from "node:fs/promises"; import { applePlatform } from "../../domain/apple/applePlatforms.js"; import type { MachoImageFacts } from "../../domain/apple/dylibResolution.js"; @@ -70,18 +70,18 @@ class CacheFile { readonly suffix: string, readonly handle: FileHandle, readonly size: number, - /** Device, inode, size, and mtime of the file as opened. */ + /** Device, inode, size, and nanosecond change times of the file as opened. */ readonly identity: string, ) {} static async open(path: string, suffix: string): Promise { const handle = await open(path, "r"); try { - const metadata = await handle.stat(); + const metadata = await handle.stat({ bigint: true }); return new CacheFile( suffix, handle, - metadata.size, + safeNumber(metadata.size, "file size"), CacheFile.identify(metadata), ); } catch (cause: unknown) { @@ -91,19 +91,27 @@ class CacheFile { } static identify(metadata: { - readonly dev: number; - readonly ino: number; - readonly size: number; - readonly mtimeMs: number; + readonly dev: bigint; + readonly ino: bigint; + readonly size: bigint; + readonly mtimeNs: bigint; + readonly ctimeNs: bigint; }): string { - return [metadata.dev, metadata.ino, metadata.size, metadata.mtimeMs].join( - ":", - ); + return [ + metadata.dev, + metadata.ino, + metadata.size, + metadata.mtimeNs, + metadata.ctimeNs, + ].join(":"); } /** Whether the open file still matches the identity recorded at open. */ async unchanged(): Promise { - return CacheFile.identify(await this.handle.stat()) === this.identity; + return ( + CacheFile.identify(await this.handle.stat({ bigint: true })) === + this.identity + ); } async read(offset: number, length: number): Promise { @@ -296,7 +304,10 @@ export class DyldSharedCache { path: string, signal?: AbortSignal, ): Promise { - const main = await CacheFile.open(path, ""); + // Inspection and tracing share this boundary: companion discovery follows + // the actual main cache file, while adapters keep the selected path spelling. + const canonicalPath = await realpath(path); + const main = await CacheFile.open(canonicalPath, ""); const files = [main]; try { const parsed = await readHeader(main); @@ -305,9 +316,9 @@ export class DyldSharedCache { // With named suffixes, dyld strips a development main file's extension // first: dyld_shared_cache_arm64e.development -> dyld_shared_cache_arm64e.01.development. const base = - named && path.endsWith(DEVELOPMENT_EXTENSION) - ? path.slice(0, -DEVELOPMENT_EXTENSION.length) - : path; + named && canonicalPath.endsWith(DEVELOPMENT_EXTENSION) + ? canonicalPath.slice(0, -DEVELOPMENT_EXTENSION.length) + : canonicalPath; const statuses: DyldCacheSubcache[] = []; const parsedFiles = [{ file: main, parsed }]; for (const entry of subcaches) { diff --git a/tests/boundary/filesystem/dyldSharedCache.test.ts b/tests/boundary/filesystem/dyldSharedCache.test.ts index 715ff271f..8a17e77bc 100644 --- a/tests/boundary/filesystem/dyldSharedCache.test.ts +++ b/tests/boundary/filesystem/dyldSharedCache.test.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { rm, writeFile } from "node:fs/promises"; +import { rm, symlink, writeFile } from "node:fs/promises"; import { dirname, join, relative } from "node:path"; import { describe, expect, it } from "vitest"; @@ -218,3 +218,24 @@ it("retains a relative caller-selected cache path in trace and inspection metada if (!inspected.ok) throw inspected.error; expect(inspected.value.parameters).toMatchObject({ cache_path: selected }); }); + +it("discovers companions beside a symlink target while preserving selected metadata", async () => { + const path = await writeCache( + dyldCacheFixture(IMAGES), + "dyld_shared_cache_arm64e.development", + ); + const aliases = await createTestTempDirectory("rea-cache-alias-"); + const alias = join(aliases, "selected-cache"); + await symlink(path, alias); + const result = await inspectDyldSharedCache({ + cache_path: alias, + images: ["/usr/lib/swift/libswiftCore.dylib"], + }); + if (!result.ok) throw result.error; + expect(result.value).toMatchObject({ + cache_path: alias, + subcaches: [{ status: "present", suffix: ".01" }], + inspected_images: [{ status: "parsed", file: ".01" }], + coverage: { status: "complete", unreadable_subcaches: [] }, + }); +});