@@ -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+
235263export 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 {
0 commit comments