├── .npmignore ├── doc └── assets │ ├── fonts │ ├── EOT │ │ ├── SourceCodePro-Bold.eot │ │ └── SourceCodePro-Regular.eot │ ├── OTF │ │ ├── SourceCodePro-Bold.otf │ │ └── SourceCodePro-Regular.otf │ ├── TTF │ │ ├── SourceCodePro-Bold.ttf │ │ └── SourceCodePro-Regular.ttf │ ├── WOFF │ │ ├── OTF │ │ │ ├── SourceCodePro-Bold.otf.woff │ │ │ └── SourceCodePro-Regular.otf.woff │ │ └── TTF │ │ │ ├── SourceCodePro-Bold.ttf.woff │ │ │ └── SourceCodePro-Regular.ttf.woff │ ├── WOFF2 │ │ ├── OTF │ │ │ ├── SourceCodePro-Bold.otf.woff2 │ │ │ └── SourceCodePro-Regular.otf.woff2 │ │ └── TTF │ │ │ ├── SourceCodePro-Bold.ttf.woff2 │ │ │ └── SourceCodePro-Regular.ttf.woff2 │ ├── source-code-pro.css │ └── LICENSE.txt │ ├── bass-addons.css │ ├── split.css │ ├── github.css │ ├── style.css │ ├── site.js │ ├── bass.css │ ├── anchor.js │ └── split.js ├── .editorconfig ├── .flowconfig ├── .babelrc ├── shell.nix ├── lib ├── Devices │ ├── null.js │ ├── zero.js │ ├── full.js │ ├── random.js │ └── tty.js ├── index.js ├── constants.js ├── CurrentDirectory.js ├── VirtualFSError.js ├── VirtualFSSingle.js ├── Stat.js ├── permissions.js ├── Devices.js ├── Streams.js ├── FileDescriptors.js ├── INodes.js └── VirtualFS.js ├── .gitignore ├── package.json ├── rollup.config.js ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /rollup.config.js 3 | /shell.nix 4 | /doc 5 | /test 6 | -------------------------------------------------------------------------------- /doc/assets/fonts/EOT/SourceCodePro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/EOT/SourceCodePro-Bold.eot -------------------------------------------------------------------------------- /doc/assets/fonts/OTF/SourceCodePro-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/OTF/SourceCodePro-Bold.otf -------------------------------------------------------------------------------- /doc/assets/fonts/TTF/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/TTF/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /doc/assets/fonts/EOT/SourceCodePro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/EOT/SourceCodePro-Regular.eot -------------------------------------------------------------------------------- /doc/assets/fonts/OTF/SourceCodePro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/OTF/SourceCodePro-Regular.otf -------------------------------------------------------------------------------- /doc/assets/fonts/TTF/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/TTF/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 -------------------------------------------------------------------------------- /doc/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-virtualfs/HEAD/doc/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/documentation/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | experimental.strict_call_arity=false 10 | 11 | [lints] 12 | all=warn 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "flow", 4 | ["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }] 9 | ], 10 | "plugins": ["transform-object-rest-spread", "transform-runtime"] 11 | } 12 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import (fetchTarball https://github.com/NixOS/nixpkgs-channels/archive/00e56fbbee06088bf3bf82169032f5f5778588b7.tar.gz) {} 3 | }: 4 | with pkgs; 5 | stdenv.mkDerivation { 6 | name = "js-virtualfs"; 7 | buildInputs = [ python2 nodejs-8_x flow ]; 8 | } 9 | -------------------------------------------------------------------------------- /doc/assets/bass-addons.css: -------------------------------------------------------------------------------- 1 | .input { 2 | font-family: inherit; 3 | display: block; 4 | width: 100%; 5 | height: 2rem; 6 | padding: .5rem; 7 | margin-bottom: 1rem; 8 | border: 1px solid #ccc; 9 | font-size: .875rem; 10 | border-radius: 3px; 11 | box-sizing: border-box; 12 | } 13 | -------------------------------------------------------------------------------- /doc/assets/split.css: -------------------------------------------------------------------------------- 1 | .gutter { 2 | background-color: #f5f5f5; 3 | background-repeat: no-repeat; 4 | background-position: 50%; 5 | } 6 | 7 | .gutter.gutter-vertical { 8 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); 9 | cursor: ns-resize; 10 | } 11 | 12 | .gutter.gutter-horizontal { 13 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 14 | cursor: ew-resize; 15 | } 16 | -------------------------------------------------------------------------------- /lib/Devices/null.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Null */ 3 | 4 | import type { DeviceInterface } from '../Devices.js'; 5 | import type { CharacterDev } from '../INodes.js'; 6 | import type { FileDescriptor } from '../FileDescriptors.js'; 7 | 8 | const nullDev: DeviceInterface = { 9 | setPos: ( 10 | fd: FileDescriptor, 11 | position: number, 12 | flags: number 13 | ) => { 14 | fd._pos = 0; 15 | return; 16 | }, 17 | read: ( 18 | fd: FileDescriptor, 19 | buffer: Buffer, 20 | position: number 21 | ) => { 22 | return 0; 23 | }, 24 | write: ( 25 | fd: FileDescriptor, 26 | buffer: Buffer, 27 | position: number, 28 | extraFlags: number 29 | ) => { 30 | return buffer.length; 31 | } 32 | }; 33 | 34 | export default nullDev; 35 | -------------------------------------------------------------------------------- /lib/Devices/zero.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Zero */ 3 | 4 | import type { DeviceInterface } from '../Devices.js'; 5 | import type { CharacterDev } from '../INodes.js'; 6 | import type { FileDescriptor } from '../FileDescriptors.js'; 7 | 8 | const zeroDev: DeviceInterface = { 9 | setPos: ( 10 | fd: FileDescriptor, 11 | position: number, 12 | flags: number 13 | ) => { 14 | fd._pos = 0; 15 | return; 16 | }, 17 | read: ( 18 | fd: FileDescriptor, 19 | buffer: Buffer, 20 | position: number 21 | ) => { 22 | buffer.fill(0); 23 | return buffer.length; 24 | }, 25 | write: ( 26 | fd: FileDescriptor, 27 | buffer: Buffer, 28 | position: number, 29 | extraFlags: number 30 | ) => { 31 | return buffer.length; 32 | } 33 | }; 34 | 35 | export default zeroDev; 36 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | export { default } from './VirtualFSSingle.js'; 3 | export { default as VirtualFS } from './VirtualFS.js'; 4 | export { default as Stat } from './Stat.js'; 5 | export { default as constants } from './constants.js'; 6 | export { default as nullDev } from './Devices/null.js'; 7 | export { default as zeroDev } from './Devices/zero.js'; 8 | export { default as fullDev } from './Devices/full.js'; 9 | export { default as randomDev } from './Devices/random.js'; 10 | export * from './VirtualFSError.js'; 11 | export * from './Devices.js'; 12 | export * from './INodes.js'; 13 | export * from './FileDescriptors.js'; 14 | export * from './Streams.js'; 15 | export * from './permissions.js'; 16 | 17 | // polyfills to be exported 18 | // $FlowFixMe: Buffer exists 19 | export { Buffer } from 'buffer'; 20 | // $FlowFixMe: nextTick exists 21 | export { nextTick } from 'process'; 22 | -------------------------------------------------------------------------------- /lib/Devices/full.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Full */ 3 | 4 | import type { DeviceInterface } from '../Devices.js'; 5 | import type { CharacterDev } from '../INodes.js'; 6 | import type { FileDescriptor } from '../FileDescriptors.js'; 7 | 8 | import { VirtualFSError, errno } from '../VirtualFSError.js'; 9 | 10 | const fullDev: DeviceInterface = { 11 | setPos: ( 12 | fd: FileDescriptor, 13 | position: number, 14 | flags: number 15 | ) => { 16 | fd._pos = 0; 17 | return; 18 | }, 19 | read: ( 20 | fd: FileDescriptor, 21 | buffer: Buffer, 22 | position: number 23 | ) => { 24 | buffer.fill(0); 25 | return buffer.length; 26 | }, 27 | write: ( 28 | fd: FileDescriptor, 29 | buffer: Buffer, 30 | position: number, 31 | extraFlags: number 32 | ) => { 33 | throw new VirtualFSError(errno.ENOSPC); 34 | } 35 | }; 36 | 37 | export default fullDev; 38 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | O_RDONLY: 0, 3 | O_WRONLY: 1, 4 | O_RDWR: 2, 5 | O_ACCMODE: 3, 6 | S_IFMT: 61440, 7 | S_IFREG: 32768, 8 | S_IFDIR: 16384, 9 | S_IFCHR: 8192, 10 | S_IFBLK: 24576, 11 | S_IFIFO: 4096, 12 | S_IFLNK: 40960, 13 | S_IFSOCK: 49152, 14 | O_CREAT: 64, 15 | O_EXCL: 128, 16 | O_NOCTTY: 256, 17 | O_TRUNC: 512, 18 | O_APPEND: 1024, 19 | O_DIRECTORY: 65536, 20 | O_NOATIME: 262144, 21 | O_NOFOLLOW: 131072, 22 | O_SYNC: 1052672, 23 | O_DIRECT: 16384, 24 | O_NONBLOCK: 2048, 25 | S_IRWXU: 448, 26 | S_IRUSR: 256, 27 | S_IWUSR: 128, 28 | S_IXUSR: 64, 29 | S_IRWXG: 56, 30 | S_IRGRP: 32, 31 | S_IWGRP: 16, 32 | S_IXGRP: 8, 33 | S_IRWXO: 7, 34 | S_IROTH: 4, 35 | S_IWOTH: 2, 36 | S_IXOTH: 1, 37 | F_OK: 0, 38 | R_OK: 4, 39 | W_OK: 2, 40 | X_OK: 1, 41 | COPYFILE_EXCL: 1, 42 | SEEK_SET: 0, 43 | SEEK_CUR: 1, 44 | SEEK_END: 2, 45 | MAP_SHARED: 1, 46 | MAP_PRIVATE: 2 47 | }); 48 | -------------------------------------------------------------------------------- /lib/Devices/random.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Random */ 3 | 4 | import type { DeviceInterface } from '../Devices.js'; 5 | import type { CharacterDev } from '../INodes.js'; 6 | import type { FileDescriptor } from '../FileDescriptors.js'; 7 | 8 | import randomBytes from 'secure-random-bytes'; 9 | 10 | const randomDev: DeviceInterface = { 11 | setPos: ( 12 | fd: FileDescriptor, 13 | position: number, 14 | flags: number 15 | ) => { 16 | fd._pos = 0; 17 | return; 18 | }, 19 | read: ( 20 | fd: FileDescriptor, 21 | buffer: Buffer, 22 | position: number 23 | ) => { 24 | const randomBuf = Buffer.from(randomBytes(buffer.length), 'ascii'); 25 | randomBuf.copy(buffer); 26 | return randomBuf.length; 27 | }, 28 | write: ( 29 | fd: FileDescriptor, 30 | buffer: Buffer, 31 | position: number, 32 | extraFlags: number 33 | ) => { 34 | return buffer.length; 35 | } 36 | }; 37 | 38 | export default randomDev; 39 | -------------------------------------------------------------------------------- /doc/assets/fonts/source-code-pro.css: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: 'Source Code Pro'; 3 | font-weight: 400; 4 | font-style: normal; 5 | font-stretch: normal; 6 | src: url('EOT/SourceCodePro-Regular.eot') format('embedded-opentype'), 7 | url('WOFF2/TTF/SourceCodePro-Regular.ttf.woff2') format('woff2'), 8 | url('WOFF/OTF/SourceCodePro-Regular.otf.woff') format('woff'), 9 | url('OTF/SourceCodePro-Regular.otf') format('opentype'), 10 | url('TTF/SourceCodePro-Regular.ttf') format('truetype'); 11 | } 12 | 13 | @font-face{ 14 | font-family: 'Source Code Pro'; 15 | font-weight: 700; 16 | font-style: normal; 17 | font-stretch: normal; 18 | src: url('EOT/SourceCodePro-Bold.eot') format('embedded-opentype'), 19 | url('WOFF2/TTF/SourceCodePro-Bold.ttf.woff2') format('woff2'), 20 | url('WOFF/OTF/SourceCodePro-Bold.otf.woff') format('woff'), 21 | url('OTF/SourceCodePro-Bold.otf') format('opentype'), 22 | url('TTF/SourceCodePro-Bold.ttf') format('truetype'); 23 | } 24 | -------------------------------------------------------------------------------- /lib/CurrentDirectory.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module CurrentDirectory */ 3 | 4 | import type { Directory, INodeManager } from './INodes.js'; 5 | 6 | class CurrentDirectory { 7 | 8 | _iNode: Directory; 9 | _curPath: Array; 10 | _iNodeMgr: INodeManager; 11 | 12 | constructor ( 13 | iNodeMgr: INodeManager, 14 | iNode: Directory, 15 | curPath: Array = [] 16 | ) { 17 | this._iNodeMgr = iNodeMgr; 18 | this._iNode = iNode; 19 | this._curPath = curPath; 20 | iNodeMgr.refINode(iNode); 21 | } 22 | 23 | changeDir (iNode: Directory, curPath: Array): void { 24 | this._iNodeMgr.refINode(iNode); 25 | this._iNodeMgr.unrefINode(this._iNode); 26 | this._iNode = iNode; 27 | this._curPath = curPath; 28 | return; 29 | } 30 | 31 | getINode (): Directory { 32 | return this._iNode; 33 | } 34 | 35 | getPathStack (): Array { 36 | return [...this._curPath]; 37 | } 38 | 39 | getPath (): string { 40 | return '/' + this._curPath.join('/'); 41 | } 42 | 43 | } 44 | 45 | export default CurrentDirectory; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /lib/VirtualFSError.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module VirtualFSError */ 3 | 4 | /** 5 | * Class representing a file system error. 6 | * @extends Error 7 | */ 8 | class VirtualFSError extends Error { 9 | 10 | errno: number; 11 | code: string; 12 | errnoDescription: string; 13 | syscall: ?string; 14 | 15 | /** 16 | * Creates VirtualFSError. 17 | */ 18 | constructor ( 19 | errnoObj: {|errno: number, code: string, description: string|}, 20 | path: ?string, 21 | dest: ?string, 22 | syscall: ?string 23 | ) { 24 | let message = errnoObj.code + ': ' + errnoObj.description; 25 | if (path != null) { 26 | message += ', ' + path; 27 | if (dest != null) message += ' -> ' + dest; 28 | } 29 | super(message); 30 | this.errno = errnoObj.errno; 31 | this.code = errnoObj.code; 32 | this.errnoDescription = errnoObj.description; 33 | if (syscall != null) { 34 | this.syscall = syscall; 35 | } 36 | } 37 | 38 | setPaths (src: string, dst: ?string) { 39 | let message = this.code + ': ' + this.errnoDescription + ', ' + src; 40 | if (dst != null) message += ' -> ' + dst; 41 | this.message = message; 42 | return; 43 | } 44 | 45 | setSyscall (syscall: string) { 46 | this.syscall = syscall; 47 | } 48 | 49 | } 50 | 51 | export { VirtualFSError }; 52 | export { code as errno } from 'errno'; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtualfs", 3 | "version": "2.2.0", 4 | "description": "In-memory fs replacement", 5 | "keywords": [ 6 | "fs", 7 | "memory" 8 | ], 9 | "author": "Roger Qiu", 10 | "license": "Apache-2.0", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/MatrixAI/js-virtualfs.git" 14 | }, 15 | "main": "dist/index.node.cjs.js", 16 | "module": "dist/index.node.es.js", 17 | "browser": "dist/index.browser.umd.js", 18 | "scripts": { 19 | "test": "ava -v", 20 | "doc": "documentation build ./lib/** -f html -o ./doc", 21 | "rollup": "rollup --config", 22 | "dos2unix": "dos2unix dist/*", 23 | "build": "npm test && npm run doc && npm run rollup && npm run dos2unix" 24 | }, 25 | "ava": { 26 | "require": "babel-register", 27 | "babel": "inherit" 28 | }, 29 | "engines": { 30 | "node": ">=6.4.0" 31 | }, 32 | "devDependencies": { 33 | "ava": "^0.19.1", 34 | "babel-cli": "^6.26.0", 35 | "babel-plugin-transform-class-properties": "^6.24.1", 36 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 37 | "babel-plugin-transform-runtime": "^6.23.0", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-preset-flow": "^6.23.0", 40 | "babel-register": "^6.26.0", 41 | "bl": "^1.0.0", 42 | "documentation": "^5.3.5", 43 | "rollup": "^0.51.3", 44 | "rollup-plugin-babel": "^3.0.2", 45 | "rollup-plugin-commonjs": "^8.2.6", 46 | "rollup-plugin-node-builtins": "^2.1.2", 47 | "rollup-plugin-node-globals": "^1.2.1", 48 | "rollup-plugin-node-resolve": "^3.0.0" 49 | }, 50 | "dependencies": { 51 | "babel-runtime": "^6.26.0", 52 | "errno": "^0.1.7", 53 | "permaproxy": "0.0.2", 54 | "readable-stream": "^2.3.6", 55 | "resource-counter": "^1.2.4", 56 | "secure-random-bytes": "^1.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/VirtualFSSingle.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module VirtualFSSingleton */ 3 | 4 | import VirtualFS from './VirtualFS'; 5 | import { DeviceManager } from './Devices.js'; 6 | import constants from './constants.js'; 7 | import nullDev from './Devices/null.js'; 8 | import zeroDev from './Devices/zero.js'; 9 | import fullDev from './Devices/full.js'; 10 | import randomDev from './Devices/random.js'; 11 | import ttyDev from './Devices/tty.js'; 12 | 13 | const devMgr = new DeviceManager; 14 | 15 | devMgr.registerChr(nullDev, 1, 3); 16 | devMgr.registerChr(zeroDev, 1, 5); 17 | devMgr.registerChr(fullDev, 1, 7); 18 | devMgr.registerChr(randomDev, 1, 8); 19 | devMgr.registerChr(randomDev, 1, 9); 20 | devMgr.registerChr(ttyDev, 4, 0); 21 | devMgr.registerChr(ttyDev, 5, 0); 22 | devMgr.registerChr(ttyDev, 5, 1); 23 | 24 | const fs = new VirtualFS(undefined, undefined, devMgr); 25 | 26 | fs.mkdirSync('/dev'); 27 | fs.chmodSync('/dev', 0o775); 28 | 29 | fs.mknodSync('/dev/null', constants.S_IFCHR, 1, 3); 30 | fs.mknodSync('/dev/zero', constants.S_IFCHR, 1, 5); 31 | fs.mknodSync('/dev/full', constants.S_IFCHR, 1, 7); 32 | fs.mknodSync('/dev/random', constants.S_IFCHR, 1, 8); 33 | fs.mknodSync('/dev/urandom', constants.S_IFCHR, 1, 9); 34 | fs.chmodSync('/dev/null', 0o666); 35 | fs.chmodSync('/dev/zero', 0o666); 36 | fs.chmodSync('/dev/full', 0o666); 37 | fs.chmodSync('/dev/random', 0o666); 38 | fs.chmodSync('/dev/urandom', 0o666); 39 | 40 | // tty0 points to the currently active virtual console (on linux this is usually tty1 or tty7) 41 | // tty points to the currently active console (physical, virtual or pseudo) 42 | // console points to the system console (it defaults to tty0) 43 | // refer to the tty character device to understand its implementation 44 | fs.mknodSync('/dev/tty0', constants.S_IFCHR, 4, 0); 45 | fs.mknodSync('/dev/tty', constants.S_IFCHR, 5, 0); 46 | fs.mknodSync('/dev/console', constants.S_IFCHR, 5, 1); 47 | fs.chmodSync('/dev/tty0', 0o600); 48 | fs.chmodSync('/dev/tty', 0o666); 49 | fs.chmodSync('/dev/console', 0o600); 50 | 51 | fs.mkdirSync('/tmp'); 52 | fs.chmodSync('/tmp', 0o777); 53 | 54 | fs.mkdirSync('/root'); 55 | fs.chmodSync('/root', 0o700); 56 | 57 | export default fs; 58 | -------------------------------------------------------------------------------- /doc/assets/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs-comment, 17 | .diff .hljs-header, 18 | .hljs-javadoc { 19 | color: #998; 20 | font-style: italic; 21 | } 22 | 23 | .hljs-keyword, 24 | .css .rule .hljs-keyword, 25 | .hljs-winutils, 26 | .nginx .hljs-title, 27 | .hljs-subst, 28 | .hljs-request, 29 | .hljs-status { 30 | color: #1184CE; 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #ed225d; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-phpdoc, 42 | .hljs-dartdoc, 43 | .tex .hljs-formula { 44 | color: #ed225d; 45 | } 46 | 47 | .hljs-title, 48 | .hljs-id, 49 | .scss .hljs-preprocessor { 50 | color: #900; 51 | font-weight: bold; 52 | } 53 | 54 | .hljs-list .hljs-keyword, 55 | .hljs-subst { 56 | font-weight: normal; 57 | } 58 | 59 | .hljs-class .hljs-title, 60 | .hljs-type, 61 | .vhdl .hljs-literal, 62 | .tex .hljs-command { 63 | color: #458; 64 | font-weight: bold; 65 | } 66 | 67 | .hljs-tag, 68 | .hljs-tag .hljs-title, 69 | .hljs-rules .hljs-property, 70 | .django .hljs-tag .hljs-keyword { 71 | color: #000080; 72 | font-weight: normal; 73 | } 74 | 75 | .hljs-attribute, 76 | .hljs-variable, 77 | .lisp .hljs-body { 78 | color: #008080; 79 | } 80 | 81 | .hljs-regexp { 82 | color: #009926; 83 | } 84 | 85 | .hljs-symbol, 86 | .ruby .hljs-symbol .hljs-string, 87 | .lisp .hljs-keyword, 88 | .clojure .hljs-keyword, 89 | .scheme .hljs-keyword, 90 | .tex .hljs-special, 91 | .hljs-prompt { 92 | color: #990073; 93 | } 94 | 95 | .hljs-built_in { 96 | color: #0086b3; 97 | } 98 | 99 | .hljs-preprocessor, 100 | .hljs-pragma, 101 | .hljs-pi, 102 | .hljs-doctype, 103 | .hljs-shebang, 104 | .hljs-cdata { 105 | color: #999; 106 | font-weight: bold; 107 | } 108 | 109 | .hljs-deletion { 110 | background: #fdd; 111 | } 112 | 113 | .hljs-addition { 114 | background: #dfd; 115 | } 116 | 117 | .diff .hljs-change { 118 | background: #0086b3; 119 | } 120 | 121 | .hljs-chunk { 122 | color: #aaa; 123 | } 124 | -------------------------------------------------------------------------------- /doc/assets/style.css: -------------------------------------------------------------------------------- 1 | .documentation { 2 | font-family: Helvetica, sans-serif; 3 | color: #666; 4 | line-height: 1.5; 5 | background: #f5f5f5; 6 | } 7 | 8 | .black { 9 | color: #666; 10 | } 11 | 12 | .bg-white { 13 | background-color: #fff; 14 | } 15 | 16 | h4 { 17 | margin: 20px 0 10px 0; 18 | } 19 | 20 | .documentation h3 { 21 | color: #000; 22 | } 23 | 24 | .border-bottom { 25 | border-color: #ddd; 26 | } 27 | 28 | a { 29 | color: #1184CE; 30 | text-decoration: none; 31 | } 32 | 33 | .documentation a[href]:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | a:hover { 38 | cursor: pointer; 39 | } 40 | 41 | .py1-ul li { 42 | padding: 5px 0; 43 | } 44 | 45 | .max-height-100 { 46 | max-height: 100%; 47 | } 48 | 49 | .height-viewport-100 { 50 | height: 100vh; 51 | } 52 | 53 | section:target h3 { 54 | font-weight:700; 55 | } 56 | 57 | .documentation td, 58 | .documentation th { 59 | padding: .25rem .25rem; 60 | } 61 | 62 | h1:hover .anchorjs-link, 63 | h2:hover .anchorjs-link, 64 | h3:hover .anchorjs-link, 65 | h4:hover .anchorjs-link { 66 | opacity: 1; 67 | } 68 | 69 | .fix-3 { 70 | width: 25%; 71 | max-width: 244px; 72 | } 73 | 74 | .fix-3 { 75 | width: 25%; 76 | max-width: 244px; 77 | } 78 | 79 | @media (min-width: 52em) { 80 | .fix-margin-3 { 81 | margin-left: 25%; 82 | } 83 | } 84 | 85 | .pre, pre, code, .code { 86 | font-family: Source Code Pro,Menlo,Consolas,Liberation Mono,monospace; 87 | font-size: 14px; 88 | } 89 | 90 | .fill-light { 91 | background: #F9F9F9; 92 | } 93 | 94 | .width2 { 95 | width: 1rem; 96 | } 97 | 98 | .input { 99 | font-family: inherit; 100 | display: block; 101 | width: 100%; 102 | height: 2rem; 103 | padding: .5rem; 104 | margin-bottom: 1rem; 105 | border: 1px solid #ccc; 106 | font-size: .875rem; 107 | border-radius: 3px; 108 | box-sizing: border-box; 109 | } 110 | 111 | table { 112 | border-collapse: collapse; 113 | } 114 | 115 | .prose table th, 116 | .prose table td { 117 | text-align: left; 118 | padding:8px; 119 | border:1px solid #ddd; 120 | } 121 | 122 | .prose table th:nth-child(1) { border-right: none; } 123 | .prose table th:nth-child(2) { border-left: none; } 124 | 125 | .prose table { 126 | border:1px solid #ddd; 127 | } 128 | 129 | .prose-big { 130 | font-size: 18px; 131 | line-height: 30px; 132 | } 133 | 134 | .quiet { 135 | opacity: 0.7; 136 | } 137 | 138 | .minishadow { 139 | box-shadow: 2px 2px 10px #f3f3f3; 140 | } 141 | -------------------------------------------------------------------------------- /lib/Devices/tty.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Tty */ 3 | 4 | import type { DeviceInterface } from '../Devices.js'; 5 | import type { CharacterDev } from '../INodes.js'; 6 | import type { FileDescriptor } from '../FileDescriptors.js'; 7 | 8 | // $FlowFixMe: Buffer exists 9 | import { Buffer } from 'buffer'; 10 | // $FlowFixMe: process exists 11 | import process from 'process'; 12 | import { VirtualFSError, errno } from '../VirtualFSError.js'; 13 | 14 | let fds = 0; 15 | let fs = null; 16 | let ttyInFd = null; 17 | let ttyOutFd = null; 18 | 19 | const ttyDev: DeviceInterface = { 20 | open: (fd: FileDescriptor) => { 21 | if (fds === 0) { 22 | if (process.release && process.release.name === 'node') { 23 | fs = require('fs'); 24 | ttyOutFd = process.stdout.fd; 25 | if (process.platform === 'win32') { 26 | // on windows, stdin is in blocking mode 27 | // NOTE: on windows node repl environment, stdin is in raw mode 28 | // make sure to set process.stdin.setRawMode(false) 29 | ttyInFd = process.stdin.fd; 30 | } else { 31 | // on non-windows, stdin is in non-blocking mode 32 | // to get blocking semantics we need to reopen stdin 33 | try { 34 | // if there are problems opening this 35 | // we assume there is no stdin 36 | ttyInFd = fs.openSync('/dev/fd/0', 'rs'); 37 | } catch (e) {} 38 | } 39 | } 40 | } 41 | ++fds; 42 | }, 43 | close: (fd: FileDescriptor) => { 44 | --fds; 45 | if (fds === 0) { 46 | if (ttyInFd && fs) { 47 | fs.closeSync(ttyInFd); 48 | } 49 | } 50 | }, 51 | read: ( 52 | fd: FileDescriptor, 53 | buffer: Buffer, 54 | position: number 55 | ) => { 56 | if (ttyInFd !== null && fs) { 57 | // $FlowFixMe: position parameter allows null 58 | return fs.readSync(ttyInFd, buffer, 0, buffer.length, null); 59 | } else { 60 | if (window && window.prompt) { 61 | return Buffer.from(window.prompt()).copy(buffer); 62 | } 63 | throw new VirtualFSError(errno.ENXIO); 64 | } 65 | }, 66 | write: ( 67 | fd: FileDescriptor, 68 | buffer: Buffer, 69 | position: number, 70 | extraFlags: number 71 | ) => { 72 | if (ttyOutFd !== null && fs) { 73 | return fs.writeSync(ttyOutFd, buffer); 74 | } else { 75 | console.log(buffer.toString()); 76 | return buffer.length; 77 | } 78 | } 79 | }; 80 | 81 | export default ttyDev; 82 | -------------------------------------------------------------------------------- /lib/Stat.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Stat */ 3 | 4 | import constants from './constants.js'; 5 | 6 | type Metadata = { 7 | dev?: number, 8 | ino: number, 9 | mode: number, 10 | nlink: number, 11 | uid: number, 12 | gid: number, 13 | rdev?: number, 14 | size: number, 15 | atime: Date, 16 | mtime: Date, 17 | ctime: Date, 18 | birthtime: Date 19 | }; 20 | 21 | /** 22 | * Class representing Stat metadata. 23 | */ 24 | class Stat { 25 | 26 | dev: number; 27 | ino: number; 28 | mode: number; 29 | nlink: number; 30 | uid: number; 31 | gid: number; 32 | rdev: number; 33 | size: number; 34 | blksize: void; 35 | blocks: void; 36 | atime: Date; 37 | mtime: Date; 38 | ctime: Date; 39 | birthtime: Date; 40 | 41 | /** 42 | * Creates Stat. 43 | */ 44 | constructor (props: Metadata) { 45 | this.dev = props.dev || 0; // in-memory has no devices 46 | this.ino = props.ino; 47 | this.mode = props.mode; 48 | this.nlink = props.nlink; 49 | this.uid = props.uid; 50 | this.gid = props.gid; 51 | this.rdev = props.rdev || 0; // is 0 for regular files and directories 52 | this.size = props.size; 53 | this.blksize = undefined; // in-memory doesn't have blocks 54 | this.blocks = undefined; // in-memory doesn't have blocks 55 | this.atime = props.atime; 56 | this.mtime = props.mtime; 57 | this.ctime = props.ctime; 58 | this.birthtime = props.birthtime; 59 | } 60 | 61 | /** 62 | * Checks if file. 63 | */ 64 | isFile (): bool { 65 | return (this.mode & constants.S_IFMT) == constants.S_IFREG; 66 | } 67 | 68 | /** 69 | * Checks if directory. 70 | */ 71 | isDirectory (): bool { 72 | return (this.mode & constants.S_IFMT) == constants.S_IFDIR; 73 | } 74 | 75 | /** 76 | * Checks if block device. 77 | */ 78 | isBlockDevice (): bool { 79 | return (this.mode & constants.S_IFMT) == constants.S_IFBLK; 80 | } 81 | 82 | /** 83 | * Checks if character device. 84 | */ 85 | isCharacterDevice (): bool { 86 | return (this.mode & constants.S_IFMT) == constants.S_IFCHR; 87 | } 88 | 89 | /** 90 | * Checks if symbolic link. 91 | */ 92 | isSymbolicLink (): bool { 93 | return (this.mode & constants.S_IFMT) == constants.S_IFLNK; 94 | } 95 | 96 | /** 97 | * Checks if FIFO. 98 | */ 99 | isFIFO (): bool { 100 | return (this.mode & constants.S_IFMT) == constants.S_IFIFO; 101 | } 102 | 103 | /** 104 | * Checks if socket. 105 | */ 106 | isSocket (): bool { 107 | return (this.mode & constants.S_IFMT) == constants.S_IFSOCK; 108 | } 109 | 110 | } 111 | 112 | export default Stat; 113 | -------------------------------------------------------------------------------- /lib/permissions.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Permissions */ 3 | 4 | import type Stat from './Stat'; 5 | 6 | import constants from './constants'; 7 | 8 | /** 9 | * Default root uid. 10 | */ 11 | const DEFAULT_ROOT_UID = 0; 12 | 13 | /** 14 | * Default root gid. 15 | */ 16 | const DEFAULT_ROOT_GID = 0; 17 | 18 | /** 19 | * Default root directory permissions of `rwxr-xr-x`. 20 | */ 21 | const DEFAULT_ROOT_PERM = (constants.S_IRWXU | 22 | constants.S_IRGRP | 23 | constants.S_IXGRP | 24 | constants.S_IROTH | 25 | constants.S_IXOTH); 26 | 27 | /** 28 | * Default file permissions of `rw-rw-rw-`. 29 | */ 30 | const DEFAULT_FILE_PERM = (constants.S_IRUSR | 31 | constants.S_IWUSR | 32 | constants.S_IRGRP | 33 | constants.S_IWGRP | 34 | constants.S_IROTH | 35 | constants.S_IWOTH); 36 | 37 | /** 38 | * Default directory permissions of `rwxrwxrwx`. 39 | */ 40 | const DEFAULT_DIRECTORY_PERM = constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO; 41 | 42 | /** 43 | * Default symlink permissions of `rwxrwxrwx`. 44 | */ 45 | const DEFAULT_SYMLINK_PERM = constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO; 46 | 47 | /** 48 | * Applies umask to default set of permissions. 49 | */ 50 | function applyUmask (perms: number, umask: number): number { 51 | return (perms & (~umask)); 52 | } 53 | 54 | /** 55 | * Permission checking relies on ownership details of the iNode. 56 | * If the accessing user is the same as the iNode user, then only user permissions are used. 57 | * If the accessing group is the same as the iNode group, then only the group permissions are used. 58 | * Otherwise the other permissions are used. 59 | */ 60 | function resolveOwnership (uid: number, gid: number, stat: Stat): number { 61 | if (uid === stat.uid) { 62 | return (stat.mode & constants.S_IRWXU) >> 6; 63 | } else if (gid === stat.gid) { 64 | return (stat.mode & constants.S_IRWXG) >> 3; 65 | } else { 66 | return stat.mode & constants.S_IRWXO; 67 | } 68 | } 69 | 70 | /** 71 | * Checks the desired permissions with user id and group id against the metadata of an iNode. 72 | * The desired permissions can be bitwise combinations of constants.R_OK, constants.W_OK and constants.X_OK. 73 | */ 74 | function checkPermissions (access: number, uid: number, gid: number, stat: Stat): boolean { 75 | return (access & resolveOwnership(uid, gid, stat)) === access; 76 | } 77 | 78 | export { 79 | DEFAULT_ROOT_UID, 80 | DEFAULT_ROOT_GID, 81 | DEFAULT_ROOT_PERM, 82 | DEFAULT_FILE_PERM, 83 | DEFAULT_DIRECTORY_PERM, 84 | DEFAULT_SYMLINK_PERM, 85 | applyUmask, 86 | checkPermissions 87 | }; 88 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import babel from 'rollup-plugin-babel'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import globals from 'rollup-plugin-node-globals'; 6 | import builtins from 'rollup-plugin-node-builtins'; 7 | 8 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 9 | 10 | export default [ 11 | { 12 | input: 'lib/index.js', 13 | output: { 14 | file: 'dist/index.node.es.js', 15 | format: 'es' 16 | }, 17 | external: (id) => { 18 | return Object.keys(packageJson.dependencies) 19 | .concat(Object.keys(packageJson.devDependencies)) 20 | .map((dep) => new RegExp('^' + dep)) 21 | .concat([ 22 | /^babel-runtime/, 23 | /^buffer/, 24 | /^events/, 25 | /^process/, 26 | /^path/, 27 | /^stream/ 28 | ]) 29 | .some((pattern) => pattern.test(id)); 30 | }, 31 | plugins: [ 32 | babel({ 33 | babelrc: false, 34 | exclude: 'node_modules/**', 35 | runtimeHelpers: true, 36 | plugins: ['transform-object-rest-spread', 'transform-runtime'], 37 | presets: [ 38 | 'flow', 39 | ['env', { 40 | modules: false, 41 | targets: { 42 | node: '6.4.0' 43 | } 44 | }] 45 | ] 46 | }) 47 | ] 48 | }, 49 | { 50 | input: 'lib/index.js', 51 | output: { 52 | file: 'dist/index.node.cjs.js', 53 | format: 'cjs' 54 | }, 55 | external: (id) => { 56 | return Object.keys(packageJson.dependencies) 57 | .concat(Object.keys(packageJson.devDependencies)) 58 | .map((dep) => new RegExp('^' + dep)) 59 | .concat([ 60 | /^babel-runtime/, 61 | /^buffer/, 62 | /^events/, 63 | /^process/, 64 | /^path/, 65 | /^stream/ 66 | ]) 67 | .some((pattern) => pattern.test(id)); 68 | }, 69 | plugins: [ 70 | babel({ 71 | babelrc: false, 72 | exclude: 'node_modules/**', 73 | runtimeHelpers: true, 74 | plugins: ['transform-object-rest-spread', 'transform-runtime'], 75 | presets: [ 76 | 'flow', 77 | ['env', { 78 | modules: false, 79 | targets: { 80 | node: '6.4.0' 81 | } 82 | }] 83 | ] 84 | }) 85 | ] 86 | }, 87 | { 88 | input: 'lib/index.js', 89 | output: { 90 | file: 'dist/index.browser.umd.js', 91 | format: 'umd', 92 | name: 'virtualfs' 93 | }, 94 | plugins: [ 95 | babel({ 96 | babelrc: false, 97 | exclude: 'node_modules/**', 98 | runtimeHelpers: true, 99 | plugins: [ 100 | 'transform-object-rest-spread', 101 | 'transform-runtime', 102 | 'transform-class-properties' 103 | ], 104 | presets: [ 105 | 'flow', 106 | ['env', { 107 | modules: false, 108 | targets: { 109 | browsers: ['last 2 versions'] 110 | } 111 | }] 112 | ] 113 | }), 114 | resolve({ 115 | preferBuiltins: true, 116 | browser: true 117 | }), 118 | commonjs(), 119 | globals(), 120 | builtins() 121 | ] 122 | } 123 | ]; 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirtualFS 2 | 3 | VirtualFS is a virtual posix-like filesystem that runs completely in memory. It is intended to work in browsers and in NodeJS. For browser usage, make use of a bundler like Webpack, Browserify or Rollup. 4 | 5 | It includes: 6 | 7 | * Proper stat metadata with MAC time handling 8 | * Symlink support 9 | * Hardlink support 10 | * Virtual inodes 11 | * File descriptor support 12 | * Proper streams implementation 13 | * Character device support (/dev/null, /dev/full, /dev/tty...) 14 | * Current working directory support 15 | * Simulation of Unix file permissions 16 | * Umask is considered when creating new iNodes 17 | * Simulates POSIX filesystem errors 18 | * Emulates `mmap` with `MAP_PRIVATE` and `MAP_SHARED` 19 | * Usage of Flow types 20 | * Removal of all Windows path support 21 | * Practically complete compatibility with Node's FileSystem API 22 | 23 | This package will be maintained as long as the Polykey project is maintained. All tests are passing in this fork. 24 | 25 | Documentation 26 | -------------- 27 | 28 | Documentation is located in the `doc` folder. You can also view the [rendered HTML](https://cdn.rawgit.com/MatrixAI/js-virtualfs/64b3d7de/doc/index.html). 29 | 30 | The VirtualFS API extends Node's `fs` API, while also leaving out functions that are not emulatable in-memory. For the functions that have the same name, you can just refer to Node's FS API: https://nodejs.org/api/fs.html. For the functions that don't have the name, refer to the generated API documentation that uses flow types. The source code is understandable so you can just read that as well. 31 | 32 | To use VirtualFS as a global polyfill for `fs`, all you need to do is: 33 | 34 | ```js 35 | import vfs from 'virtualfs'; 36 | (typeof self === 'undefined' ? typeof global === 'undefined' ? this : global : self).fs = vfs; 37 | // alternatively use the global package `import global from 'global'; global.fs = vfs;` 38 | ``` 39 | 40 | All subsequent uses of `fs` in the current module, subsequently imported modules, and __any module that imports the current module__ will also use the same `fs`. The above monkeypatch works in Node, Browsers and Web Workers. However this will also make `/dev/tty` not work because it uses Node's real `fs`. Instead you should rely on a per-module override. Unless of course you're not using `/dev/tty` in Node. 41 | 42 | In order to only override on a per-module basis you'll need to use the rewire package or the https://github.com/speedskater/babel-plugin-rewire babel plugin. 43 | 44 | When using this in a CommonJS environment, you can gain access to the default `fs` replacement by using `var fs = require('virtualfs').default;`. 45 | 46 | Development 47 | ------------- 48 | 49 | To run flow type checks: 50 | 51 | ``` 52 | flow status 53 | flow stop 54 | ``` 55 | 56 | To build this package for release: 57 | 58 | ``` 59 | npm run build 60 | ``` 61 | 62 | It will run tests, generate documentation and output multiple targets. One for browsers and one for nodejs. See `rollup.config.js` to see the target specification. 63 | 64 | If your bundler is aware of the module field in `package.json`, you'll get the ES6 module directly. 65 | 66 | Once you've updated the package run this: 67 | 68 | ``` 69 | npm version 70 | npm publish 71 | ``` 72 | 73 | The browser target makes use of these polyfills: 74 | 75 | * buffer - Used everywhere. 76 | * events - Used by streams dependency. 77 | * path - Used for `join`. 78 | * process - Used for `nextTick` and `stdin` and `stdout` streams. 79 | * stream - Used for filesystem streaming 80 | 81 | They are currently supplied through Rollup plugins. 82 | 83 | Todo 84 | ----- 85 | 86 | * Investigate mounting implementation 87 | -------------------------------------------------------------------------------- /doc/assets/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /lib/Devices.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Devices */ 3 | 4 | import type { CharacterDev } from './INodes.js'; 5 | import type { FileDescriptor } from './FileDescriptors.js'; 6 | 7 | import Counter from 'resource-counter'; 8 | 9 | const MAJOR_BITSIZE = 12; 10 | const MINOR_BITSIZE = 20; 11 | const MAJOR_MAX = (2 ** MAJOR_BITSIZE) - 1; 12 | const MINOR_MAX = (2 ** MINOR_BITSIZE) - 1; 13 | const MAJOR_MIN = 0; 14 | const MINOR_MIN = 0; 15 | 16 | class DeviceError extends Error { 17 | 18 | static ERROR_RANGE: number; 19 | static ERROR_CONFLICT: number; 20 | code: number; 21 | 22 | constructor (code: number, message: ?string) { 23 | super(message); 24 | this.code = code; 25 | } 26 | 27 | } 28 | 29 | Object.defineProperty( 30 | DeviceError, 31 | 'ERROR_RANGE', 32 | {value: 1} 33 | ); 34 | 35 | Object.defineProperty( 36 | DeviceError, 37 | 'ERROR_CONFLICT', 38 | {value: 2} 39 | ); 40 | 41 | type INodeDevices = CharacterDev; 42 | 43 | interface DeviceInterface { 44 | open?: (FileDescriptor) => void; 45 | close?: (FileDescriptor) => void; 46 | setPos?: (FileDescriptor, number, number) => void; 47 | read?: (FileDescriptor, Buffer, number) => number; 48 | write?: (FileDescriptor, Buffer, number, number) => number; 49 | } 50 | 51 | class DeviceManager { 52 | 53 | _chrCounterMaj: Counter; 54 | _chrDevices: Map>, Counter]>; 55 | 56 | constructor () { 57 | this._chrCounterMaj = new Counter(MAJOR_MIN); 58 | this._chrDevices = new Map; 59 | } 60 | 61 | getChr (major: number, minor: number): ?DeviceInterface { 62 | const devicesAndCounterMin = this._chrDevices.get(major); 63 | if (devicesAndCounterMin) { 64 | const [devicesMin] = devicesAndCounterMin; 65 | return devicesMin.get(minor); 66 | } 67 | return; 68 | } 69 | 70 | registerChr ( 71 | device: DeviceInterface, 72 | major: number|void, 73 | minor: number|void 74 | ): void { 75 | let autoAllocMaj: ?number; 76 | let autoAllocMin: ?number; 77 | let counterMin: Counter; 78 | let devicesMin: Map>; 79 | try { 80 | if (major === undefined) { 81 | major = this._chrCounterMaj.allocate(); 82 | autoAllocMaj = major; 83 | } else { 84 | const devicesCounterMin = this._chrDevices.get(major); 85 | if (!devicesCounterMin) { 86 | this._chrCounterMaj.allocate(major); 87 | autoAllocMaj = major; 88 | } else { 89 | [devicesMin, counterMin] = devicesCounterMin; 90 | } 91 | } 92 | if (!devicesMin || !counterMin) { 93 | counterMin = new Counter(MINOR_MIN); 94 | devicesMin = new Map; 95 | } 96 | if (minor === undefined) { 97 | minor = counterMin.allocate(); 98 | autoAllocMin = minor; 99 | } else { 100 | if (!devicesMin.has(minor)) { 101 | counterMin.allocate(minor); 102 | autoAllocMin = minor; 103 | } else { 104 | throw new DeviceError(DeviceError.ERROR_CONFLICT); 105 | } 106 | } 107 | if (major > MAJOR_MAX || 108 | major < MAJOR_MIN || 109 | minor > MINOR_MAX || 110 | minor < MINOR_MIN) 111 | { 112 | throw new DeviceError(DeviceError.ERROR_RANGE); 113 | } 114 | devicesMin.set(minor, device); 115 | this._chrDevices.set(major, [devicesMin, counterMin]); 116 | return; 117 | } catch (e) { 118 | if (autoAllocMaj != null) { 119 | this._chrCounterMaj.deallocate(autoAllocMaj); 120 | } 121 | if (autoAllocMin != null && counterMin) { 122 | counterMin.deallocate(autoAllocMin); 123 | } 124 | throw e; 125 | } 126 | } 127 | 128 | deregisterChr (major: number, minor: number): void { 129 | const devicesCounterMin = this._chrDevices.get(major); 130 | if (devicesCounterMin) { 131 | const [devicesMin, counterMin] = devicesCounterMin; 132 | if (devicesMin.delete(minor)) { 133 | counterMin.deallocate(minor); 134 | } 135 | if (!devicesMin.size) { 136 | this._chrDevices.delete(major); 137 | this._chrCounterMaj.deallocate(major); 138 | } 139 | } 140 | return; 141 | } 142 | 143 | } 144 | 145 | function mkDev (major: number, minor: number): number { 146 | return ((major << MINOR_BITSIZE) | minor); 147 | } 148 | 149 | function unmkDev (dev: number): [number, number] { 150 | const major = dev >> MINOR_BITSIZE; 151 | const minor = dev & ((1 << MINOR_BITSIZE) - 1); 152 | return [major, minor]; 153 | } 154 | 155 | export { 156 | MAJOR_BITSIZE, 157 | MINOR_BITSIZE, 158 | MAJOR_MAX, 159 | MINOR_MAX, 160 | MAJOR_MIN, 161 | MINOR_MIN, 162 | DeviceManager, 163 | DeviceError, 164 | mkDev, 165 | unmkDev 166 | }; 167 | 168 | export type { INodeDevices, DeviceInterface }; 169 | -------------------------------------------------------------------------------- /doc/assets/site.js: -------------------------------------------------------------------------------- 1 | /* global anchors */ 2 | 3 | // add anchor links to headers 4 | anchors.options.placement = 'left'; 5 | anchors.add('h3'); 6 | 7 | // Filter UI 8 | var tocElements = document.getElementById('toc').getElementsByTagName('li'); 9 | 10 | document.getElementById('filter-input').addEventListener('keyup', function(e) { 11 | var i, element, children; 12 | 13 | // enter key 14 | if (e.keyCode === 13) { 15 | // go to the first displayed item in the toc 16 | for (i = 0; i < tocElements.length; i++) { 17 | element = tocElements[i]; 18 | if (!element.classList.contains('display-none')) { 19 | location.replace(element.firstChild.href); 20 | return e.preventDefault(); 21 | } 22 | } 23 | } 24 | 25 | var match = function() { 26 | return true; 27 | }; 28 | 29 | var value = this.value.toLowerCase(); 30 | 31 | if (!value.match(/^\s*$/)) { 32 | match = function(element) { 33 | var html = element.firstChild.innerHTML; 34 | return html && html.toLowerCase().indexOf(value) !== -1; 35 | }; 36 | } 37 | 38 | for (i = 0; i < tocElements.length; i++) { 39 | element = tocElements[i]; 40 | children = Array.from(element.getElementsByTagName('li')); 41 | if (match(element) || children.some(match)) { 42 | element.classList.remove('display-none'); 43 | } else { 44 | element.classList.add('display-none'); 45 | } 46 | } 47 | }); 48 | 49 | var items = document.getElementsByClassName('toggle-sibling'); 50 | for (var j = 0; j < items.length; j++) { 51 | items[j].addEventListener('click', toggleSibling); 52 | } 53 | 54 | function toggleSibling() { 55 | var stepSibling = this.parentNode.getElementsByClassName('toggle-target')[0]; 56 | var icon = this.getElementsByClassName('icon')[0]; 57 | var klass = 'display-none'; 58 | if (stepSibling.classList.contains(klass)) { 59 | stepSibling.classList.remove(klass); 60 | icon.innerHTML = '▾'; 61 | } else { 62 | stepSibling.classList.add(klass); 63 | icon.innerHTML = '▸'; 64 | } 65 | } 66 | 67 | function showHashTarget(targetId) { 68 | if (targetId) { 69 | var hashTarget = document.getElementById(targetId); 70 | // new target is hidden 71 | if ( 72 | hashTarget && 73 | hashTarget.offsetHeight === 0 && 74 | hashTarget.parentNode.parentNode.classList.contains('display-none') 75 | ) { 76 | hashTarget.parentNode.parentNode.classList.remove('display-none'); 77 | } 78 | } 79 | } 80 | 81 | function scrollIntoView(targetId) { 82 | // Only scroll to element if we don't have a stored scroll position. 83 | if (targetId && !history.state) { 84 | var hashTarget = document.getElementById(targetId); 85 | if (hashTarget) { 86 | hashTarget.scrollIntoView(); 87 | } 88 | } 89 | } 90 | 91 | function gotoCurrentTarget() { 92 | showHashTarget(location.hash.substring(1)); 93 | scrollIntoView(location.hash.substring(1)); 94 | } 95 | 96 | window.addEventListener('hashchange', gotoCurrentTarget); 97 | gotoCurrentTarget(); 98 | 99 | var toclinks = document.getElementsByClassName('pre-open'); 100 | for (var k = 0; k < toclinks.length; k++) { 101 | toclinks[k].addEventListener('mousedown', preOpen, false); 102 | } 103 | 104 | function preOpen() { 105 | showHashTarget(this.hash.substring(1)); 106 | } 107 | 108 | var split_left = document.querySelector('#split-left'); 109 | var split_right = document.querySelector('#split-right'); 110 | var split_parent = split_left.parentNode; 111 | var cw_with_sb = split_left.clientWidth; 112 | split_left.style.overflow = 'hidden'; 113 | var cw_without_sb = split_left.clientWidth; 114 | split_left.style.overflow = ''; 115 | 116 | Split(['#split-left', '#split-right'], { 117 | elementStyle: function(dimension, size, gutterSize) { 118 | return { 119 | 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)' 120 | }; 121 | }, 122 | gutterStyle: function(dimension, gutterSize) { 123 | return { 124 | 'flex-basis': gutterSize + 'px' 125 | }; 126 | }, 127 | gutterSize: 20, 128 | sizes: [33, 67] 129 | }); 130 | 131 | // Chrome doesn't remember scroll position properly so do it ourselves. 132 | // Also works on Firefox and Edge. 133 | 134 | function updateState() { 135 | history.replaceState( 136 | { 137 | left_top: split_left.scrollTop, 138 | right_top: split_right.scrollTop 139 | }, 140 | document.title 141 | ); 142 | } 143 | 144 | function loadState(ev) { 145 | if (ev) { 146 | // Edge doesn't replace change history.state on popstate. 147 | history.replaceState(ev.state, document.title); 148 | } 149 | if (history.state) { 150 | split_left.scrollTop = history.state.left_top; 151 | split_right.scrollTop = history.state.right_top; 152 | } 153 | } 154 | 155 | window.addEventListener('load', function() { 156 | // Restore after Firefox scrolls to hash. 157 | setTimeout(function() { 158 | loadState(); 159 | // Update with initial scroll position. 160 | updateState(); 161 | // Update scroll positions only after we've loaded because Firefox 162 | // emits an initial scroll event with 0. 163 | split_left.addEventListener('scroll', updateState); 164 | split_right.addEventListener('scroll', updateState); 165 | }, 1); 166 | }); 167 | 168 | window.addEventListener('popstate', loadState); 169 | -------------------------------------------------------------------------------- /lib/Streams.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module Streams */ 3 | 4 | import type VirtualFS from './VirtualFS'; 5 | 6 | // $FlowFixMe: Buffer exists 7 | import { Buffer } from 'buffer'; 8 | // $FlowFixMe: nextTick exists 9 | import { nextTick } from 'process'; 10 | import { Readable, Writable } from 'stream'; 11 | import { DEFAULT_FILE_PERM } from './permissions.js'; 12 | 13 | type optionsStream = { 14 | highWaterMark?: number, 15 | flags?: string, 16 | encoding?: string, 17 | fd?: number, 18 | mode?: number, 19 | autoClose?: boolean, 20 | start?: number, 21 | end?: number 22 | }; 23 | 24 | /** 25 | * Class representing a ReadStream. 26 | * @extends Readable 27 | */ 28 | class ReadStream extends Readable { 29 | 30 | _fs: VirtualFS; 31 | bytesRead: number; 32 | path: string; 33 | fd: ?number; 34 | flags: string; 35 | mode: ?number; 36 | autoClose: boolean; 37 | start: ?number; 38 | end: number; 39 | pos: ?number; 40 | closed: ?boolean; 41 | destroy: Function; 42 | 43 | /** 44 | * Creates ReadStream. 45 | * It will asynchronously open the file descriptor if a file path was passed in. 46 | * It will automatically close the opened file descriptor by default. 47 | */ 48 | constructor (path: string, options: optionsStream, fs: VirtualFS) { 49 | super({ 50 | highWaterMark: options.highWaterMark, 51 | encoding: options.encoding 52 | }); 53 | this._fs = fs; 54 | this.bytesRead = 0; 55 | this.path = path; 56 | this.fd = (options.fd === undefined) ? null : options.fd; 57 | this.flags = (options.flags === undefined) ? 'r' : options.flags; 58 | this.mode = (options.mode === undefined) ? DEFAULT_FILE_PERM : options.mode; 59 | this.autoClose = (options.autoClose === undefined) ? true : options.autoClose; 60 | this.start = options.start; 61 | this.end = (options.end === undefined) ? Infinity : options.end; 62 | this.pos = options.start; 63 | if (typeof this.fd !== 'number') { 64 | this._open(); 65 | } 66 | super.on('end', () => { 67 | if (this.autoClose) { 68 | this.destroy(); 69 | } 70 | }); 71 | } 72 | 73 | /** 74 | * Open file descriptor if ReadStream was constructed from a file path. 75 | * @private 76 | */ 77 | _open () { 78 | this._fs.open(this.path, this.flags, this.mode, (e, fd) => { 79 | if (e) { 80 | if (this.autoClose) { 81 | this.destroy(); 82 | } 83 | super.emit('error', e); 84 | return; 85 | } 86 | this.fd = fd; 87 | super.emit('open', fd); 88 | super.read(); 89 | }); 90 | } 91 | 92 | /** 93 | * Asynchronous read hook for stream implementation. 94 | * The size passed into this function is not the requested size, but the high watermark. 95 | * It's just a heuristic buffering size to avoid sending to many syscalls. 96 | * However since this is an in-memory filesystem, the size itself is irrelevant. 97 | * @private 98 | */ 99 | _read (size: number) { 100 | if (typeof this.fd !== 'number') { 101 | super.once('open', () => { 102 | this._read(size); 103 | }); 104 | return; 105 | } 106 | if (this.destroyed) return; 107 | // this.pos is only ever used if this.start is specified 108 | if (this.pos != null) { 109 | size = Math.min(this.end - this.pos + 1, size); 110 | } 111 | if (size <= 0) { 112 | this.push(null); 113 | return; 114 | } 115 | this._fs.read( 116 | this.fd, 117 | Buffer.allocUnsafe(size), 118 | 0, 119 | size, 120 | this.pos, 121 | (e, bytesRead, buf) => { 122 | if (e) { 123 | if (this.autoClose) { 124 | this.destroy(); 125 | } 126 | super.emit('error', e); 127 | return; 128 | } 129 | if (bytesRead > 0) { 130 | this.bytesRead += bytesRead; 131 | this.push(buf.slice(0, bytesRead)); 132 | } else { 133 | this.push(null); 134 | } 135 | } 136 | ); 137 | if (this.pos != null) { 138 | this.pos += size; 139 | } 140 | } 141 | 142 | /** 143 | * Destroy hook for stream implementation. 144 | * @private 145 | */ 146 | _destroy (e: ?Error, cb: Function) { 147 | this._close((e_) => { 148 | cb(e || e_); 149 | }); 150 | } 151 | 152 | /** 153 | * Close file descriptor if ReadStream was constructed from a file path. 154 | * @private 155 | */ 156 | _close (cb: ?Function) { 157 | if (cb) { 158 | super.once('close', cb); 159 | } 160 | if (typeof this.fd !== 'number') { 161 | super.once('open', () => { 162 | this._close(); 163 | }); 164 | return; 165 | } 166 | if (this.closed) { 167 | return nextTick(() => super.emit('close')); 168 | } 169 | this.closed = true; 170 | this._fs.close(this.fd, (e) => { 171 | if (e) { 172 | this.emit('error', e); 173 | } else { 174 | this.emit('close'); 175 | } 176 | }); 177 | this.fd = null; 178 | } 179 | 180 | } 181 | 182 | /** 183 | * Class representing a WriteStream. 184 | * @extends Writable 185 | */ 186 | class WriteStream extends Writable { 187 | 188 | _fs: VirtualFS; 189 | bytesWritten: number; 190 | path: string; 191 | fd: ?number; 192 | flags: string; 193 | mode: ?number; 194 | autoClose: boolean; 195 | start: ?number; 196 | pos: ?number; 197 | closed: ?boolean; 198 | destroy: Function; 199 | 200 | /** 201 | * Creates WriteStream. 202 | */ 203 | constructor (path: string, options: optionsStream, fs: VirtualFS) { 204 | super({ 205 | highWaterMark: options.highWaterMark 206 | }); 207 | this._fs = fs; 208 | this.bytesWritten = 0; 209 | this.path = path; 210 | this.fd = options.fd === undefined ? null : options.fd; 211 | this.flags = options.flags === undefined ? 'w' : options.flags; 212 | this.mode = options.mode === undefined ? DEFAULT_FILE_PERM : options.mode; 213 | this.autoClose = options.autoClose === undefined ? true : options.autoClose; 214 | this.start = options.start; 215 | this.pos = this.start; // WriteStream maintains its own position 216 | if (options.encoding) { 217 | super.setDefaultEncoding(options.encoding); 218 | } 219 | if (typeof this.fd !== 'number') { 220 | this._open(); 221 | } 222 | super.on('finish', () => { 223 | if (this.autoClose) { 224 | this.destroy(); 225 | } 226 | }); 227 | } 228 | 229 | /** 230 | * Open file descriptor if WriteStream was constructed from a file path. 231 | * @private 232 | */ 233 | _open () { 234 | this._fs.open(this.path, this.flags, this.mode, (e, fd) => { 235 | if (e) { 236 | if (this.autoClose) { 237 | this.destroy(); 238 | } 239 | super.emit('error', e); 240 | return; 241 | } 242 | this.fd = fd; 243 | super.emit('open', fd); 244 | }); 245 | } 246 | 247 | /** 248 | * Asynchronous write hook for stream implementation. 249 | * @private 250 | */ 251 | // $FlowFixMe: _write hook adapted from Node `lib/internal/fs/streams.js` 252 | _write (data: Buffer | string, encoding: ?string, cb: Function) { 253 | if (typeof this.fd !== 'number') { 254 | return super.once('open', () => { 255 | this._write(data, encoding, cb); 256 | }); 257 | } 258 | this._fs.write(this.fd, data, 0, data.length, this.pos, (e, bytesWritten) => { 259 | if (e) { 260 | if (this.autoClose) { 261 | this.destroy(); 262 | } 263 | cb(e); 264 | return; 265 | } 266 | this.bytesWritten += bytesWritten; 267 | cb(); 268 | }); 269 | if (this.pos !== undefined) { 270 | this.pos += data.length; 271 | } 272 | } 273 | 274 | /** 275 | * Vectorised write hook for stream implementation. 276 | * @private 277 | */ 278 | _writev (chunks:Array<{chunk: Buffer}>, cb: Function) { 279 | this._write( 280 | Buffer.concat(chunks.map((chunk) => chunk.chunk)), 281 | undefined, 282 | cb 283 | ); 284 | return; 285 | } 286 | 287 | /** 288 | * Destroy hook for stream implementation. 289 | * @private 290 | */ 291 | _destroy (e: ?Error, cb: Function) { 292 | this._close((e_) => { 293 | cb(e || e_); 294 | }); 295 | } 296 | 297 | /** 298 | * Close file descriptor if WriteStream was constructed from a file path. 299 | * @private 300 | */ 301 | _close (cb: ?Function) { 302 | if (cb) { 303 | super.once('close', cb); 304 | } 305 | if (typeof this.fd !== 'number') { 306 | super.once('open', () => { 307 | this._close(); 308 | }); 309 | return; 310 | } 311 | if (this.closed) { 312 | return nextTick(() => super.emit('close')); 313 | } 314 | this.closed = true; 315 | this._fs.close(this.fd, (e) => { 316 | if (e) { 317 | this.emit('error', e); 318 | } else { 319 | this.emit('close'); 320 | } 321 | }); 322 | this.fd = null; 323 | } 324 | 325 | /** 326 | * Final hook for stream implementation. 327 | * @private 328 | */ 329 | _final (cb: Function) { 330 | cb(); 331 | return; 332 | } 333 | 334 | } 335 | 336 | export { ReadStream, WriteStream }; 337 | 338 | export type { optionsStream }; 339 | -------------------------------------------------------------------------------- /lib/FileDescriptors.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module FileDescriptors */ 3 | 4 | import type { INode, INodeManager } from './INodes.js'; 5 | 6 | // $FlowFixMe: Buffer exists 7 | import { Buffer } from 'buffer'; 8 | import Counter from 'resource-counter'; 9 | import constants from './constants.js'; 10 | import { File, Directory, CharacterDev } from './INodes.js'; 11 | import { VirtualFSError, errno } from './VirtualFSError.js'; 12 | 13 | /** 14 | * Class representing a File Descriptor 15 | */ 16 | class FileDescriptor> { 17 | 18 | _iNode: I; 19 | _flags: number; 20 | _pos: number; 21 | 22 | /** 23 | * Creates FileDescriptor 24 | * Starts the seek position at 0 25 | */ 26 | constructor (iNode: I, flags: number) { 27 | this._iNode = iNode; 28 | this._flags = flags; 29 | this._pos = 0; 30 | } 31 | 32 | /** 33 | * Gets an INode. 34 | */ 35 | getINode (): I { 36 | return this._iNode; 37 | } 38 | 39 | /** 40 | * Gets the file descriptor flags. 41 | * Unlike Linux filesystems, this retains creation and status flags. 42 | */ 43 | getFlags (): number { 44 | return this._flags; 45 | } 46 | 47 | /** 48 | * Sets the file descriptor flags. 49 | */ 50 | setFlags (flags: number): void { 51 | this._flags = flags; 52 | return; 53 | } 54 | 55 | /** 56 | * Gets the file descriptor position. 57 | */ 58 | getPos (): number { 59 | return this._pos; 60 | } 61 | 62 | /** 63 | * Sets the file descriptor position. 64 | */ 65 | setPos (pos: number, flags: number = constants.SEEK_SET): void { 66 | const iNode = this.getINode(); 67 | let newPos; 68 | switch (true) { 69 | case iNode instanceof File: 70 | case iNode instanceof Directory: 71 | switch (flags) { 72 | case constants.SEEK_SET: 73 | newPos = pos; 74 | break; 75 | case constants.SEEK_CUR: 76 | newPos = this._pos + pos; 77 | break; 78 | case constants.SEEK_END: 79 | newPos = iNode.getData().length + pos; 80 | break; 81 | default: 82 | newPos = this._pos; 83 | } 84 | if (newPos < 0) { 85 | throw new VirtualFSError(errno.EINVAL); 86 | } 87 | this._pos = newPos; 88 | break; 89 | case iNode instanceof CharacterDev: 90 | const fops = iNode.getFileDesOps(); 91 | if (!fops) { 92 | throw new VirtualFSError(errno.ENXIO); 93 | } else if (!fops.setPos) { 94 | throw new VirtualFSError(errno.ESPIPE); 95 | } else { 96 | fops.setPos(this, pos, flags); 97 | } 98 | break; 99 | default: 100 | throw new VirtualFSError(errno.ESPIPE); 101 | } 102 | } 103 | 104 | /** 105 | * Reads from this file descriptor into a buffer. 106 | * It will always try to fill the input buffer. 107 | * If position is specified, the position change does not persist. 108 | * If the current file descriptor position is greater than or equal to the length of the data, this will read 0 bytes. 109 | */ 110 | read (buffer: Buffer, position: number|null = null): number { 111 | let currentPosition; 112 | if (position === null) { 113 | currentPosition = this._pos; 114 | } else { 115 | currentPosition = position; 116 | } 117 | const iNode = this._iNode; 118 | let bytesRead; 119 | switch (true) { 120 | case iNode instanceof File: 121 | const data = iNode.getData(); 122 | const metadata = iNode.getMetadata(); 123 | bytesRead = data.copy(buffer, 0, currentPosition); 124 | metadata.atime = new Date; 125 | break; 126 | case iNode instanceof CharacterDev: 127 | const fops = iNode.getFileDesOps(); 128 | if (!fops) { 129 | throw new VirtualFSError(errno.ENXIO); 130 | } else if (!fops.read) { 131 | throw new VirtualFSError(errno.EINVAL); 132 | } else { 133 | bytesRead = fops.read( 134 | this, 135 | buffer, 136 | currentPosition 137 | ); 138 | } 139 | break; 140 | default: 141 | throw new VirtualFSError(errno.EINVAL); 142 | } 143 | if (position === null) { 144 | this._pos = currentPosition + bytesRead; 145 | } 146 | return bytesRead; 147 | } 148 | 149 | /** 150 | * Writes to this file descriptor. 151 | * If position is specified, the position change does not persist. 152 | */ 153 | write (buffer: Buffer, position: number|null = null, extraFlags: number = 0) { 154 | let currentPosition; 155 | if (position === null) { 156 | currentPosition = this._pos; 157 | } else { 158 | currentPosition = position; 159 | } 160 | const iNode = this._iNode; 161 | let bytesWritten; 162 | switch (true) { 163 | case iNode instanceof File: 164 | let data = iNode.getData(); 165 | const metadata = iNode.getMetadata(); 166 | if ((this.getFlags() | extraFlags) & constants.O_APPEND) { 167 | currentPosition = data.length; 168 | data = Buffer.concat([data, buffer]); 169 | bytesWritten = buffer.length; 170 | } else { 171 | if (currentPosition > data.length) { 172 | data = Buffer.concat([ 173 | data, 174 | Buffer.alloc(currentPosition - data.length), 175 | Buffer.allocUnsafe(buffer.length) 176 | ]); 177 | } else if (currentPosition <= data.length) { 178 | const overwrittenLength = data.length - currentPosition; 179 | const extendedLength = buffer.length - overwrittenLength; 180 | if (extendedLength > 0) { 181 | data = Buffer.concat([data, Buffer.allocUnsafe(extendedLength)]); 182 | } 183 | } 184 | bytesWritten = buffer.copy(data, currentPosition); 185 | } 186 | iNode.setData(data); 187 | const now = new Date; 188 | metadata.mtime = now; 189 | metadata.ctime = now; 190 | metadata.size = data.length; 191 | break; 192 | case iNode instanceof CharacterDev: 193 | const fops = iNode.getFileDesOps(); 194 | if (!fops) { 195 | throw new VirtualFSError(errno.ENXIO); 196 | } else if (!fops.write) { 197 | throw new VirtualFSError(errno.EINVAL); 198 | } else { 199 | bytesWritten = fops.write( 200 | this, 201 | buffer, 202 | currentPosition, 203 | extraFlags 204 | ); 205 | } 206 | break; 207 | default: 208 | throw new VirtualFSError(errno.EINVAL); 209 | } 210 | if (position === null) { 211 | this._pos = currentPosition + bytesWritten; 212 | } 213 | return bytesWritten; 214 | } 215 | 216 | } 217 | 218 | /** 219 | * Class that manages all FileDescriptors 220 | */ 221 | class FileDescriptorManager { 222 | 223 | _counter: Counter; 224 | _fds: Map>; 225 | _iNodeMgr: INodeManager; 226 | 227 | /** 228 | * Creates an instance of the FileDescriptorManager. 229 | * It starts the fd counter at 0. 230 | * Make sure not get real fd numbers confused with these fd numbers. 231 | */ 232 | constructor (iNodeMgr: INodeManager) { 233 | this._counter = new Counter(0); 234 | this._fds = new Map; 235 | this._iNodeMgr = iNodeMgr; 236 | } 237 | 238 | /** 239 | * Creates a file descriptor. 240 | * This will increment the reference to the iNode preventing garbage collection by the INodeManager. 241 | */ 242 | createFd (iNode: $Subtype, flags: number): [FileDescriptor<*>, number] { 243 | this._iNodeMgr.refINode(iNode); 244 | const index = this._counter.allocate(); 245 | const fd = new FileDescriptor(iNode, flags); 246 | if (iNode instanceof CharacterDev) { 247 | const fops = iNode.getFileDesOps(); 248 | if (!fops) { 249 | throw new VirtualFSError(errno.ENXIO); 250 | } else if (fops.open) { 251 | fops.open(fd); 252 | } 253 | } 254 | 255 | this._fds.set(index, fd); 256 | 257 | return [fd, index]; 258 | } 259 | 260 | /** 261 | * Gets the file descriptor object. 262 | */ 263 | getFd (index: number): ?FileDescriptor<*> { 264 | return this._fds.get(index); 265 | } 266 | 267 | /** 268 | * Duplicates file descriptor index. 269 | * It may return a new file descriptor index that points to the same file descriptor. 270 | */ 271 | dupFd (index: number): ?number { 272 | const fd = this._fds.get(index); 273 | if (fd) { 274 | this._iNodeMgr.refINode(fd.getINode()); 275 | const dupIndex = this._counter.allocate(); 276 | this._fds.set(dupIndex, fd); 277 | return index; 278 | } 279 | } 280 | 281 | /** 282 | * Deletes a file descriptor. 283 | * This effectively closes the file descriptor. 284 | * This will decrement the reference to the iNode allowing garbage collection by the INodeManager. 285 | */ 286 | deleteFd (fdIndex: number): void { 287 | const fd = this._fds.get(fdIndex); 288 | if (fd) { 289 | const iNode = fd.getINode(); 290 | if (iNode instanceof CharacterDev) { 291 | const fops = iNode.getFileDesOps(); 292 | if (!fops){ 293 | throw new VirtualFSError(errno.ENXIO); 294 | } else if (fops.close) { 295 | fops.close(fd); 296 | } 297 | } 298 | this._fds.delete(fdIndex); 299 | this._counter.deallocate(fdIndex); 300 | this._iNodeMgr.unrefINode(iNode); 301 | } 302 | return; 303 | } 304 | 305 | } 306 | 307 | export { 308 | FileDescriptor, 309 | FileDescriptorManager 310 | }; 311 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/INodes.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module INodes */ 3 | 4 | import type { DeviceManager, DeviceInterface } from './Devices.js'; 5 | 6 | // $FlowFixMe: Buffer exists 7 | import { Buffer } from 'buffer'; 8 | import Counter from 'resource-counter'; 9 | import constants from './constants.js'; 10 | import Stat from './Stat.js'; 11 | import { DEFAULT_ROOT_UID, DEFAULT_ROOT_GID } from './permissions.js'; 12 | import { unmkDev } from './Devices.js'; 13 | 14 | /** 15 | * Class representing an iNode. 16 | */ 17 | class INode { 18 | 19 | _metadata: Object; 20 | _iNodeMgr: INodeManager; 21 | 22 | /** 23 | * Creates iNode. 24 | * INode and INodeManager will recursively call each other. 25 | */ 26 | constructor ( 27 | metadata: { 28 | ino: number, 29 | mode: number, 30 | uid: number, 31 | gid: number, 32 | nlink?: number, 33 | size: number 34 | }, 35 | iNodeMgr: INodeManager 36 | ) { 37 | const now = new Date; 38 | this._metadata = new Stat({ 39 | ...metadata, 40 | mode: metadata.mode, 41 | nlink: metadata.nlink || 0, 42 | atime: now, 43 | mtime: now, 44 | ctime: now, 45 | birthtime: now 46 | }); 47 | this._iNodeMgr = iNodeMgr; 48 | } 49 | 50 | /** 51 | * Gets the Stat metadata instance. 52 | */ 53 | getMetadata (): Stat { 54 | return this._metadata; 55 | } 56 | 57 | } 58 | 59 | /** 60 | * Class representing a file. 61 | * @extends INode 62 | */ 63 | class File extends INode { 64 | 65 | _data: Buffer; 66 | 67 | /** 68 | * Creates a file. 69 | */ 70 | constructor ( 71 | props: { 72 | ino: number, 73 | mode: number, 74 | uid: number, 75 | gid: number, 76 | data?: Buffer 77 | }, 78 | iNodeMgr: INodeManager 79 | ) { 80 | super( 81 | { 82 | ino: props.ino, 83 | uid: props.uid, 84 | gid: props.gid, 85 | mode: constants.S_IFREG | (props.mode & (~constants.S_IFMT)), 86 | size: (props.data) ? props.data.byteLength : 0 87 | }, 88 | iNodeMgr 89 | ); 90 | this._data = (props.data) ? props.data : Buffer.allocUnsafe(0); 91 | } 92 | 93 | /** 94 | * Gets the file buffer. 95 | */ 96 | getData (): Buffer { 97 | return this._data; 98 | } 99 | 100 | /** 101 | * Sets the file buffer. 102 | */ 103 | setData (data: Buffer): void { 104 | this._data = data; 105 | return; 106 | } 107 | 108 | read () { 109 | 110 | } 111 | 112 | write (buffer: Buffer, position: number, append: boolean) { 113 | let data = this._data; 114 | let bytesWritten; 115 | if (append) { 116 | data = Buffer.concat([data, buffer]); 117 | bytesWritten = buffer.length; 118 | } else { 119 | position = Math.min(data.length, position); 120 | const overwrittenLength = data.length - position; 121 | const extendedLength = buffer.length - overwrittenLength; 122 | if (extendedLength > 0) { 123 | data = Buffer.concat([data, Buffer.allocUnsafe(extendedLength)]); 124 | } 125 | bytesWritten = buffer.copy(data, position); 126 | } 127 | this._data = data; 128 | return bytesWritten; 129 | } 130 | 131 | /** 132 | * Noop. 133 | */ 134 | destructor (): void { 135 | return; 136 | } 137 | 138 | } 139 | 140 | /** 141 | * Class representing a directory. 142 | * @extends INode 143 | */ 144 | class Directory extends INode { 145 | 146 | _dir: Map; 147 | 148 | /** 149 | * Creates a directory. 150 | * Virtual directories have 0 size. 151 | * If there's no parent inode, we assume this is the root directory. 152 | */ 153 | constructor ( 154 | props: { 155 | ino: number, 156 | mode: number, 157 | uid: number, 158 | gid: number, 159 | parent?: number 160 | }, 161 | iNodeMgr: INodeManager 162 | ) { 163 | // root will start with an nlink of 2 due to '..' 164 | // otherwise start with an nlink of 1 165 | if (props.parent === undefined) props.parent = props.ino; 166 | let nlink; 167 | if (props.parent === props.ino) { 168 | nlink = 2; 169 | } else { 170 | nlink = 1; 171 | iNodeMgr.linkINode(iNodeMgr.getINode(props.parent)); 172 | } 173 | super( 174 | { 175 | ino: props.ino, 176 | mode: constants.S_IFDIR | (props.mode & (~constants.S_IFMT)), 177 | uid: props.uid, 178 | gid: props.gid, 179 | nlink: nlink, 180 | size: 0 181 | }, 182 | iNodeMgr 183 | ); 184 | this._dir = new Map([ 185 | ['.', props.ino], 186 | ['..', props.parent] 187 | ]); 188 | } 189 | 190 | /** 191 | * Gets an iterator of name to iNode index. 192 | * This prevents giving out mutability. 193 | */ 194 | getEntries (): Iterator<[string,number]> { 195 | this._metadata.atime = new Date; 196 | return this._dir.entries(); 197 | } 198 | 199 | /** 200 | * Get the inode index for a name. 201 | */ 202 | getEntryIndex (name: string): ?number { 203 | return this._dir.get(name); 204 | } 205 | 206 | /** 207 | * Get inode for a name. 208 | */ 209 | getEntry (name: string): $Subtype|void { 210 | const index = this._dir.get(name); 211 | if (index !== undefined) { 212 | return this._iNodeMgr.getINode(index); 213 | } 214 | return; 215 | } 216 | 217 | /** 218 | * Add a name to inode index to this directory. 219 | * It will increment the link reference to the inode. 220 | * It is not allowed to add entries with the names `.` and `..`. 221 | */ 222 | addEntry (name: string, index: number) { 223 | if (name === '.' || name === '..') { 224 | throw new Error('Not allowed to add `.` or `..` entries'); 225 | } 226 | const now = new Date; 227 | this._metadata.mtime = now; 228 | this._metadata.ctime = now; 229 | this._iNodeMgr.linkINode(this._iNodeMgr.getINode(index)); 230 | this._dir.set(name, index); 231 | return; 232 | } 233 | 234 | /** 235 | * Delete a name in this directory. 236 | * It will decrement the link reference to the inode. 237 | * It is not allowed to delete entries with the names `.` and `..`. 238 | */ 239 | deleteEntry (name: string): void { 240 | if (name === '.' || name === '..') { 241 | throw new Error('Not allowed to delete `.` or `..` entries'); 242 | } 243 | const index = this._dir.get(name); 244 | if (index !== undefined) { 245 | const now = new Date; 246 | this._metadata.mtime = now; 247 | this._metadata.ctime = now; 248 | this._dir.delete(name); 249 | this._iNodeMgr.unlinkINode(this._iNodeMgr.getINode(index)); 250 | } 251 | return; 252 | } 253 | 254 | /** 255 | * Rename a name in this directory. 256 | */ 257 | renameEntry (oldName: string, newName: string): void { 258 | if (oldName === '.' || oldName === '..' || newName === '.' || oldName === '..') { 259 | throw new Error('Not allowed to rename `.` or `..` entries'); 260 | } 261 | const index = this._dir.get(oldName); 262 | if (index != null) { 263 | const now = new Date; 264 | this._metadata.mtime = now; 265 | this._metadata.ctime = now; 266 | this._dir.delete(oldName); 267 | this._dir.set(newName, index); 268 | } 269 | return; 270 | } 271 | 272 | /** 273 | * This is to be called when all hardlinks and references to this directory reduce to 0. 274 | * The destructor here is about unlinking the parent directory. 275 | * Because the `..` will no longer exist. 276 | */ 277 | destructor (): void { 278 | // decrement the parent's nlink due to '..' 279 | // however do not do this on root otherwise there will be an infinite loop 280 | if (this._dir.get('.') !== this._dir.get('..')) { 281 | const parentIndex = this._dir.get('..'); 282 | if (parentIndex != null) { 283 | this._iNodeMgr.unlinkINode(this._iNodeMgr.getINode(parentIndex)); 284 | } 285 | } 286 | return; 287 | } 288 | 289 | } 290 | 291 | /** 292 | * Class representing a Symlink. 293 | * @extends INode 294 | */ 295 | class Symlink extends INode { 296 | 297 | _link: string; 298 | 299 | /** 300 | * Creates a symlink. 301 | */ 302 | constructor ( 303 | props: { 304 | ino: number, 305 | mode: number, 306 | uid: number, 307 | gid: number, 308 | link: string 309 | }, 310 | iNodeMgr: INodeManager 311 | ) { 312 | super( 313 | { 314 | ino: props.ino, 315 | mode: constants.S_IFLNK | (props.mode & (~constants.S_IFMT)), 316 | uid: props.uid, 317 | gid: props.gid, 318 | size: Buffer.from(props.link).byteLength 319 | }, 320 | iNodeMgr 321 | ); 322 | this._link = props.link; 323 | } 324 | 325 | /** 326 | * Gets the link string. 327 | */ 328 | getLink (): string { 329 | return this._link; 330 | } 331 | 332 | /** 333 | * Noop. 334 | */ 335 | destructor (): void { 336 | return; 337 | } 338 | 339 | } 340 | 341 | /** 342 | * Class representing a character device. 343 | * @extends INode 344 | */ 345 | class CharacterDev extends INode { 346 | 347 | /** 348 | * Creates a character device. 349 | */ 350 | constructor ( 351 | props: { 352 | ino: number, 353 | mode: number, 354 | uid: number, 355 | gid: number, 356 | rdev: number 357 | }, 358 | iNodeMgr: INodeManager 359 | ) { 360 | super( 361 | { 362 | ino: props.ino, 363 | mode: constants.S_IFCHR | (props.mode & (~constants.S_IFMT)), 364 | uid: props.uid, 365 | gid: props.gid, 366 | rdev: props.rdev, 367 | size: 0 368 | }, 369 | iNodeMgr 370 | ); 371 | } 372 | 373 | getFileDesOps (): ?DeviceInterface { 374 | const [major, minor] = unmkDev(this.getMetadata().rdev); 375 | return this._iNodeMgr._devMgr.getChr(major, minor); 376 | } 377 | 378 | destructor (): void { 379 | return; 380 | } 381 | 382 | } 383 | 384 | /** 385 | * Class that manages all iNodes including creation and deletion 386 | */ 387 | class INodeManager { 388 | 389 | _counter: Counter; 390 | _iNodes: Map; 391 | _iNodeRefs: WeakMap; 392 | _devMgr: DeviceManager; 393 | 394 | /** 395 | * Creates an instance of the INodeManager. 396 | * It starts the inode counter at 1, as 0 is usually reserved in posix filesystems. 397 | */ 398 | constructor (devMgr: DeviceManager) { 399 | this._counter = new Counter(1); 400 | this._iNodes = new Map; 401 | this._iNodeRefs = new WeakMap; 402 | this._devMgr = devMgr; 403 | } 404 | 405 | /** 406 | * Creates an inode, from a INode constructor function. 407 | * The returned inode must be used and later manually deallocated. 408 | */ 409 | createINode ( 410 | iNodeConstructor: Class, 411 | props: Object = {} 412 | ): [$Subtype, number] { 413 | props.ino = this._counter.allocate(); 414 | props.mode = (typeof props.mode === 'number') ? props.mode : 0; 415 | props.uid = (typeof props.uid === 'number') ? props.uid : DEFAULT_ROOT_UID; 416 | props.gid = (typeof props.gid === 'number') ? props.gid : DEFAULT_ROOT_GID; 417 | const iNode = new iNodeConstructor(props, this); 418 | this._iNodes.set(props.ino, iNode); 419 | this._iNodeRefs.set(iNode, 0); 420 | return [iNode, props.ino]; 421 | } 422 | 423 | /** 424 | * Gets the inode. 425 | */ 426 | getINode (index: number): $Subtype|void { 427 | return this._iNodes.get(index); 428 | } 429 | 430 | /** 431 | * Links an inode, this increments the hardlink reference count. 432 | */ 433 | linkINode (iNode: ?$Subtype): void { 434 | if (iNode) { 435 | ++(iNode.getMetadata().nlink); 436 | } 437 | return; 438 | } 439 | 440 | /** 441 | * Unlinks an inode, this decrements the hardlink reference count. 442 | */ 443 | unlinkINode (iNode: ?$Subtype): void { 444 | if (iNode) { 445 | --(iNode.getMetadata().nlink); 446 | this._gcINode(iNode); 447 | } 448 | return; 449 | } 450 | 451 | /** 452 | * References an inode, this increments the private reference count. 453 | * Private reference count can be used by file descriptors and working directory position. 454 | */ 455 | refINode (iNode: ?$Subtype): void { 456 | if (iNode) { 457 | const refCount = this._iNodeRefs.get(iNode); 458 | if (refCount !== undefined) { 459 | this._iNodeRefs.set(iNode, refCount + 1); 460 | } 461 | } 462 | return; 463 | } 464 | 465 | /** 466 | * Unreferences an inode, this decrements the private reference count. 467 | */ 468 | unrefINode (iNode: ?$Subtype): void { 469 | if (iNode) { 470 | const refCount = this._iNodeRefs.get(iNode); 471 | if (refCount !== undefined) { 472 | this._iNodeRefs.set(iNode, refCount - 1); 473 | this._gcINode(iNode); 474 | } 475 | } 476 | return; 477 | } 478 | 479 | /** 480 | * Decides whether to garbage collect the inode. 481 | * The true usage count is the hardlink count plus the private reference count. 482 | * Usually if the true usage count is 0, then the inode is garbage collected. 483 | * However directories are special cased here, due to the `.` circular hardlink. 484 | * This allows directories to be garbage collected even when their usage count is 1. 485 | * This is possible also because there cannot be custom hardlinks to directories. 486 | */ 487 | _gcINode (iNode: $Subtype): void { 488 | const metadata = iNode.getMetadata(); 489 | const useCount = metadata.nlink + this._iNodeRefs.get(iNode); 490 | if ( 491 | useCount === 0 || 492 | (useCount === 1 && iNode instanceof Directory) 493 | ) { 494 | const index = metadata.ino; 495 | iNode.destructor(); 496 | this._iNodes.delete(index); 497 | this._counter.deallocate(index); 498 | } 499 | } 500 | 501 | } 502 | 503 | export { File, Directory, Symlink, CharacterDev, INodeManager }; 504 | 505 | export type { INode }; 506 | -------------------------------------------------------------------------------- /doc/assets/bass.css: -------------------------------------------------------------------------------- 1 | /*! Basscss | http://basscss.com | MIT License */ 2 | 3 | .h1{ font-size: 2rem } 4 | .h2{ font-size: 1.5rem } 5 | .h3{ font-size: 1.25rem } 6 | .h4{ font-size: 1rem } 7 | .h5{ font-size: .875rem } 8 | .h6{ font-size: .75rem } 9 | 10 | .font-family-inherit{ font-family:inherit } 11 | .font-size-inherit{ font-size:inherit } 12 | .text-decoration-none{ text-decoration:none } 13 | 14 | .bold{ font-weight: bold; font-weight: bold } 15 | .regular{ font-weight:normal } 16 | .italic{ font-style:italic } 17 | .caps{ text-transform:uppercase; letter-spacing: .2em; } 18 | 19 | .left-align{ text-align:left } 20 | .center{ text-align:center } 21 | .right-align{ text-align:right } 22 | .justify{ text-align:justify } 23 | 24 | .nowrap{ white-space:nowrap } 25 | .break-word{ word-wrap:break-word } 26 | 27 | .line-height-1{ line-height: 1 } 28 | .line-height-2{ line-height: 1.125 } 29 | .line-height-3{ line-height: 1.25 } 30 | .line-height-4{ line-height: 1.5 } 31 | 32 | .list-style-none{ list-style:none } 33 | .underline{ text-decoration:underline } 34 | 35 | .truncate{ 36 | max-width:100%; 37 | overflow:hidden; 38 | text-overflow:ellipsis; 39 | white-space:nowrap; 40 | } 41 | 42 | .list-reset{ 43 | list-style:none; 44 | padding-left:0; 45 | } 46 | 47 | .inline{ display:inline } 48 | .block{ display:block } 49 | .inline-block{ display:inline-block } 50 | .table{ display:table } 51 | .table-cell{ display:table-cell } 52 | 53 | .overflow-hidden{ overflow:hidden } 54 | .overflow-scroll{ overflow:scroll } 55 | .overflow-auto{ overflow:auto } 56 | 57 | .clearfix:before, 58 | .clearfix:after{ 59 | content:" "; 60 | display:table 61 | } 62 | .clearfix:after{ clear:both } 63 | 64 | .left{ float:left } 65 | .right{ float:right } 66 | 67 | .fit{ max-width:100% } 68 | 69 | .max-width-1{ max-width: 24rem } 70 | .max-width-2{ max-width: 32rem } 71 | .max-width-3{ max-width: 48rem } 72 | .max-width-4{ max-width: 64rem } 73 | 74 | .border-box{ box-sizing:border-box } 75 | 76 | .align-baseline{ vertical-align:baseline } 77 | .align-top{ vertical-align:top } 78 | .align-middle{ vertical-align:middle } 79 | .align-bottom{ vertical-align:bottom } 80 | 81 | .m0{ margin:0 } 82 | .mt0{ margin-top:0 } 83 | .mr0{ margin-right:0 } 84 | .mb0{ margin-bottom:0 } 85 | .ml0{ margin-left:0 } 86 | .mx0{ margin-left:0; margin-right:0 } 87 | .my0{ margin-top:0; margin-bottom:0 } 88 | 89 | .m1{ margin: .5rem } 90 | .mt1{ margin-top: .5rem } 91 | .mr1{ margin-right: .5rem } 92 | .mb1{ margin-bottom: .5rem } 93 | .ml1{ margin-left: .5rem } 94 | .mx1{ margin-left: .5rem; margin-right: .5rem } 95 | .my1{ margin-top: .5rem; margin-bottom: .5rem } 96 | 97 | .m2{ margin: 1rem } 98 | .mt2{ margin-top: 1rem } 99 | .mr2{ margin-right: 1rem } 100 | .mb2{ margin-bottom: 1rem } 101 | .ml2{ margin-left: 1rem } 102 | .mx2{ margin-left: 1rem; margin-right: 1rem } 103 | .my2{ margin-top: 1rem; margin-bottom: 1rem } 104 | 105 | .m3{ margin: 2rem } 106 | .mt3{ margin-top: 2rem } 107 | .mr3{ margin-right: 2rem } 108 | .mb3{ margin-bottom: 2rem } 109 | .ml3{ margin-left: 2rem } 110 | .mx3{ margin-left: 2rem; margin-right: 2rem } 111 | .my3{ margin-top: 2rem; margin-bottom: 2rem } 112 | 113 | .m4{ margin: 4rem } 114 | .mt4{ margin-top: 4rem } 115 | .mr4{ margin-right: 4rem } 116 | .mb4{ margin-bottom: 4rem } 117 | .ml4{ margin-left: 4rem } 118 | .mx4{ margin-left: 4rem; margin-right: 4rem } 119 | .my4{ margin-top: 4rem; margin-bottom: 4rem } 120 | 121 | .mxn1{ margin-left: -.5rem; margin-right: -.5rem; } 122 | .mxn2{ margin-left: -1rem; margin-right: -1rem; } 123 | .mxn3{ margin-left: -2rem; margin-right: -2rem; } 124 | .mxn4{ margin-left: -4rem; margin-right: -4rem; } 125 | 126 | .ml-auto{ margin-left:auto } 127 | .mr-auto{ margin-right:auto } 128 | .mx-auto{ margin-left:auto; margin-right:auto; } 129 | 130 | .p0{ padding:0 } 131 | .pt0{ padding-top:0 } 132 | .pr0{ padding-right:0 } 133 | .pb0{ padding-bottom:0 } 134 | .pl0{ padding-left:0 } 135 | .px0{ padding-left:0; padding-right:0 } 136 | .py0{ padding-top:0; padding-bottom:0 } 137 | 138 | .p1{ padding: .5rem } 139 | .pt1{ padding-top: .5rem } 140 | .pr1{ padding-right: .5rem } 141 | .pb1{ padding-bottom: .5rem } 142 | .pl1{ padding-left: .5rem } 143 | .py1{ padding-top: .5rem; padding-bottom: .5rem } 144 | .px1{ padding-left: .5rem; padding-right: .5rem } 145 | 146 | .p2{ padding: 1rem } 147 | .pt2{ padding-top: 1rem } 148 | .pr2{ padding-right: 1rem } 149 | .pb2{ padding-bottom: 1rem } 150 | .pl2{ padding-left: 1rem } 151 | .py2{ padding-top: 1rem; padding-bottom: 1rem } 152 | .px2{ padding-left: 1rem; padding-right: 1rem } 153 | 154 | .p3{ padding: 2rem } 155 | .pt3{ padding-top: 2rem } 156 | .pr3{ padding-right: 2rem } 157 | .pb3{ padding-bottom: 2rem } 158 | .pl3{ padding-left: 2rem } 159 | .py3{ padding-top: 2rem; padding-bottom: 2rem } 160 | .px3{ padding-left: 2rem; padding-right: 2rem } 161 | 162 | .p4{ padding: 4rem } 163 | .pt4{ padding-top: 4rem } 164 | .pr4{ padding-right: 4rem } 165 | .pb4{ padding-bottom: 4rem } 166 | .pl4{ padding-left: 4rem } 167 | .py4{ padding-top: 4rem; padding-bottom: 4rem } 168 | .px4{ padding-left: 4rem; padding-right: 4rem } 169 | 170 | .col{ 171 | float:left; 172 | box-sizing:border-box; 173 | } 174 | 175 | .col-right{ 176 | float:right; 177 | box-sizing:border-box; 178 | } 179 | 180 | .col-1{ 181 | width:8.33333%; 182 | } 183 | 184 | .col-2{ 185 | width:16.66667%; 186 | } 187 | 188 | .col-3{ 189 | width:25%; 190 | } 191 | 192 | .col-4{ 193 | width:33.33333%; 194 | } 195 | 196 | .col-5{ 197 | width:41.66667%; 198 | } 199 | 200 | .col-6{ 201 | width:50%; 202 | } 203 | 204 | .col-7{ 205 | width:58.33333%; 206 | } 207 | 208 | .col-8{ 209 | width:66.66667%; 210 | } 211 | 212 | .col-9{ 213 | width:75%; 214 | } 215 | 216 | .col-10{ 217 | width:83.33333%; 218 | } 219 | 220 | .col-11{ 221 | width:91.66667%; 222 | } 223 | 224 | .col-12{ 225 | width:100%; 226 | } 227 | @media (min-width: 40em){ 228 | 229 | .sm-col{ 230 | float:left; 231 | box-sizing:border-box; 232 | } 233 | 234 | .sm-col-right{ 235 | float:right; 236 | box-sizing:border-box; 237 | } 238 | 239 | .sm-col-1{ 240 | width:8.33333%; 241 | } 242 | 243 | .sm-col-2{ 244 | width:16.66667%; 245 | } 246 | 247 | .sm-col-3{ 248 | width:25%; 249 | } 250 | 251 | .sm-col-4{ 252 | width:33.33333%; 253 | } 254 | 255 | .sm-col-5{ 256 | width:41.66667%; 257 | } 258 | 259 | .sm-col-6{ 260 | width:50%; 261 | } 262 | 263 | .sm-col-7{ 264 | width:58.33333%; 265 | } 266 | 267 | .sm-col-8{ 268 | width:66.66667%; 269 | } 270 | 271 | .sm-col-9{ 272 | width:75%; 273 | } 274 | 275 | .sm-col-10{ 276 | width:83.33333%; 277 | } 278 | 279 | .sm-col-11{ 280 | width:91.66667%; 281 | } 282 | 283 | .sm-col-12{ 284 | width:100%; 285 | } 286 | 287 | } 288 | @media (min-width: 52em){ 289 | 290 | .md-col{ 291 | float:left; 292 | box-sizing:border-box; 293 | } 294 | 295 | .md-col-right{ 296 | float:right; 297 | box-sizing:border-box; 298 | } 299 | 300 | .md-col-1{ 301 | width:8.33333%; 302 | } 303 | 304 | .md-col-2{ 305 | width:16.66667%; 306 | } 307 | 308 | .md-col-3{ 309 | width:25%; 310 | } 311 | 312 | .md-col-4{ 313 | width:33.33333%; 314 | } 315 | 316 | .md-col-5{ 317 | width:41.66667%; 318 | } 319 | 320 | .md-col-6{ 321 | width:50%; 322 | } 323 | 324 | .md-col-7{ 325 | width:58.33333%; 326 | } 327 | 328 | .md-col-8{ 329 | width:66.66667%; 330 | } 331 | 332 | .md-col-9{ 333 | width:75%; 334 | } 335 | 336 | .md-col-10{ 337 | width:83.33333%; 338 | } 339 | 340 | .md-col-11{ 341 | width:91.66667%; 342 | } 343 | 344 | .md-col-12{ 345 | width:100%; 346 | } 347 | 348 | } 349 | @media (min-width: 64em){ 350 | 351 | .lg-col{ 352 | float:left; 353 | box-sizing:border-box; 354 | } 355 | 356 | .lg-col-right{ 357 | float:right; 358 | box-sizing:border-box; 359 | } 360 | 361 | .lg-col-1{ 362 | width:8.33333%; 363 | } 364 | 365 | .lg-col-2{ 366 | width:16.66667%; 367 | } 368 | 369 | .lg-col-3{ 370 | width:25%; 371 | } 372 | 373 | .lg-col-4{ 374 | width:33.33333%; 375 | } 376 | 377 | .lg-col-5{ 378 | width:41.66667%; 379 | } 380 | 381 | .lg-col-6{ 382 | width:50%; 383 | } 384 | 385 | .lg-col-7{ 386 | width:58.33333%; 387 | } 388 | 389 | .lg-col-8{ 390 | width:66.66667%; 391 | } 392 | 393 | .lg-col-9{ 394 | width:75%; 395 | } 396 | 397 | .lg-col-10{ 398 | width:83.33333%; 399 | } 400 | 401 | .lg-col-11{ 402 | width:91.66667%; 403 | } 404 | 405 | .lg-col-12{ 406 | width:100%; 407 | } 408 | 409 | } 410 | .flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 411 | 412 | @media (min-width: 40em){ 413 | .sm-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 414 | } 415 | 416 | @media (min-width: 52em){ 417 | .md-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 418 | } 419 | 420 | @media (min-width: 64em){ 421 | .lg-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 422 | } 423 | 424 | .flex-column{ -webkit-box-orient:vertical; -webkit-box-direction:normal; -webkit-flex-direction:column; -ms-flex-direction:column; flex-direction:column } 425 | .flex-wrap{ -webkit-flex-wrap:wrap; -ms-flex-wrap:wrap; flex-wrap:wrap } 426 | 427 | .items-start{ -webkit-box-align:start; -webkit-align-items:flex-start; -ms-flex-align:start; -ms-grid-row-align:flex-start; align-items:flex-start } 428 | .items-end{ -webkit-box-align:end; -webkit-align-items:flex-end; -ms-flex-align:end; -ms-grid-row-align:flex-end; align-items:flex-end } 429 | .items-center{ -webkit-box-align:center; -webkit-align-items:center; -ms-flex-align:center; -ms-grid-row-align:center; align-items:center } 430 | .items-baseline{ -webkit-box-align:baseline; -webkit-align-items:baseline; -ms-flex-align:baseline; -ms-grid-row-align:baseline; align-items:baseline } 431 | .items-stretch{ -webkit-box-align:stretch; -webkit-align-items:stretch; -ms-flex-align:stretch; -ms-grid-row-align:stretch; align-items:stretch } 432 | 433 | .self-start{ -webkit-align-self:flex-start; -ms-flex-item-align:start; align-self:flex-start } 434 | .self-end{ -webkit-align-self:flex-end; -ms-flex-item-align:end; align-self:flex-end } 435 | .self-center{ -webkit-align-self:center; -ms-flex-item-align:center; align-self:center } 436 | .self-baseline{ -webkit-align-self:baseline; -ms-flex-item-align:baseline; align-self:baseline } 437 | .self-stretch{ -webkit-align-self:stretch; -ms-flex-item-align:stretch; align-self:stretch } 438 | 439 | .justify-start{ -webkit-box-pack:start; -webkit-justify-content:flex-start; -ms-flex-pack:start; justify-content:flex-start } 440 | .justify-end{ -webkit-box-pack:end; -webkit-justify-content:flex-end; -ms-flex-pack:end; justify-content:flex-end } 441 | .justify-center{ -webkit-box-pack:center; -webkit-justify-content:center; -ms-flex-pack:center; justify-content:center } 442 | .justify-between{ -webkit-box-pack:justify; -webkit-justify-content:space-between; -ms-flex-pack:justify; justify-content:space-between } 443 | .justify-around{ -webkit-justify-content:space-around; -ms-flex-pack:distribute; justify-content:space-around } 444 | 445 | .content-start{ -webkit-align-content:flex-start; -ms-flex-line-pack:start; align-content:flex-start } 446 | .content-end{ -webkit-align-content:flex-end; -ms-flex-line-pack:end; align-content:flex-end } 447 | .content-center{ -webkit-align-content:center; -ms-flex-line-pack:center; align-content:center } 448 | .content-between{ -webkit-align-content:space-between; -ms-flex-line-pack:justify; align-content:space-between } 449 | .content-around{ -webkit-align-content:space-around; -ms-flex-line-pack:distribute; align-content:space-around } 450 | .content-stretch{ -webkit-align-content:stretch; -ms-flex-line-pack:stretch; align-content:stretch } 451 | .flex-auto{ 452 | -webkit-box-flex:1; 453 | -webkit-flex:1 1 auto; 454 | -ms-flex:1 1 auto; 455 | flex:1 1 auto; 456 | min-width:0; 457 | min-height:0; 458 | } 459 | .flex-none{ -webkit-box-flex:0; -webkit-flex:none; -ms-flex:none; flex:none } 460 | .fs0{ flex-shrink: 0 } 461 | 462 | .order-0{ -webkit-box-ordinal-group:1; -webkit-order:0; -ms-flex-order:0; order:0 } 463 | .order-1{ -webkit-box-ordinal-group:2; -webkit-order:1; -ms-flex-order:1; order:1 } 464 | .order-2{ -webkit-box-ordinal-group:3; -webkit-order:2; -ms-flex-order:2; order:2 } 465 | .order-3{ -webkit-box-ordinal-group:4; -webkit-order:3; -ms-flex-order:3; order:3 } 466 | .order-last{ -webkit-box-ordinal-group:100000; -webkit-order:99999; -ms-flex-order:99999; order:99999 } 467 | 468 | .relative{ position:relative } 469 | .absolute{ position:absolute } 470 | .fixed{ position:fixed } 471 | 472 | .top-0{ top:0 } 473 | .right-0{ right:0 } 474 | .bottom-0{ bottom:0 } 475 | .left-0{ left:0 } 476 | 477 | .z1{ z-index: 1 } 478 | .z2{ z-index: 2 } 479 | .z3{ z-index: 3 } 480 | .z4{ z-index: 4 } 481 | 482 | .border{ 483 | border-style:solid; 484 | border-width: 1px; 485 | } 486 | 487 | .border-top{ 488 | border-top-style:solid; 489 | border-top-width: 1px; 490 | } 491 | 492 | .border-right{ 493 | border-right-style:solid; 494 | border-right-width: 1px; 495 | } 496 | 497 | .border-bottom{ 498 | border-bottom-style:solid; 499 | border-bottom-width: 1px; 500 | } 501 | 502 | .border-left{ 503 | border-left-style:solid; 504 | border-left-width: 1px; 505 | } 506 | 507 | .border-none{ border:0 } 508 | 509 | .rounded{ border-radius: 3px } 510 | .circle{ border-radius:50% } 511 | 512 | .rounded-top{ border-radius: 3px 3px 0 0 } 513 | .rounded-right{ border-radius: 0 3px 3px 0 } 514 | .rounded-bottom{ border-radius: 0 0 3px 3px } 515 | .rounded-left{ border-radius: 3px 0 0 3px } 516 | 517 | .not-rounded{ border-radius:0 } 518 | 519 | .hide{ 520 | position:absolute !important; 521 | height:1px; 522 | width:1px; 523 | overflow:hidden; 524 | clip:rect(1px, 1px, 1px, 1px); 525 | } 526 | 527 | @media (max-width: 40em){ 528 | .xs-hide{ display:none !important } 529 | } 530 | 531 | @media (min-width: 40em) and (max-width: 52em){ 532 | .sm-hide{ display:none !important } 533 | } 534 | 535 | @media (min-width: 52em) and (max-width: 64em){ 536 | .md-hide{ display:none !important } 537 | } 538 | 539 | @media (min-width: 64em){ 540 | .lg-hide{ display:none !important } 541 | } 542 | 543 | .display-none{ display:none !important } 544 | 545 | -------------------------------------------------------------------------------- /doc/assets/anchor.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * AnchorJS - v4.0.0 - 2017-06-02 3 | * https://github.com/bryanbraun/anchorjs 4 | * Copyright (c) 2017 Bryan Braun; Licensed MIT 5 | */ 6 | /* eslint-env amd, node */ 7 | 8 | // https://github.com/umdjs/umd/blob/master/templates/returnExports.js 9 | (function(root, factory) { 10 | 'use strict'; 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define([], factory); 14 | } else if (typeof module === 'object' && module.exports) { 15 | // Node. Does not work with strict CommonJS, but 16 | // only CommonJS-like environments that support module.exports, 17 | // like Node. 18 | module.exports = factory(); 19 | } else { 20 | // Browser globals (root is window) 21 | root.AnchorJS = factory(); 22 | root.anchors = new root.AnchorJS(); 23 | } 24 | })(this, function() { 25 | 'use strict'; 26 | function AnchorJS(options) { 27 | this.options = options || {}; 28 | this.elements = []; 29 | 30 | /** 31 | * Assigns options to the internal options object, and provides defaults. 32 | * @param {Object} opts - Options object 33 | */ 34 | function _applyRemainingDefaultOptions(opts) { 35 | opts.icon = opts.hasOwnProperty('icon') ? opts.icon : '\ue9cb'; // Accepts characters (and also URLs?), like '#', '¶', '❡', or '§'. 36 | opts.visible = opts.hasOwnProperty('visible') ? opts.visible : 'hover'; // Also accepts 'always' & 'touch' 37 | opts.placement = opts.hasOwnProperty('placement') 38 | ? opts.placement 39 | : 'right'; // Also accepts 'left' 40 | opts.class = opts.hasOwnProperty('class') ? opts.class : ''; // Accepts any class name. 41 | // Using Math.floor here will ensure the value is Number-cast and an integer. 42 | opts.truncate = opts.hasOwnProperty('truncate') 43 | ? Math.floor(opts.truncate) 44 | : 64; // Accepts any value that can be typecast to a number. 45 | } 46 | 47 | _applyRemainingDefaultOptions(this.options); 48 | 49 | /** 50 | * Checks to see if this device supports touch. Uses criteria pulled from Modernizr: 51 | * https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40 52 | * @returns {Boolean} - true if the current device supports touch. 53 | */ 54 | this.isTouchDevice = function() { 55 | return !!( 56 | 'ontouchstart' in window || 57 | (window.DocumentTouch && document instanceof DocumentTouch) 58 | ); 59 | }; 60 | 61 | /** 62 | * Add anchor links to page elements. 63 | * @param {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links 64 | * to. Also accepts an array or nodeList containing the relavant elements. 65 | * @returns {this} - The AnchorJS object 66 | */ 67 | this.add = function(selector) { 68 | var elements, 69 | elsWithIds, 70 | idList, 71 | elementID, 72 | i, 73 | index, 74 | count, 75 | tidyText, 76 | newTidyText, 77 | readableID, 78 | anchor, 79 | visibleOptionToUse, 80 | indexesToDrop = []; 81 | 82 | // We reapply options here because somebody may have overwritten the default options object when setting options. 83 | // For example, this overwrites all options but visible: 84 | // 85 | // anchors.options = { visible: 'always'; } 86 | _applyRemainingDefaultOptions(this.options); 87 | 88 | visibleOptionToUse = this.options.visible; 89 | if (visibleOptionToUse === 'touch') { 90 | visibleOptionToUse = this.isTouchDevice() ? 'always' : 'hover'; 91 | } 92 | 93 | // Provide a sensible default selector, if none is given. 94 | if (!selector) { 95 | selector = 'h2, h3, h4, h5, h6'; 96 | } 97 | 98 | elements = _getElements(selector); 99 | 100 | if (elements.length === 0) { 101 | return this; 102 | } 103 | 104 | _addBaselineStyles(); 105 | 106 | // We produce a list of existing IDs so we don't generate a duplicate. 107 | elsWithIds = document.querySelectorAll('[id]'); 108 | idList = [].map.call(elsWithIds, function assign(el) { 109 | return el.id; 110 | }); 111 | 112 | for (i = 0; i < elements.length; i++) { 113 | if (this.hasAnchorJSLink(elements[i])) { 114 | indexesToDrop.push(i); 115 | continue; 116 | } 117 | 118 | if (elements[i].hasAttribute('id')) { 119 | elementID = elements[i].getAttribute('id'); 120 | } else if (elements[i].hasAttribute('data-anchor-id')) { 121 | elementID = elements[i].getAttribute('data-anchor-id'); 122 | } else { 123 | tidyText = this.urlify(elements[i].textContent); 124 | 125 | // Compare our generated ID to existing IDs (and increment it if needed) 126 | // before we add it to the page. 127 | newTidyText = tidyText; 128 | count = 0; 129 | do { 130 | if (index !== undefined) { 131 | newTidyText = tidyText + '-' + count; 132 | } 133 | 134 | index = idList.indexOf(newTidyText); 135 | count += 1; 136 | } while (index !== -1); 137 | index = undefined; 138 | idList.push(newTidyText); 139 | 140 | elements[i].setAttribute('id', newTidyText); 141 | elementID = newTidyText; 142 | } 143 | 144 | readableID = elementID.replace(/-/g, ' '); 145 | 146 | // The following code builds the following DOM structure in a more effiecient (albeit opaque) way. 147 | // ''; 148 | anchor = document.createElement('a'); 149 | anchor.className = 'anchorjs-link ' + this.options.class; 150 | anchor.href = '#' + elementID; 151 | anchor.setAttribute('aria-label', 'Anchor link for: ' + readableID); 152 | anchor.setAttribute('data-anchorjs-icon', this.options.icon); 153 | 154 | if (visibleOptionToUse === 'always') { 155 | anchor.style.opacity = '1'; 156 | } 157 | 158 | if (this.options.icon === '\ue9cb') { 159 | anchor.style.font = '1em/1 anchorjs-icons'; 160 | 161 | // We set lineHeight = 1 here because the `anchorjs-icons` font family could otherwise affect the 162 | // height of the heading. This isn't the case for icons with `placement: left`, so we restore 163 | // line-height: inherit in that case, ensuring they remain positioned correctly. For more info, 164 | // see https://github.com/bryanbraun/anchorjs/issues/39. 165 | if (this.options.placement === 'left') { 166 | anchor.style.lineHeight = 'inherit'; 167 | } 168 | } 169 | 170 | if (this.options.placement === 'left') { 171 | anchor.style.position = 'absolute'; 172 | anchor.style.marginLeft = '-1em'; 173 | anchor.style.paddingRight = '0.5em'; 174 | elements[i].insertBefore(anchor, elements[i].firstChild); 175 | } else { 176 | // if the option provided is `right` (or anything else). 177 | anchor.style.paddingLeft = '0.375em'; 178 | elements[i].appendChild(anchor); 179 | } 180 | } 181 | 182 | for (i = 0; i < indexesToDrop.length; i++) { 183 | elements.splice(indexesToDrop[i] - i, 1); 184 | } 185 | this.elements = this.elements.concat(elements); 186 | 187 | return this; 188 | }; 189 | 190 | /** 191 | * Removes all anchorjs-links from elements targed by the selector. 192 | * @param {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links, 193 | * OR a nodeList / array containing the DOM elements. 194 | * @returns {this} - The AnchorJS object 195 | */ 196 | this.remove = function(selector) { 197 | var index, 198 | domAnchor, 199 | elements = _getElements(selector); 200 | 201 | for (var i = 0; i < elements.length; i++) { 202 | domAnchor = elements[i].querySelector('.anchorjs-link'); 203 | if (domAnchor) { 204 | // Drop the element from our main list, if it's in there. 205 | index = this.elements.indexOf(elements[i]); 206 | if (index !== -1) { 207 | this.elements.splice(index, 1); 208 | } 209 | // Remove the anchor from the DOM. 210 | elements[i].removeChild(domAnchor); 211 | } 212 | } 213 | return this; 214 | }; 215 | 216 | /** 217 | * Removes all anchorjs links. Mostly used for tests. 218 | */ 219 | this.removeAll = function() { 220 | this.remove(this.elements); 221 | }; 222 | 223 | /** 224 | * Urlify - Refine text so it makes a good ID. 225 | * 226 | * To do this, we remove apostrophes, replace nonsafe characters with hyphens, 227 | * remove extra hyphens, truncate, trim hyphens, and make lowercase. 228 | * 229 | * @param {String} text - Any text. Usually pulled from the webpage element we are linking to. 230 | * @returns {String} - hyphen-delimited text for use in IDs and URLs. 231 | */ 232 | this.urlify = function(text) { 233 | // Regex for finding the nonsafe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\ 234 | var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\]/g, 235 | urlText; 236 | 237 | // The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently, 238 | // even after setting options. This can be useful for tests or other applications. 239 | if (!this.options.truncate) { 240 | _applyRemainingDefaultOptions(this.options); 241 | } 242 | 243 | // Note: we trim hyphens after truncating because truncating can cause dangling hyphens. 244 | // Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 245 | urlText = text 246 | .trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 247 | .replace(/\'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 248 | .replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-" 249 | .replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-" 250 | .substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-" 251 | .replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated" 252 | .toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated" 253 | 254 | return urlText; 255 | }; 256 | 257 | /** 258 | * Determines if this element already has an AnchorJS link on it. 259 | * Uses this technique: http://stackoverflow.com/a/5898748/1154642 260 | * @param {HTMLElemnt} el - a DOM node 261 | * @returns {Boolean} true/false 262 | */ 263 | this.hasAnchorJSLink = function(el) { 264 | var hasLeftAnchor = 265 | el.firstChild && 266 | (' ' + el.firstChild.className + ' ').indexOf(' anchorjs-link ') > -1, 267 | hasRightAnchor = 268 | el.lastChild && 269 | (' ' + el.lastChild.className + ' ').indexOf(' anchorjs-link ') > -1; 270 | 271 | return hasLeftAnchor || hasRightAnchor || false; 272 | }; 273 | 274 | /** 275 | * Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods). 276 | * It also throws errors on any other inputs. Used to handle inputs to .add and .remove. 277 | * @param {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links, 278 | * OR a nodeList / array containing the DOM elements. 279 | * @returns {Array} - An array containing the elements we want. 280 | */ 281 | function _getElements(input) { 282 | var elements; 283 | if (typeof input === 'string' || input instanceof String) { 284 | // See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array. 285 | elements = [].slice.call(document.querySelectorAll(input)); 286 | // I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me. 287 | } else if (Array.isArray(input) || input instanceof NodeList) { 288 | elements = [].slice.call(input); 289 | } else { 290 | throw new Error('The selector provided to AnchorJS was invalid.'); 291 | } 292 | return elements; 293 | } 294 | 295 | /** 296 | * _addBaselineStyles 297 | * Adds baseline styles to the page, used by all AnchorJS links irregardless of configuration. 298 | */ 299 | function _addBaselineStyles() { 300 | // We don't want to add global baseline styles if they've been added before. 301 | if (document.head.querySelector('style.anchorjs') !== null) { 302 | return; 303 | } 304 | 305 | var style = document.createElement('style'), 306 | linkRule = 307 | ' .anchorjs-link {' + 308 | ' opacity: 0;' + 309 | ' text-decoration: none;' + 310 | ' -webkit-font-smoothing: antialiased;' + 311 | ' -moz-osx-font-smoothing: grayscale;' + 312 | ' }', 313 | hoverRule = 314 | ' *:hover > .anchorjs-link,' + 315 | ' .anchorjs-link:focus {' + 316 | ' opacity: 1;' + 317 | ' }', 318 | anchorjsLinkFontFace = 319 | ' @font-face {' + 320 | ' font-family: "anchorjs-icons";' + // Icon from icomoon; 10px wide & 10px tall; 2 empty below & 4 above 321 | ' src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype");' + 322 | ' }', 323 | pseudoElContent = 324 | ' [data-anchorjs-icon]::after {' + 325 | ' content: attr(data-anchorjs-icon);' + 326 | ' }', 327 | firstStyleEl; 328 | 329 | style.className = 'anchorjs'; 330 | style.appendChild(document.createTextNode('')); // Necessary for Webkit. 331 | 332 | // We place it in the head with the other style tags, if possible, so as to 333 | // not look out of place. We insert before the others so these styles can be 334 | // overridden if necessary. 335 | firstStyleEl = document.head.querySelector('[rel="stylesheet"], style'); 336 | if (firstStyleEl === undefined) { 337 | document.head.appendChild(style); 338 | } else { 339 | document.head.insertBefore(style, firstStyleEl); 340 | } 341 | 342 | style.sheet.insertRule(linkRule, style.sheet.cssRules.length); 343 | style.sheet.insertRule(hoverRule, style.sheet.cssRules.length); 344 | style.sheet.insertRule(pseudoElContent, style.sheet.cssRules.length); 345 | style.sheet.insertRule(anchorjsLinkFontFace, style.sheet.cssRules.length); 346 | } 347 | } 348 | 349 | return AnchorJS; 350 | }); 351 | -------------------------------------------------------------------------------- /doc/assets/split.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.3.5 */ 2 | // https://github.com/nathancahill/Split.js 3 | // Copyright (c) 2017 Nathan Cahill; Licensed MIT 4 | 5 | (function(global, factory) { 6 | typeof exports === 'object' && typeof module !== 'undefined' 7 | ? (module.exports = factory()) 8 | : typeof define === 'function' && define.amd 9 | ? define(factory) 10 | : (global.Split = factory()); 11 | })(this, function() { 12 | 'use strict'; 13 | // The programming goals of Split.js are to deliver readable, understandable and 14 | // maintainable code, while at the same time manually optimizing for tiny minified file size, 15 | // browser compatibility without additional requirements, graceful fallback (IE8 is supported) 16 | // and very few assumptions about the user's page layout. 17 | var global = window; 18 | var document = global.document; 19 | 20 | // Save a couple long function names that are used frequently. 21 | // This optimization saves around 400 bytes. 22 | var addEventListener = 'addEventListener'; 23 | var removeEventListener = 'removeEventListener'; 24 | var getBoundingClientRect = 'getBoundingClientRect'; 25 | var NOOP = function() { 26 | return false; 27 | }; 28 | 29 | // Figure out if we're in IE8 or not. IE8 will still render correctly, 30 | // but will be static instead of draggable. 31 | var isIE8 = global.attachEvent && !global[addEventListener]; 32 | 33 | // This library only needs two helper functions: 34 | // 35 | // The first determines which prefixes of CSS calc we need. 36 | // We only need to do this once on startup, when this anonymous function is called. 37 | // 38 | // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow: 39 | // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167 40 | var calc = 41 | ['', '-webkit-', '-moz-', '-o-'] 42 | .filter(function(prefix) { 43 | var el = document.createElement('div'); 44 | el.style.cssText = 'width:' + prefix + 'calc(9px)'; 45 | 46 | return !!el.style.length; 47 | }) 48 | .shift() + 'calc'; 49 | 50 | // The second helper function allows elements and string selectors to be used 51 | // interchangeably. In either case an element is returned. This allows us to 52 | // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`. 53 | var elementOrSelector = function(el) { 54 | if (typeof el === 'string' || el instanceof String) { 55 | return document.querySelector(el); 56 | } 57 | 58 | return el; 59 | }; 60 | 61 | // The main function to initialize a split. Split.js thinks about each pair 62 | // of elements as an independant pair. Dragging the gutter between two elements 63 | // only changes the dimensions of elements in that pair. This is key to understanding 64 | // how the following functions operate, since each function is bound to a pair. 65 | // 66 | // A pair object is shaped like this: 67 | // 68 | // { 69 | // a: DOM element, 70 | // b: DOM element, 71 | // aMin: Number, 72 | // bMin: Number, 73 | // dragging: Boolean, 74 | // parent: DOM element, 75 | // isFirst: Boolean, 76 | // isLast: Boolean, 77 | // direction: 'horizontal' | 'vertical' 78 | // } 79 | // 80 | // The basic sequence: 81 | // 82 | // 1. Set defaults to something sane. `options` doesn't have to be passed at all. 83 | // 2. Initialize a bunch of strings based on the direction we're splitting. 84 | // A lot of the behavior in the rest of the library is paramatized down to 85 | // rely on CSS strings and classes. 86 | // 3. Define the dragging helper functions, and a few helpers to go with them. 87 | // 4. Loop through the elements while pairing them off. Every pair gets an 88 | // `pair` object, a gutter, and special isFirst/isLast properties. 89 | // 5. Actually size the pair elements, insert gutters and attach event listeners. 90 | var Split = function(ids, options) { 91 | if (options === void 0) options = {}; 92 | 93 | var dimension; 94 | var clientDimension; 95 | var clientAxis; 96 | var position; 97 | var paddingA; 98 | var paddingB; 99 | var elements; 100 | 101 | // All DOM elements in the split should have a common parent. We can grab 102 | // the first elements parent and hope users read the docs because the 103 | // behavior will be whacky otherwise. 104 | var parent = elementOrSelector(ids[0]).parentNode; 105 | var parentFlexDirection = global.getComputedStyle(parent).flexDirection; 106 | 107 | // Set default options.sizes to equal percentages of the parent element. 108 | var sizes = 109 | options.sizes || 110 | ids.map(function() { 111 | return 100 / ids.length; 112 | }); 113 | 114 | // Standardize minSize to an array if it isn't already. This allows minSize 115 | // to be passed as a number. 116 | var minSize = options.minSize !== undefined ? options.minSize : 100; 117 | var minSizes = Array.isArray(minSize) 118 | ? minSize 119 | : ids.map(function() { 120 | return minSize; 121 | }); 122 | var gutterSize = options.gutterSize !== undefined ? options.gutterSize : 10; 123 | var snapOffset = options.snapOffset !== undefined ? options.snapOffset : 30; 124 | var direction = options.direction || 'horizontal'; 125 | var cursor = 126 | options.cursor || 127 | (direction === 'horizontal' ? 'ew-resize' : 'ns-resize'); 128 | var gutter = 129 | options.gutter || 130 | function(i, gutterDirection) { 131 | var gut = document.createElement('div'); 132 | gut.className = 'gutter gutter-' + gutterDirection; 133 | return gut; 134 | }; 135 | var elementStyle = 136 | options.elementStyle || 137 | function(dim, size, gutSize) { 138 | var style = {}; 139 | 140 | if (typeof size !== 'string' && !(size instanceof String)) { 141 | if (!isIE8) { 142 | style[dim] = calc + '(' + size + '% - ' + gutSize + 'px)'; 143 | } else { 144 | style[dim] = size + '%'; 145 | } 146 | } else { 147 | style[dim] = size; 148 | } 149 | 150 | return style; 151 | }; 152 | var gutterStyle = 153 | options.gutterStyle || 154 | function(dim, gutSize) { 155 | return (obj = {}), (obj[dim] = gutSize + 'px'), obj; 156 | var obj; 157 | }; 158 | 159 | // 2. Initialize a bunch of strings based on the direction we're splitting. 160 | // A lot of the behavior in the rest of the library is paramatized down to 161 | // rely on CSS strings and classes. 162 | if (direction === 'horizontal') { 163 | dimension = 'width'; 164 | clientDimension = 'clientWidth'; 165 | clientAxis = 'clientX'; 166 | position = 'left'; 167 | paddingA = 'paddingLeft'; 168 | paddingB = 'paddingRight'; 169 | } else if (direction === 'vertical') { 170 | dimension = 'height'; 171 | clientDimension = 'clientHeight'; 172 | clientAxis = 'clientY'; 173 | position = 'top'; 174 | paddingA = 'paddingTop'; 175 | paddingB = 'paddingBottom'; 176 | } 177 | 178 | // 3. Define the dragging helper functions, and a few helpers to go with them. 179 | // Each helper is bound to a pair object that contains it's metadata. This 180 | // also makes it easy to store references to listeners that that will be 181 | // added and removed. 182 | // 183 | // Even though there are no other functions contained in them, aliasing 184 | // this to self saves 50 bytes or so since it's used so frequently. 185 | // 186 | // The pair object saves metadata like dragging state, position and 187 | // event listener references. 188 | 189 | function setElementSize(el, size, gutSize) { 190 | // Split.js allows setting sizes via numbers (ideally), or if you must, 191 | // by string, like '300px'. This is less than ideal, because it breaks 192 | // the fluid layout that `calc(% - px)` provides. You're on your own if you do that, 193 | // make sure you calculate the gutter size by hand. 194 | var style = elementStyle(dimension, size, gutSize); 195 | 196 | // eslint-disable-next-line no-param-reassign 197 | Object.keys(style).forEach(function(prop) { 198 | return (el.style[prop] = style[prop]); 199 | }); 200 | } 201 | 202 | function setGutterSize(gutterElement, gutSize) { 203 | var style = gutterStyle(dimension, gutSize); 204 | 205 | // eslint-disable-next-line no-param-reassign 206 | Object.keys(style).forEach(function(prop) { 207 | return (gutterElement.style[prop] = style[prop]); 208 | }); 209 | } 210 | 211 | // Actually adjust the size of elements `a` and `b` to `offset` while dragging. 212 | // calc is used to allow calc(percentage + gutterpx) on the whole split instance, 213 | // which allows the viewport to be resized without additional logic. 214 | // Element a's size is the same as offset. b's size is total size - a size. 215 | // Both sizes are calculated from the initial parent percentage, 216 | // then the gutter size is subtracted. 217 | function adjust(offset) { 218 | var a = elements[this.a]; 219 | var b = elements[this.b]; 220 | var percentage = a.size + b.size; 221 | 222 | a.size = offset / this.size * percentage; 223 | b.size = percentage - offset / this.size * percentage; 224 | 225 | setElementSize(a.element, a.size, this.aGutterSize); 226 | setElementSize(b.element, b.size, this.bGutterSize); 227 | } 228 | 229 | // drag, where all the magic happens. The logic is really quite simple: 230 | // 231 | // 1. Ignore if the pair is not dragging. 232 | // 2. Get the offset of the event. 233 | // 3. Snap offset to min if within snappable range (within min + snapOffset). 234 | // 4. Actually adjust each element in the pair to offset. 235 | // 236 | // --------------------------------------------------------------------- 237 | // | | <- a.minSize || b.minSize -> | | 238 | // | | | <- this.snapOffset || this.snapOffset -> | | | 239 | // | | | || | | | 240 | // | | | || | | | 241 | // --------------------------------------------------------------------- 242 | // | <- this.start this.size -> | 243 | function drag(e) { 244 | var offset; 245 | 246 | if (!this.dragging) { 247 | return; 248 | } 249 | 250 | // Get the offset of the event from the first side of the 251 | // pair `this.start`. Supports touch events, but not multitouch, so only the first 252 | // finger `touches[0]` is counted. 253 | if ('touches' in e) { 254 | offset = e.touches[0][clientAxis] - this.start; 255 | } else { 256 | offset = e[clientAxis] - this.start; 257 | } 258 | 259 | // If within snapOffset of min or max, set offset to min or max. 260 | // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both. 261 | // Include the appropriate gutter sizes to prevent overflows. 262 | if (offset <= elements[this.a].minSize + snapOffset + this.aGutterSize) { 263 | offset = elements[this.a].minSize + this.aGutterSize; 264 | } else if ( 265 | offset >= 266 | this.size - (elements[this.b].minSize + snapOffset + this.bGutterSize) 267 | ) { 268 | offset = this.size - (elements[this.b].minSize + this.bGutterSize); 269 | } 270 | 271 | // Actually adjust the size. 272 | adjust.call(this, offset); 273 | 274 | // Call the drag callback continously. Don't do anything too intensive 275 | // in this callback. 276 | if (options.onDrag) { 277 | options.onDrag(); 278 | } 279 | } 280 | 281 | // Cache some important sizes when drag starts, so we don't have to do that 282 | // continously: 283 | // 284 | // `size`: The total size of the pair. First + second + first gutter + second gutter. 285 | // `start`: The leading side of the first element. 286 | // 287 | // ------------------------------------------------ 288 | // | aGutterSize -> ||| | 289 | // | ||| | 290 | // | ||| | 291 | // | ||| <- bGutterSize | 292 | // ------------------------------------------------ 293 | // | <- start size -> | 294 | function calculateSizes() { 295 | // Figure out the parent size minus padding. 296 | var a = elements[this.a].element; 297 | var b = elements[this.b].element; 298 | 299 | this.size = 300 | a[getBoundingClientRect]()[dimension] + 301 | b[getBoundingClientRect]()[dimension] + 302 | this.aGutterSize + 303 | this.bGutterSize; 304 | this.start = a[getBoundingClientRect]()[position]; 305 | } 306 | 307 | // stopDragging is very similar to startDragging in reverse. 308 | function stopDragging() { 309 | var self = this; 310 | var a = elements[self.a].element; 311 | var b = elements[self.b].element; 312 | 313 | if (self.dragging && options.onDragEnd) { 314 | options.onDragEnd(); 315 | } 316 | 317 | self.dragging = false; 318 | 319 | // Remove the stored event listeners. This is why we store them. 320 | global[removeEventListener]('mouseup', self.stop); 321 | global[removeEventListener]('touchend', self.stop); 322 | global[removeEventListener]('touchcancel', self.stop); 323 | 324 | self.parent[removeEventListener]('mousemove', self.move); 325 | self.parent[removeEventListener]('touchmove', self.move); 326 | 327 | // Delete them once they are removed. I think this makes a difference 328 | // in memory usage with a lot of splits on one page. But I don't know for sure. 329 | delete self.stop; 330 | delete self.move; 331 | 332 | a[removeEventListener]('selectstart', NOOP); 333 | a[removeEventListener]('dragstart', NOOP); 334 | b[removeEventListener]('selectstart', NOOP); 335 | b[removeEventListener]('dragstart', NOOP); 336 | 337 | a.style.userSelect = ''; 338 | a.style.webkitUserSelect = ''; 339 | a.style.MozUserSelect = ''; 340 | a.style.pointerEvents = ''; 341 | 342 | b.style.userSelect = ''; 343 | b.style.webkitUserSelect = ''; 344 | b.style.MozUserSelect = ''; 345 | b.style.pointerEvents = ''; 346 | 347 | self.gutter.style.cursor = ''; 348 | self.parent.style.cursor = ''; 349 | } 350 | 351 | // startDragging calls `calculateSizes` to store the inital size in the pair object. 352 | // It also adds event listeners for mouse/touch events, 353 | // and prevents selection while dragging so avoid the selecting text. 354 | function startDragging(e) { 355 | // Alias frequently used variables to save space. 200 bytes. 356 | var self = this; 357 | var a = elements[self.a].element; 358 | var b = elements[self.b].element; 359 | 360 | // Call the onDragStart callback. 361 | if (!self.dragging && options.onDragStart) { 362 | options.onDragStart(); 363 | } 364 | 365 | // Don't actually drag the element. We emulate that in the drag function. 366 | e.preventDefault(); 367 | 368 | // Set the dragging property of the pair object. 369 | self.dragging = true; 370 | 371 | // Create two event listeners bound to the same pair object and store 372 | // them in the pair object. 373 | self.move = drag.bind(self); 374 | self.stop = stopDragging.bind(self); 375 | 376 | // All the binding. `window` gets the stop events in case we drag out of the elements. 377 | global[addEventListener]('mouseup', self.stop); 378 | global[addEventListener]('touchend', self.stop); 379 | global[addEventListener]('touchcancel', self.stop); 380 | 381 | self.parent[addEventListener]('mousemove', self.move); 382 | self.parent[addEventListener]('touchmove', self.move); 383 | 384 | // Disable selection. Disable! 385 | a[addEventListener]('selectstart', NOOP); 386 | a[addEventListener]('dragstart', NOOP); 387 | b[addEventListener]('selectstart', NOOP); 388 | b[addEventListener]('dragstart', NOOP); 389 | 390 | a.style.userSelect = 'none'; 391 | a.style.webkitUserSelect = 'none'; 392 | a.style.MozUserSelect = 'none'; 393 | a.style.pointerEvents = 'none'; 394 | 395 | b.style.userSelect = 'none'; 396 | b.style.webkitUserSelect = 'none'; 397 | b.style.MozUserSelect = 'none'; 398 | b.style.pointerEvents = 'none'; 399 | 400 | // Set the cursor, both on the gutter and the parent element. 401 | // Doing only a, b and gutter causes flickering. 402 | self.gutter.style.cursor = cursor; 403 | self.parent.style.cursor = cursor; 404 | 405 | // Cache the initial sizes of the pair. 406 | calculateSizes.call(self); 407 | } 408 | 409 | // 5. Create pair and element objects. Each pair has an index reference to 410 | // elements `a` and `b` of the pair (first and second elements). 411 | // Loop through the elements while pairing them off. Every pair gets a 412 | // `pair` object, a gutter, and isFirst/isLast properties. 413 | // 414 | // Basic logic: 415 | // 416 | // - Starting with the second element `i > 0`, create `pair` objects with 417 | // `a = i - 1` and `b = i` 418 | // - Set gutter sizes based on the _pair_ being first/last. The first and last 419 | // pair have gutterSize / 2, since they only have one half gutter, and not two. 420 | // - Create gutter elements and add event listeners. 421 | // - Set the size of the elements, minus the gutter sizes. 422 | // 423 | // ----------------------------------------------------------------------- 424 | // | i=0 | i=1 | i=2 | i=3 | 425 | // | | isFirst | | isLast | 426 | // | pair 0 pair 1 pair 2 | 427 | // | | | | | 428 | // ----------------------------------------------------------------------- 429 | var pairs = []; 430 | elements = ids.map(function(id, i) { 431 | // Create the element object. 432 | var element = { 433 | element: elementOrSelector(id), 434 | size: sizes[i], 435 | minSize: minSizes[i] 436 | }; 437 | 438 | var pair; 439 | 440 | if (i > 0) { 441 | // Create the pair object with it's metadata. 442 | pair = { 443 | a: i - 1, 444 | b: i, 445 | dragging: false, 446 | isFirst: i === 1, 447 | isLast: i === ids.length - 1, 448 | direction: direction, 449 | parent: parent 450 | }; 451 | 452 | // For first and last pairs, first and last gutter width is half. 453 | pair.aGutterSize = gutterSize; 454 | pair.bGutterSize = gutterSize; 455 | 456 | if (pair.isFirst) { 457 | pair.aGutterSize = gutterSize / 2; 458 | } 459 | 460 | if (pair.isLast) { 461 | pair.bGutterSize = gutterSize / 2; 462 | } 463 | 464 | // if the parent has a reverse flex-direction, switch the pair elements. 465 | if ( 466 | parentFlexDirection === 'row-reverse' || 467 | parentFlexDirection === 'column-reverse' 468 | ) { 469 | var temp = pair.a; 470 | pair.a = pair.b; 471 | pair.b = temp; 472 | } 473 | } 474 | 475 | // Determine the size of the current element. IE8 is supported by 476 | // staticly assigning sizes without draggable gutters. Assigns a string 477 | // to `size`. 478 | // 479 | // IE9 and above 480 | if (!isIE8) { 481 | // Create gutter elements for each pair. 482 | if (i > 0) { 483 | var gutterElement = gutter(i, direction); 484 | setGutterSize(gutterElement, gutterSize); 485 | 486 | gutterElement[addEventListener]( 487 | 'mousedown', 488 | startDragging.bind(pair) 489 | ); 490 | gutterElement[addEventListener]( 491 | 'touchstart', 492 | startDragging.bind(pair) 493 | ); 494 | 495 | parent.insertBefore(gutterElement, element.element); 496 | 497 | pair.gutter = gutterElement; 498 | } 499 | } 500 | 501 | // Set the element size to our determined size. 502 | // Half-size gutters for first and last elements. 503 | if (i === 0 || i === ids.length - 1) { 504 | setElementSize(element.element, element.size, gutterSize / 2); 505 | } else { 506 | setElementSize(element.element, element.size, gutterSize); 507 | } 508 | 509 | var computedSize = element.element[getBoundingClientRect]()[dimension]; 510 | 511 | if (computedSize < element.minSize) { 512 | element.minSize = computedSize; 513 | } 514 | 515 | // After the first iteration, and we have a pair object, append it to the 516 | // list of pairs. 517 | if (i > 0) { 518 | pairs.push(pair); 519 | } 520 | 521 | return element; 522 | }); 523 | 524 | function setSizes(newSizes) { 525 | newSizes.forEach(function(newSize, i) { 526 | if (i > 0) { 527 | var pair = pairs[i - 1]; 528 | var a = elements[pair.a]; 529 | var b = elements[pair.b]; 530 | 531 | a.size = newSizes[i - 1]; 532 | b.size = newSize; 533 | 534 | setElementSize(a.element, a.size, pair.aGutterSize); 535 | setElementSize(b.element, b.size, pair.bGutterSize); 536 | } 537 | }); 538 | } 539 | 540 | function destroy() { 541 | pairs.forEach(function(pair) { 542 | pair.parent.removeChild(pair.gutter); 543 | elements[pair.a].element.style[dimension] = ''; 544 | elements[pair.b].element.style[dimension] = ''; 545 | }); 546 | } 547 | 548 | if (isIE8) { 549 | return { 550 | setSizes: setSizes, 551 | destroy: destroy 552 | }; 553 | } 554 | 555 | return { 556 | setSizes: setSizes, 557 | getSizes: function getSizes() { 558 | return elements.map(function(element) { 559 | return element.size; 560 | }); 561 | }, 562 | collapse: function collapse(i) { 563 | if (i === pairs.length) { 564 | var pair = pairs[i - 1]; 565 | 566 | calculateSizes.call(pair); 567 | 568 | if (!isIE8) { 569 | adjust.call(pair, pair.size - pair.bGutterSize); 570 | } 571 | } else { 572 | var pair$1 = pairs[i]; 573 | 574 | calculateSizes.call(pair$1); 575 | 576 | if (!isIE8) { 577 | adjust.call(pair$1, pair$1.aGutterSize); 578 | } 579 | } 580 | }, 581 | destroy: destroy 582 | }; 583 | }; 584 | 585 | return Split; 586 | }); 587 | -------------------------------------------------------------------------------- /lib/VirtualFS.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | /** @module VirtualFS */ 3 | 4 | import type { INode } from './INodes.js'; 5 | import type { optionsStream } from './Streams.js'; 6 | 7 | // $FlowFixMe: Buffer exists 8 | import { Buffer } from 'buffer'; 9 | // $FlowFixMe: nextTick exists 10 | import { nextTick } from 'process'; 11 | import pathNode from 'path'; 12 | import permaProxy from 'permaproxy'; 13 | import constants from './constants.js'; 14 | import Stat from './Stat.js'; 15 | import CurrentDirectory from './CurrentDirectory.js'; 16 | import { 17 | DEFAULT_ROOT_UID, 18 | DEFAULT_ROOT_GID, 19 | DEFAULT_ROOT_PERM, 20 | DEFAULT_FILE_PERM, 21 | DEFAULT_DIRECTORY_PERM, 22 | DEFAULT_SYMLINK_PERM, 23 | applyUmask, 24 | checkPermissions 25 | } from './permissions.js'; 26 | import { 27 | MINOR_BITSIZE, 28 | MAJOR_MAX, 29 | MINOR_MAX, 30 | MAJOR_MIN, 31 | MINOR_MIN, 32 | DeviceManager, 33 | mkDev 34 | } from './Devices.js'; 35 | import { 36 | File, 37 | Directory, 38 | Symlink, 39 | CharacterDev, 40 | INodeManager 41 | } from './INodes.js'; 42 | import { 43 | FileDescriptor, 44 | FileDescriptorManager 45 | } from './FileDescriptors.js'; 46 | import { ReadStream, WriteStream } from './Streams.js'; 47 | import { VirtualFSError, errno } from './VirtualFSError.js'; 48 | 49 | type path = string | Buffer | {pathname: string}; 50 | type file = path | number; 51 | type data = Buffer | Uint8Array | string; 52 | type options = string | { 53 | encoding?: string|null, 54 | mode?: number, 55 | flag?: string 56 | }; 57 | type callback = (VirtualFSError|null) => void; 58 | type navigated = { 59 | dir: Directory, 60 | target: $Subtype|null, 61 | name: string, 62 | remaining: string, 63 | pathStack: Array 64 | }; 65 | 66 | /** 67 | * Prefer the posix join function if it exists. 68 | * Browser polyfills of the path module may not have the posix property. 69 | */ 70 | const pathJoin = (pathNode.posix) ? pathNode.posix.join : pathNode.join; 71 | 72 | /** 73 | * Asynchronous callback backup. 74 | */ 75 | const callbackUp: callback = (err) => { if (err) throw err; }; 76 | 77 | /** 78 | * Class representing a virtual filesystem. 79 | */ 80 | class VirtualFS { 81 | 82 | _uid: number; 83 | _gid: number; 84 | _umask: number; 85 | _devMgr: DeviceManager; 86 | _iNodeMgr: INodeManager; 87 | _fdMgr: FileDescriptorManager; 88 | _root: Directory; 89 | _cwd: CurrentDirectory; 90 | constants: Object; 91 | ReadStream: Class; 92 | WriteStream: Class; 93 | 94 | /** 95 | * Creates VirtualFS. 96 | */ 97 | constructor ( 98 | umask: number = 0o022, 99 | rootIndex: number|null = null, 100 | devMgr: DeviceManager = new DeviceManager, 101 | iNodeMgr: INodeManager = new INodeManager(devMgr), 102 | fdMgr: FileDescriptorManager = new FileDescriptorManager(iNodeMgr) 103 | ): void { 104 | let rootNode; 105 | if (typeof rootIndex === 'number') { 106 | rootNode = iNodeMgr.getINode(rootIndex); 107 | if (!(rootNode instanceof Directory)) { 108 | throw TypeError('rootIndex must point to a root directory'); 109 | } 110 | } else { 111 | [rootNode] = iNodeMgr.createINode( 112 | Directory, { mode: DEFAULT_ROOT_PERM, uid: DEFAULT_ROOT_UID, gid: DEFAULT_ROOT_GID } 113 | ); 114 | } 115 | this._uid = DEFAULT_ROOT_UID; 116 | this._gid = DEFAULT_ROOT_GID; 117 | this._umask = umask; 118 | this._devMgr = devMgr; 119 | this._iNodeMgr = iNodeMgr; 120 | this._fdMgr = fdMgr; 121 | this._root = rootNode; 122 | this._cwd = new CurrentDirectory(iNodeMgr, rootNode); 123 | this.constants = constants; 124 | this.ReadStream = ReadStream; 125 | this.WriteStream = WriteStream; 126 | } 127 | 128 | getUmask (): number { 129 | return this._umask; 130 | } 131 | 132 | setUmask (umask: number): void { 133 | this._umask = umask; 134 | } 135 | 136 | getUid (): number { 137 | return this._uid; 138 | } 139 | 140 | setUid (uid:number): void { 141 | this._uid = uid; 142 | } 143 | 144 | getGid (): number { 145 | return this._gid; 146 | } 147 | 148 | setGid (gid:number): void { 149 | this._gid = gid; 150 | } 151 | 152 | getCwd (): string { 153 | return this._cwd.getPath(); 154 | } 155 | 156 | chdir (path: string): void { 157 | path = this._getPath(path); 158 | const navigated = this._navigate(path, true); 159 | if (!navigated.target) { 160 | throw new VirtualFSError(errno.ENOENT, path); 161 | } 162 | if (!(navigated.target instanceof Directory)) { 163 | throw new VirtualFSError(errno.ENOTDIR, path); 164 | } 165 | if (!this._checkPermissions(constants.X_OK, navigated.target.getMetadata())) { 166 | throw new VirtualFSError(errno.EACCES, path); 167 | } 168 | this._cwd.changeDir(navigated.target, navigated.pathStack); 169 | } 170 | 171 | access (path: string, ...args: Array): void { 172 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 173 | const callback = args[cbIndex] || callbackUp; 174 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 175 | this._callAsync( 176 | this.accessSync.bind(this), 177 | [path, ...args.slice(0, cbIndex)], 178 | callback, 179 | callback 180 | ); 181 | return; 182 | } 183 | 184 | accessSync (path: string, mode: number = constants.F_OK): void { 185 | path = this._getPath(path); 186 | const target = this._navigate(path, true).target; 187 | if (!target) { 188 | throw new VirtualFSError(errno.ENOENT, path); 189 | } 190 | if (mode === constants.F_OK) { 191 | return; 192 | } 193 | if (!this._checkPermissions(mode, target.getMetadata())) { 194 | throw new VirtualFSError(errno.EACCES, path); 195 | } 196 | } 197 | 198 | appendFile (file: file, data: data, ...args: Array): void { 199 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 200 | const callback = args[cbIndex] || callbackUp; 201 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 202 | this._callAsync( 203 | this.appendFileSync.bind(this), 204 | [file, data, ...args.slice(0, cbIndex)], 205 | callback, 206 | callback 207 | ); 208 | return; 209 | } 210 | 211 | appendFileSync ( 212 | file: file, 213 | data: data = 'undefined', 214 | options?: options 215 | ): void { 216 | options = this._getOptions({ 217 | encoding: 'utf8', 218 | mode: DEFAULT_FILE_PERM, 219 | flag: 'a' 220 | }, options); 221 | data = this._getBuffer(data, options.encoding); 222 | let fdIndex; 223 | try { 224 | let fd; 225 | if (typeof file === 'number') { 226 | fd = this._fdMgr.getFd(file); 227 | if (!fd) throw new VirtualFSError(errno.EBADF, null, null, 'appendFile'); 228 | if (!(fd.getFlags() & (constants.O_WRONLY | constants.O_RDWR))) { 229 | throw new VirtualFSError(errno.EBADF, null, null, 'appendFile'); 230 | } 231 | } else { 232 | [fd, fdIndex] = this._openSync(file, options.flag, options.mode); 233 | } 234 | try { 235 | fd.write(data, null, constants.O_APPEND); 236 | } catch (e) { 237 | if (e instanceof RangeError) { 238 | throw new VirtualFSError(errno.EFBIG, null, null, 'appendFile'); 239 | } 240 | throw e; 241 | } 242 | } finally { 243 | if (fdIndex !== undefined) this.closeSync(fdIndex); 244 | } 245 | return; 246 | } 247 | 248 | chmod (path: path, mode: number, callback: callback = callbackUp): void { 249 | this._callAsync( 250 | this.chmodSync.bind(this), 251 | [path, mode], 252 | callback, 253 | callback 254 | ); 255 | return; 256 | } 257 | 258 | chmodSync (path: path, mode: number): void { 259 | path = this._getPath(path); 260 | const target = this._navigate(path, true).target; 261 | if (!target) { 262 | throw new VirtualFSError(errno.ENOENT, path); 263 | } 264 | if (typeof mode !== 'number') { 265 | throw new TypeError('mode must be an integer'); 266 | } 267 | const targetMetadata = target.getMetadata(); 268 | if (this._uid !== DEFAULT_ROOT_UID && this._uid !== targetMetadata.uid) { 269 | throw new VirtualFSError(errno.EPERM, null, null, 'chmod'); 270 | } 271 | targetMetadata.mode = (targetMetadata.mode & constants.S_IFMT) | mode; 272 | return; 273 | } 274 | 275 | chown (path: path, uid: number, gid: number, callback: callback = callbackUp): void { 276 | this._callAsync( 277 | this.chownSync.bind(this), 278 | [path, uid, gid], 279 | callback, 280 | callback 281 | ); 282 | return; 283 | } 284 | 285 | chownSync (path: path, uid: number, gid: number): void { 286 | path = this._getPath(path); 287 | const target = this._navigate(path, true).target; 288 | if (!target) { 289 | throw new VirtualFSError(errno.ENOENT, path); 290 | } 291 | const targetMetadata = target.getMetadata(); 292 | if (this._uid !== DEFAULT_ROOT_UID) { 293 | // you don't own the file 294 | if (targetMetadata.uid !== this._uid) { 295 | throw new VirtualFSError(errno.EPERM, null, null, 'chown'); 296 | } 297 | // you cannot give files to others 298 | if (this._uid !== uid) { 299 | throw new VirtualFSError(errno.EPERM, null, null, 'chown'); 300 | } 301 | // because we don't have user group hierarchies, we allow chowning to any group 302 | } 303 | if (typeof uid === 'number') { 304 | targetMetadata.uid = uid; 305 | } 306 | if (typeof gid === 'number') { 307 | targetMetadata.gid = gid; 308 | } 309 | return; 310 | } 311 | 312 | chownr (path: path, uid: number, gid: number, callback: callback = callbackUp): void { 313 | this._callAsync( 314 | this.chownrSync.bind(this), 315 | [path, uid, gid], 316 | callback, 317 | callback 318 | ); 319 | return; 320 | } 321 | 322 | chownrSync (path: path, uid: number, gid: number): void { 323 | path = this._getPath(path) 324 | this.chownSync(path, uid, gid); 325 | let children; 326 | try { 327 | children = this.readdirSync(path); 328 | } catch (e) { 329 | if (e && e.code === 'ENOTDIR') return; 330 | throw e; 331 | } 332 | children.forEach((child) => { 333 | // $FlowFixMe: path is string 334 | const pathChild = pathJoin(path, child); 335 | // don't traverse symlinks 336 | if (!this.lstatSync(pathChild).isSymbolicLink()) { 337 | this.chownrSync(pathChild, uid, gid); 338 | } 339 | }); 340 | return; 341 | } 342 | 343 | close (fdIndex: number, callback: callback = callbackUp): void { 344 | this._callAsync( 345 | this.closeSync.bind(this), 346 | [fdIndex], 347 | callback, 348 | callback 349 | ); 350 | return; 351 | } 352 | 353 | closeSync (fdIndex: number): void { 354 | if (!this._fdMgr.getFd(fdIndex)) { 355 | throw new VirtualFSError(errno.EBADF, null, null, 'close'); 356 | } 357 | this._fdMgr.deleteFd(fdIndex); 358 | return; 359 | } 360 | 361 | copyFile (srcPath: path, dstPath: path, ...args: Array): void { 362 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 363 | const callback = args[cbIndex] || callbackUp; 364 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 365 | this._callAsync( 366 | this.copyFileSync.bind(this), 367 | [srcPath, dstPath, ...args.slice(0, cbIndex)], 368 | callback, 369 | callback 370 | ); 371 | return; 372 | } 373 | 374 | copyFileSync (srcPath: path, dstPath: path, flags: number = 0): void { 375 | srcPath = this._getPath(srcPath); 376 | dstPath = this._getPath(dstPath); 377 | let srcFd; 378 | let srcFdIndex; 379 | let dstFd; 380 | let dstFdIndex; 381 | try { 382 | // the only things that are copied is the data and the mode 383 | [srcFd, srcFdIndex] = this._openSync(srcPath, constants.O_RDONLY); 384 | const srcINode = srcFd.getINode(); 385 | if (srcINode instanceof Directory) { 386 | throw new VirtualFSError(errno.EBADF, srcPath, dstPath); 387 | } 388 | let dstFlags = constants.WRONLY | constants.O_CREAT; 389 | if (flags & constants.COPYFILE_EXCL) { 390 | dstFlags |= constants.O_EXCL; 391 | } 392 | [dstFd, dstFdIndex] = this._openSync(dstPath, dstFlags, srcINode.getMetadata().mode); 393 | const dstINode = dstFd.getINode(); 394 | if (dstINode instanceof File) { 395 | dstINode.setData(Buffer.from(srcINode.getData())); 396 | } else { 397 | throw new VirtualFSError(errno.EINVAL, srcPath, dstPath); 398 | } 399 | } finally { 400 | if (srcFdIndex !== undefined) this.closeSync(srcFdIndex); 401 | if (dstFdIndex !== undefined) this.closeSync(dstFdIndex); 402 | } 403 | return; 404 | } 405 | 406 | createReadStream (path: path, options?: optionsStream): ReadStream { 407 | path = this._getPath(path); 408 | options = this._getOptions( 409 | { 410 | flags: 'r', 411 | encoding: null, 412 | fd: null, 413 | mode: DEFAULT_FILE_PERM, 414 | autoClose: true, 415 | end: Infinity 416 | }, 417 | options 418 | ); 419 | if (options.start !== undefined) { 420 | if (options.start > options.end) { 421 | throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); 422 | } 423 | } 424 | return new ReadStream(path, options, this); 425 | } 426 | 427 | createWriteStream (path: path, options?: optionsStream): WriteStream { 428 | path = this._getPath(path); 429 | options = this._getOptions( 430 | { 431 | flags: 'w', 432 | defaultEncoding: 'utf8', 433 | fd: null, 434 | mode: DEFAULT_FILE_PERM, 435 | autoClose: true 436 | }, 437 | options 438 | ); 439 | if (options.start !== undefined) { 440 | if (options.start < 0) { 441 | throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); 442 | } 443 | } 444 | return new WriteStream(path, options, this); 445 | } 446 | 447 | exists (path: path, callback?: (boolean) => void): void { 448 | if (!callback) { 449 | callback = () => {}; 450 | } 451 | this._callAsync( 452 | this.existsSync.bind(this), 453 | [path], 454 | callback, 455 | callback 456 | ); 457 | return; 458 | } 459 | 460 | existsSync (path: path): boolean { 461 | path = this._getPath(path); 462 | try { 463 | return !!(this._navigate(path, true).target); 464 | } catch (e) { 465 | return false; 466 | } 467 | } 468 | 469 | fallocate (fdIndex: number, offset: number, len: number, callback: callback = callbackUp): void { 470 | this._callAsync( 471 | this.fallocateSync.bind(this), 472 | [fdIndex, offset, len], 473 | callback, 474 | callback 475 | ); 476 | return; 477 | } 478 | 479 | fallocateSync (fdIndex: number, offset: number, len: number): void { 480 | if (offset < 0 || len <= 0) { 481 | throw new VirtualFSError(errno.EINVAL, null, null, 'fallocate'); 482 | } 483 | const fd = this._fdMgr.getFd(fdIndex); 484 | if (!fd) { 485 | throw new VirtualFSError(errno.EBADF, null, null, 'fallocate'); 486 | } 487 | const iNode = fd.getINode(); 488 | if (!(iNode instanceof File)) { 489 | throw new VirtualFSError(errno.ENODEV, null, null, 'fallocate'); 490 | } 491 | if (!(fd.getFlags() & (constants.O_WRONLY | constants.O_RDWR))) { 492 | throw new VirtualFSError(errno.EBADF, null, null, 'fallocate'); 493 | } 494 | const data = iNode.getData(); 495 | const metadata = iNode.getMetadata(); 496 | if ((offset + len) > data.length) { 497 | let newData; 498 | try { 499 | newData = Buffer.concat([ 500 | data, 501 | Buffer.alloc((offset + len) - data.length) 502 | ]); 503 | } catch (e) { 504 | if (e instanceof RangeError) { 505 | throw new VirtualFSError(errno.EFBIG, null, null, 'fallocate'); 506 | } 507 | throw e; 508 | } 509 | iNode.setData(newData); 510 | metadata.size = newData.length; 511 | } 512 | metadata.ctime = new Date; 513 | return; 514 | } 515 | 516 | mmap (length: number, flags: number, fdIndex: number, ...args: Array): void { 517 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 518 | const callback = args[cbIndex] || callbackUp; 519 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 520 | this._callAsync( 521 | this.mmapSync.bind(this), 522 | [length, flags, fdIndex, ...args.slice(0, cbIndex)], 523 | (buffer) => callback(null, buffer), 524 | callback 525 | ); 526 | return; 527 | } 528 | 529 | mmapSync (length: number, flags: number, fdIndex: number, offset: number = 0): Buffer { 530 | if (length < 1 || offset < 0) { 531 | throw new VirtualFSError(errno.EINVAL, null, null, 'mmap'); 532 | } 533 | const fd = this._fdMgr.getFd(fdIndex); 534 | if (!fd) { 535 | throw new VirtualFSError(errno.EBADF, null, null, 'mmap'); 536 | } 537 | const access = fd.getFlags() & constants.O_ACCMODE; 538 | if (access === constants.O_WRONLY) { 539 | throw new VirtualFSError(errno.EACCES, null, null, 'mmap'); 540 | } 541 | const iNode = fd.getINode(); 542 | if (!(iNode instanceof File)) { 543 | throw new VirtualFSError(errno.ENODEV, null, null, 'mmap'); 544 | } 545 | switch (flags) { 546 | case constants.MAP_PRIVATE: 547 | return Buffer.from(iNode.getData().slice(offset, offset + length)); 548 | case constants.MAP_SHARED: 549 | if (access !== constants.O_RDWR) { 550 | throw new VirtualFSError(errno.EACCES, null, null, 'mmap'); 551 | } 552 | return permaProxy(iNode, '_data').slice(offset, offset + length); 553 | default: 554 | throw new VirtualFSError(errno.EINVAL, null, null, 'mmap'); 555 | } 556 | } 557 | 558 | fchmod (fdIndex: number, mode: number, callback: callback = callbackUp): void { 559 | this._callAsync( 560 | this.fchmodSync.bind(this), 561 | [fdIndex, mode], 562 | callback, 563 | callback 564 | ); 565 | return; 566 | } 567 | 568 | fchmodSync (fdIndex: number, mode: number): void { 569 | const fd = this._fdMgr.getFd(fdIndex); 570 | if (!fd) { 571 | throw new VirtualFSError(errno.EBADF, null, null, 'fchmod'); 572 | } 573 | if (typeof mode !== 'number') { 574 | throw new TypeError('mode must be an integer'); 575 | } 576 | const fdMetadata = fd.getINode().getMetadata(); 577 | if (this._uid !== DEFAULT_ROOT_UID && this._uid !== fdMetadata.uid) { 578 | throw new VirtualFSError(errno.EPERM, null, null, 'fchmod'); 579 | } 580 | fdMetadata.mode = (fdMetadata.mode & constants.S_IMFT) | mode; 581 | return; 582 | } 583 | 584 | fchown (fdIndex: number, uid: number, gid: number, callback: callback = callbackUp): void { 585 | this._callAsync( 586 | this.fchmodSync.bind(this), 587 | [fdIndex, uid, gid], 588 | callback, 589 | callback 590 | ); 591 | return; 592 | } 593 | 594 | fchownSync (fdIndex: number, uid: number, gid: number): void { 595 | const fd = this._fdMgr.getFd(fdIndex); 596 | if (!fd) { 597 | throw new VirtualFSError(errno.EBADF, null, null, 'fchown'); 598 | } 599 | const fdMetadata = fd.getINode().getMetadata(); 600 | if (this._uid !== DEFAULT_ROOT_UID) { 601 | // you don't own the file 602 | if (fdMetadata.uid !== this._uid) { 603 | throw new VirtualFSError(errno.EPERM, null, null, 'fchown'); 604 | } 605 | // you cannot give files to others 606 | if (this._uid !== uid) { 607 | throw new VirtualFSError(errno.EPERM, null, null, 'fchown'); 608 | } 609 | // because we don't have user group hierarchies, we allow chowning to any group 610 | } 611 | if (typeof uid === 'number') { 612 | fdMetadata.uid = uid; 613 | } 614 | if (typeof gid === 'number') { 615 | fdMetadata.gid = gid; 616 | } 617 | return; 618 | } 619 | 620 | fdatasync (fdIndex: number, callback: callback = callbackUp): void { 621 | this._callAsync( 622 | this.fchmodSync.bind(this), 623 | [fdIndex], 624 | callback, 625 | callback 626 | ); 627 | return; 628 | } 629 | 630 | fdatasyncSync (fdIndex: number): void { 631 | if (!this._fdMgr.getFd(fdIndex)) { 632 | throw new VirtualFSError(errno.EBADF, null, null, 'fdatasync'); 633 | } 634 | return; 635 | } 636 | 637 | fstat (fdIndex: number, callback: callback = callbackUp): void { 638 | this._callAsync( 639 | this.fstatSync.bind(this), 640 | [fdIndex], 641 | (stat) => callback(null, stat), 642 | callback 643 | ); 644 | return; 645 | } 646 | 647 | fstatSync (fdIndex: number): Stat { 648 | const fd = this._fdMgr.getFd(fdIndex); 649 | if (!fd) { 650 | throw new VirtualFSError(errno.EBADF, null, null, 'fstat'); 651 | } 652 | return new Stat({...fd.getINode().getMetadata()}); 653 | } 654 | 655 | fsync (fdIndex: number, callback: callback = callbackUp): void { 656 | this._callAsync( 657 | this.fsyncSync.bind(this), 658 | [fdIndex], 659 | callback, 660 | callback 661 | ); 662 | return; 663 | } 664 | 665 | fsyncSync (fdIndex: number): void { 666 | if (!this._fdMgr.getFd(fdIndex)) { 667 | throw new VirtualFSError(errno.EBADF, null, null, 'fsync'); 668 | } 669 | return; 670 | } 671 | 672 | ftruncate (fdIndex: number, ...args: Array): void { 673 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 674 | const callback = args[cbIndex] || callbackUp; 675 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 676 | this._callAsync( 677 | this.ftruncateSync.bind(this), 678 | [fdIndex, ...args.slice(0, cbIndex)], 679 | callback, 680 | callback 681 | ); 682 | return; 683 | } 684 | 685 | ftruncateSync (fdIndex: number, len: number = 0): void { 686 | if (len < 0) { 687 | throw new VirtualFSError(errno.EINVAL, null, null, 'ftruncate'); 688 | } 689 | const fd = this._fdMgr.getFd(fdIndex); 690 | if (!fd) { 691 | throw new VirtualFSError(errno.EBADF, null, null, 'ftruncate'); 692 | } 693 | const iNode = fd.getINode(); 694 | if (!(iNode instanceof File)) { 695 | throw new VirtualFSError(errno.EINVAL, null, null, 'ftruncate'); 696 | } 697 | if (!(fd.getFlags() & (constants.O_WRONLY | constants.O_RDWR))) { 698 | throw new VirtualFSError(errno.EINVAL, null, null, 'ftruncate'); 699 | } 700 | const data = iNode.getData(); 701 | const metadata = iNode.getMetadata(); 702 | let newData; 703 | try { 704 | if (len > data.length) { 705 | newData = Buffer.alloc(len); 706 | data.copy(newData, 0, 0, data.length); 707 | iNode.setData(newData); 708 | } else if (len < data.length) { 709 | newData = Buffer.allocUnsafe(len); 710 | data.copy(newData, 0, 0, len); 711 | iNode.setData(newData); 712 | } else { 713 | newData = data; 714 | } 715 | } catch (e) { 716 | if (e instanceof RangeError) { 717 | throw new VirtualFSError(errno.EFBIG, null, null, 'ftruncate'); 718 | } 719 | throw e; 720 | } 721 | const now = new Date; 722 | metadata.mtime = now; 723 | metadata.ctime = now; 724 | metadata.size = newData.length; 725 | fd.setPos(Math.min(newData.length, fd.getPos())); 726 | return; 727 | } 728 | 729 | futimes (fdIndex: number, atime: number|Date, mtime: number|Date, callback: callback = callbackUp): void { 730 | this._callAsync( 731 | this.futimesSync.bind(this), 732 | [fdIndex, atime, mtime], 733 | callback, 734 | callback 735 | ); 736 | return; 737 | } 738 | 739 | futimesSync (fdIndex: number, atime: number|string|Date, mtime: number|string|Date): void { 740 | const fd = this._fdMgr.getFd(fdIndex); 741 | if (!fd) { 742 | throw new VirtualFSError(errno.EBADF, null, null, 'futimes'); 743 | } 744 | const metadata = fd.getINode().getMetadata(); 745 | let newAtime; 746 | let newMtime; 747 | if (typeof atime === 'number') { 748 | newAtime = new Date(atime * 1000); 749 | } else if (typeof atime === 'string') { 750 | newAtime = new Date(parseInt(atime) * 1000); 751 | } else if (atime instanceof Date) { 752 | newAtime = atime; 753 | } else { 754 | throw TypeError('atime and mtime must be dates or unixtime in seconds'); 755 | } 756 | if (typeof mtime === 'number') { 757 | newMtime = new Date(mtime * 1000); 758 | } else if (typeof mtime === 'string') { 759 | newMtime = new Date(parseInt(mtime) * 1000); 760 | } else if (mtime instanceof Date) { 761 | newMtime = mtime; 762 | } else { 763 | throw TypeError('atime and mtime must be dates or unixtime in seconds'); 764 | } 765 | metadata.atime = newAtime; 766 | metadata.mtime = newMtime; 767 | metadata.ctime = new Date; 768 | return; 769 | } 770 | 771 | lchmod (path: path, mode: number, callback: callback = callbackUp): void { 772 | this._callAsync( 773 | this.lchmodSync.bind(this), 774 | [path, mode], 775 | callback, 776 | callback 777 | ); 778 | return; 779 | } 780 | 781 | lchmodSync (path: path, mode: number): void { 782 | path = this._getPath(path); 783 | const target = this._navigate(path, false).target; 784 | if (!target) { 785 | throw new VirtualFSError(errno.ENOENT, path); 786 | } 787 | if (typeof mode !== 'number') { 788 | throw new TypeError('mode must be an integer'); 789 | } 790 | const targetMetadata = target.getMetadata(); 791 | if (this._uid !== DEFAULT_ROOT_UID && this._uid !== targetMetadata.uid) { 792 | throw new VirtualFSError(errno.EPERM, null, null, 'lchmod'); 793 | } 794 | targetMetadata.mode = (targetMetadata.mode & constants.S_IFMT) | mode; 795 | return; 796 | } 797 | 798 | lchown (path: path, uid: number, gid: number, callback: callback = callbackUp): void { 799 | this._callAsync( 800 | this.lchownSync.bind(this), 801 | [path, uid, gid], 802 | callback, 803 | callback 804 | ); 805 | return; 806 | } 807 | 808 | lchownSync (path: path, uid: number, gid: number): void { 809 | path = this._getPath(path); 810 | const target = this._navigate(path, false).target; 811 | if (!target) { 812 | throw new VirtualFSError(errno.ENOENT, path); 813 | } 814 | const targetMetadata = target.getMetadata(); 815 | if (this._uid !== DEFAULT_ROOT_UID) { 816 | // you don't own the file 817 | if (targetMetadata.uid !== this._uid) { 818 | throw new VirtualFSError(errno.EPERM, null, null, 'lchown'); 819 | } 820 | // you cannot give files to others 821 | if (this._uid !== uid) { 822 | throw new VirtualFSError(errno.EPERM, null, null, 'lchown'); 823 | } 824 | // because we don't have user group hierarchies, we allow chowning to any group 825 | } 826 | if (typeof uid === 'number') { 827 | targetMetadata.uid = uid; 828 | } 829 | if (typeof gid === 'number') { 830 | targetMetadata.gid = gid; 831 | } 832 | return; 833 | } 834 | 835 | link (existingPath: path, newPath: path, callback: callback = callbackUp): void { 836 | this._callAsync( 837 | this.linkSync.bind(this), 838 | [existingPath, newPath], 839 | callback, 840 | callback 841 | ); 842 | return; 843 | } 844 | 845 | linkSync (existingPath: path, newPath: path): void { 846 | existingPath = this._getPath(existingPath); 847 | newPath = this._getPath(newPath); 848 | let navigatedExisting; 849 | let navigatedNew; 850 | navigatedExisting = this._navigate(existingPath, false); 851 | navigatedNew = this._navigate(newPath, false); 852 | if (!navigatedExisting.target) { 853 | throw new VirtualFSError(errno.ENOENT, existingPath, newPath, 'link'); 854 | } 855 | if (navigatedExisting.target instanceof Directory) { 856 | throw new VirtualFSError(errno.EPERM, existingPath, newPath, 'link'); 857 | } 858 | if (!navigatedNew.target) { 859 | if (navigatedNew.dir.getMetadata().nlink < 2) { 860 | throw new VirtualFSError(errno.ENOENT, existingPath, newPath, 'link'); 861 | } 862 | if (!this._checkPermissions(constants.W_OK, navigatedNew.dir.getMetadata())) { 863 | throw new VirtualFSError(errno.EACCES, existingPath, newPath, 'link'); 864 | } 865 | const index = navigatedExisting.dir.getEntryIndex(navigatedExisting.name); 866 | navigatedNew.dir.addEntry(navigatedNew.name, index); 867 | navigatedExisting.target.getMetadata().ctime = new Date; 868 | } else { 869 | throw new VirtualFSError(errno.EEXIST, existingPath, newPath, 'link'); 870 | } 871 | return; 872 | } 873 | 874 | lseek (fdIndex: number, position: number, ...args: Array): void { 875 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 876 | const callback = args[cbIndex] || callbackUp; 877 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 878 | this._callAsync( 879 | this.lseekSync.bind(this), 880 | [fdIndex, position, ...args.slice(0, cbIndex)], 881 | callback, 882 | callback 883 | ); 884 | return; 885 | } 886 | 887 | lseekSync (fdIndex: number, position: number, seekFlags: number = constants.SEEK_SET): void { 888 | const fd = this._fdMgr.getFd(fdIndex); 889 | if (!fd) { 890 | throw new VirtualFSError(errno.EBADF, null, null, 'lseek'); 891 | } 892 | if ( 893 | [ 894 | constants.SEEK_SET, 895 | constants.SEEK_CUR, 896 | constants.SEEK_END 897 | ].indexOf(seekFlags) === -1 898 | ) { 899 | throw new VirtualFSError(errno.EINVAL, null, null, 'lseek'); 900 | } 901 | try { 902 | fd.setPos(position, seekFlags); 903 | } catch (e) { 904 | if (e instanceof VirtualFSError) { 905 | e.setSyscall('lseek'); 906 | } 907 | throw e; 908 | } 909 | return; 910 | } 911 | 912 | lstat (path: path, callback: callback = callbackUp): void { 913 | this._callAsync( 914 | this.lstatSync.bind(this), 915 | [path], 916 | (stat) => callback(null, stat), 917 | callback 918 | ); 919 | return; 920 | } 921 | 922 | lstatSync (path: path): Stat { 923 | path = this._getPath(path); 924 | const target = this._navigate(path, false).target; 925 | if (target) { 926 | return new Stat({...target.getMetadata()}); 927 | } else { 928 | throw new VirtualFSError(errno.ENOENT, path); 929 | } 930 | } 931 | 932 | mkdir (path: path, ...args: Array): void { 933 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 934 | const callback = args[cbIndex] || callbackUp; 935 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 936 | this._callAsync( 937 | this.mkdirSync.bind(this), 938 | [path, ...args.slice(0, cbIndex)], 939 | callback, 940 | callback 941 | ); 942 | return; 943 | } 944 | 945 | mkdirSync (path: path, mode: number = DEFAULT_DIRECTORY_PERM): void { 946 | path = this._getPath(path); 947 | // we expect a non-existent directory 948 | path = path.replace(/(.+?)\/+$/, '$1'); 949 | let navigated = this._navigate(path, true); 950 | if (navigated.target) { 951 | throw new VirtualFSError(errno.EEXIST, path, null, 'mkdir'); 952 | } else if (!navigated.target && navigated.remaining) { 953 | throw new VirtualFSError(errno.ENOENT, path, null, 'mkdir'); 954 | } else if (!navigated.target) { 955 | if (navigated.dir.getMetadata().nlink < 2) { 956 | throw new VirtualFSError(errno.ENOENT, path, null, 'mkdir'); 957 | } 958 | if (!this._checkPermissions( 959 | constants.W_OK, 960 | navigated.dir.getMetadata() 961 | )) { 962 | throw new VirtualFSError(errno.EACCES, path, null, 'mkdir'); 963 | } 964 | const [, index] = this._iNodeMgr.createINode( 965 | Directory, 966 | { 967 | mode: applyUmask(mode, this._umask), 968 | uid: this._uid, 969 | gid: this._gid, 970 | parent: navigated.dir.getEntryIndex('.') 971 | } 972 | ); 973 | navigated.dir.addEntry(navigated.name, index); 974 | } 975 | return; 976 | } 977 | 978 | mkdirp (path: path, ...args: Array): void { 979 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 980 | const callback = args[cbIndex] || callbackUp; 981 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 982 | this._callAsync( 983 | this.mkdirpSync.bind(this), 984 | [path, ...args.slice(0, cbIndex)], 985 | callback, 986 | callback 987 | ); 988 | return; 989 | } 990 | 991 | mkdirpSync (path: path, mode: number = DEFAULT_DIRECTORY_PERM): void { 992 | path = this._getPath(path); 993 | // we expect a directory 994 | path = path.replace(/(.+?)\/+$/, '$1'); 995 | let iNode; 996 | let index; 997 | let currentDir; 998 | let navigated = this._navigate(path, true); 999 | while (true) { 1000 | if (!navigated.target) { 1001 | if (navigated.dir.getMetadata().nlink < 2) { 1002 | throw new VirtualFSError(errno.ENOENT, path); 1003 | } 1004 | if (!this._checkPermissions( 1005 | constants.W_OK, 1006 | navigated.dir.getMetadata() 1007 | )) { 1008 | throw new VirtualFSError(errno.EACCES, path); 1009 | } 1010 | [iNode, index] = this._iNodeMgr.createINode( 1011 | Directory, 1012 | { 1013 | mode: applyUmask(mode, this._umask), 1014 | uid: this._uid, 1015 | gid: this._gid, 1016 | parent: navigated.dir.getEntryIndex('.') 1017 | } 1018 | ); 1019 | navigated.dir.addEntry(navigated.name, index); 1020 | if (navigated.remaining) { 1021 | currentDir = iNode; 1022 | navigated = this._navigateFrom(currentDir, navigated.remaining, true); 1023 | } else { 1024 | break; 1025 | } 1026 | } else if (!(navigated.target instanceof Directory)) { 1027 | throw new VirtualFSError(errno.ENOTDIR, path); 1028 | } else { 1029 | break; 1030 | } 1031 | } 1032 | return; 1033 | } 1034 | 1035 | mkdtemp (pathSPrefix: string, ...args: Array): void { 1036 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1037 | const callback = args[cbIndex] || callbackUp; 1038 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1039 | this._callAsync( 1040 | this.mkdtempSync.bind(this), 1041 | [pathSPrefix, ...args.slice(0, cbIndex)], 1042 | (pathS) => callback(null, pathS), 1043 | callback 1044 | ); 1045 | return; 1046 | } 1047 | 1048 | mkdtempSync (pathSPrefix: string, options?: options): string|Buffer { 1049 | options = this._getOptions({encoding: 'utf8'}, options); 1050 | if (!pathSPrefix || typeof pathSPrefix !== 'string') { 1051 | throw new TypeError('filename prefix is required'); 1052 | } 1053 | const getChar = () => { 1054 | const possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 1055 | return possibleChars[Math.floor(Math.random() * possibleChars.length)]; 1056 | }; 1057 | let pathS; 1058 | while (true) { 1059 | pathS = pathSPrefix.concat( 1060 | Array.from({length: 6}, () => getChar).map((f) => f()).join('') 1061 | ); 1062 | try { 1063 | this.mkdirSync(pathS); 1064 | if (options.encoding === 'buffer') { 1065 | return Buffer.from(pathS); 1066 | } else { 1067 | return Buffer.from(pathS).toString(options.encoding); 1068 | } 1069 | } catch (e) { 1070 | if (e.code !== errno.EEXIST) { 1071 | throw e; 1072 | } 1073 | } 1074 | } 1075 | } 1076 | 1077 | mknod ( 1078 | path: path, 1079 | type: number, 1080 | major: number, 1081 | minor: number, 1082 | ...args: Array 1083 | ): void { 1084 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1085 | const callback = args[cbIndex] || callbackUp; 1086 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1087 | this._callAsync( 1088 | this.mknodSync.bind(this), 1089 | [path, type, major, minor, ...args.slice(0, cbIndex)], 1090 | callback, 1091 | callback 1092 | ); 1093 | return; 1094 | } 1095 | 1096 | mknodSync ( 1097 | path: path, 1098 | type: number, 1099 | major: number, 1100 | minor: number, 1101 | mode: number = DEFAULT_FILE_PERM 1102 | ): void { 1103 | path = this._getPath(path); 1104 | const navigated = this._navigate(path, false); 1105 | if (navigated.target) { 1106 | throw new VirtualFSError(errno.EEXIST, path, null, 'mknod'); 1107 | } 1108 | if (navigated.dir.getMetadata().nlink < 2) { 1109 | throw new VirtualFSError(errno.ENOENT, path, null, 'mknod'); 1110 | } 1111 | if (!this._checkPermissions(constants.W_OK, navigated.dir.getMetadata())) { 1112 | throw new VirtualFSError(errno.EACCES, path, null, 'mknod'); 1113 | } 1114 | let index; 1115 | switch (type) { 1116 | case constants.S_IFREG: 1117 | [, index] = this._iNodeMgr.createINode( 1118 | File, 1119 | { 1120 | mode: applyUmask(mode, this._umask), 1121 | uid: this._uid, 1122 | gid: this._gid 1123 | } 1124 | ); 1125 | break; 1126 | case constants.S_IFCHR: 1127 | if (typeof major !== 'number' || typeof minor !== 'number') { 1128 | throw TypeError('major and minor must set as numbers when creating device nodes'); 1129 | } 1130 | if (major > MAJOR_MAX || minor > MINOR_MAX || minor < MAJOR_MIN || minor < MINOR_MIN) { 1131 | throw new VirtualFSError(errno.EINVAL, path, null, 'mknod'); 1132 | } 1133 | [, index] = this._iNodeMgr.createINode( 1134 | CharacterDev, 1135 | { 1136 | mode: applyUmask(mode, this._umask), 1137 | uid: this._uid, 1138 | gid: this._gid, 1139 | rdev: mkDev(major, minor) 1140 | } 1141 | ); 1142 | break; 1143 | default: 1144 | throw new VirtualFSError(errno.EPERM, path, null, 'mknod'); 1145 | } 1146 | navigated.dir.addEntry(navigated.name, index); 1147 | return; 1148 | } 1149 | 1150 | open (path: path, flags: string|number, ...args: Array): void { 1151 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1152 | const callback = args[cbIndex] || callbackUp; 1153 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1154 | this._callAsync( 1155 | this.openSync.bind(this), 1156 | [path, flags, ...args.slice(0, cbIndex)], 1157 | (fdIndex) => callback(null, fdIndex), 1158 | callback 1159 | ); 1160 | return; 1161 | } 1162 | 1163 | openSync ( 1164 | path: path, 1165 | flags: string|number, 1166 | mode: number = DEFAULT_FILE_PERM 1167 | ): number { 1168 | return this._openSync(path, flags, mode)[1]; 1169 | } 1170 | 1171 | _openSync ( 1172 | path: path, 1173 | flags: string|number, 1174 | mode: number = DEFAULT_FILE_PERM 1175 | ): [FileDescriptor<*>, number] { 1176 | path = this._getPath(path); 1177 | if (typeof flags === 'string') { 1178 | switch(flags) { 1179 | case 'r': 1180 | case 'rs': 1181 | flags = constants.O_RDONLY; 1182 | break; 1183 | case 'r+': 1184 | case 'rs+': 1185 | flags = constants.O_RDWR; 1186 | break; 1187 | case 'w': 1188 | flags = (constants.O_WRONLY | 1189 | constants.O_CREAT | 1190 | constants.O_TRUNC); 1191 | break; 1192 | case 'wx': 1193 | flags = (constants.O_WRONLY | 1194 | constants.O_CREAT | 1195 | constants.O_TRUNC | 1196 | constants.O_EXCL); 1197 | break; 1198 | case 'w+': 1199 | flags = (constants.O_RDWR | 1200 | constants.O_CREAT | 1201 | constants.O_TRUNC); 1202 | break; 1203 | case 'wx+': 1204 | flags = (constants.O_RDWR | 1205 | constants.O_CREAT | 1206 | constants.O_TRUNC | 1207 | constants.O_EXCL); 1208 | break; 1209 | case 'a': 1210 | flags = (constants.O_WRONLY | 1211 | constants.O_APPEND | 1212 | constants.O_CREAT); 1213 | break; 1214 | case 'ax': 1215 | flags = (constants.O_WRONLY | 1216 | constants.O_APPEND | 1217 | constants.O_CREAT | 1218 | constants.O_EXCL); 1219 | break; 1220 | case 'a+': 1221 | flags = (constants.O_RDWR | 1222 | constants.O_APPEND | 1223 | constants.O_CREAT); 1224 | break; 1225 | case 'ax+': 1226 | flags = (constants.O_RDWR | 1227 | constants.O_APPEND | 1228 | constants.O_CREAT | 1229 | constants.O_EXCL); 1230 | break; 1231 | default: 1232 | throw new TypeError('Unknown file open flag: ' + flags); 1233 | } 1234 | } 1235 | if (typeof flags !== 'number') { 1236 | throw new TypeError('Unknown file open flag: ' + flags); 1237 | } 1238 | let navigated = this._navigate(path, false); 1239 | if (navigated.target instanceof Symlink) { 1240 | // cannot be symlink if O_NOFOLLOW 1241 | if (flags & constants.O_NOFOLLOW) { 1242 | throw new VirtualFSError(errno.ELOOP, path, null, 'open'); 1243 | } 1244 | navigated = this._navigateFrom( 1245 | navigated.dir, 1246 | navigated.name + navigated.remaining, 1247 | true, 1248 | undefined, 1249 | undefined, 1250 | path 1251 | ); 1252 | } 1253 | let target = navigated.target; 1254 | // cannot be missing unless O_CREAT 1255 | if (!target) { 1256 | // O_CREAT only applies if there's a left over name without any remaining path 1257 | if (!navigated.remaining && (flags & constants.O_CREAT)) { 1258 | // cannot create if the current directory has been unlinked from its parent directory 1259 | if (navigated.dir.getMetadata().nlink < 2) { 1260 | throw new VirtualFSError(errno.ENOENT, path, null, 'open'); 1261 | } 1262 | if (!this._checkPermissions( 1263 | constants.W_OK, 1264 | navigated.dir.getMetadata() 1265 | )) { 1266 | throw new VirtualFSError(errno.EACCES, path, null, 'open'); 1267 | } 1268 | let index; 1269 | [target, index] = this._iNodeMgr.createINode( 1270 | File, 1271 | { 1272 | mode: applyUmask(mode, this._umask), 1273 | uid: this._uid, 1274 | gid: this._gid 1275 | } 1276 | ); 1277 | navigated.dir.addEntry(navigated.name, index); 1278 | } else { 1279 | throw new VirtualFSError(errno.ENOENT, path, null, 'open'); 1280 | } 1281 | } else { 1282 | // target already exists cannot be created exclusively 1283 | if ((flags & constants.O_CREAT) && (flags & constants.O_EXCL)) { 1284 | throw new VirtualFSError(errno.EEXIST, path, null, 'open'); 1285 | } 1286 | // cannot be directory if write capabilities are requested 1287 | if ((target instanceof Directory) && 1288 | (flags & (constants.O_WRONLY | flags & constants.O_RDWR))) 1289 | { 1290 | throw new VirtualFSError(errno.EISDIR, path, null, 'open'); 1291 | } 1292 | // must be directory if O_DIRECTORY 1293 | if ((flags & constants.O_DIRECTORY) && !(target instanceof Directory)) { 1294 | throw new VirtualFSError(errno.ENOTDIR, path, null, 'open'); 1295 | } 1296 | // must truncate a file if O_TRUNC 1297 | if ((flags & constants.O_TRUNC) && 1298 | (target instanceof File) && 1299 | (flags & (constants.O_WRONLY | constants.O_RDWR))) 1300 | { 1301 | target.setData(Buffer.alloc(0)); 1302 | } 1303 | // convert file descriptor access flags into bitwise permission flags 1304 | let access; 1305 | if (flags & constants.O_RDWR) { 1306 | access = constants.R_OK | constants.W_OK; 1307 | } else if (flags & constants.O_WRONLY) { 1308 | access = constants.W_OK; 1309 | } else { 1310 | access = constants.R_OK; 1311 | } 1312 | if (!this._checkPermissions(access, target.getMetadata())) { 1313 | throw new VirtualFSError(errno.EACCES, path, null, 'open'); 1314 | } 1315 | } 1316 | try { 1317 | let fd = this._fdMgr.createFd(target, flags); 1318 | return fd; 1319 | } catch (e) { 1320 | if (e instanceof VirtualFSError) { 1321 | e.setPaths(path); 1322 | e.setSyscall('open'); 1323 | } 1324 | throw e; 1325 | } 1326 | } 1327 | 1328 | read (fdIndex: number, buffer: data, ...args: Array): void { 1329 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1330 | const callback = args[cbIndex] || callbackUp; 1331 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1332 | this._callAsync( 1333 | this.readSync.bind(this), 1334 | [fdIndex, buffer, ...args.slice(0, cbIndex)], 1335 | (bytesRead) => callback(null, bytesRead, buffer), 1336 | callback 1337 | ); 1338 | return; 1339 | } 1340 | 1341 | readSync ( 1342 | fdIndex: number, 1343 | buffer: data, 1344 | offset: number = 0, 1345 | length: number = 0, 1346 | position: number|null = null 1347 | ): number { 1348 | const fd = this._fdMgr.getFd(fdIndex); 1349 | if (!fd) { 1350 | throw new VirtualFSError(errno.EBADF, null, null, 'read'); 1351 | } 1352 | if (typeof position === 'number' && position < 0) { 1353 | throw new VirtualFSError(errno.EINVAL, null, null, 'read'); 1354 | } 1355 | if (fd.getINode().getMetadata().isDirectory()) { 1356 | throw new VirtualFSError(errno.EISDIR, null, null, 'read'); 1357 | } 1358 | const flags = fd.getFlags(); 1359 | if (flags & constants.O_WRONLY) { 1360 | throw new VirtualFSError(errno.EBADF, null, null, 'read'); 1361 | } 1362 | if (offset < 0 || offset > buffer.length) { 1363 | throw new RangeError('Offset is out of bounds'); 1364 | } 1365 | if (length < 0 || length > buffer.length) { 1366 | throw new RangeError('Length extends beyond buffer'); 1367 | } 1368 | buffer = this._getBuffer(buffer).slice(offset, offset + length); 1369 | let bytesRead; 1370 | try { 1371 | bytesRead = fd.read(buffer, position); 1372 | } catch (e) { 1373 | if (e instanceof VirtualFSError) { 1374 | e.syscall = 'read'; 1375 | } 1376 | throw e; 1377 | } 1378 | return bytesRead; 1379 | } 1380 | 1381 | readdir (path: path, ...args: Array): void { 1382 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1383 | const callback = args[cbIndex] || callbackUp; 1384 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1385 | this._callAsync( 1386 | this.readdirSync.bind(this), 1387 | [path, ...args.slice(0, cbIndex)], 1388 | (files) => callback(null, files), 1389 | callback 1390 | ); 1391 | return; 1392 | } 1393 | 1394 | readdirSync (path: path, options?: options): Array { 1395 | path = this._getPath(path); 1396 | options = this._getOptions({encoding: 'utf8'}, options); 1397 | let navigated = this._navigate(path, true); 1398 | if (!navigated.target) { 1399 | throw new VirtualFSError(errno.ENOENT, path, null, 'readdir'); 1400 | } 1401 | if (!(navigated.target instanceof Directory)) { 1402 | throw new VirtualFSError(errno.ENOTDIR, path, null, 'readdir'); 1403 | } 1404 | if (!this._checkPermissions(constants.R_OK, navigated.target.getMetadata())) { 1405 | throw new VirtualFSError(errno.EACCES, path, null, 'readdir'); 1406 | } 1407 | return [...navigated.target.getEntries()] 1408 | .filter(([name, _]) => name !== '.' && name !== '..') 1409 | .map(([name, _]) => { 1410 | // $FlowFixMe: options exists 1411 | if (options.encoding === 'buffer') { 1412 | return Buffer.from(name); 1413 | } else { 1414 | // $FlowFixMe: options exists and is not a string 1415 | return Buffer.from(name).toString(options.encoding); 1416 | } 1417 | }); 1418 | } 1419 | 1420 | readFile (file: file, ...args: Array): void { 1421 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1422 | const callback = args[cbIndex] || callbackUp; 1423 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1424 | this._callAsync( 1425 | this.readFileSync.bind(this), 1426 | [file, ...args.slice(0, cbIndex)], 1427 | (data) => callback(null, data), 1428 | callback 1429 | ); 1430 | return; 1431 | } 1432 | 1433 | readFileSync (file: file, options?: options): string|Buffer { 1434 | options = this._getOptions({encoding: null, flag: 'r'}, options); 1435 | let fdIndex; 1436 | try { 1437 | const buffer = Buffer.allocUnsafe(4096); 1438 | let totalBuffer = Buffer.alloc(0); 1439 | let bytesRead = null; 1440 | if (typeof file === 'number') { 1441 | while (bytesRead !== 0) { 1442 | bytesRead = this.readSync(file, buffer, 0, buffer.length); 1443 | totalBuffer = Buffer.concat([totalBuffer, buffer.slice(0, bytesRead)]); 1444 | } 1445 | } else { 1446 | fdIndex = this.openSync(file, options.flag); 1447 | while (bytesRead !== 0) { 1448 | bytesRead = this.readSync(fdIndex, buffer, 0, buffer.length); 1449 | totalBuffer = Buffer.concat([totalBuffer, buffer.slice(0, bytesRead)]); 1450 | } 1451 | } 1452 | return (options.encoding) ? totalBuffer.toString(options.encoding) : totalBuffer; 1453 | } finally { 1454 | if (fdIndex !== undefined) this.closeSync(fdIndex); 1455 | } 1456 | } 1457 | 1458 | readlink (path: path, ...args: Array): void { 1459 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1460 | const callback = args[cbIndex] || callbackUp; 1461 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1462 | this._callAsync( 1463 | this.readlinkSync.bind(this), 1464 | [path, ...args.slice(0, cbIndex)], 1465 | (linkString) => callback(null, linkString), 1466 | callback 1467 | ); 1468 | return; 1469 | } 1470 | 1471 | readlinkSync (path: path, options?: options): string|Buffer { 1472 | path = this._getPath(path); 1473 | options = this._getOptions({encoding: 'utf8'}, options); 1474 | let target = this._navigate(path, false).target; 1475 | if (!target) { 1476 | throw new VirtualFSError(errno.ENOENT, path); 1477 | } 1478 | if (!(target instanceof Symlink)) { 1479 | throw new VirtualFSError(errno.EINVAL, path); 1480 | } 1481 | const link = target.getLink(); 1482 | if (options.encoding === 'buffer') { 1483 | return Buffer.from(link); 1484 | } else { 1485 | return Buffer.from(link).toString(options.encoding); 1486 | } 1487 | } 1488 | 1489 | realpath (path: path, ...args: Array): void { 1490 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1491 | const callback = args[cbIndex] || callbackUp; 1492 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1493 | this._callAsync( 1494 | this.realpathSync.bind(this), 1495 | [path, ...args.slice(0, cbIndex)], 1496 | (path) => callback(null, path), 1497 | callback 1498 | ); 1499 | return; 1500 | } 1501 | 1502 | realpathSync (path: path, options?: options): string|Buffer { 1503 | path = this._getPath(path); 1504 | options = this._getOptions({encoding: 'utf8'}, options); 1505 | const navigated = this._navigate(path, true); 1506 | if (!navigated.target) { 1507 | throw new VirtualFSError(errno.ENOENT, path); 1508 | } 1509 | if (options.encoding === 'buffer') { 1510 | return Buffer.from('/' + navigated.pathStack.join('/')); 1511 | } else { 1512 | return Buffer.from('/' + navigated.pathStack.join('/')).toString(options.encoding); 1513 | } 1514 | } 1515 | 1516 | rename (oldPath: path, newPath: path, callback: callback = callbackUp): void { 1517 | this._callAsync( 1518 | this.renameSync.bind(this), 1519 | [oldPath, newPath], 1520 | callback, 1521 | callback 1522 | ); 1523 | return; 1524 | } 1525 | 1526 | renameSync (oldPath: path, newPath: path): void { 1527 | oldPath = this._getPath(oldPath); 1528 | newPath = this._getPath(newPath); 1529 | const navigatedSource = this._navigate(oldPath, false); 1530 | const navigatedTarget = this._navigate(newPath, false); 1531 | if (!navigatedSource.target) { 1532 | throw new VirtualFSError(errno.ENOENT, oldPath, newPath, 'rename'); 1533 | } 1534 | if (navigatedSource.target instanceof Directory) { 1535 | // if oldPath is a directory, target must be a directory (if it exists) 1536 | if (navigatedTarget.target && 1537 | !(navigatedTarget.target instanceof Directory)) 1538 | { 1539 | throw new VirtualFSError(errno.ENOTDIR, oldPath, newPath, 'rename'); 1540 | } 1541 | // neither oldPath nor newPath can point to root 1542 | if (navigatedSource.target === this._root || 1543 | navigatedTarget.target === this._root) 1544 | { 1545 | throw new VirtualFSError(errno.EBUSY, oldPath, newPath, 'rename'); 1546 | } 1547 | // if the target directory contains elements this cannot be done 1548 | // this can be done without read permissions 1549 | if (navigatedTarget.target && ([...navigatedTarget.target.getEntries()].length - 2)) { 1550 | throw new VirtualFSError(errno.ENOTEMPTY, oldPath, newPath, 'rename'); 1551 | } 1552 | // if any of the paths used .. or ., then `dir` is not the parent directory 1553 | if (navigatedSource.name === '.' || 1554 | navigatedSource.name === '..' || 1555 | navigatedTarget.name === '.' || 1556 | navigatedTarget.name === '..' ) 1557 | { 1558 | throw new VirtualFSError(errno.EBUSY, oldPath, newPath, 'rename'); 1559 | } 1560 | // cannot rename a source prefix of target 1561 | if (navigatedSource.pathStack.length < navigatedTarget.pathStack.length) { 1562 | let prefixOf = true; 1563 | for (let i = 0; i < navigatedSource.pathStack.length; ++i) { 1564 | if (navigatedSource.pathStack[i] !== navigatedTarget.pathStack[i]) { 1565 | prefixOf = false; 1566 | break; 1567 | } 1568 | } 1569 | if (prefixOf) { 1570 | throw new VirtualFSError(errno.EINVAL, oldPath, newPath, 'rename'); 1571 | } 1572 | } 1573 | } else { 1574 | // if oldPath is not a directory, then newPath cannot be an existing directory 1575 | if (navigatedTarget.target && navigatedTarget.target instanceof Directory) { 1576 | throw new VirtualFSError(errno.EISDIR, oldPath, newPath, 'rename'); 1577 | } 1578 | } 1579 | // both the navigatedSource.dir and navigatedTarget.dir must support write permissions 1580 | if (!this._checkPermissions(constants.W_OK, navigatedSource.dir.getMetadata()) || 1581 | !this._checkPermissions(constants.W_OK, navigatedTarget.dir.getMetadata())) 1582 | { 1583 | throw new VirtualFSError(errno.EACCES, oldPath, newPath, 'rename'); 1584 | } 1585 | // if they are in the same directory, it is simple rename 1586 | if (navigatedSource.dir === navigatedTarget.dir) { 1587 | navigatedSource.dir.renameEntry(navigatedSource.name, navigatedTarget.name); 1588 | return; 1589 | } 1590 | const index = navigatedSource.dir.getEntryIndex(navigatedSource.name); 1591 | if (navigatedTarget.target) { 1592 | navigatedTarget.target.getMetadata().ctime = new Date; 1593 | navigatedTarget.dir.deleteEntry(navigatedTarget.name); 1594 | navigatedTarget.dir.addEntry(navigatedTarget.name, index); 1595 | } else { 1596 | if (navigatedTarget.dir.getMetadata().nlink < 2) { 1597 | throw new VirtualFSError(errno.ENOENT, oldPath, newPath, 'rename'); 1598 | } 1599 | navigatedTarget.dir.addEntry(navigatedTarget.name, index); 1600 | } 1601 | navigatedSource.target.getMetadata().ctime = new Date; 1602 | navigatedSource.dir.deleteEntry(navigatedSource.name); 1603 | return; 1604 | } 1605 | 1606 | rmdir (path: path, callback: callback = callbackUp): void { 1607 | this._callAsync( 1608 | this.rmdirSync.bind(this), 1609 | [path], 1610 | callback, 1611 | callback 1612 | ); 1613 | return; 1614 | } 1615 | 1616 | rmdirSync (path: path): void { 1617 | path = this._getPath(path); 1618 | // if the path has trailing slashes, navigation would traverse into it 1619 | // we must trim off these trailing slashes to allow these directories to be removed 1620 | path = path.replace(/(.+?)\/+$/, '$1'); 1621 | let navigated = this._navigate(path, false); 1622 | // this is for if the path resolved to root 1623 | if (!navigated.name) { 1624 | throw new VirtualFSError(errno.EBUSY, path, null, 'rmdir'); 1625 | } 1626 | // on linux, when .. is used, the parent directory becomes unknown 1627 | // in that case, they return with ENOTEMPTY 1628 | // but the directory may in fact be empty 1629 | // for this edge case, we instead use EINVAL 1630 | if (navigated.name === '.' || navigated.name === '..') { 1631 | throw new VirtualFSError(errno.EINVAL, path, null, 'rmdir'); 1632 | } 1633 | if (!navigated.target) { 1634 | throw new VirtualFSError(errno.ENOENT, path, null, 'rmdir'); 1635 | } 1636 | if (!(navigated.target instanceof Directory)) { 1637 | throw new VirtualFSError(errno.ENOTDIR, path, null, 'rmdir'); 1638 | } 1639 | if ([...navigated.target.getEntries()].length - 2) { 1640 | throw new VirtualFSError(errno.ENOTEMPTY, path, null, 'rmdir'); 1641 | } 1642 | if (!this._checkPermissions(constants.W_OK, navigated.dir.getMetadata())) { 1643 | throw new VirtualFSError(errno.EACCES, path, null, 'rmdir'); 1644 | } 1645 | navigated.dir.deleteEntry(navigated.name); 1646 | return; 1647 | } 1648 | 1649 | stat (path: path, callback: callback = callbackUp): void { 1650 | this._callAsync( 1651 | this.statSync.bind(this), 1652 | [path], 1653 | (stat) => callback(null, stat), 1654 | callback 1655 | ); 1656 | return; 1657 | } 1658 | 1659 | statSync (path: path): Stat { 1660 | path = this._getPath(path); 1661 | const target = this._navigate(path, true).target; 1662 | if (target) { 1663 | return new Stat({...target.getMetadata()}); 1664 | } else { 1665 | throw new VirtualFSError(errno.ENOENT, path); 1666 | } 1667 | } 1668 | 1669 | symlink (dstPath: path, srcPath: path, ...args: Array): void { 1670 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1671 | const callback = args[cbIndex] || callbackUp; 1672 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1673 | this._callAsync( 1674 | this.symlinkSync.bind(this), 1675 | [dstPath, srcPath, ...args.slice(0, cbIndex)], 1676 | callback, 1677 | callback 1678 | ); 1679 | return; 1680 | } 1681 | 1682 | symlinkSync (dstPath: path, srcPath: path, type: string = 'file'): void { 1683 | dstPath = this._getPath(dstPath); 1684 | srcPath = this._getPath(srcPath); 1685 | if (!dstPath) { 1686 | throw new VirtualFSError(errno.ENOENT, srcPath, dstPath, 'symlink'); 1687 | } 1688 | let navigated = this._navigate(srcPath, false); 1689 | if (!navigated.target) { 1690 | if (navigated.dir.getMetadata().nlink < 2) { 1691 | throw new VirtualFSError(errno.ENOENT, srcPath, dstPath, 'symlink'); 1692 | } 1693 | if (!this._checkPermissions(constants.W_OK, navigated.dir.getMetadata())) { 1694 | throw new VirtualFSError(errno.EACCES, srcPath, dstPath, 'symlink'); 1695 | } 1696 | const [, index] = this._iNodeMgr.createINode( 1697 | Symlink, 1698 | { 1699 | mode: DEFAULT_SYMLINK_PERM, 1700 | uid: this._uid, 1701 | gid: this._gid, 1702 | link: dstPath 1703 | } 1704 | ); 1705 | navigated.dir.addEntry(navigated.name, index); 1706 | return; 1707 | } else { 1708 | throw new VirtualFSError(errno.EEXIST, srcPath, dstPath, 'symlink'); 1709 | } 1710 | } 1711 | 1712 | truncate (file: file, ...args: Array): void { 1713 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1714 | const callback = args[cbIndex] || callbackUp; 1715 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1716 | this._callAsync( 1717 | this.truncateSync.bind(this), 1718 | [file, ...args.slice(0, cbIndex)], 1719 | callback, 1720 | callback 1721 | ); 1722 | return; 1723 | } 1724 | 1725 | truncateSync (file: file, len: number = 0): void { 1726 | if (len < 0) { 1727 | throw new VirtualFSError(errno.EINVAL, null, null, 'ftruncate'); 1728 | } 1729 | if (typeof file === 'number') { 1730 | this.ftruncateSync(file, len); 1731 | } else { 1732 | file = this._getPath(file); 1733 | let fdIndex; 1734 | try { 1735 | fdIndex = this.openSync(file, constants.O_WRONLY); 1736 | this.ftruncateSync(fdIndex, len); 1737 | } finally { 1738 | if (fdIndex !== undefined) this.closeSync(fdIndex); 1739 | } 1740 | } 1741 | return; 1742 | } 1743 | 1744 | unlink (path: path, callback: callback = callbackUp): void { 1745 | this._callAsync( 1746 | this.unlinkSync.bind(this), 1747 | [path], 1748 | callback, 1749 | callback 1750 | ); 1751 | return; 1752 | } 1753 | 1754 | unlinkSync (path: path): void { 1755 | path = this._getPath(path); 1756 | let navigated = this._navigate(path, false); 1757 | if (!navigated.target) { 1758 | throw new VirtualFSError(errno.ENOENT, path); 1759 | } 1760 | if (!this._checkPermissions(constants.W_OK, navigated.dir.getMetadata())) { 1761 | throw new VirtualFSError(errno.EACCES, path); 1762 | } 1763 | if (navigated.target instanceof Directory) { 1764 | throw new VirtualFSError(errno.EISDIR, path); 1765 | } 1766 | navigated.target.getMetadata().ctime = new Date; 1767 | navigated.dir.deleteEntry(navigated.name); 1768 | return; 1769 | } 1770 | 1771 | utimes ( 1772 | path: path, 1773 | atime: string|number|Date, 1774 | mtime: string|number|Date, 1775 | callback: callback = callbackUp 1776 | ): void { 1777 | this._callAsync( 1778 | this.utimesSync.bind(this), 1779 | [path, atime, mtime], 1780 | callback, 1781 | callback 1782 | ); 1783 | return; 1784 | } 1785 | 1786 | utimesSync ( 1787 | path: path, 1788 | atime: number|string|Date, 1789 | mtime: number|string|Date 1790 | ): void { 1791 | path = this._getPath(path); 1792 | const target = this._navigate(path, true).target; 1793 | if (!target) { 1794 | throw new VirtualFSError(errno.ENOENT, path, null, 'utimes'); 1795 | } 1796 | const metadata = target.getMetadata(); 1797 | let newAtime; 1798 | let newMtime; 1799 | if (typeof atime === 'number') { 1800 | newAtime = new Date(atime * 1000); 1801 | } else if (typeof atime === 'string') { 1802 | newAtime = new Date(parseInt(atime) * 1000); 1803 | } else if (atime instanceof Date) { 1804 | newAtime = atime; 1805 | } else { 1806 | throw TypeError('atime and mtime must be dates or unixtime in seconds'); 1807 | } 1808 | if (typeof mtime === 'number') { 1809 | newMtime = new Date(mtime * 1000); 1810 | } else if (typeof mtime === 'string') { 1811 | newMtime = new Date(parseInt(mtime) * 1000); 1812 | } else if (mtime instanceof Date) { 1813 | newMtime = mtime; 1814 | } else { 1815 | throw TypeError('atime and mtime must be dates or unixtime in seconds'); 1816 | } 1817 | metadata.atime = newAtime; 1818 | metadata.mtime = newMtime; 1819 | metadata.ctime = new Date; 1820 | return; 1821 | } 1822 | 1823 | write (fdIndex: number, data: data, ...args: Array): void { 1824 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1825 | const callback = args[cbIndex] || callbackUp; 1826 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1827 | this._callAsync( 1828 | this.writeSync.bind(this), 1829 | [fdIndex, data, ...args.slice(0, cbIndex)], 1830 | (bytesWritten) => callback(null, bytesWritten, data), 1831 | callback 1832 | ); 1833 | return; 1834 | } 1835 | 1836 | writeSync ( 1837 | fdIndex: number, 1838 | data: data, 1839 | offsetOrPos: ?number, 1840 | lengthOrEncoding?: number|string, 1841 | position: number|null = null 1842 | ): number { 1843 | const fd = this._fdMgr.getFd(fdIndex); 1844 | if (!fd) { 1845 | throw new VirtualFSError(errno.EBADF, null, null, 'write'); 1846 | } 1847 | if (typeof position === 'number' && position < 0) { 1848 | throw new VirtualFSError(errno.EINVAL, null, null, 'write'); 1849 | } 1850 | const flags = fd.getFlags(); 1851 | if (!(flags & (constants.O_WRONLY | constants.O_RDWR))) { 1852 | throw new VirtualFSError(errno.EBADF, null, null, 'write'); 1853 | } 1854 | let buffer; 1855 | if (typeof data === 'string') { 1856 | position = (typeof offsetOrPos === 'number') ? offsetOrPos : null; 1857 | lengthOrEncoding = (typeof lengthOrEncoding === 'string') ? lengthOrEncoding : 'utf8'; 1858 | buffer = this._getBuffer(data, lengthOrEncoding); 1859 | } else { 1860 | offsetOrPos = (typeof offsetOrPos === 'number') ? offsetOrPos : 0; 1861 | if (offsetOrPos < 0 || offsetOrPos > data.length) { 1862 | throw new RangeError('Offset is out of bounds'); 1863 | } 1864 | lengthOrEncoding = (typeof lengthOrEncoding === 'number') ? lengthOrEncoding : data.length; 1865 | if (lengthOrEncoding < 0 || lengthOrEncoding > data.length) { 1866 | throw new RangeError('Length is out of bounds'); 1867 | } 1868 | buffer = this._getBuffer(data).slice(offsetOrPos, offsetOrPos + lengthOrEncoding); 1869 | } 1870 | try { 1871 | return fd.write(buffer, position); 1872 | } catch (e) { 1873 | if (e instanceof RangeError) { 1874 | throw new VirtualFSError(errno.EFBIG, null, null, 'write'); 1875 | } 1876 | if (e instanceof VirtualFSError) { 1877 | e.setSyscall('write'); 1878 | } 1879 | throw e; 1880 | } 1881 | } 1882 | 1883 | writeFile (file: file, data: data, ...args: Array): void { 1884 | let cbIndex = args.findIndex((arg) => typeof arg === 'function'); 1885 | const callback = args[cbIndex] || callbackUp; 1886 | cbIndex = (cbIndex >= 0) ? cbIndex : args.length; 1887 | this._callAsync( 1888 | this.writeFileSync.bind(this), 1889 | [file, data, ...args.slice(0, cbIndex)], 1890 | callback, 1891 | callback 1892 | ); 1893 | return; 1894 | } 1895 | 1896 | writeFileSync (file: file, data: data = 'undefined', options?: options) { 1897 | options = this._getOptions({ 1898 | encoding: 'utf8', 1899 | mode: DEFAULT_FILE_PERM, 1900 | flag: 'w' 1901 | }, options); 1902 | let fdIndex; 1903 | try { 1904 | const buffer = this._getBuffer(data, options.encoding); 1905 | if (typeof file === 'number') { 1906 | this.writeSync(file, buffer, 0, buffer.length, 0); 1907 | } else { 1908 | fdIndex = this.openSync(file, options.flag, options.mode); 1909 | this.writeSync(fdIndex, buffer, 0, buffer.length, 0); 1910 | } 1911 | } finally { 1912 | if (fdIndex !== undefined) this.closeSync(fdIndex); 1913 | } 1914 | return; 1915 | } 1916 | 1917 | /** 1918 | * Sets up an asynchronous call in accordance with Node behaviour. 1919 | * This function should be implemented with microtask semantics. 1920 | * Because the internal readable-stream package uses process.nextTick. 1921 | * This must also use process.nextTick as well to be on the same queue. 1922 | * It is required to polyfill the process.nextTick for browsers. 1923 | * @private 1924 | */ 1925 | _callAsync ( 1926 | syncFn: Function, 1927 | args: Array, 1928 | successCall: Function, 1929 | failCall: Function 1930 | ) { 1931 | nextTick(() => { 1932 | try { 1933 | let result = syncFn(...args); 1934 | result = result === undefined ? null : result; 1935 | successCall(result); 1936 | } catch (e) { 1937 | failCall(e); 1938 | } 1939 | }); 1940 | return; 1941 | } 1942 | 1943 | /** 1944 | * Processes path types and collapses it to a string. 1945 | * The path types can be string or Buffer or URL. 1946 | * @private 1947 | */ 1948 | _getPath (path: path): string { 1949 | if (typeof path === 'string') { 1950 | return path; 1951 | } 1952 | if (path instanceof Buffer) { 1953 | return path.toString(); 1954 | } 1955 | if (typeof path === 'object' && typeof path.pathname === 'string') { 1956 | return this._getPathFromURL(path); 1957 | } 1958 | throw new TypeError('path must be a string or Buffer or URL'); 1959 | } 1960 | 1961 | /** 1962 | * Acquires the file path from an URL object. 1963 | * @private 1964 | */ 1965 | _getPathFromURL (url: {pathname: string}): string { 1966 | if (url.hostname) { 1967 | throw new TypeError('ERR_INVALID_FILE_URL_HOST'); 1968 | } 1969 | const pathname = url.pathname; 1970 | if (pathname.match(/%2[fF]/)) { 1971 | // must not allow encoded slashes 1972 | throw new TypeError('ERR_INVALID_FILE_URL_PATH'); 1973 | } 1974 | return decodeURIComponent(pathname); 1975 | } 1976 | 1977 | /** 1978 | * Processes data types and collapses it to a Buffer. 1979 | * The data types can be Buffer or Uint8Array or string. 1980 | * @private 1981 | */ 1982 | _getBuffer (data: data, encoding: string|null = null): Buffer { 1983 | if (data instanceof Buffer) { 1984 | return data; 1985 | } 1986 | if (data instanceof Uint8Array) { 1987 | // zero copy implementation 1988 | // also sliced to the view's constraint 1989 | return Buffer.from(data.buffer).slice( 1990 | data.byteOffset, 1991 | data.byteOffset + data.byteLength 1992 | ); 1993 | } 1994 | if (typeof data === 'string') { 1995 | return Buffer.from(data, encoding); 1996 | } 1997 | throw new TypeError('data must be Buffer or Uint8Array or string'); 1998 | } 1999 | 2000 | /** 2001 | * Takes a default set of options, and merges them shallowly into the user provided options. 2002 | * Object spread syntax will ignore an undefined or null options object. 2003 | * @private 2004 | */ 2005 | _getOptions (defaultOptions: Object, options: ?(Object|string)): Object { 2006 | if (typeof options === 'string') { 2007 | return {...defaultOptions, encoding: options}; 2008 | } else { 2009 | return {...defaultOptions, ...options}; 2010 | } 2011 | } 2012 | 2013 | /** 2014 | * Checks the permissions fixng the current uid and gid. 2015 | * If the user is root, they can access anything. 2016 | * @private 2017 | */ 2018 | _checkPermissions (access: number, stat: Stat): boolean { 2019 | if (this._uid !== DEFAULT_ROOT_UID) { 2020 | return checkPermissions(access, this._uid, this._gid, stat); 2021 | } else { 2022 | return true; 2023 | } 2024 | } 2025 | 2026 | /** 2027 | * Parses and extracts the first path segment. 2028 | * @private 2029 | */ 2030 | _parsePath (pathS: string): {segment: string, rest: string} { 2031 | const matches: ?Array = pathS.match(/^([\s\S]*?)(?:\/+|$)([\s\S]*)/); 2032 | if (matches) { 2033 | let segment = matches[1] || ''; 2034 | let rest = matches[2] || ''; 2035 | return { 2036 | segment: segment, 2037 | rest: rest 2038 | }; 2039 | } else { 2040 | // this should not happen 2041 | throw new Error('Could not parse pathS: ' + pathS); 2042 | } 2043 | } 2044 | 2045 | /** 2046 | * Navigates the filesystem tree from root. 2047 | * You can interpret the results like: 2048 | * !target => Non-existent segment 2049 | * name === '' => Target is at root 2050 | * name === '.' => dir is the same as target 2051 | * name === '..' => dir is a child directory 2052 | * @private 2053 | */ 2054 | _navigate ( 2055 | pathS: string, 2056 | resolveLastLink: boolean = true, 2057 | activeSymlinks: Set = (new Set), 2058 | origPathS: string = pathS 2059 | ): navigated { 2060 | if (!pathS) { 2061 | throw new VirtualFSError(errno.ENOENT, origPathS); 2062 | } 2063 | // multiple consecutive slashes are considered to be 1 slash 2064 | pathS = pathS.replace(/\/+/, '/'); 2065 | // a trailing slash is considered to refer to a directory, thus it is converted to /. 2066 | // functions that expect and specially handle missing directories should trim it away 2067 | pathS = pathS.replace(/\/$/, '/.'); 2068 | if (pathS[0] === '/') { 2069 | pathS = pathS.substring(1); 2070 | if (!pathS) { 2071 | return { 2072 | dir: this._root, 2073 | target: this._root, 2074 | name: '', // root is the only situation where the name is empty 2075 | remaining: '', 2076 | pathStack: [] 2077 | }; 2078 | } else { 2079 | return this._navigateFrom( 2080 | this._root, 2081 | pathS, 2082 | resolveLastLink, 2083 | activeSymlinks, 2084 | [], 2085 | origPathS 2086 | ); 2087 | } 2088 | } else { 2089 | return this._navigateFrom( 2090 | this._cwd.getINode(), 2091 | pathS, 2092 | resolveLastLink, 2093 | activeSymlinks, 2094 | this._cwd.getPathStack(), 2095 | origPathS 2096 | ); 2097 | } 2098 | } 2099 | 2100 | /** 2101 | * Navigates the filesystem tree from a given directory. 2102 | * You should not use this directly unless you first call _navigate and pass the remaining path to _navigateFrom. 2103 | * Note that the pathStack is always the full path to the target. 2104 | * @private 2105 | */ 2106 | _navigateFrom ( 2107 | curdir: Directory, 2108 | pathS: string, 2109 | resolveLastLink: boolean = true, 2110 | activeSymlinks: Set = (new Set), 2111 | pathStack: Array = [], 2112 | origPathS: string = pathS 2113 | ): navigated { 2114 | if (!pathS) { 2115 | throw new VirtualFSError(errno.ENOENT, origPathS); 2116 | } 2117 | if (!this._checkPermissions(constants.X_OK, curdir.getMetadata())) { 2118 | throw new VirtualFSError(errno.EACCES, origPathS); 2119 | } 2120 | let parse = this._parsePath(pathS); 2121 | if (parse.segment !== '.') { 2122 | if (parse.segment === '..') { 2123 | pathStack.pop(); // this is a noop if the pathStack is empty 2124 | } else { 2125 | pathStack.push(parse.segment); 2126 | } 2127 | } 2128 | let nextDir; 2129 | let nextPath; 2130 | let target = curdir.getEntry(parse.segment); 2131 | if (target instanceof File || target instanceof CharacterDev) { 2132 | if (!parse.rest) { 2133 | return { 2134 | dir: curdir, 2135 | target: target, 2136 | name: parse.segment, 2137 | remaining: '', 2138 | pathStack: pathStack 2139 | }; 2140 | } 2141 | throw new VirtualFSError(errno.ENOTDIR, origPathS); 2142 | } else if (target instanceof Directory) { 2143 | if (!parse.rest) { 2144 | // if parse.segment is ., dir is not the same directory as target 2145 | // if parse.segment is .., dir is the child directory 2146 | return { 2147 | dir: curdir, 2148 | target: target, 2149 | name: parse.segment, 2150 | remaining: '', 2151 | pathStack: pathStack 2152 | }; 2153 | } 2154 | nextDir = target; 2155 | nextPath = parse.rest; 2156 | } else if (target instanceof Symlink) { 2157 | if (!resolveLastLink && !parse.rest) { 2158 | return { 2159 | dir: curdir, 2160 | target: target, 2161 | name: parse.segment, 2162 | remaining: '', 2163 | pathStack: pathStack 2164 | }; 2165 | } 2166 | if (activeSymlinks.has(target)) { 2167 | throw new VirtualFSError(errno.ELOOP, origPathS); 2168 | } else { 2169 | activeSymlinks.add(target); 2170 | } 2171 | // although symlinks should not have an empty links, it's still handled correctly here 2172 | nextPath = pathJoin(target.getLink(), parse.rest); 2173 | if (nextPath[0] === '/') { 2174 | return this._navigate(nextPath, resolveLastLink, activeSymlinks, origPathS); 2175 | } else { 2176 | pathStack.pop(); 2177 | nextDir = curdir; 2178 | } 2179 | } else { 2180 | return { 2181 | dir: curdir, 2182 | target: null, 2183 | name: parse.segment, 2184 | remaining: parse.rest, 2185 | pathStack: pathStack 2186 | }; 2187 | } 2188 | return this._navigateFrom( 2189 | nextDir, 2190 | nextPath, 2191 | resolveLastLink, 2192 | activeSymlinks, 2193 | pathStack, 2194 | origPathS 2195 | ); 2196 | } 2197 | 2198 | } 2199 | 2200 | export default VirtualFS; 2201 | 2202 | export type { path, data, file, options, callback }; 2203 | --------------------------------------------------------------------------------