diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts index 4ec96ce08e0..166145d8e1e 100644 --- a/packages/core/src/codewhispererChat/tools/fsRead.ts +++ b/packages/core/src/codewhispererChat/tools/fsRead.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import { getLogger } from '../../shared/logger/logger' import { readDirectoryRecursively } from '../../shared/utilities/workspaceUtils' import fs from '../../shared/fs/fs' -import { InvokeOutput, maxToolResponseSize, OutputKind } from './toolShared' +import { InvokeOutput, maxToolResponseSize, OutputKind, sanitizePath } from './toolShared' export interface FsReadParams { path: string @@ -14,8 +14,9 @@ export interface FsReadParams { } export class FsRead { - private readonly fsPath: string + private fsPath: string private readonly readRange?: number[] + private type?: boolean // true for file, false for directory private readonly logger = getLogger('fsRead') constructor(params: FsReadParams) { @@ -23,27 +24,39 @@ export class FsRead { this.readRange = params.readRange } - public async invoke(): Promise { + public async validate(): Promise { + this.logger.debug(`Validating fsPath: ${this.fsPath}`) + if (!this.fsPath || this.fsPath.trim().length === 0) { + throw new Error('Path cannot be empty.') + } + + const sanitized = sanitizePath(this.fsPath) + this.fsPath = sanitized + + const fileUri = vscode.Uri.file(this.fsPath) + let exists: boolean try { - const fileUri = vscode.Uri.file(this.fsPath) - let exists: boolean - try { - exists = await fs.exists(fileUri) - if (!exists) { - throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`) - } - } catch (err) { - throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`) + exists = await fs.exists(fileUri) + if (!exists) { + throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`) } + } catch (err) { + throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`) + } - const isFile = await fs.existsFile(fileUri) - const isDirectory = await fs.existsDir(fileUri) + this.type = await fs.existsFile(fileUri) + this.logger.debug(`Validation succeeded for path: ${this.fsPath}`) + } + + public async invoke(): Promise { + try { + const fileUri = vscode.Uri.file(this.fsPath) - if (isFile) { + if (this.type) { const fileContents = await this.readFile(fileUri) this.logger.info(`Read file: ${this.fsPath}, size: ${fileContents.length}`) return this.handleFileRange(fileContents) - } else if (isDirectory) { + } else if (!this.type) { const maxDepth = this.getDirectoryDepth() ?? 0 const listing = await readDirectoryRecursively(fileUri, maxDepth) return this.createOutput(listing.join('\n')) diff --git a/packages/core/src/codewhispererChat/tools/toolShared.ts b/packages/core/src/codewhispererChat/tools/toolShared.ts index b221173275b..92a53d3aef1 100644 --- a/packages/core/src/codewhispererChat/tools/toolShared.ts +++ b/packages/core/src/codewhispererChat/tools/toolShared.ts @@ -2,6 +2,10 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ + +import path from 'path' +import fs from '../../shared/fs/fs' + export const maxToolResponseSize = 30720 // 30KB export enum OutputKind { @@ -15,3 +19,16 @@ export interface InvokeOutput { content: string } } + +export function sanitizePath(inputPath: string): string { + let sanitized = inputPath.trim() + + if (sanitized.startsWith('~')) { + sanitized = path.join(fs.getUserHomeDir(), sanitized.slice(1)) + } + + if (!path.isAbsolute(sanitized)) { + sanitized = path.resolve(sanitized) + } + return sanitized +} diff --git a/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts index 9f45eb33c98..bd216a31b8a 100644 --- a/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts +++ b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts @@ -14,11 +14,17 @@ describe('FsRead Tool', () => { testFolder = await TestFolder.create() }) + it('throws if path is empty', async () => { + const fsRead = new FsRead({ path: '' }) + await assert.rejects(fsRead.validate(), /Path cannot be empty/i, 'Expected an error about empty path') + }) + it('reads entire file', async () => { const fileContent = 'Line 1\nLine 2\nLine 3' const filePath = await testFolder.write('fullFile.txt', fileContent) const fsRead = new FsRead({ path: filePath }) + await fsRead.validate() const result = await fsRead.invoke() assert.strictEqual(result.output.kind, 'text', 'Output kind should be "text"') @@ -30,6 +36,7 @@ describe('FsRead Tool', () => { const filePath = await testFolder.write('partialFile.txt', fileContent) const fsRead = new FsRead({ path: filePath, readRange: [2, 4] }) + await fsRead.validate() const result = await fsRead.invoke() assert.strictEqual(result.output.kind, 'text') @@ -42,6 +49,7 @@ describe('FsRead Tool', () => { await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB') const fsRead = new FsRead({ path: testFolder.path, readRange: [1] }) + await fsRead.validate() const result = await fsRead.invoke() const lines = result.output.content.split('\n') @@ -57,7 +65,7 @@ describe('FsRead Tool', () => { const fsRead = new FsRead({ path: missingPath }) await assert.rejects( - fsRead.invoke(), + fsRead.validate(), /does not exist or cannot be accessed/i, 'Expected an error indicating the path does not exist' ) @@ -68,6 +76,7 @@ describe('FsRead Tool', () => { const bigFilePath = await testFolder.write('bigFile.txt', bigContent) const fsRead = new FsRead({ path: bigFilePath }) + await fsRead.validate() await assert.rejects( fsRead.invoke(), @@ -80,8 +89,34 @@ describe('FsRead Tool', () => { const filePath = await testFolder.write('rangeTest.txt', '1\n2\n3') const fsRead = new FsRead({ path: filePath, readRange: [3, 2] }) + await fsRead.validate() const result = await fsRead.invoke() assert.strictEqual(result.output.kind, 'text') assert.strictEqual(result.output.content, '') }) + + it('expands ~ path', async () => { + const fsRead = new FsRead({ path: '~' }) + await fsRead.validate() + const result = await fsRead.invoke() + + assert.strictEqual(result.output.kind, 'text') + assert.ok(result.output.content.length > 0) + }) + + it('resolves relative path', async () => { + await testFolder.mkdir('relTest') + const filePath = path.join('relTest', 'relFile.txt') + const content = 'Hello from a relative file!' + await testFolder.write(filePath, content) + + const relativePath = path.relative(process.cwd(), path.join(testFolder.path, filePath)) + + const fsRead = new FsRead({ path: relativePath }) + await fsRead.validate() + const result = await fsRead.invoke() + + assert.strictEqual(result.output.kind, 'text') + assert.strictEqual(result.output.content, content) + }) })