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