Repository navigation
Expand file tree
/
Copy pathroots.ts
More file actions
48 lines (40 loc) · 1.47 KB
/
Copy pathroots.ts
File metadata and controls
48 lines (40 loc) · 1.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { homedir } from "node:os";
import { isAbsolute, relative, resolve, sep } from "node:path";
export class AccessDeniedError extends Error {
constructor(message: string) {
super(message);
this.name = "AccessDeniedError";
}
}
export function expandHomePath(path: string): string {
if (path === "~") return homedir();
if (path.startsWith("~/") || path.startsWith("~\\")) {
return resolve(homedir(), path.slice(2));
}
return path;
}
export function isPathInsideRoot(path: string, root: string): boolean {
const resolvedPath = resolve(expandHomePath(path));
const resolvedRoot = resolve(expandHomePath(root));
const relationship = relative(resolvedRoot, resolvedPath);
return (
relationship === "" ||
(!isAbsolute(relationship) &&
!relationship.startsWith("..") &&
relationship !== ".." &&
!relationship.includes(`..${sep}`))
);
}
export function assertAllowedPath(path: string, allowedRoots: string[]): string {
const resolvedPath = resolve(expandHomePath(path));
if (allowedRoots.some((root) => isPathInsideRoot(resolvedPath, root))) {
return resolvedPath;
}
throw new AccessDeniedError(
`Path is outside allowed roots: ${path}\nAllowed roots:\n${allowedRoots.map((root) => `- ${root}`).join("\n")}`,
);
}
export function resolveAllowedPath(inputPath: string, cwd: string, allowedRoots: string[]): string {
const absolutePath = resolve(cwd, inputPath);
return assertAllowedPath(absolutePath, allowedRoots);
}