Skip to content

Commit 7a8848d

Browse files
vivekfyiVivek Sonidaniel-lxs
authored
fix: use decodeURIComponent in openFile (#5504)
* fix: use decodeURIComponent in openFile * feat: add error handling for decodeURIComponent and tests - Added try-catch block around decodeURIComponent to handle invalid escape sequences - Falls back to original path if decoding fails - Added comprehensive unit tests for the openFile function - Tests cover invalid URI encoding, valid encoding, and various edge cases * fix: update test to handle dynamic workspace paths in CI * fix: handle Windows path separators in open-file tests --------- Co-authored-by: Vivek Soni <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent 5f08607 commit 7a8848d

File tree

2 files changed

+251
-1
lines changed

2 files changed

+251
-1
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import * as path from "path"
4+
import * as os from "os"
5+
import { openFile } from "../open-file"
6+
7+
// Mock vscode module
8+
vi.mock("vscode", () => ({
9+
Uri: {
10+
file: vi.fn((path: string) => ({ fsPath: path })),
11+
},
12+
workspace: {
13+
fs: {
14+
stat: vi.fn(),
15+
writeFile: vi.fn(),
16+
},
17+
openTextDocument: vi.fn(),
18+
},
19+
window: {
20+
showTextDocument: vi.fn(),
21+
showErrorMessage: vi.fn(),
22+
tabGroups: {
23+
all: [],
24+
},
25+
activeTextEditor: undefined,
26+
},
27+
commands: {
28+
executeCommand: vi.fn(),
29+
},
30+
FileType: {
31+
Directory: 2,
32+
File: 1,
33+
},
34+
Selection: vi.fn((startLine: number, startChar: number, endLine: number, endChar: number) => ({
35+
start: { line: startLine, character: startChar },
36+
end: { line: endLine, character: endChar },
37+
})),
38+
TabInputText: vi.fn(),
39+
}))
40+
41+
// Mock utils
42+
vi.mock("../../utils/path", () => {
43+
const nodePath = require("path")
44+
return {
45+
arePathsEqual: vi.fn((a: string, b: string) => a === b),
46+
getWorkspacePath: vi.fn(() => {
47+
// In tests, we need to return a consistent workspace path
48+
// The actual workspace is /Users/roocode/rc2 in local, but varies in CI
49+
const cwd = process.cwd()
50+
// If we're in the src directory, go up one level to get workspace root
51+
if (cwd.endsWith("/src")) {
52+
return nodePath.dirname(cwd)
53+
}
54+
return cwd
55+
}),
56+
}
57+
})
58+
59+
// Mock i18n
60+
vi.mock("../../i18n", () => ({
61+
t: vi.fn((key: string, params?: any) => {
62+
// Return the key without namespace prefix to match actual behavior
63+
if (key.startsWith("common:")) {
64+
return key.replace("common:", "")
65+
}
66+
return key
67+
}),
68+
}))
69+
70+
describe("openFile", () => {
71+
beforeEach(() => {
72+
vi.clearAllMocks()
73+
vi.spyOn(console, "warn").mockImplementation(() => {})
74+
})
75+
76+
afterEach(() => {
77+
vi.restoreAllMocks()
78+
})
79+
80+
describe("decodeURIComponent error handling", () => {
81+
it("should handle invalid URI encoding gracefully", async () => {
82+
const invalidPath = "test%ZZinvalid.txt" // Invalid percent encoding
83+
const mockDocument = { uri: { fsPath: invalidPath } }
84+
85+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
86+
type: vscode.FileType.File,
87+
ctime: 0,
88+
mtime: 0,
89+
size: 0,
90+
})
91+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
92+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
93+
94+
await openFile(invalidPath)
95+
96+
// Should log a warning about decode failure
97+
expect(console.warn).toHaveBeenCalledWith(
98+
"Failed to decode file path: URIError: URI malformed. Using original path.",
99+
)
100+
101+
// Should still attempt to open the file with the original path
102+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
103+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
104+
})
105+
106+
it("should successfully decode valid URI-encoded paths", async () => {
107+
const encodedPath = "./%5Btest%5D/file.txt" // [test] encoded
108+
const decodedPath = "./[test]/file.txt"
109+
const mockDocument = { uri: { fsPath: decodedPath } }
110+
111+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
112+
type: vscode.FileType.File,
113+
ctime: 0,
114+
mtime: 0,
115+
size: 0,
116+
})
117+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
118+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
119+
120+
await openFile(encodedPath)
121+
122+
// Should not log any warnings
123+
expect(console.warn).not.toHaveBeenCalled()
124+
125+
// Should use the decoded path - verify it contains the decoded brackets
126+
// On Windows, the path will include backslashes instead of forward slashes
127+
const expectedPathSegment = process.platform === "win32" ? "[test]\\file.txt" : "[test]/file.txt"
128+
expect(vscode.Uri.file).toHaveBeenCalledWith(expect.stringContaining(expectedPathSegment))
129+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
130+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
131+
})
132+
133+
it("should handle paths with special characters that need encoding", async () => {
134+
const pathWithSpecialChars = "./[brackets]/file with spaces.txt"
135+
const mockDocument = { uri: { fsPath: pathWithSpecialChars } }
136+
137+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
138+
type: vscode.FileType.File,
139+
ctime: 0,
140+
mtime: 0,
141+
size: 0,
142+
})
143+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
144+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
145+
146+
await openFile(pathWithSpecialChars)
147+
148+
// Should work without errors
149+
expect(console.warn).not.toHaveBeenCalled()
150+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
151+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
152+
})
153+
154+
it("should handle already decoded paths without double-decoding", async () => {
155+
const normalPath = "./normal/file.txt"
156+
const mockDocument = { uri: { fsPath: normalPath } }
157+
158+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
159+
type: vscode.FileType.File,
160+
ctime: 0,
161+
mtime: 0,
162+
size: 0,
163+
})
164+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDocument as any)
165+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
166+
167+
await openFile(normalPath)
168+
169+
// Should work without errors
170+
expect(console.warn).not.toHaveBeenCalled()
171+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
172+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
173+
})
174+
})
175+
176+
describe("error handling", () => {
177+
it("should show error message when file does not exist", async () => {
178+
const nonExistentPath = "./does/not/exist.txt"
179+
180+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found"))
181+
182+
await openFile(nonExistentPath)
183+
184+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file")
185+
})
186+
187+
it("should handle generic errors", async () => {
188+
const testPath = "./test.txt"
189+
190+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue("Not an Error object")
191+
192+
await openFile(testPath)
193+
194+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.could_not_open_file")
195+
})
196+
})
197+
198+
describe("directory handling", () => {
199+
it("should reveal directories in explorer", async () => {
200+
const dirPath = "./components"
201+
202+
vi.mocked(vscode.workspace.fs.stat).mockResolvedValue({
203+
type: vscode.FileType.Directory,
204+
ctime: 0,
205+
mtime: 0,
206+
size: 0,
207+
})
208+
209+
await openFile(dirPath)
210+
211+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
212+
"revealInExplorer",
213+
expect.objectContaining({ fsPath: expect.stringContaining("components") }),
214+
)
215+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("list.expand")
216+
expect(vscode.workspace.openTextDocument).not.toHaveBeenCalled()
217+
})
218+
})
219+
220+
describe("file creation", () => {
221+
it("should create new files when create option is true", async () => {
222+
const newFilePath = "./new/file.txt"
223+
const content = "Hello, world!"
224+
225+
vi.mocked(vscode.workspace.fs.stat).mockRejectedValue(new Error("File not found"))
226+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue({} as any)
227+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
228+
229+
await openFile(newFilePath, { create: true, content })
230+
231+
// On Windows, the path will include backslashes instead of forward slashes
232+
const expectedPathSegment = process.platform === "win32" ? "new\\file.txt" : "new/file.txt"
233+
expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(
234+
expect.objectContaining({ fsPath: expect.stringContaining(expectedPathSegment) }),
235+
Buffer.from(content, "utf8"),
236+
)
237+
expect(vscode.workspace.openTextDocument).toHaveBeenCalled()
238+
})
239+
})
240+
})

src/integrations/misc/open-file.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@ interface OpenFileOptions {
1212

1313
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
1414
try {
15+
// Store the original path for error messages before any modifications
16+
const originalFilePathForError = filePath
17+
18+
// Try to decode the URI component, but if it fails, use the original path
19+
try {
20+
filePath = decodeURIComponent(filePath)
21+
} catch (decodeError) {
22+
// If decoding fails (e.g., invalid escape sequences), continue with the original path
23+
console.warn(`Failed to decode file path: ${decodeError}. Using original path.`)
24+
}
25+
1526
const workspaceRoot = getWorkspacePath()
1627
const homeDir = os.homedir()
17-
const originalFilePathForError = filePath // Keep original for error messages
1828

1929
const attemptPaths: string[] = []
2030

0 commit comments

Comments
 (0)