mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 03:45:39 +00:00
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:
1 parent
4b0ff68e2e
commit
e23cb3b240
4 files changed
+85
-2
No files matched your search
@@ -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 (
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user