wasm-html-to-image 1.0.2 → 1.0.5

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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Minimal WASM binary sanity check.
3
+ *
4
+ * The loader hands the `.wasm` bytes straight to the engine, so a file
5
+ * that is still being written (concurrent build) or that belongs to a
6
+ * different build than the JavaScript glue fails deep inside V8 with
7
+ * messages that do not point at the real cause:
8
+ *
9
+ * - `CompileError: section Data extends past end` — the file was read
10
+ * while it was still being written (truncated binary).
11
+ * - `Import #0 "env" is not an object` — the binary and the glue come
12
+ * from different builds (the import namespace does not match).
13
+ *
14
+ * {@link checkWasmBinary} detects the truncated case before the engine
15
+ * sees it and reports the import namespaces, so the loader can explain
16
+ * a mismatch instead of surfacing a cryptic engine error.
17
+ */
18
+ /** WASM magic header (`\0asm`). */
19
+ const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d];
20
+ /** Section id of the import section. */
21
+ const SECTION_IMPORT = 2;
22
+ /** Human-readable section names (for the failure reason). */
23
+ const SECTION_NAMES = {
24
+ 0: "Custom",
25
+ 1: "Type",
26
+ 2: "Import",
27
+ 3: "Function",
28
+ 4: "Table",
29
+ 5: "Memory",
30
+ 6: "Global",
31
+ 7: "Export",
32
+ 8: "Start",
33
+ 9: "Element",
34
+ 10: "Code",
35
+ 11: "Data",
36
+ 12: "DataCount",
37
+ 13: "Tag",
38
+ };
39
+ /** Read an unsigned LEB128 value; returns `null` when malformed. */
40
+ function readU32Leb(bytes, offset) {
41
+ let result = 0;
42
+ let shift = 0;
43
+ let pos = offset;
44
+ for (;;) {
45
+ if (pos >= bytes.length)
46
+ return null;
47
+ const byte = bytes[pos++];
48
+ result += (byte & 0x7f) * 2 ** shift;
49
+ if ((byte & 0x80) === 0)
50
+ break;
51
+ shift += 7;
52
+ if (shift > 35)
53
+ return null; // malformed / truncated
54
+ }
55
+ return { value: result, next: pos };
56
+ }
57
+ /** Read a length-prefixed name; returns `null` when malformed. */
58
+ function readName(bytes, offset) {
59
+ const len = readU32Leb(bytes, offset);
60
+ if (!len)
61
+ return null;
62
+ const start = len.next;
63
+ const end = start + len.value;
64
+ if (end > bytes.length)
65
+ return null;
66
+ let name = "";
67
+ for (let i = start; i < end; i++)
68
+ name += String.fromCharCode(bytes[i]);
69
+ return { value: name, next: end };
70
+ }
71
+ /**
72
+ * Collect the import namespaces (module names) of an import section.
73
+ * Malformed input simply yields no namespaces (the caller reports the
74
+ * structural problem separately).
75
+ */
76
+ function collectImportNamespaces(section, namespaces) {
77
+ const count = readU32Leb(section, 0);
78
+ if (!count)
79
+ return;
80
+ let off = count.next;
81
+ for (let i = 0; i < count.value; i++) {
82
+ const mod = readName(section, off);
83
+ if (!mod)
84
+ return;
85
+ const field = readName(section, mod.next);
86
+ if (!field)
87
+ return;
88
+ off = field.next;
89
+ const kind = section[off++];
90
+ if (kind === undefined)
91
+ return;
92
+ // Skip the kind-specific descriptor.
93
+ if (kind === 0) {
94
+ const typeIdx = readU32Leb(section, off);
95
+ if (!typeIdx)
96
+ return;
97
+ off = typeIdx.next;
98
+ }
99
+ else if (kind === 1) {
100
+ off += 1; // element type
101
+ const flags = section[off++];
102
+ if (flags === undefined)
103
+ return;
104
+ const min = readU32Leb(section, off);
105
+ if (!min)
106
+ return;
107
+ off = min.next;
108
+ if (flags & 0x01) {
109
+ const max = readU32Leb(section, off);
110
+ if (!max)
111
+ return;
112
+ off = max.next;
113
+ }
114
+ }
115
+ else if (kind === 2) {
116
+ const flags = section[off++];
117
+ if (flags === undefined)
118
+ return;
119
+ const min = readU32Leb(section, off);
120
+ if (!min)
121
+ return;
122
+ off = min.next;
123
+ if (flags & 0x01) {
124
+ const max = readU32Leb(section, off);
125
+ if (!max)
126
+ return;
127
+ off = max.next;
128
+ }
129
+ }
130
+ else if (kind === 3) {
131
+ off += 2; // value type + mutability
132
+ }
133
+ else if (kind === 4) {
134
+ const attr = section[off++];
135
+ if (attr === undefined)
136
+ return;
137
+ const typeIdx = readU32Leb(section, off);
138
+ if (!typeIdx)
139
+ return;
140
+ off = typeIdx.next;
141
+ }
142
+ else {
143
+ return; // unknown kind: stop parsing
144
+ }
145
+ if (!namespaces.includes(mod.value))
146
+ namespaces.push(mod.value);
147
+ }
148
+ }
149
+ /**
150
+ * Check that `bytes` is a structurally complete WASM binary.
151
+ *
152
+ * Walks the section table: a truncated file (the classic symptom of
153
+ * reading a file that is still being written) shows up as a section
154
+ * whose declared length runs past the end of the buffer.
155
+ */
156
+ export function checkWasmBinary(bytes) {
157
+ const namespaces = [];
158
+ if (bytes.length < 8) {
159
+ return {
160
+ ok: false,
161
+ reason: `too short to be a WebAssembly binary (${bytes.length} bytes)`,
162
+ namespaces,
163
+ };
164
+ }
165
+ for (let i = 0; i < WASM_MAGIC.length; i++) {
166
+ if (bytes[i] !== WASM_MAGIC[i]) {
167
+ return {
168
+ ok: false,
169
+ reason: "not a WebAssembly binary (bad magic header)",
170
+ namespaces,
171
+ };
172
+ }
173
+ }
174
+ const version = bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24);
175
+ if (version !== 1) {
176
+ return {
177
+ ok: false,
178
+ reason: `unsupported WebAssembly version ${version >>> 0}`,
179
+ namespaces,
180
+ };
181
+ }
182
+ let off = 8;
183
+ while (off < bytes.length) {
184
+ const id = bytes[off++];
185
+ const size = readU32Leb(bytes, off);
186
+ if (!size) {
187
+ return {
188
+ ok: false,
189
+ reason: "malformed section header (truncated binary)",
190
+ namespaces,
191
+ };
192
+ }
193
+ off = size.next;
194
+ const end = off + size.value;
195
+ if (end > bytes.length) {
196
+ const name = SECTION_NAMES[id] ?? `#${id}`;
197
+ return {
198
+ ok: false,
199
+ reason: `section ${name} extends past end of the module ` +
200
+ `(length ${size.value}, remaining bytes ${bytes.length - off})`,
201
+ namespaces,
202
+ };
203
+ }
204
+ if (id === SECTION_IMPORT) {
205
+ collectImportNamespaces(bytes.subarray(off, end), namespaces);
206
+ }
207
+ off = end;
208
+ }
209
+ return { ok: true, namespaces };
210
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  // @ts-expect-error — dist/html-to-image-wasm.js is generated at build time
11
11
  import { wasmBase64, wasmEncoding } from "../dist/html-to-image-wasm.js";
12
+ import { checkWasmBinary } from "./wasm-binary.js";
12
13
  /** Decompress the embedded WASM payload and return the raw bytes. */
13
14
  export async function loadCompressedWasmBinary() {
14
15
  const encoding = wasmEncoding;
@@ -29,6 +30,13 @@ export async function loadCompressedWasmBinary() {
29
30
  if (bytes.length === 0) {
30
31
  throw new Error("decompressed to 0 bytes");
31
32
  }
33
+ // A payload captured from a half-written `.wasm` decompresses fine but
34
+ // is not a complete module: reject it here instead of letting the
35
+ // engine fail with `section Data extends past end`.
36
+ const check = checkWasmBinary(bytes);
37
+ if (!check.ok) {
38
+ throw new Error(`restored payload is not a complete module: ${check.reason}`);
39
+ }
32
40
  return bytes;
33
41
  }
34
42
  catch (e) {