sandbox-fs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +418 -0
- package/dist/FSModule.d.ts +153 -0
- package/dist/FSModule.d.ts.map +1 -0
- package/dist/FSModule.js +555 -0
- package/dist/FSModule.js.map +1 -0
- package/dist/PathMapper.d.ts +30 -0
- package/dist/PathMapper.d.ts.map +1 -0
- package/dist/PathMapper.js +122 -0
- package/dist/PathMapper.js.map +1 -0
- package/dist/PathModule.d.ts +69 -0
- package/dist/PathModule.d.ts.map +1 -0
- package/dist/PathModule.js +159 -0
- package/dist/PathModule.js.map +1 -0
- package/dist/ResourceTracker.d.ts +74 -0
- package/dist/ResourceTracker.d.ts.map +1 -0
- package/dist/ResourceTracker.js +175 -0
- package/dist/ResourceTracker.js.map +1 -0
- package/dist/VirtualFileSystem.d.ts +145 -0
- package/dist/VirtualFileSystem.d.ts.map +1 -0
- package/dist/VirtualFileSystem.js +155 -0
- package/dist/VirtualFileSystem.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/operations/newer.d.ts +36 -0
- package/dist/operations/newer.d.ts.map +1 -0
- package/dist/operations/newer.js +239 -0
- package/dist/operations/newer.js.map +1 -0
- package/dist/operations/read.d.ts +24 -0
- package/dist/operations/read.d.ts.map +1 -0
- package/dist/operations/read.js +313 -0
- package/dist/operations/read.js.map +1 -0
- package/dist/operations/symlink.d.ts +8 -0
- package/dist/operations/symlink.d.ts.map +1 -0
- package/dist/operations/symlink.js +33 -0
- package/dist/operations/symlink.js.map +1 -0
- package/dist/operations/write.d.ts +29 -0
- package/dist/operations/write.d.ts.map +1 -0
- package/dist/operations/write.js +191 -0
- package/dist/operations/write.js.map +1 -0
- package/dist/utils/ErrorFilter.d.ts +6 -0
- package/dist/utils/ErrorFilter.d.ts.map +1 -0
- package/dist/utils/ErrorFilter.js +57 -0
- package/dist/utils/ErrorFilter.js.map +1 -0
- package/dist/utils/callbackify.d.ts +9 -0
- package/dist/utils/callbackify.d.ts.map +1 -0
- package/dist/utils/callbackify.js +48 -0
- package/dist/utils/callbackify.js.map +1 -0
- package/dist/wrappers/VirtualDir.d.ts +34 -0
- package/dist/wrappers/VirtualDir.d.ts.map +1 -0
- package/dist/wrappers/VirtualDir.js +72 -0
- package/dist/wrappers/VirtualDir.js.map +1 -0
- package/dist/wrappers/VirtualDirent.d.ts +21 -0
- package/dist/wrappers/VirtualDirent.d.ts.map +1 -0
- package/dist/wrappers/VirtualDirent.js +50 -0
- package/dist/wrappers/VirtualDirent.js.map +1 -0
- package/example.js +95 -0
- package/example.ts +32 -0
- package/package.json +29 -0
- package/src/FSModule.ts +546 -0
- package/src/PathMapper.ts +102 -0
- package/src/PathModule.ts +142 -0
- package/src/ResourceTracker.ts +162 -0
- package/src/VirtualFileSystem.ts +172 -0
- package/src/index.ts +9 -0
- package/src/operations/newer.ts +223 -0
- package/src/operations/read.ts +319 -0
- package/src/operations/symlink.ts +31 -0
- package/src/operations/write.ts +189 -0
- package/src/utils/ErrorFilter.ts +57 -0
- package/src/utils/callbackify.ts +54 -0
- package/src/wrappers/VirtualDir.ts +84 -0
- package/src/wrappers/VirtualDirent.ts +60 -0
- package/test-data/example.txt +1 -0
- package/test-data/subdir/nested.txt +1 -0
- package/tsconfig.example.json +8 -0
- package/tsconfig.json +21 -0
package/src/FSModule.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import type { PathMapper } from './PathMapper';
|
|
3
|
+
import type { ResourceTracker } from './ResourceTracker';
|
|
4
|
+
import { callbackify, callbackifyVoid } from './utils/callbackify';
|
|
5
|
+
|
|
6
|
+
// Import operations
|
|
7
|
+
import * as readOps from './operations/read';
|
|
8
|
+
import * as writeOps from './operations/write';
|
|
9
|
+
import * as symlinkOps from './operations/symlink';
|
|
10
|
+
import * as newerOps from './operations/newer';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a Node.js fs-compatible module
|
|
14
|
+
*/
|
|
15
|
+
export function createFSModule(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
16
|
+
// Create promise-based operations
|
|
17
|
+
const promiseOps = {
|
|
18
|
+
// Read operations
|
|
19
|
+
readFile: readOps.createReadFileOperation(pathMapper, resourceTracker),
|
|
20
|
+
readdir: readOps.createReaddirOperation(pathMapper, resourceTracker),
|
|
21
|
+
stat: readOps.createStatOperation(pathMapper, resourceTracker),
|
|
22
|
+
lstat: readOps.createLstatOperation(pathMapper, resourceTracker),
|
|
23
|
+
access: readOps.createAccessOperation(pathMapper, resourceTracker),
|
|
24
|
+
realpath: readOps.createRealpathOperation(pathMapper, resourceTracker),
|
|
25
|
+
open: readOps.createOpenOperation(pathMapper, resourceTracker),
|
|
26
|
+
read: readOps.createReadOperation(pathMapper, resourceTracker),
|
|
27
|
+
close: readOps.createCloseOperation(pathMapper, resourceTracker),
|
|
28
|
+
fstat: readOps.createFstatOperation(pathMapper, resourceTracker),
|
|
29
|
+
opendir: readOps.createOpendirOperation(pathMapper, resourceTracker),
|
|
30
|
+
exists: readOps.createExistsOperation(pathMapper, resourceTracker),
|
|
31
|
+
|
|
32
|
+
// Write operations (all denied)
|
|
33
|
+
writeFile: writeOps.createWriteFileOperation(),
|
|
34
|
+
appendFile: writeOps.createAppendFileOperation(),
|
|
35
|
+
mkdir: writeOps.createMkdirOperation(),
|
|
36
|
+
rmdir: writeOps.createRmdirOperation(),
|
|
37
|
+
rm: writeOps.createRmOperation(),
|
|
38
|
+
unlink: writeOps.createUnlinkOperation(),
|
|
39
|
+
rename: writeOps.createRenameOperation(),
|
|
40
|
+
copyFile: writeOps.createCopyFileOperation(),
|
|
41
|
+
truncate: writeOps.createTruncateOperation(),
|
|
42
|
+
ftruncate: writeOps.createFtruncateOperation(),
|
|
43
|
+
chmod: writeOps.createChmodOperation(),
|
|
44
|
+
fchmod: writeOps.createFchmodOperation(),
|
|
45
|
+
lchmod: writeOps.createLchmodOperation(),
|
|
46
|
+
chown: writeOps.createChownOperation(),
|
|
47
|
+
fchown: writeOps.createFchownOperation(),
|
|
48
|
+
lchown: writeOps.createLchownOperation(),
|
|
49
|
+
utimes: writeOps.createUtimesOperation(),
|
|
50
|
+
futimes: writeOps.createFutimesOperation(),
|
|
51
|
+
lutimes: writeOps.createLutimesOperation(),
|
|
52
|
+
write: writeOps.createWriteOperation(),
|
|
53
|
+
writev: writeOps.createWritevOperation(),
|
|
54
|
+
fsync: writeOps.createFsyncOperation(),
|
|
55
|
+
fdatasync: writeOps.createFdatasyncOperation(),
|
|
56
|
+
|
|
57
|
+
// Symlink operations (all denied)
|
|
58
|
+
symlink: symlinkOps.createSymlinkOperation(),
|
|
59
|
+
link: symlinkOps.createLinkOperation(),
|
|
60
|
+
readlink: symlinkOps.createReadlinkOperation(),
|
|
61
|
+
|
|
62
|
+
// Newer operations
|
|
63
|
+
glob: newerOps.createGlobOperation(pathMapper, resourceTracker),
|
|
64
|
+
statfs: newerOps.createStatfsOperation(pathMapper, resourceTracker),
|
|
65
|
+
cp: newerOps.createCpOperation(pathMapper, resourceTracker),
|
|
66
|
+
openAsBlob: newerOps.createOpenAsBlobOperation(pathMapper, resourceTracker),
|
|
67
|
+
readv: newerOps.createReadvOperation(pathMapper, resourceTracker),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create callback-based operations
|
|
71
|
+
const callbackOps = {
|
|
72
|
+
// Read operations
|
|
73
|
+
readFile: callbackify(promiseOps.readFile),
|
|
74
|
+
readdir: callbackify(promiseOps.readdir),
|
|
75
|
+
stat: callbackify(promiseOps.stat),
|
|
76
|
+
lstat: callbackify(promiseOps.lstat),
|
|
77
|
+
access: callbackifyVoid(promiseOps.access),
|
|
78
|
+
realpath: callbackify(promiseOps.realpath),
|
|
79
|
+
open: callbackify(promiseOps.open),
|
|
80
|
+
read: callbackify(promiseOps.read),
|
|
81
|
+
close: callbackifyVoid(promiseOps.close),
|
|
82
|
+
fstat: callbackify(promiseOps.fstat),
|
|
83
|
+
opendir: callbackify(promiseOps.opendir),
|
|
84
|
+
exists: (path: string, callback: (exists: boolean) => void) => {
|
|
85
|
+
promiseOps.exists(path).then(callback).catch(() => callback(false));
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Write operations (all denied)
|
|
89
|
+
writeFile: callbackifyVoid(promiseOps.writeFile),
|
|
90
|
+
appendFile: callbackifyVoid(promiseOps.appendFile),
|
|
91
|
+
mkdir: callbackify(promiseOps.mkdir),
|
|
92
|
+
rmdir: callbackifyVoid(promiseOps.rmdir),
|
|
93
|
+
rm: callbackifyVoid(promiseOps.rm),
|
|
94
|
+
unlink: callbackifyVoid(promiseOps.unlink),
|
|
95
|
+
rename: callbackifyVoid(promiseOps.rename),
|
|
96
|
+
copyFile: callbackifyVoid(promiseOps.copyFile),
|
|
97
|
+
truncate: callbackifyVoid(promiseOps.truncate),
|
|
98
|
+
ftruncate: callbackifyVoid(promiseOps.ftruncate),
|
|
99
|
+
chmod: callbackifyVoid(promiseOps.chmod),
|
|
100
|
+
fchmod: callbackifyVoid(promiseOps.fchmod),
|
|
101
|
+
lchmod: callbackifyVoid(promiseOps.lchmod),
|
|
102
|
+
chown: callbackifyVoid(promiseOps.chown),
|
|
103
|
+
fchown: callbackifyVoid(promiseOps.fchown),
|
|
104
|
+
lchown: callbackifyVoid(promiseOps.lchown),
|
|
105
|
+
utimes: callbackifyVoid(promiseOps.utimes),
|
|
106
|
+
futimes: callbackifyVoid(promiseOps.futimes),
|
|
107
|
+
lutimes: callbackifyVoid(promiseOps.lutimes),
|
|
108
|
+
write: callbackify(promiseOps.write),
|
|
109
|
+
writev: callbackify(promiseOps.writev),
|
|
110
|
+
fsync: callbackifyVoid(promiseOps.fsync),
|
|
111
|
+
fdatasync: callbackifyVoid(promiseOps.fdatasync),
|
|
112
|
+
|
|
113
|
+
// Symlink operations (all denied)
|
|
114
|
+
symlink: callbackifyVoid(promiseOps.symlink),
|
|
115
|
+
link: callbackifyVoid(promiseOps.link),
|
|
116
|
+
readlink: callbackify(promiseOps.readlink),
|
|
117
|
+
|
|
118
|
+
// Newer operations
|
|
119
|
+
statfs: callbackify(promiseOps.statfs),
|
|
120
|
+
cp: callbackifyVoid(promiseOps.cp),
|
|
121
|
+
openAsBlob: callbackify(promiseOps.openAsBlob),
|
|
122
|
+
readv: callbackify(promiseOps.readv),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Sync operations - all throw EACCES or operate on the real filesystem
|
|
126
|
+
const syncOps = {
|
|
127
|
+
readFileSync: (path: string, options?: any) => {
|
|
128
|
+
resourceTracker.assertNotClosed();
|
|
129
|
+
const realPath = pathMapper.toRealPath(path);
|
|
130
|
+
try {
|
|
131
|
+
return fs.readFileSync(realPath, options);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
readdirSync: (path: string, options?: any) => {
|
|
138
|
+
resourceTracker.assertNotClosed();
|
|
139
|
+
const realPath = pathMapper.toRealPath(path);
|
|
140
|
+
try {
|
|
141
|
+
return fs.readdirSync(realPath, options);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
statSync: (path: string, options?: any) => {
|
|
148
|
+
resourceTracker.assertNotClosed();
|
|
149
|
+
const realPath = pathMapper.toRealPath(path);
|
|
150
|
+
try {
|
|
151
|
+
const stats = fs.statSync(realPath, options);
|
|
152
|
+
if (stats.isSymbolicLink()) {
|
|
153
|
+
const err = new Error('stat: operation not permitted') as NodeJS.ErrnoException;
|
|
154
|
+
err.code = 'EACCES';
|
|
155
|
+
err.errno = -13;
|
|
156
|
+
err.syscall = 'stat';
|
|
157
|
+
err.path = path;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
return stats;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
lstatSync: (path: string, options?: any) => {
|
|
167
|
+
resourceTracker.assertNotClosed();
|
|
168
|
+
const realPath = pathMapper.toRealPath(path);
|
|
169
|
+
try {
|
|
170
|
+
const stats = fs.lstatSync(realPath, options);
|
|
171
|
+
if (stats.isSymbolicLink()) {
|
|
172
|
+
const err = new Error('lstat: operation not permitted') as NodeJS.ErrnoException;
|
|
173
|
+
err.code = 'EACCES';
|
|
174
|
+
err.errno = -13;
|
|
175
|
+
err.syscall = 'lstat';
|
|
176
|
+
err.path = path;
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
return stats;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
accessSync: (path: string, mode?: number) => {
|
|
186
|
+
resourceTracker.assertNotClosed();
|
|
187
|
+
const realPath = pathMapper.toRealPath(path);
|
|
188
|
+
try {
|
|
189
|
+
return fs.accessSync(realPath, mode);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
realpathSync: (path: string, options?: any) => {
|
|
196
|
+
resourceTracker.assertNotClosed();
|
|
197
|
+
const realPath = pathMapper.toRealPath(path);
|
|
198
|
+
try {
|
|
199
|
+
const resolved = fs.realpathSync(realPath, options);
|
|
200
|
+
const resolvedStr = Buffer.isBuffer(resolved) ? resolved.toString() : resolved;
|
|
201
|
+
return pathMapper.toVirtualPath(resolvedStr);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
openSync: (path: string, flags?: string | number, mode?: number) => {
|
|
208
|
+
resourceTracker.assertNotClosed();
|
|
209
|
+
const flagsStr = typeof flags === 'number' ? flags.toString() : (flags || 'r');
|
|
210
|
+
const isWriteMode = /w|a|\+/.test(flagsStr.toString());
|
|
211
|
+
|
|
212
|
+
if (isWriteMode) {
|
|
213
|
+
const err = new Error('open: operation not permitted') as NodeJS.ErrnoException;
|
|
214
|
+
err.code = 'EACCES';
|
|
215
|
+
err.errno = -13;
|
|
216
|
+
err.syscall = 'open';
|
|
217
|
+
err.path = path;
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const realPath = pathMapper.toRealPath(path);
|
|
222
|
+
try {
|
|
223
|
+
const fd = fs.openSync(realPath, (flags || 'r') as any, mode);
|
|
224
|
+
resourceTracker.trackFD(fd, {
|
|
225
|
+
virtualPath: path,
|
|
226
|
+
realPath,
|
|
227
|
+
flags: flags || 'r',
|
|
228
|
+
mode,
|
|
229
|
+
isStream: false,
|
|
230
|
+
});
|
|
231
|
+
return fd;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
readSync: (fd: number, buffer: NodeJS.ArrayBufferView, offset: number, length: number, position: number | null) => {
|
|
238
|
+
resourceTracker.assertNotClosed();
|
|
239
|
+
if (!resourceTracker.isTracked(fd)) {
|
|
240
|
+
const err = new Error('bad file descriptor') as NodeJS.ErrnoException;
|
|
241
|
+
err.code = 'EBADF';
|
|
242
|
+
err.errno = -9;
|
|
243
|
+
err.syscall = 'read';
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
return fs.readSync(fd, buffer, offset, length, position);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
closeSync: (fd: number) => {
|
|
250
|
+
resourceTracker.assertNotClosed();
|
|
251
|
+
resourceTracker.untrackFD(fd);
|
|
252
|
+
return fs.closeSync(fd);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
fstatSync: (fd: number, options?: any) => {
|
|
256
|
+
resourceTracker.assertNotClosed();
|
|
257
|
+
if (!resourceTracker.isTracked(fd)) {
|
|
258
|
+
const err = new Error('bad file descriptor') as NodeJS.ErrnoException;
|
|
259
|
+
err.code = 'EBADF';
|
|
260
|
+
err.errno = -9;
|
|
261
|
+
err.syscall = 'fstat';
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
return fs.fstatSync(fd, options);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
opendirSync: (path: string, options?: any) => {
|
|
268
|
+
resourceTracker.assertNotClosed();
|
|
269
|
+
const realPath = pathMapper.toRealPath(path);
|
|
270
|
+
try {
|
|
271
|
+
const realDir = fs.opendirSync(realPath, options);
|
|
272
|
+
return new (require('./wrappers/VirtualDir').VirtualDir)(realDir, path, pathMapper);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
existsSync: (path: string) => {
|
|
279
|
+
if (resourceTracker.closed) return false;
|
|
280
|
+
try {
|
|
281
|
+
const realPath = pathMapper.toRealPath(path);
|
|
282
|
+
return fs.existsSync(realPath);
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Write operations - all throw EACCES
|
|
289
|
+
writeFileSync: (path: string) => {
|
|
290
|
+
const err = new Error('open: operation not permitted') as NodeJS.ErrnoException;
|
|
291
|
+
err.code = 'EACCES';
|
|
292
|
+
err.syscall = 'open';
|
|
293
|
+
err.path = path;
|
|
294
|
+
throw err;
|
|
295
|
+
},
|
|
296
|
+
appendFileSync: (path: string) => {
|
|
297
|
+
const err = new Error('open: operation not permitted') as NodeJS.ErrnoException;
|
|
298
|
+
err.code = 'EACCES';
|
|
299
|
+
err.syscall = 'open';
|
|
300
|
+
err.path = path;
|
|
301
|
+
throw err;
|
|
302
|
+
},
|
|
303
|
+
mkdirSync: (path: string) => {
|
|
304
|
+
const err = new Error('mkdir: operation not permitted') as NodeJS.ErrnoException;
|
|
305
|
+
err.code = 'EACCES';
|
|
306
|
+
err.syscall = 'mkdir';
|
|
307
|
+
err.path = path;
|
|
308
|
+
throw err;
|
|
309
|
+
},
|
|
310
|
+
rmdirSync: (path: string) => {
|
|
311
|
+
const err = new Error('rmdir: operation not permitted') as NodeJS.ErrnoException;
|
|
312
|
+
err.code = 'EACCES';
|
|
313
|
+
err.syscall = 'rmdir';
|
|
314
|
+
err.path = path;
|
|
315
|
+
throw err;
|
|
316
|
+
},
|
|
317
|
+
rmSync: (path: string) => {
|
|
318
|
+
const err = new Error('rm: operation not permitted') as NodeJS.ErrnoException;
|
|
319
|
+
err.code = 'EACCES';
|
|
320
|
+
err.syscall = 'rm';
|
|
321
|
+
err.path = path;
|
|
322
|
+
throw err;
|
|
323
|
+
},
|
|
324
|
+
unlinkSync: (path: string) => {
|
|
325
|
+
const err = new Error('unlink: operation not permitted') as NodeJS.ErrnoException;
|
|
326
|
+
err.code = 'EACCES';
|
|
327
|
+
err.syscall = 'unlink';
|
|
328
|
+
err.path = path;
|
|
329
|
+
throw err;
|
|
330
|
+
},
|
|
331
|
+
renameSync: (oldPath: string) => {
|
|
332
|
+
const err = new Error('rename: operation not permitted') as NodeJS.ErrnoException;
|
|
333
|
+
err.code = 'EACCES';
|
|
334
|
+
err.syscall = 'rename';
|
|
335
|
+
err.path = oldPath;
|
|
336
|
+
throw err;
|
|
337
|
+
},
|
|
338
|
+
copyFileSync: (src: string, dest: string) => {
|
|
339
|
+
const err = new Error('copyfile: operation not permitted') as NodeJS.ErrnoException;
|
|
340
|
+
err.code = 'EACCES';
|
|
341
|
+
err.syscall = 'copyfile';
|
|
342
|
+
err.path = dest;
|
|
343
|
+
throw err;
|
|
344
|
+
},
|
|
345
|
+
truncateSync: (path: string) => {
|
|
346
|
+
const err = new Error('open: operation not permitted') as NodeJS.ErrnoException;
|
|
347
|
+
err.code = 'EACCES';
|
|
348
|
+
err.syscall = 'open';
|
|
349
|
+
err.path = path;
|
|
350
|
+
throw err;
|
|
351
|
+
},
|
|
352
|
+
ftruncateSync: (fd: number) => {
|
|
353
|
+
const err = new Error('ftruncate: operation not permitted') as NodeJS.ErrnoException;
|
|
354
|
+
err.code = 'EACCES';
|
|
355
|
+
err.syscall = 'ftruncate';
|
|
356
|
+
throw err;
|
|
357
|
+
},
|
|
358
|
+
chmodSync: (path: string) => {
|
|
359
|
+
const err = new Error('chmod: operation not permitted') as NodeJS.ErrnoException;
|
|
360
|
+
err.code = 'EACCES';
|
|
361
|
+
err.syscall = 'chmod';
|
|
362
|
+
err.path = path;
|
|
363
|
+
throw err;
|
|
364
|
+
},
|
|
365
|
+
fchmodSync: (fd: number) => {
|
|
366
|
+
const err = new Error('fchmod: operation not permitted') as NodeJS.ErrnoException;
|
|
367
|
+
err.code = 'EACCES';
|
|
368
|
+
err.syscall = 'fchmod';
|
|
369
|
+
throw err;
|
|
370
|
+
},
|
|
371
|
+
lchmodSync: (path: string) => {
|
|
372
|
+
const err = new Error('lchmod: operation not permitted') as NodeJS.ErrnoException;
|
|
373
|
+
err.code = 'EACCES';
|
|
374
|
+
err.syscall = 'lchmod';
|
|
375
|
+
err.path = path;
|
|
376
|
+
throw err;
|
|
377
|
+
},
|
|
378
|
+
chownSync: (path: string) => {
|
|
379
|
+
const err = new Error('chown: operation not permitted') as NodeJS.ErrnoException;
|
|
380
|
+
err.code = 'EACCES';
|
|
381
|
+
err.syscall = 'chown';
|
|
382
|
+
err.path = path;
|
|
383
|
+
throw err;
|
|
384
|
+
},
|
|
385
|
+
fchownSync: (fd: number) => {
|
|
386
|
+
const err = new Error('fchown: operation not permitted') as NodeJS.ErrnoException;
|
|
387
|
+
err.code = 'EACCES';
|
|
388
|
+
err.syscall = 'fchown';
|
|
389
|
+
throw err;
|
|
390
|
+
},
|
|
391
|
+
lchownSync: (path: string) => {
|
|
392
|
+
const err = new Error('lchown: operation not permitted') as NodeJS.ErrnoException;
|
|
393
|
+
err.code = 'EACCES';
|
|
394
|
+
err.syscall = 'lchown';
|
|
395
|
+
err.path = path;
|
|
396
|
+
throw err;
|
|
397
|
+
},
|
|
398
|
+
utimesSync: (path: string) => {
|
|
399
|
+
const err = new Error('utime: operation not permitted') as NodeJS.ErrnoException;
|
|
400
|
+
err.code = 'EACCES';
|
|
401
|
+
err.syscall = 'utime';
|
|
402
|
+
err.path = path;
|
|
403
|
+
throw err;
|
|
404
|
+
},
|
|
405
|
+
futimesSync: (fd: number) => {
|
|
406
|
+
const err = new Error('futime: operation not permitted') as NodeJS.ErrnoException;
|
|
407
|
+
err.code = 'EACCES';
|
|
408
|
+
err.syscall = 'futime';
|
|
409
|
+
throw err;
|
|
410
|
+
},
|
|
411
|
+
lutimesSync: (path: string) => {
|
|
412
|
+
const err = new Error('lutime: operation not permitted') as NodeJS.ErrnoException;
|
|
413
|
+
err.code = 'EACCES';
|
|
414
|
+
err.syscall = 'lutime';
|
|
415
|
+
err.path = path;
|
|
416
|
+
throw err;
|
|
417
|
+
},
|
|
418
|
+
writeSync: (fd: number) => {
|
|
419
|
+
const err = new Error('write: operation not permitted') as NodeJS.ErrnoException;
|
|
420
|
+
err.code = 'EACCES';
|
|
421
|
+
err.syscall = 'write';
|
|
422
|
+
throw err;
|
|
423
|
+
},
|
|
424
|
+
writevSync: (fd: number) => {
|
|
425
|
+
const err = new Error('writev: operation not permitted') as NodeJS.ErrnoException;
|
|
426
|
+
err.code = 'EACCES';
|
|
427
|
+
err.syscall = 'writev';
|
|
428
|
+
throw err;
|
|
429
|
+
},
|
|
430
|
+
fsyncSync: (fd: number) => {
|
|
431
|
+
const err = new Error('fsync: operation not permitted') as NodeJS.ErrnoException;
|
|
432
|
+
err.code = 'EACCES';
|
|
433
|
+
err.syscall = 'fsync';
|
|
434
|
+
throw err;
|
|
435
|
+
},
|
|
436
|
+
fdatasyncSync: (fd: number) => {
|
|
437
|
+
const err = new Error('fdatasync: operation not permitted') as NodeJS.ErrnoException;
|
|
438
|
+
err.code = 'EACCES';
|
|
439
|
+
err.syscall = 'fdatasync';
|
|
440
|
+
throw err;
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
// Symlink operations - all throw EACCES
|
|
444
|
+
symlinkSync: (target: string, path: string) => {
|
|
445
|
+
const err = new Error('symlink: operation not permitted') as NodeJS.ErrnoException;
|
|
446
|
+
err.code = 'EACCES';
|
|
447
|
+
err.syscall = 'symlink';
|
|
448
|
+
err.path = path;
|
|
449
|
+
throw err;
|
|
450
|
+
},
|
|
451
|
+
linkSync: (existingPath: string, newPath: string) => {
|
|
452
|
+
const err = new Error('link: operation not permitted') as NodeJS.ErrnoException;
|
|
453
|
+
err.code = 'EACCES';
|
|
454
|
+
err.syscall = 'link';
|
|
455
|
+
err.path = newPath;
|
|
456
|
+
throw err;
|
|
457
|
+
},
|
|
458
|
+
readlinkSync: (path: string) => {
|
|
459
|
+
const err = new Error('readlink: operation not permitted') as NodeJS.ErrnoException;
|
|
460
|
+
err.code = 'EACCES';
|
|
461
|
+
err.syscall = 'readlink';
|
|
462
|
+
err.path = path;
|
|
463
|
+
throw err;
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
// Newer operations
|
|
467
|
+
cpSync: (source: string, destination: string) => {
|
|
468
|
+
const err = new Error('cp: operation not permitted') as NodeJS.ErrnoException;
|
|
469
|
+
err.code = 'EACCES';
|
|
470
|
+
err.syscall = 'cp';
|
|
471
|
+
err.path = destination;
|
|
472
|
+
throw err;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
statfsSync: (path: string, options?: any) => {
|
|
476
|
+
resourceTracker.assertNotClosed();
|
|
477
|
+
if (typeof fs.statfsSync !== 'function') {
|
|
478
|
+
throw new Error('statfsSync is not supported in this Node.js version');
|
|
479
|
+
}
|
|
480
|
+
const realPath = pathMapper.toRealPath(path);
|
|
481
|
+
try {
|
|
482
|
+
return fs.statfsSync(realPath, options);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw require('./utils/ErrorFilter').filterError(error, pathMapper);
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
readvSync: (fd: number, buffers: NodeJS.ArrayBufferView[], position?: number) => {
|
|
489
|
+
resourceTracker.assertNotClosed();
|
|
490
|
+
if (!resourceTracker.isTracked(fd)) {
|
|
491
|
+
const err = new Error('bad file descriptor') as NodeJS.ErrnoException;
|
|
492
|
+
err.code = 'EBADF';
|
|
493
|
+
err.errno = -9;
|
|
494
|
+
err.syscall = 'readv';
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
497
|
+
if (typeof (fs as any).readvSync !== 'function') {
|
|
498
|
+
throw new Error('readvSync is not supported in this Node.js version');
|
|
499
|
+
}
|
|
500
|
+
return (fs as any).readvSync(fd, buffers, position);
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Stream operations
|
|
505
|
+
const streamOps = {
|
|
506
|
+
createReadStream: readOps.createCreateReadStreamOperation(pathMapper, resourceTracker),
|
|
507
|
+
createWriteStream: writeOps.createCreateWriteStreamOperation(),
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Watch operations
|
|
511
|
+
const watchOps = {
|
|
512
|
+
watch: newerOps.createWatchOperation(pathMapper, resourceTracker),
|
|
513
|
+
watchFile: newerOps.createWatchFileOperation(pathMapper, resourceTracker),
|
|
514
|
+
unwatchFile: newerOps.createUnwatchFileOperation(pathMapper, resourceTracker),
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Combine all operations
|
|
518
|
+
return {
|
|
519
|
+
// Callback-based operations
|
|
520
|
+
...callbackOps,
|
|
521
|
+
|
|
522
|
+
// Sync operations
|
|
523
|
+
...syncOps,
|
|
524
|
+
|
|
525
|
+
// Stream operations
|
|
526
|
+
...streamOps,
|
|
527
|
+
|
|
528
|
+
// Watch operations
|
|
529
|
+
...watchOps,
|
|
530
|
+
|
|
531
|
+
// Promise-based API
|
|
532
|
+
promises: {
|
|
533
|
+
...promiseOps,
|
|
534
|
+
// Note: glob is already an async generator, so it's included as-is
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
// Constants
|
|
538
|
+
constants: fs.constants,
|
|
539
|
+
|
|
540
|
+
// Classes
|
|
541
|
+
Stats: fs.Stats,
|
|
542
|
+
Dirent: fs.Dirent,
|
|
543
|
+
ReadStream: fs.ReadStream,
|
|
544
|
+
WriteStream: fs.WriteStream,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles conversion between virtual Unix-style paths and real platform-native paths
|
|
6
|
+
*/
|
|
7
|
+
export class PathMapper {
|
|
8
|
+
private readonly normalizedRoot: string;
|
|
9
|
+
|
|
10
|
+
constructor(rootPath: string) {
|
|
11
|
+
// Normalize and resolve the root path to absolute platform-native format
|
|
12
|
+
this.normalizedRoot = path.resolve(rootPath);
|
|
13
|
+
|
|
14
|
+
// Validate that the root path exists
|
|
15
|
+
if (!fs.existsSync(this.normalizedRoot)) {
|
|
16
|
+
throw new Error(`VFS root path does not exist: ${rootPath}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate that it's a directory
|
|
20
|
+
const stats = fs.statSync(this.normalizedRoot);
|
|
21
|
+
if (!stats.isDirectory()) {
|
|
22
|
+
throw new Error(`VFS root path is not a directory: ${rootPath}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the normalized root path
|
|
28
|
+
*/
|
|
29
|
+
getRoot(): string {
|
|
30
|
+
return this.normalizedRoot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert a virtual path (Unix-style with forward slashes) to a real platform-native path
|
|
35
|
+
* @param virtualPath - Virtual path starting with /
|
|
36
|
+
* @returns Real absolute path
|
|
37
|
+
* @throws Error if path traversal is detected
|
|
38
|
+
*/
|
|
39
|
+
toRealPath(virtualPath: string): string {
|
|
40
|
+
// Validate virtual path format
|
|
41
|
+
if (!virtualPath.startsWith('/')) {
|
|
42
|
+
throw new Error(`Virtual path must start with /: ${virtualPath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Remove leading slash and normalize
|
|
46
|
+
const relativePath = virtualPath.slice(1);
|
|
47
|
+
|
|
48
|
+
// Join with root to get real path
|
|
49
|
+
const realPath = path.join(this.normalizedRoot, relativePath);
|
|
50
|
+
|
|
51
|
+
// Resolve to get absolute path (handles .. and .)
|
|
52
|
+
const resolvedPath = path.resolve(realPath);
|
|
53
|
+
|
|
54
|
+
// Security check: ensure resolved path is within root
|
|
55
|
+
// Use path.relative to check if we've escaped the root
|
|
56
|
+
const rel = path.relative(this.normalizedRoot, resolvedPath);
|
|
57
|
+
|
|
58
|
+
// If relative path starts with .. or is an absolute path, we've escaped
|
|
59
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
60
|
+
throw new Error(`Path traversal detected: ${virtualPath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return resolvedPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert a real platform-native path to a virtual Unix-style path
|
|
68
|
+
* @param realPath - Real absolute path
|
|
69
|
+
* @returns Virtual path starting with /
|
|
70
|
+
* @throws Error if real path is not within VFS root
|
|
71
|
+
*/
|
|
72
|
+
toVirtualPath(realPath: string): string {
|
|
73
|
+
// Normalize the real path
|
|
74
|
+
const normalized = path.resolve(realPath);
|
|
75
|
+
|
|
76
|
+
// Get relative path from root
|
|
77
|
+
const rel = path.relative(this.normalizedRoot, normalized);
|
|
78
|
+
|
|
79
|
+
// Check if path is outside root
|
|
80
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
81
|
+
throw new Error(`Real path is outside VFS root: ${realPath}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Convert platform separators to forward slashes
|
|
85
|
+
const virtualPath = '/' + rel.split(path.sep).join('/');
|
|
86
|
+
|
|
87
|
+
return virtualPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a real path is within the VFS root
|
|
92
|
+
*/
|
|
93
|
+
isWithinRoot(realPath: string): boolean {
|
|
94
|
+
try {
|
|
95
|
+
const normalized = path.resolve(realPath);
|
|
96
|
+
const rel = path.relative(this.normalizedRoot, normalized);
|
|
97
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|