@standardagents/code-web 0.14.3-canary.6cc75f → 0.14.4-branch.9e48e914.ee06ef
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 +9 -1
- package/dist/build.json +1 -1
- package/dist/licenses/jco-transpile.txt +220 -0
- package/dist/manifest.json +1 -1
- package/dist/manifest.sig +1 -1
- package/dist/pkg/standard_web.d.ts +50 -12
- package/dist/pkg/standard_web.js +292 -20
- package/dist/pkg/standard_web_bg.wasm +0 -0
- package/dist/pkg/standard_web_bg.wasm.d.ts +17 -12
- package/dist/plugins/clock-cells/clock_cells.core.wasm +0 -0
- package/dist/plugins/clock-cells/clock_cells.wasm +0 -0
- package/dist/plugins/clock-cells/standard-plugin.json +11 -0
- package/dist/plugins/host/transpile.js +51 -0
- package/dist/plugins/host/ui-jspi.js +14851 -0
- package/dist/plugins/host/ui.core2.wasm +0 -0
- package/dist/plugins/host/ui.core3.wasm +0 -0
- package/dist/plugins/host/ui.js +14675 -0
- package/dist/plugins/host/world.js +172 -0
- package/dist/plugins/jco/cli.js +141 -0
- package/dist/plugins/jco/common.js +16 -0
- package/dist/plugins/jco/config.js +7 -0
- package/dist/plugins/jco/environment.js +24 -0
- package/dist/plugins/jco/filesystem.js +339 -0
- package/dist/plugins/jco/in-memory-filesystem.js +620 -0
- package/dist/plugins/jco/io.js +366 -0
- package/dist/plugins/jco/js-component-bindgen-component.core.wasm +0 -0
- package/dist/plugins/jco/js-component-bindgen-component.core2.wasm +0 -0
- package/dist/plugins/jco/js-component-bindgen-component.js +14272 -0
- package/dist/plugins/jco/opfs-filesystem.js +245 -0
- package/dist/plugins/jco/random.js +51 -0
- package/dist/plugins/plasma-pixels/plasma_pixels.core.wasm +0 -0
- package/dist/plugins/plasma-pixels/plasma_pixels.wasm +0 -0
- package/dist/plugins/plasma-pixels/standard-plugin.json +11 -0
- package/dist/plugins/runtime.js +1044 -0
- package/dist/plugins/samples.json +81 -0
- package/dist/plugins/worker.js +411 -0
- package/package.json +8 -2
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { _getCwd } from "./config.js";
|
|
2
|
+
import { inputStreamCreate, outputStreamCreate } from "./io.js";
|
|
3
|
+
const rootEntries = new WeakSet();
|
|
4
|
+
const timeZero = {
|
|
5
|
+
seconds: 0n,
|
|
6
|
+
nanoseconds: 0,
|
|
7
|
+
};
|
|
8
|
+
/** Coerce the given object to a safe integer */
|
|
9
|
+
function coerceToSafeIntegerNumber(obj) {
|
|
10
|
+
let n;
|
|
11
|
+
if (typeof obj === "number") {
|
|
12
|
+
n = obj;
|
|
13
|
+
}
|
|
14
|
+
else if (typeof obj == "bigint") {
|
|
15
|
+
n = Number(obj);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw new TypeError(`unexpected non-numeric type: ${obj}`);
|
|
19
|
+
}
|
|
20
|
+
if (n > Number.MAX_SAFE_INTEGER) {
|
|
21
|
+
throw new TypeError(`excessively large number: ${n}`);
|
|
22
|
+
}
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
// Bound the total number of symlinks followed during a single path resolution.
|
|
26
|
+
const MAX_SYMLINK_DEPTH = 40;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve from the descriptor's base directory, retaining the directory stack
|
|
29
|
+
* across symlink expansions so `..` cannot escape that capability. Intermediate
|
|
30
|
+
* symlinks always follow; the final segment follows only when requested.
|
|
31
|
+
* Creation may return a missing final entry, together with its resolved parent.
|
|
32
|
+
*/
|
|
33
|
+
function resolveEntry(root, path, followFinal, allowMissingFinal = false) {
|
|
34
|
+
const directories = [root];
|
|
35
|
+
const pending = path.split("/").reverse();
|
|
36
|
+
let followed = 0;
|
|
37
|
+
while (pending.length) {
|
|
38
|
+
const parent = directories[directories.length - 1];
|
|
39
|
+
if (!parent.dir) {
|
|
40
|
+
throw "not-directory";
|
|
41
|
+
}
|
|
42
|
+
const name = pending.pop();
|
|
43
|
+
if (name === "" || name === ".") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (name === "..") {
|
|
47
|
+
if (directories.length === 1) {
|
|
48
|
+
throw "not-permitted";
|
|
49
|
+
}
|
|
50
|
+
directories.pop();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const entry = parent.dir[name];
|
|
54
|
+
const isFinal = pending.length === 0;
|
|
55
|
+
if (!entry) {
|
|
56
|
+
if (isFinal && allowMissingFinal) {
|
|
57
|
+
return { entry: undefined, parent, name };
|
|
58
|
+
}
|
|
59
|
+
throw "no-entry";
|
|
60
|
+
}
|
|
61
|
+
if (entry.symlink !== undefined && (!isFinal || followFinal)) {
|
|
62
|
+
if (++followed > MAX_SYMLINK_DEPTH) {
|
|
63
|
+
throw "loop";
|
|
64
|
+
}
|
|
65
|
+
if (entry.symlink.startsWith("/")) {
|
|
66
|
+
throw "not-permitted";
|
|
67
|
+
}
|
|
68
|
+
for (const segment of entry.symlink.split("/").reverse()) {
|
|
69
|
+
pending.push(segment);
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (isFinal) {
|
|
74
|
+
return { entry, parent, name };
|
|
75
|
+
}
|
|
76
|
+
directories.push(entry);
|
|
77
|
+
}
|
|
78
|
+
const entry = directories[directories.length - 1];
|
|
79
|
+
return { entry, parent: entry, name: "" };
|
|
80
|
+
}
|
|
81
|
+
// Preserve the legacy root lookup of `.` without applying CWD substitution to
|
|
82
|
+
// symlink targets or to the parent paths used by mutations.
|
|
83
|
+
function lookupPath(root, path) {
|
|
84
|
+
if (path === "." && rootEntries.has(root)) {
|
|
85
|
+
return _getCwd();
|
|
86
|
+
}
|
|
87
|
+
return path;
|
|
88
|
+
}
|
|
89
|
+
function getChildEntry(parentEntry, subpath, followFinal) {
|
|
90
|
+
return resolveEntry(parentEntry, lookupPath(parentEntry, subpath), !!followFinal).entry;
|
|
91
|
+
}
|
|
92
|
+
function getParentEntry(root, path) {
|
|
93
|
+
const segments = path.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
94
|
+
const name = segments.pop();
|
|
95
|
+
if (!name || name === "..") {
|
|
96
|
+
throw "invalid";
|
|
97
|
+
}
|
|
98
|
+
const parent = resolveEntry(root, segments.join("/"), true).entry;
|
|
99
|
+
if (!parent.dir) {
|
|
100
|
+
throw "not-directory";
|
|
101
|
+
}
|
|
102
|
+
return [parent, name];
|
|
103
|
+
}
|
|
104
|
+
function getSource(fileEntry) {
|
|
105
|
+
if (typeof fileEntry.source === "string") {
|
|
106
|
+
fileEntry.source = new TextEncoder().encode(fileEntry.source);
|
|
107
|
+
}
|
|
108
|
+
return fileEntry.source;
|
|
109
|
+
}
|
|
110
|
+
function describeEntry(entry) {
|
|
111
|
+
if (entry.symlink !== undefined) {
|
|
112
|
+
// Matches POSIX lstat: a symlink's size is the byte length of its target.
|
|
113
|
+
return {
|
|
114
|
+
type: "symbolic-link",
|
|
115
|
+
size: BigInt(new TextEncoder().encode(entry.symlink).byteLength),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (entry.dir) {
|
|
119
|
+
return { type: "directory", size: 0n };
|
|
120
|
+
}
|
|
121
|
+
return { type: "regular-file", size: BigInt(getSource(entry).byteLength) };
|
|
122
|
+
}
|
|
123
|
+
function containsEntry(root, target) {
|
|
124
|
+
if (root === target) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return root.dir ? Object.values(root.dir).some((entry) => containsEntry(entry, target)) : false;
|
|
128
|
+
}
|
|
129
|
+
// Keep spare capacity separate so FileDataEntry.source always reflects the logical file size.
|
|
130
|
+
const fileWriteBuffers = new WeakMap();
|
|
131
|
+
let nextEntryId = 0n;
|
|
132
|
+
const entryMetadata = new WeakMap();
|
|
133
|
+
function metadata(entry) {
|
|
134
|
+
let value = entryMetadata.get(entry);
|
|
135
|
+
if (!value) {
|
|
136
|
+
value = { id: ++nextEntryId, version: 0n, linkCount: 1n };
|
|
137
|
+
entryMetadata.set(entry, value);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
const fileLocks = new WeakMap();
|
|
142
|
+
function lockState(entry) {
|
|
143
|
+
let state = fileLocks.get(entry);
|
|
144
|
+
if (!state) {
|
|
145
|
+
state = { exclusiveHolder: null, sharedHolders: new Set() };
|
|
146
|
+
fileLocks.set(entry, state);
|
|
147
|
+
}
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
const touchListeners = new Set();
|
|
151
|
+
/**
|
|
152
|
+
* Subscribe to every mutation across every in-memory tree (regardless of which
|
|
153
|
+
* adapter loaded it). Lets a persistence layer (e.g. `OpfsFilesystemAdapter`)
|
|
154
|
+
* react to writes without wrapping every mutating method individually. Returns
|
|
155
|
+
* an unsubscribe function.
|
|
156
|
+
*/
|
|
157
|
+
export function _onTouch(listener) {
|
|
158
|
+
touchListeners.add(listener);
|
|
159
|
+
return () => touchListeners.delete(listener);
|
|
160
|
+
}
|
|
161
|
+
function touch(entry) {
|
|
162
|
+
metadata(entry).version++;
|
|
163
|
+
for (const listener of touchListeners) {
|
|
164
|
+
listener(entry);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function getFileWriteBuffer(entry, source, requiredLength) {
|
|
168
|
+
let buffer = fileWriteBuffers.get(entry);
|
|
169
|
+
if (!buffer || buffer.buffer !== source.buffer || buffer.byteOffset !== source.byteOffset) {
|
|
170
|
+
buffer = source;
|
|
171
|
+
}
|
|
172
|
+
if (requiredLength <= buffer.byteLength) {
|
|
173
|
+
return buffer;
|
|
174
|
+
}
|
|
175
|
+
const newBuffer = new Uint8Array(Math.max(requiredLength, source.byteLength * 2));
|
|
176
|
+
newBuffer.set(source);
|
|
177
|
+
fileWriteBuffers.set(entry, newBuffer);
|
|
178
|
+
return newBuffer;
|
|
179
|
+
}
|
|
180
|
+
class DirectoryEntryStream {
|
|
181
|
+
idx = 0;
|
|
182
|
+
entries = [];
|
|
183
|
+
static _create(entries) {
|
|
184
|
+
const stream = new DirectoryEntryStream();
|
|
185
|
+
stream.entries = entries;
|
|
186
|
+
return stream;
|
|
187
|
+
}
|
|
188
|
+
readDirectoryEntry() {
|
|
189
|
+
if (this.idx === this.entries.length) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const [name, entry] = this.entries[this.idx];
|
|
193
|
+
this.idx += 1;
|
|
194
|
+
return {
|
|
195
|
+
name,
|
|
196
|
+
type: describeEntry(entry).type,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const descriptorEntryStreamCreate = DirectoryEntryStream._create;
|
|
201
|
+
// @ts-expect-error - Deleting static method
|
|
202
|
+
delete DirectoryEntryStream._create;
|
|
203
|
+
class Descriptor {
|
|
204
|
+
#stream;
|
|
205
|
+
#entry;
|
|
206
|
+
#flags = {
|
|
207
|
+
read: true,
|
|
208
|
+
write: true,
|
|
209
|
+
mutateDirectory: true,
|
|
210
|
+
};
|
|
211
|
+
#advice = "normal";
|
|
212
|
+
_getEntry(descriptor) {
|
|
213
|
+
return descriptor.#entry;
|
|
214
|
+
}
|
|
215
|
+
static _create(entry, isStream) {
|
|
216
|
+
const descriptor = new Descriptor();
|
|
217
|
+
if (isStream) {
|
|
218
|
+
descriptor.#stream = entry;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
descriptor.#entry = entry;
|
|
222
|
+
}
|
|
223
|
+
return descriptor;
|
|
224
|
+
}
|
|
225
|
+
readViaStream(_offset) {
|
|
226
|
+
const source = getSource(this.#entry);
|
|
227
|
+
let offset = Number(_offset);
|
|
228
|
+
return inputStreamCreate({
|
|
229
|
+
blockingRead(len) {
|
|
230
|
+
if (offset === source.byteLength) {
|
|
231
|
+
throw { tag: "closed" };
|
|
232
|
+
}
|
|
233
|
+
const bytes = source.slice(offset, offset + Number(len));
|
|
234
|
+
offset += bytes.byteLength;
|
|
235
|
+
return bytes;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
writeViaStream(_offset) {
|
|
240
|
+
const entry = this.#entry;
|
|
241
|
+
let offset = coerceToSafeIntegerNumber(_offset);
|
|
242
|
+
return outputStreamCreate({
|
|
243
|
+
write(buf) {
|
|
244
|
+
if (buf.byteLength === 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const source = getSource(entry);
|
|
248
|
+
const end = offset + buf.byteLength;
|
|
249
|
+
if (!Number.isSafeInteger(end)) {
|
|
250
|
+
throw new TypeError(`excessively large number: ${end}`);
|
|
251
|
+
}
|
|
252
|
+
const buffer = getFileWriteBuffer(entry, source, end);
|
|
253
|
+
if (offset > source.byteLength) {
|
|
254
|
+
buffer.fill(0, source.byteLength, offset);
|
|
255
|
+
}
|
|
256
|
+
buffer.set(buf, offset);
|
|
257
|
+
entry.source = buffer.subarray(0, Math.max(source.byteLength, end));
|
|
258
|
+
offset = end;
|
|
259
|
+
touch(entry);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
appendViaStream() {
|
|
264
|
+
return this.writeViaStream(this.stat().size);
|
|
265
|
+
}
|
|
266
|
+
advise(_offset, _length, advice) {
|
|
267
|
+
if (this.getType() === "directory") {
|
|
268
|
+
throw "bad-descriptor";
|
|
269
|
+
}
|
|
270
|
+
// All data is already resident in memory, so there's nothing to prefetch or
|
|
271
|
+
// evict here. Retain the last hint so it can be inspected by adapters that
|
|
272
|
+
// do have a real backing store to optimize (e.g. a persistent/OPFS adapter).
|
|
273
|
+
this.#advice = advice;
|
|
274
|
+
}
|
|
275
|
+
syncData() { }
|
|
276
|
+
getFlags() {
|
|
277
|
+
return { ...this.#flags };
|
|
278
|
+
}
|
|
279
|
+
getType() {
|
|
280
|
+
if (this.#stream) {
|
|
281
|
+
return "fifo";
|
|
282
|
+
}
|
|
283
|
+
if (this.#entry.symlink !== undefined) {
|
|
284
|
+
return "symbolic-link";
|
|
285
|
+
}
|
|
286
|
+
if (this.#entry.dir) {
|
|
287
|
+
return "directory";
|
|
288
|
+
}
|
|
289
|
+
if (this.#entry.source) {
|
|
290
|
+
return "regular-file";
|
|
291
|
+
}
|
|
292
|
+
return "unknown";
|
|
293
|
+
}
|
|
294
|
+
setSize(size) {
|
|
295
|
+
if (this.getType() === "directory") {
|
|
296
|
+
throw "is-directory";
|
|
297
|
+
}
|
|
298
|
+
const length = coerceToSafeIntegerNumber(size);
|
|
299
|
+
const source = getSource(this.#entry);
|
|
300
|
+
const resized = new Uint8Array(length);
|
|
301
|
+
resized.set(source.subarray(0, length));
|
|
302
|
+
this.#entry.source = resized;
|
|
303
|
+
touch(this.#entry);
|
|
304
|
+
}
|
|
305
|
+
setTimes(dataAccessTimestamp, dataModificationTimestamp) {
|
|
306
|
+
if (dataAccessTimestamp?.tag !== "no-change" ||
|
|
307
|
+
dataModificationTimestamp?.tag !== "no-change") {
|
|
308
|
+
touch(this.#entry);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
read(length, offset) {
|
|
312
|
+
const source = getSource(this.#entry);
|
|
313
|
+
const off = coerceToSafeIntegerNumber(offset);
|
|
314
|
+
const len = coerceToSafeIntegerNumber(length);
|
|
315
|
+
const result = [
|
|
316
|
+
source.slice(off, off + len),
|
|
317
|
+
off + len >= source.byteLength,
|
|
318
|
+
];
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
write(buffer, offset) {
|
|
322
|
+
if (this.getType() === "directory") {
|
|
323
|
+
throw "is-directory";
|
|
324
|
+
}
|
|
325
|
+
const off = coerceToSafeIntegerNumber(offset);
|
|
326
|
+
const source = getSource(this.#entry);
|
|
327
|
+
const end = off + buffer.byteLength;
|
|
328
|
+
if (!Number.isSafeInteger(end)) {
|
|
329
|
+
throw "file-too-large";
|
|
330
|
+
}
|
|
331
|
+
const target = new Uint8Array(Math.max(source.byteLength, end));
|
|
332
|
+
target.set(source);
|
|
333
|
+
target.set(buffer, off);
|
|
334
|
+
this.#entry.source = target;
|
|
335
|
+
touch(this.#entry);
|
|
336
|
+
return BigInt(buffer.byteLength);
|
|
337
|
+
}
|
|
338
|
+
readDirectory() {
|
|
339
|
+
if (!this.#entry?.dir) {
|
|
340
|
+
throw "bad-descriptor";
|
|
341
|
+
}
|
|
342
|
+
return descriptorEntryStreamCreate(Object.entries(this.#entry.dir).sort(([a], [b]) => (a > b ? 1 : -1)));
|
|
343
|
+
}
|
|
344
|
+
sync() { }
|
|
345
|
+
createDirectoryAt(path) {
|
|
346
|
+
try {
|
|
347
|
+
// Existence is checked against the literal entry (mkdir never follows
|
|
348
|
+
// the final path component, even if it's a symlink).
|
|
349
|
+
getChildEntry(this.#entry, path, false);
|
|
350
|
+
throw "exist";
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (error !== "no-entry") {
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const [parent, name] = getParentEntry(this.#entry, path);
|
|
358
|
+
parent.dir[name] = { dir: {} };
|
|
359
|
+
touch(parent);
|
|
360
|
+
}
|
|
361
|
+
stat() {
|
|
362
|
+
const { type, size } = describeEntry(this.#entry);
|
|
363
|
+
return {
|
|
364
|
+
type,
|
|
365
|
+
linkCount: metadata(this.#entry).linkCount,
|
|
366
|
+
size,
|
|
367
|
+
dataAccessTimestamp: timeZero,
|
|
368
|
+
dataModificationTimestamp: timeZero,
|
|
369
|
+
statusChangeTimestamp: timeZero,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
statAt(pathFlags, path) {
|
|
373
|
+
const entry = getChildEntry(this.#entry, path, pathFlags.symlinkFollow);
|
|
374
|
+
const { type, size } = describeEntry(entry);
|
|
375
|
+
return {
|
|
376
|
+
type,
|
|
377
|
+
linkCount: metadata(entry).linkCount,
|
|
378
|
+
size,
|
|
379
|
+
dataAccessTimestamp: timeZero,
|
|
380
|
+
dataModificationTimestamp: timeZero,
|
|
381
|
+
statusChangeTimestamp: timeZero,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
setTimesAt(pathFlags, path, _atime, mtime) {
|
|
385
|
+
const entry = getChildEntry(this.#entry, path, pathFlags.symlinkFollow);
|
|
386
|
+
if (mtime?.tag !== "no-change") {
|
|
387
|
+
// Metadata is currently descriptor-local; touching the entry makes
|
|
388
|
+
// the mutation visible through metadata hashes on newly opened handles.
|
|
389
|
+
fileWriteBuffers.delete(entry);
|
|
390
|
+
touch(entry);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
linkAt(oldPathFlags, oldPath, newDescriptor, newPath) {
|
|
394
|
+
// Unlike open/stat, link never follows the final symlink unless the
|
|
395
|
+
// caller explicitly asked for it via `symlink-follow`.
|
|
396
|
+
const entry = getChildEntry(this.#entry, oldPath, oldPathFlags.symlinkFollow);
|
|
397
|
+
if (entry.dir) {
|
|
398
|
+
throw "not-permitted";
|
|
399
|
+
}
|
|
400
|
+
const [newParent, newName] = getParentEntry(descriptorGetEntry(unwrapDescriptor(newDescriptor)), newPath);
|
|
401
|
+
if (newParent.dir[newName]) {
|
|
402
|
+
throw "exist";
|
|
403
|
+
}
|
|
404
|
+
newParent.dir[newName] = entry;
|
|
405
|
+
metadata(entry).linkCount++;
|
|
406
|
+
touch(newParent);
|
|
407
|
+
}
|
|
408
|
+
openAt(pathFlags, path, openFlags, _flags) {
|
|
409
|
+
const exclusiveCreate = !!(openFlags.create && openFlags.exclusive);
|
|
410
|
+
const resolved = resolveEntry(this.#entry, lookupPath(this.#entry, path), !!pathFlags.symlinkFollow && !exclusiveCreate, !!openFlags.create);
|
|
411
|
+
let childEntry = resolved.entry;
|
|
412
|
+
if (childEntry && exclusiveCreate) {
|
|
413
|
+
throw "exist";
|
|
414
|
+
}
|
|
415
|
+
if (!childEntry) {
|
|
416
|
+
const { parent, name } = resolved;
|
|
417
|
+
childEntry = parent.dir[name] = openFlags.directory
|
|
418
|
+
? { dir: {} }
|
|
419
|
+
: { source: new Uint8Array() };
|
|
420
|
+
touch(parent);
|
|
421
|
+
}
|
|
422
|
+
if (childEntry.symlink !== undefined) {
|
|
423
|
+
// `symlink-follow` was unset and the final path component is a
|
|
424
|
+
// symlink - matches opening with O_NOFOLLOW against a symlink.
|
|
425
|
+
throw "loop";
|
|
426
|
+
}
|
|
427
|
+
if (openFlags.directory && !childEntry.dir) {
|
|
428
|
+
throw "not-directory";
|
|
429
|
+
}
|
|
430
|
+
if (openFlags.truncate) {
|
|
431
|
+
if (childEntry.dir) {
|
|
432
|
+
throw "is-directory";
|
|
433
|
+
}
|
|
434
|
+
childEntry.source = new Uint8Array();
|
|
435
|
+
touch(childEntry);
|
|
436
|
+
}
|
|
437
|
+
return descriptorCreate(childEntry);
|
|
438
|
+
}
|
|
439
|
+
readlinkAt(path) {
|
|
440
|
+
// The literal entry, never resolved - readlink always reports the
|
|
441
|
+
// immediate target, not what it ultimately points to.
|
|
442
|
+
const entry = getChildEntry(this.#entry, path, false);
|
|
443
|
+
if (entry.symlink === undefined) {
|
|
444
|
+
throw "invalid";
|
|
445
|
+
}
|
|
446
|
+
if (entry.symlink.startsWith("/")) {
|
|
447
|
+
throw "not-permitted";
|
|
448
|
+
}
|
|
449
|
+
return entry.symlink;
|
|
450
|
+
}
|
|
451
|
+
removeDirectoryAt(path) {
|
|
452
|
+
const [parent, name] = getParentEntry(this.#entry, path);
|
|
453
|
+
const entry = parent.dir?.[name];
|
|
454
|
+
if (!entry) {
|
|
455
|
+
throw "no-entry";
|
|
456
|
+
}
|
|
457
|
+
if (!entry.dir) {
|
|
458
|
+
throw "not-directory";
|
|
459
|
+
}
|
|
460
|
+
if (Object.keys(entry.dir).length) {
|
|
461
|
+
throw "not-empty";
|
|
462
|
+
}
|
|
463
|
+
delete parent.dir[name];
|
|
464
|
+
metadata(entry).linkCount--;
|
|
465
|
+
touch(parent);
|
|
466
|
+
}
|
|
467
|
+
renameAt(oldPath, newDescriptor, newPath) {
|
|
468
|
+
const [oldParent, oldName] = getParentEntry(this.#entry, oldPath);
|
|
469
|
+
const entry = oldParent.dir?.[oldName];
|
|
470
|
+
if (!entry) {
|
|
471
|
+
throw "no-entry";
|
|
472
|
+
}
|
|
473
|
+
const [newParent, newName] = getParentEntry(descriptorGetEntry(unwrapDescriptor(newDescriptor)), newPath);
|
|
474
|
+
const replaced = newParent.dir[newName];
|
|
475
|
+
if ((oldParent === newParent && oldName === newName) || replaced === entry) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (entry.dir && containsEntry(entry, newParent)) {
|
|
479
|
+
throw "invalid";
|
|
480
|
+
}
|
|
481
|
+
if (replaced) {
|
|
482
|
+
if (entry.dir && !replaced.dir) {
|
|
483
|
+
throw "not-directory";
|
|
484
|
+
}
|
|
485
|
+
if (!entry.dir && replaced.dir) {
|
|
486
|
+
throw "is-directory";
|
|
487
|
+
}
|
|
488
|
+
if (replaced.dir && Object.keys(replaced.dir).length > 0) {
|
|
489
|
+
throw "not-empty";
|
|
490
|
+
}
|
|
491
|
+
metadata(replaced).linkCount--;
|
|
492
|
+
}
|
|
493
|
+
newParent.dir[newName] = entry;
|
|
494
|
+
delete oldParent.dir[oldName];
|
|
495
|
+
touch(oldParent);
|
|
496
|
+
if (newParent !== oldParent) {
|
|
497
|
+
touch(newParent);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
symlinkAt(oldPath, newPath) {
|
|
501
|
+
// Matches the Node backend: guest-visible symlink targets are always
|
|
502
|
+
// relative, since this sandboxed filesystem has no host-absolute root.
|
|
503
|
+
if (oldPath.startsWith("/")) {
|
|
504
|
+
throw "not-permitted";
|
|
505
|
+
}
|
|
506
|
+
const [parent, name] = getParentEntry(this.#entry, newPath);
|
|
507
|
+
if (parent.dir[name]) {
|
|
508
|
+
throw "exist";
|
|
509
|
+
}
|
|
510
|
+
parent.dir[name] = { symlink: oldPath };
|
|
511
|
+
touch(parent);
|
|
512
|
+
}
|
|
513
|
+
unlinkFileAt(path) {
|
|
514
|
+
const [parent, name] = getParentEntry(this.#entry, path);
|
|
515
|
+
const entry = parent.dir?.[name];
|
|
516
|
+
if (!entry) {
|
|
517
|
+
throw "no-entry";
|
|
518
|
+
}
|
|
519
|
+
if (entry.dir) {
|
|
520
|
+
throw "is-directory";
|
|
521
|
+
}
|
|
522
|
+
delete parent.dir[name];
|
|
523
|
+
metadata(entry).linkCount--;
|
|
524
|
+
touch(parent);
|
|
525
|
+
}
|
|
526
|
+
isSameObject(other) {
|
|
527
|
+
return descriptorGetEntry(unwrapDescriptor(other)) === this.#entry;
|
|
528
|
+
}
|
|
529
|
+
metadataHash() {
|
|
530
|
+
const value = metadata(this.#entry);
|
|
531
|
+
return { upper: value.id, lower: value.version };
|
|
532
|
+
}
|
|
533
|
+
metadataHashAt(pathFlags, path) {
|
|
534
|
+
const value = metadata(getChildEntry(this.#entry, path, pathFlags.symlinkFollow));
|
|
535
|
+
return { upper: value.id, lower: value.version };
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Default advisory-locking implementation: a same-process reader/writer lock
|
|
539
|
+
* keyed on the underlying entry. There's no real contention to wait out in a
|
|
540
|
+
* single-threaded environment, so `lockShared`/`lockExclusive` don't block -
|
|
541
|
+
* they throw `would-block` immediately when the lock isn't free, same as the
|
|
542
|
+
* `tryLock*` variants report `false`.
|
|
543
|
+
*/
|
|
544
|
+
tryLockShared() {
|
|
545
|
+
const state = lockState(this.#entry);
|
|
546
|
+
if (state.exclusiveHolder && state.exclusiveHolder !== this) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
state.sharedHolders.add(this);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
tryLockExclusive() {
|
|
553
|
+
const state = lockState(this.#entry);
|
|
554
|
+
if (state.exclusiveHolder && state.exclusiveHolder !== this) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
const otherReaders = state.sharedHolders.size - (state.sharedHolders.has(this) ? 1 : 0);
|
|
558
|
+
if (otherReaders > 0) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
state.sharedHolders.delete(this);
|
|
562
|
+
state.exclusiveHolder = this;
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
lockShared() {
|
|
566
|
+
if (!this.tryLockShared()) {
|
|
567
|
+
throw "would-block";
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
lockExclusive() {
|
|
571
|
+
if (!this.tryLockExclusive()) {
|
|
572
|
+
throw "would-block";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
unlock() {
|
|
576
|
+
const state = fileLocks.get(this.#entry);
|
|
577
|
+
if (!state) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
state.sharedHolders.delete(this);
|
|
581
|
+
if (state.exclusiveHolder === this) {
|
|
582
|
+
state.exclusiveHolder = null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const descriptorGetEntry = Descriptor.prototype._getEntry;
|
|
587
|
+
// @ts-expect-error - Deleting prototype method
|
|
588
|
+
delete Descriptor.prototype._getEntry;
|
|
589
|
+
const descriptorCreate = Descriptor._create;
|
|
590
|
+
// @ts-expect-error - Deleting static method
|
|
591
|
+
delete Descriptor._create;
|
|
592
|
+
/**
|
|
593
|
+
* Well-known symbol a `BrowserFilesystemDescriptor` wrapper (e.g. the cross-tab-locking
|
|
594
|
+
* `Proxy` from `OpfsFilesystemAdapter`) can implement to hand back the real descriptor it
|
|
595
|
+
* wraps. `renameAt`/`linkAt`/`isSameObject` reach into a *second* descriptor argument's
|
|
596
|
+
* private `#entry` field directly - private-field access bypasses `Proxy` traps and fails
|
|
597
|
+
* its brand check against a wrapper, so a wrapper must be unwrapped via ordinary property
|
|
598
|
+
* access (which a `Proxy` handles correctly) before that field access happens.
|
|
599
|
+
*/
|
|
600
|
+
export const UNWRAP_DESCRIPTOR = Symbol("browserFilesystemDescriptor.unwrap");
|
|
601
|
+
function unwrapDescriptor(descriptor) {
|
|
602
|
+
let current = descriptor;
|
|
603
|
+
for (;;) {
|
|
604
|
+
const inner = current[UNWRAP_DESCRIPTOR];
|
|
605
|
+
if (!inner || inner === current) {
|
|
606
|
+
return current;
|
|
607
|
+
}
|
|
608
|
+
current = inner;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** Explicit ephemeral storage adapter for browser applications and tests. */
|
|
612
|
+
export class InMemoryFilesystemAdapter {
|
|
613
|
+
getRoot(capability) {
|
|
614
|
+
if (!capability.dir) {
|
|
615
|
+
throw new TypeError("an in-memory preopen root must be a directory");
|
|
616
|
+
}
|
|
617
|
+
rootEntries.add(capability);
|
|
618
|
+
return descriptorCreate(capability);
|
|
619
|
+
}
|
|
620
|
+
}
|