preprocess-wasm-bytes 0.1.5 → 0.1.7

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.
@@ -1,738 +1,711 @@
1
- // Copyright (c) 2025 justing2k5. All rights reserved.
2
- // src/preprocess_wasm_bytes.ts
1
+ // preprocessWasmBytes.ts
3
2
  //
4
- // What changed vs your last version:
5
- // - We now *directly parse the WASM binary* to find all function indices referenced by the **element section**
6
- // (covers active/passive/declarative + elem expressions). This avoids any @webassemblyjs AST-shape quirks.
7
- // - We also parse the binary export section to learn existing export names/indices (so we never collide).
8
- // - We export **all tables** (0..totalTables-1) unconditionally.
9
- // - Debug logging is OFF by default. Toggle it via:
10
- // globalThis.__WASM_PREPROCESS_DEBUG__ = true/false
11
- const DEFAULT_DEBUG = false;
12
- function debugEnabled() {
13
- const v = globalThis.__WASM_PREPROCESS_DEBUG__;
14
- return typeof v === "boolean" ? v : DEFAULT_DEBUG;
15
- }
16
- function dbg(...args) {
17
- if (debugEnabled())
18
- console.log("[wasm-preprocess]", ...args);
19
- }
20
- function uniqueName(base, used, fallback) {
21
- const cleanBase = base && typeof base === "string" ? base : fallback;
22
- if (!used.has(cleanBase)) {
23
- used.add(cleanBase);
24
- return cleanBase;
3
+ // Goal: replicate the Rust/walrus “preprocess_wasm_bytes” in TypeScript, with a
4
+ // performance-first approach:
5
+ // - Parse only the sections we need.
6
+ // - Scan function bodies for the `ref.func` opcode (0xd2) without building a full AST.
7
+ // - Patch/insert the Export section (id=7) by copying existing bytes and appending new exports.
8
+ //
9
+ // Notes / assumptions (matching typical Rust-produced wasm):
10
+ // - Element/global const-exprs are expected to be “simple” (i32.const/global.get/ref.func/ref.null/...)
11
+ // If an unsupported opcode appears inside a const-expr, this implementation bails out and returns empty bytes.
12
+ // - Handles funcref tables encoded as `funcref` (0x70) and also typed refs `(ref ... func)` / `(ref null ... func)`
13
+ // encoded as 0x64/0x63 + heaptype = -16 (func) or -13 (nofunc).
14
+ const td = new TextDecoder("utf-8");
15
+ const te = new TextEncoder();
16
+ // Section ids
17
+ const SECTION_CUSTOM = 0;
18
+ const SECTION_IMPORT = 2;
19
+ const SECTION_FUNCTION = 3;
20
+ const SECTION_TABLE = 4;
21
+ const SECTION_MEMORY = 5;
22
+ const SECTION_GLOBAL = 6;
23
+ const SECTION_EXPORT = 7;
24
+ const SECTION_ELEMENT = 9;
25
+ const SECTION_CODE = 10;
26
+ // Export kinds (ExternalKind)
27
+ const EXTERNAL_KIND_FUNCTION = 0;
28
+ const EXTERNAL_KIND_TABLE = 1;
29
+ const EXTERNAL_KIND_MEMORY = 2;
30
+ const EXTERNAL_KIND_GLOBAL = 3;
31
+ // Opcode we care about inside function bodies / const-exprs.
32
+ const OPCODE_REF_FUNC = 0xd2;
33
+ // Type encodings (commonly used)
34
+ const VALTYPE_FUNCREF = 0x70; // funcref
35
+ const VALTYPE_REF_NULL = 0x63; // (ref null ht) - in GC encoding, followed by heaptype
36
+ const VALTYPE_REF = 0x64; // (ref ht) - in GC encoding, followed by heaptype
37
+ // Heaptype signed LEB values for abstract heap types (GC encoding).
38
+ // `func` is -16, `nofunc` is -13. These are widely used encodings (e.g. V8 constants).
39
+ const HEAPTYPE_FUNC = -16;
40
+ const HEAPTYPE_NOFUNC = -13;
41
+ class Reader {
42
+ bytes;
43
+ pos;
44
+ constructor(bytes, pos = 0) {
45
+ this.bytes = bytes;
46
+ this.pos = pos;
47
+ }
48
+ ensure(n) {
49
+ if (this.pos + n > this.bytes.length) {
50
+ throw new Error("Unexpected EOF");
51
+ }
52
+ }
53
+ u8() {
54
+ this.ensure(1);
55
+ return this.bytes[this.pos++];
56
+ }
57
+ skip(n) {
58
+ this.ensure(n);
59
+ this.pos += n;
60
+ }
61
+ // Unsigned LEB128 (u32)
62
+ varU32() {
63
+ let result = 0;
64
+ let mul = 1;
65
+ while (true) {
66
+ const b = this.u8();
67
+ result += (b & 0x7f) * mul;
68
+ if ((b & 0x80) === 0)
69
+ return result >>> 0;
70
+ mul *= 128;
71
+ // varuint32 max 5 bytes; this is just a safety net
72
+ if (mul > 128 ** 6)
73
+ throw new Error("varU32 too large");
74
+ }
75
+ }
76
+ // Signed LEB128, up to ~33 bits (s33), used by heaptype in GC encoding.
77
+ varS33() {
78
+ let result = 0;
79
+ let mul = 1;
80
+ let b = 0;
81
+ while (true) {
82
+ b = this.u8();
83
+ result += (b & 0x7f) * mul;
84
+ mul *= 128;
85
+ if ((b & 0x80) === 0)
86
+ break;
87
+ if (mul > 128 ** 7)
88
+ throw new Error("varS33 too large");
89
+ }
90
+ // sign-extend based on 0x40 of final byte
91
+ if (b & 0x40)
92
+ result -= mul;
93
+ return result;
94
+ }
95
+ // Signed LEB128 (s32), used by i32.const
96
+ varS32() {
97
+ let result = 0;
98
+ let mul = 1;
99
+ let b = 0;
100
+ while (true) {
101
+ b = this.u8();
102
+ result += (b & 0x7f) * mul;
103
+ mul *= 128;
104
+ if ((b & 0x80) === 0)
105
+ break;
106
+ if (mul > 128 ** 6)
107
+ throw new Error("varS32 too large");
108
+ }
109
+ if (b & 0x40)
110
+ result -= mul;
111
+ return result | 0;
112
+ }
113
+ // Skip a signed LEB128 of arbitrary length (used for i64.const, etc.)
114
+ skipVarInt() {
115
+ while (true) {
116
+ const b = this.u8();
117
+ if ((b & 0x80) === 0)
118
+ return;
119
+ }
120
+ }
121
+ readBytes(n) {
122
+ this.ensure(n);
123
+ const sub = this.bytes.subarray(this.pos, this.pos + n);
124
+ this.pos += n;
125
+ return sub;
126
+ }
127
+ skipString() {
128
+ const len = this.varU32();
129
+ this.skip(len);
130
+ }
131
+ readString() {
132
+ const len = this.varU32();
133
+ const start = this.pos;
134
+ this.skip(len);
135
+ return td.decode(this.bytes.subarray(start, start + len));
25
136
  }
26
- let i = 1;
27
- while (used.has(`${cleanBase}__${i}`))
28
- i++;
29
- const out = `${cleanBase}__${i}`;
30
- used.add(out);
31
- return out;
32
137
  }
33
- const utf8enc = new TextEncoder();
34
- function writeU32LebTo(out, value) {
138
+ class Writer {
139
+ buf;
140
+ len = 0;
141
+ constructor(initialCapacity = 1024) {
142
+ this.buf = new Uint8Array(initialCapacity);
143
+ }
144
+ ensure(n) {
145
+ const needed = this.len + n;
146
+ if (needed <= this.buf.length)
147
+ return;
148
+ let cap = this.buf.length;
149
+ while (cap < needed)
150
+ cap = cap * 2;
151
+ const next = new Uint8Array(cap);
152
+ next.set(this.buf, 0);
153
+ this.buf = next;
154
+ }
155
+ u8(v) {
156
+ this.ensure(1);
157
+ this.buf[this.len++] = v & 0xff;
158
+ }
159
+ bytes(arr) {
160
+ this.ensure(arr.length);
161
+ this.buf.set(arr, this.len);
162
+ this.len += arr.length;
163
+ }
35
164
  // u32 LEB128
36
- let v = value >>> 0;
37
- do {
38
- let byte = v & 0x7f;
39
- v >>>= 7;
40
- if (v !== 0)
41
- byte |= 0x80;
42
- out.push(byte);
43
- } while (v !== 0);
44
- }
45
- function writeNameTo(out, s) {
46
- const bytes = utf8enc.encode(s);
47
- writeU32LebTo(out, bytes.length);
48
- for (let i = 0; i < bytes.length; i++)
49
- out.push(bytes[i]);
50
- }
51
- function buildExportSectionBytes(entries) {
52
- // section payload
53
- const body = [];
54
- writeU32LebTo(body, entries.length);
55
- for (const e of entries) {
56
- writeNameTo(body, e.name);
57
- body.push(e.kind);
58
- writeU32LebTo(body, e.index >>> 0);
165
+ varU32(v) {
166
+ let x = v >>> 0;
167
+ while (true) {
168
+ const byte = x & 0x7f;
169
+ x >>>= 7;
170
+ if (x === 0) {
171
+ this.u8(byte);
172
+ return;
173
+ }
174
+ else {
175
+ this.u8(byte | 0x80);
176
+ }
177
+ }
178
+ }
179
+ string(s) {
180
+ const data = te.encode(s);
181
+ this.varU32(data.length);
182
+ this.bytes(data);
183
+ }
184
+ finish() {
185
+ return this.buf.subarray(0, this.len);
59
186
  }
60
- // section header: id=7 + size
61
- const out = [];
62
- out.push(0x07);
63
- writeU32LebTo(out, body.length);
64
- for (let i = 0; i < body.length; i++)
65
- out.push(body[i]);
66
- return new Uint8Array(out);
67
187
  }
68
- function concatChunks(chunks) {
188
+ function concat(chunks) {
69
189
  let total = 0;
70
190
  for (const c of chunks)
71
- total += c.byteLength;
191
+ total += c.length;
72
192
  const out = new Uint8Array(total);
73
193
  let off = 0;
74
194
  for (const c of chunks) {
75
195
  out.set(c, off);
76
- off += c.byteLength;
196
+ off += c.length;
77
197
  }
78
198
  return out;
79
199
  }
80
- function rewriteWasmWithExportSection(original, newExportSection) {
81
- const bytes = original;
82
- const endAll = bytes.length;
83
- if (endAll < 8)
84
- throw new Error("[wasm-preprocess] WASM too small");
85
- const chunks = [];
86
- // Copy magic + version
87
- chunks.push(bytes.subarray(0, 8));
88
- let pos = 8;
89
- let inserted = false;
90
- let replaced = false;
91
- // Insert before first *known* section with id > 7 (custom sections (0) can appear anywhere)
92
- while (pos < endAll) {
93
- const sectionStart = pos;
94
- const id = bytes[pos++];
95
- const rSize = readU32Leb(bytes, pos, endAll);
96
- const size = rSize.value;
97
- pos = rSize.pos;
98
- const payloadStart = pos;
99
- const payloadEnd = payloadStart + size;
100
- if (payloadEnd > endAll)
101
- throw new Error("[wasm-preprocess] section size out of bounds");
102
- // If export section is missing, insert it before the first known section > 7
103
- if (!inserted && !replaced && id !== 0 && id > 7) {
104
- chunks.push(newExportSection);
105
- inserted = true;
106
- }
107
- if (id === 7) {
108
- // Replace existing export section
109
- if (!replaced) {
110
- chunks.push(newExportSection);
111
- replaced = true;
112
- inserted = true;
113
- }
114
- // Skip the old export section bytes
115
- }
116
- else {
117
- // Copy original section unchanged
118
- chunks.push(bytes.subarray(sectionStart, payloadEnd));
119
- }
120
- pos = payloadEnd;
121
- }
122
- // If we never inserted/replaced (no export section and no later section to trigger insertion), append it.
123
- if (!inserted && !replaced) {
124
- chunks.push(newExportSection);
125
- }
126
- return concatChunks(chunks);
200
+ function isWasm(bytes) {
201
+ return (bytes.length >= 8 &&
202
+ bytes[0] === 0x00 &&
203
+ bytes[1] === 0x61 &&
204
+ bytes[2] === 0x73 &&
205
+ bytes[3] === 0x6d &&
206
+ // version (usually 1)
207
+ bytes[4] === 0x01 &&
208
+ bytes[5] === 0x00 &&
209
+ bytes[6] === 0x00 &&
210
+ bytes[7] === 0x00);
127
211
  }
128
- /* =======================================================================================
129
- * Minimal WASM binary reader (just enough for exports/imports/tables/mems/globals/elems)
130
- * ======================================================================================= */
131
- const utf8 = new TextDecoder("utf-8");
132
- function fail(msg) {
133
- throw new Error(`[wasm-preprocess] ${msg}`);
212
+ function encodeSection(id, payload) {
213
+ const w = new Writer(1 + 5 + payload.length);
214
+ w.u8(id);
215
+ w.varU32(payload.length);
216
+ w.bytes(payload);
217
+ return w.finish();
134
218
  }
135
- function readU32Leb(bytes, pos, end) {
136
- let result = 0;
137
- let shift = 0;
138
- for (let i = 0; i < 5; i++) {
139
- if (pos >= end)
140
- fail("unexpected EOF while reading u32 leb");
141
- const b = bytes[pos++];
142
- result |= (b & 0x7f) << shift;
143
- if ((b & 0x80) === 0)
144
- return { value: result >>> 0, pos };
145
- shift += 7;
146
- }
147
- fail("u32 leb too long");
219
+ function skipLimits(r) {
220
+ const flags = r.u8();
221
+ r.varU32(); // min
222
+ if (flags & 0x01)
223
+ r.varU32(); // max
224
+ // Other flags (shared/memory64) exist; we don't need them here.
148
225
  }
149
- function tryReadU32Leb(bytes, pos, end) {
150
- let result = 0;
151
- let shift = 0;
152
- for (let i = 0; i < 5; i++) {
153
- if (pos >= end)
154
- return null;
155
- const b = bytes[pos++];
156
- result |= (b & 0x7f) << shift;
157
- if ((b & 0x80) === 0)
158
- return { value: result >>> 0, pos };
159
- shift += 7;
226
+ // Reads a `reftype`/`valtype` byte and any trailing heaptype if it is ref/refnull (0x64/0x63).
227
+ function readRefType(r) {
228
+ const code = r.u8();
229
+ if (code === VALTYPE_REF || code === VALTYPE_REF_NULL) {
230
+ const heapType = r.varS33();
231
+ return { code, heapType };
160
232
  }
161
- return null;
233
+ return { code };
162
234
  }
163
- function skipLeb(bytes, pos, end, maxBytes) {
164
- for (let i = 0; i < maxBytes; i++) {
165
- if (pos >= end)
166
- fail("unexpected EOF while skipping leb");
167
- const b = bytes[pos++];
168
- if ((b & 0x80) === 0)
169
- return pos;
170
- }
171
- fail("leb too long");
235
+ function skipValType(r) {
236
+ // valtype is the same grammar as reftype for our purposes here.
237
+ readRefType(r);
172
238
  }
173
- function readName(bytes, pos, end) {
174
- const r = readU32Leb(bytes, pos, end);
175
- const len = r.value;
176
- pos = r.pos;
177
- const next = pos + len;
178
- if (next > end)
179
- fail("unexpected EOF while reading name");
180
- const s = utf8.decode(bytes.subarray(pos, next));
181
- return { value: s, pos: next };
239
+ function isFuncRefTableType(rt) {
240
+ if (rt.code === VALTYPE_FUNCREF)
241
+ return true;
242
+ if ((rt.code === VALTYPE_REF || rt.code === VALTYPE_REF_NULL) && rt.heapType != null) {
243
+ return rt.heapType === HEAPTYPE_FUNC || rt.heapType === HEAPTYPE_NOFUNC;
244
+ }
245
+ return false;
182
246
  }
183
- /**
184
- * Parse a const expr and collect any ref.func immediates.
185
- * NOTE: For our purposes we only fully support the common const-expr opcodes.
186
- * For unknown opcodes we fail fast (with a useful error).
187
- */
188
- function parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, ctx) {
247
+ // Skip a const expr, optionally collecting ref.func indices.
248
+ // This only supports a conservative subset of opcodes typical in wasm init/offset exprs.
249
+ // If you hit an unsupported opcode, we throw (caller returns empty output, like the Rust code).
250
+ function skipConstExpr(r, onRefFunc) {
189
251
  while (true) {
190
- if (pos >= end)
191
- fail(`unexpected EOF in const expr (${ctx})`);
192
- const op = bytes[pos++];
252
+ const op = r.u8();
253
+ if (op === 0x0b)
254
+ return; // end
193
255
  switch (op) {
194
- case 0x0b: // end
195
- return pos;
196
- case 0xd2: {
197
- // ref.func <funcidx:u32>
198
- const r = readU32Leb(bytes, pos, end);
199
- outFuncs.add(r.value);
200
- pos = r.pos;
201
- break;
202
- }
203
- case 0xd0:
204
- // ref.null <reftype>
205
- if (pos >= end)
206
- fail(`unexpected EOF after ref.null (${ctx})`);
207
- pos += 1;
208
- break;
209
- case 0x23:
210
- // global.get <globalidx:u32>
211
- pos = skipLeb(bytes, pos, end, 5);
212
- break;
213
- case 0x41:
214
- // i32.const <varint32>
215
- pos = skipLeb(bytes, pos, end, 5);
216
- break;
217
- case 0x42:
218
- // i64.const <varint64>
219
- pos = skipLeb(bytes, pos, end, 10);
220
- break;
221
- case 0x43:
222
- // f32.const <4 bytes>
223
- if (pos + 4 > end)
224
- fail(`unexpected EOF after f32.const (${ctx})`);
225
- pos += 4;
256
+ case 0x41: // i32.const
257
+ r.varS32();
226
258
  break;
227
- case 0x44:
228
- // f64.const <8 bytes>
229
- if (pos + 8 > end)
230
- fail(`unexpected EOF after f64.const (${ctx})`);
231
- pos += 8;
232
- break;
233
- // Accept a few ref ops that have no immediates (rare in const exprs, but safe to skip)
234
- case 0xd1: // ref.is_null
235
- case 0xd3: // ref.eq
236
- case 0xd4: // ref.as_non_null
237
- break;
238
- default:
239
- fail(`unsupported opcode 0x${op.toString(16)} in const expr (${ctx}). ` +
240
- `If your modules use extended-const-exprs, we can add more opcode support.`);
241
- }
242
- }
243
- }
244
- function parseLimitsU32(bytes, pos, end, ctx) {
245
- if (pos >= end)
246
- fail(`unexpected EOF in limits (${ctx})`);
247
- const flags = bytes[pos++];
248
- // Core variants:
249
- // 0x00: min
250
- // 0x01: min, max
251
- // Additional variants exist in newer proposals (shared/memory64), but we just skip in a tolerant way.
252
- if (flags === 0x00) {
253
- pos = skipLeb(bytes, pos, end, 5);
254
- return pos;
255
- }
256
- if (flags === 0x01 || flags === 0x03) {
257
- // treat 0x03 (shared) same shape for skipping: min + max
258
- pos = skipLeb(bytes, pos, end, 5);
259
- pos = skipLeb(bytes, pos, end, 5);
260
- return pos;
261
- }
262
- // Some proposals introduce more flags; best-effort: read min + max.
263
- dbg(`limits(${ctx}) uses uncommon flags 0x${flags.toString(16)}; best-effort skipping min+max`);
264
- pos = skipLeb(bytes, pos, end, 5);
265
- pos = skipLeb(bytes, pos, end, 5);
266
- return pos;
267
- }
268
- function parseTableType(bytes, pos, end) {
269
- if (pos >= end)
270
- fail("unexpected EOF in tabletype");
271
- // reftype byte
272
- pos += 1;
273
- pos = parseLimitsU32(bytes, pos, end, "tabletype.limits");
274
- return pos;
275
- }
276
- function parseMemType(bytes, pos, end) {
277
- // memtype is limits
278
- pos = parseLimitsU32(bytes, pos, end, "memtype.limits");
279
- return pos;
280
- }
281
- function parseGlobalType(bytes, pos, end) {
282
- if (pos + 2 > end)
283
- fail("unexpected EOF in globaltype");
284
- // valtype byte
285
- pos += 1;
286
- // mutability byte: 0 const, 1 var
287
- const mut = bytes[pos++];
288
- return { pos, mutable: mut === 1 };
289
- }
290
- /**
291
- * Element section parsing based on the spec forms 0..7:
292
- * https://webassembly.github.io/spec/core/binary/modules.html#element-section
293
- */
294
- function parseElementSectionCollectFuncIdxs(bytes, start, end, outFuncs) {
295
- let pos = start;
296
- const rCount = readU32Leb(bytes, pos, end);
297
- const count = rCount.value;
298
- pos = rCount.pos;
299
- dbg(`element section: ${count} segment(s)`);
300
- for (let i = 0; i < count; i++) {
301
- const rForm = readU32Leb(bytes, pos, end);
302
- const form = rForm.value;
303
- pos = rForm.pos;
304
- const segCollected = [];
305
- const addFunc = (idx) => {
306
- outFuncs.add(idx);
307
- segCollected.push(idx);
308
- };
309
- const readFuncIdxVec = () => {
310
- const rN = readU32Leb(bytes, pos, end);
311
- const n = rN.value;
312
- pos = rN.pos;
313
- for (let k = 0; k < n; k++) {
314
- const rF = readU32Leb(bytes, pos, end);
315
- pos = rF.pos;
316
- addFunc(rF.value);
317
- }
318
- };
319
- const readExprVec = (ctx) => {
320
- const rN = readU32Leb(bytes, pos, end);
321
- const n = rN.value;
322
- pos = rN.pos;
323
- for (let k = 0; k < n; k++) {
324
- pos = parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, ctx);
325
- }
326
- };
327
- switch (form) {
328
- case 0: {
329
- // 0: u32, offset expr, vec(funcidx) (active, table 0, funcref implicit)
330
- pos = parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, `elem[${i}].offset(form0)`);
331
- readFuncIdxVec();
332
- break;
333
- }
334
- case 1: {
335
- // 1: u32, elemkind, vec(funcidx) (passive)
336
- if (pos >= end)
337
- fail("unexpected EOF reading elemkind (form1)");
338
- pos += 1; // elemkind
339
- readFuncIdxVec();
340
- break;
341
- }
342
- case 2: {
343
- // 2: u32, tableidx, offset expr, elemkind, vec(funcidx) (active, explicit table)
344
- pos = readU32Leb(bytes, pos, end).pos; // tableidx
345
- pos = parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, `elem[${i}].offset(form2)`);
346
- if (pos >= end)
347
- fail("unexpected EOF reading elemkind (form2)");
348
- pos += 1; // elemkind
349
- readFuncIdxVec();
259
+ case 0x42: // i64.const
260
+ r.skipVarInt();
350
261
  break;
351
- }
352
- case 3: {
353
- // 3: u32, elemkind, vec(funcidx) (declarative)
354
- if (pos >= end)
355
- fail("unexpected EOF reading elemkind (form3)");
356
- pos += 1; // elemkind
357
- readFuncIdxVec();
262
+ case 0x43: // f32.const
263
+ r.skip(4);
358
264
  break;
359
- }
360
- case 4: {
361
- // 4: u32, offset expr, vec(expr) (active, table 0, funcref implicit)
362
- pos = parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, `elem[${i}].offset(form4)`);
363
- readExprVec(`elem[${i}].init(form4)`);
265
+ case 0x44: // f64.const
266
+ r.skip(8);
364
267
  break;
365
- }
366
- case 5: {
367
- // 5: u32, reftype, vec(expr) (passive)
368
- if (pos >= end)
369
- fail("unexpected EOF reading reftype (form5)");
370
- pos += 1; // reftype
371
- readExprVec(`elem[${i}].init(form5)`);
268
+ case 0x23: // global.get
269
+ r.varU32();
372
270
  break;
373
- }
374
- case 6: {
375
- // 6:u32 x:tableidx e:expr et:reftype el*:vec(expr)
376
- pos = readU32Leb(bytes, pos, end).pos; // tableidx
377
- pos = parseConstExprCollectRefFuncs(bytes, pos, end, outFuncs, `elem[${i}].offset(form6)`);
378
- if (pos >= end)
379
- fail("unexpected EOF reading reftype (form6)");
380
- pos += 1; // reftype
381
- readExprVec(`elem[${i}].init(form6)`);
271
+ case 0xd0: // ref.null (ref types / GC): immediate is heaptype (signed LEB)
272
+ r.varS33();
382
273
  break;
383
- }
384
- case 7: {
385
- // 7: u32, reftype, vec(expr) (declarative)
386
- if (pos >= end)
387
- fail("unexpected EOF reading reftype (form7)");
388
- pos += 1; // reftype
389
- readExprVec(`elem[${i}].init(form7)`);
274
+ case 0xd2: {
275
+ // ref.func
276
+ const idx = r.varU32();
277
+ onRefFunc?.(idx);
390
278
  break;
391
279
  }
280
+ // Many const-safe ops have no immediates (e.g. i32.add). If you want to be more permissive,
281
+ // you can add them here as "no-op" (no immediate to skip).
392
282
  default:
393
- fail(`unknown element segment form ${form} (expected 0..7)`);
394
- }
395
- if (debugEnabled()) {
396
- const uniq = Array.from(new Set(segCollected)).sort((a, b) => a - b);
397
- dbg(` elem[${i}] form=${form} collected funcidx:`, uniq);
398
- }
399
- }
400
- if (pos !== end) {
401
- // Not necessarily fatal, but useful signal.
402
- dbg(`element section: parsed ${pos - start} bytes, expected ${end - start} bytes (diff=${end - pos})`);
403
- }
404
- }
405
- function parseExportSection(bytes, start, end, exportNames, alreadyExported, exportEntries) {
406
- let pos = start;
407
- const rCount = readU32Leb(bytes, pos, end);
408
- const count = rCount.value;
409
- pos = rCount.pos;
410
- for (let i = 0; i < count; i++) {
411
- const nm = readName(bytes, pos, end);
412
- const name = nm.value;
413
- pos = nm.pos;
414
- if (pos >= end)
415
- fail("unexpected EOF in export entry kind");
416
- const kind = bytes[pos++];
417
- const rIdx = readU32Leb(bytes, pos, end);
418
- const idx = rIdx.value;
419
- pos = rIdx.pos;
420
- exportNames.add(name);
421
- // keep exact original entries so we can rebuild export section without webassemblyjs
422
- if (kind === 0 || kind === 1 || kind === 2 || kind === 3) {
423
- exportEntries.push({ name, kind: kind, index: idx >>> 0 });
424
- }
425
- switch (kind) {
426
- case 0: // func
427
- alreadyExported.Func.add(idx);
428
- break;
429
- case 1: // table
430
- alreadyExported.Table.add(idx);
431
- break;
432
- case 2: // mem
433
- alreadyExported.Memory.add(idx);
434
- break;
435
- case 3: // global
436
- alreadyExported.Global.add(idx);
437
- break;
438
- default:
439
- // ignore unknown kinds (future-proof)
440
- break;
283
+ throw new Error(`Unsupported opcode in const-expr: 0x${op.toString(16)}`);
441
284
  }
442
285
  }
443
- dbg("existing export names:", Array.from(exportNames).sort());
444
- dbg("existing exported indices:", {
445
- Func: Array.from(alreadyExported.Func).sort((a, b) => a - b),
446
- Table: Array.from(alreadyExported.Table).sort((a, b) => a - b),
447
- Memory: Array.from(alreadyExported.Memory).sort((a, b) => a - b),
448
- Global: Array.from(alreadyExported.Global).sort((a, b) => a - b),
449
- });
450
286
  }
451
- function scanWasmBinary(wasmBytes) {
452
- const bytes = wasmBytes;
453
- const endAll = bytes.length;
454
- if (endAll < 8)
455
- fail("WASM too small");
456
- if (bytes[0] !== 0x00 || bytes[1] !== 0x61 || bytes[2] !== 0x73 || bytes[3] !== 0x6d) {
457
- fail("bad WASM magic");
458
- }
459
- // version bytes[4..8) (ignored)
460
- let pos = 8;
461
- const exportNames = new Set();
462
- const alreadyExported = {
463
- Func: new Set(),
464
- Table: new Set(),
465
- Memory: new Set(),
466
- Global: new Set(),
467
- };
468
- const exportEntries = [];
469
- const funcsToExport = new Set();
470
- let importFunctions = 0;
471
- let localFunctions = 0;
472
- let importTables = 0, localTables = 0;
473
- let importMemories = 0, localMemories = 0;
474
- let importGlobals = 0, localGlobals = 0;
475
- const mutableGlobalsToExport = new Set();
476
- const noteSection = (id) => {
477
- switch (id) {
478
- case 2:
479
- return "import";
480
- case 4:
481
- return "table";
482
- case 5:
483
- return "memory";
484
- case 6:
485
- return "global";
486
- case 7:
487
- return "export";
488
- case 9:
489
- return "element";
490
- default:
491
- return `section(${id})`;
287
+ export function preprocess_wasm_bytes(wasmBytes, opts = {}) {
288
+ try {
289
+ if (!isWasm(wasmBytes)) {
290
+ if (opts.debug)
291
+ console.warn("Not a wasm binary (bad header).");
292
+ return new Uint8Array(0);
492
293
  }
493
- };
494
- while (pos < endAll) {
495
- const id = bytes[pos++];
496
- const rSize = readU32Leb(bytes, pos, endAll);
497
- const size = rSize.value;
498
- pos = rSize.pos;
499
- const start = pos;
500
- const end = start + size;
501
- if (end > endAll)
502
- fail(`section ${id} size out of bounds`);
503
- try {
504
- if (id === 2) {
505
- // import section
506
- let p = start;
507
- const rCount = readU32Leb(bytes, p, end);
508
- const n = rCount.value;
509
- p = rCount.pos;
510
- for (let i = 0; i < n; i++) {
511
- // module/name
512
- p = readName(bytes, p, end).pos;
513
- p = readName(bytes, p, end).pos;
514
- if (p >= end)
515
- fail("unexpected EOF in import kind");
516
- const kind = bytes[p++];
517
- switch (kind) {
518
- case 0: // func
519
- p = readU32Leb(bytes, p, end).pos; // typeidx
520
- importFunctions++;
521
- break;
522
- case 1: // table
523
- p = parseTableType(bytes, p, end);
524
- importTables++;
525
- break;
526
- case 2: // mem
527
- p = parseMemType(bytes, p, end);
528
- importMemories++;
529
- break;
530
- case 3: {
531
- // global
532
- const gt = parseGlobalType(bytes, p, end);
533
- p = gt.pos;
534
- if (gt.mutable)
535
- mutableGlobalsToExport.add(importGlobals);
536
- importGlobals++;
537
- break;
294
+ const sections = [];
295
+ // Imports + definitions summary
296
+ let importedFuncCount = 0;
297
+ let importedTableCount = 0;
298
+ let importedMemCount = 0;
299
+ let importedGlobalCount = 0;
300
+ const importedTableTypes = [];
301
+ const importedGlobalMutable = [];
302
+ let localFuncCount = 0;
303
+ const localTableTypes = [];
304
+ let localMemCount = 0;
305
+ const localGlobalMutable = [];
306
+ // Name section maps (optional)
307
+ const funcNameByIndex = new Map();
308
+ const globalNameByIndex = new Map();
309
+ // Existing exports
310
+ let exportSectionStart = -1;
311
+ let exportSectionEnd = -1;
312
+ let exportCount = 0;
313
+ let exportEntriesBytes = null;
314
+ const usedExportNames = new Set();
315
+ const exportedFuncs = new Set();
316
+ const exportedGlobals = new Set();
317
+ const exportedMems = new Set();
318
+ const exportedTables = new Set();
319
+ // Things we will export (collected)
320
+ const funcsToExport = new Set();
321
+ const markFunc = (idx) => {
322
+ const total = importedFuncCount + localFuncCount;
323
+ if (idx >>> 0 >= total >>> 0)
324
+ return; // bounds check (helps avoid false positives)
325
+ funcsToExport.add(idx >>> 0);
326
+ };
327
+ // Parse the module sections
328
+ const r = new Reader(wasmBytes, 8);
329
+ while (r.pos < wasmBytes.length) {
330
+ const start = r.pos;
331
+ const id = r.u8();
332
+ const size = r.varU32();
333
+ const payloadStart = r.pos;
334
+ const payloadEnd = payloadStart + size;
335
+ if (payloadEnd > wasmBytes.length) {
336
+ throw new Error("Section extends past end of file");
337
+ }
338
+ sections.push({ id, start, end: payloadEnd, payloadStart, payloadEnd });
339
+ // Slice reader to payload while parsing.
340
+ const savedPos = r.pos;
341
+ r.pos = payloadStart;
342
+ switch (id) {
343
+ case SECTION_CUSTOM: {
344
+ // custom section: name + bytes
345
+ const name = r.readString();
346
+ if (name === "name") {
347
+ // name subsection parsing
348
+ while (r.pos < payloadEnd) {
349
+ const subId = r.u8();
350
+ const subSize = r.varU32();
351
+ const subEnd = r.pos + subSize;
352
+ if (subEnd > payloadEnd)
353
+ throw new Error("Name subsection out of bounds");
354
+ const subReader = new Reader(wasmBytes.subarray(r.pos, subEnd), 0);
355
+ if (subId === 1) {
356
+ // function names
357
+ const count = subReader.varU32();
358
+ for (let i = 0; i < count; i++) {
359
+ const idx = subReader.varU32();
360
+ const nm = subReader.readString();
361
+ funcNameByIndex.set(idx, nm);
362
+ }
363
+ }
364
+ else if (subId === 7) {
365
+ // global names
366
+ const count = subReader.varU32();
367
+ for (let i = 0; i < count; i++) {
368
+ const idx = subReader.varU32();
369
+ const nm = subReader.readString();
370
+ globalNameByIndex.set(idx, nm);
371
+ }
372
+ }
373
+ r.pos = subEnd;
538
374
  }
539
- case 4: // tag (future-proof)
540
- p = readU32Leb(bytes, p, end).pos;
541
- break;
542
- default:
543
- dbg(`unknown import kind ${kind}; cannot safely skip (bailing import parse)`);
544
- // safest: bail parsing this section
545
- i = n;
546
- break;
547
375
  }
376
+ else {
377
+ // skip rest
378
+ r.pos = payloadEnd;
379
+ }
380
+ break;
548
381
  }
549
- dbg("imports:", { importTables, importMemories, importGlobals });
550
- }
551
- else if (id === 3) {
552
- // function section: vec(typeidx) for defined (non-imported) functions
553
- let p = start;
554
- const rCount = readU32Leb(bytes, p, end);
555
- const n = rCount.value;
556
- p = rCount.pos;
557
- localFunctions = n;
558
- // Skip typeidx entries
559
- for (let i = 0; i < n; i++) {
560
- p = skipLeb(bytes, p, end, 5);
382
+ case SECTION_IMPORT: {
383
+ const count = r.varU32();
384
+ for (let i = 0; i < count; i++) {
385
+ r.skipString(); // module
386
+ r.skipString(); // name
387
+ const kind = r.u8();
388
+ switch (kind) {
389
+ case EXTERNAL_KIND_FUNCTION:
390
+ importedFuncCount++;
391
+ r.varU32(); // type idx
392
+ break;
393
+ case EXTERNAL_KIND_TABLE: {
394
+ importedTableCount++;
395
+ const rt = readRefType(r);
396
+ importedTableTypes.push(rt);
397
+ skipLimits(r);
398
+ break;
399
+ }
400
+ case EXTERNAL_KIND_MEMORY:
401
+ importedMemCount++;
402
+ skipLimits(r);
403
+ break;
404
+ case EXTERNAL_KIND_GLOBAL: {
405
+ importedGlobalCount++;
406
+ skipValType(r);
407
+ const mut = r.u8();
408
+ importedGlobalMutable.push(mut !== 0);
409
+ break;
410
+ }
411
+ default:
412
+ throw new Error(`Unknown import kind: ${kind}`);
413
+ }
414
+ }
415
+ break;
561
416
  }
562
- dbg("functions:", { importFunctions, localFunctions, totalFunctions: importFunctions + localFunctions });
563
- }
564
- else if (id === 4) {
565
- // table section
566
- let p = start;
567
- const rCount = readU32Leb(bytes, p, end);
568
- const n = rCount.value;
569
- p = rCount.pos;
570
- for (let i = 0; i < n; i++)
571
- p = parseTableType(bytes, p, end);
572
- localTables += n;
573
- dbg("tables:", { localTables, totalTables: importTables + localTables });
574
- }
575
- else if (id === 5) {
576
- // memory section
577
- let p = start;
578
- const rCount = readU32Leb(bytes, p, end);
579
- const n = rCount.value;
580
- p = rCount.pos;
581
- for (let i = 0; i < n; i++)
582
- p = parseMemType(bytes, p, end);
583
- localMemories += n;
584
- dbg("memories:", { localMemories, totalMemories: importMemories + localMemories });
585
- }
586
- else if (id === 6) {
587
- // global section
588
- let p = start;
589
- const rCount = readU32Leb(bytes, p, end);
590
- const n = rCount.value;
591
- p = rCount.pos;
592
- let globalIndex = importGlobals;
593
- for (let i = 0; i < n; i++) {
594
- const gt = parseGlobalType(bytes, p, end);
595
- p = gt.pos;
596
- if (gt.mutable)
597
- mutableGlobalsToExport.add(globalIndex);
598
- // init expr (also collect ref.func if present)
599
- p = parseConstExprCollectRefFuncs(bytes, p, end, funcsToExport, `global[${globalIndex}].init`);
600
- globalIndex++;
417
+ case SECTION_FUNCTION: {
418
+ const count = r.varU32();
419
+ localFuncCount = count;
420
+ // skip type indices
421
+ for (let i = 0; i < count; i++)
422
+ r.varU32();
423
+ break;
601
424
  }
602
- localGlobals += n;
603
- dbg("globals:", {
604
- localGlobals,
605
- totalGlobals: importGlobals + localGlobals,
606
- mutableGlobals: Array.from(mutableGlobalsToExport).sort((a, b) => a - b),
607
- });
608
- }
609
- else if (id === 7) {
610
- // export section
611
- parseExportSection(bytes, start, end, exportNames, alreadyExported, exportEntries);
612
- }
613
- else if (id === 9) {
614
- // element section (the big one we care about for function refs)
615
- parseElementSectionCollectFuncIdxs(bytes, start, end, funcsToExport);
616
- }
617
- else if (id === 10) {
618
- // code section: vec(code)
619
- // We scan for opcode 0xD2 (ref.func) and record its immediate funcidx.
620
- const totalFunctions = importFunctions + localFunctions;
621
- let p = start;
622
- const rCount = readU32Leb(bytes, p, end);
623
- const nBodies = rCount.value;
624
- p = rCount.pos;
625
- dbg("code section bodies:", nBodies);
626
- for (let bi = 0; bi < nBodies; bi++) {
627
- const rBodySize = readU32Leb(bytes, p, end);
628
- const bodySize = rBodySize.value;
629
- p = rBodySize.pos;
630
- const bodyStart = p;
631
- const bodyEnd = bodyStart + bodySize;
632
- if (bodyEnd > end)
633
- fail("code body size out of bounds");
634
- // Parse locals vec to find instruction start
635
- let q = bodyStart;
636
- const rLocalCount = readU32Leb(bytes, q, bodyEnd);
637
- const localCount = rLocalCount.value;
638
- q = rLocalCount.pos;
639
- for (let li = 0; li < localCount; li++) {
640
- // locals entry: count:u32, valtype:byte
641
- const rN = readU32Leb(bytes, q, bodyEnd);
642
- q = rN.pos;
643
- if (q >= bodyEnd)
644
- fail("unexpected EOF in locals");
645
- q += 1; // valtype
425
+ case SECTION_TABLE: {
426
+ const count = r.varU32();
427
+ for (let i = 0; i < count; i++) {
428
+ const rt = readRefType(r);
429
+ localTableTypes.push(rt);
430
+ skipLimits(r);
646
431
  }
647
- // Now q is at instruction bytes. We do a simple linear scan.
648
- // IMPORTANT: do NOT "jump" past immediates; false-positives are okay,
649
- // but skipping could cause us to miss real 0xD2 later.
650
- for (let i = q; i < bodyEnd; i++) {
651
- if (bytes[i] !== 0xd2)
652
- continue; // ref.func
653
- const rIdx = tryReadU32Leb(bytes, i + 1, bodyEnd);
654
- if (!rIdx)
655
- continue;
656
- if (rIdx.value < totalFunctions) {
657
- funcsToExport.add(rIdx.value);
432
+ break;
433
+ }
434
+ case SECTION_MEMORY: {
435
+ const count = r.varU32();
436
+ localMemCount = count;
437
+ for (let i = 0; i < count; i++)
438
+ skipLimits(r);
439
+ break;
440
+ }
441
+ case SECTION_GLOBAL: {
442
+ const count = r.varU32();
443
+ for (let i = 0; i < count; i++) {
444
+ skipValType(r);
445
+ const mut = r.u8();
446
+ localGlobalMutable.push(mut !== 0);
447
+ // init expr: collect ref.func
448
+ skipConstExpr(r, (f) => markFunc(f));
449
+ }
450
+ break;
451
+ }
452
+ case SECTION_EXPORT: {
453
+ exportSectionStart = start;
454
+ exportSectionEnd = payloadEnd;
455
+ exportCount = r.varU32();
456
+ const entriesStart = r.pos;
457
+ for (let i = 0; i < exportCount; i++) {
458
+ const nm = r.readString();
459
+ usedExportNames.add(nm);
460
+ const kind = r.u8();
461
+ const idx = r.varU32();
462
+ switch (kind) {
463
+ case EXTERNAL_KIND_FUNCTION:
464
+ exportedFuncs.add(idx);
465
+ break;
466
+ case EXTERNAL_KIND_TABLE:
467
+ exportedTables.add(idx);
468
+ break;
469
+ case EXTERNAL_KIND_MEMORY:
470
+ exportedMems.add(idx);
471
+ break;
472
+ case EXTERNAL_KIND_GLOBAL:
473
+ exportedGlobals.add(idx);
474
+ break;
475
+ default:
476
+ // ignore unknown kinds
477
+ break;
658
478
  }
659
479
  }
660
- p = bodyEnd;
480
+ exportEntriesBytes = wasmBytes.subarray(entriesStart, payloadEnd);
481
+ break;
661
482
  }
662
- dbg("funcsToExport after code scan:", Array.from(funcsToExport).sort((a, b) => a - b));
483
+ case SECTION_ELEMENT: {
484
+ const segCount = r.varU32();
485
+ for (let s = 0; s < segCount; s++) {
486
+ const flags = r.varU32();
487
+ const isExpr = (flags & 0x04) !== 0;
488
+ const kind = flags & 0x03;
489
+ // kind: 0 active (table 0), 1 passive, 2 active (explicit table), 3 declarative
490
+ if (kind === 2) {
491
+ // table index
492
+ r.varU32();
493
+ }
494
+ if (kind === 0 || kind === 2) {
495
+ // active segment has offset expr
496
+ skipConstExpr(r);
497
+ }
498
+ if (!isExpr) {
499
+ // function indices encoding
500
+ if (kind === 1 || kind === 3) {
501
+ // elemkind byte
502
+ r.u8();
503
+ }
504
+ const n = r.varU32();
505
+ for (let i = 0; i < n; i++) {
506
+ const f = r.varU32();
507
+ markFunc(f);
508
+ }
509
+ }
510
+ else {
511
+ // expression encoding: has elemtype/reftype
512
+ readRefType(r);
513
+ const n = r.varU32();
514
+ for (let i = 0; i < n; i++) {
515
+ // each element is a const expr (e.g. ref.func ...)
516
+ skipConstExpr(r, (f) => markFunc(f));
517
+ }
518
+ }
519
+ }
520
+ break;
521
+ }
522
+ case SECTION_CODE: {
523
+ const bodyCount = r.varU32();
524
+ for (let i = 0; i < bodyCount; i++) {
525
+ const bodySize = r.varU32();
526
+ const bodyStart = r.pos;
527
+ const bodyEnd = bodyStart + bodySize;
528
+ if (bodyEnd > payloadEnd)
529
+ throw new Error("Function body out of bounds");
530
+ // locals
531
+ const localDecls = r.varU32();
532
+ for (let d = 0; d < localDecls; d++) {
533
+ r.varU32(); // count
534
+ skipValType(r); // type
535
+ }
536
+ // Scan the remaining bytes of the body (instructions) for opcode 0xd2.
537
+ // This is a fast, conservative scan (may over-approximate, but bounded by total func count).
538
+ const instrBytes = wasmBytes.subarray(r.pos, bodyEnd);
539
+ let searchFrom = 0;
540
+ while (true) {
541
+ const hit = instrBytes.indexOf(OPCODE_REF_FUNC, searchFrom);
542
+ if (hit === -1)
543
+ break;
544
+ // Decode following u32 LEB safely, within bounds.
545
+ const abs = r.pos + hit + 1;
546
+ if (abs < bodyEnd) {
547
+ const rr = new Reader(wasmBytes, abs);
548
+ try {
549
+ const idx = rr.varU32();
550
+ // only accept if rr.pos didn't run past end of body
551
+ if (rr.pos <= bodyEnd)
552
+ markFunc(idx);
553
+ }
554
+ catch {
555
+ // ignore malformed occurrences (likely false positive)
556
+ }
557
+ }
558
+ searchFrom = hit + 1;
559
+ }
560
+ // Jump to end of body
561
+ r.pos = bodyEnd;
562
+ }
563
+ break;
564
+ }
565
+ default:
566
+ // ignore
567
+ break;
663
568
  }
569
+ // restore pos to end of section payload
570
+ r.pos = payloadEnd;
571
+ // safeguard: should match payloadEnd
572
+ if (r.pos !== payloadEnd)
573
+ r.pos = payloadEnd;
574
+ // restore savedPos is not needed; we control r.pos explicitly.
575
+ void savedPos;
664
576
  }
665
- catch (e) {
666
- dbg(`parse error in ${noteSection(id)}:`, e);
667
- // keep going; we'll still try to add exports using whatever we found
577
+ // Build lists to export based on parsed info
578
+ // 3) mutable globals (imports + locals) => export them
579
+ const globalsToExport = [];
580
+ const allGlobalsMutable = importedGlobalMutable.concat(localGlobalMutable);
581
+ for (let i = 0; i < allGlobalsMutable.length; i++) {
582
+ if (allGlobalsMutable[i])
583
+ globalsToExport.push(i);
584
+ }
585
+ // 4) all memories (imports + locals)
586
+ const memoryCount = importedMemCount + localMemCount;
587
+ const memoriesToExport = [];
588
+ for (let i = 0; i < memoryCount; i++)
589
+ memoriesToExport.push(i);
590
+ // 5) any table of funcrefs (imports + locals)
591
+ const tablesToExport = [];
592
+ const allTableTypes = importedTableTypes.concat(localTableTypes);
593
+ for (let i = 0; i < allTableTypes.length; i++) {
594
+ if (isFuncRefTableType(allTableTypes[i]))
595
+ tablesToExport.push(i);
596
+ }
597
+ // Prepare new exports
598
+ const newExports = [];
599
+ const makeUniqueName = (base, fallbackSuffix) => {
600
+ if (!usedExportNames.has(base)) {
601
+ usedExportNames.add(base);
602
+ return base;
603
+ }
604
+ const alt = `${base}${fallbackSuffix}`;
605
+ if (!usedExportNames.has(alt)) {
606
+ usedExportNames.add(alt);
607
+ return alt;
608
+ }
609
+ // deterministic collision resolver
610
+ let n = 2;
611
+ while (true) {
612
+ const cand = `${alt}_${n++}`;
613
+ if (!usedExportNames.has(cand)) {
614
+ usedExportNames.add(cand);
615
+ return cand;
616
+ }
617
+ }
618
+ };
619
+ // Functions
620
+ const funcIdxs = Array.from(funcsToExport);
621
+ funcIdxs.sort((a, b) => a - b); // deterministic
622
+ for (const funcIdx of funcIdxs) {
623
+ if (exportedFuncs.has(funcIdx))
624
+ continue;
625
+ const debugName = funcNameByIndex.get(funcIdx);
626
+ const base = debugName ?? `__f${funcIdx}`;
627
+ const name = makeUniqueName(base, `__f${funcIdx}`);
628
+ newExports.push({ name, kind: EXTERNAL_KIND_FUNCTION, index: funcIdx });
629
+ }
630
+ // Globals (mutable)
631
+ for (const g of globalsToExport) {
632
+ if (exportedGlobals.has(g))
633
+ continue;
634
+ const debugName = globalNameByIndex.get(g);
635
+ const base = debugName ?? `__global_${g}`;
636
+ const name = makeUniqueName(base, `__global_${g}`);
637
+ newExports.push({ name, kind: EXTERNAL_KIND_GLOBAL, index: g });
638
+ }
639
+ // Memories (all)
640
+ for (const m of memoriesToExport) {
641
+ if (exportedMems.has(m))
642
+ continue;
643
+ const base = `__memory_${m}`;
644
+ const name = makeUniqueName(base, `__memory_${m}`);
645
+ newExports.push({ name, kind: EXTERNAL_KIND_MEMORY, index: m });
646
+ }
647
+ // Tables (funcref-ish)
648
+ for (const t of tablesToExport) {
649
+ if (exportedTables.has(t))
650
+ continue;
651
+ const base = `__table_${t}`;
652
+ const name = makeUniqueName(base, `__table_${t}`);
653
+ newExports.push({ name, kind: EXTERNAL_KIND_TABLE, index: t });
654
+ }
655
+ if (newExports.length === 0) {
656
+ // Nothing to change
657
+ return wasmBytes;
658
+ }
659
+ // Encode appended export entries
660
+ const newEntriesW = new Writer(Math.max(256, newExports.length * 20));
661
+ for (const e of newExports) {
662
+ newEntriesW.string(e.name);
663
+ newEntriesW.u8(e.kind);
664
+ newEntriesW.varU32(e.index);
665
+ }
666
+ const newEntriesBytes = newEntriesW.finish();
667
+ // Build new export section payload
668
+ let newExportPayload;
669
+ if (exportEntriesBytes) {
670
+ const w = new Writer(10 + exportEntriesBytes.length + newEntriesBytes.length);
671
+ w.varU32(exportCount + newExports.length);
672
+ w.bytes(exportEntriesBytes);
673
+ w.bytes(newEntriesBytes);
674
+ newExportPayload = w.finish();
675
+ }
676
+ else {
677
+ const w = new Writer(10 + newEntriesBytes.length);
678
+ w.varU32(newExports.length);
679
+ w.bytes(newEntriesBytes);
680
+ newExportPayload = w.finish();
681
+ }
682
+ const newExportSectionBytes = encodeSection(SECTION_EXPORT, newExportPayload);
683
+ // Splice into original module bytes
684
+ if (exportSectionStart >= 0 && exportSectionEnd >= 0) {
685
+ // Replace existing export section
686
+ const before = wasmBytes.subarray(0, exportSectionStart);
687
+ const after = wasmBytes.subarray(exportSectionEnd);
688
+ return concat([before, newExportSectionBytes, after]);
689
+ }
690
+ else {
691
+ // Insert export section before first non-custom section with id > 7 (Start/Element/Code/etc),
692
+ // otherwise append at end.
693
+ let insertAt = wasmBytes.length;
694
+ for (const s of sections) {
695
+ if (s.id !== SECTION_CUSTOM && s.id > SECTION_EXPORT) {
696
+ insertAt = s.start;
697
+ break;
698
+ }
699
+ }
700
+ const before = wasmBytes.subarray(0, insertAt);
701
+ const after = wasmBytes.subarray(insertAt);
702
+ return concat([before, newExportSectionBytes, after]);
668
703
  }
669
- pos = end;
670
- }
671
- const totalTables = importTables + localTables;
672
- const totalMemories = importMemories + localMemories;
673
- dbg("binary scan summary:", {
674
- funcsToExport: Array.from(funcsToExport).sort((a, b) => a - b),
675
- mutableGlobalsToExport: Array.from(mutableGlobalsToExport).sort((a, b) => a - b),
676
- totalTables,
677
- totalMemories,
678
- });
679
- return {
680
- exportNames,
681
- alreadyExported,
682
- exportEntries,
683
- funcsToExport,
684
- mutableGlobalsToExport,
685
- totalMemories,
686
- totalTables,
687
- };
688
- }
689
- /* =======================================================================================
690
- * Main entry
691
- * ======================================================================================= */
692
- export async function preprocess_wasm_bytes(wasmBytes) {
693
- // Scan is authoritative for what we need to export
694
- const { exportNames, alreadyExported, exportEntries, funcsToExport, mutableGlobalsToExport, totalMemories, totalTables, } = scanWasmBinary(wasmBytes);
695
- const added = [];
696
- // 1) Functions referenced by element segments / ref.func
697
- const funcList = Array.from(funcsToExport).sort((a, b) => a - b);
698
- for (const idx of funcList) {
699
- if (alreadyExported.Func.has(idx))
700
- continue;
701
- const name = uniqueName(`__f${idx}`, exportNames, `__f${idx}`);
702
- added.push({ name, kind: 0, index: idx >>> 0 });
703
- alreadyExported.Func.add(idx);
704
- }
705
- // 2) Mutable globals
706
- const globList = Array.from(mutableGlobalsToExport).sort((a, b) => a - b);
707
- for (const idx of globList) {
708
- if (alreadyExported.Global.has(idx))
709
- continue;
710
- const name = uniqueName(`__global_${idx}`, exportNames, `__global_${idx}`);
711
- added.push({ name, kind: 3, index: idx >>> 0 });
712
- alreadyExported.Global.add(idx);
713
- }
714
- // 3) All memories
715
- for (let idx = 0; idx < totalMemories; idx++) {
716
- if (alreadyExported.Memory.has(idx))
717
- continue;
718
- const name = uniqueName(`__memory_${idx}`, exportNames, `__memory_${idx}`);
719
- added.push({ name, kind: 2, index: idx >>> 0 });
720
- alreadyExported.Memory.add(idx);
721
704
  }
722
- // 4) All tables
723
- for (let idx = 0; idx < totalTables; idx++) {
724
- if (alreadyExported.Table.has(idx))
725
- continue;
726
- const name = uniqueName(`__table_${idx}`, exportNames, `__table_${idx}`);
727
- added.push({ name, kind: 1, index: idx >>> 0 });
728
- alreadyExported.Table.add(idx);
705
+ catch (e) {
706
+ if (opts.debug)
707
+ console.warn("preprocessWasmBytes failed:", e);
708
+ return new Uint8Array(0);
729
709
  }
730
- if (added.length === 0)
731
- return wasmBytes;
732
- // Rebuild export section = existing entries + added entries
733
- const allExports = exportEntries.concat(added);
734
- const newExportSection = buildExportSectionBytes(allExports);
735
- // Replace (or insert) export section in the original binary
736
- return rewriteWasmWithExportSection(wasmBytes, newExportSection);
737
710
  }
738
711
  //# sourceMappingURL=preprocess_wasm_bytes.js.map