feat: release 2.7.0-rc2
This commit is contained in:
+65
-23
@@ -9,14 +9,7 @@ import {
|
||||
rm,
|
||||
symlink,
|
||||
} from "node:fs/promises";
|
||||
import {
|
||||
basename,
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
relative,
|
||||
resolve,
|
||||
} from "node:path";
|
||||
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
||||
import {
|
||||
BUILD_PROTOCOL_VERSION,
|
||||
assertSha256Digest,
|
||||
@@ -129,16 +122,50 @@ export function parseWorkspaceManifest(data: Uint8Array): WorkspaceManifest {
|
||||
return manifest as WorkspaceManifest;
|
||||
}
|
||||
|
||||
function safeSymlinkTarget(filePath: string, target: string): boolean {
|
||||
if (
|
||||
!target ||
|
||||
isAbsolute(target) ||
|
||||
target.includes("\\") ||
|
||||
target.includes("\0")
|
||||
)
|
||||
return false;
|
||||
const resolved = resolve("/workspace", dirname(filePath), target);
|
||||
return resolved === "/workspace" || resolved.startsWith("/workspace/");
|
||||
function validateSymlinkTargets(targets: Map<string, string>): void {
|
||||
for (const [path, target] of targets) {
|
||||
if (
|
||||
!target ||
|
||||
isAbsolute(target) ||
|
||||
target.includes("\\") ||
|
||||
target.includes("\0")
|
||||
) {
|
||||
throw new Error(`Unsafe symlink target for ${path}`);
|
||||
}
|
||||
|
||||
// Resolve components in filesystem order: a symlink is expanded before
|
||||
// processing the following `..`, unlike node:path.resolve's lexical result.
|
||||
const pending: (string | { end: string })[] = [
|
||||
...dirname(path).split("/"),
|
||||
...target.split("/"),
|
||||
];
|
||||
const resolved: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
while (pending.length > 0) {
|
||||
const component = pending.shift()!;
|
||||
if (typeof component !== "string") {
|
||||
visited.delete(component.end);
|
||||
continue;
|
||||
}
|
||||
if (!component || component === ".") continue;
|
||||
if (component === "..") {
|
||||
if (resolved.length === 0)
|
||||
throw new Error(`Unsafe symlink target for ${path}`);
|
||||
resolved.pop();
|
||||
continue;
|
||||
}
|
||||
const candidate = [...resolved, component].join("/");
|
||||
const link = targets.get(candidate);
|
||||
if (link === undefined) {
|
||||
resolved.push(component);
|
||||
continue;
|
||||
}
|
||||
if (visited.has(candidate))
|
||||
throw new Error(`Unsafe symlink cycle for ${path}`);
|
||||
visited.add(candidate);
|
||||
pending.unshift(...link.split("/"), { end: candidate });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function materializeWorkspace(
|
||||
@@ -171,6 +198,21 @@ export async function materializeWorkspace(
|
||||
const manifestData = await read(manifestDigest);
|
||||
if (timing) timing.manifestBytes = manifestData.byteLength;
|
||||
const manifest = parseWorkspaceManifest(manifestData);
|
||||
const symlinkData = new Map<string, Uint8Array>();
|
||||
const symlinkTargets = new Map<string, string>();
|
||||
await mapConcurrent(
|
||||
manifest.files.filter((file) => file.type === "symlink"),
|
||||
async (file) => {
|
||||
const data = await read(file.digest);
|
||||
if (data.byteLength !== file.size) {
|
||||
throw new Error(`Workspace blob size mismatch for ${file.path}`);
|
||||
}
|
||||
const link = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
||||
symlinkData.set(file.path, data);
|
||||
symlinkTargets.set(file.path, link);
|
||||
},
|
||||
);
|
||||
validateSymlinkTargets(symlinkTargets);
|
||||
if (timing) {
|
||||
timing.fileCount = manifest.files.length;
|
||||
timing.fileBytes = manifest.files.reduce(
|
||||
@@ -199,15 +241,15 @@ export async function materializeWorkspace(
|
||||
await write(() =>
|
||||
mkdir(dirname(target), { recursive: true, mode: 0o755 }),
|
||||
);
|
||||
const data = await read(file.digest);
|
||||
const data =
|
||||
file.type === "symlink"
|
||||
? symlinkData.get(file.path)!
|
||||
: await read(file.digest);
|
||||
if (data.byteLength !== file.size) {
|
||||
throw new Error(`Workspace blob size mismatch for ${file.path}`);
|
||||
}
|
||||
if (file.type === "symlink") {
|
||||
const link = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
||||
if (!safeSymlinkTarget(file.path, link))
|
||||
throw new Error(`Unsafe symlink target for ${file.path}`);
|
||||
await write(() => symlink(link, target));
|
||||
await write(() => symlink(symlinkTargets.get(file.path)!, target));
|
||||
} else {
|
||||
const handle = await write(() =>
|
||||
open(
|
||||
|
||||
Reference in New Issue
Block a user