source dump of claude code
at main 155 lines 5.7 kB view raw
1import { homedir } from 'os' 2import { dirname, isAbsolute, join, normalize, relative, resolve } from 'path' 3import { getCwd } from './cwd.js' 4import { getFsImplementation } from './fsOperations.js' 5import { getPlatform } from './platform.js' 6import { posixPathToWindowsPath } from './windowsPaths.js' 7 8/** 9 * Expands a path that may contain tilde notation (~) to an absolute path. 10 * 11 * On Windows, POSIX-style paths (e.g., `/c/Users/...`) are automatically converted 12 * to Windows format (e.g., `C:\Users\...`). The function always returns paths in 13 * the native format for the current platform. 14 * 15 * @param path - The path to expand, may contain: 16 * - `~` - expands to user's home directory 17 * - `~/path` - expands to path within user's home directory 18 * - absolute paths - returned normalized 19 * - relative paths - resolved relative to baseDir 20 * - POSIX paths on Windows - converted to Windows format 21 * @param baseDir - The base directory for resolving relative paths (defaults to current working directory) 22 * @returns The expanded absolute path in the native format for the current platform 23 * 24 * @throws {Error} If path is invalid 25 * 26 * @example 27 * expandPath('~') // '/home/user' 28 * expandPath('~/Documents') // '/home/user/Documents' 29 * expandPath('./src', '/project') // '/project/src' 30 * expandPath('/absolute/path') // '/absolute/path' 31 */ 32export function expandPath(path: string, baseDir?: string): string { 33 // Set default baseDir to getCwd() if not provided 34 const actualBaseDir = baseDir ?? getCwd() ?? getFsImplementation().cwd() 35 36 // Input validation 37 if (typeof path !== 'string') { 38 throw new TypeError(`Path must be a string, received ${typeof path}`) 39 } 40 41 if (typeof actualBaseDir !== 'string') { 42 throw new TypeError( 43 `Base directory must be a string, received ${typeof actualBaseDir}`, 44 ) 45 } 46 47 // Security: Check for null bytes 48 if (path.includes('\0') || actualBaseDir.includes('\0')) { 49 throw new Error('Path contains null bytes') 50 } 51 52 // Handle empty or whitespace-only paths 53 const trimmedPath = path.trim() 54 if (!trimmedPath) { 55 return normalize(actualBaseDir).normalize('NFC') 56 } 57 58 // Handle home directory notation 59 if (trimmedPath === '~') { 60 return homedir().normalize('NFC') 61 } 62 63 if (trimmedPath.startsWith('~/')) { 64 return join(homedir(), trimmedPath.slice(2)).normalize('NFC') 65 } 66 67 // On Windows, convert POSIX-style paths (e.g., /c/Users/...) to Windows format 68 let processedPath = trimmedPath 69 if (getPlatform() === 'windows' && trimmedPath.match(/^\/[a-z]\//i)) { 70 try { 71 processedPath = posixPathToWindowsPath(trimmedPath) 72 } catch { 73 // If conversion fails, use original path 74 processedPath = trimmedPath 75 } 76 } 77 78 // Handle absolute paths 79 if (isAbsolute(processedPath)) { 80 return normalize(processedPath).normalize('NFC') 81 } 82 83 // Handle relative paths 84 return resolve(actualBaseDir, processedPath).normalize('NFC') 85} 86 87/** 88 * Converts an absolute path to a relative path from cwd, to save tokens in 89 * tool output. If the path is outside cwd (relative path would start with ..), 90 * returns the absolute path unchanged so it stays unambiguous. 91 * 92 * @param absolutePath - The absolute path to relativize 93 * @returns Relative path if under cwd, otherwise the original absolute path 94 */ 95export function toRelativePath(absolutePath: string): string { 96 const relativePath = relative(getCwd(), absolutePath) 97 // If the relative path would go outside cwd (starts with ..), keep absolute 98 return relativePath.startsWith('..') ? absolutePath : relativePath 99} 100 101/** 102 * Gets the directory path for a given file or directory path. 103 * If the path is a directory, returns the path itself. 104 * If the path is a file or doesn't exist, returns the parent directory. 105 * 106 * @param path - The file or directory path 107 * @returns The directory path 108 */ 109export function getDirectoryForPath(path: string): string { 110 const absolutePath = expandPath(path) 111 // SECURITY: Skip filesystem operations for UNC paths to prevent NTLM credential leaks. 112 if (absolutePath.startsWith('\\\\') || absolutePath.startsWith('//')) { 113 return dirname(absolutePath) 114 } 115 try { 116 const stats = getFsImplementation().statSync(absolutePath) 117 if (stats.isDirectory()) { 118 return absolutePath 119 } 120 } catch { 121 // Path doesn't exist or can't be accessed 122 } 123 // If it's not a directory or doesn't exist, return the parent directory 124 return dirname(absolutePath) 125} 126 127/** 128 * Checks if a path contains directory traversal patterns that navigate to parent directories. 129 * 130 * @param path - The path to check for traversal patterns 131 * @returns true if the path contains traversal (e.g., '../', '..\', or ends with '..') 132 */ 133export function containsPathTraversal(path: string): boolean { 134 return /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path) 135} 136 137// Re-export from the shared zero-dep source. 138export { sanitizePath } from './sessionStoragePortable.js' 139 140/** 141 * Normalizes a path for use as a JSON config key. 142 * On Windows, paths can have inconsistent separators (C:\path vs C:/path) 143 * depending on whether they come from git, Node.js APIs, or user input. 144 * This normalizes to forward slashes for consistent JSON serialization. 145 * 146 * @param path - The path to normalize 147 * @returns The normalized path with consistent forward slashes 148 */ 149export function normalizePathForConfigKey(path: string): string { 150 // First use Node's normalize to resolve . and .. segments 151 const normalized = normalize(path) 152 // Then convert all backslashes to forward slashes for consistent JSON keys 153 // This is safe because forward slashes work in Windows paths for most operations 154 return normalized.replace(/\\/g, '/') 155}