(projectRoot: string, relativePath: string)
| 272 | * the path escapes the root by traversal or symlink. |
| 273 | */ |
| 274 | export function resolveProjectFile(projectRoot: string, relativePath: string): string { |
| 275 | if (typeof relativePath !== 'string' || relativePath.trim() === '') { |
| 276 | throw new PathRefusalError('No file path was given.'); |
| 277 | } |
| 278 | const decoded = decodePath(relativePath); |
| 279 | if (decoded === null) { |
| 280 | throw new PathRefusalError(`Refusing to read an unusable path: ${relativePath}`); |
| 281 | } |
| 282 | |
| 283 | // Sensitive-directory refusal, same list the MCP entry points use. Checked on |
| 284 | // the ROOT rather than the leaf: a root of `/etc` makes every path under it |
| 285 | // sensitive, and a leaf check would have to enumerate the world. |
| 286 | const rootError = validateProjectPath(projectRoot); |
| 287 | if (rootError) throw new PathRefusalError(rootError); |
| 288 | |
| 289 | if (path.isAbsolute(decoded)) { |
| 290 | throw new PathRefusalError(`Refusing to read an absolute path: ${decoded}`); |
| 291 | } |
| 292 | |
| 293 | const absolute = validatePathWithinRoot(projectRoot, decoded); |
| 294 | if (!absolute) { |
| 295 | throw new PathRefusalError(`Refusing to read a path outside the project: ${decoded}`); |
| 296 | } |
| 297 | return absolute; |
| 298 | } |
no test coverage detected