fix(core): preserve significant path whitespace

This commit is contained in:
Nicolas Meienberger
2026-05-04 07:19:57 +02:00
parent 7be7c9edae
commit 38f5a669ae
4 changed files with 13 additions and 10 deletions

View File

@@ -179,6 +179,7 @@
"name": "@zerobyte/core",
"devDependencies": {
"@types/bun": "^1.3.11",
"fast-check": "^4.7.0",
},
"peerDependencies": {
"typescript": "^5 || ^6.0.0",
@@ -3063,8 +3064,6 @@
"@zerobyte/contracts/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@zerobyte/core/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"agent/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -3269,8 +3268,6 @@
"@zerobyte/contracts/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"@zerobyte/core/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"agent/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],

View File

@@ -34,7 +34,8 @@
"test": "bunx --bun vitest run --config ./vitest.config.ts"
},
"devDependencies": {
"@types/bun": "^1.3.11"
"@types/bun": "^1.3.11",
"fast-check": "^4.7.0"
},
"peerDependencies": {
"typescript": "^5 || ^6.0.0"

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import fc from "fast-check";
import { describe, expect, test } from "vitest";
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
import { isPathWithin, normalizeAbsolutePath } from "../path";
const safePathSegmentArb = fc
.array(fc.constantFrom("a", "b", "c", "x", "y", "z", "0", "1", "2", "-", "_", ".", " "), {
@@ -41,6 +41,12 @@ describe("normalizeAbsolutePath", () => {
expect(normalizeAbsolutePath("foo%2Fbar")).toBe("/foo/bar");
});
test("preserves spaces inside path segments", () => {
expect(normalizeAbsolutePath("! \\")).toBe("/! ");
expect(normalizeAbsolutePath("/foo ")).toBe("/foo ");
expect(normalizeAbsolutePath(" foo")).toBe("/ foo");
});
test("prevents parent traversal beyond root", () => {
expect(normalizeAbsolutePath("..")).toBe("/");
expect(normalizeAbsolutePath("/..")).toBe("/");

View File

@@ -1,12 +1,11 @@
export const normalizeAbsolutePath = (value?: string): string => {
const trimmed = value?.trim();
if (!trimmed) return "/";
if (!value?.trim()) return "/";
let normalizedInput: string;
try {
normalizedInput = decodeURIComponent(trimmed).replace(/\\+/g, "/");
normalizedInput = decodeURIComponent(value).replace(/\\+/g, "/");
} catch {
normalizedInput = trimmed.replace(/\\+/g, "/");
normalizedInput = value.replace(/\\+/g, "/");
}
const withLeadingSlash = normalizedInput.startsWith("/") ? normalizedInput : `/${normalizedInput}`;