(filePath: string, projectRoot: string)
| 20 | * - `/abs/path/outside/project/file.tsx` |
| 21 | */ |
| 22 | export function resolveProjectFilePath(filePath: string, projectRoot: string): string | null { |
| 23 | const normalizedRoot = path.resolve(projectRoot); |
| 24 | const incomingPath = filePath.trim(); |
| 25 | if (!incomingPath) return null; |
| 26 | |
| 27 | if (path.isAbsolute(incomingPath)) { |
| 28 | const absoluteCandidate = path.resolve(incomingPath); |
| 29 | if (isWithinProjectRoot(absoluteCandidate, normalizedRoot)) { |
| 30 | return absoluteCandidate; |
| 31 | } |
| 32 | |
| 33 | // If the absolute path exists outside the project, treat it as a real |
| 34 | // filesystem absolute and reject it. Only reinterpret missing leading-slash |
| 35 | // paths like "/src/App.tsx" as project-root-relative source paths. |
| 36 | if (fs.existsSync(absoluteCandidate)) { |
| 37 | return null; |
| 38 | } |
| 39 | |
| 40 | const projectRelativeCandidate = path.resolve( |
| 41 | normalizedRoot, |
| 42 | incomingPath.replace(/^[/\\]+/, ""), |
| 43 | ); |
| 44 | return isWithinProjectRoot(projectRelativeCandidate, normalizedRoot) |
| 45 | ? projectRelativeCandidate |
| 46 | : null; |
| 47 | } |
| 48 | |
| 49 | const relativeCandidate = path.resolve(normalizedRoot, incomingPath); |
| 50 | return isWithinProjectRoot(relativeCandidate, normalizedRoot) |
| 51 | ? relativeCandidate |
| 52 | : null; |
| 53 | } |
| 54 | |
| 55 | export function isProjectFilePathSafe(filePath: string, projectRoot: string): boolean { |
| 56 | return resolveProjectFilePath(filePath, projectRoot) !== null; |
no test coverage detected