fix(browser): preserve script exports with filesystem-unrepresentable paths (#7526)

fix(browser): isolate unrepresentable script export paths

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-29 02:13:54 +08:00
1 parent 4b0ff68e2e
commit e23cb3b240
4 files changed
+85 -2

No files matched your search

+15
View File
@@ -47,6 +47,21 @@ export class DestinationCaseCollisionFailure extends ArtifactReaderFailure {
}
}
/** The destination rejected a name before a file or directory was acquired. */
export class DestinationPathUnrepresentableFailure extends ArtifactReaderFailure {
constructor(
readonly requestedPath: string,
options: ErrorOptions,
) {
super(
"path",
`Destination filesystem cannot represent ${pathDiagnostic(requestedPath)}.`,
options,
);
this.name = "DestinationPathUnrepresentableFailure";
}
}
/** Normalize slash-separated logical names; backslashes are literal components. */
export const normalizeArtifactPath = (input: string): string => {
if (
+10
View File
@@ -18,6 +18,7 @@ import { streamChunkToBuffer } from "./StreamBytes.js";
import {
ArtifactPathRegistry,
DestinationPathUnrepresentableFailure,
destinationCaseCollision,
normalizeArtifactPath,
} from "./ArtifactPaths.js";
@@ -269,6 +270,8 @@ export class SafeOutputTree {
constants.O_NOFOLLOW,
0o600,
).catch(async (cause: unknown) => {
if (hasUnrepresentableName(cause))
throw new DestinationPathUnrepresentableFailure(path, { cause });
if (isAlreadyExists(cause))
await throwIfDestinationCaseCollision(parent.path, path, cause);
throw new ArtifactReaderFailure(
@@ -571,6 +574,10 @@ export class SafeOutputTree {
const creation = await mkdir(current, { mode: 0o700 }).then(
() => ({ kind: "created" }) as const,
(cause: unknown) => {
if (hasUnrepresentableName(cause))
throw new DestinationPathUnrepresentableFailure(logicalPath, {
cause,
});
if (!isAlreadyExists(cause)) throw cause;
return { kind: "existing", cause } as const;
},
@@ -777,3 +784,6 @@ const throwIfDestinationCaseCollision = async (
);
if (failure !== undefined) throw failure;
};
const hasUnrepresentableName = (cause: unknown): boolean =>
cause instanceof Error && "code" in cause && cause.code === "ENAMETOOLONG";
+8 -2
View File
@@ -4,7 +4,10 @@ import { Readable } from "node:stream";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { DestinationCaseCollisionFailure } from "../../artifacts/ArtifactPaths.js";
import {
DestinationCaseCollisionFailure,
DestinationPathUnrepresentableFailure,
} from "../../artifacts/ArtifactPaths.js";
import { SafeOutputTree } from "../../artifacts/SafeOutputTree.js";
import { outputDirectoryAdmissionRefusal } from "../../artifacts/SafeOutputTreeAdmission.js";
import { SafeOutputTreeCreationFailure } from "../../artifacts/SafeOutputTreeCreationFailure.js";
@@ -209,7 +212,10 @@ const publishPlannedScript = async (
return record;
} catch (cause: unknown) {
if (
!(cause instanceof DestinationCaseCollisionFailure) ||
!(
cause instanceof DestinationCaseCollisionFailure ||
cause instanceof DestinationPathUnrepresentableFailure
) ||
record.content.layout !== "url-path"
)
throw cause;
@@ -1453,3 +1453,55 @@ it("isolates an observed exclusive-create alias using typed facts when diagnosti
write.mockRestore();
}
});
it.each(["basename", "directory"])(
"isolates a destination-unrepresentable %s without losing peer scripts",
async (kind) => {
const long = "b".repeat(kind === "basename" ? 253 : 256);
const scripts = [
{
url: "https://fixture.test/regular.js",
bytes: Buffer.from("export const regular = 1;"),
},
{
url: `https://fixture.test/${"c".repeat(252)}.js`,
bytes: Buffer.from("export const boundary = 1;"),
},
{
url: `https://fixture.test/${kind === "basename" ? `${long}.js` : `${long}/main.js`}`,
bytes: Buffer.from("export const long = 2;"),
},
];
const { input, resources } = await setup(scriptScenarioFixture(scripts));
const exported = await exportWebScripts(input, resources);
if (!exported.ok) throw exported.error;
const result = webScriptExportResultSchema.parse(
exported.value.normalized_result,
);
expect(result.scripts).toHaveLength(3);
expect(result.scripts[0]?.content).toMatchObject({ layout: "url-path" });
expect(result.scripts[1]?.content).toMatchObject({ layout: "url-path" });
expect(result.scripts[2]?.content).toMatchObject({
layout: "isolated",
layout_reason: expect.stringContaining("cannot represent"),
});
for (const [index, script] of result.scripts.entries()) {
if (script.content.state !== "exported" || result.analysis_input === null)
throw new Error("Missing exported source");
expect(script.url).toBe(scripts[index]!.url);
expect(
(
await readFile(
join(
result.analysis_input.input_path,
script.content.relative_path,
),
)
).equals(scripts[index]!.bytes),
).toBe(true);
}
expect(
JSON.parse(await readFile(result.manifest.path, "utf8")).scripts,
).toEqual(result.scripts);
},
);