diff --git a/src/native/NativePlistSource.ts b/src/native/NativePlistSource.ts index 96bbf5fa63..f7d42d304e 100644 --- a/src/native/NativePlistSource.ts +++ b/src/native/NativePlistSource.ts @@ -260,6 +260,14 @@ export const nativePlistFailure = ( cause, capturedOutput, }); + if (code === "ELOOP" || code === "ENAMETOOLONG") + return unreadablePlist( + selection, + code === "ELOOP" + ? "symbolic-link traversal looped or exceeded the host limit (ELOOP)" + : "the path exceeds the host path or component length limit (ENAMETOOLONG)", + { cause, capturedOutput }, + ); if (code === "EACCES" || code === "EPERM") return new BinaryTargetError( path, diff --git a/tests/boundary/providers/native/nativePlistSelection.test.ts b/tests/boundary/providers/native/nativePlistSelection.test.ts index 892a0f8d50..56022d0358 100644 --- a/tests/boundary/providers/native/nativePlistSelection.test.ts +++ b/tests/boundary/providers/native/nativePlistSelection.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { chmod, mkdir, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; @@ -89,6 +89,62 @@ it.each(["missing", "directory"] as const)( }, ); +it.skipIf(process.platform === "win32")( + "reports selected and default plist path-resolution failures as selection failures", + async () => { + const root = await createTestTempDirectory("rea-plist-origin-resolve-"); + const loop = join(root, "Loop.plist"); + await symlink("Loop.plist", loop); + const long = join(root, `${"x".repeat(300)}.plist`); + const program = join(root, "program"); + await writeFile(program, "fixture program"); + await mkdir(join(root, "Contents")); + await symlink("Info.plist", join(root, "Contents/Info.plist")); + const failure = async ( + target: Parameters[0], + ) => { + const client = new NativeMacOSProvider( + {}, + new NativeFixtureRunner(), + "darwin", + ).createClient(target); + onTestFinished(async () => { + await client.close(); + }); + const result = await client.execute("inspect_plist", {}); + if (result.ok) throw new Error("Expected plist refusal"); + return projectAnalysisError(result.error); + }; + for (const [path, code] of [ + [loop, "ELOOP"], + [long, "ENAMETOOLONG"], + ] as const) + expect( + await failure({ + kind: "artifact", + format: "plist", + path, + sha256: "0".repeat(64), + }), + ).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: ["path"], + reason: "invalid_value", + message: expect.stringContaining(`(${code})`), + }, + ], + }, + }); + const fallback = await failure(nativeMachoTarget(program)); + expect(fallback).toMatchObject({ code: "capability_unavailable" }); + expect(fallback.message).toContain("no readable default plist"); + expect(fallback.message).toContain("(ELOOP)"); + }, +); + it.skipIf(process.getuid?.() === 0 || process.platform === "win32")( "keeps permission failure distinct from selection and format", async () => {