Skip to content

Commit 428a541

Browse files
committed
fix(init): bound associated domain reads
1 parent 129de6b commit 428a541

3 files changed

Lines changed: 82 additions & 27 deletions

File tree

packages/cli-core/src/commands/init/ios/associated-domain.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,38 @@ struct MyApp: App {
606606
expect(await treeDigest(linked)).toEqual(before);
607607
});
608608

609+
test("blocks an oversized entitlements file without changing it", async () => {
610+
const root = await directFixture();
611+
const path = join(root, "MyApp", "MyApp.entitlements");
612+
const oversized = Buffer.alloc(1_000_001, 0x20);
613+
await writeFile(path, oversized);
614+
615+
const plan = await planIOSAssociatedDomain(planOptions(root));
616+
const result = await applyIOSAssociatedDomain(plan);
617+
618+
expect(plan.status).toBe("blocked");
619+
expect(plan.blockers[0]?.code).toBe("unsupported-entitlements");
620+
expect(result.status).toBe("blocked");
621+
expect(await readFile(path)).toEqual(oversized);
622+
});
623+
624+
test("returns stale without touching an entitlements file that grows beyond the limit", async () => {
625+
const root = await directFixture();
626+
const path = join(root, "MyApp", "MyApp.entitlements");
627+
await removeAssociatedDomains(root);
628+
const plan = await planIOSAssociatedDomain(planOptions(root));
629+
const oversized = Buffer.alloc(1_000_001, 0x20);
630+
await writeFile(path, oversized);
631+
632+
const prepared = await prepareIOSAssociatedDomainMutation(plan);
633+
const result = await applyIOSAssociatedDomain(plan);
634+
635+
expect(plan.status).toBe("ready");
636+
expect(prepared.status).toBe("stale");
637+
expect(result.status).toBe("stale");
638+
expect(await readFile(path)).toEqual(oversized);
639+
});
640+
609641
test("blocks an entity-encoded Associated Domains key without rewriting it", async () => {
610642
const root = await directFixture();
611643
const path = join(root, "MyApp", "MyApp.entitlements");

packages/cli-core/src/commands/init/ios/associated-domain.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { lstat, readFile, realpath } from "node:fs/promises";
22
import { dirname, resolve } from "node:path";
33
import { parse as parsePbxProject } from "@bacons/xcode/json";
44
import { decodePublishableKey } from "../../../lib/fapi.ts";
5+
import { readBoundedRegularFile } from "./bounded-file.ts";
56
import { inspectTargetBuildConfigurations } from "./build-settings.ts";
67
import {
78
discoverLocalIOSProjects,
@@ -249,20 +250,46 @@ async function inspectEntitlementsFile(
249250
};
250251
}
251252

252-
try {
253-
const info = await lstat(absolutePath);
254-
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_ENTITLEMENTS_BYTES) {
255-
return {
256-
blocker: blocker(
257-
"unsupported-entitlements",
258-
`${relativeIOSPath(
259-
root,
260-
absolutePath,
261-
)} must be a regular, non-symlink XML plist no larger than 1 MB.`,
262-
),
263-
};
253+
const file = await readBoundedRegularFile(absolutePath, MAX_ENTITLEMENTS_BYTES);
254+
if (file.status === "not-regular" || file.status === "too-large") {
255+
return {
256+
blocker: blocker(
257+
"unsupported-entitlements",
258+
`${relativeIOSPath(
259+
root,
260+
absolutePath,
261+
)} must be a regular, non-symlink XML plist no larger than 1 MB.`,
262+
),
263+
};
264+
}
265+
if (file.status !== "ok") {
266+
try {
267+
const info = await lstat(absolutePath);
268+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_ENTITLEMENTS_BYTES) {
269+
return {
270+
blocker: blocker(
271+
"unsupported-entitlements",
272+
`${relativeIOSPath(
273+
root,
274+
absolutePath,
275+
)} must be a regular, non-symlink XML plist no larger than 1 MB.`,
276+
),
277+
};
278+
}
279+
} catch {
280+
// Preserve the unreadable classification below when the current path
281+
// cannot explain the bounded reader's failure.
264282
}
265-
const bytes = new Uint8Array(await readFile(absolutePath));
283+
return {
284+
blocker: blocker(
285+
"unreadable-entitlements",
286+
`${relativeIOSPath(root, absolutePath)} could not be read as a UTF-8 XML plist dictionary.`,
287+
),
288+
};
289+
}
290+
291+
try {
292+
const bytes = file.bytes;
266293
if (new TextDecoder().decode(bytes.slice(0, 8)).startsWith("bplist")) {
267294
return {
268295
blocker: blocker(
@@ -330,7 +357,7 @@ async function inspectEntitlementsFile(
330357
relativePath: relativeIOSPath(root, absolutePath),
331358
bytes,
332359
hash: hashIOSFileBytes(bytes),
333-
mode: info.mode & 0o7777,
360+
mode: file.mode,
334361
source,
335362
bom,
336363
domains,
@@ -866,17 +893,9 @@ export async function prepareIOSAssociatedDomainMutation(
866893
}
867894
continue;
868895
}
869-
try {
870-
if (!plannedFile.expectedHash) return { status: "blocked", plan };
871-
const info = await lstat(absolutePath);
872-
if (
873-
!info.isFile() ||
874-
info.isSymbolicLink() ||
875-
hashIOSFileBytes(await readFile(absolutePath)) !== plannedFile.expectedHash
876-
) {
877-
return { status: "stale", plan };
878-
}
879-
} catch {
896+
if (!plannedFile.expectedHash) return { status: "blocked", plan };
897+
const current = await readBoundedRegularFile(absolutePath, MAX_ENTITLEMENTS_BYTES);
898+
if (current.status !== "ok" || hashIOSFileBytes(current.bytes) !== plannedFile.expectedHash) {
880899
return { status: "stale", plan };
881900
}
882901
}

packages/cli-core/src/commands/init/ios/bounded-file.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { open } from "node:fs/promises";
44
const READ_CHUNK_BYTES = 64 * 1024;
55

66
export type BoundedRegularFileReadResult =
7-
| { status: "ok"; bytes: Uint8Array }
7+
| { status: "ok"; bytes: Uint8Array; mode: number }
88
| { status: "missing" | "not-regular" | "too-large" | "unreadable" };
99

1010
function missingPath(error: unknown): boolean {
@@ -49,7 +49,11 @@ export async function readBoundedRegularFile(
4949
chunks.push(chunk.subarray(0, bytesRead));
5050
}
5151

52-
return { status: "ok", bytes: Buffer.concat(chunks, totalBytes) };
52+
return {
53+
status: "ok",
54+
bytes: Buffer.concat(chunks, totalBytes),
55+
mode: info.mode & 0o7777,
56+
};
5357
} catch {
5458
return { status: "unreadable" };
5559
} finally {

0 commit comments

Comments
 (0)