| |
| |
| |
| |
| |
|
|
| import * as path from 'node:path'; |
| import * as fs from 'node:fs/promises'; |
| import { validatePath } from './path-validator.js'; |
| import { type Config } from '../config/config.js'; |
| import { isNodeError, getErrorMessage } from './errors.js'; |
|
|
| export interface ResolvedAtCommandPath { |
| absolutePath: string; |
| relativePath: string; |
| stats: { |
| isDirectory(): boolean; |
| isFile(): boolean; |
| }; |
| } |
|
|
| |
| |
| |
| export type ResolveAtCommandPathResult = |
| | { status: 'resolved'; resolved: ResolvedAtCommandPath } |
| | { status: 'unauthorized'; absolutePath: string; error: string } |
| | { status: 'invalid'; error: string } |
| | { status: 'not_found' }; |
|
|
| |
| |
| |
| |
| export async function resolveAtCommandPath( |
| pathName: string, |
| config: Config, |
| onDebugMessage: (msg: string) => void = () => {}, |
| ): Promise<ResolveAtCommandPathResult> { |
| const pathValidation = validatePath(pathName); |
| if (!pathValidation.isValid) { |
| |
| const extractedPath = tryExtractPath(pathName); |
| if (extractedPath && extractedPath !== pathName) { |
| onDebugMessage( |
| `Identified invalid path fragment, attempting to extract path: "${extractedPath}" from "${pathName}"`, |
| ); |
| |
| return resolveAtCommandPath(extractedPath, config, onDebugMessage); |
| } |
|
|
| onDebugMessage( |
| `Skipping invalid path in @-command: ${pathName}. Reason: ${pathValidation.error}`, |
| ); |
| return { status: 'invalid', error: pathValidation.error! }; |
| } |
|
|
| const workspaceDirs = config.getWorkspaceContext().getDirectories(); |
|
|
| |
| if (path.isAbsolute(pathName)) { |
| const validationError = config.validatePathAccess(pathName, 'read'); |
| if (validationError) { |
| onDebugMessage( |
| `Skipping unauthorized absolute path: ${pathName}. Reason: ${validationError}`, |
| ); |
| return { |
| status: 'unauthorized', |
| absolutePath: pathName, |
| error: validationError, |
| }; |
| } |
|
|
| try { |
| const stats = await fs.stat(pathName); |
| |
| let relativePath = pathName; |
| for (const dir of workspaceDirs) { |
| const rel = path.relative(dir, pathName); |
| if (!rel.startsWith('..') && !path.isAbsolute(rel)) { |
| relativePath = rel; |
| break; |
| } |
| } |
|
|
| return { |
| status: 'resolved', |
| resolved: { |
| absolutePath: pathName, |
| relativePath, |
| stats, |
| }, |
| }; |
| } catch (error) { |
| if (isNodeError(error) && error.code === 'ENOENT') { |
| return { status: 'not_found' }; |
| } |
| onDebugMessage( |
| `Unexpected error stating path ${pathName}: ${getErrorMessage(error)}`, |
| ); |
| return { status: 'not_found' }; |
| } |
| } |
|
|
| |
| let lastUnauthorized: { absolutePath: string; error: string } | null = null; |
|
|
| for (const dir of workspaceDirs) { |
| const absolutePath = path.resolve(dir, pathName); |
|
|
| |
| const validationError = config.validatePathAccess(absolutePath, 'read'); |
| if (validationError) { |
| onDebugMessage( |
| `Skipping unauthorized path: ${absolutePath}. Reason: ${validationError}`, |
| ); |
| |
| lastUnauthorized = { absolutePath, error: validationError }; |
| continue; |
| } |
|
|
| try { |
| const stats = await fs.stat(absolutePath); |
| return { |
| status: 'resolved', |
| resolved: { |
| absolutePath, |
| relativePath: pathName, |
| stats, |
| }, |
| }; |
| } catch (error) { |
| if (isNodeError(error) && error.code === 'ENOENT') { |
| |
| continue; |
| } |
| onDebugMessage( |
| `Unexpected error stating path ${absolutePath}: ${getErrorMessage(error)}`, |
| ); |
| } |
| } |
|
|
| if (lastUnauthorized) { |
| return { status: 'unauthorized', ...lastUnauthorized }; |
| } |
|
|
| return { status: 'not_found' }; |
| } |
|
|
| |
| |
| |
| function tryExtractPath(noisyString: string): string | null { |
| |
| const segments = noisyString.split(/\s+/); |
|
|
| for (const segment of segments) { |
| |
| |
| let segmentToClean = segment; |
| const wrappers = [ |
| '(', |
| ')', |
| '[', |
| ']', |
| '{', |
| '}', |
| '"', |
| "'", |
| ',', |
| ';', |
| '!', |
| '.', |
| ]; |
|
|
| let wasStripped = true; |
| while (wasStripped && segmentToClean.length > 0) { |
| wasStripped = false; |
| const firstChar = segmentToClean[0]; |
| const lastChar = segmentToClean[segmentToClean.length - 1]; |
|
|
| |
| if (wrappers.includes(firstChar)) { |
| segmentToClean = segmentToClean.slice(1); |
| wasStripped = true; |
| } else if (wrappers.includes(lastChar)) { |
| segmentToClean = segmentToClean.slice(0, -1); |
| wasStripped = true; |
| } |
| } |
|
|
| if (segmentToClean.length === 0) continue; |
|
|
| |
| |
| const lineMatch = segmentToClean.match(/^(.+?):(\d+)(?::\d+)?/); |
| const pathOnly = lineMatch ? lineMatch[1] : segmentToClean; |
|
|
| |
| |
| |
| if (validatePath(pathOnly).isValid) { |
| |
| if ( |
| pathOnly.includes('/') || |
| pathOnly.includes('\\') || |
| pathOnly.includes('.') |
| ) { |
| return pathOnly; |
| } |
| } |
| } |
|
|
| return null; |
| } |
|
|