Skip to content

Commit

Permalink
Unable to open files with special characters in the file name (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeschli committed Sep 29, 2023
1 parent 3f0f858 commit 73bdc01
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 22 deletions.
32 changes: 19 additions & 13 deletions fs-provider/src/fsExtensionMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -23,11 +23,11 @@ class ServerBackedFile implements File {
readonly type = FileType.File;
private _stats: Promise<FileStat> | undefined;
private _content: Promise<Uint8Array> | undefined;
constructor(private readonly _serverUri: Uri, public name: string) {
constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) {
}
get stats(): Promise<FileStat> {
if (this._stats === undefined) {
this._stats = getStats(this._serverUri);
this._stats = getStats(this._serverRoot, this.pathSegments);
}
return this._stats;
}
Expand All @@ -36,7 +36,7 @@ class ServerBackedFile implements File {
}
get content(): Promise<Uint8Array> {
if (this._content === undefined) {
this._content = getContent(this._serverUri);
this._content = getContent(this._serverRoot, this.pathSegments);
}
return this._content;
}
Expand All @@ -49,11 +49,11 @@ class ServerBackedDirectory implements Directory {
readonly type = FileType.Directory;
private _stats: Promise<FileStat> | undefined;
private _entries: Promise<Map<string, Entry>> | undefined;
constructor(private readonly _serverUri: Uri, public name: string) {
constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) {
}
get stats(): Promise<FileStat> {
if (this._stats === undefined) {
this._stats = getStats(this._serverUri);
this._stats = getStats(this._serverRoot, this.pathSegments);
}
return this._stats;
}
Expand All @@ -62,7 +62,7 @@ class ServerBackedDirectory implements Directory {
}
get entries(): Promise<Map<string, Entry>> {
if (this._entries === undefined) {
this._entries = getEntries(this._serverUri);
this._entries = getEntries(this._serverRoot, this.pathSegments);
}
return this._entries;
}
Expand All @@ -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<Map<string, Entry>> {
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<Map<string, Entry>> {
const url = getServerUri(serverRoot, pathSegments).with({ query: 'readdir' }).toString(/*skipEncoding*/ true);
const response = await xhr({ url });
if (response.status === 200 && response.status <= 204) {
try {
Expand All @@ -91,8 +95,8 @@ async function getEntries(serverUri: Uri): Promise<Map<string, Entry>> {
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);
}
}
Expand All @@ -108,7 +112,8 @@ async function getEntries(serverUri: Uri): Promise<Map<string, Entry>> {
return new Map();
}

async function getStats(serverUri: Uri): Promise<FileStat> {
async function getStats(serverRoot: Uri, pathSegments: readonly string[]): Promise<FileStat> {
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) {
Expand All @@ -121,7 +126,8 @@ async function getStats(serverUri: Uri): Promise<FileStat> {
throw FileSystemError.FileNotFound(`Invalid server response for ${serverUri.toString(/*skipEncoding*/ true)}. Status ${response.status}.`);
}

async function getContent(serverUri: Uri): Promise<Uint8Array> {
async function getContent(serverRoot: Uri, pathSegments: readonly string[]): Promise<Uint8Array> {
const serverUri = getServerUri(serverRoot, pathSegments);
const response = await xhr({ url: serverUri.toString(/*skipEncoding*/ true) });
if (response.status >= 200 && response.status <= 204) {
return response.body;
Expand Down
16 changes: 9 additions & 7 deletions sample/src/web/test/suite/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 () => {
Expand All @@ -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']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test_utf_8_🧿
4 changes: 2 additions & 2 deletions src/server/mounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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) }));
Expand Down

0 comments on commit 73bdc01

Please sign in to comment.