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