( fs: FsOperations, filePath: string, )
| 136 | * @returns Object containing the resolved path and whether it was a symlink |
| 137 | */ |
| 138 | export function safeResolvePath( |
| 139 | fs: FsOperations, |
| 140 | filePath: string, |
| 141 | ): { resolvedPath: string; isSymlink: boolean; isCanonical: boolean } { |
| 142 | // Block UNC paths before any filesystem access to prevent network |
| 143 | // requests (DNS/SMB) during validation on Windows |
| 144 | if (filePath.startsWith('//') || filePath.startsWith('\\\\')) { |
| 145 | return { resolvedPath: filePath, isSymlink: false, isCanonical: false } |
| 146 | } |
| 147 | |
| 148 | try { |
| 149 | // Check for special file types (FIFOs, sockets, devices) before calling realpathSync. |
| 150 | // realpathSync can block on FIFOs waiting for a writer, causing hangs. |
| 151 | // If the file doesn't exist, lstatSync throws ENOENT which the catch |
| 152 | // below handles by returning the original path (allows file creation). |
| 153 | const stats = fs.lstatSync(filePath) |
| 154 | if ( |
| 155 | stats.isFIFO() || |
| 156 | stats.isSocket() || |
| 157 | stats.isCharacterDevice() || |
| 158 | stats.isBlockDevice() |
| 159 | ) { |
| 160 | return { resolvedPath: filePath, isSymlink: false, isCanonical: false } |
| 161 | } |
| 162 | |
| 163 | const resolvedPath = fs.realpathSync(filePath) |
| 164 | return { |
| 165 | resolvedPath, |
| 166 | isSymlink: resolvedPath !== filePath, |
| 167 | // realpathSync returned: resolvedPath is canonical (all symlinks in |
| 168 | // all path components resolved). Callers can skip further symlink |
| 169 | // resolution on this path. |
| 170 | isCanonical: true, |
| 171 | } |
| 172 | } catch (_error) { |
| 173 | // If lstat/realpath fails for any reason (ENOENT, broken symlink, |
| 174 | // EACCES, ELOOP, etc.), return the original path to allow operations |
| 175 | // to proceed |
| 176 | return { resolvedPath: filePath, isSymlink: false, isCanonical: false } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Check if a file path is a duplicate and should be skipped. |
no outgoing calls
no test coverage detected