From 38f5a669aeb78a44fb58b75a323eda770cc5a494 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 4 May 2026 07:19:57 +0200 Subject: [PATCH] fix(core): preserve significant path whitespace --- bun.lock | 5 +---- packages/core/package.json | 3 ++- {app => packages/core/src}/utils/__tests__/path.test.ts | 8 +++++++- packages/core/src/utils/path.ts | 7 +++---- 4 files changed, 13 insertions(+), 10 deletions(-) rename {app => packages/core/src}/utils/__tests__/path.test.ts (91%) diff --git a/bun.lock b/bun.lock index c23c40c1..1c5c75c2 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/core/package.json b/packages/core/package.json index f60e7406..4a748571 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" diff --git a/app/utils/__tests__/path.test.ts b/packages/core/src/utils/__tests__/path.test.ts similarity index 91% rename from app/utils/__tests__/path.test.ts rename to packages/core/src/utils/__tests__/path.test.ts index a3114720..b8aa0dba 100644 --- a/app/utils/__tests__/path.test.ts +++ b/packages/core/src/utils/__tests__/path.test.ts @@ -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("/"); diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts index 3cfdcc2e..53d76878 100644 --- a/packages/core/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -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}`;