@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 +21 -0
- package/README.md +24 -0
- package/dist/index.d.mts +97 -0
- package/dist/index.mjs +431 -0
- package/package.json +57 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|