Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to open files with special characters in the file name #98

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading