@eryx-org/eryx 0.0.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +281 -4
  2. package/eryx-sandbox.conditional-host-import-0.wasm +0 -0
  3. package/eryx-sandbox.conditional-host-import-1.wasm +0 -0
  4. package/eryx-sandbox.conditional-suspending-1-i32-to-i32.wasm +0 -0
  5. package/eryx-sandbox.core.wasm +0 -0
  6. package/eryx-sandbox.core10.wasm +0 -0
  7. package/eryx-sandbox.core11.wasm +0 -0
  8. package/eryx-sandbox.core12.wasm +0 -0
  9. package/eryx-sandbox.core13.wasm +0 -0
  10. package/eryx-sandbox.core2.wasm +0 -0
  11. package/eryx-sandbox.core3.wasm +0 -0
  12. package/eryx-sandbox.core4.wasm +0 -0
  13. package/eryx-sandbox.core5.wasm +0 -0
  14. package/eryx-sandbox.core6.wasm +0 -0
  15. package/eryx-sandbox.core7.wasm +0 -0
  16. package/eryx-sandbox.core8.wasm +0 -0
  17. package/eryx-sandbox.core9.wasm +0 -0
  18. package/eryx-sandbox.d.ts +185 -0
  19. package/eryx-sandbox.js +55329 -0
  20. package/index.d.ts +331 -0
  21. package/index.js +698 -0
  22. package/instance.js +174 -0
  23. package/interfaces/eryx-net-tcp.d.ts +39 -0
  24. package/interfaces/eryx-net-tls.d.ts +33 -0
  25. package/interfaces/wasi-cli-environment.d.ts +3 -0
  26. package/interfaces/wasi-cli-exit.d.ts +3 -0
  27. package/interfaces/wasi-cli-stderr.d.ts +3 -0
  28. package/interfaces/wasi-cli-stdin.d.ts +3 -0
  29. package/interfaces/wasi-cli-stdout.d.ts +3 -0
  30. package/interfaces/wasi-cli-terminal-input.d.ts +8 -0
  31. package/interfaces/wasi-cli-terminal-output.d.ts +8 -0
  32. package/interfaces/wasi-cli-terminal-stderr.d.ts +3 -0
  33. package/interfaces/wasi-cli-terminal-stdin.d.ts +3 -0
  34. package/interfaces/wasi-cli-terminal-stdout.d.ts +3 -0
  35. package/interfaces/wasi-clocks-monotonic-clock.d.ts +8 -0
  36. package/interfaces/wasi-clocks-wall-clock.d.ts +7 -0
  37. package/interfaces/wasi-filesystem-preopens.d.ts +3 -0
  38. package/interfaces/wasi-filesystem-types.d.ts +288 -0
  39. package/interfaces/wasi-io-error.d.ts +8 -0
  40. package/interfaces/wasi-io-poll.d.ts +10 -0
  41. package/interfaces/wasi-io-streams.d.ts +51 -0
  42. package/interfaces/wasi-random-random.d.ts +2 -0
  43. package/interfaces/wasi-sockets-instance-network.d.ts +3 -0
  44. package/interfaces/wasi-sockets-ip-name-lookup.d.ts +21 -0
  45. package/interfaces/wasi-sockets-network.d.ts +92 -0
  46. package/interfaces/wasi-sockets-tcp-create-socket.d.ts +8 -0
  47. package/interfaces/wasi-sockets-tcp.d.ts +127 -0
  48. package/interfaces/wasi-sockets-udp-create-socket.d.ts +8 -0
  49. package/interfaces/wasi-sockets-udp.d.ts +93 -0
  50. package/package.json +69 -3
  51. package/python-stdlib.tar.gz +0 -0
  52. package/shims/callbacks.d.ts +45 -0
  53. package/shims/callbacks.js +469 -0
  54. package/shims/ip.js +46 -0
  55. package/shims/net-fetch.js +264 -0
  56. package/shims/net-node.js +236 -0
  57. package/shims/net.d.ts +84 -0
  58. package/shims/net.js +260 -0
  59. package/shims/preview2-node.js +35 -0
  60. package/shims/raw-json.js +111 -0
  61. package/shims/sockets.js +53 -0
  62. package/stdlib-loader.js +174 -0
  63. package/worker.js +100 -0
