Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-core/src/paths.ts

Snapshot at 34e3175

Containing directory

1,642 bytes · typescriptView raw
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