@ucdjs/path-utils 0.1.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Lucas Nørgård
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @ucdjs/path-utils
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![codecov][codecov-src]][codecov-href]
6
+
7
+ A collection of path utility functions for the UCD project.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @ucdjs/path-utils
13
+ ```
14
+
15
+ ## 📄 License
16
+
17
+ Published under [MIT License](./LICENSE).
18
+
19
+ [npm-version-src]: https://img.shields.io/npm/v/@ucdjs/path-utils?style=flat&colorA=18181B&colorB=4169E1
20
+ [npm-version-href]: https://npmjs.com/package/@ucdjs/path-utils
21
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@ucdjs/path-utils?style=flat&colorA=18181B&colorB=4169E1
22
+ [npm-downloads-href]: https://npmjs.com/package/@ucdjs/path-utils
23
+ [codecov-src]: https://img.shields.io/codecov/c/gh/ucdjs/ucd?style=flat&colorA=18181B&colorB=4169E1
24
+ [codecov-href]: https://codecov.io/gh/ucdjs/ucd
@@ -0,0 +1,97 @@
1
+ //#region src/errors.d.ts
2
+ declare abstract class PathUtilsBaseError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
5
+ declare class MaximumDecodingIterationsExceededError extends PathUtilsBaseError {
6
+ constructor();
7
+ }
8
+ declare class PathTraversalError extends PathUtilsBaseError {
9
+ readonly accessedPath: string;
10
+ readonly basePath: string;
11
+ constructor(basePath: string, accessedPath: string);
12
+ }
13
+ declare class WindowsDriveMismatchError extends PathUtilsBaseError {
14
+ readonly accessedDrive: string;
15
+ readonly baseDrive: string;
16
+ constructor(baseDrive: string, accessedDrive: string);
17
+ }
18
+ declare class FailedToDecodePathError extends PathUtilsBaseError {
19
+ constructor();
20
+ }
21
+ declare class IllegalCharacterInPathError extends PathUtilsBaseError {
22
+ constructor(character: string);
23
+ }
24
+ declare class WindowsPathBehaviorNotImplementedError extends PathUtilsBaseError {
25
+ constructor();
26
+ }
27
+ declare class UNCPathNotSupportedError extends PathUtilsBaseError {
28
+ readonly path: string;
29
+ constructor(path: string);
30
+ }
31
+ //#endregion
32
+ //#region src/platform.d.ts
33
+ /**
34
+ * Extracts the Windows drive letter from a given string, if present.
35
+ * @param {string} str - The input string to check for a Windows drive letter.
36
+ * @returns {string | null} The uppercase drive letter (e.g., "C") if found, otherwise null.
37
+ */
38
+ declare function getWindowsDriveLetter(str: string): string | null;
39
+ /**
40
+ * Checks if the given path is a Windows drive path (e.g., "C:", "D:\").
41
+ * @param {string} path - The path to check.
42
+ * @returns {boolean} True if the path is a Windows drive path, false otherwise.
43
+ */
44
+ declare function isWindowsDrivePath(path: string): boolean;
45
+ /**
46
+ * Removes the Windows drive letter from a path string.
47
+ * @param {string} path - The path to strip the drive letter from.
48
+ * @returns {string} The path without the Windows drive letter.
49
+ * @throws {TypeError} If the provided path is not a string.
50
+ */
51
+ declare function stripDriveLetter(path: string): string;
52
+ /**
53
+ * Checks if the given path is a UNC (Universal Naming Convention) path.
54
+ * @param {string} path - The path to check.
55
+ * @returns {boolean} True if the path is a UNC path, false otherwise.
56
+ */
57
+ declare function isUNCPath(path: string): boolean;
58
+ /**
59
+ * Asserts that the given path is not a UNC path. Throws UNCPathNotSupportedError if it is.
60
+ * @param {string} path - The path to check.
61
+ * @throws {UNCPathNotSupportedError} If the path is a UNC path.
62
+ */
63
+ declare function assertNotUNCPath(path: string): void;
64
+ /**
65
+ * Converts a path to Unix format by normalizing separators, stripping Windows drive letters, and ensuring a leading slash.
66
+ * @param {string} inputPath - The input path to convert.
67
+ * @returns {string} The path in Unix format.
68
+ */
69
+ declare function toUnixFormat(inputPath: string): string;
70
+ //#endregion
71
+ //#region src/security.d.ts
72
+ /**
73
+ * Checks if the resolved path is within the specified base path, considering case sensitivity.
74
+ * This function normalizes paths, applies leading slashes, and ensures the resolved path starts with the base path
75
+ * followed by a separator to prevent partial matches.
76
+ * @param {string} basePath - The base path to check against, must be a non-empty string.
77
+ * @param {string} resolvedPath - The path to check, must be a non-empty string.
78
+ * @returns {boolean} True if the resolved path is within the base path, false otherwise.
79
+ */
80
+ declare function isWithinBase(basePath: string, resolvedPath: string): boolean;
81
+ declare function decodePathSafely(encodedPath: string): string;
82
+ declare function resolveSafePath(basePath: string, inputPath: string): string;
83
+ //#endregion
84
+ //#region src/utils.d.ts
85
+ declare const isCaseSensitive: boolean;
86
+ declare const osPlatform: NodeJS.Platform | null;
87
+ //#endregion
88
+ //#region src/index.d.ts
89
+ declare const patheBasename: (path: string, suffix?: string) => string;
90
+ declare const patheDirname: (path: string) => string;
91
+ declare const patheExtname: (path: string) => string;
92
+ declare const patheJoin: (...paths: string[]) => string;
93
+ declare const patheNormalize: (path: string) => string;
94
+ declare const patheRelative: (from: string, to: string) => string;
95
+ declare const patheResolve: (...paths: string[]) => string;
96
+ //#endregion
97
+ export { FailedToDecodePathError, IllegalCharacterInPathError, MaximumDecodingIterationsExceededError, PathTraversalError, PathUtilsBaseError, UNCPathNotSupportedError, WindowsDriveMismatchError, WindowsPathBehaviorNotImplementedError, assertNotUNCPath, decodePathSafely, getWindowsDriveLetter, isCaseSensitive, isUNCPath, isWindowsDrivePath, isWithinBase, osPlatform, patheBasename, patheDirname, patheExtname, patheJoin, patheNormalize, patheRelative, patheResolve, resolveSafePath, stripDriveLetter, toUnixFormat };
package/dist/index.mjs ADDED
@@ -0,0 +1,431 @@
1
+ import pathe, { basename, dirname, extname, join, normalize, relative, resolve } from "pathe";
2
+ import { prependLeadingSlash, trimTrailingSlash } from "@luxass/utils";
3
+ import { createDebugger } from "@ucdjs-internal/shared";
4
+
5
+ //#region src/errors.ts
6
+ var PathUtilsBaseError = class extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ this.name = "PathUtilsBaseError";
10
+ }
11
+ };
12
+ var MaximumDecodingIterationsExceededError = class extends PathUtilsBaseError {
13
+ constructor() {
14
+ super("Maximum decoding iterations exceeded - possible malicious input");
15
+ this.name = "MaximumDecodingIterationsExceededError";
16
+ }
17
+ };
18
+ var PathTraversalError = class extends PathUtilsBaseError {
19
+ accessedPath;
20
+ basePath;
21
+ constructor(basePath, accessedPath) {
22
+ super(`Path traversal detected: attempted to access '${accessedPath}' which is outside the allowed base path '${basePath}'`);
23
+ this.name = "PathTraversalError";
24
+ this.basePath = basePath;
25
+ this.accessedPath = accessedPath;
26
+ }
27
+ };
28
+ var WindowsDriveMismatchError = class extends PathUtilsBaseError {
29
+ accessedDrive;
30
+ baseDrive;
31
+ constructor(baseDrive, accessedDrive) {
32
+ super(`Drive letter mismatch detected: attempted to access '${accessedDrive}' which is on a different drive than the allowed base drive '${baseDrive}'`);
33
+ this.name = "WindowsDriveMismatchError";
34
+ this.baseDrive = baseDrive;
35
+ this.accessedDrive = accessedDrive;
36
+ }
37
+ };
38
+ var FailedToDecodePathError = class extends PathUtilsBaseError {
39
+ constructor() {
40
+ super("Failed to decode path");
41
+ this.name = "FailedToDecodePathError";
42
+ }
43
+ };
44
+ var IllegalCharacterInPathError = class extends PathUtilsBaseError {
45
+ constructor(character) {
46
+ super(`Illegal character detected in path: '${character}'`);
47
+ this.name = "IllegalCharacterInPathError";
48
+ }
49
+ };
50
+ var WindowsPathBehaviorNotImplementedError = class extends PathUtilsBaseError {
51
+ constructor() {
52
+ super("Windows path behavior not implemented");
53
+ this.name = "WindowsPathBehaviorNotImplementedError";
54
+ }
55
+ };
56
+ var UNCPathNotSupportedError = class extends PathUtilsBaseError {
57
+ path;
58
+ constructor(path) {
59
+ super(`UNC paths are not supported: '${path}'`);
60
+ this.name = "UNCPathNotSupportedError";
61
+ this.path = path;
62
+ }
63
+ };
64
+
65
+ //#endregion
66
+ //#region src/constants.ts
67
+ const MAX_DECODING_ITERATIONS = 10;
68
+ const WINDOWS_DRIVE_LETTER_START_RE = /^[A-Z]:/i;
69
+ const WINDOWS_DRIVE_LETTER_EVERYWHERE_RE = /[A-Z]:/i;
70
+ const WINDOWS_DRIVE_RE = /^[A-Z]:[/\\]/i;
71
+ const WINDOWS_UNC_ROOT_RE = /^\\\\(?![.?]\\)[^\\]+\\[^\\]+/;
72
+ const CONTROL_CHARACTER_RE = /\p{Cc}/u;
73
+
74
+ //#endregion
75
+ //#region src/platform.ts
76
+ const debug$1 = createDebugger("ucdjs:path-utils:platform");
77
+ /**
78
+ * Extracts the Windows drive letter from a given string, if present.
79
+ * @param {string} str - The input string to check for a Windows drive letter.
80
+ * @returns {string | null} The uppercase drive letter (e.g., "C") if found, otherwise null.
81
+ */
82
+ function getWindowsDriveLetter(str) {
83
+ const match = str.match(WINDOWS_DRIVE_LETTER_START_RE);
84
+ if (match == null) return null;
85
+ return match[0]?.[0]?.toUpperCase() || null;
86
+ }
87
+ /**
88
+ * Checks if the given path is a Windows drive path (e.g., "C:", "D:\").
89
+ * @param {string} path - The path to check.
90
+ * @returns {boolean} True if the path is a Windows drive path, false otherwise.
91
+ */
92
+ function isWindowsDrivePath(path) {
93
+ return WINDOWS_DRIVE_RE.test(path);
94
+ }
95
+ /**
96
+ * Removes the Windows drive letter from a path string.
97
+ * @param {string} path - The path to strip the drive letter from.
98
+ * @returns {string} The path without the Windows drive letter.
99
+ * @throws {TypeError} If the provided path is not a string.
100
+ */
101
+ function stripDriveLetter(path) {
102
+ if (typeof path !== "string") throw new TypeError("Path must be a string");
103
+ assertNotUNCPath(path);
104
+ return path.replace(WINDOWS_DRIVE_LETTER_START_RE, "");
105
+ }
106
+ /**
107
+ * Checks if the given path is a UNC (Universal Naming Convention) path.
108
+ * @param {string} path - The path to check.
109
+ * @returns {boolean} True if the path is a UNC path, false otherwise.
110
+ */
111
+ function isUNCPath(path) {
112
+ return WINDOWS_UNC_ROOT_RE.test(path);
113
+ }
114
+ /**
115
+ * Asserts that the given path is not a UNC path. Throws UNCPathNotSupportedError if it is.
116
+ * @param {string} path - The path to check.
117
+ * @throws {UNCPathNotSupportedError} If the path is a UNC path.
118
+ */
119
+ function assertNotUNCPath(path) {
120
+ if (isUNCPath(path)) {
121
+ debug$1?.("UNC path detected and rejected", { path });
122
+ throw new UNCPathNotSupportedError(path);
123
+ }
124
+ }
125
+ /**
126
+ * Converts a path to Unix format by normalizing separators, stripping Windows drive letters, and ensuring a leading slash.
127
+ * @param {string} inputPath - The input path to convert.
128
+ * @returns {string} The path in Unix format.
129
+ */
130
+ function toUnixFormat(inputPath) {
131
+ if (typeof inputPath !== "string") throw new TypeError("Input path must be a string");
132
+ if (inputPath.trim() === "") return "/";
133
+ assertNotUNCPath(inputPath);
134
+ let normalized = pathe.normalize(inputPath.trim());
135
+ normalized = normalized.replace(WINDOWS_DRIVE_LETTER_EVERYWHERE_RE, "");
136
+ normalized = prependLeadingSlash(normalized);
137
+ return trimTrailingSlash(pathe.normalize(normalized));
138
+ }
139
+
140
+ //#endregion
141
+ //#region src/utils.ts
142
+ const isCaseSensitive = "process" in globalThis && typeof globalThis.process === "object" && "platform" in globalThis.process && typeof globalThis.process.platform === "string" && globalThis.process.platform !== "win32" && globalThis.process.platform !== "darwin";
143
+ const osPlatform = /* @__PURE__ */ (() => {
144
+ if ("process" in globalThis && typeof globalThis.process === "object" && "platform" in globalThis.process && typeof globalThis.process.platform === "string") return globalThis.process.platform;
145
+ return null;
146
+ })();
147
+
148
+ //#endregion
149
+ //#region src/security.ts
150
+ const debug = createDebugger("ucdjs:path-utils:security");
151
+ /**
152
+ * Checks if the resolved path is within the specified base path, considering case sensitivity.
153
+ * This function normalizes paths, applies leading slashes, and ensures the resolved path starts with the base path
154
+ * followed by a separator to prevent partial matches.
155
+ * @param {string} basePath - The base path to check against, must be a non-empty string.
156
+ * @param {string} resolvedPath - The path to check, must be a non-empty string.
157
+ * @returns {boolean} True if the resolved path is within the base path, false otherwise.
158
+ */
159
+ function isWithinBase(basePath, resolvedPath) {
160
+ if (typeof resolvedPath !== "string" || typeof basePath !== "string") {
161
+ debug?.("isWithinBase: invalid input types", {
162
+ resolvedPathType: typeof resolvedPath,
163
+ basePathType: typeof basePath
164
+ });
165
+ return false;
166
+ }
167
+ resolvedPath = resolvedPath.trim();
168
+ basePath = basePath.trim();
169
+ if (resolvedPath === "" || basePath === "") {
170
+ debug?.("isWithinBase: empty path(s)", {
171
+ resolvedPathEmpty: resolvedPath === "",
172
+ basePathEmpty: basePath === ""
173
+ });
174
+ return false;
175
+ }
176
+ assertNotUNCPath(resolvedPath);
177
+ assertNotUNCPath(basePath);
178
+ basePath = isWindowsDrivePath(basePath) ? basePath : prependLeadingSlash(basePath);
179
+ resolvedPath = isWindowsDrivePath(resolvedPath) ? resolvedPath : prependLeadingSlash(resolvedPath);
180
+ const normalizedResolved = pathe.normalize(resolvedPath);
181
+ const normalizedBase = pathe.normalize(basePath);
182
+ const resolved = isCaseSensitive ? normalizedResolved : normalizedResolved.toLowerCase();
183
+ const base = isCaseSensitive ? normalizedBase : normalizedBase.toLowerCase();
184
+ const baseWithSeparator = base.endsWith(pathe.sep) ? base : base + pathe.sep;
185
+ const isWithin = resolved === base || resolved.startsWith(baseWithSeparator);
186
+ debug?.("isWithinBase: check completed", {
187
+ normalizedBase,
188
+ normalizedResolved,
189
+ baseWithSeparator,
190
+ isCaseSensitive,
191
+ isWithin
192
+ });
193
+ return isWithin;
194
+ }
195
+ function decodePathSafely(encodedPath) {
196
+ if (typeof encodedPath !== "string") throw new TypeError("Encoded path must be a string");
197
+ debug?.("decodePathSafely: starting decode", { encodedPath });
198
+ let decodedPath = encodedPath;
199
+ let previousPath;
200
+ let iterations = 0;
201
+ do {
202
+ previousPath = decodedPath;
203
+ try {
204
+ decodedPath = decodeURIComponent(decodedPath);
205
+ } catch {}
206
+ decodedPath = decodedPath.replace(/%2e/gi, ".").replace(/%2f/gi, "/").replace(/%5c/gi, "\\");
207
+ iterations++;
208
+ } while (decodedPath !== previousPath && iterations < MAX_DECODING_ITERATIONS);
209
+ if (iterations >= MAX_DECODING_ITERATIONS) {
210
+ debug?.("decodePathSafely: max iterations exceeded", {
211
+ iterations,
212
+ originalPath: encodedPath,
213
+ finalPath: decodedPath
214
+ });
215
+ throw new MaximumDecodingIterationsExceededError();
216
+ }
217
+ debug?.("decodePathSafely: completed", {
218
+ iterations,
219
+ originalPath: encodedPath,
220
+ decodedPath,
221
+ wasEncoded: encodedPath !== decodedPath
222
+ });
223
+ return decodedPath;
224
+ }
225
+ function resolveSafePath(basePath, inputPath) {
226
+ if (typeof basePath !== "string") throw new TypeError("Base path must be a string");
227
+ basePath = basePath.trim();
228
+ inputPath = inputPath.trim();
229
+ debug?.("resolveSafePath: called", {
230
+ basePath,
231
+ inputPath
232
+ });
233
+ if (basePath === "") throw new Error("Base path cannot be empty");
234
+ assertNotUNCPath(basePath);
235
+ assertNotUNCPath(inputPath);
236
+ let decodedPath;
237
+ try {
238
+ decodedPath = decodePathSafely(inputPath);
239
+ } catch (err) {
240
+ debug?.("resolveSafePath: failed to decode input path", {
241
+ inputPath,
242
+ error: err
243
+ });
244
+ throw new FailedToDecodePathError();
245
+ }
246
+ assertNotUNCPath(decodedPath);
247
+ const originalBasePath = basePath;
248
+ const originalInputPath = inputPath;
249
+ basePath = isWindowsDrivePath(basePath) ? basePath : prependLeadingSlash(basePath);
250
+ inputPath = isWindowsDrivePath(inputPath) ? inputPath : prependLeadingSlash(inputPath);
251
+ debug?.("resolveSafePath: after leading slash normalization", {
252
+ basePath: {
253
+ original: originalBasePath,
254
+ normalized: basePath
255
+ },
256
+ inputPath: {
257
+ original: originalInputPath,
258
+ normalized: inputPath
259
+ },
260
+ decodedPath
261
+ });
262
+ const normalizedBasePath = pathe.normalize(basePath);
263
+ const illegalMatch = decodedPath.match(CONTROL_CHARACTER_RE);
264
+ if (decodedPath.includes("\0") || illegalMatch != null) {
265
+ const illegalChar = decodedPath.includes("\0") ? "\0" : illegalMatch?.[0] ?? "[unknown]";
266
+ debug?.("resolveSafePath: illegal character detected", {
267
+ decodedPath,
268
+ illegalChar,
269
+ charCode: illegalChar.charCodeAt(0)
270
+ });
271
+ throw new IllegalCharacterInPathError(illegalChar);
272
+ }
273
+ let resolvedPath;
274
+ const absoluteInputPath = pathe.normalize(decodedPath);
275
+ const isAbsoluteInput = WINDOWS_DRIVE_RE.test(decodedPath) || pathe.isAbsolute(toUnixFormat(decodedPath));
276
+ debug?.("resolveSafePath: path analysis", {
277
+ absoluteInputPath,
278
+ isAbsoluteInput,
279
+ normalizedBasePath
280
+ });
281
+ if (isAbsoluteInput && isWithinBase(normalizedBasePath, absoluteInputPath)) {
282
+ const normalizedBase = pathe.normalize(basePath);
283
+ const normalizedInput = pathe.normalize(absoluteInputPath);
284
+ if (normalizedInput.toLowerCase().startsWith(normalizedBase.toLowerCase())) {
285
+ const tailAfterBase = normalizedInput.slice(normalizedBase.length);
286
+ const result = pathe.normalize(normalizedBase + tailAfterBase);
287
+ debug?.("resolveSafePath: absolute path within base (preserving casing)", {
288
+ normalizedBase,
289
+ normalizedInput,
290
+ tailAfterBase,
291
+ result
292
+ });
293
+ return result;
294
+ }
295
+ debug?.("resolveSafePath: absolute path within base", { result: absoluteInputPath });
296
+ return pathe.normalize(absoluteInputPath);
297
+ }
298
+ const isWindows = osPlatform === "win32" || !isCaseSensitive && osPlatform !== "darwin";
299
+ if (isWindows && isWindowsDrivePath(decodedPath)) {
300
+ debug?.("resolveSafePath: delegating to Windows path resolution", {
301
+ isWindows,
302
+ decodedPath
303
+ });
304
+ return internal_resolveWindowsPath(basePath, decodedPath);
305
+ }
306
+ const unixPath = decodedPath.replace(/\\/g, "/");
307
+ if (pathe.isAbsolute(unixPath)) {
308
+ debug?.("resolveSafePath: handling absolute Unix path", {
309
+ unixPath,
310
+ normalizedBasePath
311
+ });
312
+ resolvedPath = internal_handleAbsolutePath(unixPath, normalizedBasePath);
313
+ } else {
314
+ debug?.("resolveSafePath: handling relative Unix path", {
315
+ unixPath,
316
+ normalizedBasePath
317
+ });
318
+ resolvedPath = internal_handleRelativePath(unixPath, normalizedBasePath);
319
+ }
320
+ if (!isWithinBase(normalizedBasePath, resolvedPath)) {
321
+ debug?.("resolveSafePath: path traversal detected", {
322
+ basePath: normalizedBasePath,
323
+ resolvedPath,
324
+ originalInput: inputPath
325
+ });
326
+ throw new PathTraversalError(normalizedBasePath, resolvedPath);
327
+ }
328
+ const normalized = pathe.normalize(resolvedPath);
329
+ debug?.("resolveSafePath: completed successfully", {
330
+ originalInput: originalInputPath,
331
+ basePath: normalizedBasePath,
332
+ result: normalized
333
+ });
334
+ return normalized;
335
+ }
336
+ /**
337
+ * @internal
338
+ */
339
+ function internal_resolveWindowsPath(basePath, decodedPath) {
340
+ debug?.("internal_resolveWindowsPath: called", {
341
+ basePath,
342
+ decodedPath
343
+ });
344
+ if (isWindowsDrivePath(decodedPath) && isWindowsDrivePath(basePath)) {
345
+ const normalizedBasePath = pathe.normalize(basePath);
346
+ const baseDriveLetter = getWindowsDriveLetter(basePath);
347
+ const inputDriveLetter = getWindowsDriveLetter(decodedPath);
348
+ debug?.("internal_resolveWindowsPath: drive letter comparison", {
349
+ baseDriveLetter,
350
+ inputDriveLetter,
351
+ match: baseDriveLetter === inputDriveLetter
352
+ });
353
+ if (baseDriveLetter != null && inputDriveLetter != null && baseDriveLetter !== inputDriveLetter) {
354
+ debug?.("internal_resolveWindowsPath: drive letter mismatch", {
355
+ baseDriveLetter,
356
+ inputDriveLetter
357
+ });
358
+ throw new WindowsDriveMismatchError(baseDriveLetter, inputDriveLetter);
359
+ }
360
+ const normalizedDecodedPath = pathe.normalize(decodedPath);
361
+ if (!isWithinBase(normalizedBasePath, normalizedDecodedPath)) {
362
+ debug?.("internal_resolveWindowsPath: path traversal detected", {
363
+ basePath: normalizedBasePath,
364
+ resolvedPath: normalizedDecodedPath
365
+ });
366
+ throw new PathTraversalError(normalizedBasePath, normalizedDecodedPath);
367
+ }
368
+ const result = pathe.normalize(normalizedDecodedPath);
369
+ debug?.("internal_resolveWindowsPath: completed successfully", {
370
+ basePath: normalizedBasePath,
371
+ result
372
+ });
373
+ return result;
374
+ }
375
+ debug?.("internal_resolveWindowsPath: unhandled path combination", {
376
+ basePath,
377
+ decodedPath,
378
+ isBaseWindowsDrive: isWindowsDrivePath(basePath),
379
+ isInputWindowsDrive: isWindowsDrivePath(decodedPath)
380
+ });
381
+ throw new WindowsPathBehaviorNotImplementedError();
382
+ }
383
+ /**
384
+ * Handles absolute Unix-style paths by treating them as relative to the base path boundary.
385
+ * @internal
386
+ */
387
+ function internal_handleAbsolutePath(absoluteUnixPath, basePath) {
388
+ if (absoluteUnixPath === "/") {
389
+ debug?.("internal_handleAbsolutePath: root path mapped to base", { basePath });
390
+ return basePath;
391
+ }
392
+ const pathWithoutLeadingSlash = absoluteUnixPath.replace(/^\/+/, "");
393
+ const resolved = pathe.resolve(basePath, pathWithoutLeadingSlash);
394
+ debug?.("internal_handleAbsolutePath: resolved", {
395
+ absoluteUnixPath,
396
+ pathWithoutLeadingSlash,
397
+ basePath,
398
+ resolved
399
+ });
400
+ return resolved;
401
+ }
402
+ /**
403
+ * Handles relative Unix-style paths by resolving them against the base path.
404
+ * @internal
405
+ */
406
+ function internal_handleRelativePath(relativeUnixPath, basePath) {
407
+ if (relativeUnixPath === "." || relativeUnixPath === "./") {
408
+ debug?.("internal_handleRelativePath: current directory mapped to base", { basePath });
409
+ return basePath;
410
+ }
411
+ const resolved = pathe.resolve(basePath, relativeUnixPath);
412
+ debug?.("internal_handleRelativePath: resolved", {
413
+ relativeUnixPath,
414
+ basePath,
415
+ resolved
416
+ });
417
+ return resolved;
418
+ }
419
+
420
+ //#endregion
421
+ //#region src/index.ts
422
+ const patheBasename = basename;
423
+ const patheDirname = dirname;
424
+ const patheExtname = extname;
425
+ const patheJoin = join;
426
+ const patheNormalize = normalize;
427
+ const patheRelative = relative;
428
+ const patheResolve = resolve;
429
+
430
+ //#endregion
431
+ export { FailedToDecodePathError, IllegalCharacterInPathError, MaximumDecodingIterationsExceededError, PathTraversalError, PathUtilsBaseError, UNCPathNotSupportedError, WindowsDriveMismatchError, WindowsPathBehaviorNotImplementedError, assertNotUNCPath, decodePathSafely, getWindowsDriveLetter, isCaseSensitive, isUNCPath, isWindowsDrivePath, isWithinBase, osPlatform, patheBasename, patheDirname, patheExtname, patheJoin, patheNormalize, patheRelative, patheResolve, resolveSafePath, stripDriveLetter, toUnixFormat };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@ucdjs/path-utils",
3
+ "version": "0.1.1-beta.1",
4
+ "type": "module",
5
+ "author": {
6
+ "name": "Lucas Nørgård",
7
+ "email": "lucasnrgaard@gmail.com",
8
+ "url": "https://luxass.dev"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/ucdjs/ucd",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ucdjs/ucd.git",
15
+ "directory": "packages/path-utils"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/ucdjs/ucd/issues"
19
+ },
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": "./dist/index.mjs",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "types": "./dist/index.d.mts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22.18"
31
+ },
32
+ "dependencies": {
33
+ "@luxass/utils": "2.7.3",
34
+ "pathe": "2.0.3",
35
+ "@ucdjs-internal/shared": "0.1.1-beta.1"
36
+ },
37
+ "devDependencies": {
38
+ "@luxass/eslint-config": "7.2.0",
39
+ "eslint": "10.0.0",
40
+ "publint": "0.3.17",
41
+ "tsdown": "0.20.3",
42
+ "typescript": "5.9.3",
43
+ "vitest-testdirs": "4.4.2",
44
+ "@ucdjs-tooling/tsconfig": "1.0.0",
45
+ "@ucdjs-tooling/tsdown-config": "1.0.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown --tsconfig=./tsconfig.build.json",
52
+ "dev": "tsdown --watch",
53
+ "clean": "git clean -xdf dist node_modules",
54
+ "lint": "eslint .",
55
+ "typecheck": "tsc --noEmit -p tsconfig.build.json"
56
+ }
57
+ }