// Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. // Lexical POSIX and Windows path operations adapted from Node.js // `lib/path.js` at v22.16.0 (commit 9774069718d9f80578079d252dddf37ae6fd550d). // See vendor/AVRIL.md for provenance and the full list of local differences: // no `process` or `process.env` access (avril's permission-checked cwd op // with drive-root fallback instead of per-drive environment entries), no // `glob`/`matchesGlob`, no legacy `_makeLong`, and hardened primordial use. import { ArrayIsArray, ArrayPrototypeJoin, ArrayPrototypePop, ArrayPrototypePush, ArrayPrototypeSlice, ObjectFreeze, StringPrototypeCharCodeAt, StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeLastIndexOf, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeToLowerCase, TypeError, } from "ext:core/primordials"; import { op_path_platform, op_process_cwd } from "ext:core/ops"; import { result } from "ext:file/errors.js"; const CHAR_UPPERCASE_A = 65; // A const CHAR_LOWERCASE_A = 97; // a const CHAR_UPPERCASE_Z = 90; // Z const CHAR_LOWERCASE_Z = 122; // z const CHAR_DOT = 46; // . const CHAR_FORWARD_SLASH = 47; // / const CHAR_BACKWARD_SLASH = 92; // \ const CHAR_COLON = 58; // : const CHAR_QUESTION_MARK = 63; // ? const hostIsWindows = op_path_platform() === "win32"; function describeReceived(value: unknown): string { if (value === null) return "null"; if (typeof value !== "object") return typeof value; return ArrayIsArray(value) ? "an instance of Array" : "an instance of Object"; } function validateString(value: unknown, name: string): asserts value is string { if (typeof value !== "string") { throw new TypeError( `The "${name}" argument must be of type string. Received ${describeReceived(value)}`, ); } } function validateObject(value: unknown, name: string): asserts value is Record { if (value === null || ArrayIsArray(value) || typeof value !== "object") { throw new TypeError( `The "${name}" argument must be of type Object. Received ${describeReceived(value)}`, ); } } // Single-character string split without consulting a separator object's // Symbol.split hook: only primitive one-code-unit separators are supported. function splitOnChar(input: string, code: number): string[] { const parts: string[] = []; let start = 0; for (let i = 0; i < input.length; i++) { if (StringPrototypeCharCodeAt(input, i) === code) { ArrayPrototypePush(parts, StringPrototypeSlice(input, start, i)); start = i + 1; } } ArrayPrototypePush(parts, StringPrototypeSlice(input, start, input.length)); return parts; } // Permission-checked working directory. Called only when a relative, // root-relative, or drive-relative input actually needs it; fully absolute // inputs never reach here, so they need no read grant. A missing grant // throws the usual PermissionDenied failure. function nativeCwd() { return result(op_process_cwd()); } function isPathSeparator(code: number): boolean { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; } function isPosixPathSeparator(code: number): boolean { return code === CHAR_FORWARD_SLASH; } function isWindowsDeviceRoot(code: number): boolean { return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z); } // Resolves . and .. elements in a path with directory names function normalizeString(path: string, allowAboveRoot: boolean, separator: string, isPathSeparator: (code: number) => boolean): string { let res = ""; let lastSegmentLength = 0; let lastSlash = -1; let dots = 0; let code = 0; for (let i = 0; i <= path.length; ++i) { if (i < path.length) { code = StringPrototypeCharCodeAt(path, i); } else if (isPathSeparator(code)) { break; } else { code = CHAR_FORWARD_SLASH; } if (isPathSeparator(code)) { if (lastSlash === i - 1 || dots === 1) { // NOOP } else if (dots === 2) { if ( res.length < 2 || lastSegmentLength !== 2 || StringPrototypeCharCodeAt(res, res.length - 1) !== CHAR_DOT || StringPrototypeCharCodeAt(res, res.length - 2) !== CHAR_DOT ) { if (res.length > 2) { const lastSlashIndex = StringPrototypeLastIndexOf(res, separator); if (lastSlashIndex === -1) { res = ""; lastSegmentLength = 0; } else { res = StringPrototypeSlice(res, 0, lastSlashIndex); lastSegmentLength = res.length - 1 - StringPrototypeLastIndexOf(res, separator); } lastSlash = i; dots = 0; continue; } else if (res.length !== 0) { res = ""; lastSegmentLength = 0; lastSlash = i; dots = 0; continue; } } if (allowAboveRoot) { res += res.length > 0 ? `${separator}..` : ".."; lastSegmentLength = 2; } } else { if (res.length > 0) { res += `${separator}${StringPrototypeSlice(path, lastSlash + 1, i)}`; } else { res = StringPrototypeSlice(path, lastSlash + 1, i); } lastSegmentLength = i - lastSlash - 1; } lastSlash = i; dots = 0; } else if (code === CHAR_DOT && dots !== -1) { ++dots; } else { dots = -1; } } return res; } function formatExt(ext: string | undefined): string { return ext ? `${ext[0] === "." ? "" : "."}${ext}` : ""; } function _format(sep: string, pathObject: PathFormatObject): string { validateObject(pathObject, "pathObject"); const dir = pathObject.dir || pathObject.root; const base = pathObject.base || `${pathObject.name || ""}${formatExt(pathObject.ext)}`; if (!dir) { return base; } return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`; } // The working directory for a Windows drive. Node reads the drive's hidden // per-drive directory from the process environment here; avril grants no // environment access for paths, so a cwd on another drive (or with no drive // letter at all, as on POSIX hosts) falls back to the drive's root. function cwdForDrive(device: string): string { const cwd = nativeCwd(); if ( cwd.length >= 2 && StringPrototypeToLowerCase(StringPrototypeSlice(cwd, 0, 2)) === StringPrototypeToLowerCase(device) && (cwd.length === 2 || StringPrototypeCharCodeAt(cwd, 2) === CHAR_BACKWARD_SLASH || StringPrototypeCharCodeAt(cwd, 2) === CHAR_FORWARD_SLASH) ) { return cwd; } return `${device}\\`; } type PathFormatObject = { dir?: string; root?: string; base?: string; name?: string; ext?: string }; type ParsedPath = { root: string; dir: string; base: string; ext: string; name: string }; type PathApi = { resolve(...paths: string[]): string; normalize(path: string): string; isAbsolute(path: string): boolean; join(...paths: string[]): string; relative(from: string, to: string): string; toNamespacedPath(path: string): string; dirname(path: string): string; basename(path: string, suffix?: string): string; extname(path: string): string; format(pathObject: PathFormatObject): string; parse(path: string): ParsedPath; sep: string; delimiter: string; }; const win32: PathApi = { resolve(...args) { let resolvedDevice = ""; let resolvedTail = ""; let resolvedAbsolute = false; for (let i = args.length - 1; i >= -1; i--) { let path; if (i >= 0) { path = args[i]!; validateString(path, `paths[${i}]`); // Skip empty entries if (path.length === 0) { continue; } } else if (resolvedDevice.length === 0) { path = nativeCwd(); } else { // We're sure the device is not a UNC path at this point, because UNC // paths are always absolute. path = cwdForDrive(resolvedDevice); } const len = path.length; let rootEnd = 0; let device = ""; let isAbsolute = false; const code = StringPrototypeCharCodeAt(path, 0); // Try to match a root if (len === 1) { if (isPathSeparator(code)) { // `path` contains just a path separator rootEnd = 1; isAbsolute = true; } } else if (isPathSeparator(code)) { // Possible UNC root // If we started with a separator, we know we at least have an // absolute path of some kind (UNC or otherwise) isAbsolute = true; if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) { // Matched double path separator at beginning let j = 2; let last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { const firstPart = StringPrototypeSlice(path, last, j); // Matched! last = j; // Match 1 or more path separators while (j < len && isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j === len || j !== last) { // We matched a UNC root device = `\\\\${firstPart}\\${StringPrototypeSlice(path, last, j)}`; rootEnd = j; } } } } else { rootEnd = 1; } } else if ( isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON ) { // Possible device root device = StringPrototypeSlice(path, 0, 2); rootEnd = 2; if (len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2))) { // Treat separator following drive name as an absolute path // indicator isAbsolute = true; rootEnd = 3; } } if (device.length > 0) { if (resolvedDevice.length > 0) { if ( StringPrototypeToLowerCase(device) !== StringPrototypeToLowerCase(resolvedDevice) ) { // This path points to another device so it is not applicable continue; } } else { resolvedDevice = device; } } if (resolvedAbsolute) { if (resolvedDevice.length > 0) { break; } } else { resolvedTail = `${StringPrototypeSlice(path, rootEnd)}\\${resolvedTail}`; resolvedAbsolute = isAbsolute; if (isAbsolute && resolvedDevice.length > 0) { break; } } } // At this point the path should be resolved to a full absolute path, // but handle relative paths to be safe (might happen when the working // directory lookup fails, which surfaces as PermissionDenied instead). // Normalize the tail path resolvedTail = normalizeString( resolvedTail, !resolvedAbsolute, "\\", isPathSeparator, ); return resolvedAbsolute ? `${resolvedDevice}\\${resolvedTail}` : `${resolvedDevice}${resolvedTail}` || "."; }, normalize(path) { validateString(path, "path"); const len = path.length; if (len === 0) { return "."; } let rootEnd = 0; let device; let isAbsolute = false; const code = StringPrototypeCharCodeAt(path, 0); // Try to match a root if (len === 1) { // `path` contains just a single char, exit early to avoid // unnecessary work return isPosixPathSeparator(code) ? "\\" : path; } if (isPathSeparator(code)) { // Possible UNC root // If we started with a separator, we know we at least have an absolute // path of some kind (UNC or otherwise) isAbsolute = true; if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) { // Matched double path separator at beginning let j = 2; let last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { const firstPart = StringPrototypeSlice(path, last, j); // Matched! last = j; // Match 1 or more path separators while (j < len && isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j === len) { // We matched a UNC root only // Return the normalized version of the UNC root since there // is nothing left to process return `\\\\${firstPart}\\${StringPrototypeSlice(path, last)}\\`; } if (j !== last) { // We matched a UNC root with leftovers device = `\\\\${firstPart}\\${StringPrototypeSlice(path, last, j)}`; rootEnd = j; } } } } else { rootEnd = 1; } } else if ( isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON ) { // Possible device root device = StringPrototypeSlice(path, 0, 2); rootEnd = 2; if (len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2))) { // Treat separator following drive name as an absolute path // indicator isAbsolute = true; rootEnd = 3; } } let tail = rootEnd < len ? normalizeString( StringPrototypeSlice(path, rootEnd), !isAbsolute, "\\", isPathSeparator, ) : ""; if (tail.length === 0 && !isAbsolute) { tail = "."; } if ( tail.length > 0 && isPathSeparator(StringPrototypeCharCodeAt(path, len - 1)) ) { tail += "\\"; } if (!isAbsolute && device === undefined && StringPrototypeIncludes(path, ":")) { // If the original path was not absolute and if we have not been able to // resolve it relative to a particular device, we need to ensure that the // `tail` has not become something that Windows might interpret as an // absolute path. See CVE-2024-36139. if ( tail.length >= 2 && isWindowsDeviceRoot(StringPrototypeCharCodeAt(tail, 0)) && StringPrototypeCharCodeAt(tail, 1) === CHAR_COLON ) { return `.\\${tail}`; } let index = StringPrototypeIndexOf(path, ":"); do { if ( index === len - 1 || isPathSeparator(StringPrototypeCharCodeAt(path, index + 1)) ) { return `.\\${tail}`; } } while ((index = StringPrototypeIndexOf(path, ":", index + 1)) !== -1); } if (device === undefined) { return isAbsolute ? `\\${tail}` : tail; } return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`; }, isAbsolute(path) { validateString(path, "path"); const len = path.length; if (len === 0) { return false; } const code = StringPrototypeCharCodeAt(path, 0); return isPathSeparator(code) || // Possible device root (len > 2 && isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON && isPathSeparator(StringPrototypeCharCodeAt(path, 2))); }, join(...args) { if (args.length === 0) { return "."; } let joined; let firstPart: string; for (let i = 0; i < args.length; ++i) { const arg = args[i]; validateString(arg, "path"); if (arg.length > 0) { if (joined === undefined) { joined = firstPart = arg; } else { joined += `\\${arg}`; } } } if (joined === undefined) { return "."; } // Make sure that the joined path doesn't start with two slashes, because // normalize() will mistake it for a UNC path then. // // This step is skipped when it is very clear that the user actually // intended to point at a UNC path. This is assumed when the first // non-empty string arguments starts with exactly two slashes followed by // at least one more non-slash character. // // Note that for normalize() to treat a path as a UNC path it needs to // have at least 2 components, so we don't filter for that here. // This means that the user can use join to construct UNC paths from // a server name and a share name; for example: // path.join('//server', 'share') -> '\\\\server\\share\\') let needsReplace = true; let slashCount = 0; if (isPathSeparator(StringPrototypeCharCodeAt(firstPart!, 0))) { ++slashCount; const firstLen = firstPart!.length; if ( firstLen > 1 && isPathSeparator(StringPrototypeCharCodeAt(firstPart!, 1)) ) { ++slashCount; if (firstLen > 2) { if (isPathSeparator(StringPrototypeCharCodeAt(firstPart!, 2))) { ++slashCount; } else { // We matched a UNC path in the first part needsReplace = false; } } } } if (needsReplace) { // Find any more consecutive slashes we need to replace while ( slashCount < joined.length && isPathSeparator(StringPrototypeCharCodeAt(joined, slashCount)) ) { slashCount++; } // Replace the slashes if needed if (slashCount >= 2) { joined = `\\${StringPrototypeSlice(joined, slashCount)}`; } } return win32.normalize(joined); }, relative(from, to) { validateString(from, "from"); validateString(to, "to"); if (from === to) { return ""; } const fromOrig = win32.resolve(from); const toOrig = win32.resolve(to); if (fromOrig === toOrig) { return ""; } from = StringPrototypeToLowerCase(fromOrig); to = StringPrototypeToLowerCase(toOrig); if (from === to) { return ""; } if ( fromOrig.length !== from.length || toOrig.length !== to.length ) { const fromSplit = splitOnChar(fromOrig, CHAR_BACKWARD_SLASH); const toSplit = splitOnChar(toOrig, CHAR_BACKWARD_SLASH); if (fromSplit[fromSplit.length - 1] === "") { ArrayPrototypePop(fromSplit); } if (toSplit[toSplit.length - 1] === "") { ArrayPrototypePop(toSplit); } const fromLen = fromSplit.length; const toLen = toSplit.length; const length = fromLen < toLen ? fromLen : toLen; let i; for (i = 0; i < length; i++) { if ( StringPrototypeToLowerCase(fromSplit[i]) !== StringPrototypeToLowerCase(toSplit[i]) ) { break; } } if (i === 0) { return toOrig; } else if (i === length) { if (toLen > length) { return ArrayPrototypeJoin(ArrayPrototypeSlice(toSplit, i), "\\"); } if (fromLen > length) { return StringPrototypeRepeat("..\\", fromLen - 1 - i) + ".."; } return ""; } return StringPrototypeRepeat("..\\", fromLen - i) + ArrayPrototypeJoin(ArrayPrototypeSlice(toSplit, i), "\\"); } // Trim any leading backslashes let fromStart = 0; while ( fromStart < from.length && StringPrototypeCharCodeAt(from, fromStart) === CHAR_BACKWARD_SLASH ) { fromStart++; } // Trim trailing backslashes (applicable to UNC paths only) let fromEnd = from.length; while ( fromEnd - 1 > fromStart && StringPrototypeCharCodeAt(from, fromEnd - 1) === CHAR_BACKWARD_SLASH ) { fromEnd--; } const fromLen = fromEnd - fromStart; // Trim any leading backslashes let toStart = 0; while ( toStart < to.length && StringPrototypeCharCodeAt(to, toStart) === CHAR_BACKWARD_SLASH ) { toStart++; } // Trim trailing backslashes (applicable to UNC paths only) let toEnd = to.length; while ( toEnd - 1 > toStart && StringPrototypeCharCodeAt(to, toEnd - 1) === CHAR_BACKWARD_SLASH ) { toEnd--; } const toLen = toEnd - toStart; // Compare paths to find the longest common path from root const length = fromLen < toLen ? fromLen : toLen; let lastCommonSep = -1; let i = 0; for (; i < length; i++) { const fromCode = StringPrototypeCharCodeAt(from, fromStart + i); if (fromCode !== StringPrototypeCharCodeAt(to, toStart + i)) { break; } else if (fromCode === CHAR_BACKWARD_SLASH) { lastCommonSep = i; } } // We found a mismatch before the first common path separator was seen, so // return the original `to`. if (i !== length) { if (lastCommonSep === -1) { return toOrig; } } else { if (toLen > length) { if ( StringPrototypeCharCodeAt(to, toStart + i) === CHAR_BACKWARD_SLASH ) { // We get here if `from` is the exact base path for `to`. // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' return StringPrototypeSlice(toOrig, toStart + i + 1); } if (i === 2) { // We get here if `from` is the device root. // For example: from='C:\\'; to='C:\\foo' return StringPrototypeSlice(toOrig, toStart + i); } } if (fromLen > length) { if ( StringPrototypeCharCodeAt(from, fromStart + i) === CHAR_BACKWARD_SLASH ) { // We get here if `to` is the exact base path for `from`. // For example: from='C:\\foo\\bar'; to='C:\\foo' lastCommonSep = i; } else if (i === 2) { // We get here if `to` is the device root. // For example: from='C:\\foo\\bar'; to='C:\\' lastCommonSep = 3; } } if (lastCommonSep === -1) { lastCommonSep = 0; } } let out = ""; // Generate the relative path based on the path difference between `to` // and `from` for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { if ( i === fromEnd || StringPrototypeCharCodeAt(from, i) === CHAR_BACKWARD_SLASH ) { out += out.length === 0 ? ".." : "\\.."; } } toStart += lastCommonSep; // Lastly, append the rest of the destination (`to`) path that comes after // the common path parts if (out.length > 0) { return `${out}${StringPrototypeSlice(toOrig, toStart, toEnd)}`; } if (StringPrototypeCharCodeAt(toOrig, toStart) === CHAR_BACKWARD_SLASH) { ++toStart; } return StringPrototypeSlice(toOrig, toStart, toEnd); }, toNamespacedPath(path) { // Note: this will *probably* throw somewhere. if (typeof path !== "string" || path.length === 0) { return path; } const resolvedPath = win32.resolve(path); if (resolvedPath.length <= 2) { return path; } if (StringPrototypeCharCodeAt(resolvedPath, 0) === CHAR_BACKWARD_SLASH) { // Possible UNC root if (StringPrototypeCharCodeAt(resolvedPath, 1) === CHAR_BACKWARD_SLASH) { const code = StringPrototypeCharCodeAt(resolvedPath, 2); if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) { // Matched non-long UNC root, convert the path to a long UNC path return `\\\\?\\UNC\\${StringPrototypeSlice(resolvedPath, 2)}`; } } } else if ( isWindowsDeviceRoot(StringPrototypeCharCodeAt(resolvedPath, 0)) && StringPrototypeCharCodeAt(resolvedPath, 1) === CHAR_COLON && StringPrototypeCharCodeAt(resolvedPath, 2) === CHAR_BACKWARD_SLASH ) { // Matched device root, convert the path to a long UNC path return `\\\\?\\${resolvedPath}`; } return resolvedPath; }, dirname(path) { validateString(path, "path"); const len = path.length; if (len === 0) { return "."; } let rootEnd = -1; let offset = 0; const code = StringPrototypeCharCodeAt(path, 0); if (len === 1) { // `path` contains just a path separator, exit early to avoid // unnecessary work or a dot. return isPathSeparator(code) ? path : "."; } // Try to match a root if (isPathSeparator(code)) { // Possible UNC root rootEnd = offset = 1; if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) { // Matched double path separator at beginning let j = 2; let last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more path separators while (j < len && isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j === len) { // We matched a UNC root only return path; } if (j !== last) { // We matched a UNC root with leftovers // Offset by 1 to include the separator after the UNC root to // treat it as a "normal root" on top of a (UNC) root rootEnd = offset = j + 1; } } } } // Possible device root } else if ( isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON ) { rootEnd = len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2)) ? 3 : 2; offset = rootEnd; } let end = -1; let matchedSlash = true; for (let i = len - 1; i >= offset; --i) { if (isPathSeparator(StringPrototypeCharCodeAt(path, i))) { if (!matchedSlash) { end = i; break; } } else { // We saw the first non-path separator matchedSlash = false; } } if (end === -1) { if (rootEnd === -1) { return "."; } end = rootEnd; } return StringPrototypeSlice(path, 0, end); }, basename(path, suffix) { if (suffix !== undefined) { validateString(suffix, "suffix"); } validateString(path, "path"); let start = 0; let end = -1; let matchedSlash = true; // Check for a drive letter prefix so as not to mistake the following // path separator as an extra separator at the end of the path that can be // disregarded if ( path.length >= 2 && isWindowsDeviceRoot(StringPrototypeCharCodeAt(path, 0)) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON ) { start = 2; } if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { if (suffix === path) { return ""; } let extIdx = suffix.length - 1; let firstNonSlashEnd = -1; for (let i = path.length - 1; i >= start; --i) { const code = StringPrototypeCharCodeAt(path, i); if (isPathSeparator(code)) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { start = i + 1; break; } } else { if (firstNonSlashEnd === -1) { // We saw the first non-path separator, remember this index in case // we need it if the extension ends up not matching matchedSlash = false; firstNonSlashEnd = i + 1; } if (extIdx >= 0) { // Try to match the explicit extension if (code === StringPrototypeCharCodeAt(suffix, extIdx)) { if (--extIdx === -1) { // We matched the extension, so mark this as the end of our path // component end = i; } } else { // Extension does not match, so our result is the entire path // component extIdx = -1; end = firstNonSlashEnd; } } } } if (start === end) { end = firstNonSlashEnd; } else if (end === -1) { end = path.length; } return StringPrototypeSlice(path, start, end); } for (let i = path.length - 1; i >= start; --i) { if (isPathSeparator(StringPrototypeCharCodeAt(path, i))) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { start = i + 1; break; } } else if (end === -1) { // We saw the first non-path separator, mark this as the end of our // path component matchedSlash = false; end = i + 1; } } if (end === -1) { return ""; } return StringPrototypeSlice(path, start, end); }, extname(path) { validateString(path, "path"); let start = 0; let startDot = -1; let startPart = 0; let end = -1; let matchedSlash = true; // Track the state of characters (if any) we see before our first dot and // after any path separator we find let preDotState = 0; // Check for a drive letter prefix so as not to mistake the following // path separator as an extra separator at the end of the path that can be // disregarded if ( path.length >= 2 && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON && isWindowsDeviceRoot(StringPrototypeCharCodeAt(path, 0)) ) { start = startPart = 2; } for (let i = path.length - 1; i >= start; --i) { const code = StringPrototypeCharCodeAt(path, i); if (isPathSeparator(code)) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { // We saw the first non-path separator, mark this as the end of our // extension matchedSlash = false; end = i + 1; } if (code === CHAR_DOT) { // If this is our first dot, mark it as the start of our extension if (startDot === -1) { startDot = i; } else if (preDotState !== 1) { preDotState = 1; } } else if (startDot !== -1) { // We saw a non-dot and non-path separator before our dot, so we should // have a good chance at having a non-empty extension preDotState = -1; } } if ( startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot preDotState === 0 || // The (right-most) trimmed path component is exactly '..' (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) ) { return ""; } return StringPrototypeSlice(path, startDot, end); }, format(pathObject) { return _format("\\", pathObject); }, parse(path) { validateString(path, "path"); const ret = { root: "", dir: "", base: "", ext: "", name: "" }; if (path.length === 0) { return ret; } const len = path.length; let rootEnd = 0; let code = StringPrototypeCharCodeAt(path, 0); if (len === 1) { if (isPathSeparator(code)) { // `path` contains just a path separator, exit early to avoid // unnecessary work ret.root = ret.dir = path; return ret; } ret.base = ret.name = path; return ret; } // Try to match a root if (isPathSeparator(code)) { // Possible UNC root rootEnd = 1; if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) { // Matched double path separator at beginning let j = 2; let last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more path separators while (j < len && isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j < len && j !== last) { // Matched! last = j; // Match 1 or more non-path separators while (j < len && !isPathSeparator(StringPrototypeCharCodeAt(path, j))) { j++; } if (j === len) { // We matched a UNC root only rootEnd = j; } else if (j !== last) { // We matched a UNC root with leftovers rootEnd = j + 1; } } } } } else if ( isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON ) { // Possible device root if (len <= 2) { // `path` contains just a drive root, exit early to avoid // unnecessary work ret.root = ret.dir = path; return ret; } rootEnd = 2; if (isPathSeparator(StringPrototypeCharCodeAt(path, 2))) { if (len === 3) { // `path` contains just a drive root, exit early to avoid // unnecessary work ret.root = ret.dir = path; return ret; } rootEnd = 3; } } if (rootEnd > 0) { ret.root = StringPrototypeSlice(path, 0, rootEnd); } let startDot = -1; let startPart = rootEnd; let end = -1; let matchedSlash = true; let i = path.length - 1; // Track the state of characters (if any) we see before our first dot and // after any path separator we find let preDotState = 0; // Get non-dir info for (; i >= rootEnd; --i) { code = StringPrototypeCharCodeAt(path, i); if (isPathSeparator(code)) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { // We saw the first non-path separator, mark this as the end of our // extension matchedSlash = false; end = i + 1; } if (code === CHAR_DOT) { // If this is our first dot, mark it as the start of our extension if (startDot === -1) { startDot = i; } else if (preDotState !== 1) { preDotState = 1; } } else if (startDot !== -1) { // We saw a non-dot and non-path separator before our dot, so we should // have a good chance at having a non-empty extension preDotState = -1; } } if (end !== -1) { if ( startDot === -1 || // We saw a non-dot character immediately before the dot preDotState === 0 || // The (right-most) trimmed path component is exactly '..' (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) ) { ret.base = ret.name = StringPrototypeSlice(path, startPart, end); } else { ret.name = StringPrototypeSlice(path, startPart, startDot); ret.base = StringPrototypeSlice(path, startPart, end); ret.ext = StringPrototypeSlice(path, startDot, end); } } // If the directory is the root, use the entire root as the `dir` including // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the // trailing slash (`C:\abc\def` -> `C:\abc`). if (startPart > 0 && startPart !== rootEnd) { ret.dir = StringPrototypeSlice(path, 0, startPart - 1); } else { ret.dir = ret.root; } return ret; }, sep: "\\", delimiter: ";", }; // Node views a Windows working directory through POSIX eyes by converting // separators and truncating any drive indicator. function posixCwd() { if (!hostIsWindows) { return nativeCwd(); } const cwd = nativeCwd(); const forward = ArrayPrototypeJoin(splitOnChar(cwd, CHAR_BACKWARD_SLASH), "/"); return StringPrototypeSlice(forward, StringPrototypeIndexOf(forward, "/")); } const posix: PathApi = { resolve(...args) { let resolvedPath = ""; let resolvedAbsolute = false; for (let i = args.length - 1; i >= 0 && !resolvedAbsolute; i--) { const path = args[i]!; validateString(path, `paths[${i}]`); // Skip empty entries if (path.length === 0) { continue; } resolvedPath = `${path}/${resolvedPath}`; resolvedAbsolute = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; } if (!resolvedAbsolute) { const cwd = posixCwd(); resolvedPath = `${cwd}/${resolvedPath}`; resolvedAbsolute = StringPrototypeCharCodeAt(cwd, 0) === CHAR_FORWARD_SLASH; } // At this point the path should be resolved to a full absolute path, but // handle relative paths to be safe (might happen when the working // directory lookup fails, which surfaces as PermissionDenied instead). // Normalize the path resolvedPath = normalizeString( resolvedPath, !resolvedAbsolute, "/", isPosixPathSeparator, ); if (resolvedAbsolute) { return `/${resolvedPath}`; } return resolvedPath.length > 0 ? resolvedPath : "."; }, normalize(path) { validateString(path, "path"); if (path.length === 0) { return "."; } const isAbsolute = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; const trailingSeparator = StringPrototypeCharCodeAt(path, path.length - 1) === CHAR_FORWARD_SLASH; // Normalize the path path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator); if (path.length === 0) { if (isAbsolute) { return "/"; } return trailingSeparator ? "./" : "."; } if (trailingSeparator) { path += "/"; } return isAbsolute ? `/${path}` : path; }, isAbsolute(path) { validateString(path, "path"); return path.length > 0 && StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; }, join(...args) { if (args.length === 0) { return "."; } const path: string[] = []; for (let i = 0; i < args.length; ++i) { const arg = args[i]; validateString(arg, "path"); if (arg.length > 0) { ArrayPrototypePush(path, arg); } } if (path.length === 0) { return "."; } return posix.normalize(ArrayPrototypeJoin(path, "/")); }, relative(from, to) { validateString(from, "from"); validateString(to, "to"); if (from === to) { return ""; } // Trim leading forward slashes. from = posix.resolve(from); to = posix.resolve(to); if (from === to) { return ""; } const fromStart = 1; const fromEnd = from.length; const fromLen = fromEnd - fromStart; const toStart = 1; const toLen = to.length - toStart; // Compare paths to find the longest common path from root const length = (fromLen < toLen ? fromLen : toLen); let lastCommonSep = -1; let i = 0; for (; i < length; i++) { const fromCode = StringPrototypeCharCodeAt(from, fromStart + i); if (fromCode !== StringPrototypeCharCodeAt(to, toStart + i)) { break; } else if (fromCode === CHAR_FORWARD_SLASH) { lastCommonSep = i; } } if (i === length) { if (toLen > length) { if (StringPrototypeCharCodeAt(to, toStart + i) === CHAR_FORWARD_SLASH) { // We get here if `from` is the exact base path for `to`. // For example: from='/foo/bar'; to='/foo/bar/baz' return StringPrototypeSlice(to, toStart + i + 1); } if (i === 0) { // We get here if `from` is the root // For example: from='/'; to='/foo' return StringPrototypeSlice(to, toStart + i); } } else if (fromLen > length) { if ( StringPrototypeCharCodeAt(from, fromStart + i) === CHAR_FORWARD_SLASH ) { // We get here if `to` is the exact base path for `from`. // For example: from='/foo/bar/baz'; to='/foo/bar' lastCommonSep = i; } else if (i === 0) { // We get here if `to` is the root. // For example: from='/foo/bar'; to='/' lastCommonSep = 0; } } } let out = ""; // Generate the relative path based on the path difference between `to` // and `from`. for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { if ( i === fromEnd || StringPrototypeCharCodeAt(from, i) === CHAR_FORWARD_SLASH ) { out += out.length === 0 ? ".." : "/.."; } } // Lastly, append the rest of the destination (`to`) path that comes after // the common path parts. return `${out}${StringPrototypeSlice(to, toStart + lastCommonSep)}`; }, toNamespacedPath(path) { // Non-op on posix systems return path; }, dirname(path) { validateString(path, "path"); if (path.length === 0) { return "."; } const hasRoot = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; let end = -1; let matchedSlash = true; for (let i = path.length - 1; i >= 1; --i) { if (StringPrototypeCharCodeAt(path, i) === CHAR_FORWARD_SLASH) { if (!matchedSlash) { end = i; break; } } else { // We saw the first non-path separator matchedSlash = false; } } if (end === -1) { return hasRoot ? "/" : "."; } if (hasRoot && end === 1) { return "//"; } return StringPrototypeSlice(path, 0, end); }, basename(path, suffix) { if (suffix !== undefined) { validateString(suffix, "suffix"); } validateString(path, "path"); let start = 0; let end = -1; let matchedSlash = true; if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) { if (suffix === path) { return ""; } let extIdx = suffix.length - 1; let firstNonSlashEnd = -1; for (let i = path.length - 1; i >= 0; --i) { const code = StringPrototypeCharCodeAt(path, i); if (code === CHAR_FORWARD_SLASH) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { start = i + 1; break; } } else { if (firstNonSlashEnd === -1) { // We saw the first non-path separator, remember this index in case // we need it if the extension ends up not matching matchedSlash = false; firstNonSlashEnd = i + 1; } if (extIdx >= 0) { // Try to match the explicit extension if (code === StringPrototypeCharCodeAt(suffix, extIdx)) { if (--extIdx === -1) { // We matched the extension, so mark this as the end of our path // component end = i; } } else { // Extension does not match, so our result is the entire path // component extIdx = -1; end = firstNonSlashEnd; } } } } if (start === end) { end = firstNonSlashEnd; } else if (end === -1) { end = path.length; } return StringPrototypeSlice(path, start, end); } for (let i = path.length - 1; i >= 0; --i) { if (StringPrototypeCharCodeAt(path, i) === CHAR_FORWARD_SLASH) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { start = i + 1; break; } } else if (end === -1) { // We saw the first non-path separator, mark this as the end of our // path component matchedSlash = false; end = i + 1; } } if (end === -1) { return ""; } return StringPrototypeSlice(path, start, end); }, extname(path) { validateString(path, "path"); let startDot = -1; let startPart = 0; let end = -1; let matchedSlash = true; // Track the state of characters (if any) we see before our first dot and // after any path separator we find let preDotState = 0; for (let i = path.length - 1; i >= 0; --i) { const code = StringPrototypeCharCodeAt(path, i); if (code === CHAR_FORWARD_SLASH) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { // We saw the first non-path separator, mark this as the end of our // extension matchedSlash = false; end = i + 1; } if (code === CHAR_DOT) { // If this is our first dot, mark it as the start of our extension if (startDot === -1) { startDot = i; } else if (preDotState !== 1) { preDotState = 1; } } else if (startDot !== -1) { // We saw a non-dot and non-path separator before our dot, so we should // have a good chance at having a non-empty extension preDotState = -1; } } if ( startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot preDotState === 0 || // The (right-most) trimmed path component is exactly '..' (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) ) { return ""; } return StringPrototypeSlice(path, startDot, end); }, format(pathObject) { return _format("/", pathObject); }, parse(path) { validateString(path, "path"); const ret = { root: "", dir: "", base: "", ext: "", name: "" }; if (path.length === 0) { return ret; } const isAbsolute = StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; let start; if (isAbsolute) { ret.root = "/"; start = 1; } else { start = 0; } let startDot = -1; let startPart = 0; let end = -1; let matchedSlash = true; let i = path.length - 1; // Track the state of characters (if any) we see before our first dot and // after any path separator we find let preDotState = 0; // Get non-dir info for (; i >= start; --i) { const code = StringPrototypeCharCodeAt(path, i); if (code === CHAR_FORWARD_SLASH) { // If we reached a path separator that was not part of a set of path // separators at the end of the string, stop now if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) { // We saw the first non-path separator, mark this as the end of our // extension matchedSlash = false; end = i + 1; } if (code === CHAR_DOT) { // If this is our first dot, mark it as the start of our extension if (startDot === -1) { startDot = i; } else if (preDotState !== 1) { preDotState = 1; } } else if (startDot !== -1) { // We saw a non-dot and non-path separator before our dot, so we should // have a good chance at having a non-empty extension preDotState = -1; } } if (end !== -1) { const baseStart = startPart === 0 && isAbsolute ? 1 : startPart; if ( startDot === -1 || // We saw a non-dot character immediately before the dot preDotState === 0 || // The (right-most) trimmed path component is exactly '..' (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) ) { ret.base = ret.name = StringPrototypeSlice(path, baseStart, end); } else { ret.name = StringPrototypeSlice(path, baseStart, startDot); ret.base = StringPrototypeSlice(path, baseStart, end); ret.ext = StringPrototypeSlice(path, startDot, end); } } if (startPart > 0) { ret.dir = StringPrototypeSlice(path, 0, startPart - 1); } else if (isAbsolute) { ret.dir = "/"; } return ret; }, sep: "/", delimiter: ":", }; const posixBase = ObjectFreeze({ join: posix.join, resolve: posix.resolve, normalize: posix.normalize, relative: posix.relative, dirname: posix.dirname, basename: posix.basename, extname: posix.extname, isAbsolute: posix.isAbsolute, parse: posix.parse, format: posix.format, toNamespacedPath: posix.toNamespacedPath, sep: posix.sep, delimiter: posix.delimiter, }); const win32Base = ObjectFreeze({ join: win32.join, resolve: win32.resolve, normalize: win32.normalize, relative: win32.relative, dirname: win32.dirname, basename: win32.basename, extname: win32.extname, isAbsolute: win32.isAbsolute, parse: win32.parse, format: win32.format, toNamespacedPath: win32.toNamespacedPath, sep: win32.sep, delimiter: win32.delimiter, }); export { posixBase, win32Base }; export { splitOnChar, validateString };