forked from EduCraft/curriculum-project-hub
61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
import {
|
|
WorkspaceFileBoundaryError,
|
|
readWorkspaceFileNoFollow,
|
|
} from "../security/workspaceFiles.js";
|
|
|
|
export interface DeliverableFile {
|
|
readonly path: string;
|
|
readonly name: string;
|
|
readonly data: Buffer;
|
|
}
|
|
|
|
const DELIVERABLE_CPH_DIRECTORY = "inbox";
|
|
|
|
export async function resolveDeliverableFile(
|
|
requestedPath: string,
|
|
workspaceRoot: string,
|
|
workspaceDir: string,
|
|
maxBytes?: number,
|
|
): Promise<DeliverableFile | null> {
|
|
const token = requestedPath.trim();
|
|
if (token === "") return null;
|
|
|
|
for (const candidate of candidatePaths(token)) {
|
|
assertNotPlatformRuntimePath(candidate, workspaceDir);
|
|
try {
|
|
return await readWorkspaceFileNoFollow(workspaceRoot, workspaceDir, candidate, maxBytes);
|
|
} catch (error) {
|
|
if (!(error instanceof WorkspaceFileBoundaryError) || error.reason !== "not_found") {
|
|
throw error;
|
|
}
|
|
// Try the explicit workspace/build fallback for a bare filename. No
|
|
// candidate outside the project workspace is ever considered.
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function assertNotPlatformRuntimePath(candidate: string, workspaceDir: string): void {
|
|
const workspace = resolve(workspaceDir);
|
|
const target = isAbsolute(candidate) ? resolve(candidate) : resolve(workspace, candidate);
|
|
const rel = relative(workspace, target);
|
|
const components = rel.split(sep);
|
|
if (components[0] === ".cph" && components[1] !== DELIVERABLE_CPH_DIRECTORY) {
|
|
throw new WorkspaceFileBoundaryError(
|
|
`platform runtime files cannot be delivered: ${candidate}`,
|
|
candidate,
|
|
"boundary",
|
|
);
|
|
}
|
|
}
|
|
|
|
function candidatePaths(token: string): string[] {
|
|
if (isAbsolute(token)) return [token];
|
|
const candidates = [token];
|
|
if (!token.includes("/") && !token.includes("\\")) {
|
|
candidates.push(join("build", token));
|
|
}
|
|
return [...new Set(candidates)];
|
|
}
|