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