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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/dist/FSModule.d.ts +153 -0
  4. package/dist/FSModule.d.ts.map +1 -0
  5. package/dist/FSModule.js +555 -0
  6. package/dist/FSModule.js.map +1 -0
  7. package/dist/PathMapper.d.ts +30 -0
  8. package/dist/PathMapper.d.ts.map +1 -0
  9. package/dist/PathMapper.js +122 -0
  10. package/dist/PathMapper.js.map +1 -0
  11. package/dist/PathModule.d.ts +69 -0
  12. package/dist/PathModule.d.ts.map +1 -0
  13. package/dist/PathModule.js +159 -0
  14. package/dist/PathModule.js.map +1 -0
  15. package/dist/ResourceTracker.d.ts +74 -0
  16. package/dist/ResourceTracker.d.ts.map +1 -0
  17. package/dist/ResourceTracker.js +175 -0
  18. package/dist/ResourceTracker.js.map +1 -0
  19. package/dist/VirtualFileSystem.d.ts +145 -0
  20. package/dist/VirtualFileSystem.d.ts.map +1 -0
  21. package/dist/VirtualFileSystem.js +155 -0
  22. package/dist/VirtualFileSystem.js.map +1 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +13 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/operations/newer.d.ts +36 -0
  28. package/dist/operations/newer.d.ts.map +1 -0
  29. package/dist/operations/newer.js +239 -0
  30. package/dist/operations/newer.js.map +1 -0
  31. package/dist/operations/read.d.ts +24 -0
  32. package/dist/operations/read.d.ts.map +1 -0
  33. package/dist/operations/read.js +313 -0
  34. package/dist/operations/read.js.map +1 -0
  35. package/dist/operations/symlink.d.ts +8 -0
  36. package/dist/operations/symlink.d.ts.map +1 -0
  37. package/dist/operations/symlink.js +33 -0
  38. package/dist/operations/symlink.js.map +1 -0
  39. package/dist/operations/write.d.ts +29 -0
  40. package/dist/operations/write.d.ts.map +1 -0
  41. package/dist/operations/write.js +191 -0
  42. package/dist/operations/write.js.map +1 -0
  43. package/dist/utils/ErrorFilter.d.ts +6 -0
  44. package/dist/utils/ErrorFilter.d.ts.map +1 -0
  45. package/dist/utils/ErrorFilter.js +57 -0
  46. package/dist/utils/ErrorFilter.js.map +1 -0
  47. package/dist/utils/callbackify.d.ts +9 -0
  48. package/dist/utils/callbackify.d.ts.map +1 -0
  49. package/dist/utils/callbackify.js +48 -0
  50. package/dist/utils/callbackify.js.map +1 -0
  51. package/dist/wrappers/VirtualDir.d.ts +34 -0
  52. package/dist/wrappers/VirtualDir.d.ts.map +1 -0
  53. package/dist/wrappers/VirtualDir.js +72 -0
  54. package/dist/wrappers/VirtualDir.js.map +1 -0
  55. package/dist/wrappers/VirtualDirent.d.ts +21 -0
  56. package/dist/wrappers/VirtualDirent.d.ts.map +1 -0
  57. package/dist/wrappers/VirtualDirent.js +50 -0
  58. package/dist/wrappers/VirtualDirent.js.map +1 -0
  59. package/example.js +95 -0
  60. package/example.ts +32 -0
  61. package/package.json +29 -0
  62. package/src/FSModule.ts +546 -0
  63. package/src/PathMapper.ts +102 -0
  64. package/src/PathModule.ts +142 -0
  65. package/src/ResourceTracker.ts +162 -0
  66. package/src/VirtualFileSystem.ts +172 -0
  67. package/src/index.ts +9 -0
  68. package/src/operations/newer.ts +223 -0
  69. package/src/operations/read.ts +319 -0
  70. package/src/operations/symlink.ts +31 -0
  71. package/src/operations/write.ts +189 -0
  72. package/src/utils/ErrorFilter.ts +57 -0
  73. package/src/utils/callbackify.ts +54 -0
  74. package/src/wrappers/VirtualDir.ts +84 -0
  75. package/src/wrappers/VirtualDirent.ts +60 -0
  76. package/test-data/example.txt +1 -0
  77. package/test-data/subdir/nested.txt +1 -0
  78. package/tsconfig.example.json +8 -0
  79. package/tsconfig.json +21 -0
@@ -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
+ }