@polyengine/wasi 0.1.0-pre.g633468a
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 +202 -0
- package/README.md +23 -0
- package/esm/cli.js +160 -0
- package/esm/cli_stdio.js +203 -0
- package/esm/clocks.js +84 -0
- package/esm/filesystem.js +83 -0
- package/esm/filesystem_node.js +463 -0
- package/esm/filesystem_web.js +315 -0
- package/esm/http.js +663 -0
- package/esm/internal/cli_shared.js +26 -0
- package/esm/internal/fs_provider.js +795 -0
- package/esm/internal/sockets_02.js +992 -0
- package/esm/internal/sockets_03.js +969 -0
- package/esm/internal/sockets_platform.js +530 -0
- package/esm/internal/sockets_shared.js +268 -0
- package/esm/io.js +643 -0
- package/esm/mod.js +80 -0
- package/esm/package.json +3 -0
- package/esm/random.js +96 -0
- package/esm/sockets.js +172 -0
- package/package.json +98 -0
- package/types/cli.d.ts +44 -0
- package/types/cli_stdio.d.ts +31 -0
- package/types/clocks.d.ts +8 -0
- package/types/filesystem.d.ts +30 -0
- package/types/filesystem_node.d.ts +16 -0
- package/types/filesystem_web.d.ts +61 -0
- package/types/http.d.ts +169 -0
- package/types/internal/cli_shared.d.ts +26 -0
- package/types/internal/fs_provider.d.ts +174 -0
- package/types/internal/sockets_02.d.ts +37 -0
- package/types/internal/sockets_03.d.ts +12 -0
- package/types/internal/sockets_platform.d.ts +112 -0
- package/types/internal/sockets_shared.d.ts +256 -0
- package/types/io.d.ts +177 -0
- package/types/mod.d.ts +31 -0
- package/types/random.d.ts +17 -0
- package/types/sockets.d.ts +22 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// INTERNAL shared vocabulary of the `wasi:sockets` tracks
|
|
2
|
+
// (sockets_03.ts, sockets_02.ts) — not a package export; the public home
|
|
3
|
+
// of these names is `@polyengine/wasi/sockets`. Address codec, wasmtime-parity
|
|
4
|
+
// validation, platform error mapping, and the WIT-facing type shapes.
|
|
5
|
+
import { ComponentException } from "@polyengine/runtime/embedder";
|
|
6
|
+
/**
|
|
7
|
+
* The datagram payload ceiling, matching wasmtime-wasi's
|
|
8
|
+
* `MAX_UDP_DATAGRAM_SIZE` (`u16::MAX`). Larger sends fail
|
|
9
|
+
* `datagram-too-large` before reaching the OS; receives use a buffer of
|
|
10
|
+
* this size so no datagram the OS delivers is ever truncated.
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_UDP_DATAGRAM_SIZE = 65535;
|
|
13
|
+
/**
|
|
14
|
+
* A WIT `err` the branded way (contracts/embedder-api.md, "Error model"):
|
|
15
|
+
* an unbranded throw would become a trap naming the import instead of a
|
|
16
|
+
* guest-visible err.
|
|
17
|
+
*/
|
|
18
|
+
export function componentError(payload, detail) {
|
|
19
|
+
return new ComponentException(payload, `wasi:sockets/types@0.3: ${detail}`);
|
|
20
|
+
}
|
|
21
|
+
// --- address codec ------------------------------------------------------------
|
|
22
|
+
/** Render the address part of `addr` as a Deno hostname string. */
|
|
23
|
+
export function ipHostname(addr) {
|
|
24
|
+
if (addr.kind === "ipv4")
|
|
25
|
+
return addr.value.address.join(".");
|
|
26
|
+
// The uncompressed spelling; Deno's address parser accepts it.
|
|
27
|
+
return addr.value.address.map((g) => g.toString(16)).join(":");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a Deno `NetAddr` back into a WIT `ip-socket-address`.
|
|
31
|
+
*
|
|
32
|
+
* Handles the compressed (`::1`), full, IPv4-embedded (`::ffff:127.0.0.1` —
|
|
33
|
+
* what a dual-stack socket reports for IPv4 senders), and zoned
|
|
34
|
+
* (`fe80::1%3`) hostname spellings. A zone parses as the numeric scope-id
|
|
35
|
+
* when it is numeric and drops to 0 otherwise (interface names are not
|
|
36
|
+
* representable in the WIT shape); flow-info is not observable and is
|
|
37
|
+
* always 0.
|
|
38
|
+
*/
|
|
39
|
+
export function parseNetAddr(addr) {
|
|
40
|
+
const host = addr.hostname;
|
|
41
|
+
if (!host.includes(":")) {
|
|
42
|
+
const octets = host.split(".").map(Number);
|
|
43
|
+
if (octets.length !== 4 || octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) {
|
|
44
|
+
throw componentError({ kind: "other", value: `unparseable IPv4 hostname ${JSON.stringify(host)}` }, `unparseable IPv4 hostname ${JSON.stringify(host)}`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
kind: "ipv4",
|
|
48
|
+
value: { port: addr.port, address: octets },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const { groups, scopeId } = parseIpv6Hostname(host);
|
|
52
|
+
return {
|
|
53
|
+
kind: "ipv6",
|
|
54
|
+
value: { port: addr.port, flowInfo: 0, address: groups, scopeId },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseIpv6Hostname(hostname) {
|
|
58
|
+
let host = hostname;
|
|
59
|
+
let scopeId = 0;
|
|
60
|
+
const pct = host.indexOf("%");
|
|
61
|
+
if (pct >= 0) {
|
|
62
|
+
const zone = Number(host.slice(pct + 1));
|
|
63
|
+
scopeId = Number.isInteger(zone) && zone >= 0 ? zone : 0;
|
|
64
|
+
host = host.slice(0, pct);
|
|
65
|
+
}
|
|
66
|
+
const fail = () => {
|
|
67
|
+
throw componentError({ kind: "other", value: `unparseable IPv6 hostname ${JSON.stringify(hostname)}` }, `unparseable IPv6 hostname ${JSON.stringify(hostname)}`);
|
|
68
|
+
};
|
|
69
|
+
const dc = host.indexOf("::");
|
|
70
|
+
let head;
|
|
71
|
+
let tail;
|
|
72
|
+
if (dc >= 0) {
|
|
73
|
+
if (host.indexOf("::", dc + 1) >= 0)
|
|
74
|
+
fail();
|
|
75
|
+
head = dc === 0 ? [] : host.slice(0, dc).split(":");
|
|
76
|
+
tail = dc + 2 === host.length ? [] : host.slice(dc + 2).split(":");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
head = host.split(":");
|
|
80
|
+
tail = [];
|
|
81
|
+
}
|
|
82
|
+
// An embedded dotted quad ("::ffff:127.0.0.1") occupies the last two
|
|
83
|
+
// groups.
|
|
84
|
+
const last = tail.length > 0 ? tail : head;
|
|
85
|
+
const pieces = [...head];
|
|
86
|
+
let tailPieces = [...tail];
|
|
87
|
+
let v4Tail;
|
|
88
|
+
if (last.length > 0 && last[last.length - 1].includes(".")) {
|
|
89
|
+
const quad = last[last.length - 1].split(".").map(Number);
|
|
90
|
+
if (quad.length !== 4 || quad.some((o) => !Number.isInteger(o) || o < 0 || o > 255))
|
|
91
|
+
fail();
|
|
92
|
+
v4Tail = [(quad[0] << 8) | quad[1], (quad[2] << 8) | quad[3]];
|
|
93
|
+
if (tail.length > 0)
|
|
94
|
+
tailPieces = tail.slice(0, -1);
|
|
95
|
+
else
|
|
96
|
+
pieces.pop();
|
|
97
|
+
}
|
|
98
|
+
const parseGroup = (piece) => {
|
|
99
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(piece))
|
|
100
|
+
fail();
|
|
101
|
+
return parseInt(piece, 16);
|
|
102
|
+
};
|
|
103
|
+
const headGroups = pieces.map(parseGroup);
|
|
104
|
+
const tailGroups = [...tailPieces.map(parseGroup), ...(v4Tail ?? [])];
|
|
105
|
+
const total = headGroups.length + tailGroups.length;
|
|
106
|
+
if (dc >= 0) {
|
|
107
|
+
if (total > 7)
|
|
108
|
+
fail();
|
|
109
|
+
while (headGroups.length + tailGroups.length < 8)
|
|
110
|
+
headGroups.push(0);
|
|
111
|
+
}
|
|
112
|
+
else if (total !== 8) {
|
|
113
|
+
fail();
|
|
114
|
+
}
|
|
115
|
+
return { groups: [...headGroups, ...tailGroups], scopeId };
|
|
116
|
+
}
|
|
117
|
+
// --- validation (wasmtime-wasi `sockets/util.rs` parity) ----------------------
|
|
118
|
+
function isV4MappedV6(groups) {
|
|
119
|
+
return groups[0] === 0 && groups[1] === 0 && groups[2] === 0 &&
|
|
120
|
+
groups[3] === 0 && groups[4] === 0 && groups[5] === 0xffff;
|
|
121
|
+
}
|
|
122
|
+
/** The deprecated IPv4-compatible range, excluding `::` and `::1`. */
|
|
123
|
+
function isDeprecatedV4CompatibleV6(groups) {
|
|
124
|
+
const headZero = groups.slice(0, 6).every((g) => g === 0);
|
|
125
|
+
if (!headZero)
|
|
126
|
+
return false;
|
|
127
|
+
const unspecified = groups[6] === 0 && groups[7] === 0;
|
|
128
|
+
const localhost = groups[6] === 0 && groups[7] === 1;
|
|
129
|
+
return !unspecified && !localhost;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether `addr` may cross this socket's family boundary: same family, and
|
|
133
|
+
* never an IPv4-mapped or deprecated IPv4-compatible IPv6 address.
|
|
134
|
+
*/
|
|
135
|
+
export function isValidAddressFamily(family, addr) {
|
|
136
|
+
if (family === "ipv4")
|
|
137
|
+
return addr.kind === "ipv4";
|
|
138
|
+
return addr.kind === "ipv6" &&
|
|
139
|
+
!isV4MappedV6(addr.value.address) &&
|
|
140
|
+
!isDeprecatedV4CompatibleV6(addr.value.address);
|
|
141
|
+
}
|
|
142
|
+
export function isUnspecified(addr) {
|
|
143
|
+
if (addr.kind === "ipv4")
|
|
144
|
+
return addr.value.address.every((o) => o === 0);
|
|
145
|
+
return addr.value.address.every((g) => g === 0);
|
|
146
|
+
}
|
|
147
|
+
/** Same endpoint: family, address, and port (udp connected-mode filter). */
|
|
148
|
+
export function sameSocketAddress(a, b) {
|
|
149
|
+
if (a.kind !== b.kind || a.value.port !== b.value.port)
|
|
150
|
+
return false;
|
|
151
|
+
return a.value.address.length === b.value.address.length &&
|
|
152
|
+
a.value.address.every((part, i) => part === b.value.address[i]);
|
|
153
|
+
}
|
|
154
|
+
// --- error mapping ------------------------------------------------------------
|
|
155
|
+
/** `e instanceof Deno.errors[name]`, tolerating hosts/versions lacking the class. */
|
|
156
|
+
export function isDenoError(e, name) {
|
|
157
|
+
const deno = globalThis.Deno;
|
|
158
|
+
if (typeof deno !== "object" || deno === null)
|
|
159
|
+
return false;
|
|
160
|
+
const errors = deno.errors;
|
|
161
|
+
if (typeof errors !== "object" || errors === null)
|
|
162
|
+
return false;
|
|
163
|
+
const cls = errors[name];
|
|
164
|
+
return typeof cls === "function" &&
|
|
165
|
+
e instanceof cls;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Node-style `err.code` -> WIT `error-code` (the node backend's whole
|
|
169
|
+
* error vocabulary, and a sharper channel than Deno's classes where both
|
|
170
|
+
* exist). The `ERR_*` rows are the adapters' closed-under-a-pending-op
|
|
171
|
+
* signals, mirroring what Deno's BadResource maps to.
|
|
172
|
+
*/
|
|
173
|
+
const CODE_ERRORS = {
|
|
174
|
+
EADDRINUSE: { kind: "address-in-use" },
|
|
175
|
+
EADDRNOTAVAIL: { kind: "address-not-bindable" },
|
|
176
|
+
ECONNREFUSED: { kind: "connection-refused" },
|
|
177
|
+
ECONNRESET: { kind: "connection-reset" },
|
|
178
|
+
ECONNABORTED: { kind: "connection-aborted" },
|
|
179
|
+
EHOSTUNREACH: { kind: "remote-unreachable" },
|
|
180
|
+
EHOSTDOWN: { kind: "remote-unreachable" },
|
|
181
|
+
ENETUNREACH: { kind: "remote-unreachable" },
|
|
182
|
+
ENETDOWN: { kind: "remote-unreachable" },
|
|
183
|
+
ENONET: { kind: "remote-unreachable" },
|
|
184
|
+
EACCES: { kind: "access-denied" },
|
|
185
|
+
EPERM: { kind: "access-denied" },
|
|
186
|
+
ETIMEDOUT: { kind: "timeout" },
|
|
187
|
+
EMSGSIZE: { kind: "datagram-too-large" },
|
|
188
|
+
EPIPE: { kind: "connection-broken" },
|
|
189
|
+
EINVAL: { kind: "invalid-argument" },
|
|
190
|
+
ENOTSUP: { kind: "not-supported" },
|
|
191
|
+
EOPNOTSUPP: { kind: "not-supported" },
|
|
192
|
+
ERR_SOCKET_DGRAM_NOT_RUNNING: { kind: "invalid-state" },
|
|
193
|
+
ERR_SERVER_NOT_RUNNING: { kind: "invalid-state" },
|
|
194
|
+
ERR_STREAM_DESTROYED: { kind: "invalid-state" },
|
|
195
|
+
ERR_STREAM_WRITE_AFTER_END: { kind: "invalid-state" },
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Map a platform failure onto the WIT `error-code` vocabulary, mirroring
|
|
199
|
+
* wasmtime-wasi's io-error table where the platform exposes the
|
|
200
|
+
* distinction — Deno's error classes first, then Node-style `code`
|
|
201
|
+
* strings, then the plain-`Error` spellings Deno leaves unclassified. An
|
|
202
|
+
* already-branded error passes through unchanged — the codec and the
|
|
203
|
+
* capability re-detection throw branded errors from inside the same try
|
|
204
|
+
* blocks that guard the platform calls, and re-wrapping one would demote
|
|
205
|
+
* its payload to `other`.
|
|
206
|
+
*/
|
|
207
|
+
export function mapPlatformError(e, what) {
|
|
208
|
+
if (e instanceof ComponentException)
|
|
209
|
+
return e;
|
|
210
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
211
|
+
const err = (payload) => componentError(payload, `${what}: ${message}`);
|
|
212
|
+
if (isDenoError(e, "AddrInUse"))
|
|
213
|
+
return err({ kind: "address-in-use" });
|
|
214
|
+
if (isDenoError(e, "AddrNotAvailable"))
|
|
215
|
+
return err({ kind: "address-not-bindable" });
|
|
216
|
+
if (isDenoError(e, "ConnectionRefused"))
|
|
217
|
+
return err({ kind: "connection-refused" });
|
|
218
|
+
if (isDenoError(e, "ConnectionReset"))
|
|
219
|
+
return err({ kind: "connection-reset" });
|
|
220
|
+
if (isDenoError(e, "ConnectionAborted"))
|
|
221
|
+
return err({ kind: "connection-aborted" });
|
|
222
|
+
if (isDenoError(e, "NetworkUnreachable") || isDenoError(e, "HostUnreachable")) {
|
|
223
|
+
return err({ kind: "remote-unreachable" });
|
|
224
|
+
}
|
|
225
|
+
if (isDenoError(e, "PermissionDenied") || isDenoError(e, "NotCapable")) {
|
|
226
|
+
return err({ kind: "access-denied" });
|
|
227
|
+
}
|
|
228
|
+
if (isDenoError(e, "TimedOut"))
|
|
229
|
+
return err({ kind: "timeout" });
|
|
230
|
+
// A socket closed under a pending operation: the operation was not valid
|
|
231
|
+
// in the socket's (now closed) state.
|
|
232
|
+
if (isDenoError(e, "Interrupted") || isDenoError(e, "BadResource")) {
|
|
233
|
+
return err({ kind: "invalid-state" });
|
|
234
|
+
}
|
|
235
|
+
if (isDenoError(e, "NotSupported"))
|
|
236
|
+
return err({ kind: "not-supported" });
|
|
237
|
+
const code = e?.code;
|
|
238
|
+
if (typeof code === "string" && code in CODE_ERRORS)
|
|
239
|
+
return err(CODE_ERRORS[code]);
|
|
240
|
+
// EMSGSIZE surfaces as a plain Error, not a Deno.errors class.
|
|
241
|
+
if (/message too long/i.test(message))
|
|
242
|
+
return err({ kind: "datagram-too-large" });
|
|
243
|
+
// EPIPE (a write on a peer-closed connection) also surfaces as a plain
|
|
244
|
+
// Error in Deno.
|
|
245
|
+
if (/broken pipe/i.test(message))
|
|
246
|
+
return err({ kind: "connection-broken" });
|
|
247
|
+
if (e instanceof TypeError)
|
|
248
|
+
return err({ kind: "invalid-argument" });
|
|
249
|
+
return err({ kind: "other", value: message });
|
|
250
|
+
}
|
|
251
|
+
export const RESULT_OK = { kind: "ok" };
|
|
252
|
+
export const RESULT_INVALID_STATE = {
|
|
253
|
+
kind: "err",
|
|
254
|
+
value: { kind: "invalid-state" },
|
|
255
|
+
};
|
|
256
|
+
/** The err side of a `SocketResult`, mapped from a platform failure. */
|
|
257
|
+
export function resultErrOf(e, what) {
|
|
258
|
+
return { kind: "err", value: mapPlatformError(e, what).payload };
|
|
259
|
+
}
|
|
260
|
+
/** The family's wildcard address, port 0 (tcp listen's implicit bind). */
|
|
261
|
+
export function wildcardAddress(family) {
|
|
262
|
+
return family === "ipv4"
|
|
263
|
+
? { kind: "ipv4", value: { port: 0, address: [0, 0, 0, 0] } }
|
|
264
|
+
: {
|
|
265
|
+
kind: "ipv6",
|
|
266
|
+
value: { port: 0, flowInfo: 0, address: [0, 0, 0, 0, 0, 0, 0, 0], scopeId: 0 },
|
|
267
|
+
};
|
|
268
|
+
}
|