Files
zerobyte/app/utils/__tests__/path.test.ts

86 lines
3.1 KiB
TypeScript

import path from "node:path";
import fc from "fast-check";
import { describe, expect, test } from "vitest";
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
const safePathSegmentArb = fc
.array(fc.constantFrom("a", "b", "c", "x", "y", "z", "0", "1", "2", "-", "_", ".", " "), {
minLength: 1,
maxLength: 12,
})
.map((chars) => chars.join(""))
.filter((segment) => segment.trim() !== "" && segment !== "." && segment !== "..");
describe("normalizeAbsolutePath", () => {
test("handles undefined and empty inputs", () => {
expect(normalizeAbsolutePath()).toBe("/");
expect(normalizeAbsolutePath("")).toBe("/");
expect(normalizeAbsolutePath(" ")).toBe("/");
});
test("normalizes posix paths", () => {
expect(normalizeAbsolutePath("/foo/bar")).toBe("/foo/bar");
expect(normalizeAbsolutePath("foo/bar")).toBe("/foo/bar");
expect(normalizeAbsolutePath("/foo//bar")).toBe("/foo/bar");
expect(normalizeAbsolutePath("/foo/./bar")).toBe("/foo/bar");
expect(normalizeAbsolutePath("/foo/../bar")).toBe("/bar");
});
test("trims trailing slashes", () => {
expect(normalizeAbsolutePath("/foo/bar/")).toBe("/foo/bar");
expect(normalizeAbsolutePath("/foo/bar//")).toBe("/foo/bar");
});
test("handles windows style paths from URI", () => {
expect(normalizeAbsolutePath("foo\\\\bar")).toBe("/foo/bar");
expect(normalizeAbsolutePath("foo\\\\bar\\\\")).toBe("/foo/bar");
});
test("handles URI encoded paths", () => {
expect(normalizeAbsolutePath("/foo%20bar")).toBe("/foo bar");
expect(normalizeAbsolutePath("foo%2Fbar")).toBe("/foo/bar");
});
test("prevents parent traversal beyond root", () => {
expect(normalizeAbsolutePath("..")).toBe("/");
expect(normalizeAbsolutePath("/..")).toBe("/");
expect(normalizeAbsolutePath("/foo/../../bar")).toBe("/bar");
});
test("is idempotent and always returns a normalized absolute path", () => {
fc.assert(
fc.property(fc.string({ maxLength: 200 }), (input) => {
const normalized = normalizeAbsolutePath(input);
expect(normalized.startsWith("/")).toBe(true);
expect(normalized).not.toContain("\\");
expect(normalized === "/" || !normalized.endsWith("/")).toBe(true);
expect(normalizeAbsolutePath(normalized)).toBe(normalized);
}),
);
});
});
describe("isPathWithin", () => {
test("matches the same path and nested paths", () => {
expect(isPathWithin("/var/lib/zerobyte", "/var/lib/zerobyte")).toBe(true);
expect(isPathWithin("/var/lib/zerobyte", "/var/lib/zerobyte/data/restic.pass")).toBe(true);
});
test("does not match sibling or parent-escape paths", () => {
expect(isPathWithin("/var/lib/zerobyte/data", "/var/lib/zerobyte/database")).toBe(false);
expect(isPathWithin("/var/lib/zerobyte/data", "/var/lib/zerobyte/data/../ssh")).toBe(false);
});
test("matches descendants created under the same normalized base", () => {
fc.assert(
fc.property(fc.string({ maxLength: 80 }), fc.array(safePathSegmentArb, { maxLength: 5 }), (base, segments) => {
const normalizedBase = normalizeAbsolutePath(base);
const descendant = path.posix.join(normalizedBase, ...segments);
expect(isPathWithin(base, descendant)).toBe(true);
}),
);
});
});