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