1namespace vfs { 2 /** 3 * Posix-style path to the TypeScript compiler build outputs (including tsc.js, lib.d.ts, etc.) 4 */ 5 export const builtFolder = "/.ts"; 6 7 /** 8 * Posix-style path to additional mountable folders (./tests/projects in this repo) 9 */ 10 export const projectsFolder = "/.projects"; 11 12 /** 13 * Posix-style path to additional test libraries 14 */ 15 export const testLibFolder = "/.lib"; 16 17 /** 18 * Posix-style path to sources under test 19 */ 20 export const srcFolder = "/.src"; 21 22 // file type 23 const S_IFMT = 0o170000; // file type 24 const S_IFSOCK = 0o140000; // socket 25 const S_IFLNK = 0o120000; // symbolic link 26 const S_IFREG = 0o100000; // regular file 27 const S_IFBLK = 0o060000; // block device 28 const S_IFDIR = 0o040000; // directory 29 const S_IFCHR = 0o020000; // character device 30 const S_IFIFO = 0o010000; // FIFO 31 32 let devCount = 0; // A monotonically increasing count of device ids 33 let inoCount = 0; // A monotonically increasing count of inodes 34 35 export interface DiffOptions { 36 includeChangedFileWithSameContent?: boolean; 37 baseIsNotShadowRoot?: boolean; 38 } 39 40 /** 41 * Represents a virtual POSIX-like file system. 42 */ 43 export class FileSystem { 44 /** Indicates whether the file system is case-sensitive (`false`) or case-insensitive (`true`). */ 45 public readonly ignoreCase: boolean; 46 47 /** Gets the comparison function used to compare two paths. */ 48 public readonly stringComparer: (a: string, b: string) => number; 49 50 // lazy-initialized state that should be mutable even if the FileSystem is frozen. 51 private _lazy: { 52 links?: collections.SortedMap<string, Inode>; 53 shadows?: Map<number, Inode>; 54 meta?: collections.Metadata; 55 } = {}; 56 57 private _cwd: string; // current working directory 58 private _time: number | Date | (() => number | Date); 59 private _shadowRoot: FileSystem | undefined; 60 private _dirStack: string[] | undefined; 61 62 constructor(ignoreCase: boolean, options: FileSystemOptions = {}) { 63 const { time = -1, files, meta } = options; 64 this.ignoreCase = ignoreCase; 65 this.stringComparer = this.ignoreCase ? vpath.compareCaseInsensitive : vpath.compareCaseSensitive; 66 this._time = time; 67 68 if (meta) { 69 for (const key of Object.keys(meta)) { 70 this.meta.set(key, meta[key]); 71 } 72 } 73 74 if (files) { 75 this._applyFiles(files, /*dirname*/ ""); 76 } 77 78 let cwd = options.cwd; 79 if ((!cwd || !vpath.isRoot(cwd)) && this._lazy.links) { 80 const iterator = collections.getIterator(this._lazy.links.keys()); 81 try { 82 for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { 83 const name = i.value; 84 cwd = cwd ? vpath.resolve(name, cwd) : name; 85 break; 86 } 87 } 88 finally { 89 collections.closeIterator(iterator); 90 } 91 } 92 93 if (cwd) { 94 vpath.validate(cwd, vpath.ValidationFlags.Absolute); 95 this.mkdirpSync(cwd); 96 } 97 98 this._cwd = cwd || ""; 99 } 100 101 /** 102 * Gets metadata for this `FileSystem`. 103 */ 104 public get meta(): collections.Metadata { 105 if (!this._lazy.meta) { 106 this._lazy.meta = new collections.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined); 107 } 108 return this._lazy.meta; 109 } 110 111 /** 112 * Gets a value indicating whether the file system is read-only. 113 */ 114 public get isReadonly() { 115 return Object.isFrozen(this); 116 } 117 118 /** 119 * Makes the file system read-only. 120 */ 121 public makeReadonly() { 122 Object.freeze(this); 123 return this; 124 } 125 126 /** 127 * Gets the file system shadowed by this file system. 128 */ 129 public get shadowRoot() { 130 return this._shadowRoot; 131 } 132 133 /** 134 * Snapshots the current file system, effectively shadowing itself. This is useful for 135 * generating file system patches using `.diff()` from one snapshot to the next. Performs 136 * no action if this file system is read-only. 137 */ 138 public snapshot() { 139 if (this.isReadonly) return; 140 const fs = new FileSystem(this.ignoreCase, { time: this._time }); 141 fs._lazy = this._lazy; 142 fs._cwd = this._cwd; 143 fs._time = this._time; 144 fs._shadowRoot = this._shadowRoot; 145 fs._dirStack = this._dirStack; 146 fs.makeReadonly(); 147 this._lazy = {}; 148 this._shadowRoot = fs; 149 } 150 151 /** 152 * Gets a shadow copy of this file system. Changes to the shadow copy do not affect the 153 * original, allowing multiple copies of the same core file system without multiple copies 154 * of the same data. 155 */ 156 public shadow(ignoreCase = this.ignoreCase) { 157 if (!this.isReadonly) throw new Error("Cannot shadow a mutable file system."); 158 if (ignoreCase && !this.ignoreCase) throw new Error("Cannot create a case-insensitive file system from a case-sensitive one."); 159 const fs = new FileSystem(ignoreCase, { time: this._time }); 160 fs._shadowRoot = this; 161 fs._cwd = this._cwd; 162 return fs; 163 } 164 165 /** 166 * Gets or sets the timestamp (in milliseconds) used for file status, returning the previous timestamp. 167 * 168 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/time.html 169 */ 170 public time(value?: number | Date | (() => number | Date)): number { 171 if (value !== undefined && this.isReadonly) throw createIOError("EPERM"); 172 let result = this._time; 173 if (typeof result === "function") result = result(); 174 if (typeof result === "object") result = result.getTime(); 175 if (result === -1) result = Date.now(); 176 if (value !== undefined) { 177 this._time = value; 178 } 179 return result; 180 } 181 182 /** 183 * Gets the metadata object for a path. 184 * @param path 185 */ 186 public filemeta(path: string): collections.Metadata { 187 const { node } = this._walk(this._resolve(path)); 188 if (!node) throw createIOError("ENOENT"); 189 return this._filemeta(node); 190 } 191 192 private _filemeta(node: Inode): collections.Metadata { 193 if (!node.meta) { 194 const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot); 195 node.meta = new collections.Metadata(parentMeta); 196 } 197 return node.meta; 198 } 199 200 /** 201 * Get the pathname of the current working directory. 202 * 203 * @link - http://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html 204 */ 205 public cwd() { 206 if (!this._cwd) throw new Error("The current working directory has not been set."); 207 const { node } = this._walk(this._cwd); 208 if (!node) throw createIOError("ENOENT"); 209 if (!isDirectory(node)) throw createIOError("ENOTDIR"); 210 return this._cwd; 211 } 212 213 /** 214 * Changes the current working directory. 215 * 216 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/chdir.html 217 */ 218 public chdir(path: string) { 219 if (this.isReadonly) throw createIOError("EPERM"); 220 path = this._resolve(path); 221 const { node } = this._walk(path); 222 if (!node) throw createIOError("ENOENT"); 223 if (!isDirectory(node)) throw createIOError("ENOTDIR"); 224 this._cwd = path; 225 } 226 227 /** 228 * Pushes the current directory onto the directory stack and changes the current working directory to the supplied path. 229 */ 230 public pushd(path?: string) { 231 if (this.isReadonly) throw createIOError("EPERM"); 232 if (path) path = this._resolve(path); 233 if (this._cwd) { 234 if (!this._dirStack) this._dirStack = []; 235 this._dirStack.push(this._cwd); 236 } 237 if (path && path !== this._cwd) { 238 this.chdir(path); 239 } 240 } 241 242 /** 243 * Pops the previous directory from the location stack and changes the current directory to that directory. 244 */ 245 public popd() { 246 if (this.isReadonly) throw createIOError("EPERM"); 247 const path = this._dirStack && this._dirStack.pop(); 248 if (path) { 249 this.chdir(path); 250 } 251 } 252 253 /** 254 * Update the file system with a set of files. 255 */ 256 public apply(files: FileSet) { 257 this._applyFiles(files, this._cwd); 258 } 259 260 /** 261 * Scan file system entries along a path. If `path` is a symbolic link, it is dereferenced. 262 * @param path The path at which to start the scan. 263 * @param axis The axis along which to traverse. 264 * @param traversal The traversal scheme to use. 265 */ 266 public scanSync(path: string, axis: Axis, traversal: Traversal) { 267 path = this._resolve(path); 268 const results: string[] = []; 269 this._scan(path, this._stat(this._walk(path)), axis, traversal, /*noFollow*/ false, results); 270 return results; 271 } 272 273 /** 274 * Scan file system entries along a path. 275 * @param path The path at which to start the scan. 276 * @param axis The axis along which to traverse. 277 * @param traversal The traversal scheme to use. 278 */ 279 public lscanSync(path: string, axis: Axis, traversal: Traversal) { 280 path = this._resolve(path); 281 const results: string[] = []; 282 this._scan(path, this._stat(this._walk(path, /*noFollow*/ true)), axis, traversal, /*noFollow*/ true, results); 283 return results; 284 } 285 286 private _scan(path: string, stats: Stats, axis: Axis, traversal: Traversal, noFollow: boolean, results: string[]) { 287 if (axis === "ancestors-or-self" || axis === "self" || axis === "descendants-or-self") { 288 if (!traversal.accept || traversal.accept(path, stats)) { 289 results.push(path); 290 } 291 } 292 if (axis === "ancestors-or-self" || axis === "ancestors") { 293 const dirname = vpath.dirname(path); 294 if (dirname !== path) { 295 try { 296 const stats = this._stat(this._walk(dirname, noFollow)); 297 if (!traversal.traverse || traversal.traverse(dirname, stats)) { 298 this._scan(dirname, stats, "ancestors-or-self", traversal, noFollow, results); 299 } 300 } 301 catch { /*ignored*/ } 302 } 303 } 304 if (axis === "descendants-or-self" || axis === "descendants") { 305 if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) { 306 for (const file of this.readdirSync(path)) { 307 try { 308 const childpath = vpath.combine(path, file); 309 const stats = this._stat(this._walk(childpath, noFollow)); 310 this._scan(childpath, stats, "descendants-or-self", traversal, noFollow, results); 311 } 312 catch { /*ignored*/ } 313 } 314 } 315 } 316 } 317 318 /** 319 * Mounts a physical or virtual file system at a location in this virtual file system. 320 * 321 * @param source The path in the physical (or other virtual) file system. 322 * @param target The path in this virtual file system. 323 * @param resolver An object used to resolve files in `source`. 324 */ 325 public mountSync(source: string, target: string, resolver: FileSystemResolver) { 326 if (this.isReadonly) throw createIOError("EROFS"); 327 328 source = vpath.validate(source, vpath.ValidationFlags.Absolute); 329 330 const { parent, links, node: existingNode, basename } = this._walk(this._resolve(target), /*noFollow*/ true); 331 if (existingNode) throw createIOError("EEXIST"); 332 333 const time = this.time(); 334 const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time); 335 node.source = source; 336 node.resolver = resolver; 337 this._addLink(parent, links, basename, node, time); 338 } 339 340 /** 341 * Recursively remove all files and directories underneath the provided path. 342 */ 343 public rimrafSync(path: string) { 344 try { 345 const stats = this.lstatSync(path); 346 if (stats.isFile() || stats.isSymbolicLink()) { 347 this.unlinkSync(path); 348 } 349 else if (stats.isDirectory()) { 350 for (const file of this.readdirSync(path)) { 351 this.rimrafSync(vpath.combine(path, file)); 352 } 353 this.rmdirSync(path); 354 } 355 } 356 catch (e) { 357 if (e.code === "ENOENT") return; 358 throw e; 359 } 360 } 361 362 /** 363 * Make a directory and all of its parent paths (if they don't exist). 364 */ 365 public mkdirpSync(path: string) { 366 path = this._resolve(path); 367 const result = this._walk(path, /*noFollow*/ true, (error, result) => { 368 if (error.code === "ENOENT") { 369 this._mkdir(result); 370 return "retry"; 371 } 372 return "throw"; 373 }); 374 375 if (!result.node) this._mkdir(result); 376 } 377 378 public getFileListing(): string { 379 let result = ""; 380 const printLinks = (dirname: string | undefined, links: collections.SortedMap<string, Inode>) => { 381 const iterator = collections.getIterator(links); 382 try { 383 for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { 384 const [name, node] = i.value; 385 const path = dirname ? vpath.combine(dirname, name) : name; 386 const marker = vpath.compare(this._cwd, path, this.ignoreCase) === 0 ? "*" : " "; 387 if (result) result += "\n"; 388 result += marker; 389 if (isDirectory(node)) { 390 result += vpath.addTrailingSeparator(path); 391 printLinks(path, this._getLinks(node)); 392 } 393 else if (isFile(node)) { 394 result += path; 395 } 396 else if (isSymlink(node)) { 397 result += path + " -> " + node.symlink; 398 } 399 } 400 } 401 finally { 402 collections.closeIterator(iterator); 403 } 404 }; 405 printLinks(/*dirname*/ undefined, this._getRootLinks()); 406 return result; 407 } 408 409 /** 410 * Print diagnostic information about the structure of the file system to the console. 411 */ 412 public debugPrint(): void { 413 console.log(this.getFileListing()); 414 } 415 416 // POSIX API (aligns with NodeJS "fs" module API) 417 418 /** 419 * Determines whether a path exists. 420 */ 421 public existsSync(path: string) { 422 const result = this._walk(this._resolve(path), /*noFollow*/ true, () => "stop"); 423 return result !== undefined && result.node !== undefined; 424 } 425 426 /** 427 * Get file status. If `path` is a symbolic link, it is dereferenced. 428 * 429 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html 430 * 431 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 432 */ 433 public statSync(path: string) { 434 return this._stat(this._walk(this._resolve(path))); 435 } 436 437 /** 438 * Change file access times 439 * 440 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 441 */ 442 public utimesSync(path: string, atime: Date, mtime: Date) { 443 if (this.isReadonly) throw createIOError("EROFS"); 444 if (!isFinite(+atime) || !isFinite(+mtime)) throw createIOError("EINVAL"); 445 446 const entry = this._walk(this._resolve(path)); 447 if (!entry || !entry.node) { 448 throw createIOError("ENOENT"); 449 } 450 entry.node.atimeMs = +atime; 451 entry.node.mtimeMs = +mtime; 452 entry.node.ctimeMs = this.time(); 453 } 454 455 /** 456 * Get file status. If `path` is a symbolic link, it is dereferenced. 457 * 458 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html 459 * 460 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 461 */ 462 public lstatSync(path: string) { 463 return this._stat(this._walk(this._resolve(path), /*noFollow*/ true)); 464 } 465 466 467 private _stat(entry: WalkResult) { 468 const node = entry.node; 469 if (!node) throw createIOError(`ENOENT`, entry.realpath); 470 return new Stats( 471 node.dev, 472 node.ino, 473 node.mode, 474 node.nlink, 475 /*rdev*/ 0, 476 /*size*/ isFile(node) ? this._getSize(node) : isSymlink(node) ? node.symlink.length : 0, 477 /*blksize*/ 4096, 478 /*blocks*/ 0, 479 node.atimeMs, 480 node.mtimeMs, 481 node.ctimeMs, 482 node.birthtimeMs, 483 ); 484 } 485 486 /** 487 * Read a directory. If `path` is a symbolic link, it is dereferenced. 488 * 489 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html 490 * 491 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 492 */ 493 public readdirSync(path: string) { 494 const { node } = this._walk(this._resolve(path)); 495 if (!node) throw createIOError("ENOENT"); 496 if (!isDirectory(node)) throw createIOError("ENOTDIR"); 497 return Array.from(this._getLinks(node).keys()); 498 } 499 500 /** 501 * Make a directory. 502 * 503 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html 504 * 505 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 506 */ 507 public mkdirSync(path: string) { 508 if (this.isReadonly) throw createIOError("EROFS"); 509 510 this._mkdir(this._walk(this._resolve(path), /*noFollow*/ true)); 511 } 512 513 private _mkdir({ parent, links, node: existingNode, basename }: WalkResult) { 514 if (existingNode) throw createIOError("EEXIST"); 515 const time = this.time(); 516 const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time); 517 this._addLink(parent, links, basename, node, time); 518 } 519 520 /** 521 * Remove a directory. 522 * 523 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html 524 * 525 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 526 */ 527 public rmdirSync(path: string) { 528 if (this.isReadonly) throw createIOError("EROFS"); 529 path = this._resolve(path); 530 531 const { parent, links, node, basename } = this._walk(path, /*noFollow*/ true); 532 if (!parent) throw createIOError("EPERM"); 533 if (!isDirectory(node)) throw createIOError("ENOTDIR"); 534 if (this._getLinks(node).size !== 0) throw createIOError("ENOTEMPTY"); 535 536 this._removeLink(parent, links, basename, node); 537 } 538 539 /** 540 * Link one file to another file (also known as a "hard link"). 541 * 542 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html 543 * 544 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 545 */ 546 public linkSync(oldpath: string, newpath: string) { 547 if (this.isReadonly) throw createIOError("EROFS"); 548 549 const { node } = this._walk(this._resolve(oldpath)); 550 if (!node) throw createIOError("ENOENT"); 551 if (isDirectory(node)) throw createIOError("EPERM"); 552 553 const { parent, links, basename, node: existingNode } = this._walk(this._resolve(newpath), /*noFollow*/ true); 554 if (!parent) throw createIOError("EPERM"); 555 if (existingNode) throw createIOError("EEXIST"); 556 557 this._addLink(parent, links, basename, node); 558 } 559 560 /** 561 * Remove a directory entry. 562 * 563 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html 564 * 565 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 566 */ 567 public unlinkSync(path: string) { 568 if (this.isReadonly) throw createIOError("EROFS"); 569 570 const { parent, links, node, basename } = this._walk(this._resolve(path), /*noFollow*/ true); 571 if (!parent) throw createIOError("EPERM"); 572 if (!node) throw createIOError("ENOENT"); 573 if (isDirectory(node)) throw createIOError("EISDIR"); 574 575 this._removeLink(parent, links, basename, node); 576 } 577 578 /** 579 * Rename a file. 580 * 581 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html 582 * 583 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 584 */ 585 public renameSync(oldpath: string, newpath: string) { 586 if (this.isReadonly) throw createIOError("EROFS"); 587 588 const { parent: oldParent, links: oldParentLinks, node, basename: oldBasename } = this._walk(this._resolve(oldpath), /*noFollow*/ true); 589 if (!oldParent) throw createIOError("EPERM"); 590 if (!node) throw createIOError("ENOENT"); 591 592 const { parent: newParent, links: newParentLinks, node: existingNode, basename: newBasename } = this._walk(this._resolve(newpath), /*noFollow*/ true); 593 if (!newParent) throw createIOError("EPERM"); 594 595 const time = this.time(); 596 if (existingNode) { 597 if (isDirectory(node)) { 598 if (!isDirectory(existingNode)) throw createIOError("ENOTDIR"); 599 // if both old and new arguments point to the same directory, just pass. So we could rename /src/a/1 to /src/A/1 in Win. 600 // if not and the directory pointed by the new path is not empty, throw an error. 601 if (this.stringComparer(oldpath, newpath) !== 0 && this._getLinks(existingNode).size > 0) throw createIOError("ENOTEMPTY"); 602 } 603 else { 604 if (isDirectory(existingNode)) throw createIOError("EISDIR"); 605 } 606 this._removeLink(newParent, newParentLinks, newBasename, existingNode, time); 607 } 608 609 this._replaceLink(oldParent, oldParentLinks, oldBasename, newParent, newParentLinks, newBasename, node, time); 610 } 611 612 /** 613 * Make a symbolic link. 614 * 615 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html 616 * 617 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 618 */ 619 public symlinkSync(target: string, linkpath: string) { 620 if (this.isReadonly) throw createIOError("EROFS"); 621 622 const { parent, links, node: existingNode, basename } = this._walk(this._resolve(linkpath), /*noFollow*/ true); 623 if (!parent) throw createIOError("EPERM"); 624 if (existingNode) throw createIOError("EEXIST"); 625 626 const time = this.time(); 627 const node = this._mknod(parent.dev, S_IFLNK, /*mode*/ 0o666, time); 628 node.symlink = vpath.validate(target, vpath.ValidationFlags.RelativeOrAbsolute); 629 this._addLink(parent, links, basename, node, time); 630 } 631 632 /** 633 * Resolve a pathname. 634 * 635 * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html 636 * 637 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 638 */ 639 public realpathSync(path: string) { 640 const { realpath } = this._walk(this._resolve(path)); 641 return realpath; 642 } 643 644 /** 645 * Read from a file. 646 * 647 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 648 */ 649 public readFileSync(path: string, encoding?: null): Buffer; 650 /** 651 * Read from a file. 652 * 653 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 654 */ 655 public readFileSync(path: string, encoding: BufferEncoding): string; 656 /** 657 * Read from a file. 658 * 659 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 660 */ 661 public readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer; 662 public readFileSync(path: string, encoding: BufferEncoding | null = null) { // eslint-disable-line no-null/no-null 663 const { node } = this._walk(this._resolve(path)); 664 if (!node) throw createIOError("ENOENT"); 665 if (isDirectory(node)) throw createIOError("EISDIR"); 666 if (!isFile(node)) throw createIOError("EBADF"); 667 668 const buffer = this._getBuffer(node).slice(); 669 return encoding ? buffer.toString(encoding) : buffer; 670 } 671 672 /** 673 * Write to a file. 674 * 675 * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. 676 */ 677 // eslint-disable-next-line no-null/no-null 678 public writeFileSync(path: string, data: string | Buffer, encoding: string | null = null) { 679 if (this.isReadonly) throw createIOError("EROFS"); 680 681 const { parent, links, node: existingNode, basename } = this._walk(this._resolve(path), /*noFollow*/ false); 682 if (!parent) throw createIOError("EPERM"); 683 684 const time = this.time(); 685 let node = existingNode; 686 if (!node) { 687 node = this._mknod(parent.dev, S_IFREG, 0o666, time); 688 this._addLink(parent, links, basename, node, time); 689 } 690 691 if (isDirectory(node)) throw createIOError("EISDIR"); 692 if (!isFile(node)) throw createIOError("EBADF"); 693 node.buffer = Buffer.isBuffer(data) ? data.slice() : ts.sys.bufferFrom!("" + data, encoding || "utf8") as Buffer; 694 node.size = node.buffer.byteLength; 695 node.mtimeMs = time; 696 node.ctimeMs = time; 697 } 698 699 /** 700 * Generates a `FileSet` patch containing all the entries in this `FileSystem` that are not in `base`. 701 * @param base The base file system. If not provided, this file system's `shadowRoot` is used (if present). 702 */ 703 public diff(base?: FileSystem | undefined, options: DiffOptions = {}) { 704 if (!base && !options.baseIsNotShadowRoot) base = this.shadowRoot; 705 const differences: FileSet = {}; 706 const hasDifferences = base ? 707 FileSystem.rootDiff(differences, this, base, options) : 708 FileSystem.trackCreatedInodes(differences, this, this._getRootLinks()); 709 return hasDifferences ? differences : undefined; 710 } 711 712 /** 713 * Generates a `FileSet` patch containing all the entries in `changed` that are not in `base`. 714 */ 715 public static diff(changed: FileSystem, base: FileSystem, options: DiffOptions = {}) { 716 const differences: FileSet = {}; 717 return FileSystem.rootDiff(differences, changed, base, options) ? 718 differences : 719 undefined; 720 } 721 722 private static diffWorker(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode> | undefined, base: FileSystem, baseLinks: ReadonlyMap<string, Inode> | undefined, options: DiffOptions) { 723 if (changedLinks && !baseLinks) return FileSystem.trackCreatedInodes(container, changed, changedLinks); 724 if (baseLinks && !changedLinks) return FileSystem.trackDeletedInodes(container, baseLinks); 725 if (changedLinks && baseLinks) { 726 let hasChanges = false; 727 // track base items missing in changed 728 baseLinks.forEach((node, basename) => { 729 if (!changedLinks.has(basename)) { 730 container[basename] = isDirectory(node) ? new Rmdir() : new Unlink(); 731 hasChanges = true; 732 } 733 }); 734 // track changed items missing or differing in base 735 changedLinks.forEach((changedNode, basename) => { 736 const baseNode = baseLinks.get(basename); 737 if (baseNode) { 738 if (isDirectory(changedNode) && isDirectory(baseNode)) { 739 return hasChanges = FileSystem.directoryDiff(container, basename, changed, changedNode, base, baseNode, options) || hasChanges; 740 } 741 if (isFile(changedNode) && isFile(baseNode)) { 742 return hasChanges = FileSystem.fileDiff(container, basename, changed, changedNode, base, baseNode, options) || hasChanges; 743 } 744 if (isSymlink(changedNode) && isSymlink(baseNode)) { 745 return hasChanges = FileSystem.symlinkDiff(container, basename, changedNode, baseNode) || hasChanges; 746 } 747 } 748 return hasChanges = FileSystem.trackCreatedInode(container, basename, changed, changedNode) || hasChanges; 749 }); 750 return hasChanges; 751 } 752 return false; 753 } 754 755 private static rootDiff(container: FileSet, changed: FileSystem, base: FileSystem, options: DiffOptions) { 756 while (!changed._lazy.links && changed._shadowRoot) changed = changed._shadowRoot; 757 while (!base._lazy.links && base._shadowRoot) base = base._shadowRoot; 758 759 // no difference if the file systems are the same reference 760 if (changed === base) return false; 761 762 // no difference if the root links are empty and unshadowed 763 if (!changed._lazy.links && !changed._shadowRoot && !base._lazy.links && !base._shadowRoot) return false; 764 765 return FileSystem.diffWorker(container, changed, changed._getRootLinks(), base, base._getRootLinks(), options); 766 } 767 768 private static directoryDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: DirectoryInode, base: FileSystem, baseNode: DirectoryInode, options: DiffOptions) { 769 while (!changedNode.links && changedNode.shadowRoot) changedNode = changedNode.shadowRoot; 770 while (!baseNode.links && baseNode.shadowRoot) baseNode = baseNode.shadowRoot; 771 772 // no difference if the nodes are the same reference 773 if (changedNode === baseNode) return false; 774 775 // no difference if both nodes are non shadowed and have no entries 776 if (isEmptyNonShadowedDirectory(changedNode) && isEmptyNonShadowedDirectory(baseNode)) return false; 777 778 // no difference if both nodes are unpopulated and point to the same mounted file system 779 if (!changedNode.links && !baseNode.links && 780 changedNode.resolver && changedNode.source !== undefined && 781 baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false; 782 783 // no difference if both nodes have identical children 784 const children: FileSet = {}; 785 if (!FileSystem.diffWorker(children, changed, changed._getLinks(changedNode), base, base._getLinks(baseNode), options)) { 786 return false; 787 } 788 789 container[basename] = new Directory(children); 790 return true; 791 } 792 793 private static fileDiff(container: FileSet, basename: string, changed: FileSystem, changedNode: FileInode, base: FileSystem, baseNode: FileInode, options: DiffOptions) { 794 while (!changedNode.buffer && changedNode.shadowRoot) changedNode = changedNode.shadowRoot; 795 while (!baseNode.buffer && baseNode.shadowRoot) baseNode = baseNode.shadowRoot; 796 797 // no difference if the nodes are the same reference 798 if (changedNode === baseNode) return false; 799 800 // no difference if both nodes are non shadowed and have no entries 801 if (isEmptyNonShadowedFile(changedNode) && isEmptyNonShadowedFile(baseNode)) return false; 802 803 // no difference if both nodes are unpopulated and point to the same mounted file system 804 if (!changedNode.buffer && !baseNode.buffer && 805 changedNode.resolver && changedNode.source !== undefined && 806 baseNode.resolver === changedNode.resolver && baseNode.source === changedNode.source) return false; 807 808 const changedBuffer = changed._getBuffer(changedNode); 809 const baseBuffer = base._getBuffer(baseNode); 810 811 // no difference if both buffers are the same reference 812 if (changedBuffer === baseBuffer) return false; 813 814 // no difference if both buffers are identical 815 if (Buffer.compare(changedBuffer, baseBuffer) === 0) { 816 if (!options.includeChangedFileWithSameContent) return false; 817 container[basename] = new SameFileContentFile(changedBuffer); 818 return true; 819 } 820 821 container[basename] = new File(changedBuffer); 822 return true; 823 } 824 825 private static symlinkDiff(container: FileSet, basename: string, changedNode: SymlinkInode, baseNode: SymlinkInode) { 826 // no difference if the nodes are the same reference 827 if (changedNode.symlink === baseNode.symlink) return false; 828 container[basename] = new Symlink(changedNode.symlink); 829 return true; 830 } 831 832 private static trackCreatedInode(container: FileSet, basename: string, changed: FileSystem, node: Inode) { 833 if (isDirectory(node)) { 834 const children: FileSet = {}; 835 FileSystem.trackCreatedInodes(children, changed, changed._getLinks(node)); 836 container[basename] = new Directory(children); 837 } 838 else if (isSymlink(node)) { 839 container[basename] = new Symlink(node.symlink); 840 } 841 else { 842 container[basename] = new File(node.buffer || ""); 843 } 844 return true; 845 } 846 847 private static trackCreatedInodes(container: FileSet, changed: FileSystem, changedLinks: ReadonlyMap<string, Inode>) { 848 // no difference if links are empty 849 if (!changedLinks.size) return false; 850 851 changedLinks.forEach((node, basename) => { FileSystem.trackCreatedInode(container, basename, changed, node); }); 852 return true; 853 } 854 855 private static trackDeletedInodes(container: FileSet, baseLinks: ReadonlyMap<string, Inode>) { 856 // no difference if links are empty 857 if (!baseLinks.size) return false; 858 baseLinks.forEach((node, basename) => { container[basename] = isDirectory(node) ? new Rmdir() : new Unlink(); }); 859 return true; 860 } 861 862 private _mknod(dev: number, type: typeof S_IFREG, mode: number, time?: number): FileInode; 863 private _mknod(dev: number, type: typeof S_IFDIR, mode: number, time?: number): DirectoryInode; 864 private _mknod(dev: number, type: typeof S_IFLNK, mode: number, time?: number): SymlinkInode; 865 private _mknod(dev: number, type: number, mode: number, time = this.time()) { 866 return <Inode>{ 867 dev, 868 ino: ++inoCount, 869 mode: (mode & ~S_IFMT & ~0o022 & 0o7777) | (type & S_IFMT), 870 atimeMs: time, 871 mtimeMs: time, 872 ctimeMs: time, 873 birthtimeMs: time, 874 nlink: 0 875 }; 876 } 877 878 private _addLink(parent: DirectoryInode | undefined, links: collections.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) { 879 links.set(name, node); 880 node.nlink++; 881 node.ctimeMs = time; 882 if (parent) parent.mtimeMs = time; 883 if (!parent && !this._cwd) this._cwd = name; 884 } 885 886 private _removeLink(parent: DirectoryInode | undefined, links: collections.SortedMap<string, Inode>, name: string, node: Inode, time = this.time()) { 887 links.delete(name); 888 node.nlink--; 889 node.ctimeMs = time; 890 if (parent) parent.mtimeMs = time; 891 } 892 893 private _replaceLink(oldParent: DirectoryInode, oldLinks: collections.SortedMap<string, Inode>, oldName: string, newParent: DirectoryInode, newLinks: collections.SortedMap<string, Inode>, newName: string, node: Inode, time: number) { 894 if (oldParent !== newParent) { 895 this._removeLink(oldParent, oldLinks, oldName, node, time); 896 this._addLink(newParent, newLinks, newName, node, time); 897 } 898 else { 899 oldLinks.delete(oldName); 900 oldLinks.set(newName, node); 901 oldParent.mtimeMs = time; 902 newParent.mtimeMs = time; 903 } 904 } 905 906 private _getRootLinks() { 907 if (!this._lazy.links) { 908 this._lazy.links = new collections.SortedMap<string, Inode>(this.stringComparer); 909 if (this._shadowRoot) { 910 this._copyShadowLinks(this._shadowRoot._getRootLinks(), this._lazy.links); 911 } 912 this._lazy.links = this._lazy.links; 913 } 914 return this._lazy.links; 915 } 916 917 private _getLinks(node: DirectoryInode) { 918 if (!node.links) { 919 const links = new collections.SortedMap<string, Inode>(this.stringComparer); 920 const { source, resolver } = node; 921 if (source && resolver) { 922 node.source = undefined; 923 node.resolver = undefined; 924 for (const name of resolver.readdirSync(source)) { 925 const path = vpath.combine(source, name); 926 const stats = resolver.statSync(path); 927 switch (stats.mode & S_IFMT) { 928 case S_IFDIR: 929 const dir = this._mknod(node.dev, S_IFDIR, 0o777); 930 dir.source = vpath.combine(source, name); 931 dir.resolver = resolver; 932 this._addLink(node, links, name, dir); 933 break; 934 case S_IFREG: 935 const file = this._mknod(node.dev, S_IFREG, 0o666); 936 file.source = vpath.combine(source, name); 937 file.resolver = resolver; 938 file.size = stats.size; 939 this._addLink(node, links, name, file); 940 break; 941 } 942 } 943 } 944 else if (this._shadowRoot && node.shadowRoot) { 945 this._copyShadowLinks(this._shadowRoot._getLinks(node.shadowRoot), links); 946 } 947 node.links = links; 948 } 949 return node.links; 950 } 951 952 private _getShadow(root: DirectoryInode): DirectoryInode; 953 private _getShadow(root: Inode): Inode; 954 private _getShadow(root: Inode) { 955 const shadows = this._lazy.shadows || (this._lazy.shadows = new Map<number, Inode>()); 956 957 let shadow = shadows.get(root.ino); 958 if (!shadow) { 959 shadow = <Inode>{ 960 dev: root.dev, 961 ino: root.ino, 962 mode: root.mode, 963 atimeMs: root.atimeMs, 964 mtimeMs: root.mtimeMs, 965 ctimeMs: root.ctimeMs, 966 birthtimeMs: root.birthtimeMs, 967 nlink: root.nlink, 968 shadowRoot: root 969 }; 970 971 if (isSymlink(root)) (<SymlinkInode>shadow).symlink = root.symlink; 972 shadows.set(shadow.ino, shadow); 973 } 974 975 return shadow; 976 } 977 978 private _copyShadowLinks(source: ReadonlyMap<string, Inode>, target: collections.SortedMap<string, Inode>) { 979 const iterator = collections.getIterator(source); 980 try { 981 for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { 982 const [name, root] = i.value; 983 target.set(name, this._getShadow(root)); 984 } 985 } 986 finally { 987 collections.closeIterator(iterator); 988 } 989 } 990 991 private _getSize(node: FileInode): number { 992 if (node.buffer) return node.buffer.byteLength; 993 if (node.size !== undefined) return node.size; 994 if (node.source && node.resolver) return node.size = node.resolver.statSync(node.source).size; 995 if (this._shadowRoot && node.shadowRoot) return node.size = this._shadowRoot._getSize(node.shadowRoot); 996 return 0; 997 } 998 999 private _getBuffer(node: FileInode): Buffer { 1000 if (!node.buffer) { 1001 const { source, resolver } = node; 1002 if (source && resolver) { 1003 node.source = undefined; 1004 node.resolver = undefined; 1005 node.size = undefined; 1006 node.buffer = resolver.readFileSync(source); 1007 } 1008 else if (this._shadowRoot && node.shadowRoot) { 1009 node.buffer = this._shadowRoot._getBuffer(node.shadowRoot); 1010 } 1011 else { 1012 node.buffer = Buffer.allocUnsafe(0); 1013 } 1014 } 1015 return node.buffer; 1016 } 1017 1018 /** 1019 * Walk a path to its end. 1020 * 1021 * @param path The path to follow. 1022 * @param noFollow A value indicating whether to *not* dereference a symbolic link at the 1023 * end of a path. 1024 * 1025 * @link http://man7.org/linux/man-pages/man7/path_resolution.7.html 1026 */ 1027 private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "retry" | "throw"): WalkResult; 1028 private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "stop" | "retry" | "throw"): WalkResult | undefined; 1029 private _walk(path: string, noFollow?: boolean, onError?: (error: NodeJS.ErrnoException, fragment: WalkResult) => "stop" | "retry" | "throw"): WalkResult | undefined { 1030 let links = this._getRootLinks(); 1031 let parent: DirectoryInode | undefined; 1032 let components = vpath.parse(path); 1033 let step = 0; 1034 let depth = 0; 1035 let retry = false; 1036 while (true) { 1037 if (depth >= 40) throw createIOError("ELOOP"); 1038 const lastStep = step === components.length - 1; 1039 let basename = components[step]; 1040 const linkEntry = links.getEntry(basename); 1041 if (linkEntry) { 1042 components[step] = basename = linkEntry[0]; 1043 } 1044 const node = linkEntry?.[1]; 1045 if (lastStep && (noFollow || !isSymlink(node))) { 1046 return { realpath: vpath.format(components), basename, parent, links, node }; 1047 } 1048 if (node === undefined) { 1049 if (trapError(createIOError("ENOENT"), node)) continue; 1050 return undefined; 1051 } 1052 if (isSymlink(node)) { 1053 const dirname = vpath.format(components.slice(0, step)); 1054 const symlink = vpath.resolve(dirname, node.symlink); 1055 links = this._getRootLinks(); 1056 parent = undefined; 1057 components = vpath.parse(symlink).concat(components.slice(step + 1)); 1058 step = 0; 1059 depth++; 1060 retry = false; 1061 continue; 1062 } 1063 if (isDirectory(node)) { 1064 links = this._getLinks(node); 1065 parent = node; 1066 step++; 1067 retry = false; 1068 continue; 1069 } 1070 if (trapError(createIOError("ENOTDIR"), node)) continue; 1071 return undefined; 1072 } 1073 1074 function trapError(error: NodeJS.ErrnoException, node?: Inode) { 1075 const realpath = vpath.format(components.slice(0, step + 1)); 1076 const basename = components[step]; 1077 const result = !retry && onError ? onError(error, { realpath, basename, parent, links, node }) : "throw"; 1078 if (result === "stop") return false; 1079 if (result === "retry") { 1080 retry = true; 1081 return true; 1082 } 1083 throw error; 1084 } 1085 } 1086 1087 /** 1088 * Resolve a path relative to the current working directory. 1089 */ 1090 private _resolve(path: string) { 1091 return this._cwd 1092 ? vpath.resolve(this._cwd, vpath.validate(path, vpath.ValidationFlags.RelativeOrAbsolute | vpath.ValidationFlags.AllowWildcard)) 1093 : vpath.validate(path, vpath.ValidationFlags.Absolute | vpath.ValidationFlags.AllowWildcard); 1094 } 1095 1096 private _applyFiles(files: FileSet, dirname: string) { 1097 const deferred: [Symlink | Link | Mount, string][] = []; 1098 this._applyFilesWorker(files, dirname, deferred); 1099 for (const [entry, path] of deferred) { 1100 this.mkdirpSync(vpath.dirname(path)); 1101 this.pushd(vpath.dirname(path)); 1102 if (entry instanceof Symlink) { 1103 if (this.stringComparer(vpath.dirname(path), path) === 0) { 1104 throw new TypeError("Roots cannot be symbolic links."); 1105 } 1106 this.symlinkSync(vpath.resolve(dirname, entry.symlink), path); 1107 this._applyFileExtendedOptions(path, entry); 1108 } 1109 else if (entry instanceof Link) { 1110 if (this.stringComparer(vpath.dirname(path), path) === 0) { 1111 throw new TypeError("Roots cannot be hard links."); 1112 } 1113 this.linkSync(entry.path, path); 1114 } 1115 else { 1116 this.mountSync(entry.source, path, entry.resolver); 1117 this._applyFileExtendedOptions(path, entry); 1118 } 1119 this.popd(); 1120 } 1121 } 1122 1123 private _applyFileExtendedOptions(path: string, entry: Directory | File | Symlink | Mount) { 1124 const { meta } = entry; 1125 if (meta !== undefined) { 1126 const filemeta = this.filemeta(path); 1127 for (const key of Object.keys(meta)) { 1128 filemeta.set(key, meta[key]); 1129 } 1130 } 1131 } 1132 1133 private _applyFilesWorker(files: FileSet, dirname: string, deferred: [Symlink | Link | Mount, string][]) { 1134 for (const key of Object.keys(files)) { 1135 const value = normalizeFileSetEntry(files[key]); 1136 const path = dirname ? vpath.resolve(dirname, key) : key; 1137 vpath.validate(path, vpath.ValidationFlags.Absolute); 1138 1139 // eslint-disable-next-line no-null/no-null 1140 if (value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink) { 1141 if (this.stringComparer(vpath.dirname(path), path) === 0) { 1142 throw new TypeError("Roots cannot be deleted."); 1143 } 1144 this.rimrafSync(path); 1145 } 1146 else if (value instanceof File) { 1147 if (this.stringComparer(vpath.dirname(path), path) === 0) { 1148 throw new TypeError("Roots cannot be files."); 1149 } 1150 this.mkdirpSync(vpath.dirname(path)); 1151 this.writeFileSync(path, value.data, value.encoding); 1152 this._applyFileExtendedOptions(path, value); 1153 } 1154 else if (value instanceof Directory) { 1155 this.mkdirpSync(path); 1156 this._applyFileExtendedOptions(path, value); 1157 this._applyFilesWorker(value.files, path, deferred); 1158 } 1159 else { 1160 deferred.push([value, path]); 1161 } 1162 } 1163 } 1164 } 1165 1166 export interface FileSystemOptions { 1167 // Sets the initial timestamp for new files and directories, or the function used 1168 // to calculate timestamps. 1169 time?: number | Date | (() => number | Date); 1170 1171 // A set of file system entries to initially add to the file system. 1172 files?: FileSet; 1173 1174 // Sets the initial working directory for the file system. 1175 cwd?: string; 1176 1177 // Sets initial metadata attached to the file system. 1178 meta?: Record<string, any>; 1179 } 1180 1181 export interface FileSystemCreateOptions extends FileSystemOptions { 1182 // Sets the documents to add to the file system. 1183 documents?: readonly documents.TextDocument[]; 1184 } 1185 1186 export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants"; 1187 1188 export interface Traversal { 1189 /** A function called to choose whether to continue to traverse to either ancestors or descendants. */ 1190 traverse?(path: string, stats: Stats): boolean; 1191 /** A function called to choose whether to accept a path as part of the result. */ 1192 accept?(path: string, stats: Stats): boolean; 1193 } 1194 1195 export interface FileSystemResolver { 1196 statSync(path: string): { mode: number; size: number; }; 1197 readdirSync(path: string): string[]; 1198 readFileSync(path: string): Buffer; 1199 } 1200 1201 export interface FileSystemResolverHost { 1202 useCaseSensitiveFileNames(): boolean; 1203 getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries; 1204 directoryExists(path: string): boolean; 1205 fileExists(path: string): boolean; 1206 getFileSize(path: string): number; 1207 readFile(path: string): string | undefined; 1208 getWorkspaceRoot(): string; 1209 } 1210 1211 export function createResolver(host: FileSystemResolverHost): FileSystemResolver { 1212 return { 1213 readdirSync(path: string): string[] { 1214 const { files, directories } = host.getAccessibleFileSystemEntries(path); 1215 return directories.concat(files); 1216 }, 1217 statSync(path: string): { mode: number; size: number; } { 1218 if (host.directoryExists(path)) { 1219 return { mode: S_IFDIR | 0o777, size: 0 }; 1220 } 1221 else if (host.fileExists(path)) { 1222 return { mode: S_IFREG | 0o666, size: host.getFileSize(path) }; 1223 } 1224 else { 1225 throw new Error("ENOENT: path does not exist"); 1226 } 1227 }, 1228 readFileSync(path: string): Buffer { 1229 return ts.sys.bufferFrom!(host.readFile(path)!, "utf8") as Buffer; // TODO: GH#18217 1230 } 1231 }; 1232 } 1233 1234 /** 1235 * Create a virtual file system from a physical file system using the following path mappings: 1236 * 1237 * - `/.ts` is a directory mapped to `${workspaceRoot}/built/local` 1238 * - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib` 1239 * - `/.src` is a virtual directory to be used for tests. 1240 * 1241 * Unless overridden, `/.src` will be the current working directory for the virtual file system. 1242 */ 1243 export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, files, cwd, time, meta }: FileSystemCreateOptions = {}) { 1244 const fs = getBuiltLocal(host, ignoreCase).shadow(); 1245 if (meta) { 1246 for (const key of Object.keys(meta)) { 1247 fs.meta.set(key, meta[key]); 1248 } 1249 } 1250 if (time) { 1251 fs.time(time); 1252 } 1253 if (cwd) { 1254 fs.mkdirpSync(cwd); 1255 fs.chdir(cwd); 1256 } 1257 if (documents) { 1258 for (const document of documents) { 1259 fs.mkdirpSync(vpath.dirname(document.file)); 1260 fs.writeFileSync(document.file, document.text, "utf8"); 1261 fs.filemeta(document.file).set("document", document); 1262 // Add symlinks 1263 const symlink = document.meta.get("symlink"); 1264 if (symlink) { 1265 for (const link of symlink.split(",").map(link => link.trim())) { 1266 fs.mkdirpSync(vpath.dirname(link)); 1267 fs.symlinkSync(vpath.resolve(fs.cwd(), document.file), link); 1268 } 1269 } 1270 } 1271 } 1272 if (files) { 1273 fs.apply(files); 1274 } 1275 return fs; 1276 } 1277 1278 export class Stats { 1279 public dev: number; 1280 public ino: number; 1281 public mode: number; 1282 public nlink: number; 1283 public uid: number; 1284 public gid: number; 1285 public rdev: number; 1286 public size: number; 1287 public blksize: number; 1288 public blocks: number; 1289 public atimeMs: number; 1290 public mtimeMs: number; 1291 public ctimeMs: number; 1292 public birthtimeMs: number; 1293 public atime: Date; 1294 public mtime: Date; 1295 public ctime: Date; 1296 public birthtime: Date; 1297 1298 constructor(); 1299 constructor(dev: number, ino: number, mode: number, nlink: number, rdev: number, size: number, blksize: number, blocks: number, atimeMs: number, mtimeMs: number, ctimeMs: number, birthtimeMs: number); 1300 constructor(dev = 0, ino = 0, mode = 0, nlink = 0, rdev = 0, size = 0, blksize = 0, blocks = 0, atimeMs = 0, mtimeMs = 0, ctimeMs = 0, birthtimeMs = 0) { 1301 this.dev = dev; 1302 this.ino = ino; 1303 this.mode = mode; 1304 this.nlink = nlink; 1305 this.uid = 0; 1306 this.gid = 0; 1307 this.rdev = rdev; 1308 this.size = size; 1309 this.blksize = blksize; 1310 this.blocks = blocks; 1311 this.atimeMs = atimeMs; 1312 this.mtimeMs = mtimeMs; 1313 this.ctimeMs = ctimeMs; 1314 this.birthtimeMs = birthtimeMs; 1315 this.atime = new Date(this.atimeMs); 1316 this.mtime = new Date(this.mtimeMs); 1317 this.ctime = new Date(this.ctimeMs); 1318 this.birthtime = new Date(this.birthtimeMs); 1319 } 1320 1321 public isFile() { return (this.mode & S_IFMT) === S_IFREG; } 1322 public isDirectory() { return (this.mode & S_IFMT) === S_IFDIR; } 1323 public isSymbolicLink() { return (this.mode & S_IFMT) === S_IFLNK; } 1324 public isBlockDevice() { return (this.mode & S_IFMT) === S_IFBLK; } 1325 public isCharacterDevice() { return (this.mode & S_IFMT) === S_IFCHR; } 1326 public isFIFO() { return (this.mode & S_IFMT) === S_IFIFO; } 1327 public isSocket() { return (this.mode & S_IFMT) === S_IFSOCK; } 1328 } 1329 1330 export const IOErrorMessages = Object.freeze({ 1331 EACCES: "access denied", 1332 EIO: "an I/O error occurred", 1333 ENOENT: "no such file or directory", 1334 EEXIST: "file already exists", 1335 ELOOP: "too many symbolic links encountered", 1336 ENOTDIR: "no such directory", 1337 EISDIR: "path is a directory", 1338 EBADF: "invalid file descriptor", 1339 EINVAL: "invalid value", 1340 ENOTEMPTY: "directory not empty", 1341 EPERM: "operation not permitted", 1342 EROFS: "file system is read-only" 1343 }); 1344 1345 export function createIOError(code: keyof typeof IOErrorMessages, details = "") { 1346 const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]} ${details}`); 1347 err.code = code; 1348 if (Error.captureStackTrace) Error.captureStackTrace(err, createIOError); 1349 return err; 1350 } 1351 1352 /** 1353 * A template used to populate files, directories, links, etc. in a virtual file system. 1354 */ 1355 export interface FileSet { 1356 [name: string]: DirectoryLike | FileLike | Link | Symlink | Mount | Rmdir | Unlink | null | undefined; 1357 } 1358 1359 export type DirectoryLike = FileSet | Directory; 1360 export type FileLike = File | Buffer | string; 1361 1362 /** Extended options for a directory in a `FileSet` */ 1363 export class Directory { 1364 public readonly files: FileSet; 1365 public readonly meta: Record<string, any> | undefined; 1366 constructor(files: FileSet, { meta }: { meta?: Record<string, any> } = {}) { 1367 this.files = files; 1368 this.meta = meta; 1369 } 1370 } 1371 1372 /** Extended options for a file in a `FileSet` */ 1373 export class File { 1374 public readonly data: Buffer | string; 1375 public readonly encoding: string | undefined; 1376 public readonly meta: Record<string, any> | undefined; 1377 constructor(data: Buffer | string, { meta, encoding }: { encoding?: string, meta?: Record<string, any> } = {}) { 1378 this.data = data; 1379 this.encoding = encoding; 1380 this.meta = meta; 1381 } 1382 } 1383 1384 export class SameFileContentFile extends File { 1385 constructor(data: Buffer | string, metaAndEncoding?: { encoding?: string, meta?: Record<string, any> }) { 1386 super(data, metaAndEncoding); 1387 } 1388 } 1389 1390 /** Extended options for a hard link in a `FileSet` */ 1391 export class Link { 1392 public readonly path: string; 1393 constructor(path: string) { 1394 this.path = path; 1395 } 1396 } 1397 1398 /** Removes a directory in a `FileSet` */ 1399 export class Rmdir { 1400 public _rmdirBrand?: never; // brand necessary for proper type guards 1401 } 1402 1403 /** Unlinks a file in a `FileSet` */ 1404 export class Unlink { 1405 public _unlinkBrand?: never; // brand necessary for proper type guards 1406 } 1407 1408 /** Extended options for a symbolic link in a `FileSet` */ 1409 export class Symlink { 1410 public readonly symlink: string; 1411 public readonly meta: Record<string, any> | undefined; 1412 constructor(symlink: string, { meta }: { meta?: Record<string, any> } = {}) { 1413 this.symlink = symlink; 1414 this.meta = meta; 1415 } 1416 } 1417 1418 /** Extended options for mounting a virtual copy of an external file system via a `FileSet` */ 1419 export class Mount { 1420 public readonly source: string; 1421 public readonly resolver: FileSystemResolver; 1422 public readonly meta: Record<string, any> | undefined; 1423 constructor(source: string, resolver: FileSystemResolver, { meta }: { meta?: Record<string, any> } = {}) { 1424 this.source = source; 1425 this.resolver = resolver; 1426 this.meta = meta; 1427 } 1428 } 1429 1430 // a generic POSIX inode 1431 type Inode = FileInode | DirectoryInode | SymlinkInode; 1432 1433 interface FileInode { 1434 dev: number; // device id 1435 ino: number; // inode id 1436 mode: number; // file mode 1437 atimeMs: number; // access time 1438 mtimeMs: number; // modified time 1439 ctimeMs: number; // status change time 1440 birthtimeMs: number; // creation time 1441 nlink: number; // number of hard links 1442 size?: number; 1443 buffer?: Buffer; 1444 source?: string; 1445 resolver?: FileSystemResolver; 1446 shadowRoot?: FileInode; 1447 meta?: collections.Metadata; 1448 } 1449 1450 interface DirectoryInode { 1451 dev: number; // device id 1452 ino: number; // inode id 1453 mode: number; // file mode 1454 atimeMs: number; // access time 1455 mtimeMs: number; // modified time 1456 ctimeMs: number; // status change time 1457 birthtimeMs: number; // creation time 1458 nlink: number; // number of hard links 1459 links?: collections.SortedMap<string, Inode>; 1460 source?: string; 1461 resolver?: FileSystemResolver; 1462 shadowRoot?: DirectoryInode; 1463 meta?: collections.Metadata; 1464 } 1465 1466 interface SymlinkInode { 1467 dev: number; // device id 1468 ino: number; // inode id 1469 mode: number; // file mode 1470 atimeMs: number; // access time 1471 mtimeMs: number; // modified time 1472 ctimeMs: number; // status change time 1473 birthtimeMs: number; // creation time 1474 nlink: number; // number of hard links 1475 symlink: string; 1476 shadowRoot?: SymlinkInode; 1477 meta?: collections.Metadata; 1478 } 1479 1480 function isEmptyNonShadowedDirectory(node: DirectoryInode) { 1481 return !node.links && !node.shadowRoot && !node.resolver && !node.source; 1482 } 1483 1484 function isEmptyNonShadowedFile(node: FileInode) { 1485 return !node.buffer && !node.shadowRoot && !node.resolver && !node.source; 1486 } 1487 1488 function isFile(node: Inode | undefined): node is FileInode { 1489 return node !== undefined && (node.mode & S_IFMT) === S_IFREG; 1490 } 1491 1492 function isDirectory(node: Inode | undefined): node is DirectoryInode { 1493 return node !== undefined && (node.mode & S_IFMT) === S_IFDIR; 1494 } 1495 1496 function isSymlink(node: Inode | undefined): node is SymlinkInode { 1497 return node !== undefined && (node.mode & S_IFMT) === S_IFLNK; 1498 } 1499 1500 interface WalkResult { 1501 realpath: string; 1502 basename: string; 1503 parent: DirectoryInode | undefined; 1504 links: collections.SortedMap<string, Inode>; 1505 node: Inode | undefined; 1506 } 1507 1508 let builtLocalHost: FileSystemResolverHost | undefined; 1509 let builtLocalCI: FileSystem | undefined; 1510 let builtLocalCS: FileSystem | undefined; 1511 1512 function getBuiltLocal(host: FileSystemResolverHost, ignoreCase: boolean): FileSystem { 1513 if (builtLocalHost !== host) { 1514 builtLocalCI = undefined; 1515 builtLocalCS = undefined; 1516 builtLocalHost = host; 1517 } 1518 if (!builtLocalCI) { 1519 const resolver = createResolver(host); 1520 builtLocalCI = new FileSystem(/*ignoreCase*/ true, { 1521 files: { 1522 [builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver), 1523 [testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver), 1524 [projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver), 1525 [srcFolder]: {} 1526 }, 1527 cwd: srcFolder, 1528 meta: { defaultLibLocation: builtFolder } 1529 }); 1530 builtLocalCI.makeReadonly(); 1531 } 1532 if (ignoreCase) return builtLocalCI; 1533 if (!builtLocalCS) { 1534 builtLocalCS = builtLocalCI.shadow(/*ignoreCase*/ false); 1535 builtLocalCS.makeReadonly(); 1536 } 1537 return builtLocalCS; 1538 } 1539 1540 /* eslint-disable no-null/no-null */ 1541 function normalizeFileSetEntry(value: FileSet[string]) { 1542 if (value === undefined || 1543 value === null || 1544 value instanceof Directory || 1545 value instanceof File || 1546 value instanceof Link || 1547 value instanceof Symlink || 1548 value instanceof Mount || 1549 value instanceof Rmdir || 1550 value instanceof Unlink) { 1551 return value; 1552 } 1553 return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value); 1554 } 1555 1556 export function formatPatch(patch: FileSet): string; 1557 export function formatPatch(patch: FileSet | undefined): string | null; 1558 export function formatPatch(patch: FileSet | undefined) { 1559 return patch ? formatPatchWorker("", patch) : null; 1560 } 1561 /* eslint-enable no-null/no-null */ 1562 1563 function formatPatchWorker(dirname: string, container: FileSet): string { 1564 let text = ""; 1565 for (const name of Object.keys(container)) { 1566 const entry = normalizeFileSetEntry(container[name]); 1567 const file = dirname ? vpath.combine(dirname, name) : name; 1568 // eslint-disable-next-line no-null/no-null 1569 if (entry === null || entry === undefined || entry instanceof Unlink || entry instanceof Rmdir) { 1570 text += `//// [${file}] unlink\r\n`; 1571 } 1572 else if (entry instanceof Rmdir) { 1573 text += `//// [${vpath.addTrailingSeparator(file)}] rmdir\r\n`; 1574 } 1575 else if (entry instanceof Directory) { 1576 text += formatPatchWorker(file, entry.files); 1577 } 1578 else if (entry instanceof SameFileContentFile) { 1579 text += `//// [${file}] file written with same contents\r\n`; 1580 } 1581 else if (entry instanceof File) { 1582 const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8"); 1583 text += `//// [${file}]\r\n${content}\r\n\r\n`; 1584 } 1585 else if (entry instanceof Link) { 1586 text += `//// [${file}] link(${entry.path})\r\n`; 1587 } 1588 else if (entry instanceof Symlink) { 1589 text += `//// [${file}] symlink(${entry.symlink})\r\n`; 1590 } 1591 else if (entry instanceof Mount) { 1592 text += `//// [${file}] mount(${entry.source})\r\n`; 1593 } 1594 } 1595 return text; 1596 } 1597 1598 export function iteratePatch(patch: FileSet | undefined): IterableIterator<[string, string]> | null { 1599 // eslint-disable-next-line no-null/no-null 1600 return patch ? Harness.Compiler.iterateOutputs(iteratePatchWorker("", patch)) : null; 1601 } 1602 1603 function* iteratePatchWorker(dirname: string, container: FileSet): IterableIterator<documents.TextDocument> { 1604 for (const name of Object.keys(container)) { 1605 const entry = normalizeFileSetEntry(container[name]); 1606 const file = dirname ? vpath.combine(dirname, name) : name; 1607 if (entry instanceof Directory) { 1608 yield* ts.arrayFrom(iteratePatchWorker(file, entry.files)); 1609 } 1610 else if (entry instanceof File) { 1611 const content = typeof entry.data === "string" ? entry.data : entry.data.toString("utf8"); 1612 yield new documents.TextDocument(file, content); 1613 } 1614 } 1615 } 1616} 1617