• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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