UNPKG

@zenfs/core

Version:

A filesystem, anywhere

404 lines (403 loc) 13.7 kB
// SPDX-License-Identifier: LGPL-3.0-or-later import { contextOf } from './internal/contexts.js'; import { globToRegex } from './utils.js'; export const sep = '/'; function validateObject(str, name) { if (typeof str != 'object') { throw new TypeError(`"${name}" is not an object`); } } // Resolves . and .. elements in a path with directory names export function normalizeString(path, allowAboveRoot) { let res = ''; let lastSegmentLength = 0; let lastSlash = -1; let dots = 0; let char = '\x00'; for (let i = 0; i <= path.length; ++i) { if (i < path.length) { char = path[i]; } else if (char == '/') { break; } else { char = '/'; } if (char == '/') { if (lastSlash === i - 1 || dots === 1) { // NOOP } else if (dots === 2) { if (res.length < 2 || lastSegmentLength !== 2 || res.at(-1) !== '.' || res.at(-2) !== '.') { if (res.length > 2) { const lastSlashIndex = res.lastIndexOf('/'); if (lastSlashIndex === -1) { res = ''; lastSegmentLength = 0; } else { res = res.slice(0, lastSlashIndex); lastSegmentLength = res.length - 1 - res.lastIndexOf('/'); } lastSlash = i; dots = 0; continue; } else if (res.length !== 0) { res = ''; lastSegmentLength = 0; lastSlash = i; dots = 0; continue; } } if (allowAboveRoot) { res += res.length > 0 ? '/..' : '..'; lastSegmentLength = 2; } } else { if (res.length > 0) res += '/' + path.slice(lastSlash + 1, i); else res = path.slice(lastSlash + 1, i); lastSegmentLength = i - lastSlash - 1; } lastSlash = i; dots = 0; } else if (char === '.' && dots !== -1) { ++dots; } else { dots = -1; } } return res; } export function formatExt(ext) { return ext ? `${ext[0] === '.' ? '' : '.'}${ext}` : ''; } export function resolve(...parts) { let resolved = ''; for (const part of [...parts.reverse(), contextOf(this).pwd]) { if (!part?.length) continue; resolved = `${part}/${resolved}`; if (part.startsWith('/')) { break; } } const absolute = resolved.startsWith('/'); // At this point the path should be resolved to a full absolute path, but // handle relative paths to be safe (might happen when cwd fails) // Normalize the path resolved = normalizeString(resolved, !absolute); if (absolute) { return `/${resolved}`; } return resolved.length ? resolved : '/'; } export function normalize(path) { if (!path.length) return '.'; const isAbsolute = path[0] === '/'; const trailingSeparator = path.at(-1) === '/'; // Normalize the path path = normalizeString(path, !isAbsolute); if (!path.length) { if (isAbsolute) return '/'; return trailingSeparator ? './' : '.'; } if (trailingSeparator) path += '/'; return isAbsolute ? `/${path}` : path; } export function isAbsolute(path) { return path.startsWith('/'); } export function join(...parts) { if (!parts.length) return '.'; const joined = parts.filter(p => p).join('/'); if (!joined?.length) return '.'; return normalize(joined); } export function relative(from, to) { if (from === to) return ''; // Trim leading forward slashes. from = resolve.call(this, from); to = resolve.call(this, 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[fromStart + i]; if (fromCode !== to[toStart + i]) break; else if (fromCode === '/') lastCommonSep = i; } if (i === length) { if (toLen > length) { if (to[toStart + i] === '/') { // 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[fromStart + i] === '/') { // 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[i] === '/') { 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)}`; } export function dirname(path) { if (path.length === 0) return '.'; const hasRoot = path[0] === '/'; let end = -1; let matchedSlash = true; for (let i = path.length - 1; i >= 1; --i) { if (path[i] === '/') { 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 path.slice(0, end); } export function basename(path, suffix) { 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) { if (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 (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 (path[i] === 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 path.slice(start, end); } for (let i = path.length - 1; i >= 0; --i) { if (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 path.slice(start, end); } export function extname(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) { if (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) { 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 (path[i] === '.') { // 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 path.slice(startDot, end); } export function format(pathObject) { 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}/${base}`; } export function parse(path) { const isAbsolute = path[0] === '/'; const ret = { root: isAbsolute ? '/' : '', dir: '', base: '', ext: '', name: '' }; if (path.length === 0) return ret; const start = isAbsolute ? 1 : 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) { if (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) { 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 (path[i] === '.') { // 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 start = 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 = path.slice(start, end); } else { ret.name = path.slice(start, startDot); ret.base = path.slice(start, end); ret.ext = path.slice(startDot, end); } } if (startPart > 0) ret.dir = path.slice(0, startPart - 1); else if (isAbsolute) ret.dir = '/'; return ret; } export function matchesGlob(pattern, str) { return globToRegex(pattern).test(str); }