import { Dirent , ReadStream , WriteStream } from 'fs' ; import { URI } from 'vscode-uri' ; import { FileSystem , FileWatcher , FileWatcherEventHandler , TmpfileOptions } from '../../../common/fileSystem' ; import * as pathUtil from '../../../common/pathUtils' ; import { bufferFrom , createIOError } from '../utils' ; import { closeIterator , getIterator , Metadata , nextResult , SortedMap } from './../utils' ; import { validate , ValidationFlags } from './pathValidation' ; export const MODULE_PATH = pathUtil . normalizeSlashes ( '/' ) ; let devCount = 0 ; let inoCount = 0 ; export interface DiffOptions { includeChangedFileWithSameContent ? : boolean ; } /**
 * Represents a virtual POSIX-like file system.
 */ export class TestFileSystem implements FileSystem { /** Indicates whether the file system is case-sensitive (`false`) or case-insensitive (`true`). */ readonly ignoreCase : boolean ; /** Gets the comparison function used to compare two paths. */ readonly stringComparer : ( a : string , b : string ) => number ; private _lazy : { links ? : SortedMap < string , Inode > ; shadows ? : Map < number , Inode > ; meta ? : Metadata ; } = { } ; private _cwd : string ; private _time : number | Date | ( ( ) => number | Date ) ; private _shadowRoot : TestFileSystem | undefined ; private _dirStack : string [ ] | undefined ; private _tmpfileCounter = 0 ; constructor ( ignoreCase , options = { } ) { const { time = - 1 , files , meta } = options ; this . ignoreCase = ignoreCase ; this . stringComparer = this . ignoreCase ? pathUtil . comparePathsCaseInsensitive : pathUtil . comparePathsCaseSensitive ; this . _time = time ; if ( meta ) { for ( const key of Object . keys ( meta ) ) { this . meta . set ( key , meta [ key ] ) ; } } if ( files ) { this . _applyFiles ( files , '' ) ; } let cwd = options . cwd ; if ( ( ! cwd || ! pathUtil . isDiskPathRoot ( cwd ) ) && this . _lazy . links ) { const iterator = getIterator ( this . _lazy . links . keys ( ) ) ; try { for ( let i = nextResult ( iterator ) ; i ; i = nextResult ( iterator ) ) { const name = i . value ; cwd = cwd ? pathUtil . resolvePaths ( name , cwd ) : name ; break ; } } finally { closeIterator ( iterator ) ; } } if ( cwd ) { validate ( cwd , ValidationFlags . Absolute ) ; this . mkdirpSync ( cwd ) ; } this . _cwd = cwd || '' ; } /**
     * Gets metadata for this `FileSystem`.
     */ get meta ( ) : Metadata { if ( ! this . _lazy . meta ) { this . _lazy . meta = new Metadata ( this . _shadowRoot ? this . _shadowRoot . meta : undefined ) ; } return this . _lazy . meta ; } /**
     * Gets a value indicating whether the file system is read-only.
     */ get isReadonly ( ) { return Object . isFrozen ( this ) ; } /**
     * Makes the file system read-only.
     */ makeReadonly ( ) { Object . freeze ( this ) ; return this ; } /**
     * Gets the file system shadowed by this file system.
     */ get shadowRoot ( ) { return this . _shadowRoot ; } /**
     * Snapshots the current file system, effectively shadowing itself. This is useful for
     * generating file system patches using `.diff()` from one snapshot to the next. Performs
     * no action if this file system is read-only.
     */ snapshot ( ) { if ( this . isReadonly ) { return ; } const fs = new TestFileSystem ( this . ignoreCase , { time : this . _time } ) ; fs . _lazy = this . _lazy ; fs . _cwd = this . _cwd ; fs . _time = this . _time ; fs . _shadowRoot = this . _shadowRoot ; fs . _dirStack = this . _dirStack ; fs . makeReadonly ( ) ; this . _lazy = { } ; this . _shadowRoot = fs ; } /**
     * Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
     * original, allowing multiple copies of the same core file system without multiple copies
     * of the same data.
     */ shadow ( ignoreCase = this . ignoreCase ) { if ( ! this . isReadonly ) { throw new Error ( 'Cannot shadow a mutable file system.' ) ; } if ( ignoreCase && ! this . ignoreCase ) { throw new Error ( 'Cannot create a case-insensitive file system from a case-sensitive one.' ) ; } const fs = new TestFileSystem ( ignoreCase , { time : this . _time } ) ; fs . _shadowRoot = this ; fs . _cwd = this . _cwd ; return fs ; } link time ( value ? ) { if ( value !== undefined && this . isReadonly ) { throw createIOError ( 'EPERM' ) ; } let result = this . _time ; if ( typeof result === 'function' ) { result = result ( ) ; } if ( typeof result === 'object' ) { result = result . getTime ( ) ; } if ( result === - 1 ) { result = Date . now ( ) ; } if ( value !== undefined ) { this . _time = value ; } return result ; } param path filemeta ( path ) { const { node } = this . _walk ( this . _resolve ( path ) ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } return this . _filemeta ( node ) ; } private _filemeta ( node ) { if ( ! node . meta ) { const parentMeta = node . shadowRoot && this . _shadowRoot && this . _shadowRoot . _filemeta ( node . shadowRoot ) ; node . meta = new Metadata ( parentMeta ) ; } return node . meta ; } link cwd ( ) { if ( ! this . _cwd ) { throw new Error ( 'The current working directory has not been set.' ) ; } const { node } = this . _walk ( this . _cwd ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( ! isDirectory ( node ) ) { throw createIOError ( 'ENOTDIR' ) ; } return this . _cwd ; } link chdir ( path ) { if ( this . isReadonly ) { throw createIOError ( 'EPERM' ) ; } path = this . _resolve ( path ) ; const { node } = this . _walk ( path ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( ! isDirectory ( node ) ) { throw createIOError ( 'ENOTDIR' ) ; } this . _cwd = path ; } /**
     * Pushes the current directory onto the directory stack and changes the current working directory to the supplied path.
     */ pushd ( path ? ) { if ( this . isReadonly ) { throw createIOError ( 'EPERM' ) ; } if ( path ) { path = this . _resolve ( path ) ; } if ( this . _cwd ) { if ( ! this . _dirStack ) { this . _dirStack = [ ] ; } this . _dirStack . push ( this . _cwd ) ; } if ( path && path !== this . _cwd ) { this . chdir ( path ) ; } } /**
     * Pops the previous directory from the location stack and changes the current directory to that directory.
     */ popd ( ) { if ( this . isReadonly ) { throw createIOError ( 'EPERM' ) ; } const path = this . _dirStack && this . _dirStack . pop ( ) ; if ( path ) { this . chdir ( path ) ; } } /**
     * Update the file system with a set of files.
     */ apply ( files ) { this . _applyFiles ( files , this . _cwd ) ; } param path param axis param traversal scanSync ( path , axis , traversal ) { path = this . _resolve ( path ) ; const results = [ ] ; this . _scan ( path , this . _stat ( this . _walk ( path ) ) , axis , traversal , false , results ) ; return results ; } param path param axis param traversal lscanSync ( path , axis , traversal ) { path = this . _resolve ( path ) ; const results = [ ] ; this . _scan ( path , this . _stat ( this . _walk ( path , true ) ) , axis , traversal , true , results ) ; return results ; } createFileSystemWatcher ( paths , listener ) { return { close : ( ) => { } , } ; } getModulePath ( ) { return MODULE_PATH ; } tmpdir ( ) { this . mkdirpSync ( '/tmp' ) ; return pathUtil . normalizeSlashes ( '/tmp' ) ; } tmpfile ( options ? ) { const prefix = options ?. prefix || 'tmp' ; const postfix = options ?. prefix ? '-' + options . prefix : '' ; const name = `${ prefix }-${ this . _tmpfileCounter ++ }${ postfix }` ; const path = pathUtil . combinePaths ( this . tmpdir ( ) , name ) ; this . writeFileSync ( path , '' ) ; return path ; } realCasePath ( path ) { return path ; } isMappedFilePath ( filepath ) { return false ; } getOriginalFilePath ( mappedFilePath ) { return mappedFilePath ; } getMappedFilePath ( originalFilepath ) { return originalFilepath ; } getUri ( path ) { return URI . file ( path ) . toString ( ) ; } private _scan ( path , stats , axis , traversal , noFollow , results ) { if ( axis === 'ancestors-or-self' || axis === 'self' || axis === 'descendants-or-self' ) { if ( ! traversal . accept || traversal . accept ( path , stats ) ) { results . push ( path ) ; } } if ( axis === 'ancestors-or-self' || axis === 'ancestors' ) { const dirname = pathUtil . getDirectoryPath ( path ) ; if ( dirname !== path ) { try { const stats = this . _stat ( this . _walk ( dirname , noFollow ) ) ; if ( ! traversal . traverse || traversal . traverse ( dirname , stats ) ) { this . _scan ( dirname , stats , 'ancestors-or-self' , traversal , noFollow , results ) ; } } catch { } } } if ( axis === 'descendants-or-self' || axis === 'descendants' ) { if ( stats . isDirectory ( ) && ( ! traversal . traverse || traversal . traverse ( path , stats ) ) ) { for ( const file of this . readdirSync ( path ) ) { try { const childpath = pathUtil . combinePaths ( path , file ) ; const stats = this . _stat ( this . _walk ( childpath , noFollow ) ) ; this . _scan ( childpath , stats , 'descendants-or-self' , traversal , noFollow , results ) ; } catch { } } } } } param source param target param resolver mountSync ( source , target , resolver ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } source = validate ( source , ValidationFlags . Absolute ) ; const { parent , links , node : existingNode , basename } = this . _walk ( this . _resolve ( target ) , true ) ; if ( existingNode ) { throw createIOError ( 'EEXIST' ) ; } const time = this . time ( ) ; const node = this . _mknod ( parent ? parent . dev : ++ devCount , S_IFDIR , 0o777 , time ) ; node . source = source ; node . resolver = resolver ; this . _addLink ( parent , links , basename , node , time ) ; } /**
     * Recursively remove all files and directories underneath the provided path.
     */ rimrafSync ( path ) { try { const stats = this . lstatSync ( path ) ; if ( stats . isFile ( ) || stats . isSymbolicLink ( ) ) { this . unlinkSync ( path ) ; } else if ( stats . isDirectory ( ) ) { for ( const file of this . readdirSync ( path ) ) { this . rimrafSync ( pathUtil . combinePaths ( path , file ) ) ; } this . rmdirSync ( path ) ; } } catch ( e ) { if ( e . code === 'ENOENT' ) { return ; } throw e ; } } /**
     * Make a directory and all of its parent paths (if they don't exist).
     */ mkdirpSync ( path ) { path = this . _resolve ( path ) ; const result = this . _walk ( path , true , ( error , result ) => { if ( error . code === 'ENOENT' ) { this . _mkdir ( result ) ; return 'retry' ; } return 'throw' ; } ) ; if ( ! result . node ) { this . _mkdir ( result ) ; } } getFileListing ( ) { let result = '' ; const printLinks = ( dirname , links ) => { const iterator = getIterator ( links ) ; try { for ( let i = nextResult ( iterator ) ; i ; i = nextResult ( iterator ) ) { const [ name , node ] = i . value ; const path = dirname ? pathUtil . combinePaths ( dirname , name ) : name ; const marker = pathUtil . comparePaths ( this . _cwd , path , this . ignoreCase ) === 0 ? '*' : ' ' ; if ( result ) { result += '\n' ; } result += marker ; if ( isDirectory ( node ) ) { result += pathUtil . ensureTrailingDirectorySeparator ( path ) ; printLinks ( path , this . _getLinks ( node ) ) ; } else if ( isFile ( node ) ) { result += path ; } else if ( isSymlink ( node ) ) { result += `${ path } -> ${ node . symlink }` ; } } } finally { closeIterator ( iterator ) ; } } ; printLinks ( undefined , this . _getRootLinks ( ) ) ; return result ; } /**
     * Print diagnostic information about the structure of the file system to the console.
     */ debugPrint ( ) { console . log ( this . getFileListing ( ) ) ; } /**
     * Determines whether a path exists.
     */ existsSync ( path ) { const result = this . _walk ( this . _resolve ( path ) , true , ( ) => 'stop' ) ; return result !== undefined && result . node !== undefined ; } link statSync ( path ) { return this . _stat ( this . _walk ( this . _resolve ( path ) ) ) ; } /**
     * Change file access times
     *
     * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
     */ utimesSync ( path , atime , mtime ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } if ( ! isFinite ( + atime ) || ! isFinite ( + mtime ) ) { throw createIOError ( 'EINVAL' ) ; } const entry = this . _walk ( this . _resolve ( path ) ) ; if ( ! entry || ! entry . node ) { throw createIOError ( 'ENOENT' ) ; } entry . node . atimeMs = + atime ; entry . node . mtimeMs = + mtime ; entry . node . ctimeMs = this . time ( ) ; } link lstatSync ( path ) { return this . _stat ( this . _walk ( this . _resolve ( path ) , true ) ) ; } private _stat ( entry ) { const node = entry . node ; if ( ! node ) { throw createIOError ( `ENOENT` , entry . realpath ) ; } return new Stats ( node . dev , node . ino , node . mode , node . nlink , 0 , isFile ( node ) ? this . _getSize ( node ) : isSymlink ( node ) ? node . symlink . length : 0 , 4096 , 0 , node . atimeMs , node . mtimeMs , node . ctimeMs , node . birthtimeMs ) ; } link readdirSync ( path ) { const { node } = this . _walk ( this . _resolve ( path ) ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( ! isDirectory ( node ) ) { throw createIOError ( 'ENOTDIR' ) ; } return Array . from ( this . _getLinks ( node ) . keys ( ) ) ; } link readdirEntriesSync ( path ) { const { node } = this . _walk ( this . _resolve ( path ) ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( ! isDirectory ( node ) ) { throw createIOError ( 'ENOTDIR' ) ; } const entries = Array . from ( this . _getLinks ( node ) . entries ( ) ) ; return entries . map ( ( [ k , v ] ) => makeDirEnt ( k , v ) ) ; } link mkdirSync ( path ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } this . _mkdir ( this . _walk ( this . _resolve ( path ) , true ) ) ; } private _mkdir ( { parent , links , node : existingNode , basename } ) { if ( existingNode ) { throw createIOError ( 'EEXIST' ) ; } const time = this . time ( ) ; const node = this . _mknod ( parent ? parent . dev : ++ devCount , S_IFDIR , 0o777 , time ) ; this . _addLink ( parent , links , basename , node , time ) ; } link rmdirSync ( path ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } path = this . _resolve ( path ) ; const { parent , links , node , basename } = this . _walk ( path , true ) ; if ( ! parent ) { throw createIOError ( 'EPERM' ) ; } if ( ! isDirectory ( node ) ) { throw createIOError ( 'ENOTDIR' ) ; } if ( this . _getLinks ( node ) . size !== 0 ) { throw createIOError ( 'ENOTEMPTY' ) ; } this . _removeLink ( parent , links , basename , node ) ; } link linkSync ( oldpath , newpath ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } const { node } = this . _walk ( this . _resolve ( oldpath ) ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( isDirectory ( node ) ) { throw createIOError ( 'EPERM' ) ; } const { parent , links , basename , node : existingNode } = this . _walk ( this . _resolve ( newpath ) , true ) ; if ( ! parent ) { throw createIOError ( 'EPERM' ) ; } if ( existingNode ) { throw createIOError ( 'EEXIST' ) ; } this . _addLink ( parent , links , basename , node ) ; } link unlinkSync ( path ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } const { parent , links , node , basename } = this . _walk ( this . _resolve ( path ) , true ) ; if ( ! parent ) { throw createIOError ( 'EPERM' ) ; } if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( isDirectory ( node ) ) { throw createIOError ( 'EISDIR' ) ; } this . _removeLink ( parent , links , basename , node ) ; } link renameSync ( oldpath , newpath ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } const { parent : oldParent , links : oldParentLinks , node , basename : oldBasename , } = this . _walk ( this . _resolve ( oldpath ) , true ) ; if ( ! oldParent ) { throw createIOError ( 'EPERM' ) ; } if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } const { parent : newParent , links : newParentLinks , node : existingNode , basename : newBasename , } = this . _walk ( this . _resolve ( newpath ) , true ) ; if ( ! newParent ) { throw createIOError ( 'EPERM' ) ; } const time = this . time ( ) ; if ( existingNode ) { if ( isDirectory ( node ) ) { if ( ! isDirectory ( existingNode ) ) { throw createIOError ( 'ENOTDIR' ) ; } if ( this . _getLinks ( existingNode ) . size > 0 ) { throw createIOError ( 'ENOTEMPTY' ) ; } } else { if ( isDirectory ( existingNode ) ) { throw createIOError ( 'EISDIR' ) ; } } this . _removeLink ( newParent , newParentLinks , newBasename , existingNode , time ) ; } this . _replaceLink ( oldParent , oldParentLinks , oldBasename , newParent , newParentLinks , newBasename , node , time ) ; } link symlinkSync ( target , linkpath ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } const { parent , links , node : existingNode , basename } = this . _walk ( this . _resolve ( linkpath ) , true ) ; if ( ! parent ) { throw createIOError ( 'EPERM' ) ; } if ( existingNode ) { throw createIOError ( 'EEXIST' ) ; } const time = this . time ( ) ; const node = this . _mknod ( parent . dev , S_IFLNK , 0o666 , time ) ; node . symlink = validate ( target , ValidationFlags . RelativeOrAbsolute ) ; this . _addLink ( parent , links , basename , node , time ) ; } link realpathSync ( path ) { const { realpath } = this . _walk ( this . _resolve ( path ) ) ; return realpath ; } /**
     * Read from a file.
     *
     * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
     */ readFileSync ( path , encoding ? ) ; /**
     * Read from a file.
     *
     * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
     */ readFileSync ( path , encoding ) ; /**
     * Read from a file.
     *
     * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
     */ readFileSync ( path , encoding ? ) ; readFileSync ( path , encoding = null ) { const { node } = this . _walk ( this . _resolve ( path ) ) ; if ( ! node ) { throw createIOError ( 'ENOENT' ) ; } if ( isDirectory ( node ) ) { throw createIOError ( 'EISDIR' ) ; } if ( ! isFile ( node ) ) { throw createIOError ( 'EBADF' ) ; } const buffer = this . _getBuffer ( node ) . slice ( ) ; return encoding ? buffer . toString ( encoding ) : buffer ; } /**
     * Write to a file.
     *
     * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
     */ writeFileSync ( path , data , encoding = null ) { if ( this . isReadonly ) { throw createIOError ( 'EROFS' ) ; } const { parent , links , node : existingNode , basename } = this . _walk ( this . _resolve ( path ) , false ) ; if ( ! parent ) { throw createIOError ( 'EPERM' ) ; } const time = this . time ( ) ; let node = existingNode ; if ( ! node ) { node = this . _mknod ( parent . dev , S_IFREG , 0o666 , time ) ; this . _addLink ( parent , links , basename , node , time ) ; } if ( isDirectory ( node ) ) { throw createIOError ( 'EISDIR' ) ; } if ( ! isFile ( node ) ) { throw createIOError ( 'EBADF' ) ; } node . buffer = Buffer . isBuffer ( data ) ? data . slice ( ) : bufferFrom ( '' + data , ( encoding as BufferEncoding ) || 'utf8' ) ; node . size = node . buffer . byteLength ; node . mtimeMs = time ; node . ctimeMs = time ; } readFile ( filePath ) { return Promise . resolve ( this . readFileSync ( filePath ) ) ; } readFileText ( filePath , encoding ? ) { return Promise . resolve ( this . readFileSync ( filePath , encoding || 'utf8' ) ) ; } createReadStream ( path ) { throw new Error ( 'Not implemented in test file system.' ) ; } createWriteStream ( path ) { throw new Error ( 'Not implemented in test file system.' ) ; } copyFileSync ( src , dst ) { throw new Error ( 'Not implemented in test file system.' ) ; } param base diff ( base = this . shadowRoot , options = { } ) { const differences = { } ; const hasDifferences = base ? TestFileSystem . _rootDiff ( differences , this , base , options ) : TestFileSystem . _trackCreatedInodes ( differences , this , this . _getRootLinks ( ) ) ; return hasDifferences ? differences : undefined ; } /**
     * Generates a `FileSet` patch containing all the entries in `changed` that are not in `base`.
     */ static diff ( changed , base , options = { } ) { const differences = { } ; return TestFileSystem . _rootDiff ( differences , changed , base , options ) ? differences : undefined ; } isInZipOrEgg ( path ) { return false ; } private static _diffWorker ( container , changed , changedLinks , base , baseLinks , options ) { if ( changedLinks && ! baseLinks ) { return TestFileSystem . _trackCreatedInodes ( container , changed , changedLinks ) ; } if ( baseLinks && ! changedLinks ) { return TestFileSystem . _trackDeletedInodes ( container , baseLinks ) ; } if ( changedLinks && baseLinks ) { let hasChanges = false ; baseLinks . forEach ( ( node , basename ) => { if ( ! changedLinks . has ( basename ) ) { container [ basename ] = isDirectory ( node ) ? new Rmdir ( ) : new Unlink ( ) ; hasChanges = true ; } } ) ; changedLinks . forEach ( ( changedNode , basename ) => { const baseNode = baseLinks . get ( basename ) ; if ( baseNode ) { if ( isDirectory ( changedNode ) && isDirectory ( baseNode ) ) { return ( hasChanges = TestFileSystem . _directoryDiff ( container , basename , changed , changedNode , base , baseNode , options ) || hasChanges ) ; } if ( isFile ( changedNode ) && isFile ( baseNode ) ) { return ( hasChanges = TestFileSystem . _fileDiff ( container , basename , changed , changedNode , base , baseNode , options ) || hasChanges ) ; } if ( isSymlink ( changedNode ) && isSymlink ( baseNode ) ) { return ( hasChanges = TestFileSystem . _symlinkDiff ( container , basename , changedNode , baseNode ) || hasChanges ) ; } } return ( hasChanges = TestFileSystem . _trackCreatedInode ( container , basename , changed , changedNode ) || hasChanges ) ; } ) ; return hasChanges ; } return false ; } private static _rootDiff ( container , changed , base , options ) { while ( ! changed . _lazy . links && changed . _shadowRoot ) { changed = changed . _shadowRoot ; } while ( ! base . _lazy . links && base . _shadowRoot ) { base = base . _shadowRoot ; } if ( changed === base ) { return false ; } if ( ! changed . _lazy . links && ! changed . _shadowRoot && ! base . _lazy . links && ! base . _shadowRoot ) { return false ; } return TestFileSystem . _diffWorker ( container , changed , changed . _getRootLinks ( ) , base , base . _getRootLinks ( ) , options ) ; } private static _directoryDiff ( container , basename , changed , changedNode , base , baseNode , options ) { while ( ! changedNode . links && changedNode . shadowRoot ) { changedNode = changedNode . shadowRoot ; } while ( ! baseNode . links && baseNode . shadowRoot ) { baseNode = baseNode . shadowRoot ; } if ( changedNode === baseNode ) { return false ; } if ( isEmptyNonShadowedDirectory ( changedNode ) && isEmptyNonShadowedDirectory ( baseNode ) ) { return false ; } if ( ! changedNode . links && ! baseNode . links && changedNode . resolver && changedNode . source !== undefined && baseNode . resolver === changedNode . resolver && baseNode . source === changedNode . source ) { return false ; } const children = { } ; if ( ! TestFileSystem . _diffWorker ( children , changed , changed . _getLinks ( changedNode ) , base , base . _getLinks ( baseNode ) , options ) ) { return false ; } container [ basename ] = new Directory ( children ) ; return true ; } private static _fileDiff ( container , basename , changed , changedNode , base , baseNode , options ) { while ( ! changedNode . buffer && changedNode . shadowRoot ) { changedNode = changedNode . shadowRoot ; } while ( ! baseNode . buffer && baseNode . shadowRoot ) { baseNode = baseNode . shadowRoot ; } if ( changedNode === baseNode ) { return false ; } if ( isEmptyNonShadowedFile ( changedNode ) && isEmptyNonShadowedFile ( baseNode ) ) { return false ; } if ( ! changedNode . buffer && ! baseNode . buffer && changedNode . resolver && changedNode . source !== undefined && baseNode . resolver === changedNode . resolver && baseNode . source === changedNode . source ) { return false ; } const changedBuffer = changed . _getBuffer ( changedNode ) ; const baseBuffer = base . _getBuffer ( baseNode ) ; if ( changedBuffer === baseBuffer ) { return false ; } if ( Buffer . compare ( changedBuffer , baseBuffer ) === 0 ) { if ( ! options . includeChangedFileWithSameContent ) { return false ; } container [ basename ] = new SameFileContentFile ( changedBuffer ) ; return true ; } container [ basename ] = new File ( changedBuffer ) ; return true ; } private static _symlinkDiff ( container , basename , changedNode , baseNode ) { if ( changedNode . symlink === baseNode . symlink ) { return false ; } container [ basename ] = new Symlink ( changedNode . symlink ) ; return true ; } private static _trackCreatedInode ( container , basename , changed , node ) { if ( isDirectory ( node ) ) { const children = { } ; TestFileSystem . _trackCreatedInodes ( children , changed , changed . _getLinks ( node ) ) ; container [ basename ] = new Directory ( children ) ; } else if ( isSymlink ( node ) ) { container [ basename ] = new Symlink ( node . symlink ) ; } else { container [ basename ] = new File ( node . buffer || '' ) ; } return true ; } private static _trackCreatedInodes ( container , changed , changedLinks ) { if ( ! changedLinks . size ) { return false ; } changedLinks . forEach ( ( node , basename ) => { TestFileSystem . _trackCreatedInode ( container , basename , changed , node ) ; } ) ; return true ; } private static _trackDeletedInodes ( container , baseLinks ) { if ( ! baseLinks . size ) { return false ; } baseLinks . forEach ( ( node , basename ) => { container [ basename ] = isDirectory ( node ) ? new Rmdir ( ) : new Unlink ( ) ; } ) ; return true ; } private _mknod ( dev , type , mode , time ? ) ; private _mknod ( dev , type , mode , time ? ) ; private _mknod ( dev , type , mode , time ? ) ; private _mknod ( dev , type , mode , time = this . time ( ) ) { return { dev , ino : ++ inoCount , mode : ( mode & ~ S_IFMT & ~ 0o022 & 0o7777 ) | ( type & S_IFMT ) , atimeMs : time , mtimeMs : time , ctimeMs : time , birthtimeMs : time , nlink : 0 , } ; } private _addLink ( parent , links , name , node , time = this . time ( ) ) { links . set ( name , node ) ; node . nlink ++ ; node . ctimeMs = time ; if ( parent ) { parent . mtimeMs = time ; } if ( ! parent && ! this . _cwd ) { this . _cwd = name ; } } private _removeLink ( parent , links , name , node , time = this . time ( ) ) { links . delete ( name ) ; node . nlink -- ; node . ctimeMs = time ; if ( parent ) { parent . mtimeMs = time ; } } private _replaceLink ( oldParent , oldLinks , oldName , newParent , newLinks , newName , node , time ) { if ( oldParent !== newParent ) { this . _removeLink ( oldParent , oldLinks , oldName , node , time ) ; this . _addLink ( newParent , newLinks , newName , node , time ) ; } else { oldLinks . delete ( oldName ) ; oldLinks . set ( newName , node ) ; oldParent . mtimeMs = time ; newParent . mtimeMs = time ; } } private _getRootLinks ( ) { if ( ! this . _lazy . links ) { const links = new SortedMap < string , Inode > ( this . stringComparer ) ; if ( this . _shadowRoot ) { this . _copyShadowLinks ( this . _shadowRoot . _getRootLinks ( ) , links ) ; } this . _lazy . links = links ; } return this . _lazy . links ; } private _getLinks ( node ) { if ( ! node . links ) { const links = new SortedMap < string , Inode > ( this . stringComparer ) ; const { source , resolver } = node ; if ( source && resolver ) { node . source = undefined ; node . resolver = undefined ; for ( const name of resolver . readdirSync ( source ) ) { const path = pathUtil . combinePaths ( source , name ) ; const stats = resolver . statSync ( path ) ; switch ( stats . mode & S_IFMT ) { case S_IFDIR : { const dir = this . _mknod ( node . dev , S_IFDIR , 0o777 ) ; dir . source = pathUtil . combinePaths ( source , name ) ; dir . resolver = resolver ; this . _addLink ( node , links , name , dir ) ; break ; } case S_IFREG : { const file = this . _mknod ( node . dev , S_IFREG , 0o666 ) ; file . source = pathUtil . combinePaths ( source , name ) ; file . resolver = resolver ; file . size = stats . size ; this . _addLink ( node , links , name , file ) ; break ; } } } } else if ( this . _shadowRoot && node . shadowRoot ) { this . _copyShadowLinks ( this . _shadowRoot . _getLinks ( node . shadowRoot ) , links ) ; } node . links = links ; } return node . links ; } private _getShadow ( root ) ; private _getShadow ( root ) ; private _getShadow ( root ) { const shadows = this . _lazy . shadows || ( this . _lazy . shadows = new Map < number , Inode > ( ) ) ; let shadow = shadows . get ( root . ino ) ; if ( ! shadow ) { shadow = { dev : root . dev , ino : root . ino , mode : root . mode , atimeMs : root . atimeMs , mtimeMs : root . mtimeMs , ctimeMs : root . ctimeMs , birthtimeMs : root . birthtimeMs , nlink : root . nlink , shadowRoot : root , } ; if ( isSymlink ( root ) ) { ( shadow as SymlinkInode ) . symlink = root . symlink ; } shadows . set ( shadow . ino , shadow ) ; } return shadow ; } private _copyShadowLinks ( source , target ) { const iterator = getIterator ( source ) ; try { for ( let i = nextResult ( iterator ) ; i ; i = nextResult ( iterator ) ) { const [ name , root ] = i . value ; target . set ( name , this . _getShadow ( root ) ) ; } } finally { closeIterator ( iterator ) ; } } private _getSize ( node ) { if ( node . buffer ) { return node . buffer . byteLength ; } if ( node . size !== undefined ) { return node . size ; } if ( node . source && node . resolver ) { return ( node . size = node . resolver . statSync ( node . source ) . size ) ; } if ( this . _shadowRoot && node . shadowRoot ) { return ( node . size = this . _shadowRoot . _getSize ( node . shadowRoot ) ) ; } return 0 ; } private _getBuffer ( node ) { if ( ! node . buffer ) { const { source , resolver } = node ; if ( source && resolver ) { node . source = undefined ; node . resolver = undefined ; node . size = undefined ; node . buffer = resolver . readFileSync ( source ) ; } else if ( this . _shadowRoot && node . shadowRoot ) { node . buffer = this . _shadowRoot . _getBuffer ( node . shadowRoot ) ; } else { node . buffer = Buffer . allocUnsafe ( 0 ) ; } } return node . buffer ; } param path param noFollow link private _walk ( path , noFollow ? , onError ? ) ; private _walk ( path , noFollow ? , onError ? ) ; private _walk ( path , noFollow ? , onError ? ) { let links = this . _getRootLinks ( ) ; let parent ; let components = pathUtil . getPathComponents ( path ) ; let step = 0 ; let depth = 0 ; let retry = false ; while ( true ) { if ( depth >= 40 ) { throw createIOError ( 'ELOOP' ) ; } const lastStep = step === components . length - 1 ; const basename = components [ step ] ; const node = links . get ( basename ) ; if ( lastStep && ( noFollow || ! isSymlink ( node ) ) ) { return { realpath : pathUtil . combinePathComponents ( components ) , basename , parent , links , node } ; } if ( node === undefined ) { if ( trapError ( createIOError ( 'ENOENT' ) , node ) ) { continue ; } return undefined ; } if ( isSymlink ( node ) ) { const dirname = pathUtil . combinePathComponents ( components . slice ( 0 , step ) ) ; const symlink = pathUtil . resolvePaths ( dirname , node . symlink ) ; links = this . _getRootLinks ( ) ; parent = undefined ; components = pathUtil . getPathComponents ( symlink ) . concat ( components . slice ( step + 1 ) ) ; step = 0 ; depth ++ ; retry = false ; continue ; } if ( isDirectory ( node ) ) { links = this . _getLinks ( node ) ; parent = node ; step ++ ; retry = false ; continue ; } if ( trapError ( createIOError ( 'ENOTDIR' ) , node ) ) { continue ; } return undefined ; } function trapError ( error , node ? ) { const realpath = pathUtil . combinePathComponents ( components . slice ( 0 , step + 1 ) ) ; const basename = components [ step ] ; const result = ! retry && onError ? onError ( error , { realpath , basename , parent , links , node } ) : 'throw' ; if ( result === 'stop' ) { return false ; } if ( result === 'retry' ) { retry = true ; return true ; } throw error ; } } /**
     * Resolve a path relative to the current working directory.
     */ private _resolve ( path ) { return this . _cwd ? pathUtil . resolvePaths ( this . _cwd , validate ( path , ValidationFlags . RelativeOrAbsolute | ValidationFlags . AllowWildcard ) ) : validate ( path , ValidationFlags . Absolute | ValidationFlags . AllowWildcard ) ; } private _applyFiles ( files , dirname ) { const deferred = [ ] ; this . _applyFilesWorker ( files , dirname , deferred ) ; for ( const [ entry , path ] of deferred ) { this . mkdirpSync ( pathUtil . getDirectoryPath ( path ) ) ; this . pushd ( pathUtil . getDirectoryPath ( path ) ) ; if ( entry instanceof Symlink ) { if ( this . stringComparer ( pathUtil . getDirectoryPath ( path ) , path ) === 0 ) { throw new TypeError ( 'Roots cannot be symbolic links.' ) ; } this . symlinkSync ( pathUtil . resolvePaths ( dirname , entry . symlink ) , path ) ; this . _applyFileExtendedOptions ( path , entry ) ; } else if ( entry instanceof Link ) { if ( this . stringComparer ( pathUtil . getDirectoryPath ( path ) , path ) === 0 ) { throw new TypeError ( 'Roots cannot be hard links.' ) ; } this . linkSync ( entry . path , path ) ; } else { this . mountSync ( entry . source , path , entry . resolver ) ; this . _applyFileExtendedOptions ( path , entry ) ; } this . popd ( ) ; } } private _applyFileExtendedOptions ( path , entry ) { const { meta } = entry ; if ( meta !== undefined ) { const filemeta = this . filemeta ( path ) ; for ( const key of Object . keys ( meta ) ) { filemeta . set ( key , meta [ key ] ) ; } } } private _applyFilesWorker ( files , dirname , deferred ) { for ( const key of Object . keys ( files ) ) { const value = normalizeFileSetEntry ( files [ key ] ) ; const path = dirname ? pathUtil . resolvePaths ( dirname , key ) : key ; validate ( path , ValidationFlags . Absolute ) ; if ( value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink ) { if ( this . stringComparer ( pathUtil . getDirectoryPath ( path ) , path ) === 0 ) { throw new TypeError ( 'Roots cannot be deleted.' ) ; } this . rimrafSync ( path ) ; } else if ( value instanceof File ) { if ( this . stringComparer ( pathUtil . getDirectoryPath ( path ) , path ) === 0 ) { throw new TypeError ( 'Roots cannot be files.' ) ; } this . mkdirpSync ( pathUtil . getDirectoryPath ( path ) ) ; this . writeFileSync ( path , value . data , value . encoding ) ; this . _applyFileExtendedOptions ( path , value ) ; } else if ( value instanceof Directory ) { this . mkdirpSync ( path ) ; this . _applyFileExtendedOptions ( path , value ) ; this . _applyFilesWorker ( value . files , path , deferred ) ; } else { deferred . push ( [ value , path ] ) ; } } } } export interface FileSystemOptions { time ? : number | Date | ( ( ) => number | Date ) | undefined ; files ? : FileSet | undefined ; cwd ? : string | undefined ; meta ? : Record < string , any > | undefined ; } export type Axis = 'ancestors' | 'ancestors-or-self' | 'self' | 'descendants-or-self' | 'descendants' ; export interface Traversal { /** A function called to choose whether to continue to traverse to either ancestors or descendants. */ traverse ? ( path , stats ) : boolean ; /** A function called to choose whether to accept a path as part of the result. */ accept ? ( path , stats ) : boolean ; } export interface FileSystemResolver { statSync ( path ) : { mode : number ; size : number ; } ; readdirSync ( path ) : string [ ] ; readFileSync ( path ) : Buffer ; } /**
 * A template used to populate files, directories, links, etc. in a virtual file system.
 */ export interface FileSet { [ name ] : DirectoryLike | FileLike | Link | Symlink | Mount | Rmdir | Unlink | null | undefined ; } export type DirectoryLike = FileSet | Directory ; export type FileLike = File | Buffer | string ; /** Extended options for a directory in a `FileSet` */ export class Directory { readonly files : FileSet ; readonly meta : Record < string , any > | undefined ; constructor ( files , { meta } = { } ) { this . files = files ; this . meta = meta ; } } /** Extended options for a file in a `FileSet` */ export class File { readonly data : Buffer | string ; readonly encoding : BufferEncoding | undefined ; readonly meta : Record < string , any > | undefined ; constructor ( data , { meta , encoding } = { } ) { this . data = data ; this . encoding = encoding ; this . meta = meta ; } } export class SameFileContentFile extends File { constructor ( data , metaAndEncoding ? ) { super ( data , metaAndEncoding ) ; } } /** Extended options for a hard link in a `FileSet` */ export class Link { readonly path : string ; constructor ( path ) { this . path = path ; } } /** Removes a directory in a `FileSet` */ export class Rmdir { ' rmdirBrand' ? : never ; } /** Unlinks a file in a `FileSet` */ export class Unlink { ' unlinkBrand' ? : never ; } /** Extended options for a symbolic link in a `FileSet` */ export class Symlink { readonly symlink : string ; readonly meta : Record < string , any > | undefined ; constructor ( symlink , { meta } = { } ) { this . symlink = symlink ; this . meta = meta ; } } export const S_IFMT = 0o170000 ; export const S_IFSOCK = 0o140000 ; export const S_IFLNK = 0o120000 ; export const S_IFREG = 0o100000 ; export const S_IFBLK = 0o060000 ; export const S_IFDIR = 0o040000 ; export const S_IFCHR = 0o020000 ; export const S_IFIFO = 0o010000 ; /** Extended options for mounting a virtual copy of an external file system via a `FileSet` */ export class Mount { readonly source : string ; readonly resolver : FileSystemResolver ; readonly meta : Record < string , any > | undefined ; constructor ( source , resolver , { meta } = { } ) { this . source = source ; this . resolver = resolver ; this . meta = meta ; } } type Inode = FileInode | DirectoryInode | SymlinkInode ; interface FileInode { dev : number ; ino : number ; mode : number ; atimeMs : number ; mtimeMs : number ; ctimeMs : number ; birthtimeMs : number ; nlink : number ; size ? : number | undefined ; buffer ? : Buffer | undefined ; source ? : string | undefined ; resolver ? : FileSystemResolver | undefined ; shadowRoot ? : FileInode | undefined ; meta ? : Metadata | undefined ; } interface DirectoryInode { dev : number ; ino : number ; mode : number ; atimeMs : number ; mtimeMs : number ; ctimeMs : number ; birthtimeMs : number ; nlink : number ; links ? : SortedMap < string , Inode > | undefined ; source ? : string | undefined ; resolver ? : FileSystemResolver | undefined ; shadowRoot ? : DirectoryInode | undefined ; meta ? : Metadata | undefined ; } interface SymlinkInode { dev : number ; ino : number ; mode : number ; atimeMs : number ; mtimeMs : number ; ctimeMs : number ; birthtimeMs : number ; nlink : number ; symlink : string ; shadowRoot ? : SymlinkInode | undefined ; meta ? : Metadata | undefined ; } function isEmptyNonShadowedDirectory ( node ) { return ! node . links && ! node . shadowRoot && ! node . resolver && ! node . source ; } function isEmptyNonShadowedFile ( node ) { return ! node . buffer && ! node . shadowRoot && ! node . resolver && ! node . source ; } function isFile ( node ) { return node !== undefined && ( node . mode & S_IFMT ) === S_IFREG ; } function isDirectory ( node ) { return node !== undefined && ( node . mode & S_IFMT ) === S_IFDIR ; } function isSymlink ( node ) { return node !== undefined && ( node . mode & S_IFMT ) === S_IFLNK ; } interface WalkResult { realpath : string ; basename : string ; parent : DirectoryInode | undefined ; links : SortedMap < string , Inode > ; node : Inode | undefined ; } function normalizeFileSetEntry ( value ) { if ( value === undefined || value === null || value instanceof Directory || value instanceof File || value instanceof Link || value instanceof Symlink || value instanceof Mount || value instanceof Rmdir || value instanceof Unlink ) { return value ; } return typeof value === 'string' || Buffer . isBuffer ( value ) ? new File ( value ) : new Directory ( value ) ; } export function formatPatch ( patch ) ; export function formatPatch ( patch ) ; export function formatPatch ( patch ) { return patch ? formatPatchWorker ( '' , patch ) : null ; } function formatPatchWorker ( dirname , container ) { let text = '' ; for ( const name of Object . keys ( container ) ) { const entry = normalizeFileSetEntry ( container [ name ] ) ; const file = dirname ? pathUtil . combinePaths ( dirname , name ) : name ; if ( entry === null || entry === undefined || entry instanceof Unlink ) { text += `//// [${ file }] unlink\r\n` ; } else if ( entry instanceof Rmdir ) { text += `//// [${ pathUtil . ensureTrailingDirectorySeparator ( file ) }] rmdir\r\n` ; } else if ( entry instanceof Directory ) { text += formatPatchWorker ( file , entry . files ) ; } else if ( entry instanceof SameFileContentFile ) { text += `//// [${ file }] file written with same contents\r\n` ; } else if ( entry instanceof File ) { const content = typeof entry . data === 'string' ? entry . data : entry . data . toString ( 'utf8' ) ; text += `//// [${ file }]\r\n${ content }\r\n\r\n` ; } else if ( entry instanceof Link ) { text += `//// [${ file }] link(${ entry . path })\r\n` ; } else if ( entry instanceof Symlink ) { text += `//// [${ file }] symlink(${ entry . symlink })\r\n` ; } else if ( entry instanceof Mount ) { text += `//// [${ file }] mount(${ entry . source })\r\n` ; } } return text ; } function makeDirEnt ( name , node ) { const de = { isFile : ( ) => isFile ( node ) , isDirectory : ( ) => isDirectory ( node ) , isBlockDevice : ( ) => false , isCharacterDevice : ( ) => false , isFIFO : ( ) => false , isSocket : ( ) => false , isSymbolicLink : ( ) => isSymlink ( node ) , name , } ; return de ; } class Stats { dev : number ; ino : number ; mode : number ; nlink : number ; uid : number ; gid : number ; rdev : number ; size : number ; blksize : number ; blocks : number ; atimeMs : number ; mtimeMs : number ; ctimeMs : number ; birthtimeMs : number ; atime : Date ; mtime : Date ; ctime : Date ; birthtime : Date ; constructor ( ) ; constructor ( dev , ino , mode , nlink , rdev , size , blksize , blocks , atimeMs , mtimeMs , ctimeMs , birthtimeMs ) ; 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 ) { this . dev = dev ; this . ino = ino ; this . mode = mode ; this . nlink = nlink ; this . uid = 0 ; this . gid = 0 ; this . rdev = rdev ; this . size = size ; this . blksize = blksize ; this . blocks = blocks ; this . atimeMs = atimeMs ; this . mtimeMs = mtimeMs ; this . ctimeMs = ctimeMs ; this . birthtimeMs = birthtimeMs ; this . atime = new Date ( this . atimeMs ) ; this . mtime = new Date ( this . mtimeMs ) ; this . ctime = new Date ( this . ctimeMs ) ; this . birthtime = new Date ( this . birthtimeMs ) ; } isFile ( ) { return ( this . mode & S_IFMT ) === S_IFREG ; } isDirectory ( ) { return ( this . mode & S_IFMT ) === S_IFDIR ; } isSymbolicLink ( ) { return ( this . mode & S_IFMT ) === S_IFLNK ; } isBlockDevice ( ) { return ( this . mode & S_IFMT ) === S_IFBLK ; } isCharacterDevice ( ) { return ( this . mode & S_IFMT ) === S_IFCHR ; } isFIFO ( ) { return ( this . mode & S_IFMT ) === S_IFIFO ; } isSocket ( ) { return ( this . mode & S_IFMT ) === S_IFSOCK ; } }