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
No known key found for this signature in database
4 changed files with 13 additions and 10 deletions

View file

@ -179,6 +179,7 @@
"name": "@zerobyte/core", "name": "@zerobyte/core",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.11", "@types/bun": "^1.3.11",
"fast-check": "^4.7.0",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5 || ^6.0.0", "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/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=="], "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=="], "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/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=="], "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=="], "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" "test": "bunx --bun vitest run --config ./vitest.config.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.11" "@types/bun": "^1.3.11",
"fast-check": "^4.7.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5 || ^6.0.0" "typescript": "^5 || ^6.0.0"

View file

@ -1,7 +1,7 @@
import path from "node:path"; import path from "node:path";
import fc from "fast-check"; import fc from "fast-check";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils"; import { isPathWithin, normalizeAbsolutePath } from "../path";
const safePathSegmentArb = fc const safePathSegmentArb = fc
.array(fc.constantFrom("a", "b", "c", "x", "y", "z", "0", "1", "2", "-", "_", ".", " "), { .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"); 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", () => { test("prevents parent traversal beyond root", () => {
expect(normalizeAbsolutePath("..")).toBe("/"); expect(normalizeAbsolutePath("..")).toBe("/");
expect(normalizeAbsolutePath("/..")).toBe("/"); expect(normalizeAbsolutePath("/..")).toBe("/");

View file

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