Skip to content

Commit 7db6cac

Browse files
authored
Prevent users from accidentally opening multiple copies of the same class or routine (#1666)
1 parent 14bd8af commit 7db6cac

File tree

2 files changed

+44
-1
lines changed

2 files changed

+44
-1
lines changed

src/providers/FileSystemProvider/FileSystemProvider.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,34 @@ export function isfsDocumentName(uri: vscode.Uri, csp?: boolean, pkg = false): s
232232
return pkg && !csp && !doc.split("/").pop().includes(".") ? `${doc}.PKG` : doc;
233233
}
234234

235+
/**
236+
* Validate that `uri`'s path is in "canonical form" for classes and routines.
237+
* For example, the "canonical" uri path representing `%Library.CHUIScreen.cls`
238+
* is `/%Library/CHUIScreen.cls`. Paths that will not be rejected include
239+
* `/%CHUIScreen.cls` (short alias), `/%Library.CHUIScreen.cls` (dotted packages),
240+
* and `/%Library/CHUIScreen.CLS` (extension has wrong case). This is needed to
241+
* prevent the user from opening multiple copies of the same document. This
242+
* function does not return a value; it throws a `vscode.FileSystemError.FileNotFound`
243+
* error when `uri`'s path is not in "canonical form".
244+
*/
245+
function validateUriIsCanonical(uri: vscode.Uri): void {
246+
if (
247+
!isfsConfig(uri).csp &&
248+
[".cls", ".mac", ".int", ".inc"].includes(uri.path.slice(-4).toLowerCase()) &&
249+
// dotted packages
250+
(uri.path.split(".").length > 2 ||
251+
// extension has wrong case
252+
![".cls", ".mac", ".int", ".inc"].includes(uri.path.slice(-4)) ||
253+
// short alias for %Library class
254+
(uri.path.startsWith("/%") &&
255+
uri.path.slice(-4) == ".cls" &&
256+
uri.path.split(".").length == 2 &&
257+
uri.path.split("/").length == 2))
258+
) {
259+
throw vscode.FileSystemError.FileNotFound(uri);
260+
}
261+
}
262+
235263
export class FileSystemProvider implements vscode.FileSystemProvider {
236264
private superRoot = new Directory("", "");
237265

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

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

668699
public async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise<void> {
669700
uri = redirectDotvscodeRoot(uri, vscode.FileSystemError.FileNotFound(uri));
701+
validateUriIsCanonical(uri);
670702
const { project } = isfsConfig(uri);
671703
const csp = isCSP(uri);
672704
const api = new AtelierAPI(uri);
@@ -759,6 +791,8 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
759791
if (vscode.workspace.getWorkspaceFolder(oldUri) != vscode.workspace.getWorkspaceFolder(newUri)) {
760792
throw new vscode.FileSystemError("Cannot rename a file across workspace folders");
761793
}
794+
validateUriIsCanonical(oldUri);
795+
validateUriIsCanonical(newUri);
762796
// Check if the destination exists
763797
let newFileStat: vscode.FileStat;
764798
try {

src/utils/documentPicker.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,18 @@ export async function pickDocument(api: AtelierAPI, prompt?: string): Promise<st
427427
quickPick.enabled = false;
428428
const item = quickPick.selectedItems[0];
429429
if (!item || item.label.startsWith("$(")) {
430-
const doc = item?.fullName ?? quickPick.value.trim();
430+
let doc = item?.fullName ?? quickPick.value.trim();
431431
if (!item) {
432432
// The document name came from the value text, so validate it first
433+
// Normalize the file extension case for classes and routines
434+
doc = [".cls", ".mac", ".int", ".inc"].includes(doc.slice(-4).toLowerCase())
435+
? doc.slice(0, -3) + doc.slice(-3).toLowerCase()
436+
: doc;
437+
// Expand the short form of %Library classes to the long form
438+
doc =
439+
doc.startsWith("%") && doc.split(".").length == 2 && doc.slice(-4) == ".cls"
440+
? `%Library.${doc.slice(1)}`
441+
: doc;
433442
api
434443
.headDoc(doc)
435444
.then(() => resolve(doc))

0 commit comments

Comments
 (0)