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