Skip to content

Commit 81f625d

Browse files
authored
Merge pull request #489 from XmiliaH/add-filesystem-api
Added custom file system API
2 parents ffa9398 + f2e935a commit 81f625d

File tree

7 files changed

+167
-29
lines changed

7 files changed

+167
-29
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
146146
* `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths.
147147
* `require.customRequire` - Use instead of the `require` function to load modules from the host.
148148
* `require.strict` - `false` to not force strict mode on modules loaded by require (default: `true`).
149+
* `require.fs` - Custom file system implementation.
149150
* `nesting` - **WARNING**: Allowing this is a security risk as scripts can create a NodeVM which can require any host module. `true` to enable VMs nesting (default: `false`).
150151
* `wrapper` - `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script.
151152
* `argv` - Array to be passed to `process.argv`.

index.d.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,51 @@
11
import {EventEmitter} from 'events';
2+
import fs from 'fs';
3+
import pa from 'path';
4+
5+
/**
6+
* Interface for nodes fs module
7+
*/
8+
export interface VMFS {
9+
/** Implements fs.statSync */
10+
statSync: typeof fs.statSync;
11+
/** Implements fs.readFileSync */
12+
readFileSync: typeof fs.readFileSync;
13+
}
14+
15+
/**
16+
* Interface for nodes path module
17+
*/
18+
export interface VMPath {
19+
/** Implements path.resolve */
20+
resolve: typeof pa.resolve;
21+
/** Implements path.isAbsolute */
22+
isAbsolute: typeof pa.isAbsolute;
23+
/** Implements path.join */
24+
join: typeof pa.join;
25+
/** Implements path.basename */
26+
basename: typeof pa.basename;
27+
/** Implements path.dirname */
28+
dirname: typeof pa.dirname;
29+
/** Implements fs.statSync */
30+
statSync: typeof fs.statSync;
31+
/** Implements fs.readFileSync */
32+
readFileSync: typeof fs.readFileSync;
33+
}
34+
35+
/**
36+
* Custom file system which abstracts functions from node's fs and path modules.
37+
*/
38+
export interface VMFileSystemInterface implements VMFS, VMPath {
39+
/** Implements (sep) => sep === path.sep */
40+
isSeparator(char: string): boolean;
41+
}
42+
43+
/**
44+
* Implementation of a default file system.
45+
*/
46+
export class VMFileSystem implements VMFileSystemInterface {
47+
constructor(options?: {fs?: VMFS, path?: VMPath});
48+
}
249

350
/**
451
* Require options for a VM
@@ -26,6 +73,8 @@ export interface VMRequire {
2673
customRequire?: (id: string) => any;
2774
/** Load modules in strict mode. (default: true) */
2875
strict?: boolean;
76+
/** FileSystem to load files from */
77+
fs?: VMFileSystemInterface;
2978
}
3079

