Skip to content
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
34 changes: 34 additions & 0 deletions src/providers/FileSystemProvider/FileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,34 @@ export function isfsDocumentName(uri: vscode.Uri, csp?: boolean, pkg = false): s
return pkg && !csp && !doc.split("/").pop().includes(".") ? `${doc}.PKG` : doc;
}

/**
* Validate that `uri`'s path is in "canonical form" for classes and routines.
* For example, the "canonical" uri path representing `%Library.CHUIScreen.cls`
* is `/%Library/CHUIScreen.cls`. Paths that will not be rejected include
* `/%CHUIScreen.cls` (short alias), `/%Library.CHUIScreen.cls` (dotted packages),
* and `/%Library/CHUIScreen.CLS` (extension has wrong case). This is needed to
* prevent the user from opening multiple copies of the same document. This
* function does not return a value; it throws a `vscode.FileSystemError.FileNotFound`
* error when `uri`'s path is not in "canonical form".
*/
function validateUriIsCanonical(uri: vscode.Uri): void {
if (
!isfsConfig(uri).csp &&
[".cls", ".mac", ".int", ".inc"].includes(uri.path.slice(-4).toLowerCase()) &&
// dotted packages
(uri.path.split(".").length > 2 ||
// extension has wrong case
![".cls", ".mac", ".int", ".inc"].includes(uri.path.slice(-4)) ||
// short alias for %Library class
(uri.path.startsWith("/%") &&
uri.path.slice(-4) == ".cls" &&
uri.path.split(".").length == 2 &&
uri.path.split("/").length == 2))
) {
throw vscode.FileSystemError.FileNotFound(uri);
}
}

export class FileSystemProvider implements vscode.FileSystemProvider {
private superRoot = new Directory("", "");

Expand All @@ -253,6 +281,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
public async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
const api = new AtelierAPI(uri);
if (!api.active) throw vscode.FileSystemError.Unavailable("Server connection is inactive");
validateUriIsCanonical(uri);
let entryPromise: Promise<Entry>;
let result: Entry;
const redirectedUri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri));
Expand Down Expand Up @@ -434,6 +463,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
}

public async readFile(uri: vscode.Uri): Promise<Uint8Array> {
validateUriIsCanonical(uri);
// Use _lookup() instead of _lookupAsFile() so we send
// our cached mtime with the GET /doc request if we have it
return this._lookup(uri, true).then((file: File) => file.data);
Expand All @@ -451,6 +481,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
if (uri.path.startsWith("/.")) {
throw new vscode.FileSystemError("dot-folders are not supported by server");
}
validateUriIsCanonical(uri);
const csp = isCSP(uri);
const fileName = isfsDocumentName(uri, csp);
if (fileName.startsWith(".")) {
Expand Down Expand Up @@ -667,6 +698,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider {

public async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise<void> {
uri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri));
validateUriIsCanonical(uri);
const { project } = isfsConfig(uri);
const csp = isCSP(uri);
const api = new AtelierAPI(uri);
Expand Down Expand Up @@ -759,6 +791,8 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
if (vscode.workspace.getWorkspaceFolder(oldUri) != vscode.workspace.getWorkspaceFolder(newUri)) {
throw new vscode.FileSystemError("Cannot rename a file across workspace folders");
}
validateUriIsCanonical(oldUri);
validateUriIsCanonical(newUri);
// Check if the destination exists
let newFileStat: vscode.FileStat;
try {
Expand Down
11 changes: 10 additions & 1 deletion src/utils/documentPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,18 @@ export async function pickDocument(api: AtelierAPI, prompt?: string): Promise<st
quickPick.enabled = false;
const item = quickPick.selectedItems[0];
if (!item || item.label.startsWith("$(")) {
const doc = item?.fullName ?? quickPick.value.trim();
let doc = item?.fullName ?? quickPick.value.trim();
if (!item) {
// The document name came from the value text, so validate it first
// Normalize the file extension case for classes and routines
doc = [".cls", ".mac", ".int", ".inc"].includes(doc.slice(-4).toLowerCase())
? doc.slice(0, -3) + doc.slice(-3).toLowerCase()
: doc;
// Expand the short form of %Library classes to the long form
doc =
doc.startsWith("%") && doc.split(".").length == 2 && doc.slice(-4) == ".cls"
? `%Library.${doc.slice(1)}`
: doc;
api
.headDoc(doc)
.then(() => resolve(doc))
Expand Down