Skip to content

Commit 7060687

Browse files
authored
Merge pull request #8965 from continuedev/dallin/path-uri-cli
fix(cli): path to uri and vice versa conversion
2 parents 61f0ba0 + 6419b44 commit 7060687

File tree

2 files changed

+276
-2
lines changed

2 files changed

+276
-2
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { platform } from "os";
2+
import { normalize, resolve } from "path";
3+
4+
import { describe, expect, it } from "vitest";
5+
6+
import { pathToUri, slugToUri, uriToPath, uriToSlug } from "./uriUtils.js";
7+
8+
describe("uriUtils", () => {
9+
describe("pathToUri", () => {
10+
it("should convert Unix-style absolute paths to file URIs", () => {
11+
const unixPath = "/home/user/documents/file.txt";
12+
const result = pathToUri(unixPath);
13+
expect(result).toMatch(/^file:\/\/\//);
14+
expect(result).toContain("file.txt");
15+
});
16+
17+
it("should convert Unix-style relative paths to file URIs", () => {
18+
const relativePath = "./documents/file.txt";
19+
const result = pathToUri(relativePath);
20+
expect(result).toMatch(/^file:\/\/\//);
21+
expect(result).toContain("file.txt");
22+
});
23+
24+
it("should handle Windows-style paths with drive letters", () => {
25+
const windowsPath = "C:\\Users\\user\\Documents\\file.txt";
26+
const result = pathToUri(windowsPath);
27+
expect(result).toMatch(/^file:\/\/\//);
28+
expect(result).toContain("file.txt");
29+
// Windows paths should be converted to forward slashes in URI
30+
if (platform() === "win32") {
31+
expect(result).toContain("C:");
32+
}
33+
});
34+
35+
it("should handle Windows UNC paths", () => {
36+
const uncPath = "\\\\server\\share\\folder\\file.txt";
37+
const result = pathToUri(uncPath);
38+
// UNC paths produce file://server/share/... format (two slashes, not three)
39+
expect(result).toMatch(/^file:\/\//);
40+
expect(result).toContain("file.txt");
41+
});
42+
43+
it("should handle paths with special characters", () => {
44+
const pathWithSpaces = "/home/user/my documents/file with spaces.txt";
45+
const result = pathToUri(pathWithSpaces);
46+
expect(result).toMatch(/^file:\/\/\//);
47+
// Spaces should be properly encoded
48+
expect(result).toContain("file%20with%20spaces.txt");
49+
});
50+
51+
it("should handle paths with Unicode characters", () => {
52+
const unicodePath = "/home/user/文档/файл.txt";
53+
const result = pathToUri(unicodePath);
54+
expect(result).toMatch(/^file:\/\/\//);
55+
// Should handle Unicode properly
56+
expect(result).toContain("%E6%96%87%E6%A1%A3"); // 文档 encoded
57+
});
58+
59+
it("should normalize mixed path separators", () => {
60+
const mixedPath = "/home/user\\documents/file.txt";
61+
const result = pathToUri(mixedPath);
62+
expect(result).toMatch(/^file:\/\/\//);
63+
expect(result).toContain("file.txt");
64+
});
65+
66+
it("should handle empty string", () => {
67+
const result = pathToUri("");
68+
expect(result).toMatch(/^file:\/\/\//);
69+
});
70+
71+
it("should handle current directory", () => {
72+
const result = pathToUri(".");
73+
expect(result).toMatch(/^file:\/\/\//);
74+
});
75+
});
76+
77+
describe("uriToPath", () => {
78+
it("should convert file URIs to Unix-style paths", () => {
79+
const uri = "file:///home/user/documents/file.txt";
80+
const result = uriToPath(uri);
81+
// On Windows, Unix-style URIs without a drive letter are invalid
82+
if (platform() === "win32") {
83+
expect(result).toBeNull();
84+
} else {
85+
expect(result).toBeTruthy();
86+
expect(result).toContain("file.txt");
87+
expect(result).toBe("/home/user/documents/file.txt");
88+
}
89+
});
90+
91+
it("should convert Windows file URIs to Windows paths", () => {
92+
const uri = "file:///C:/Users/user/Documents/file.txt";
93+
const result = uriToPath(uri);
94+
expect(result).toBeTruthy();
95+
expect(result).toContain("file.txt");
96+
if (platform() === "win32") {
97+
expect(result).toContain("C:");
98+
// fileURLToPath returns paths with single backslashes as separators
99+
expect(result).toContain("\\");
100+
}
101+
});
102+
103+
it("should handle URIs with encoded special characters", () => {
104+
const uri = "file:///home/user/my%20documents/file%20with%20spaces.txt";
105+
const result = uriToPath(uri);
106+
// Note: This test may fail on Windows if the URI lacks a drive letter
107+
if (result) {
108+
expect(result).toContain("my documents");
109+
expect(result).toContain("file with spaces.txt");
110+
}
111+
});
112+
113+
it("should handle URIs with Unicode characters", () => {
114+
const uri = "file:///home/user/%E6%96%87%E6%A1%A3/%E6%96%87%E4%BB%B6.txt";
115+
const result = uriToPath(uri);
116+
// Note: This test may fail on Windows if the URI lacks a drive letter
117+
if (result) {
118+
expect(result).toContain("文档");
119+
expect(result).toContain("文件.txt");
120+
}
121+
});
122+
123+
it("should return null for non-file URIs", () => {
124+
const httpUri = "http://example.com/file.txt";
125+
const result = uriToPath(httpUri);
126+
expect(result).toBeNull();
127+
});
128+
129+
it("should return null for malformed URIs", () => {
130+
const malformedUri = "file://invalid-uri";
131+
const result = uriToPath(malformedUri);
132+
// On Windows, file://invalid-uri is interpreted as a UNC path
133+
// On Unix, it may throw an error and return null, or parse successfully
134+
if (platform() === "win32") {
135+
// Windows interprets this as \\invalid-uri
136+
expect(result).toBeTruthy();
137+
}
138+
// On other platforms behavior varies, so we don't assert
139+
});
140+
141+
it("should handle UNC paths on Windows", () => {
142+
const uri = "file://server/share/folder/file.txt";
143+
const result = uriToPath(uri);
144+
if (platform() === "win32") {
145+
expect(result).toBeTruthy();
146+
expect(result).toContain("file.txt");
147+
}
148+
// On Unix systems, this format is not valid and behavior is undefined
149+
});
150+
151+
it("should return null for empty URI", () => {
152+
const result = uriToPath("");
153+
expect(result).toBeNull();
154+
});
155+
156+
it("should handle root directory URI", () => {
157+
const uri = "file:///";
158+
const result = uriToPath(uri);
159+
// On Windows, file:/// without a drive letter may fail
160+
// On Unix, it should return /
161+
if (platform() !== "win32") {
162+
expect(result).toBe("/");
163+
}
164+
// Skip assertion on Windows as behavior is platform-specific
165+
});
166+
});
167+
168+
describe("round-trip conversion", () => {
169+
const testPaths = [
170+
"/home/user/documents/file.txt", // Unix absolute
171+
"./relative/path/file.txt", // Relative
172+
"../parent/file.txt", // Parent relative
173+
"/tmp/file with spaces.txt", // Spaces
174+
"/home/用户/文档/文件.txt", // Unicode
175+
];
176+
177+
// Add Windows-specific paths if running on Windows
178+
const windowsPaths = [
179+
"C:\\Users\\user\\Documents\\file.txt", // Windows absolute
180+
"D:\\Program Files\\app\\file.exe", // Program Files
181+
"\\\\server\\share\\file.txt", // UNC path
182+
];
183+
184+
const allPaths =
185+
platform() === "win32" ? [...testPaths, ...windowsPaths] : testPaths;
186+
187+
allPaths.forEach((originalPath) => {
188+
it(`should maintain path integrity for: ${originalPath}`, () => {
189+
const uri = pathToUri(originalPath);
190+
const roundTripPath = uriToPath(uri);
191+
192+
expect(roundTripPath).toBeTruthy();
193+
194+
// Normalize both paths for comparison since path separators might differ
195+
const normalizedOriginal = normalize(resolve(originalPath));
196+
const normalizedRoundTrip = normalize(roundTripPath!);
197+
198+
expect(normalizedRoundTrip).toBe(normalizedOriginal);
199+
});
200+
});
201+
});
202+
203+
describe("slugToUri", () => {
204+
it("should convert slug to URI", () => {
205+
const slug = "my-awesome-project";
206+
const result = slugToUri(slug);
207+
expect(result).toBe("slug://my-awesome-project");
208+
});
209+
210+
it("should handle empty slug", () => {
211+
const slug = "";
212+
const result = slugToUri(slug);
213+
expect(result).toBe("slug://");
214+
});
215+
216+
it("should handle slug with special characters", () => {
217+
const slug = "my-project_123";
218+
const result = slugToUri(slug);
219+
expect(result).toBe("slug://my-project_123");
220+
});
221+
});
222+
223+
describe("uriToSlug", () => {
224+
it("should extract slug from URI", () => {
225+
const uri = "slug://my-awesome-project";
226+
const result = uriToSlug(uri);
227+
expect(result).toBe("my-awesome-project");
228+
});
229+
230+
it("should return null for non-slug URIs", () => {
231+
const uri = "file:///path/to/file";
232+
const result = uriToSlug(uri);
233+
expect(result).toBeNull();
234+
});
235+
236+
it("should handle empty slug URI", () => {
237+
const uri = "slug://";
238+
const result = uriToSlug(uri);
239+
expect(result).toBe("");
240+
});
241+
242+
it("should return null for malformed URI", () => {
243+
const uri = "slug:/missing-slash";
244+
const result = uriToSlug(uri);
245+
expect(result).toBeNull();
246+
});
247+
});
248+
249+
describe("slug round-trip conversion", () => {
250+
const testSlugs = [
251+
"simple-slug",
252+
"complex_slug-123",
253+
"",
254+
"a",
255+
"very-long-slug-name-with-many-parts",
256+
];
257+
258+
testSlugs.forEach((originalSlug) => {
259+
it(`should maintain slug integrity for: "${originalSlug}"`, () => {
260+
const uri = slugToUri(originalSlug);
261+
const roundTripSlug = uriToSlug(uri);
262+
expect(roundTripSlug).toBe(originalSlug);
263+
});
264+
});
265+
});
266+
});

extensions/cli/src/auth/uriUtils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { normalize } from "path";
2+
import { fileURLToPath, pathToFileURL } from "url";
3+
14
/**
25
* URI utility functions for auth config
36
*/
47

58
export function pathToUri(path: string): string {
6-
return `file://${path}`;
9+
const normalizedPath = normalize(path);
10+
return pathToFileURL(normalizedPath).href;
711
}
812

913
export function slugToUri(slug: string): string {
@@ -14,7 +18,11 @@ export function uriToPath(uri: string): string | null {
1418
if (!uri.startsWith("file://")) {
1519
return null;
1620
}
17-
return uri.slice(7);
21+
try {
22+
return fileURLToPath(uri);
23+
} catch {
24+
return null;
25+
}
1826
}
1927

2028
export function uriToSlug(uri: string): string | null {

0 commit comments

Comments
 (0)