Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-core/src/paths.ts
Snapshot at 34e3175
1import { HttpError } from "./http";
2
3/** Literal repository paths; a trailing slash and double star denotes a subtree. */
4export function claimPath(value: unknown): value is string {
5 if (typeof value !== "string") return false;
6 return filePath(value.endsWith("/**") ? value.slice(0, -3) : value);
7}
8export function filePath(value: unknown): value is string {
9 return (
10 typeof value === "string" &&
11 value.length > 0 &&
12 value.length <= 512 &&
13 // Control bytes and Windows separators cannot denote portable repository paths.
14 ![...value].some(
15 (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
16 ) &&
17 !/[\\*?[\]{}:]/.test(value) &&
18 value
19 .split("/")
20 .every(
21 (part) =>
22 part !== "" &&
23 part !== "." &&
24 part !== ".." &&
25 part.toLowerCase() !== ".git" &&
26 !part.endsWith(".") &&
27 !part.endsWith(" "),
28 )
29 );
30}
31function root(path: string): string {
32 return path.replace(/\/\*\*$/, "");
33}
34/** A plain path also claims descendants; sibling prefixes do not overlap. */
35export function overlaps(a: string, b: string): boolean {
36 a = root(a);
37 b = root(b);
38 return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
39}
40export function covers(claim: string, path: string): boolean {
41 claim = root(claim);
42 return path === claim || path.startsWith(claim + "/");
43}
44export function requireCovered(paths: string[], files: string[]): void {
45 if (files.some((file) => !paths.some((path) => covers(path, file))))
46 throw new HttpError(403, "Files must be inside the packet's claimed paths.");
47}
48