sandbox-fs 1.0.0 → 1.1.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/README.md +16 -0
- package/dist/FSModule.d.ts.map +1 -1
- package/dist/FSModule.js +81 -18
- package/dist/FSModule.js.map +1 -1
- package/dist/PathMapper.d.ts.map +1 -1
- package/dist/PathMapper.js +147 -41
- package/dist/PathMapper.js.map +1 -1
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +27 -1
- package/dist/VirtualFileSystem.js.map +1 -1
- package/dist/operations/newer.d.ts.map +1 -1
- package/dist/operations/newer.js +32 -3
- package/dist/operations/newer.js.map +1 -1
- package/dist/operations/read.d.ts.map +1 -1
- package/dist/operations/read.js +100 -14
- package/dist/operations/read.js.map +1 -1
- package/dist/utils/ErrorFilter.d.ts.map +1 -1
- package/dist/utils/ErrorFilter.js +21 -24
- package/dist/utils/ErrorFilter.js.map +1 -1
- package/dist/wrappers/VirtualDir.d.ts.map +1 -1
- package/dist/wrappers/VirtualDir.js +36 -35
- package/dist/wrappers/VirtualDir.js.map +1 -1
- package/dist/wrappers/VirtualDirent.d.ts.map +1 -1
- package/dist/wrappers/VirtualDirent.js +28 -9
- package/dist/wrappers/VirtualDirent.js.map +1 -1
- package/package.json +8 -4
- package/dist/FSModule.d.ts +0 -153
- package/dist/PathMapper.d.ts +0 -30
- package/dist/PathModule.d.ts +0 -69
- package/dist/ResourceTracker.d.ts +0 -74
- package/dist/VirtualFileSystem.d.ts +0 -145
- package/dist/index.d.ts +0 -9
- package/dist/operations/newer.d.ts +0 -36
- package/dist/operations/read.d.ts +0 -24
- package/dist/operations/symlink.d.ts +0 -8
- package/dist/operations/write.d.ts +0 -29
- package/dist/utils/ErrorFilter.d.ts +0 -6
- package/dist/utils/callbackify.d.ts +0 -9
- package/dist/wrappers/VirtualDir.d.ts +0 -34
- package/dist/wrappers/VirtualDirent.d.ts +0 -21
- package/example.js +0 -95
- package/example.ts +0 -32
- package/src/FSModule.ts +0 -546
- package/src/PathMapper.ts +0 -102
- package/src/PathModule.ts +0 -142
- package/src/ResourceTracker.ts +0 -162
- package/src/VirtualFileSystem.ts +0 -172
- package/src/index.ts +0 -9
- package/src/operations/newer.ts +0 -223
- package/src/operations/read.ts +0 -319
- package/src/operations/symlink.ts +0 -31
- package/src/operations/write.ts +0 -189
- package/src/utils/ErrorFilter.ts +0 -57
- package/src/utils/callbackify.ts +0 -54
- package/src/wrappers/VirtualDir.ts +0 -84
- package/src/wrappers/VirtualDirent.ts +0 -60
- package/test-data/example.txt +0 -1
- package/test-data/subdir/nested.txt +0 -1
- package/tsconfig.example.json +0 -8
- package/tsconfig.json +0 -21
package/src/PathModule.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a path module that operates in virtual path space
|
|
5
|
-
* All paths use Unix-style forward slashes
|
|
6
|
-
* Fixed virtual CWD of '/'
|
|
7
|
-
*/
|
|
8
|
-
export function createPathModule() {
|
|
9
|
-
const virtualCwd = '/';
|
|
10
|
-
|
|
11
|
-
const pathModule = {
|
|
12
|
-
/**
|
|
13
|
-
* Normalize a path, reducing '..' and '.' parts
|
|
14
|
-
*/
|
|
15
|
-
normalize(p: string): string {
|
|
16
|
-
const normalized = path.posix.normalize(p);
|
|
17
|
-
// Ensure it starts with / if it's absolute-like
|
|
18
|
-
return normalized.startsWith('/') ? normalized : normalized;
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Join all path segments together and normalize the result
|
|
23
|
-
*/
|
|
24
|
-
join(...paths: string[]): string {
|
|
25
|
-
return path.posix.join(...paths);
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Resolve a sequence of paths to an absolute path
|
|
30
|
-
*/
|
|
31
|
-
resolve(...paths: string[]): string {
|
|
32
|
-
if (paths.length === 0) {
|
|
33
|
-
return virtualCwd;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let resolved = virtualCwd;
|
|
37
|
-
|
|
38
|
-
for (const p of paths) {
|
|
39
|
-
if (path.posix.isAbsolute(p)) {
|
|
40
|
-
resolved = p;
|
|
41
|
-
} else {
|
|
42
|
-
resolved = path.posix.join(resolved, p);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return path.posix.normalize(resolved);
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Return the relative path from 'from' to 'to'
|
|
51
|
-
*/
|
|
52
|
-
relative(from: string, to: string): string {
|
|
53
|
-
return path.posix.relative(from, to);
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Return the directory name of a path
|
|
58
|
-
*/
|
|
59
|
-
dirname(p: string): string {
|
|
60
|
-
return path.posix.dirname(p);
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Return the last portion of a path
|
|
65
|
-
*/
|
|
66
|
-
basename(p: string, ext?: string): string {
|
|
67
|
-
return path.posix.basename(p, ext);
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Return the extension of the path
|
|
72
|
-
*/
|
|
73
|
-
extname(p: string): string {
|
|
74
|
-
return path.posix.extname(p);
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Parse a path into an object
|
|
79
|
-
*/
|
|
80
|
-
parse(p: string): path.ParsedPath {
|
|
81
|
-
const parsed = path.posix.parse(p);
|
|
82
|
-
// Ensure root is always '/' for virtual paths
|
|
83
|
-
if (parsed.root) {
|
|
84
|
-
parsed.root = '/';
|
|
85
|
-
}
|
|
86
|
-
return parsed;
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Format a path object into a string
|
|
91
|
-
*/
|
|
92
|
-
format(pathObject: path.FormatInputPathObject): string {
|
|
93
|
-
// Ensure root is '/' for virtual paths
|
|
94
|
-
const virtualPathObject = { ...pathObject };
|
|
95
|
-
if (virtualPathObject.root) {
|
|
96
|
-
virtualPathObject.root = '/';
|
|
97
|
-
}
|
|
98
|
-
return path.posix.format(virtualPathObject);
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Check if a path is absolute
|
|
103
|
-
*/
|
|
104
|
-
isAbsolute(p: string): boolean {
|
|
105
|
-
return p.startsWith('/');
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Convert path to namespaced path (Windows-only, no-op in virtual space)
|
|
110
|
-
*/
|
|
111
|
-
toNamespacedPath(p: string): string {
|
|
112
|
-
// In virtual space, just return the path as-is
|
|
113
|
-
return p;
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Path segment separator (always / in virtual space)
|
|
118
|
-
*/
|
|
119
|
-
sep: '/',
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Path delimiter (always : in virtual space)
|
|
123
|
-
*/
|
|
124
|
-
delimiter: ':',
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* POSIX-specific methods (same as main module in virtual space)
|
|
128
|
-
*/
|
|
129
|
-
posix: null as any,
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Windows-specific methods (same as main module in virtual space)
|
|
133
|
-
*/
|
|
134
|
-
win32: null as any,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Both posix and win32 reference the same virtual implementation
|
|
138
|
-
pathModule.posix = pathModule;
|
|
139
|
-
pathModule.win32 = pathModule;
|
|
140
|
-
|
|
141
|
-
return pathModule;
|
|
142
|
-
}
|
package/src/ResourceTracker.ts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import { Readable, Writable } from 'stream';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Information about a tracked file descriptor
|
|
6
|
-
*/
|
|
7
|
-
export interface FDInfo {
|
|
8
|
-
fd: number;
|
|
9
|
-
virtualPath: string;
|
|
10
|
-
realPath: string;
|
|
11
|
-
flags: string | number;
|
|
12
|
-
mode?: number;
|
|
13
|
-
isStream: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Manages lifecycle of file descriptors and streams to prevent resource leaks
|
|
18
|
-
* and ensure proper cleanup when VFS is closed
|
|
19
|
-
*/
|
|
20
|
-
export class ResourceTracker {
|
|
21
|
-
private fdMap: Map<number, FDInfo> = new Map();
|
|
22
|
-
private streams: Set<Readable | Writable> = new Set();
|
|
23
|
-
private abortControllers: Set<AbortController> = new Set();
|
|
24
|
-
private _closed: boolean = false;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check if the VFS has been closed
|
|
28
|
-
*/
|
|
29
|
-
get closed(): boolean {
|
|
30
|
-
return this._closed;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Assert that the VFS is not closed, throwing EBADF if it is
|
|
35
|
-
*/
|
|
36
|
-
assertNotClosed(): void {
|
|
37
|
-
if (this._closed) {
|
|
38
|
-
const error = new Error('bad file descriptor') as NodeJS.ErrnoException;
|
|
39
|
-
error.code = 'EBADF';
|
|
40
|
-
error.errno = -9;
|
|
41
|
-
error.syscall = 'read';
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Track a file descriptor
|
|
48
|
-
*/
|
|
49
|
-
trackFD(fd: number, info: Omit<FDInfo, 'fd'>): void {
|
|
50
|
-
this.fdMap.set(fd, { fd, ...info });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Get information about a tracked file descriptor
|
|
55
|
-
*/
|
|
56
|
-
getFD(fd: number): FDInfo | undefined {
|
|
57
|
-
return this.fdMap.get(fd);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check if a file descriptor is tracked
|
|
62
|
-
*/
|
|
63
|
-
isTracked(fd: number): boolean {
|
|
64
|
-
return this.fdMap.has(fd);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Untrack a file descriptor (when closed)
|
|
69
|
-
*/
|
|
70
|
-
untrackFD(fd: number): FDInfo | undefined {
|
|
71
|
-
const info = this.fdMap.get(fd);
|
|
72
|
-
this.fdMap.delete(fd);
|
|
73
|
-
return info;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Track a stream
|
|
78
|
-
*/
|
|
79
|
-
trackStream(stream: Readable | Writable): void {
|
|
80
|
-
this.streams.add(stream);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Untrack a stream (when closed)
|
|
85
|
-
*/
|
|
86
|
-
untrackStream(stream: Readable | Writable): void {
|
|
87
|
-
this.streams.delete(stream);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Register an AbortSignal listener for cleanup
|
|
92
|
-
*/
|
|
93
|
-
registerAbortListener(signal: AbortSignal | undefined, cleanup: () => void): void {
|
|
94
|
-
if (!signal) return;
|
|
95
|
-
|
|
96
|
-
const controller = new AbortController();
|
|
97
|
-
this.abortControllers.add(controller);
|
|
98
|
-
|
|
99
|
-
signal.addEventListener('abort', () => {
|
|
100
|
-
cleanup();
|
|
101
|
-
this.abortControllers.delete(controller);
|
|
102
|
-
}, { once: true });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Dispose all tracked resources and mark as closed
|
|
107
|
-
*/
|
|
108
|
-
dispose(): void {
|
|
109
|
-
if (this._closed) return;
|
|
110
|
-
|
|
111
|
-
// Close all tracked file descriptors
|
|
112
|
-
for (const [fd, info] of this.fdMap) {
|
|
113
|
-
try {
|
|
114
|
-
if (!info.isStream) {
|
|
115
|
-
fs.closeSync(fd);
|
|
116
|
-
}
|
|
117
|
-
} catch (err) {
|
|
118
|
-
// Ignore errors during cleanup
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
this.fdMap.clear();
|
|
122
|
-
|
|
123
|
-
// Destroy all tracked streams
|
|
124
|
-
for (const stream of this.streams) {
|
|
125
|
-
try {
|
|
126
|
-
stream.destroy();
|
|
127
|
-
} catch (err) {
|
|
128
|
-
// Ignore errors during cleanup
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
this.streams.clear();
|
|
132
|
-
|
|
133
|
-
// Abort all pending operations
|
|
134
|
-
for (const controller of this.abortControllers) {
|
|
135
|
-
try {
|
|
136
|
-
controller.abort();
|
|
137
|
-
} catch (err) {
|
|
138
|
-
// Ignore errors during cleanup
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
this.abortControllers.clear();
|
|
142
|
-
|
|
143
|
-
this._closed = true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get all tracked file descriptors (for debugging)
|
|
148
|
-
*/
|
|
149
|
-
getAllFDs(): FDInfo[] {
|
|
150
|
-
return Array.from(this.fdMap.values());
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get count of tracked resources
|
|
155
|
-
*/
|
|
156
|
-
getResourceCount(): { fds: number; streams: number } {
|
|
157
|
-
return {
|
|
158
|
-
fds: this.fdMap.size,
|
|
159
|
-
streams: this.streams.size,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
package/src/VirtualFileSystem.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { PathMapper } from './PathMapper';
|
|
2
|
-
import { ResourceTracker } from './ResourceTracker';
|
|
3
|
-
import { createFSModule } from './FSModule';
|
|
4
|
-
import { createPathModule } from './PathModule';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Options for creating a VirtualFileSystem
|
|
8
|
-
*/
|
|
9
|
-
export interface VFSOptions {
|
|
10
|
-
/**
|
|
11
|
-
* The real filesystem path to use as the VFS root.
|
|
12
|
-
* Can be a Windows path (C:\data) or Unix path (/opt/data).
|
|
13
|
-
* All virtual paths will be mapped relative to this root.
|
|
14
|
-
*/
|
|
15
|
-
root: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A read-only virtual file system that maps Unix-style virtual paths
|
|
20
|
-
* to a real directory root.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```typescript
|
|
24
|
-
* const vfs = new VirtualFileSystem({ root: '/opt/data' });
|
|
25
|
-
* const fs = vfs.createNodeFSModule();
|
|
26
|
-
* const path = vfs.createNodePathModule();
|
|
27
|
-
*
|
|
28
|
-
* // Read a file (real path: /opt/data/file.txt)
|
|
29
|
-
* const content = fs.readFileSync('/file.txt', 'utf8');
|
|
30
|
-
*
|
|
31
|
-
* // Write operations are denied
|
|
32
|
-
* fs.writeFileSync('/file.txt', 'data'); // throws EACCES
|
|
33
|
-
*
|
|
34
|
-
* // Clean up when done
|
|
35
|
-
* vfs.close();
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
export class VirtualFileSystem {
|
|
39
|
-
private pathMapper: PathMapper;
|
|
40
|
-
private resourceTracker: ResourceTracker;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create a new VirtualFileSystem instance
|
|
44
|
-
*
|
|
45
|
-
* @param options - Configuration options
|
|
46
|
-
* @throws Error if root path doesn't exist or is not a directory
|
|
47
|
-
*/
|
|
48
|
-
constructor(options: VFSOptions) {
|
|
49
|
-
this.pathMapper = new PathMapper(options.root);
|
|
50
|
-
this.resourceTracker = new ResourceTracker();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Convert a virtual path to a real filesystem path
|
|
55
|
-
*
|
|
56
|
-
* @param virtualPath - Virtual path (Unix-style, starts with /)
|
|
57
|
-
* @returns Real filesystem path (platform-native)
|
|
58
|
-
* @throws Error if path traversal is detected
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```typescript
|
|
62
|
-
* const vfs = new VirtualFileSystem({ root: 'C:\\data' });
|
|
63
|
-
* vfs.toRealPath('/foo/bar.txt'); // Returns: C:\data\foo\bar.txt
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
toRealPath(virtualPath: string): string {
|
|
67
|
-
return this.pathMapper.toRealPath(virtualPath);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Convert a real filesystem path to a virtual path
|
|
72
|
-
*
|
|
73
|
-
* @param realPath - Real filesystem path (platform-native)
|
|
74
|
-
* @returns Virtual path (Unix-style, starts with /)
|
|
75
|
-
* @throws Error if real path is outside VFS root
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```typescript
|
|
79
|
-
* const vfs = new VirtualFileSystem({ root: 'C:\\data' });
|
|
80
|
-
* vfs.toVirtualPath('C:\\data\\foo\\bar.txt'); // Returns: /foo/bar.txt
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
toVirtualPath(realPath: string): string {
|
|
84
|
-
return this.pathMapper.toVirtualPath(realPath);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Create a Node.js fs-compatible module
|
|
89
|
-
*
|
|
90
|
-
* Returns an object that matches the Node.js fs module API.
|
|
91
|
-
* All read operations work normally, write operations return EACCES,
|
|
92
|
-
* and symlink operations return EACCES. Error messages have real paths
|
|
93
|
-
* filtered out and replaced with virtual paths.
|
|
94
|
-
*
|
|
95
|
-
* @returns An fs-compatible object with both callback and promise APIs
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```typescript
|
|
99
|
-
* const vfs = new VirtualFileSystem({ root: '/opt/data' });
|
|
100
|
-
* const fs = vfs.createNodeFSModule();
|
|
101
|
-
*
|
|
102
|
-
* // Callback API
|
|
103
|
-
* fs.readFile('/file.txt', 'utf8', (err, data) => {
|
|
104
|
-
* if (err) throw err;
|
|
105
|
-
* console.log(data);
|
|
106
|
-
* });
|
|
107
|
-
*
|
|
108
|
-
* // Promise API
|
|
109
|
-
* const data = await fs.promises.readFile('/file.txt', 'utf8');
|
|
110
|
-
*
|
|
111
|
-
* // Sync API
|
|
112
|
-
* const data = fs.readFileSync('/file.txt', 'utf8');
|
|
113
|
-
* ```
|
|
114
|
-
*/
|
|
115
|
-
createNodeFSModule(): any {
|
|
116
|
-
return createFSModule(this.pathMapper, this.resourceTracker);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Create a Node.js path-compatible module
|
|
121
|
-
*
|
|
122
|
-
* Returns an object that matches the Node.js path module API.
|
|
123
|
-
* All operations use Unix-style paths with forward slashes.
|
|
124
|
-
* The virtual current working directory is always '/'.
|
|
125
|
-
*
|
|
126
|
-
* @returns A path-compatible object
|
|
127
|
-
*
|
|
128
|
-
* @example
|
|
129
|
-
* ```typescript
|
|
130
|
-
* const vfs = new VirtualFileSystem({ root: '/opt/data' });
|
|
131
|
-
* const path = vfs.createNodePathModule();
|
|
132
|
-
*
|
|
133
|
-
* path.resolve('.'); // Returns: /
|
|
134
|
-
* path.resolve('foo/bar.txt'); // Returns: /foo/bar.txt
|
|
135
|
-
* path.join('/foo', 'bar'); // Returns: /foo/bar
|
|
136
|
-
* path.sep; // Returns: /
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
139
|
-
createNodePathModule(): any {
|
|
140
|
-
return createPathModule();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Close the VFS and release all resources
|
|
145
|
-
*
|
|
146
|
-
* Closes all tracked file descriptors, destroys all streams,
|
|
147
|
-
* and marks the VFS as closed. After calling close(), all
|
|
148
|
-
* subsequent fs operations will throw EBADF errors.
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```typescript
|
|
152
|
-
* const vfs = new VirtualFileSystem({ root: '/opt/data' });
|
|
153
|
-
* const fs = vfs.createNodeFSModule();
|
|
154
|
-
*
|
|
155
|
-
* const fd = fs.openSync('/file.txt', 'r');
|
|
156
|
-
*
|
|
157
|
-
* vfs.close(); // Closes fd and all other resources
|
|
158
|
-
*
|
|
159
|
-
* fs.readFileSync('/file.txt'); // Throws EBADF
|
|
160
|
-
* ```
|
|
161
|
-
*/
|
|
162
|
-
close(): void {
|
|
163
|
-
this.resourceTracker.dispose();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Check if the VFS has been closed
|
|
168
|
-
*/
|
|
169
|
-
get closed(): boolean {
|
|
170
|
-
return this.resourceTracker.closed;
|
|
171
|
-
}
|
|
172
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sandbox-fs - A read-only virtual file system for Node.js
|
|
3
|
-
*
|
|
4
|
-
* Maps Unix-style virtual paths to a real directory root while preventing
|
|
5
|
-
* write operations and filtering real paths from error messages.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { VirtualFileSystem, VirtualFileSystem as default } from './VirtualFileSystem';
|
|
9
|
-
export type { VFSOptions } from './VirtualFileSystem';
|
package/src/operations/newer.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import type { PathMapper } from '../PathMapper';
|
|
3
|
-
import type { ResourceTracker } from '../ResourceTracker';
|
|
4
|
-
import { filterError } from '../utils/ErrorFilter';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Wrap an async operation with error filtering and closed check
|
|
8
|
-
*/
|
|
9
|
-
async function wrapOperation<T>(
|
|
10
|
-
operation: () => Promise<T>,
|
|
11
|
-
pathMapper: PathMapper,
|
|
12
|
-
resourceTracker: ResourceTracker
|
|
13
|
-
): Promise<T> {
|
|
14
|
-
resourceTracker.assertNotClosed();
|
|
15
|
-
try {
|
|
16
|
-
return await operation();
|
|
17
|
-
} catch (error) {
|
|
18
|
-
throw filterError(error as Error, pathMapper);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Create access denied error for write operations
|
|
24
|
-
*/
|
|
25
|
-
function createEACCESError(syscall: string, path: string): NodeJS.ErrnoException {
|
|
26
|
-
const error = new Error(`${syscall}: operation not permitted, ${syscall} '${path}'`) as NodeJS.ErrnoException;
|
|
27
|
-
error.code = 'EACCES';
|
|
28
|
-
error.errno = -13;
|
|
29
|
-
error.syscall = syscall;
|
|
30
|
-
error.path = path;
|
|
31
|
-
return error;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* glob - Pattern matching (Node.js 22+)
|
|
36
|
-
*/
|
|
37
|
-
export function createGlobOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
38
|
-
return async function* (pattern: string, options?: any): AsyncIterableIterator<string> {
|
|
39
|
-
resourceTracker.assertNotClosed();
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
// Check if glob is available (Node.js 22+)
|
|
43
|
-
if (typeof (fs.promises as any).glob !== 'function') {
|
|
44
|
-
throw new Error('glob is not supported in this Node.js version');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Convert pattern to real path context
|
|
48
|
-
const realRoot = pathMapper.getRoot();
|
|
49
|
-
|
|
50
|
-
if (options?.signal) {
|
|
51
|
-
resourceTracker.registerAbortListener(options.signal, () => {});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Execute glob in real path context
|
|
55
|
-
const realOptions = { ...options, cwd: realRoot };
|
|
56
|
-
const globIterator = (fs.promises as any).glob(pattern, realOptions);
|
|
57
|
-
|
|
58
|
-
for await (const realPath of globIterator) {
|
|
59
|
-
try {
|
|
60
|
-
// Convert real path back to virtual path
|
|
61
|
-
const virtualPath = pathMapper.toVirtualPath(realPath);
|
|
62
|
-
yield virtualPath;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
// Skip paths that can't be converted (outside VFS root)
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch (error) {
|
|
69
|
-
throw filterError(error as Error, pathMapper);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* statfs - File system statistics (Node.js 19+)
|
|
76
|
-
*/
|
|
77
|
-
export function createStatfsOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
78
|
-
return async (virtualPath: string, options?: any): Promise<fs.StatsFsBase<any>> => {
|
|
79
|
-
return wrapOperation(async () => {
|
|
80
|
-
// Check if statfs is available (Node.js 19+)
|
|
81
|
-
if (typeof (fs.promises as any).statfs !== 'function') {
|
|
82
|
-
throw new Error('statfs is not supported in this Node.js version');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const realPath = pathMapper.toRealPath(virtualPath);
|
|
86
|
-
|
|
87
|
-
if (options?.signal) {
|
|
88
|
-
resourceTracker.registerAbortListener(options.signal, () => {});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (fs.promises as any).statfs(realPath, options);
|
|
92
|
-
}, pathMapper, resourceTracker);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* cp - Copy files/directories (read source only, write dest is denied)
|
|
98
|
-
*/
|
|
99
|
-
export function createCpOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
100
|
-
return async (source: string, destination: string, options?: any): Promise<void> => {
|
|
101
|
-
// Always deny - this is a write operation
|
|
102
|
-
throw createEACCESError('cp', destination);
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* openAsBlob - Open file as Blob (Node.js 19+)
|
|
108
|
-
*/
|
|
109
|
-
export function createOpenAsBlobOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
110
|
-
return async (virtualPath: string, options?: any): Promise<Blob> => {
|
|
111
|
-
return wrapOperation(async () => {
|
|
112
|
-
// Check if openAsBlob is available (Node.js 19+)
|
|
113
|
-
if (typeof (fs as any).openAsBlob !== 'function') {
|
|
114
|
-
throw new Error('openAsBlob is not supported in this Node.js version');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const realPath = pathMapper.toRealPath(virtualPath);
|
|
118
|
-
|
|
119
|
-
if (options?.signal) {
|
|
120
|
-
resourceTracker.registerAbortListener(options.signal, () => {});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return (fs as any).openAsBlob(realPath, options);
|
|
124
|
-
}, pathMapper, resourceTracker);
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* readv - Vector read (Node.js 13+)
|
|
130
|
-
*/
|
|
131
|
-
export function createReadvOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
132
|
-
return async (fd: number, buffers: NodeJS.ArrayBufferView[], position?: number): Promise<number> => {
|
|
133
|
-
return wrapOperation(async () => {
|
|
134
|
-
if (!resourceTracker.isTracked(fd)) {
|
|
135
|
-
const error = new Error('bad file descriptor') as NodeJS.ErrnoException;
|
|
136
|
-
error.code = 'EBADF';
|
|
137
|
-
error.errno = -9;
|
|
138
|
-
error.syscall = 'readv';
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check if readv is available
|
|
143
|
-
if (typeof (fs.promises as any).readv !== 'function') {
|
|
144
|
-
throw new Error('readv is not supported in this Node.js version');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return (fs.promises as any).readv(fd, buffers, position);
|
|
148
|
-
}, pathMapper, resourceTracker);
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* watch - Watch for file changes
|
|
154
|
-
*/
|
|
155
|
-
export function createWatchOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
156
|
-
return (virtualPath: string, options?: any): fs.FSWatcher => {
|
|
157
|
-
resourceTracker.assertNotClosed();
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const realPath = pathMapper.toRealPath(virtualPath);
|
|
161
|
-
const watcher = fs.watch(realPath, options);
|
|
162
|
-
|
|
163
|
-
// Wrap error events to filter paths
|
|
164
|
-
const originalOn = watcher.on.bind(watcher);
|
|
165
|
-
watcher.on = function(event: string, listener: any) {
|
|
166
|
-
if (event === 'error') {
|
|
167
|
-
const wrappedListener = (error: Error) => {
|
|
168
|
-
listener(filterError(error, pathMapper));
|
|
169
|
-
};
|
|
170
|
-
return originalOn(event, wrappedListener);
|
|
171
|
-
}
|
|
172
|
-
return originalOn(event, listener);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
return watcher;
|
|
176
|
-
} catch (error) {
|
|
177
|
-
throw filterError(error as Error, pathMapper);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* watchFile - Watch a file for changes
|
|
184
|
-
*/
|
|
185
|
-
export function createWatchFileOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
186
|
-
return (virtualPath: string, options: any, listener?: any): void => {
|
|
187
|
-
resourceTracker.assertNotClosed();
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const realPath = pathMapper.toRealPath(virtualPath);
|
|
191
|
-
|
|
192
|
-
// Handle both signatures: (path, listener) and (path, options, listener)
|
|
193
|
-
if (typeof options === 'function') {
|
|
194
|
-
fs.watchFile(realPath, options);
|
|
195
|
-
} else {
|
|
196
|
-
fs.watchFile(realPath, options, listener);
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
throw filterError(error as Error, pathMapper);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* unwatchFile - Stop watching a file
|
|
206
|
-
*/
|
|
207
|
-
export function createUnwatchFileOperation(pathMapper: PathMapper, resourceTracker: ResourceTracker) {
|
|
208
|
-
return (virtualPath: string, listener?: any): void => {
|
|
209
|
-
resourceTracker.assertNotClosed();
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const realPath = pathMapper.toRealPath(virtualPath);
|
|
213
|
-
|
|
214
|
-
if (listener) {
|
|
215
|
-
fs.unwatchFile(realPath, listener);
|
|
216
|
-
} else {
|
|
217
|
-
fs.unwatchFile(realPath);
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
throw filterError(error as Error, pathMapper);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|