package/shims/net.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Network shims for the eryx sandbox (eryx:net/tcp and eryx:net/tls).
3
+ *
4
+ * Networking is disabled until the host sets a policy, per sandbox or as the
5
+ * default with setNetConfig(). Host policy mirrors the Rust crate's NetConfig:
6
+ * blocked patterns are always enforced, then the host must match allowedHosts
7
+ * unless allowAllHosts is set. Hosts are
8
+ * matched in their WHATWG URL form too, so `2130706433` and `0x7f.1` read as
9
+ * `127.0.0.1`. IP addresses are checked against blockedNetworks (minus
10
+ * allowedNetworks); in Node, so are the addresses a name resolves to, unless
11
+ * the host matches internalHosts. Browsers don't expose DNS answers, so there
12
+ * only an allowlist keeps names that resolve to private addresses out.
13
+ *
14
+ * The transport depends on the environment:
15
+ * - Node.js: real TCP/TLS sockets (net-node.js)
16
+ * - Browsers: HTTP(S) only, replayed through fetch() (net-fetch.js)
17
+ *
18
+ * The guest calls these imports synchronously; they return Promises and are
19
+ * transpiled as JSPI async imports, which suspend the wasm stack until they
20
+ * settle. jco lowers `result<T, E>` imports as: return T, or throw
21
+ * `{ payload: E }`.
22
+ */
23
+
24
+ const isNode =
25
+ typeof globalThis.process !== "undefined" &&
26
+ typeof globalThis.process.versions?.node !== "undefined";
27
+
28
+ const transport = isNode ? await import("./net-node.js") : await import("./net-fetch.js");
29
+
30
+ /** The Rust crate's `NetConfig::default().blocked_hosts`, plus `0.0.0.0` and `[::]`. */
31
+ export const DEFAULT_BLOCKED_HOSTS = Object.freeze([
32
+ "localhost",
33
+ "*.localhost",
34
+ "127.*",
35
+ "10.*",
36
+ ...Array.from({ length: 16 }, (_, i) => `172.${16 + i}.*`),
37
+ "192.168.*",
38
+ "169.254.*",
39
+ "[::1]",
40
+ "0.0.0.0",
41
+ "[::]",
42
+ ]);
43
+
44
+ /**
45
+ * The Rust crate's `DEFAULT_BLOCKED_NETWORKS`. IPv4-mapped, IPv4-compatible,
46
+ * NAT64 and Teredo addresses are also checked by the IPv4 address they embed.
47
+ */
48
+ export const DEFAULT_BLOCKED_NETWORKS = Object.freeze([
49
+ "0.0.0.0/8",
50
+ "10.0.0.0/8",
51
+ "100.64.0.0/10",
52
+ "127.0.0.0/8",
53
+ "169.254.0.0/16",
54
+ "172.16.0.0/12",
55
+ "192.0.0.0/24",
56
+ "192.168.0.0/16",
57
+ "198.18.0.0/15",
58
+ "224.0.0.0/4",
59
+ "240.0.0.0/4",
60
+ "::/96",
61
+ "2001::/32",
62
+ "2001:db8::/32",
63
+ "2002::/16",
64
+ "64:ff9b:1::/48",
65
+ "fc00::/7",
66
+ "fe80::/10",
67
+ "fec0::/10",
68
+ "ff00::/8",
69
+ ]);
70
+
71
+ /** Same defaults as the Rust crate's `NetConfig::default()`. */
72
+ const DEFAULT_MAX_CONNECTIONS = 10;
73
+ const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
74
+ const DEFAULT_IO_TIMEOUT_MS = 60_000;
75
+
76
+ /**
77
+ * A checked policy; `isAllowed` checks resolved addresses (null where the
78
+ * transport can't see them).
79
+ *
80
+ * @typedef {Required<import("./net.js").NetConfig> & { isAllowed: ((address: string, family: number) => boolean) | null }} Policy
81
+ */
82
+
83
+ /**
84
+ * The policy new instances start with, set by setNetConfig().
85
+ * @type {Policy | null}
86
+ */
87
+ let defaultPolicy = null;
88
+
89
+ /** Strict CIDR syntax, so a typo can't parse as a wider network (`10.0.0.0/` as `/0`). */
90
+ function checkCidrs(cidrs) {
91
+ for (const cidr of cidrs) {
92
+ const m = /^([0-9a-fA-F:.]+)\/(\d{1,3})$/.exec(cidr);
93
+ if (!m || Number(m[2]) > (m[1].includes(":") ? 128 : 32)) {
94
+ throw new TypeError(`invalid CIDR '${cidr}'`);
95
+ }
96
+ }
97
+ return cidrs;
98
+ }
99
+
100
+ /** A non-negative integer, or a TypeError. */
101
+ function checkCount(name, value) {
102
+ if (!Number.isInteger(value) || value < 0) {
103
+ throw new TypeError(`${name} must be a non-negative integer, got ${value}`);
104
+ }
105
+ return value;
106
+ }
107
+
108
+ /** A timeout above zero, or a TypeError: zero would time out every connection, as in Rust. */
109
+ function checkTimeout(name, value) {
110
+ if (typeof value !== "number" || !(value > 0)) {
111
+ throw new TypeError(`${name} must be greater than zero, got ${value}`);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ /**
117
+ * Check `netConfig` and fill in the defaults; null disables networking.
118
+ *
119
+ * Throws a TypeError if a network is not a valid CIDR, maxConnections is
120
+ * not a non-negative integer, a timeout is not greater than zero, or
121
+ * httpRules (the Rust crate's method and path rules) are given.
122
+ *
123
+ * @param {import("./net.js").NetConfig | null} netConfig
124
+ * @returns {Policy | null}
125
+ */
126
+ function toPolicy(netConfig) {
127
+ if (!netConfig) return null;
128
+ // Requests aren't parsed here, so rules couldn't be enforced.
129
+ if (netConfig.httpRules?.length) {
130
+ throw new TypeError("httpRules are not supported by the JavaScript host");
131
+ }
132
+ // Copies, so changing the caller's arrays later can't change the policy.
133
+ const list = (xs, fallback) => Object.freeze([...(xs ?? fallback)]);
134
+ const next = {
135
+ allowAllHosts: netConfig.allowAllHosts ?? false,
136
+ allowedHosts: list(netConfig.allowedHosts, []),
137
+ blockedHosts: list(netConfig.blockedHosts, DEFAULT_BLOCKED_HOSTS),
138
+ blockedNetworks: checkCidrs(list(netConfig.blockedNetworks, DEFAULT_BLOCKED_NETWORKS)),
139
+ allowedNetworks: checkCidrs(list(netConfig.allowedNetworks, [])),
140
+ internalHosts: list(netConfig.internalHosts, []),
141
+ maxConnections: checkCount("maxConnections", netConfig.maxConnections ?? DEFAULT_MAX_CONNECTIONS),
142
+ connectTimeoutMs: checkTimeout("connectTimeoutMs", netConfig.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS),
143
+ ioTimeoutMs: checkTimeout("ioTimeoutMs", netConfig.ioTimeoutMs ?? DEFAULT_IO_TIMEOUT_MS),
144
+ };
145
+ return Object.freeze({ ...next, isAllowed: transport.networkFilter(next) });
146
+ }
147
+
148
+ /**
149
+ * Set the default policy (`null` disables networking), which each sandbox
150
+ * copies when it is created; sandboxes that already exist keep theirs.
151
+ *
152
+ * @param {import("./net.js").NetConfig | null} netConfig
153
+ */
154
+ export function setNetConfig(netConfig) {
155
+ defaultPolicy = toPolicy(netConfig);
156
+ defaultNet.setNetConfig(netConfig);
157
+ }
158
+
159
+ /**
160
+ * The guest's timeout (`0` for none) clamped to the host's, so sandboxed code
161
+ * can shorten a timeout but never extend it.
162
+ */
163
+ // setTimeout fires at once for delays past 2^31 - 1 ms.
164
+ const clamp = (hostMs, guestMs) => Math.min(guestMs ? Math.min(guestMs, hostMs) : hostMs, 2 ** 31 - 1);
165
+
166
+ /**
167
+ * `host` in WHATWG URL form (lower-cased, IPv4 in dotted decimal, IPv6
168
+ * bracketed and compressed) without a trailing dot, or null if it isn't a
169
+ * bare host name or address.
170
+ */
171
+ function canonicalHost(host) {
172
+ // URL parsing would read these as userinfo, path or port delimiters.
173
+ if (!host || /[\s/?#@\\]/.test(host)) return null;
174
+ try {
175
+ return new URL(`http://${host.includes(":") ? `[${host}]` : host}`).hostname.replace(/\.$/, "");
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /** Glob match with `*` wildcards, case-insensitive (like the Rust matcher). */
182
+ function hostMatches(host, pattern) {
183
+ const re = pattern
184
+ .split("*")
185
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
186
+ .join(".*");
187
+ return new RegExp(`^${re}$`, "i").test(host);
188
+ }
189
+
190
+ const notPermitted = (val) => ({ payload: { tag: "not-permitted", val } });
191
+
192
+ /** Check `host` against `config` and return its canonical form. */
193
+ function checkHost(config, host) {
194
+ if (!config) throw notPermitted("networking is disabled; enable it with setNetConfig()");
195
+ const canonical = canonicalHost(host);
196
+ if (canonical === null) throw notPermitted(`host '${host}' is not a valid host name`);
197
+ // Refusals match either spelling; allowances only the canonical one.
198
+ const matches = (patterns) => patterns.some((p) => hostMatches(canonical, p));
199
+ if (config.blockedHosts.some((p) => hostMatches(host, p)) || matches(config.blockedHosts)) {
200
+ throw notPermitted(`host '${host}' is blocked`);
201
+ }
202
+ if (!config.allowAllHosts && !matches(config.allowedHosts)) {
203
+ throw notPermitted(`host '${host}' not in allowed list`);
204
+ }
205
+ return { canonical, internal: matches(config.internalHosts) };
206
+ }
207
+
208
+ /**
209
+ * The eryx:net imports for one sandbox instance, with its own connection
210
+ * table and policy. The policy starts as the default set with setNetConfig();
211
+ * `setNetConfig()` on the result replaces it for this instance only, for
212
+ * connections opened after the call.
213
+ *
214
+ * `closeConnections()` closes every connection the instance's code left open;
215
+ * the sandbox calls it after each call into the instance.
216
+ */
217
+ export function createNet() {
218
+ const t = transport.createTransport();
219
+ let config = defaultPolicy;
220
+ const connectTimeout = (ms) => clamp(config?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS, ms);
221
+ const ioTimeout = (ms) => clamp(config?.ioTimeoutMs ?? DEFAULT_IO_TIMEOUT_MS, ms);
222
+ return {
223
+ tcp: {
224
+ async connect(host, port, timeoutMs) {
225
+ const { canonical, internal } = checkHost(config, host);
226
+ // 0 means no limit, as in the Rust crate.
227
+ if (config.maxConnections > 0 && t.openConnections() >= config.maxConnections) {
228
+ throw notPermitted(`connection limit reached (${config.maxConnections})`);
229
+ }
230
+ // The transport checks the address (Node: the resolved addresses)
231
+ // with this; internal hosts skip it.
232
+ const isAllowed = internal ? null : config.isAllowed;
233
+ // Connect to the form that was checked, unbracketed for the transport.
234
+ const target = canonical.replace(/^\[(.*)\]$/, "$1");
235
+ return t.tcp.connect(target, port, connectTimeout(timeoutMs), isAllowed);
236
+ },
237
+ read: (h, len, timeoutMs) => t.tcp.read(h, len, ioTimeout(timeoutMs)),
238
+ write: (h, timeoutMs, data) => t.tcp.write(h, ioTimeout(timeoutMs), data),
239
+ close: t.tcp.close,
240
+ },
241
+ tls: {
242
+ upgrade: (h, hostname, timeoutMs) => t.tls.upgrade(h, hostname, connectTimeout(timeoutMs)),
243
+ read: (h, len, timeoutMs) => t.tls.read(h, len, ioTimeout(timeoutMs)),
244
+ write: (h, timeoutMs, data) => t.tls.write(h, ioTimeout(timeoutMs), data),
245
+ close: t.tls.close,
246
+ },
247
+ closeConnections: t.closeAll,
248
+ /** @param {import("./net.js").NetConfig | null} netConfig */
249
+ setNetConfig(netConfig) {
250
+ config = toPolicy(netConfig);
251
+ },
252
+ };
253
+ }
254
+
255
+ /**
256
+ * The default instance, for code that imports the shims directly. It follows
257
+ * setNetConfig().
258
+ */
259
+ const defaultNet = createNet();
260
+ export const { tcp, tls, closeConnections } = defaultNet;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Node.js entry for the `#preview2/*` package imports.
3
+ *
4
+ * Under the "node" export condition, `@bytecodealliance/preview2-shim` resolves to
5
+ * its Node.js shims, which back WASI with the host filesystem and process and lack
6
+ * `_setFileData`. The sandbox needs the in-memory browser shims everywhere, so this
7
+ * loads them by file URL (bypassing the package's export conditions). Browsers and
8
+ * bundlers take the "default" branch in package.json and import them directly.
9
+ */
10
+
11
+ async function load(name) {
12
+ const url = import.meta.resolve(`@bytecodealliance/preview2-shim/${name}`);
13
+ if (!url.includes("/dist/nodejs/")) {
14
+ throw new Error(`unexpected preview2-shim layout: ${url}`);
15
+ }
16
+ return import(url.replace("/dist/nodejs/", "/dist/browser/"));
17
+ }
18
+
19
+ export const {
20
+ environment,
21
+ exit,
22
+ stderr,
23
+ stdin,
24
+ stdout,
25
+ terminalInput,
26
+ terminalOutput,
27
+ terminalStderr,
28
+ terminalStdin,
29
+ terminalStdout,
30
+ } = await load("cli");
31
+ export const { monotonicClock, wallClock } = await load("clocks");
32
+ export const { preopens, types, _setFileData, createFilesystem, InMemoryFilesystemAdapter } =
33
+ await load("filesystem");
34
+ export const { error, poll, streams } = await load("io");
35
+ export const { random } = await load("random");
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A JSON parser that keeps each value's source text.
3
+ *
4
+ * Callback replay needs this to store and replay callback results exactly as
5
+ * they were returned (`1.0` stays a float, large integers stay exact), which
6
+ * `JSON.parse` cannot do. Journal keys need no parsing: they are the guest's
7
+ * canonical argument text, used verbatim.
8
+ */
9
+
10
+ const SPACE = /\s*/y;
11
+ // Strings are scanned by hand in parseJson (linear by construction, and
12
+ // guest-controlled input never meets a backtracking string pattern).
13
+ const TOKEN =
14
+ /(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}[\]:,])|(true|false|null)/y;
15
+
16
+ /**
17
+ * A parsed JSON value that keeps its source text.
18
+ * @typedef {Object} JsonNode
19
+ * @property {"string"|"number"|"literal"|"array"|"object"} kind
20
+ * @property {*} value - Decoded string, number lexeme, literal text, array of
21
+ * nodes, or Map of key to node (duplicate keys: last wins)
22
+ * @property {string} raw - The node's exact source text
23
+ */
24
+
25
+ /**
26
+ * Parse JSON without losing number lexemes or source text.
27
+ *
28
+ * (Not `JSON.parse` with a reviver's `context.source`: besides being missing on
29
+ * older engines, it intermittently returned wrong keys and no source under
30
+ * Node 24 during differential testing.)
31
+ *
32
+ * @param {string} text - A JSON document
33
+ * @returns {JsonNode}
34
+ * @throws {SyntaxError} If `text` is not valid JSON
35
+ */
36
+ export function parseJson(text) {
37
+ let pos = 0;
38
+ // Returns [text consumed, string, number, punctuation, literal].
39
+ const next = () => {
40
+ const from = pos;
41
+ SPACE.lastIndex = pos;
42
+ SPACE.exec(text);
43
+ const start = SPACE.lastIndex;
44
+ if (text[start] === '"') {
45
+ let end = start + 1;
46
+ while (end < text.length && text[end] !== '"') {
47
+ end += text[end] === "\\" ? 2 : 1;
48
+ }
49
+ if (end >= text.length) {
50
+ throw new SyntaxError(`Unterminated string at position ${start}`);
51
+ }
52
+ pos = end + 1;
53
+ return [text.slice(from, pos), text.slice(start, pos)];
54
+ }
55
+ TOKEN.lastIndex = start;
56
+ const m = TOKEN.exec(text);
57
+ if (!m) throw new SyntaxError(`Invalid JSON at position ${start}`);
58
+ pos = TOKEN.lastIndex;
59
+ return [text.slice(from, pos), undefined, m[1], m[2], m[3]];
60
+ };
61
+ const expect = (m, punct) => {
62
+ if (m[3] !== punct) {
63
+ throw new SyntaxError(`Expected '${punct}' at position ${pos}`);
64
+ }
65
+ };
66
+ const node = (m) => {
67
+ const start = pos - m[0].trimStart().length;
68
+ const done = (kind, value) => ({
69
+ kind,
70
+ value,
71
+ raw: text.slice(start, pos),
72
+ });
73
+ if (m[1]) return done("string", JSON.parse(m[1]));
74
+ if (m[2]) return done("number", m[2]);
75
+ if (m[4]) return done("literal", m[4]);
76
+ if (m[3] === "[") {
77
+ const items = [];
78
+ for (let t = next(); t[3] !== "]";) {
79
+ if (items.length) {
80
+ expect(t, ",");
81
+ t = next();
82
+ }
83
+ items.push(node(t));
84
+ t = next();
85
+ }
86
+ return done("array", items);
87
+ }
88
+ if (m[3] === "{") {
89
+ const entries = new Map();
90
+ let first = true;
91
+ for (let t = next(); t[3] !== "}"; first = false) {
92
+ if (!first) {
93
+ expect(t, ",");
94
+ t = next();
95
+ }
96
+ if (!t[1]) throw new SyntaxError(`Expected a key at position ${pos}`);
97
+ const key = JSON.parse(t[1]);
98
+ expect(next(), ":");
99
+ entries.set(key, node(next()));
100
+ t = next();
101
+ }
102
+ return done("object", entries);
103
+ }
104
+ throw new SyntaxError(`Unexpected token at position ${pos}`);
105
+ };
106
+ const root = node(next());
107
+ if (text.slice(pos).trim() !== "") {
108
+ throw new SyntaxError(`Unexpected data at position ${pos}`);
109
+ }
110
+ return root;
111
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sockets shims for the eryx sandbox.
3
+ *
4
+ * The Python sandbox imports wasi:sockets/* but doesn't actually use them
5
+ * (networking goes through eryx:net/tcp and eryx:net/tls instead).
6
+ * These stubs satisfy the imports without providing real functionality.
7
+ */
8
+
9
+ class ResolveAddressStream {}
10
+ class TcpSocket {}
11
+ class UdpSocket {}
12
+ class IncomingDatagramStream {}
13
+ class OutgoingDatagramStream {}
14
+ class Network {}
15
+
16
+ export const instanceNetwork = {
17
+ instanceNetwork() {
18
+ return new Network();
19
+ },
20
+ };
21
+
22
+ export const ipNameLookup = {
23
+ ResolveAddressStream,
24
+ resolveAddresses() {
25
+ throw new Error("networking not available in sandbox");
26
+ },
27
+ };
28
+
29
+ export const network = {
30
+ Network,
31
+ };
32
+
33
+ export const tcpCreateSocket = {
34
+ createTcpSocket() {
35
+ throw new Error("networking not available in sandbox");
36
+ },
37
+ };
38
+
39
+ export const tcp = {
40
+ TcpSocket,
41
+ };
42
+
43
+ export const udpCreateSocket = {
44
+ createUdpSocket() {
45
+ throw new Error("networking not available in sandbox");
46
+ },
47
+ };
48
+
49
+ export const udp = {
50
+ UdpSocket,
51
+ IncomingDatagramStream,
52
+ OutgoingDatagramStream,
53
+ };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Python stdlib loader for browser/Node.js environments.
3
+ *
4
+ * Fetches python-stdlib.tar.gz, decompresses it, parses the tar archive,
5
+ * and returns a file tree compatible with the @bytecodealliance/preview2-shim
6
+ * filesystem.
7
+ */
8
+
9
+ /**
10
+ * Parse a tar archive from a Uint8Array into a preview2-shim file tree.
11
+ *
12
+ * @param {Uint8Array} tarData - Raw tar file bytes
13
+ * @returns {object} File tree with { dir: { ... } } structure
14
+ */
15
+ function parseTar(tarData) {
16
+ const root = { dir: {} };
17
+ let offset = 0;
18
+
19
+ while (offset + 512 <= tarData.length) {
20
+ // Read 512-byte header
21
+ const header = tarData.subarray(offset, offset + 512);
22
+
23
+ // Check for end-of-archive (two consecutive zero blocks)
24
+ if (header.every((b) => b === 0)) break;
25
+
26
+ // Extract filename: prefix (bytes 345-499) + name (bytes 0-99)
27
+ const prefix = readString(header, 345, 155);
28
+ const name = readString(header, 0, 100);
29
+ const fullName = prefix ? `${prefix}/${name}` : name;
30
+
31
+ // File size in octal (bytes 124-135)
32
+ const sizeStr = readString(header, 124, 12);
33
+ const size = parseInt(sizeStr, 8) || 0;
34
+
35
+ // Type flag (byte 156): '0' or '\0' = file, '5' = directory
36
+ const typeFlag = header[156];
37
+
38
+ offset += 512; // Move past header
39
+
40
+ if (fullName && fullName !== "pax_global_header") {
41
+ // Normalize path: strip leading ./ and trailing /
42
+ const cleanPath = fullName.replace(/^\.\//, "").replace(/\/$/, "");
43
+
44
+ if (cleanPath) {
45
+ if (typeFlag === 53 /* '5' = directory */) {
46
+ ensureDir(root, cleanPath);
47
+ } else if (
48
+ typeFlag === 48 /* '0' = regular file */ ||
49
+ typeFlag === 0 /* '\0' = regular file (old format) */
50
+ ) {
51
+ const content = tarData.slice(offset, offset + size);
52
+ setFile(root, cleanPath, content);
53
+ }
54
+ }
55
+ }
56
+
57
+ // Advance past file data (padded to 512-byte boundary)
58
+ offset += Math.ceil(size / 512) * 512;
59
+ }
60
+
61
+ return root;
62
+ }
63
+
64
+ /** Read a null-terminated string from a buffer at the given offset. */
65
+ function readString(buf, start, maxLen) {
66
+ let end = start;
67
+ while (end < start + maxLen && buf[end] !== 0) end++;
68
+ return new TextDecoder().decode(buf.subarray(start, end));
69
+ }
70
+
71
+ /** Ensure a directory path exists in the file tree. */
72
+ function ensureDir(root, path) {
73
+ const parts = path.split("/");
74
+ let current = root;
75
+ for (const part of parts) {
76
+ if (!part) continue;
77
+ if (!current.dir) current.dir = {};
78
+ if (!current.dir[part]) current.dir[part] = { dir: {} };
79
+ current = current.dir[part];
80
+ }
81
+ return current;
82
+ }
83
+
84
+ /** Set a file at the given path in the file tree. */
85
+ function setFile(root, path, content) {
86
+ const parts = path.split("/");
87
+ const fileName = parts.pop();
88
+ const dir = parts.length > 0 ? ensureDir(root, parts.join("/")) : root;
89
+ if (!dir.dir) dir.dir = {};
90
+ dir.dir[fileName] = { source: content };
91
+ }
92
+
93
+ /**
94
+ * Read and decompress the stdlib tar.gz in Node.js using fs and zlib.
95
+ * @param {string} filePath - Absolute path to the tar.gz file
96
+ * @returns {Promise<Uint8Array>} Decompressed tar data
97
+ */
98
+ async function loadTarGzNode(filePath) {
99
+ const { readFileSync } = await import(/* webpackIgnore: true */ "node:fs");
100
+ const { gunzipSync } = await import(/* webpackIgnore: true */ "node:zlib");
101
+ const compressed = readFileSync(filePath);
102
+ return gunzipSync(compressed);
103
+ }
104
+
105
+ /**
106
+ * Read and decompress the stdlib tar.gz in the browser using fetch + DecompressionStream.
107
+ * @param {string} url - URL to the tar.gz file
108
+ * @returns {Promise<Uint8Array>} Decompressed tar data
109
+ */
110
+ async function loadTarGzBrowser(url) {
111
+ const response = await fetch(url);
112
+ if (!response.ok) {
113
+ throw new Error(
114
+ `Failed to fetch Python stdlib: ${response.status} ${response.statusText}`,
115
+ );
116
+ }
117
+
118
+ if (typeof DecompressionStream !== "undefined") {
119
+ const ds = new DecompressionStream("gzip");
120
+ const decompressed = response.body.pipeThrough(ds);
121
+ const reader = decompressed.getReader();
122
+ const chunks = [];
123
+ let totalLen = 0;
124
+ while (true) {
125
+ const { done, value } = await reader.read();
126
+ if (done) break;
127
+ chunks.push(value);
128
+ totalLen += value.length;
129
+ }
130
+ const tarData = new Uint8Array(totalLen);
131
+ let pos = 0;
132
+ for (const chunk of chunks) {
133
+ tarData.set(chunk, pos);
134
+ pos += chunk.length;
135
+ }
136
+ return tarData;
137
+ }
138
+
139
+ // Fallback: try node:zlib
140
+ const buf = new Uint8Array(await response.arrayBuffer());
141
+ const { gunzipSync } = await import(/* webpackIgnore: true */ "node:zlib");
142
+ return gunzipSync(buf);
143
+ }
144
+
145
+ /**
146
+ * Fetch and decompress python-stdlib.tar.gz, returning a file tree.
147
+ *
148
+ * @param {string|URL} [url] - URL to python-stdlib.tar.gz. Defaults to
149
+ * './python-stdlib.tar.gz' relative to this module.
150
+ * @returns {Promise<object>} File tree with { dir: { ... } } structure
151
+ */
152
+ export async function loadStdlib(url) {
153
+ const stdlibUrl =
154
+ url || new URL("./python-stdlib.tar.gz", import.meta.url).href;
155
+
156
+ let tarData;
157
+
158
+ // In Node.js, use fs + zlib for file:// URLs (fetch doesn't support file://)
159
+ const isNode =
160
+ typeof globalThis.process !== "undefined" &&
161
+ typeof globalThis.process.versions?.node !== "undefined";
162
+
163
+ if (isNode && stdlibUrl.startsWith("file://")) {
164
+ const { fileURLToPath } = await import(
165
+ /* webpackIgnore: true */ "node:url"
166
+ );
167
+ const filePath = fileURLToPath(stdlibUrl);
168
+ tarData = await loadTarGzNode(filePath);
169
+ } else {
170
+ tarData = await loadTarGzBrowser(stdlibUrl);
171
+ }
172
+
173
+ return parseTar(tarData);
174
+ }