@secure-exec/core 0.2.0-rc.2 → 0.2.1-rc.1
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/dist/generated/isolate-runtime.d.ts +1 -1
- package/dist/generated/isolate-runtime.js +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +145 -7
- package/dist/kernel/device-backend.d.ts +14 -0
- package/dist/kernel/device-backend.js +251 -0
- package/dist/kernel/device-layer.js +9 -0
- package/dist/kernel/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -119
- package/dist/kernel/mount-table.d.ts +75 -0
- package/dist/kernel/mount-table.js +353 -0
- package/dist/kernel/permissions.d.ts +9 -0
- package/dist/kernel/permissions.js +33 -1
- package/dist/kernel/proc-backend.d.ts +30 -0
- package/dist/kernel/proc-backend.js +428 -0
- package/dist/kernel/proc-layer.js +6 -0
- package/dist/kernel/process-table.d.ts +3 -1
- package/dist/kernel/process-table.js +23 -3
- package/dist/kernel/pty.d.ts +3 -2
- package/dist/kernel/pty.js +13 -2
- package/dist/kernel/types.d.ts +45 -4
- package/dist/kernel/types.js +9 -0
- package/dist/kernel/vfs.d.ts +30 -2
- package/dist/kernel/vfs.js +19 -2
- package/dist/shared/api-types.d.ts +6 -0
- package/dist/shared/console-formatter.js +8 -8
- package/dist/shared/in-memory-fs.d.ts +14 -62
- package/dist/shared/in-memory-fs.js +101 -636
- package/dist/shared/permissions.js +5 -0
- package/dist/test/block-store-conformance.d.ts +34 -0
- package/dist/test/block-store-conformance.js +251 -0
- package/dist/test/metadata-store-conformance.d.ts +37 -0
- package/dist/test/metadata-store-conformance.js +646 -0
- package/dist/test/vfs-conformance.d.ts +65 -0
- package/dist/test/vfs-conformance.js +842 -0
- package/dist/types.d.ts +1 -0
- package/dist/vfs/chunked-vfs.d.ts +66 -0
- package/dist/vfs/chunked-vfs.js +1290 -0
- package/dist/vfs/host-block-store.d.ts +19 -0
- package/dist/vfs/host-block-store.js +97 -0
- package/dist/vfs/memory-block-store.d.ts +16 -0
- package/dist/vfs/memory-block-store.js +45 -0
- package/dist/vfs/memory-metadata.d.ts +75 -0
- package/dist/vfs/memory-metadata.js +528 -0
- package/dist/vfs/sqlite-metadata.d.ts +91 -0
- package/dist/vfs/sqlite-metadata.js +582 -0
- package/dist/vfs/types.d.ts +210 -0
- package/dist/vfs/types.js +8 -0
- package/package.json +20 -1
- package/dist/kernel/inode-table.d.ts +0 -43
- package/dist/kernel/inode-table.js +0 -85
|
@@ -1,650 +1,115 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating an in-memory VirtualFileSystem backed by ChunkedVFS.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old monolithic InMemoryFileSystem with
|
|
5
|
+
* ChunkedVFS(InMemoryMetadataStore + InMemoryBlockStore).
|
|
6
|
+
*/
|
|
2
7
|
import { KernelError, O_CREAT, O_EXCL, O_TRUNC } from "../kernel/types.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const S_IFSOCK = 0o140000;
|
|
7
|
-
function normalizePath(path) {
|
|
8
|
-
if (!path)
|
|
9
|
-
return "/";
|
|
10
|
-
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
11
|
-
normalized = normalized.replace(/\/+/g, "/");
|
|
12
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
13
|
-
normalized = normalized.slice(0, -1);
|
|
14
|
-
}
|
|
15
|
-
return normalized;
|
|
16
|
-
}
|
|
17
|
-
function splitPath(path) {
|
|
18
|
-
const normalized = normalizePath(path);
|
|
19
|
-
return normalized === "/" ? [] : normalized.slice(1).split("/");
|
|
20
|
-
}
|
|
21
|
-
function dirname(path) {
|
|
22
|
-
const parts = splitPath(path);
|
|
23
|
-
if (parts.length <= 1)
|
|
24
|
-
return "/";
|
|
25
|
-
return `/${parts.slice(0, -1).join("/")}`;
|
|
26
|
-
}
|
|
8
|
+
import { createChunkedVfs } from "../vfs/chunked-vfs.js";
|
|
9
|
+
import { InMemoryMetadataStore } from "../vfs/memory-metadata.js";
|
|
10
|
+
import { InMemoryBlockStore } from "../vfs/memory-block-store.js";
|
|
27
11
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
12
|
+
* Create an in-memory VirtualFileSystem using the chunked storage architecture.
|
|
13
|
+
*
|
|
14
|
+
* The returned VFS stores all data in memory via InMemoryMetadataStore and
|
|
15
|
+
* InMemoryBlockStore, composed through ChunkedVFS. It also includes a
|
|
16
|
+
* synchronous `prepareOpenSync` method used by the kernel for O_CREAT/O_EXCL/O_TRUNC
|
|
17
|
+
* handling during fdOpen.
|
|
31
18
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.dirs.set("/", this.allocateDirectoryInode().ino);
|
|
41
|
-
}
|
|
42
|
-
// Rebind the filesystem to the kernel's shared inode table.
|
|
43
|
-
setInodeTable(inodeTable) {
|
|
44
|
-
if (this.inodeTable === inodeTable)
|
|
45
|
-
return;
|
|
46
|
-
const oldTable = this.inodeTable;
|
|
47
|
-
this.inodeTable = inodeTable;
|
|
48
|
-
this.reindexInodes(oldTable);
|
|
49
|
-
}
|
|
50
|
-
getInodeForPath(path) {
|
|
51
|
-
const normalized = normalizePath(path);
|
|
52
|
-
const resolved = this.resolveSymlink(normalized);
|
|
53
|
-
return this.files.get(resolved) ?? this.dirs.get(resolved) ?? null;
|
|
54
|
-
}
|
|
55
|
-
readFileByInode(ino) {
|
|
56
|
-
const data = this.fileContents.get(ino);
|
|
57
|
-
if (!data) {
|
|
58
|
-
throw new Error(`ENOENT: inode ${ino} has no file data`);
|
|
59
|
-
}
|
|
60
|
-
this.requireInode(ino).atime = new Date();
|
|
61
|
-
return data;
|
|
62
|
-
}
|
|
63
|
-
writeFileByInode(ino, content) {
|
|
64
|
-
this.requireFileInode(ino);
|
|
65
|
-
this.fileContents.set(ino, content);
|
|
66
|
-
this.updateFileMetadata(ino, content.byteLength);
|
|
67
|
-
}
|
|
68
|
-
preadByInode(ino, offset, length) {
|
|
69
|
-
const data = this.readFileByInode(ino);
|
|
70
|
-
return data.slice(offset, offset + length);
|
|
71
|
-
}
|
|
72
|
-
statByInode(ino) {
|
|
73
|
-
return this.statForInode(this.requireInode(ino));
|
|
74
|
-
}
|
|
75
|
-
deleteInodeData(ino) {
|
|
76
|
-
this.fileContents.delete(ino);
|
|
77
|
-
}
|
|
78
|
-
listDirEntries(path) {
|
|
79
|
-
const normalized = normalizePath(path);
|
|
80
|
-
const dirIno = this.dirs.get(normalized);
|
|
81
|
-
if (dirIno === undefined) {
|
|
82
|
-
throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);
|
|
83
|
-
}
|
|
84
|
-
const prefix = normalized === "/" ? "/" : `${normalized}/`;
|
|
85
|
-
const entries = new Map();
|
|
86
|
-
const parentPath = normalized === "/" ? "/" : dirname(normalized);
|
|
87
|
-
const parentIno = this.dirs.get(parentPath) ?? dirIno;
|
|
88
|
-
entries.set(".", {
|
|
89
|
-
name: ".",
|
|
90
|
-
isDirectory: true,
|
|
91
|
-
isSymbolicLink: false,
|
|
92
|
-
ino: dirIno,
|
|
93
|
-
});
|
|
94
|
-
entries.set("..", {
|
|
95
|
-
name: "..",
|
|
96
|
-
isDirectory: true,
|
|
97
|
-
isSymbolicLink: false,
|
|
98
|
-
ino: parentIno,
|
|
99
|
-
});
|
|
100
|
-
for (const [filePath, ino] of this.files.entries()) {
|
|
101
|
-
if (!filePath.startsWith(prefix))
|
|
102
|
-
continue;
|
|
103
|
-
const rest = filePath.slice(prefix.length);
|
|
104
|
-
if (rest && !rest.includes("/")) {
|
|
105
|
-
entries.set(rest, {
|
|
106
|
-
name: rest,
|
|
107
|
-
isDirectory: false,
|
|
108
|
-
isSymbolicLink: false,
|
|
109
|
-
ino,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const [dirPath, ino] of this.dirs.entries()) {
|
|
114
|
-
if (!dirPath.startsWith(prefix))
|
|
115
|
-
continue;
|
|
116
|
-
const rest = dirPath.slice(prefix.length);
|
|
117
|
-
if (rest && !rest.includes("/")) {
|
|
118
|
-
entries.set(rest, {
|
|
119
|
-
name: rest,
|
|
120
|
-
isDirectory: true,
|
|
121
|
-
isSymbolicLink: false,
|
|
122
|
-
ino,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
for (const [linkPath, link] of this.symlinks.entries()) {
|
|
127
|
-
if (!linkPath.startsWith(prefix))
|
|
128
|
-
continue;
|
|
129
|
-
const rest = linkPath.slice(prefix.length);
|
|
130
|
-
if (rest && !rest.includes("/")) {
|
|
131
|
-
entries.set(rest, {
|
|
132
|
-
name: rest,
|
|
133
|
-
isDirectory: false,
|
|
134
|
-
isSymbolicLink: true,
|
|
135
|
-
ino: link.ino,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return Array.from(entries.values());
|
|
140
|
-
}
|
|
141
|
-
async readFile(path) {
|
|
142
|
-
const normalized = normalizePath(path);
|
|
143
|
-
const resolved = this.resolveSymlink(normalized);
|
|
144
|
-
const ino = this.files.get(resolved);
|
|
145
|
-
if (ino === undefined) {
|
|
146
|
-
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
147
|
-
}
|
|
148
|
-
return this.readFileByInode(ino);
|
|
149
|
-
}
|
|
150
|
-
async readTextFile(path) {
|
|
151
|
-
const data = await this.readFile(path);
|
|
152
|
-
return new TextDecoder().decode(data);
|
|
153
|
-
}
|
|
154
|
-
async readDir(path) {
|
|
155
|
-
return this.listDirEntries(path).map((entry) => entry.name);
|
|
156
|
-
}
|
|
157
|
-
async readDirWithTypes(path) {
|
|
158
|
-
return this.listDirEntries(path);
|
|
159
|
-
}
|
|
160
|
-
async writeFile(path, content) {
|
|
161
|
-
const normalized = normalizePath(path);
|
|
162
|
-
await this.mkdir(dirname(normalized));
|
|
163
|
-
const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
164
|
-
const resolved = this.resolveIfSymlink(normalized) ?? normalized;
|
|
165
|
-
const existing = this.files.get(resolved);
|
|
166
|
-
if (existing !== undefined) {
|
|
167
|
-
this.writeFileByInode(existing, data);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const inode = this.allocateFileInode();
|
|
171
|
-
this.files.set(resolved, inode.ino);
|
|
172
|
-
this.fileContents.set(inode.ino, data);
|
|
173
|
-
this.updateFileMetadata(inode.ino, data.byteLength);
|
|
174
|
-
}
|
|
175
|
-
prepareOpenSync(path, flags) {
|
|
176
|
-
const normalized = normalizePath(path);
|
|
177
|
-
const resolved = this.resolveIfSymlink(normalized) ?? normalized;
|
|
19
|
+
export function createInMemoryFileSystem() {
|
|
20
|
+
const metadata = new InMemoryMetadataStore();
|
|
21
|
+
const blocks = new InMemoryBlockStore();
|
|
22
|
+
const vfs = createChunkedVfs({ metadata, blocks });
|
|
23
|
+
// The kernel's fdOpen calls prepareOpenSync synchronously for O_CREAT,
|
|
24
|
+
// O_EXCL, and O_TRUNC flags. Since InMemoryMetadataStore is backed by
|
|
25
|
+
// synchronous Maps, we use its synchronous accessor methods directly.
|
|
26
|
+
function prepareOpenSync(path, flags) {
|
|
178
27
|
const hasCreate = (flags & O_CREAT) !== 0;
|
|
179
28
|
const hasExcl = (flags & O_EXCL) !== 0;
|
|
180
29
|
const hasTrunc = (flags & O_TRUNC) !== 0;
|
|
181
|
-
|
|
182
|
-
|
|
30
|
+
// Check if path exists via synchronous resolution.
|
|
31
|
+
let resolvedIno;
|
|
32
|
+
try {
|
|
33
|
+
resolvedIno = metadata.resolvePathSync(path);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// ENOENT is expected when the file doesn't exist yet.
|
|
37
|
+
}
|
|
38
|
+
const exists = resolvedIno !== undefined;
|
|
183
39
|
if (hasCreate && hasExcl && exists) {
|
|
184
|
-
throw new KernelError("EEXIST", `file already exists, open '${
|
|
40
|
+
throw new KernelError("EEXIST", `file already exists, open '${path}'`);
|
|
185
41
|
}
|
|
186
42
|
let created = false;
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
43
|
+
if (!exists && hasCreate) {
|
|
44
|
+
// Create parent directories and the file synchronously.
|
|
45
|
+
const parts = path.replace(/\/+/g, "/").replace(/\/$/, "").split("/").filter(Boolean);
|
|
46
|
+
let parentIno = 1; // root
|
|
47
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
48
|
+
const childIno = metadata.lookupSync(parentIno, parts[i]);
|
|
49
|
+
if (childIno === null) {
|
|
50
|
+
const newIno = metadata.createInodeSync({
|
|
51
|
+
type: "directory",
|
|
52
|
+
mode: 0o755,
|
|
53
|
+
uid: 0,
|
|
54
|
+
gid: 0,
|
|
55
|
+
});
|
|
56
|
+
metadata.updateInodeSync(newIno, { nlink: 2 });
|
|
57
|
+
metadata.createDentrySync(parentIno, parts[i], newIno, "directory");
|
|
58
|
+
// Increment parent nlink for subdirectory
|
|
59
|
+
const parentMeta = metadata.getInodeSync(parentIno);
|
|
60
|
+
if (parentMeta) {
|
|
61
|
+
metadata.updateInodeSync(parentIno, { nlink: parentMeta.nlink + 1 });
|
|
62
|
+
}
|
|
63
|
+
parentIno = newIno;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
parentIno = childIno;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Create the file inode.
|
|
70
|
+
const fileName = parts[parts.length - 1];
|
|
71
|
+
if (fileName) {
|
|
72
|
+
const fileIno = metadata.createInodeSync({
|
|
73
|
+
type: "file",
|
|
74
|
+
mode: 0o644,
|
|
75
|
+
uid: 0,
|
|
76
|
+
gid: 0,
|
|
77
|
+
});
|
|
78
|
+
metadata.updateInodeSync(fileIno, {
|
|
79
|
+
nlink: 1,
|
|
80
|
+
size: 0,
|
|
81
|
+
storageMode: "inline",
|
|
82
|
+
inlineContent: new Uint8Array(0),
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
metadata.createDentrySync(parentIno, fileName, fileIno, "file");
|
|
86
|
+
created = true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// EEXIST from race condition, ignore.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (hasTrunc && resolvedIno !== undefined) {
|
|
94
|
+
// Check that the target is a file, not a directory.
|
|
95
|
+
const meta = metadata.getInodeSync(resolvedIno);
|
|
96
|
+
if (meta && meta.type === "directory") {
|
|
97
|
+
throw new KernelError("EISDIR", `illegal operation on a directory, open '${path}'`);
|
|
98
|
+
}
|
|
99
|
+
// Truncate file to 0 bytes.
|
|
100
|
+
metadata.updateInodeSync(resolvedIno, {
|
|
101
|
+
size: 0,
|
|
102
|
+
storageMode: "inline",
|
|
103
|
+
inlineContent: new Uint8Array(0),
|
|
104
|
+
});
|
|
105
|
+
// Delete any existing chunks synchronously.
|
|
106
|
+
const keys = metadata.deleteAllChunksSync(resolvedIno);
|
|
107
|
+
if (keys.length > 0) {
|
|
108
|
+
// Fire-and-forget async block deletion. Blocks are in memory so this resolves immediately.
|
|
109
|
+
void blocks.deleteMany(keys);
|
|
207
110
|
}
|
|
208
|
-
this.fileContents.set(truncateIno, new Uint8Array(0));
|
|
209
|
-
this.updateFileMetadata(truncateIno, 0);
|
|
210
111
|
}
|
|
211
112
|
return created;
|
|
212
113
|
}
|
|
213
|
-
|
|
214
|
-
const normalized = normalizePath(path);
|
|
215
|
-
const parent = dirname(normalized);
|
|
216
|
-
if (!this.dirs.has(parent)) {
|
|
217
|
-
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
218
|
-
}
|
|
219
|
-
this.ensureDirectory(normalized);
|
|
220
|
-
}
|
|
221
|
-
async mkdir(path, _options) {
|
|
222
|
-
const parts = splitPath(path);
|
|
223
|
-
let current = "";
|
|
224
|
-
for (const part of parts) {
|
|
225
|
-
current += `/${part}`;
|
|
226
|
-
this.ensureDirectory(current);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
resolveIfSymlink(normalized) {
|
|
230
|
-
return this.symlinks.has(normalized) ? this.resolveSymlink(normalized) : null;
|
|
231
|
-
}
|
|
232
|
-
resolveSymlink(normalized, maxDepth = 16) {
|
|
233
|
-
let current = normalized;
|
|
234
|
-
for (let i = 0; i < maxDepth; i++) {
|
|
235
|
-
const link = this.symlinks.get(current);
|
|
236
|
-
if (!link)
|
|
237
|
-
return current;
|
|
238
|
-
current = link.target.startsWith("/")
|
|
239
|
-
? normalizePath(link.target)
|
|
240
|
-
: normalizePath(`${dirname(current)}/${link.target}`);
|
|
241
|
-
}
|
|
242
|
-
throw new Error(`ELOOP: too many levels of symbolic links, stat '${normalized}'`);
|
|
243
|
-
}
|
|
244
|
-
statForInode(inode) {
|
|
245
|
-
const isDirectory = (inode.mode & 0o170000) === S_IFDIR;
|
|
246
|
-
const isSymbolicLink = (inode.mode & 0o170000) === S_IFLNK;
|
|
247
|
-
return {
|
|
248
|
-
mode: inode.mode,
|
|
249
|
-
size: isDirectory ? 4096 : inode.size,
|
|
250
|
-
isDirectory,
|
|
251
|
-
isSymbolicLink,
|
|
252
|
-
atimeMs: inode.atime.getTime(),
|
|
253
|
-
mtimeMs: inode.mtime.getTime(),
|
|
254
|
-
ctimeMs: inode.ctime.getTime(),
|
|
255
|
-
birthtimeMs: inode.birthtime.getTime(),
|
|
256
|
-
ino: inode.ino,
|
|
257
|
-
nlink: inode.nlink,
|
|
258
|
-
uid: inode.uid,
|
|
259
|
-
gid: inode.gid,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
statEntry(normalized) {
|
|
263
|
-
const fileIno = this.files.get(normalized);
|
|
264
|
-
if (fileIno !== undefined) {
|
|
265
|
-
return this.statByInode(fileIno);
|
|
266
|
-
}
|
|
267
|
-
const dirIno = this.dirs.get(normalized);
|
|
268
|
-
if (dirIno !== undefined) {
|
|
269
|
-
return this.statByInode(dirIno);
|
|
270
|
-
}
|
|
271
|
-
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
|
|
272
|
-
}
|
|
273
|
-
async exists(path) {
|
|
274
|
-
const normalized = normalizePath(path);
|
|
275
|
-
if (this.symlinks.has(normalized)) {
|
|
276
|
-
try {
|
|
277
|
-
const resolved = this.resolveSymlink(normalized);
|
|
278
|
-
return this.files.has(resolved) || this.dirs.has(resolved);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return this.files.has(normalized) || this.dirs.has(normalized);
|
|
285
|
-
}
|
|
286
|
-
async stat(path) {
|
|
287
|
-
const normalized = normalizePath(path);
|
|
288
|
-
const resolved = this.resolveSymlink(normalized);
|
|
289
|
-
return this.statEntry(resolved);
|
|
290
|
-
}
|
|
291
|
-
async removeFile(path) {
|
|
292
|
-
const normalized = normalizePath(path);
|
|
293
|
-
const symlink = this.symlinks.get(normalized);
|
|
294
|
-
if (symlink) {
|
|
295
|
-
this.symlinks.delete(normalized);
|
|
296
|
-
this.inodeTable.decrementLinks(symlink.ino);
|
|
297
|
-
if (this.inodeTable.shouldDelete(symlink.ino)) {
|
|
298
|
-
this.inodeTable.delete(symlink.ino);
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const resolved = this.resolveSymlink(normalized);
|
|
303
|
-
const ino = this.files.get(resolved);
|
|
304
|
-
if (ino === undefined) {
|
|
305
|
-
throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);
|
|
306
|
-
}
|
|
307
|
-
this.files.delete(resolved);
|
|
308
|
-
this.inodeTable.decrementLinks(ino);
|
|
309
|
-
if (this.inodeTable.shouldDelete(ino)) {
|
|
310
|
-
this.deleteInodeData(ino);
|
|
311
|
-
this.inodeTable.delete(ino);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async removeDir(path) {
|
|
315
|
-
const normalized = normalizePath(path);
|
|
316
|
-
if (normalized === "/") {
|
|
317
|
-
throw new Error("EPERM: operation not permitted, rmdir '/'");
|
|
318
|
-
}
|
|
319
|
-
if (!this.dirs.has(normalized)) {
|
|
320
|
-
throw new Error(`ENOENT: no such file or directory, rmdir '${normalized}'`);
|
|
321
|
-
}
|
|
322
|
-
const prefix = normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
323
|
-
for (const filePath of this.files.keys()) {
|
|
324
|
-
if (filePath.startsWith(prefix)) {
|
|
325
|
-
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
for (const dirPath of this.dirs.keys()) {
|
|
329
|
-
if (dirPath !== normalized && dirPath.startsWith(prefix)) {
|
|
330
|
-
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
for (const linkPath of this.symlinks.keys()) {
|
|
334
|
-
if (linkPath.startsWith(prefix)) {
|
|
335
|
-
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const ino = this.dirs.get(normalized);
|
|
339
|
-
this.dirs.delete(normalized);
|
|
340
|
-
this.inodeTable.decrementLinks(ino);
|
|
341
|
-
this.inodeTable.decrementLinks(ino);
|
|
342
|
-
this.adjustParentDirectoryLinkCount(normalized, -1);
|
|
343
|
-
if (this.inodeTable.shouldDelete(ino)) {
|
|
344
|
-
this.inodeTable.delete(ino);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
async rename(oldPath, newPath) {
|
|
348
|
-
const oldNormalized = normalizePath(oldPath);
|
|
349
|
-
const newNormalized = normalizePath(newPath);
|
|
350
|
-
if (oldNormalized === newNormalized) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (!this.dirs.has(dirname(newNormalized))) {
|
|
354
|
-
throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
355
|
-
}
|
|
356
|
-
if (this.files.has(oldNormalized)) {
|
|
357
|
-
if (this.dirs.has(newNormalized)) {
|
|
358
|
-
throw new Error(`EISDIR: illegal operation on a directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
359
|
-
}
|
|
360
|
-
if (this.files.has(newNormalized) || this.symlinks.has(newNormalized)) {
|
|
361
|
-
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
362
|
-
}
|
|
363
|
-
const ino = this.files.get(oldNormalized);
|
|
364
|
-
this.files.delete(oldNormalized);
|
|
365
|
-
this.files.set(newNormalized, ino);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (this.symlinks.has(oldNormalized)) {
|
|
369
|
-
if (this.files.has(newNormalized) ||
|
|
370
|
-
this.dirs.has(newNormalized) ||
|
|
371
|
-
this.symlinks.has(newNormalized)) {
|
|
372
|
-
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
373
|
-
}
|
|
374
|
-
const target = this.symlinks.get(oldNormalized);
|
|
375
|
-
this.symlinks.delete(oldNormalized);
|
|
376
|
-
this.symlinks.set(newNormalized, target);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (!this.dirs.has(oldNormalized)) {
|
|
380
|
-
throw new Error(`ENOENT: no such file or directory, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
381
|
-
}
|
|
382
|
-
if (oldNormalized === "/") {
|
|
383
|
-
throw new Error(`EPERM: operation not permitted, rename '${oldNormalized}'`);
|
|
384
|
-
}
|
|
385
|
-
if (newNormalized.startsWith(`${oldNormalized}/`)) {
|
|
386
|
-
throw new Error(`EINVAL: invalid argument, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
387
|
-
}
|
|
388
|
-
if (this.dirs.has(newNormalized) ||
|
|
389
|
-
this.files.has(newNormalized) ||
|
|
390
|
-
this.symlinks.has(newNormalized)) {
|
|
391
|
-
throw new Error(`EEXIST: file already exists, rename '${oldNormalized}' -> '${newNormalized}'`);
|
|
392
|
-
}
|
|
393
|
-
const sourcePrefix = `${oldNormalized}/`;
|
|
394
|
-
const targetPrefix = `${newNormalized}/`;
|
|
395
|
-
const dirEntries = Array.from(this.dirs.entries())
|
|
396
|
-
.filter(([path]) => path === oldNormalized || path.startsWith(sourcePrefix))
|
|
397
|
-
.sort(([a], [b]) => a.length - b.length);
|
|
398
|
-
const fileEntries = Array.from(this.files.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
399
|
-
const symlinkEntries = Array.from(this.symlinks.entries()).filter(([path]) => path.startsWith(sourcePrefix));
|
|
400
|
-
for (const [path] of dirEntries)
|
|
401
|
-
this.dirs.delete(path);
|
|
402
|
-
for (const [path] of fileEntries)
|
|
403
|
-
this.files.delete(path);
|
|
404
|
-
for (const [path] of symlinkEntries)
|
|
405
|
-
this.symlinks.delete(path);
|
|
406
|
-
for (const [path, ino] of dirEntries) {
|
|
407
|
-
const nextPath = path === oldNormalized
|
|
408
|
-
? newNormalized
|
|
409
|
-
: `${targetPrefix}${path.slice(sourcePrefix.length)}`;
|
|
410
|
-
this.dirs.set(nextPath, ino);
|
|
411
|
-
}
|
|
412
|
-
for (const [path, ino] of fileEntries) {
|
|
413
|
-
this.files.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, ino);
|
|
414
|
-
}
|
|
415
|
-
for (const [path, target] of symlinkEntries) {
|
|
416
|
-
this.symlinks.set(`${targetPrefix}${path.slice(sourcePrefix.length)}`, target);
|
|
417
|
-
}
|
|
418
|
-
if (dirname(oldNormalized) !== dirname(newNormalized)) {
|
|
419
|
-
this.adjustParentDirectoryLinkCount(oldNormalized, -1);
|
|
420
|
-
this.adjustParentDirectoryLinkCount(newNormalized, 1);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
async symlink(target, linkPath) {
|
|
424
|
-
const normalized = normalizePath(linkPath);
|
|
425
|
-
if (this.files.has(normalized) ||
|
|
426
|
-
this.dirs.has(normalized) ||
|
|
427
|
-
this.symlinks.has(normalized)) {
|
|
428
|
-
throw new Error(`EEXIST: file already exists, symlink '${target}' -> '${normalized}'`);
|
|
429
|
-
}
|
|
430
|
-
await this.mkdir(dirname(normalized));
|
|
431
|
-
const inode = this.allocateSymlinkInode(target);
|
|
432
|
-
this.symlinks.set(normalized, { target, ino: inode.ino });
|
|
433
|
-
}
|
|
434
|
-
async readlink(path) {
|
|
435
|
-
const normalized = normalizePath(path);
|
|
436
|
-
const link = this.symlinks.get(normalized);
|
|
437
|
-
if (link === undefined) {
|
|
438
|
-
throw new Error(`EINVAL: invalid argument, readlink '${normalized}'`);
|
|
439
|
-
}
|
|
440
|
-
return link.target;
|
|
441
|
-
}
|
|
442
|
-
async lstat(path) {
|
|
443
|
-
const normalized = normalizePath(path);
|
|
444
|
-
const link = this.symlinks.get(normalized);
|
|
445
|
-
if (link !== undefined) {
|
|
446
|
-
return this.statForInode(this.requireInode(link.ino));
|
|
447
|
-
}
|
|
448
|
-
return this.statEntry(normalized);
|
|
449
|
-
}
|
|
450
|
-
async link(oldPath, newPath) {
|
|
451
|
-
const oldNormalized = normalizePath(oldPath);
|
|
452
|
-
const newNormalized = normalizePath(newPath);
|
|
453
|
-
const resolved = this.resolveSymlink(oldNormalized);
|
|
454
|
-
const ino = this.files.get(resolved);
|
|
455
|
-
if (ino === undefined) {
|
|
456
|
-
throw new Error(`ENOENT: no such file or directory, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
457
|
-
}
|
|
458
|
-
if (this.files.has(newNormalized) ||
|
|
459
|
-
this.dirs.has(newNormalized) ||
|
|
460
|
-
this.symlinks.has(newNormalized)) {
|
|
461
|
-
throw new Error(`EEXIST: file already exists, link '${oldNormalized}' -> '${newNormalized}'`);
|
|
462
|
-
}
|
|
463
|
-
await this.mkdir(dirname(newNormalized));
|
|
464
|
-
this.files.set(newNormalized, ino);
|
|
465
|
-
this.inodeTable.incrementLinks(ino);
|
|
466
|
-
}
|
|
467
|
-
async chmod(path, mode) {
|
|
468
|
-
const inode = this.requirePathInode(path, "chmod");
|
|
469
|
-
const callerTypeBits = mode & 0o170000;
|
|
470
|
-
if (callerTypeBits !== 0) {
|
|
471
|
-
inode.mode = mode;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
const existingTypeBits = inode.mode & 0o170000;
|
|
475
|
-
inode.mode = existingTypeBits | (mode & 0o7777);
|
|
476
|
-
}
|
|
477
|
-
inode.ctime = new Date();
|
|
478
|
-
}
|
|
479
|
-
async chown(path, uid, gid) {
|
|
480
|
-
const inode = this.requirePathInode(path, "chown");
|
|
481
|
-
inode.uid = uid;
|
|
482
|
-
inode.gid = gid;
|
|
483
|
-
inode.ctime = new Date();
|
|
484
|
-
}
|
|
485
|
-
async utimes(path, atime, mtime) {
|
|
486
|
-
const inode = this.requirePathInode(path, "utimes");
|
|
487
|
-
inode.atime = new Date(atime * 1000);
|
|
488
|
-
inode.mtime = new Date(mtime * 1000);
|
|
489
|
-
inode.ctime = new Date();
|
|
490
|
-
}
|
|
491
|
-
async realpath(path) {
|
|
492
|
-
const normalized = normalizePath(path);
|
|
493
|
-
const resolved = this.resolveSymlink(normalized);
|
|
494
|
-
if (!this.files.has(resolved) && !this.dirs.has(resolved)) {
|
|
495
|
-
throw new Error(`ENOENT: no such file or directory, realpath '${normalized}'`);
|
|
496
|
-
}
|
|
497
|
-
return resolved;
|
|
498
|
-
}
|
|
499
|
-
async pread(path, offset, length) {
|
|
500
|
-
const normalized = normalizePath(path);
|
|
501
|
-
const resolved = this.resolveSymlink(normalized);
|
|
502
|
-
const ino = this.files.get(resolved);
|
|
503
|
-
if (ino === undefined) {
|
|
504
|
-
throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);
|
|
505
|
-
}
|
|
506
|
-
return this.preadByInode(ino, offset, length);
|
|
507
|
-
}
|
|
508
|
-
async truncate(path, length) {
|
|
509
|
-
const normalized = normalizePath(path);
|
|
510
|
-
const resolved = this.resolveSymlink(normalized);
|
|
511
|
-
const ino = this.files.get(resolved);
|
|
512
|
-
if (ino === undefined) {
|
|
513
|
-
throw new Error(`ENOENT: no such file or directory, truncate '${normalized}'`);
|
|
514
|
-
}
|
|
515
|
-
const file = this.readFileByInode(ino);
|
|
516
|
-
const next = length >= file.byteLength
|
|
517
|
-
? (() => {
|
|
518
|
-
const padded = new Uint8Array(length);
|
|
519
|
-
padded.set(file);
|
|
520
|
-
return padded;
|
|
521
|
-
})()
|
|
522
|
-
: file.slice(0, length);
|
|
523
|
-
this.fileContents.set(ino, next);
|
|
524
|
-
this.updateFileMetadata(ino, next.byteLength);
|
|
525
|
-
}
|
|
526
|
-
reindexInodes(oldTable) {
|
|
527
|
-
const oldContents = new Map(this.fileContents);
|
|
528
|
-
const oldFiles = new Map(this.files);
|
|
529
|
-
const oldSymlinks = new Map(this.symlinks);
|
|
530
|
-
const oldDirs = Array.from(this.dirs.entries()).sort(([a], [b]) => a.length - b.length);
|
|
531
|
-
const inoMap = new Map();
|
|
532
|
-
this.files = new Map();
|
|
533
|
-
this.fileContents = new Map();
|
|
534
|
-
this.dirs = new Map();
|
|
535
|
-
this.symlinks = new Map();
|
|
536
|
-
for (const [dirPath, oldIno] of oldDirs) {
|
|
537
|
-
const ino = this.cloneInode(oldIno, oldTable, S_IFDIR | 0o755).ino;
|
|
538
|
-
this.dirs.set(dirPath, ino);
|
|
539
|
-
}
|
|
540
|
-
if (!this.dirs.has("/")) {
|
|
541
|
-
this.dirs.set("/", this.allocateDirectoryInode().ino);
|
|
542
|
-
}
|
|
543
|
-
for (const [path, oldIno] of oldFiles) {
|
|
544
|
-
const mapped = inoMap.get(oldIno) ?? (() => {
|
|
545
|
-
const inode = this.cloneInode(oldIno, oldTable, S_IFREG | 0o644);
|
|
546
|
-
inoMap.set(oldIno, inode.ino);
|
|
547
|
-
return inode.ino;
|
|
548
|
-
})();
|
|
549
|
-
this.files.set(path, mapped);
|
|
550
|
-
const content = oldContents.get(oldIno);
|
|
551
|
-
if (content) {
|
|
552
|
-
this.fileContents.set(mapped, content);
|
|
553
|
-
this.requireInode(mapped).size = content.byteLength;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
for (const [path, link] of oldSymlinks) {
|
|
557
|
-
const mapped = this.cloneInode(link.ino, oldTable, S_IFLNK | 0o777).ino;
|
|
558
|
-
this.symlinks.set(path, { target: link.target, ino: mapped });
|
|
559
|
-
this.requireInode(mapped).size = new TextEncoder().encode(link.target).byteLength;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
cloneInode(oldIno, oldTable, fallbackMode) {
|
|
563
|
-
const source = oldTable.get(oldIno);
|
|
564
|
-
const inode = this.inodeTable.allocate(source?.mode ?? fallbackMode, source?.uid ?? 0, source?.gid ?? 0);
|
|
565
|
-
inode.nlink = source?.nlink ?? 1;
|
|
566
|
-
inode.openRefCount = 0;
|
|
567
|
-
inode.size = source?.size ?? 0;
|
|
568
|
-
inode.atime = source?.atime ? new Date(source.atime) : new Date();
|
|
569
|
-
inode.mtime = source?.mtime ? new Date(source.mtime) : new Date();
|
|
570
|
-
inode.ctime = source?.ctime ? new Date(source.ctime) : new Date();
|
|
571
|
-
inode.birthtime = source?.birthtime ? new Date(source.birthtime) : new Date();
|
|
572
|
-
return inode;
|
|
573
|
-
}
|
|
574
|
-
allocateFileInode() {
|
|
575
|
-
return this.inodeTable.allocate(S_IFREG | 0o644, 0, 0);
|
|
576
|
-
}
|
|
577
|
-
allocateDirectoryInode() {
|
|
578
|
-
const inode = this.inodeTable.allocate(S_IFDIR | 0o755, 0, 0);
|
|
579
|
-
inode.nlink = 2;
|
|
580
|
-
inode.size = 4096;
|
|
581
|
-
return inode;
|
|
582
|
-
}
|
|
583
|
-
allocateSymlinkInode(target) {
|
|
584
|
-
const inode = this.inodeTable.allocate(S_IFLNK | 0o777, 0, 0);
|
|
585
|
-
inode.size = new TextEncoder().encode(target).byteLength;
|
|
586
|
-
return inode;
|
|
587
|
-
}
|
|
588
|
-
updateFileMetadata(ino, size) {
|
|
589
|
-
const inode = this.requireFileInode(ino);
|
|
590
|
-
const now = new Date();
|
|
591
|
-
inode.size = size;
|
|
592
|
-
inode.atime = now;
|
|
593
|
-
inode.mtime = now;
|
|
594
|
-
inode.ctime = now;
|
|
595
|
-
}
|
|
596
|
-
requirePathInode(path, op) {
|
|
597
|
-
const normalized = normalizePath(path);
|
|
598
|
-
const resolved = this.resolveSymlink(normalized);
|
|
599
|
-
const ino = this.files.get(resolved) ?? this.dirs.get(resolved);
|
|
600
|
-
if (ino === undefined) {
|
|
601
|
-
throw new Error(`ENOENT: no such file or directory, ${op} '${normalized}'`);
|
|
602
|
-
}
|
|
603
|
-
return this.requireInode(ino);
|
|
604
|
-
}
|
|
605
|
-
requireFileInode(ino) {
|
|
606
|
-
const inode = this.requireInode(ino);
|
|
607
|
-
if ((inode.mode & 0o170000) !== S_IFREG && (inode.mode & 0o170000) !== S_IFSOCK) {
|
|
608
|
-
throw new Error(`EINVAL: inode ${ino} is not a regular file`);
|
|
609
|
-
}
|
|
610
|
-
return inode;
|
|
611
|
-
}
|
|
612
|
-
requireInode(ino) {
|
|
613
|
-
const inode = this.inodeTable.get(ino);
|
|
614
|
-
if (!inode) {
|
|
615
|
-
throw new Error(`ENOENT: inode ${ino} not found`);
|
|
616
|
-
}
|
|
617
|
-
return inode;
|
|
618
|
-
}
|
|
619
|
-
ensureDirectory(path) {
|
|
620
|
-
const normalized = normalizePath(path);
|
|
621
|
-
if (normalized === "/")
|
|
622
|
-
return;
|
|
623
|
-
if (this.dirs.has(normalized))
|
|
624
|
-
return;
|
|
625
|
-
const parent = dirname(normalized);
|
|
626
|
-
if (!this.dirs.has(parent)) {
|
|
627
|
-
throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);
|
|
628
|
-
}
|
|
629
|
-
this.dirs.set(normalized, this.allocateDirectoryInode().ino);
|
|
630
|
-
this.adjustParentDirectoryLinkCount(normalized, 1);
|
|
631
|
-
}
|
|
632
|
-
adjustParentDirectoryLinkCount(path, delta) {
|
|
633
|
-
const normalized = normalizePath(path);
|
|
634
|
-
if (normalized === "/")
|
|
635
|
-
return;
|
|
636
|
-
const parent = dirname(normalized);
|
|
637
|
-
const parentIno = this.dirs.get(parent);
|
|
638
|
-
if (parentIno === undefined)
|
|
639
|
-
return;
|
|
640
|
-
if (delta > 0) {
|
|
641
|
-
this.inodeTable.incrementLinks(parentIno);
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
this.inodeTable.decrementLinks(parentIno);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
export function createInMemoryFileSystem() {
|
|
649
|
-
return new InMemoryFileSystem();
|
|
114
|
+
return Object.assign(vfs, { prepareOpenSync });
|
|
650
115
|
}
|