import { validateString } from "./util.mjs"; const CHAR_DOT = 46; /* . */ const CHAR_FORWARD_SLASH = 47; /* / */ const CHAR_BACKWARD_SLASH = 92; /* \ */ const CHAR_COLON = 58; /* : */ const CHAR_UPPERCASE_A = 65; /* A */ const CHAR_UPPERCASE_Z = 90; /* Z */ const CHAR_LOWERCASE_A = 97; /* a */ const CHAR_LOWERCASE_Z = 122; /* z */ function isPathSeparatorWin(code) { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; } function isWindowsDeviceRoot(code) { return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z); } const _isWin32 = typeof process !== 'undefined' && process.platform === 'win32'; /** * Windows variant of `resolve()`. Mirrors Node's `path.win32.resolve` * semantics enough for the WASI shim's needs (drive-letter absolutes * + UNC paths + per-drive cwd lookups via `process.env`/`process.cwd`). * * Why this is needed: on Windows, `FileDescriptor.realPath` is the * host realpath returned by `fs.realpathSync(realPath, 'utf8')` — the * backslash form `D:\…`. The POSIX-only `resolveImpl` below only * treats `/` as a separator, reads `D` as non-`/`, decides realPath * is "relative", and produces a garbage joined path. Downstream * `fs.openSync(garbage)` then returns `EINVAL` and the WASI caller * sees a permanent failure. This function gives us the correct * Windows-style resolution without taking a Node-only `path` import * (which would break browser bundles of this package). * * Cribbed from Node's `lib/path.js` `win32.resolve()` (MIT-licensed), * trimmed to what WASI realpath resolution actually exercises. */ function resolveWin32(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, 'path'); if (path.length === 0) continue; } else if (resolvedDevice.length === 0) { path = (typeof process !== 'undefined' && typeof process.cwd === 'function') ? process.cwd() : ''; } else { // Look up per-drive cwd via the `=X:` env var convention; fall back // to the global cwd if absent. const envKey = `=${resolvedDevice}`; const env = (typeof process !== 'undefined') ? process.env : undefined; path = (env && typeof env[envKey] === 'string') ? env[envKey] : (typeof process !== 'undefined' && typeof process.cwd === 'function') ? process.cwd() : ''; if (path === undefined || (path.slice(0, 2).toLowerCase() !== resolvedDevice.toLowerCase() && path.charCodeAt(2) === CHAR_BACKWARD_SLASH)) { path = `${resolvedDevice}\\`; } } const len = path.length; let rootEnd = 0; let device = ''; let isAbsolute = false; const code = path.charCodeAt(0); if (len === 1) { if (isPathSeparatorWin(code)) { rootEnd = 1; isAbsolute = true; } } else if (isPathSeparatorWin(code)) { isAbsolute = true; if (isPathSeparatorWin(path.charCodeAt(1))) { // UNC path: `\\server\share\…` let j = 2; let last = j; while (j < len && !isPathSeparatorWin(path.charCodeAt(j))) j++; if (j < len && j !== last) { const firstPart = path.slice(last, j); last = j; while (j < len && isPathSeparatorWin(path.charCodeAt(j))) j++; if (j < len && j !== last) { last = j; while (j < len && !isPathSeparatorWin(path.charCodeAt(j))) j++; if (j === len || j !== last) { device = `\\\\${firstPart}\\${path.slice(last, j)}`; rootEnd = j; } } } } else { rootEnd = 1; } } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) { device = path.slice(0, 2); rootEnd = 2; if (len > 2 && isPathSeparatorWin(path.charCodeAt(2))) { isAbsolute = true; rootEnd = 3; } } if (device.length > 0) { if (resolvedDevice.length > 0) { if (device.toLowerCase() !== resolvedDevice.toLowerCase()) continue; } else { resolvedDevice = device; } } if (resolvedAbsolute) { if (resolvedDevice.length > 0) break; } else { resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`; resolvedAbsolute = isAbsolute; if (isAbsolute && resolvedDevice.length > 0) break; } } resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute, '\\', isPathSeparatorWin); return resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail || '.'; } function isPosixPathSeparator(code) { return code === CHAR_FORWARD_SLASH; } function normalizeString(path, allowAboveRoot, separator, isPathSeparator) { 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 = path.charCodeAt(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 || res.charCodeAt(res.length - 1) !== CHAR_DOT || res.charCodeAt(res.length - 2) !== CHAR_DOT) { if (res.length > 2) { const lastSlashIndex = res.indexOf(separator); if (lastSlashIndex === -1) { res = ''; lastSegmentLength = 0; } else { res = res.slice(0, lastSlashIndex); lastSegmentLength = res.length - 1 - res.indexOf(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}${path.slice(lastSlash + 1, i)}`; } else { res = path.slice(lastSlash + 1, i); } lastSegmentLength = i - lastSlash - 1; } lastSlash = i; dots = 0; } else if (code === CHAR_DOT && dots !== -1) { ++dots; } else { dots = -1; } } return res; } export function resolve(...args) { // On Windows, host paths are `D:\…` style. The POSIX-only resolver // below treats `D` as a non-`/` character and produces garbage; route // to the Windows-aware variant. POSIX hosts (Linux/macOS/browser) // run the original code path unchanged — `_isWin32` is constant-folded // away on those targets. if (_isWin32) return resolveWin32(args); let resolvedPath = ''; let resolvedAbsolute = false; for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { const path = i >= 0 ? args[i] : '/'; validateString(path, 'path'); // Skip empty entries if (path.length === 0) { continue; } resolvedPath = `${path}/${resolvedPath}`; resolvedAbsolute = path.charCodeAt(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 process.cwd() fails) // Normalize the path resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/', isPosixPathSeparator); if (resolvedAbsolute) { return `/${resolvedPath}`; } return resolvedPath.length > 0 ? resolvedPath : '.'; } export function relative(from, to) { validateString(from, 'from'); validateString(to, 'to'); if (from === to) return ''; // Trim leading forward slashes. from = resolve(from); to = 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 = from.charCodeAt(fromStart + i); if (fromCode !== to.charCodeAt(toStart + i)) { break; } else if (fromCode === CHAR_FORWARD_SLASH) { lastCommonSep = i; } } if (i === length) { if (toLen > length) { if (to.charCodeAt(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 to.slice(toStart + i + 1); } if (i === 0) { // We get here if `from` is the root // For example: from='/'; to='/foo' return to.slice(toStart + i); } } else if (fromLen > length) { if (from.charCodeAt(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 || from.charCodeAt(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}${to.slice(toStart + lastCommonSep)}`; }