@bytecodealliance/preview3-shim 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +220 -0
- package/README.md +14 -0
- package/lib/nodejs/cli.js +127 -0
- package/lib/nodejs/clocks.js +100 -0
- package/lib/nodejs/filesystem/descriptor.js +997 -0
- package/lib/nodejs/filesystem/error.js +92 -0
- package/lib/nodejs/filesystem/utils.js +19 -0
- package/lib/nodejs/filesystem.js +2 -0
- package/lib/nodejs/finalization.js +33 -0
- package/lib/nodejs/future.js +187 -0
- package/lib/nodejs/http/client.js +146 -0
- package/lib/nodejs/http/error.js +71 -0
- package/lib/nodejs/http/fields.js +359 -0
- package/lib/nodejs/http/request.js +589 -0
- package/lib/nodejs/http/response.js +212 -0
- package/lib/nodejs/http/server.js +157 -0
- package/lib/nodejs/http.js +34 -0
- package/lib/nodejs/index.js +8 -0
- package/lib/nodejs/random.js +14 -0
- package/lib/nodejs/sockets/address.js +146 -0
- package/lib/nodejs/sockets/error.js +123 -0
- package/lib/nodejs/sockets/ip-name-lookup.js +93 -0
- package/lib/nodejs/sockets/tcp.js +776 -0
- package/lib/nodejs/sockets/udp.js +516 -0
- package/lib/nodejs/sockets.js +14 -0
- package/lib/nodejs/stream.js +423 -0
- package/lib/nodejs/workers/cli-worker.js +49 -0
- package/lib/nodejs/workers/filesystem-worker.js +119 -0
- package/lib/nodejs/workers/http-worker.js +387 -0
- package/lib/nodejs/workers/resource-worker.js +151 -0
- package/lib/nodejs/workers/tcp-worker.js +283 -0
- package/lib/nodejs/workers/udp-worker.js +194 -0
- package/package.json +57 -0
- package/types/cli.d.ts +12 -0
- package/types/clocks.d.ts +2 -0
- package/types/filesystem.d.ts +2 -0
- package/types/http.d.ts +4 -0
- package/types/index.d.ts +6 -0
- package/types/interfaces/wasi-cli-environment.d.ts +21 -0
- package/types/interfaces/wasi-cli-exit.d.ts +6 -0
- package/types/interfaces/wasi-cli-run.d.ts +5 -0
- package/types/interfaces/wasi-cli-stderr.d.ts +14 -0
- package/types/interfaces/wasi-cli-stdin.d.ts +19 -0
- package/types/interfaces/wasi-cli-stdout.d.ts +14 -0
- package/types/interfaces/wasi-cli-terminal-input.d.ts +8 -0
- package/types/interfaces/wasi-cli-terminal-output.d.ts +8 -0
- package/types/interfaces/wasi-cli-terminal-stderr.d.ts +7 -0
- package/types/interfaces/wasi-cli-terminal-stdin.d.ts +7 -0
- package/types/interfaces/wasi-cli-terminal-stdout.d.ts +7 -0
- package/types/interfaces/wasi-cli-types.d.ts +15 -0
- package/types/interfaces/wasi-clocks-monotonic-clock.d.ts +33 -0
- package/types/interfaces/wasi-clocks-system-clock.d.ts +38 -0
- package/types/interfaces/wasi-clocks-types.d.ts +5 -0
- package/types/interfaces/wasi-filesystem-preopens.d.ts +6 -0
- package/types/interfaces/wasi-filesystem-types.d.ts +780 -0
- package/types/interfaces/wasi-http-client.d.ts +9 -0
- package/types/interfaces/wasi-http-handler.d.ts +9 -0
- package/types/interfaces/wasi-http-types.d.ts +619 -0
- package/types/interfaces/wasi-random-insecure-seed.d.ts +21 -0
- package/types/interfaces/wasi-random-insecure.d.ts +26 -0
- package/types/interfaces/wasi-random-random.d.ts +31 -0
- package/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +77 -0
- package/types/interfaces/wasi-sockets-types.d.ts +830 -0
- package/types/random.d.ts +3 -0
- package/types/sockets.d.ts +2 -0
- package/types/wasi-cli-command.d.ts +23 -0
- package/types/wasi-http-service.d.ts +14 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
import { StreamReader, readableByteStreamFromReader } from "../stream.js";
|
|
5
|
+
import { FutureReader } from "../future.js";
|
|
6
|
+
import { ResourceWorker } from "../workers/resource-worker.js";
|
|
7
|
+
import { earlyDispose, registerDispose } from "../finalization.js";
|
|
8
|
+
|
|
9
|
+
import { FSError } from "./error.js";
|
|
10
|
+
import { wasiTypeFromDirent } from "./utils.js";
|
|
11
|
+
|
|
12
|
+
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
13
|
+
|
|
14
|
+
let WORKER = null;
|
|
15
|
+
function worker() {
|
|
16
|
+
return (WORKER ??= new ResourceWorker(
|
|
17
|
+
new URL("../workers/filesystem-worker.js", import.meta.url),
|
|
18
|
+
));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Descriptor {
|
|
22
|
+
/** Node.js file handle for file operations (FileHandle from fs.promises.open) */
|
|
23
|
+
#handle;
|
|
24
|
+
/** Descriptor flags */
|
|
25
|
+
#mode;
|
|
26
|
+
/** Absolute filesystem path to the file or directory */
|
|
27
|
+
#fullPath;
|
|
28
|
+
/** Cleanup finalizer */
|
|
29
|
+
#finalizer;
|
|
30
|
+
/** Host filesystem path for preopened directories */
|
|
31
|
+
#hostPreopen;
|
|
32
|
+
|
|
33
|
+
static _create(handle, mode, fullPath) {
|
|
34
|
+
const {
|
|
35
|
+
read = false,
|
|
36
|
+
write = false,
|
|
37
|
+
fileIntegritySync = false,
|
|
38
|
+
dataIntegritySync = false,
|
|
39
|
+
requestedWriteSync = false,
|
|
40
|
+
mutateDirectory = false,
|
|
41
|
+
} = mode;
|
|
42
|
+
|
|
43
|
+
const merged = {
|
|
44
|
+
read,
|
|
45
|
+
write,
|
|
46
|
+
fileIntegritySync,
|
|
47
|
+
dataIntegritySync,
|
|
48
|
+
requestedWriteSync,
|
|
49
|
+
mutateDirectory,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const desc = new Descriptor();
|
|
53
|
+
desc.#handle = handle;
|
|
54
|
+
desc.#fullPath = fullPath;
|
|
55
|
+
desc.#mode = merged;
|
|
56
|
+
desc.#finalizer = registerDispose(desc, null, handle, (handle) => handle.close());
|
|
57
|
+
|
|
58
|
+
return desc;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static _createPreopen(hostPreopen) {
|
|
62
|
+
const desc = new Descriptor();
|
|
63
|
+
|
|
64
|
+
if (hostPreopen.endsWith("/")) {
|
|
65
|
+
desc.#hostPreopen = hostPreopen.slice(0, -1) || "/";
|
|
66
|
+
} else {
|
|
67
|
+
desc.#hostPreopen = hostPreopen;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Windows requires UNC paths at minimum
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
desc.#hostPreopen = desc.#hostPreopen.replace(/\\/g, "/");
|
|
73
|
+
if (desc.#hostPreopen === "/") {
|
|
74
|
+
desc.#hostPreopen = "//";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return desc;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[symbolDispose]() {
|
|
81
|
+
if (this.#finalizer) {
|
|
82
|
+
earlyDispose(this.#finalizer);
|
|
83
|
+
this.#finalizer = null;
|
|
84
|
+
}
|
|
85
|
+
this.#handle = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return a stream for reading from a file.
|
|
90
|
+
* WIT:
|
|
91
|
+
* ```
|
|
92
|
+
* read-via-stream: func(offset: filesize) -> tuple<stream<u8>, future<result<_, error-code>>>
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @param {bigint} offset The offset within the file.
|
|
96
|
+
* @returns {{stream: StreamReader, future: FutureReader}}
|
|
97
|
+
* A tuple: a readable byte stream and a future that resolves to an error code.
|
|
98
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
99
|
+
*/
|
|
100
|
+
readViaStream(offset) {
|
|
101
|
+
this.#ensureHandle();
|
|
102
|
+
const transform = new TransformStream();
|
|
103
|
+
const promise = worker()
|
|
104
|
+
.run(
|
|
105
|
+
{
|
|
106
|
+
op: "read",
|
|
107
|
+
fd: this.#handle.fd,
|
|
108
|
+
offset,
|
|
109
|
+
stream: transform.writable,
|
|
110
|
+
},
|
|
111
|
+
[transform.writable],
|
|
112
|
+
)
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
throw FSError.from(err);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return [new StreamReader(transform.readable), new FutureReader(promise)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write to a file via a byte stream.
|
|
122
|
+
* WIT:
|
|
123
|
+
* ```
|
|
124
|
+
* write-via-stream: async func(data: stream<u8>, offset: filesize) -> result<_, error-code>
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* @async
|
|
128
|
+
* @param {object} data A readable byte stream.
|
|
129
|
+
* @param {bigint} offset The offset within the file.
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
132
|
+
*/
|
|
133
|
+
async writeViaStream(data, offset) {
|
|
134
|
+
this.#ensureWritable(data);
|
|
135
|
+
const stream = readableByteStreamFromReader(data, { name: "file write data" });
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await worker().run({ op: "write", fd: this.#handle.fd, offset, stream }, [stream]);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw FSError.from(err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Append to a file via a byte stream.
|
|
146
|
+
* WIT:
|
|
147
|
+
* ```
|
|
148
|
+
* append-via-stream: async func(data: stream<u8>) -> result<_, error-code>
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @async
|
|
152
|
+
* @param {object} data A readable byte stream.
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
155
|
+
*/
|
|
156
|
+
async appendViaStream(data) {
|
|
157
|
+
this.#ensureWritable(data);
|
|
158
|
+
const offset = await this.#handle.stat().then((s) => s.size);
|
|
159
|
+
return this.writeViaStream(data, offset);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Advisory on file access patterns.
|
|
164
|
+
* WIT:
|
|
165
|
+
* ```
|
|
166
|
+
* advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @async
|
|
170
|
+
* @param {bigint} offset Start of the region.
|
|
171
|
+
* @param {bigint} length Length of the region.
|
|
172
|
+
* @param {string} advice One of the Wasi `advice` values.
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
175
|
+
*/
|
|
176
|
+
async advise(_offset, _length, _advice) {
|
|
177
|
+
// TODO: This not directly suported on Node.js:
|
|
178
|
+
// https://github.com/bytecodealliance/jco/issues/718
|
|
179
|
+
if ((await this.getType()).tag === "directory") {
|
|
180
|
+
throw new FSError("bad-descriptor");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Synchronize file data to disk.
|
|
186
|
+
* WIT:
|
|
187
|
+
* ```
|
|
188
|
+
* sync-data: async func() -> result<_, error-code>
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* @async
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
194
|
+
*/
|
|
195
|
+
async syncData() {
|
|
196
|
+
this.#ensureHandle();
|
|
197
|
+
try {
|
|
198
|
+
await this.#handle.datasync();
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// On windows, `sync_data` uses `FileFlushBuffers` which fails with `EPERM` if
|
|
201
|
+
// the file is not upen for writing. Ignore this error, for POSIX compatibility.
|
|
202
|
+
if (process.platform === "win32" && e.code === "EPERM") {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
throw FSError.from(e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get flags associated with a descriptor.
|
|
211
|
+
* WIT:
|
|
212
|
+
* ```
|
|
213
|
+
* get-flags: async func() -> result<descriptor-flags, error-code>
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @async
|
|
217
|
+
* @returns {Promise<object>} The descriptor flags object.
|
|
218
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
219
|
+
*/
|
|
220
|
+
async getFlags() {
|
|
221
|
+
return this.#mode;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get the dynamic type of a descriptor.
|
|
226
|
+
* WIT:
|
|
227
|
+
* ```
|
|
228
|
+
* get-type: async func() -> result<descriptor-type, error-code>
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @async
|
|
232
|
+
* @returns {Promise<string>} The descriptor type.
|
|
233
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
234
|
+
*/
|
|
235
|
+
async getType() {
|
|
236
|
+
if (this.#hostPreopen) {
|
|
237
|
+
return { tag: "directory" };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const stats = await this.#handle.stat();
|
|
241
|
+
return wasiTypeFromDirent(stats);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
throw FSError.from(e);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Adjust the size of an open file.
|
|
249
|
+
* WIT:
|
|
250
|
+
* ```
|
|
251
|
+
* set-size: async func(size: filesize) -> result<_, error-code>
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* @async
|
|
255
|
+
* @param {bigint} size The new size.
|
|
256
|
+
* @returns {Promise<void>}
|
|
257
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
258
|
+
*/
|
|
259
|
+
async setSize(size) {
|
|
260
|
+
this.#ensureHandle();
|
|
261
|
+
|
|
262
|
+
if (size > Number.MAX_SAFE_INTEGER) {
|
|
263
|
+
throw new FSError("invalid");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await this.#handle.truncate(Number(size));
|
|
268
|
+
} catch (e) {
|
|
269
|
+
if (process.platform === "win32" && e.code === "EPERM") {
|
|
270
|
+
throw new FSError("access");
|
|
271
|
+
}
|
|
272
|
+
throw FSError.from(e);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Adjust the timestamps of an open file or directory.
|
|
278
|
+
* WIT:
|
|
279
|
+
* ```
|
|
280
|
+
* set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @async
|
|
284
|
+
* @param {object} atimeDesc new-timestamp descriptor
|
|
285
|
+
* @param {object} mtimeDesc new-timestamp descriptor
|
|
286
|
+
* @returns {Promise<void>}
|
|
287
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
288
|
+
*/
|
|
289
|
+
async setTimes(atimeDesc, mtimeDesc) {
|
|
290
|
+
this.#ensureHandle();
|
|
291
|
+
|
|
292
|
+
const { atime, mtime } = await this.#computeTimestamps(atimeDesc, mtimeDesc);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
await this.#handle.utimes(atime, mtime);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
throw FSError.from(e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Read directory entries via a stream.
|
|
303
|
+
* WIT:
|
|
304
|
+
* ```
|
|
305
|
+
* read-directory: async func() -> tuple<stream<directory-entry>, future<result<_, error-code>>>
|
|
306
|
+
* ```
|
|
307
|
+
*
|
|
308
|
+
* @async
|
|
309
|
+
* @returns {{stream: StreamReader, future: FutureReader}}
|
|
310
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
311
|
+
*/
|
|
312
|
+
readDirectory() {
|
|
313
|
+
const fullPath = this.#fullPath ?? this.#hostPreopen;
|
|
314
|
+
if (!fullPath) {
|
|
315
|
+
throw new FSError("invalid");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const preopens = preopenEntries.map(([desc, _virtualPath]) => desc.#hostPreopen);
|
|
319
|
+
|
|
320
|
+
const transform = new TransformStream();
|
|
321
|
+
const promise = worker()
|
|
322
|
+
.run(
|
|
323
|
+
{
|
|
324
|
+
op: "readDir",
|
|
325
|
+
fullPath,
|
|
326
|
+
stream: transform.writable,
|
|
327
|
+
preopens,
|
|
328
|
+
},
|
|
329
|
+
[transform.writable],
|
|
330
|
+
)
|
|
331
|
+
.catch((err) => {
|
|
332
|
+
throw FSError.from(err);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return [new StreamReader(transform.readable), new FutureReader(promise)];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Synchronize data & metadata to disk.
|
|
340
|
+
* WIT:
|
|
341
|
+
* ```
|
|
342
|
+
* sync: async func() -> result<_, error-code>
|
|
343
|
+
* ```
|
|
344
|
+
*
|
|
345
|
+
* @async
|
|
346
|
+
* @returns {Promise<void>}
|
|
347
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
348
|
+
*/
|
|
349
|
+
async sync() {
|
|
350
|
+
return this.#hostPreopen ? this.#syncDir() : this.#syncFile();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a directory.
|
|
355
|
+
* WIT:
|
|
356
|
+
* ```
|
|
357
|
+
* create-directory-at: async func(path: string) -> result<_, error-code>
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* @async
|
|
361
|
+
* @param {string} path Relative path.
|
|
362
|
+
* @returns {Promise<void>}
|
|
363
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
364
|
+
*/
|
|
365
|
+
async createDirectoryAt(path) {
|
|
366
|
+
const full = this.#getFullPath(path, false);
|
|
367
|
+
try {
|
|
368
|
+
await fs.mkdir(full);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
throw FSError.from(e);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Return attributes of an open file or directory.
|
|
376
|
+
* WIT:
|
|
377
|
+
* ```
|
|
378
|
+
* stat: async func() -> result<descriptor-stat, error-code>
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* @async
|
|
382
|
+
* @returns {Promise<object>} File statistics.
|
|
383
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
384
|
+
*/
|
|
385
|
+
async stat() {
|
|
386
|
+
try {
|
|
387
|
+
let s;
|
|
388
|
+
if (this.#hostPreopen) {
|
|
389
|
+
s = await fs.stat(this.#hostPreopen, { bigint: true });
|
|
390
|
+
} else {
|
|
391
|
+
this.#ensureHandle();
|
|
392
|
+
s = await this.#handle.stat({ bigint: true });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
type: wasiTypeFromDirent(s),
|
|
397
|
+
linkCount: s.nlink,
|
|
398
|
+
size: s.size,
|
|
399
|
+
dataAccessTimestamp: nsToDateTime(s.atimeNs),
|
|
400
|
+
dataModificationTimestamp: nsToDateTime(s.mtimeNs),
|
|
401
|
+
statusChangeTimestamp: nsToDateTime(s.ctimeNs),
|
|
402
|
+
};
|
|
403
|
+
} catch (e) {
|
|
404
|
+
throw FSError.from(e);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Adjust timestamps on a file or symlink.
|
|
410
|
+
* WIT:
|
|
411
|
+
* ```
|
|
412
|
+
* set-times-at: async func(
|
|
413
|
+
* path-flags: path-flags,
|
|
414
|
+
* path: string,
|
|
415
|
+
* data-access-timestamp: new-timestamp,
|
|
416
|
+
* data-modification-timestamp: new-timestamp
|
|
417
|
+
* ) -> result<_, error-code>
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @async
|
|
421
|
+
* @param {object} flags Path flags.
|
|
422
|
+
* @param {string} path Relative path.
|
|
423
|
+
* @param {object} atimeDesc New access timestamp.
|
|
424
|
+
* @param {object} mtimeDesc New modification timestamp.
|
|
425
|
+
* @returns {Promise<void>}
|
|
426
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
427
|
+
*/
|
|
428
|
+
async statAt(flags, path) {
|
|
429
|
+
const full = this.#getFullPath(path, flags.symlinkFollow);
|
|
430
|
+
try {
|
|
431
|
+
const statFn = flags.symlinkFollow ? fs.stat : fs.lstat;
|
|
432
|
+
const s = await statFn(full, { bigint: true });
|
|
433
|
+
return {
|
|
434
|
+
type: wasiTypeFromDirent(s),
|
|
435
|
+
linkCount: s.nlink,
|
|
436
|
+
size: s.size,
|
|
437
|
+
dataAccessTimestamp: nsToDateTime(s.atimeNs),
|
|
438
|
+
dataModificationTimestamp: nsToDateTime(s.mtimeNs),
|
|
439
|
+
statusChangeTimestamp: nsToDateTime(s.ctimeNs),
|
|
440
|
+
};
|
|
441
|
+
} catch (e) {
|
|
442
|
+
throw FSError.from(e);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Adjust timestamps on a file or symlink.
|
|
448
|
+
* WIT:
|
|
449
|
+
* ```
|
|
450
|
+
* set-times-at: async func(
|
|
451
|
+
* path-flags: path-flags,
|
|
452
|
+
* path: string,
|
|
453
|
+
* data-access-timestamp: new-timestamp,
|
|
454
|
+
* data-modification-timestamp: new-timestamp
|
|
455
|
+
* ) -> result<_, error-code>
|
|
456
|
+
* ```
|
|
457
|
+
*
|
|
458
|
+
* @async
|
|
459
|
+
* @param {object} flags Path flags.
|
|
460
|
+
* @param {string} path Relative path.
|
|
461
|
+
* @param {object} atimeDesc New access timestamp.
|
|
462
|
+
* @param {object} mtimeDesc New modification timestamp.
|
|
463
|
+
* @returns {Promise<void>}
|
|
464
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
465
|
+
*/
|
|
466
|
+
async setTimesAt(flags, path, atimeDesc, mtimeDesc) {
|
|
467
|
+
const { atime, mtime } = await this.#computeTimestamps(atimeDesc, mtimeDesc, path);
|
|
468
|
+
|
|
469
|
+
if (!flags.symlinkFollow && !fs.lutimes) {
|
|
470
|
+
throw new FSError("unsupported");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const full = this.#getFullPath(path, flags.symlinkFollow);
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const utimesFn = flags.symlinkFollow ? fs.utimes : fs.lutimes;
|
|
477
|
+
await utimesFn(full, atime, mtime);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
throw FSError.from(e);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Create a hard link.
|
|
485
|
+
* WIT:
|
|
486
|
+
* ```
|
|
487
|
+
* link-at: async func(
|
|
488
|
+
* old-path-flags: path-flags,
|
|
489
|
+
* old-path: string,
|
|
490
|
+
* new-descriptor: borrow<descriptor>,
|
|
491
|
+
* new-path: string
|
|
492
|
+
* ) -> result<_, error-code>
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* @async
|
|
496
|
+
* @param {object} oldFlags Old path flags.
|
|
497
|
+
* @param {string} oldPath Source path.
|
|
498
|
+
* @param {Descriptor} newDesc Target descriptor.
|
|
499
|
+
* @param {string} newPath Destination path.
|
|
500
|
+
* @returns {Promise<void>}
|
|
501
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
502
|
+
*/
|
|
503
|
+
async linkAt(oldFlags, oldPath, newDesc, newPath) {
|
|
504
|
+
const src = this.#getFullPath(oldPath, oldFlags.symlinkFollow);
|
|
505
|
+
const dst = newDesc.#getFullPath(newPath, false);
|
|
506
|
+
|
|
507
|
+
// On Windows, a trailing '/' means the path is treated as a
|
|
508
|
+
// directory and hard links to directory are not supported
|
|
509
|
+
if (process.platform === "win32" && dst.endsWith("/")) {
|
|
510
|
+
throw new FSError("no-entry");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await fs.link(src, dst);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
throw FSError.from(e);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Open a file or directory.
|
|
522
|
+
* WIT:
|
|
523
|
+
* ```
|
|
524
|
+
* open-at: async func(
|
|
525
|
+
* path-flags: path-flags,
|
|
526
|
+
* path: string,
|
|
527
|
+
* open-flags: open-flags,
|
|
528
|
+
* %flags: descriptor-flags
|
|
529
|
+
* ) -> result<descriptor, error-code>
|
|
530
|
+
* ```
|
|
531
|
+
*
|
|
532
|
+
* @async
|
|
533
|
+
* @param {object} pf - path flags
|
|
534
|
+
* @param {string} path - relative path
|
|
535
|
+
* @param {object} of - open flags
|
|
536
|
+
* @param {object} df - descriptor flags
|
|
537
|
+
* @returns {Promise<Descriptor>}
|
|
538
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
539
|
+
*/
|
|
540
|
+
async openAt(pf, path, of, df) {
|
|
541
|
+
if (!preopenEntries.length) {
|
|
542
|
+
throw new FSError("access");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const fullPath = this.#getFullPath(path, pf.symlinkFollow);
|
|
546
|
+
const target = stripTrailingSlash(fullPath);
|
|
547
|
+
|
|
548
|
+
const makeFsFlags = () => {
|
|
549
|
+
let fsFlags = 0;
|
|
550
|
+
if (of.create) {
|
|
551
|
+
fsFlags |= fs.constants.O_CREAT;
|
|
552
|
+
}
|
|
553
|
+
if (of.directory) {
|
|
554
|
+
fsFlags |= fs.constants.O_DIRECTORY;
|
|
555
|
+
}
|
|
556
|
+
if (of.exclusive) {
|
|
557
|
+
fsFlags |= fs.constants.O_EXCL;
|
|
558
|
+
}
|
|
559
|
+
if (of.truncate) {
|
|
560
|
+
fsFlags |= fs.constants.O_TRUNC;
|
|
561
|
+
}
|
|
562
|
+
if (df.read && df.write) {
|
|
563
|
+
fsFlags |= fs.constants.O_RDWR;
|
|
564
|
+
} else if (df.write) {
|
|
565
|
+
fsFlags |= fs.constants.O_WRONLY;
|
|
566
|
+
} else if (df.read) {
|
|
567
|
+
fsFlags |= fs.constants.O_RDONLY;
|
|
568
|
+
}
|
|
569
|
+
if (df.fileIntegritySync) {
|
|
570
|
+
fsFlags |= fs.constants.O_SYNC;
|
|
571
|
+
}
|
|
572
|
+
if (df.requestedWriteSync) {
|
|
573
|
+
fsFlags |= fs.constants.O_SYNC;
|
|
574
|
+
}
|
|
575
|
+
if (df.dataIntegritySync) {
|
|
576
|
+
fsFlags |= fs.constants.O_DSYNC;
|
|
577
|
+
}
|
|
578
|
+
if (!pf.symlinkFollow) {
|
|
579
|
+
fsFlags |= fs.constants.O_NOFOLLOW;
|
|
580
|
+
}
|
|
581
|
+
return fsFlags;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const fsFlags = makeFsFlags();
|
|
585
|
+
|
|
586
|
+
if (process.platform === "win32") {
|
|
587
|
+
if (!pf.symlinkFollow && !of.create) {
|
|
588
|
+
const isSymlink = await fs
|
|
589
|
+
.lstat(fullPath)
|
|
590
|
+
.then((p) => p.isSymbolicLink())
|
|
591
|
+
.catch(() => false);
|
|
592
|
+
|
|
593
|
+
if (isSymlink) {
|
|
594
|
+
const tag = of.directory ? "not-directory" : "loop";
|
|
595
|
+
throw new FSError(tag);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (pf.symlinkFollow && of.directory) {
|
|
600
|
+
const isFile = await fs
|
|
601
|
+
.stat(fullPath)
|
|
602
|
+
.then((p) => !p.isDirectory())
|
|
603
|
+
.catch(() => false);
|
|
604
|
+
|
|
605
|
+
if (isFile) {
|
|
606
|
+
throw new FSError("not-directory");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const handle = await fs.open(target, fsFlags);
|
|
613
|
+
const desc = descriptorCreate(handle, df, fullPath);
|
|
614
|
+
const isDir = (await desc.getType()).tag === "directory";
|
|
615
|
+
|
|
616
|
+
if (fullPath.endsWith("/") && !isDir) {
|
|
617
|
+
desc[symbolDispose]();
|
|
618
|
+
throw new FSError("not-directory");
|
|
619
|
+
}
|
|
620
|
+
return desc;
|
|
621
|
+
} catch (err) {
|
|
622
|
+
if (err.code === "ERR_INVALID_ARG_VALUE") {
|
|
623
|
+
throw new FSError(process.platform === "win32" ? "no-entry" : "invalid");
|
|
624
|
+
}
|
|
625
|
+
throw FSError.from(err);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Read the contents of a symbolic link.
|
|
631
|
+
* WIT:
|
|
632
|
+
* ```
|
|
633
|
+
* readlink-at: async func(path: string) -> result<string, error-code>
|
|
634
|
+
* ```
|
|
635
|
+
*
|
|
636
|
+
* @async
|
|
637
|
+
* @param {string} path Relative path.
|
|
638
|
+
* @returns {Promise<string>}
|
|
639
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
640
|
+
*/
|
|
641
|
+
async readlinkAt(path) {
|
|
642
|
+
const full = this.#getFullPath(path, false);
|
|
643
|
+
try {
|
|
644
|
+
return await fs.readlink(full);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
throw FSError.from(e);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Remove a directory.
|
|
652
|
+
* WIT:
|
|
653
|
+
* ```
|
|
654
|
+
* remove-directory-at: async func(path: string) -> result<_, error-code>
|
|
655
|
+
* ```
|
|
656
|
+
*
|
|
657
|
+
* @async
|
|
658
|
+
* @param {string} path Relative path.
|
|
659
|
+
* @returns {Promise<void>}
|
|
660
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
661
|
+
*/
|
|
662
|
+
async removeDirectoryAt(path) {
|
|
663
|
+
const full = this.#getFullPath(path, false);
|
|
664
|
+
try {
|
|
665
|
+
await fs.rmdir(full);
|
|
666
|
+
} catch (e) {
|
|
667
|
+
if (process.platform === "win32" && e.code === "ENOENT") {
|
|
668
|
+
throw new FSError("not-directory");
|
|
669
|
+
}
|
|
670
|
+
throw FSError.from(e);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Rename a filesystem object.
|
|
676
|
+
* WIT:
|
|
677
|
+
* ```
|
|
678
|
+
* rename-at: async func(old-path: string, new-descriptor: borrow<descriptor>, new-path: string) -> result<_, error-code>
|
|
679
|
+
* ```
|
|
680
|
+
*
|
|
681
|
+
* @async
|
|
682
|
+
* @param {string} oldPath Source path.
|
|
683
|
+
* @param {Descriptor} newDesc Target descriptor.
|
|
684
|
+
* @param {string} newPath Destination path.
|
|
685
|
+
* @returns {Promise<void>}
|
|
686
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
687
|
+
*/
|
|
688
|
+
async renameAt(oldPath, newDesc, newPath) {
|
|
689
|
+
const src = this.#getFullPath(oldPath, false);
|
|
690
|
+
const dst = newDesc.#getFullPath(newPath, false);
|
|
691
|
+
try {
|
|
692
|
+
await fs.rename(src, dst);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
if (process.platform === "win32" && e.code === "EPERM") {
|
|
695
|
+
throw new FSError("access");
|
|
696
|
+
}
|
|
697
|
+
throw FSError.from(e);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Create a symbolic link.
|
|
703
|
+
* WIT:
|
|
704
|
+
* ```
|
|
705
|
+
* symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>
|
|
706
|
+
* ```
|
|
707
|
+
*
|
|
708
|
+
* @async
|
|
709
|
+
* @param {string} target Link target.
|
|
710
|
+
* @param {string} path Destination path.
|
|
711
|
+
* @returns {Promise<void>}
|
|
712
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
713
|
+
*/
|
|
714
|
+
async symlinkAt(target, path) {
|
|
715
|
+
if (target.startsWith("/")) {
|
|
716
|
+
throw new FSError("not-permitted");
|
|
717
|
+
}
|
|
718
|
+
const full = this.#getFullPath(path, false);
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
await fs.symlink(target, full);
|
|
722
|
+
} catch (e) {
|
|
723
|
+
if (full.endsWith("/") && e.code === "EEXIST") {
|
|
724
|
+
const isDir = (await fs.stat(full)).isDirectory();
|
|
725
|
+
const isWin = process.platform === "win32";
|
|
726
|
+
if (!isDir) {
|
|
727
|
+
const tag = isWin ? "no-entry" : "not-directory";
|
|
728
|
+
throw new FSError(tag);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (process.platform === "win32" && ["EPERM", "EEXIST"].includes(e.code)) {
|
|
733
|
+
throw new FSError("no-entry");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
throw FSError.from(e);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Unlink a filesystem object that is not a directory.
|
|
742
|
+
* WIT:
|
|
743
|
+
* ```
|
|
744
|
+
* unlink-file-at: async func(path: string) -> result<_, error-code>
|
|
745
|
+
* ```
|
|
746
|
+
*
|
|
747
|
+
* @async
|
|
748
|
+
* @param {string} path Relative path.
|
|
749
|
+
* @returns {Promise<void>}
|
|
750
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
751
|
+
*/
|
|
752
|
+
async unlinkFileAt(path) {
|
|
753
|
+
const full = this.#getFullPath(path, false);
|
|
754
|
+
if (full.endsWith("/")) {
|
|
755
|
+
const isDir = (await fs.stat(full)).isDirectory();
|
|
756
|
+
if (isDir) {
|
|
757
|
+
if (process.platform === "win32") {
|
|
758
|
+
throw new FSError("access");
|
|
759
|
+
} else if (process.platform === "darwin") {
|
|
760
|
+
throw new FSError("not-permitted");
|
|
761
|
+
} else {
|
|
762
|
+
throw new FSError("is-directory");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
await fs.unlink(full);
|
|
769
|
+
} catch (err) {
|
|
770
|
+
if (process.platform === "win32" && err.code === "EPERM") {
|
|
771
|
+
throw new FSError("access");
|
|
772
|
+
} else {
|
|
773
|
+
throw FSError.from(err);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Test whether two descriptors refer to the same object.
|
|
780
|
+
* WIT:
|
|
781
|
+
* ```
|
|
782
|
+
* is-same-object: async func(other: borrow<descriptor>) -> bool
|
|
783
|
+
* ```
|
|
784
|
+
*
|
|
785
|
+
* @async
|
|
786
|
+
* @param {Descriptor} other Another descriptor.
|
|
787
|
+
* @returns {Promise<boolean>}
|
|
788
|
+
*/
|
|
789
|
+
async isSameObject(other) {
|
|
790
|
+
return other === this;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Return a hash of the metadata associated with this descriptor.
|
|
795
|
+
* WIT:
|
|
796
|
+
* ```
|
|
797
|
+
* metadata-hash: async func() -> result<metadata-hash-value, error-code>
|
|
798
|
+
* ```
|
|
799
|
+
*
|
|
800
|
+
* @async
|
|
801
|
+
* @returns {Promise<{upper: bigint, lower: bigint}>}
|
|
802
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
803
|
+
*/
|
|
804
|
+
async metadataHash() {
|
|
805
|
+
if (this.#hostPreopen) {
|
|
806
|
+
return { upper: 0n, lower: BigInt(this._id) };
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const s = await this.#handle.stat();
|
|
810
|
+
return { upper: s.mtimeNs, lower: s.ino };
|
|
811
|
+
} catch (e) {
|
|
812
|
+
throw FSError.from(e);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Return a hash of the metadata via path.
|
|
818
|
+
* WIT:
|
|
819
|
+
* ```
|
|
820
|
+
* metadata-hash-at: async func(path-flags: path-flags, path: string) -> result<metadata-hash-value, error-code>
|
|
821
|
+
* ```
|
|
822
|
+
*
|
|
823
|
+
* @async
|
|
824
|
+
* @param {object} flags Path flags.
|
|
825
|
+
* @param {string} path Relative path.
|
|
826
|
+
* @returns {Promise<{upper: bigint, lower: bigint}>}
|
|
827
|
+
* @throws {FSError} `payload.tag` contains mapped WASI error code.
|
|
828
|
+
*/
|
|
829
|
+
async metadataHashAt(flags, path) {
|
|
830
|
+
const full = this.#getFullPath(path, false);
|
|
831
|
+
try {
|
|
832
|
+
const statFn = flags.symlinkFollow ? fs.stat : fs.lstat;
|
|
833
|
+
const s = await statFn(full);
|
|
834
|
+
return { upper: s.mtimeNs, lower: s.ino };
|
|
835
|
+
} catch (e) {
|
|
836
|
+
throw FSError.from(e);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async #syncDir() {
|
|
841
|
+
try {
|
|
842
|
+
const handle = await fs.open(this.#hostPreopen, fs.constants.O_RDONLY);
|
|
843
|
+
try {
|
|
844
|
+
await handle.sync();
|
|
845
|
+
} finally {
|
|
846
|
+
await handle.close();
|
|
847
|
+
}
|
|
848
|
+
} catch (e) {
|
|
849
|
+
throw FSError.from(e);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async #syncFile() {
|
|
854
|
+
this.#ensureHandle();
|
|
855
|
+
try {
|
|
856
|
+
await this.#handle.sync();
|
|
857
|
+
} catch (e) {
|
|
858
|
+
// On windows, `sync_data` uses `FileFlushBuffers` which fails with `EPERM` if
|
|
859
|
+
// the file is not upen for writing. Ignore this error, for POSIX compatibility.
|
|
860
|
+
if (process.platform === "win32" && e.code === "EPERM") {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
throw FSError.from(e);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#getNewTimestamp(newTimestamp, oldTimestamp) {
|
|
868
|
+
switch (newTimestamp.tag) {
|
|
869
|
+
case "now":
|
|
870
|
+
return Math.floor(Date.now() / 1e3);
|
|
871
|
+
case "no-change":
|
|
872
|
+
return timestampToSec(oldTimestamp);
|
|
873
|
+
case "timestamp":
|
|
874
|
+
return timestampToSec(newTimestamp.val);
|
|
875
|
+
default:
|
|
876
|
+
throw new FSError("invalid", `Unknown new-timestamp tag: ${newTimestamp.tag}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async #computeTimestamps(atimeDesc, mtimeDesc, path = null) {
|
|
881
|
+
let stats;
|
|
882
|
+
if (atimeDesc.tag === "no-change" || mtimeDesc.tag === "no-change") {
|
|
883
|
+
const flags = { symlinkFollow: true };
|
|
884
|
+
const p = path === null ? this.stat() : this.statAt(flags, path);
|
|
885
|
+
stats = await p;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const atime = this.#getNewTimestamp(atimeDesc, stats?.dataAccessTimestamp);
|
|
889
|
+
const mtime = this.#getNewTimestamp(mtimeDesc, stats?.dataModificationTimestamp);
|
|
890
|
+
return { atime, mtime };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Resolve a relative subpath against this descriptor.
|
|
895
|
+
*/
|
|
896
|
+
#getFullPath(subpath, _followSymlinks) {
|
|
897
|
+
subpath = subpath.replaceAll("\\", "/").replace(/\/\/+/g, "/");
|
|
898
|
+
|
|
899
|
+
if (subpath.startsWith("/")) {
|
|
900
|
+
throw new FSError("not-permitted");
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const segments = [];
|
|
904
|
+
for (const seg of subpath.split("/")) {
|
|
905
|
+
if (seg === "" || seg === ".") {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (seg === "..") {
|
|
909
|
+
if (segments.length === 0) {
|
|
910
|
+
throw new FSError("not-permitted");
|
|
911
|
+
}
|
|
912
|
+
segments.pop();
|
|
913
|
+
} else {
|
|
914
|
+
segments.push(seg);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const base = this.#hostPreopen ?? this.#fullPath;
|
|
919
|
+
if (segments.length === 0) {
|
|
920
|
+
return base;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const baseNormalized = stripTrailingSlash(base);
|
|
924
|
+
return `${baseNormalized}/${segments.join("/")}`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
#ensureHandle() {
|
|
928
|
+
if (!this.#handle) {
|
|
929
|
+
throw new FSError("bad-descriptor");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
#ensureWritable(data) {
|
|
934
|
+
this.#ensureHandle();
|
|
935
|
+
if (!this.#mode.write) {
|
|
936
|
+
data?.[symbolDispose]?.();
|
|
937
|
+
data?.close?.();
|
|
938
|
+
throw new FSError("bad-descriptor");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const descriptorCreatePreopen = Descriptor._createPreopen;
|
|
944
|
+
delete Descriptor._createPreopen;
|
|
945
|
+
|
|
946
|
+
const descriptorCreate = Descriptor._create;
|
|
947
|
+
delete Descriptor._create;
|
|
948
|
+
|
|
949
|
+
const NS_PER_SEC = 1_000_000_000n;
|
|
950
|
+
function nsToDateTime(ns) {
|
|
951
|
+
const seconds = ns / NS_PER_SEC;
|
|
952
|
+
const nanoseconds = Number(ns % NS_PER_SEC);
|
|
953
|
+
return { seconds, nanoseconds };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function timestampToSec({ seconds, nanoseconds }) {
|
|
957
|
+
return Number(seconds) + nanoseconds / 1e9;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function stripTrailingSlash(path) {
|
|
961
|
+
if (path === "/") {
|
|
962
|
+
return "/";
|
|
963
|
+
}
|
|
964
|
+
return path.replace(/\/+$/, "");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const preopenEntries = [];
|
|
968
|
+
export const preopens = {
|
|
969
|
+
Descriptor,
|
|
970
|
+
getDirectories: () => preopenEntries,
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
export const types = {
|
|
974
|
+
Descriptor,
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Set the preopen table.
|
|
979
|
+
*
|
|
980
|
+
* @param {Record<string, string>} entries
|
|
981
|
+
*/
|
|
982
|
+
export function _setPreopens(entries) {
|
|
983
|
+
preopenEntries.length = 0;
|
|
984
|
+
Object.entries(entries).forEach(([virtualPath, hostPreopen]) =>
|
|
985
|
+
_addPreopen(virtualPath, hostPreopen),
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Add a new preopen mapping.
|
|
991
|
+
*
|
|
992
|
+
* @param {string} virtualPath
|
|
993
|
+
* @param {string} hostPreopen
|
|
994
|
+
*/
|
|
995
|
+
export function _addPreopen(virtualPath, hostPreopen) {
|
|
996
|
+
preopenEntries.push([descriptorCreatePreopen(hostPreopen), virtualPath]);
|
|
997
|
+
}
|