3180
/**

lib/filesystem.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
const pa = require('path');
4+
const fs = require('fs');
5+
6+
class DefaultFileSystem {
7+
8+
resolve(path) {
9+
return pa.resolve(path);
10+
}
11+
12+
isSeparator(char) {
13+
return char === '/' || char === pa.sep;
14+
}
15+
16+
isAbsolute(path) {
17+
return pa.isAbsolute(path);
18+
}
19+
20+
join(...paths) {
21+
return pa.join(...paths);
22+
}
23+
24+
basename(path) {
25+
return pa.basename(path);
26+
}
27+
28+
dirname(path) {
29+
return pa.dirname(path);
30+
}
31+
32+
statSync(path, options) {
33+
return fs.statSync(path, options);
34+
}
35+
36+
readFileSync(path, options) {
37+
return fs.readFileSync(path, options);
38+
}
39+
40+
}
41+
42+
class VMFileSystem {
43+
44+
constructor({fs: fsModule = fs, path: pathModule = pa} = {}) {
45+
this.fs = fsModule;
46+
this.path = pathModule;
47+
}
48+
49+
resolve(path) {
50+
return this.path.resolve(path);
51+
}
52+
53+
isSeparator(char) {
54+
return char === '/' || char === this.path.sep;
55+
}
56+
57+
isAbsolute(path) {
58+
return this.path.isAbsolute(path);
59+
}
60+
61+
join(...paths) {
62+
return this.path.join(...paths);
63+
}
64+
65+
basename(path) {
66+
return this.path.basename(path);
67+
}
68+
69+
dirname(path) {
70+
return this.path.dirname(path);
71+
}
72+
73+
statSync(path, options) {
74+
return this.fs.statSync(path, options);
75+
}
76+
77+
readFileSync(path, options) {
78+
return this.fs.readFileSync(path, options);
79+
}
80+
81+
}
82+
83+
exports.DefaultFileSystem = DefaultFileSystem;
84+
exports.VMFileSystem = VMFileSystem;

lib/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ const {
1212
const {
1313
NodeVM
1414
} = require('./nodevm');
15+
const {
16+
VMFileSystem
17+
} = require('./filesystem');
1518

1619
exports.VMError = VMError;
1720
exports.VMScript = VMScript;
1821
exports.NodeVM = NodeVM;
1922
exports.VM = VM;
23+
exports.VMFileSystem = VMFileSystem;

lib/resolver-compat.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// Translate the old options to the new Resolver functionality.
44

55
const fs = require('fs');
6-
const pa = require('path');
76
const nmod = require('module');
87
const {EventEmitter} = require('events');
98
const util = require('util');
@@ -15,6 +14,7 @@ const {
1514
const {VMScript} = require('./script');
1615
const {VM} = require('./vm');
1716
const {VMError} = require('./bridge');
17+
const {DefaultFileSystem} = require('./filesystem');
1818

1919
/**
2020
* Require wrapper to be able to annotate require with webpackIgnore.
@@ -46,8 +46,8 @@ function makeExternalMatcher(obj) {
4646

4747
class LegacyResolver extends DefaultResolver {
4848

49-
constructor(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
50-
super(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict);
49+
constructor(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
50+
super(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict);
5151
this.externals = externals;
5252
this.currMod = undefined;
5353
this.trustedMods = new WeakMap();
@@ -264,15 +264,17 @@ function defaultCustomResolver() {
264264
return undefined;
265265
}
266266

267-
const DENY_RESOLVER = new Resolver({__proto__: null}, [], id => {
267+
const DEFAULT_FS = new DefaultFileSystem();
268+
269+
const DENY_RESOLVER = new Resolver(DEFAULT_FS, {__proto__: null}, [], id => {
268270
throw new VMError(`Access denied to require '${id}'`, 'EDENIED');
269271
});
270272

271273
function resolverFromOptions(vm, options, override, compiler) {
272274
if (!options) {
273275
if (!override) return DENY_RESOLVER;
274276
const builtins = genBuiltinsFromOptions(vm, undefined, undefined, override);
275-
return new Resolver(builtins, [], defaultRequire);
277+
return new Resolver(DEFAULT_FS, builtins, [], defaultRequire);
276278
}
277279

278280
const {
@@ -284,22 +286,22 @@ function resolverFromOptions(vm, options, override, compiler) {
284286
customRequire: hostRequire = defaultRequire,
285287
context = 'host',
286288
strict = true,
289+
fs: fsOpt = DEFAULT_FS,
287290
} = options;
288291

289292
const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override);
290293

291-
if (!externalOpt) return new Resolver(builtins, [], hostRequire);
294+
if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire);
292295

293296
let checkPath;
294297
if (rootPaths) {
295-
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => pa.resolve(f));
298+
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f));
296299
checkPath = (filename) => {
297300
return checkedRootPaths.some(path => {
298301
if (!filename.startsWith(path)) return false;
299302
const len = path.length;
300-
if (filename.length === len || (len > 0 && path[len-1] === pa.sep)) return true;
301-
const sep = filename[len];
302-
return sep === '/' || sep === pa.sep;
303+
if (filename.length === len || (len > 0 && fsOpt.isSeparator(path[len-1]))) return true;
304+
return fsOpt.isSeparator(filename[len]);
303305
});
304306
};
305307
} else {
@@ -326,7 +328,7 @@ function resolverFromOptions(vm, options, override, compiler) {
326328
}
327329

328330
if (typeof externalOpt !== 'object') {
329-
return new DefaultResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict);
331+
return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict);
330332
}
331333

332334
let transitive = false;
@@ -337,7 +339,7 @@ function resolverFromOptions(vm, options, override, compiler) {
337339
transitive = context === 'sandbox' && externalOpt.transitive;
338340
}
339341
externals = external.map(makeExternalMatcher);
340-
return new LegacyResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
342+
return new LegacyResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
341343
}
342344

343345
exports.resolverFromOptions = resolverFromOptions;

lib/resolver.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
// The Resolver is currently experimental and might be exposed to users in the future.
44

5-
const pa = require('path');
6-
const fs = require('fs');
7-
85
const {
96
VMError
107
} = require('./bridge');
@@ -24,7 +21,8 @@ function isArrayIndex(key) {
2421

2522
class Resolver {
2623

27-
constructor(builtinModules, globalPaths, hostRequire) {
24+
constructor(fs, builtinModules, globalPaths, hostRequire) {
25+
this.fs = fs;
2826
this.builtinModules = builtinModules;
2927
this.globalPaths = globalPaths;
3028
this.hostRequire = hostRequire;
@@ -35,31 +33,31 @@ class Resolver {
3533
}
3634

3735
pathResolve(path) {
38-
return pa.resolve(path);
36+
return this.fs.resolve(path);
3937
}
4038

4139
pathIsRelative(path) {
4240
if (path === '' || path[0] !== '.') return false;
4341
if (path.length === 1) return true;
4442
const idx = path[1] === '.' ? 2 : 1;
4543
if (path.length <= idx) return false;
46-
return path[idx] === '/' || path[idx] === pa.sep;
44+
return this.fs.isSeparator(path[idx]);
4745
}
4846

4947
pathIsAbsolute(path) {
50-
return pa.isAbsolute(path);
48+
return path !== '' && (this.fs.isSeparator(path[0]) || this.fs.isAbsolute(path));
5149
}
5250

5351
pathConcat(...paths) {
54-
return pa.join(...paths);
52+
return this.fs.join(...paths);
5553
}
5654

5755
pathBasename(path) {
58-
return pa.basename(path);
56+
return this.fs.basename(path);
5957
}
6058

6159
pathDirname(path) {
62-
return pa.dirname(path);
60+
return this.fs.dirname(path);
6361
}
6462

6563
lookupPaths(mod, id) {
@@ -140,8 +138,8 @@ class Resolver {
140138

141139
class DefaultResolver extends Resolver {
142140

143-
constructor(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict) {
144-
super(builtinModules, globalPaths, hostRequire);
141+
constructor(fs, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict) {
142+
super(fs, builtinModules, globalPaths, hostRequire);
145143
this.checkPath = checkPath;
146144
this.pathContext = pathContext;
147145
this.customResolver = customResolver;
@@ -157,7 +155,7 @@ class DefaultResolver extends Resolver {
157155

158156
pathTestIsDirectory(path) {
159157
try {
160-
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
158+
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
161159
return stat && stat.isDirectory();
162160
} catch (e) {
163161
return false;
@@ -166,15 +164,15 @@ class DefaultResolver extends Resolver {
166164

167165
pathTestIsFile(path) {
168166
try {
169-
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
167+
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
170168
return stat && stat.isFile();
171169
} catch (e) {
172170
return false;
173171
}
174172
}
175173

176174
readFile(path) {
177-
return fs.readFileSync(path, {encoding: 'utf8'});
175+
return this.fs.readFileSync(path, {encoding: 'utf8'});
178176
}
179177

180178
readFileWhenExists(path) {

lib/setup-node-sandbox.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function requireImpl(mod, id, direct) {
107107
return nmod;
108108
}
109109

110-
const path = resolver.pathDirname(filename);
110+
const path = resolver.fs.dirname(filename);
111111
const module = new Module(filename, path, mod);
112112
resolver.registerModule(module, filename, path, mod, direct);
113113
mod._updateChildren(module, true);
@@ -146,7 +146,7 @@ Module._cache = {__proto__: null};
146146
}
147147

148148
function findBestExtensionHandler(filename) {
149-
const name = resolver.pathBasename(filename);
149+
const name = resolver.fs.basename(filename);
150150
for (let i = 0; (i = localStringPrototypeIndexOf(name, '.', i + 1)) !== -1;) {
151151
const ext = localStringPrototypeSlice(name, i);
152152
const handler = Module._extensions[ext];

0 commit comments

Comments
 (0)