From 73bdc0183fdd795e252619d8d3760dde40a7fd2e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 29 Sep 2023 21:11:32 +0200 Subject: [PATCH] Unable to open files with special characters in the file name (#98) --- fs-provider/src/fsExtensionMain.ts | 32 +++++++++++-------- sample/src/web/test/suite/fs.test.ts | 16 ++++++---- ...TUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" | 1 + src/server/mounts.ts | 4 +-- 4 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 "sample/test-workspace/folder_with_utf_8_\360\237\247\277/!#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" diff --git a/fs-provider/src/fsExtensionMain.ts b/fs-provider/src/fsExtensionMain.ts index b75b5c9..7ab09a0 100644 --- a/fs-provider/src/fsExtensionMain.ts +++ b/fs-provider/src/fsExtensionMain.ts @@ -11,7 +11,7 @@ const SCHEME = 'vscode-test-web'; export function activate(context: ExtensionContext) { const serverUri = context.extensionUri.with({ path: '/static/mount', query: undefined }); - const serverBackedRootDirectory = new ServerBackedDirectory(serverUri, ''); + const serverBackedRootDirectory = new ServerBackedDirectory(serverUri, [], ''); const disposable = workspace.registerFileSystemProvider(SCHEME, new MemFileSystemProvider(SCHEME, serverBackedRootDirectory)); context.subscriptions.push(disposable); @@ -23,11 +23,11 @@ class ServerBackedFile implements File { readonly type = FileType.File; private _stats: Promise | undefined; private _content: Promise | undefined; - constructor(private readonly _serverUri: Uri, public name: string) { + constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) { } get stats(): Promise { if (this._stats === undefined) { - this._stats = getStats(this._serverUri); + this._stats = getStats(this._serverRoot, this.pathSegments); } return this._stats; } @@ -36,7 +36,7 @@ class ServerBackedFile implements File { } get content(): Promise { if (this._content === undefined) { - this._content = getContent(this._serverUri); + this._content = getContent(this._serverRoot, this.pathSegments); } return this._content; } @@ -49,11 +49,11 @@ class ServerBackedDirectory implements Directory { readonly type = FileType.Directory; private _stats: Promise | undefined; private _entries: Promise> | undefined; - constructor(private readonly _serverUri: Uri, public name: string) { + constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) { } get stats(): Promise { if (this._stats === undefined) { - this._stats = getStats(this._serverUri); + this._stats = getStats(this._serverRoot, this.pathSegments); } return this._stats; } @@ -62,7 +62,7 @@ class ServerBackedDirectory implements Directory { } get entries(): Promise> { if (this._entries === undefined) { - this._entries = getEntries(this._serverUri); + this._entries = getEntries(this._serverRoot, this.pathSegments); } return this._entries; } @@ -81,8 +81,12 @@ function isStat(e: any): e is FileStat { return e && (e.type === FileType.Directory || e.type === FileType.File) && typeof e.ctime === 'number' && typeof e.mtime === 'number' && typeof e.size === 'number'; } -async function getEntries(serverUri: Uri): Promise> { - const url = serverUri.with({ query: 'readdir' }).toString(/*skipEncoding*/ true); +function getServerUri(serverRoot: Uri, pathSegments: readonly string[]): Uri { + return Uri.joinPath(serverRoot, ...pathSegments); +} + +async function getEntries(serverRoot: Uri, pathSegments: readonly string[]): Promise> { + const url = getServerUri(serverRoot, pathSegments).with({ query: 'readdir' }).toString(/*skipEncoding*/ true); const response = await xhr({ url }); if (response.status === 200 && response.status <= 204) { try { @@ -91,8 +95,8 @@ async function getEntries(serverUri: Uri): Promise> { const entries = new Map(); for (const r of res) { if (isEntry(r)) { - const childPath = Uri.joinPath(serverUri, r.name); - const newEntry: Entry = r.type === FileType.Directory ? new ServerBackedDirectory(childPath, r.name) : new ServerBackedFile(childPath, r.name); + const newPathSegments = [...pathSegments, encodeURIComponent(r.name)]; + const newEntry: Entry = r.type === FileType.Directory ? new ServerBackedDirectory(serverRoot, newPathSegments, r.name) : new ServerBackedFile(serverRoot, newPathSegments, r.name); entries.set(newEntry.name, newEntry); } } @@ -108,7 +112,8 @@ async function getEntries(serverUri: Uri): Promise> { return new Map(); } -async function getStats(serverUri: Uri): Promise { +async function getStats(serverRoot: Uri, pathSegments: readonly string[]): Promise { + const serverUri = getServerUri(serverRoot, pathSegments); const url = serverUri.with({ query: 'stat' }).toString(/*skipEncoding*/ true); const response = await xhr({ url }); if (response.status === 200 && response.status <= 204) { @@ -121,7 +126,8 @@ async function getStats(serverUri: Uri): Promise { throw FileSystemError.FileNotFound(`Invalid server response for ${serverUri.toString(/*skipEncoding*/ true)}. Status ${response.status}.`); } -async function getContent(serverUri: Uri): Promise { +async function getContent(serverRoot: Uri, pathSegments: readonly string[]): Promise { + const serverUri = getServerUri(serverRoot, pathSegments); const response = await xhr({ url: serverUri.toString(/*skipEncoding*/ true) }); if (response.status >= 200 && response.status <= 204) { return response.body; diff --git a/sample/src/web/test/suite/fs.test.ts b/sample/src/web/test/suite/fs.test.ts index bf0f550..02ffca0 100644 --- a/sample/src/web/test/suite/fs.test.ts +++ b/sample/src/web/test/suite/fs.test.ts @@ -56,7 +56,7 @@ suite('Workspace folder access', () => { let array = await vscode.workspace.fs.readFile(getUri(path)); const content = new TextDecoder().decode(array); assert.deepStrictEqual(content, expected); - await assertStats(path, true, content.length); + await assertStats(path, true, array.length); } async function assertStats(path: string, isFile: boolean, expectedSize?: number) { @@ -80,14 +80,15 @@ suite('Workspace folder access', () => { test('Folder contents', async () => { await assertEntries('/folder', ['x.txt'], ['.bar']); await assertEntries('/folder/', ['x.txt'], ['.bar']); - await assertEntries('/', ['hello.txt', 'world.txt'], ['folder']); + await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); await assertEntries('/folder/.bar', ['.foo'], []); + await assertEntries('/folder_with_utf_8_🧿', ['!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~'], []); }); test('File contents', async () => { await assertContent('/hello.txt', '// hello'); await assertContent('/world.txt', '// world'); - await assertContent('/folder/x.txt', '// x'); + await assertContent('/folder_with_utf_8_🧿/!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~', 'test_utf_8_🧿'); }); test('File stats', async () => { @@ -97,23 +98,24 @@ suite('Workspace folder access', () => { await assertStats('/folder/', false); await assertStats('/folder/.bar', false); await assertStats('/folder/.bar/.foo', true, 3); + await assertStats('/folder_with_utf_8_🧿/!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~', true, 15); await assertStats('/', false); }); test('Create and delete directory', async () => { await createFolder('/more'); - await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'more']); + await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿', 'more' ]); await deleteEntry('/more', false); - await assertEntries('/', ['hello.txt', 'world.txt'], ['folder']); + await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); }); test('Create and delete file', async () => { await createFile('/more.txt', 'content'); - await assertEntries('/', ['hello.txt', 'world.txt', 'more.txt'], ['folder']); + await assertEntries('/', ['hello.txt', 'world.txt', 'more.txt'], ['folder', 'folder_with_utf_8_🧿']); await assertContent('/more.txt', 'content'); await deleteEntry('/more.txt', true); - await assertEntries('/', ['hello.txt', 'world.txt'], ['folder']); + await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); await createFile('/folder/more.txt', 'moreContent'); await assertEntries('/folder', ['x.txt', 'more.txt'], ['.bar']); diff --git "a/sample/test-workspace/folder_with_utf_8_\360\237\247\277/!#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" "b/sample/test-workspace/folder_with_utf_8_\360\237\247\277/!#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" new file mode 100644 index 0000000..b648702 --- /dev/null +++ "b/sample/test-workspace/folder_with_utf_8_\360\237\247\277/!#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~" @@ -0,0 +1 @@ +test_utf_8_🧿 \ No newline at end of file diff --git a/src/server/mounts.ts b/src/server/mounts.ts index 5d65cc4..28f651e 100644 --- a/src/server/mounts.ts +++ b/src/server/mounts.ts @@ -32,7 +32,7 @@ function fileOps(mountPrefix: string, folderMountPath: string): Router.Middlewar const router = new Router(); router.get(`${mountPrefix}(/.*)?`, async (ctx, next) => { if (ctx.query.stat !== undefined) { - const p = path.join(folderMountPath, ctx.path.substring(mountPrefix.length)); + const p = path.join(folderMountPath, decodeURIComponent(ctx.path.substring(mountPrefix.length))); try { const stats = await fs.stat(p); ctx.body = { @@ -45,7 +45,7 @@ function fileOps(mountPrefix: string, folderMountPath: string): Router.Middlewar ctx.body = { error: (e as NodeJS.ErrnoException).code }; } } else if (ctx.query.readdir !== undefined) { - const p = path.join(folderMountPath, ctx.path.substring(mountPrefix.length)); + const p = path.join(folderMountPath, decodeURIComponent(ctx.path.substring(mountPrefix.length))); try { const entries = await fs.readdir(p, { withFileTypes: true }); ctx.body = entries.map((d) => ({ name: d.name, type: getFileType(d) }));