@scriptc/runtime 0.0.3 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "scriptc native runtime — C sources, compiled into every scriptc binary",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scriptc.dev",
package/src/scr_island.c CHANGED
@@ -2945,7 +2945,7 @@ static const char isl_modules_bootstrap[] =
2945
2945
  /* The reason carries the __scr_wasm_stub marker (non-enumerable):
2946
2946
  * the rejection tracker below SKIPS ledgering it, so a top-level
2947
2947
  * `WebAssembly.compile(...)` chain the program never awaits (es-
2948
- * module-lexer's `export const init`, alive in the vercel CLI's
2948
+ * module-lexer's `export const init`, alive in real CLI
2949
2949
  * graph) stays silent at teardown — under real wasm the compile
2950
2950
  * SUCCEEDS unobserved, so silence is Node's observable — while an
2951
2951
  * actual await site still sees the rejection untouched. The marker
@@ -7489,7 +7489,7 @@ static const char isl_modules_bootstrap[] =
7489
7489
  " return ah;\n"
7490
7490
  " });\n"
7491
7491
  /* node:domain — the deprecated legacy module, shimmed because
7492
- * @sentry/node (inside the vercel CLI's graph) REQUIRES it at load
7492
+ * @sentry/node (inside a real CLI's graph) REQUIRES it at load
7493
7493
  * on every path (async/domain.js's top level) while only DRIVING it
7494
7494
  * on Node < 14, which never happens here. The shim keeps the module
7495
7495
  * loadable with the real synchronous surface: create()/Domain,
@@ -7560,7 +7560,7 @@ static const char isl_modules_bootstrap[] =
7560
7560
  " return d;\n"
7561
7561
  " });\n"
7562
7562
  /* node:worker_threads — the MAIN-THREAD surface, loadable because
7563
- * undici (proxy-agent's dispatcher, in the vercel CLI's graph when
7563
+ * undici (proxy-agent's dispatcher, in a real CLI's graph when
7564
7564
  * proxy env vars exist) requires it UNGUARDED at load: fetch/
7565
7565
  * constants.js destructures MessageChannel/receiveMessageOnPort for
7566
7566
  * its structuredClone fallback, websocket/events.js MessagePort.
@@ -7680,7 +7680,7 @@ static const char isl_modules_bootstrap[] =
7680
7680
  " return v8;\n"
7681
7681
  " });\n"
7682
7682
  /* node:dns — LOADABLE with Node's surface shape, answers fenced at
7683
- * the call. proxy-agent's pac-resolver (in the vercel CLI's graph
7683
+ * the call. proxy-agent's pac-resolver (in a real CLI's graph
7684
7684
  * whenever proxy env vars exist) requires dns at LOAD and only calls
7685
7685
  * lookup when a PAC proxy actually resolves — so the module must
7686
7686
  * import cleanly, and the callback-taking members deliver their
@@ -8625,7 +8625,7 @@ static const char isl_modules_bootstrap[] =
8625
8625
  " exitCode: undefined,\n"
8626
8626
  " stdout: stream(1),\n"
8627
8627
  " stderr: stream(2),\n"
8628
- /* stdin: a REAL Readable over a whole-input host read (prettier's
8628
+ /* stdin: a REAL Readable over a whole-input host read (the formatter idiom's
8629
8629
  * get-stdin async-iterates it when no file arguments arrive). The
8630
8630
  * host read happens lazily on the first pull — a program that only
8631
8631
  * probes isTTY or registers listeners never blocks on a silent pipe,
@@ -9232,7 +9232,7 @@ static const char isl_url_src[] =
9232
9232
  " }\n"
9233
9233
  " const c = parse(s);\n"
9234
9234
  /* href and search are LIVE-COUPLED (the one WHATWG mutation loop the
9235
- * vercel CLI's API client drives: url.searchParams.set('teamId', …)
9235
+ * a real CLI's API client drives: url.searchParams.set('teamId', …)
9236
9236
  * then fetch(url)): both live in writable slots, the search setter
9237
9237
  * recomposes href around the old query, and the searchParams getter
9238
9238
  * hands out ONE URLSearchParams whose mutators write back through
package/src/scr_lib.c CHANGED
@@ -1093,6 +1093,16 @@ double scr_process_uptime(void) {
1093
1093
  return (scr_uptime_now_ms() - scr_uptime_t0_ms) / 1000.0;
1094
1094
  }
1095
1095
 
1096
+ /* perf_hooks performance.now(): milliseconds since the process's own
1097
+ * start (Node's timeOrigin anchor), fractional — the same monotonic
1098
+ * clock and anchor as uptime, in Node's performance.now units. */
1099
+ double scr_perf_now(void) {
1100
+ #ifdef _WIN32
1101
+ if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
1102
+ #endif
1103
+ return scr_uptime_now_ms() - scr_uptime_t0_ms;
1104
+ }
1105
+
1096
1106
  #ifdef _WIN32
1097
1107
  /* GetProcessTimes/GetThreadTimes answer 100ns units; Node reports µs. */
1098
1108
  static double scr_filetime_us(FILETIME ft) {
package/src/scr_qs.c ADDED
@@ -0,0 +1,496 @@
1
+ /* node:querystring (scr_runtime.h has the API contract): Node v24's
2
+ * legacy query-string codec — NOT URLSearchParams (the escaping and '+'
3
+ * rules differ; that surface lives in scr_url_params.c). LINK-GATED:
4
+ * compiled into the binary only when the IR uses the qs surface
5
+ * (moduleUsesQs — the scr_url_params gating precedent), so qs-free
6
+ * programs pay zero bytes. querystring.escape never reaches this unit at
7
+ * all: Node's qsEscape encodes exactly the component unreserved set, so
8
+ * the frontend lowers it to the always-linked
9
+ * scr_str_encode_uri_component.
10
+ *
11
+ * Everything here is a quirk-faithful port of lib/querystring.js (Node is
12
+ * the oracle): parse's interleaved sep/eq scan with its naive
13
+ * partial-match resets and the encodeCheck fast path, unescapeBuffer's
14
+ * UTF-16-code-unit byte truncation, and stringify's encodeStringified
15
+ * value rules. The scans run byte-wise over the runtime's UTF-8 storage —
16
+ * equivalent to Node's code-unit scans for well-formed input, because the
17
+ * machine only assigns meaning to ASCII ('%', '+', hex, and the sep/eq
18
+ * sequences positionally) and UTF-8 multibyte alignment mirrors UTF-16
19
+ * unit alignment. */
20
+ #include "scr_runtime.h"
21
+
22
+ #include <math.h>
23
+ #include <stdio.h>
24
+ #include <stdlib.h>
25
+ #include <string.h>
26
+
27
+ /* ── a tiny growable byte buffer (scr_url_params.c's, redeclared) ────── */
28
+
29
+ typedef struct {
30
+ char *data;
31
+ size_t len;
32
+ size_t cap;
33
+ } QsBuf;
34
+
35
+ static void qb_init(QsBuf *b) {
36
+ b->cap = 64;
37
+ b->len = 0;
38
+ b->data = malloc(b->cap);
39
+ if (!b->data) {
40
+ fputs("scriptc: out of memory\n", stderr);
41
+ abort();
42
+ }
43
+ }
44
+
45
+ static void qb_append(QsBuf *b, const char *bytes, size_t n) {
46
+ if (b->len + n > b->cap) {
47
+ while (b->len + n > b->cap) b->cap *= 2;
48
+ char *grown = realloc(b->data, b->cap);
49
+ if (!grown) {
50
+ fputs("scriptc: out of memory\n", stderr);
51
+ abort();
52
+ }
53
+ b->data = grown;
54
+ }
55
+ memcpy(b->data + b->len, bytes, n);
56
+ b->len += n;
57
+ }
58
+
59
+ static void qb_push(QsBuf *b, char c) { qb_append(b, &c, 1); }
60
+
61
+ static void qb_append_str(QsBuf *b, const ScrStr *s) {
62
+ qb_append(b, s->data, s->len);
63
+ }
64
+
65
+ static ScrStr *qb_take(QsBuf *b) {
66
+ ScrStr *s = scr_str_new(b->data, b->len);
67
+ free(b->data);
68
+ return s;
69
+ }
70
+
71
+ /* ── shared scan helpers ─────────────────────────────────────────────── */
72
+
73
+ static int qs_unhex(unsigned c) {
74
+ if (c >= '0' && c <= '9') return (int)(c - '0');
75
+ if (c >= 'A' && c <= 'F') return (int)(c - 'A' + 10);
76
+ if (c >= 'a' && c <= 'f') return (int)(c - 'a' + 10);
77
+ return -1;
78
+ }
79
+
80
+ /* One UTF-8 code point at p (well-formed by the runtime invariant);
81
+ * *adv gets the byte length. */
82
+ static uint32_t qs_utf8_decode(const char *p, size_t *adv) {
83
+ unsigned char b0 = (unsigned char)p[0];
84
+ if (b0 < 0x80) {
85
+ *adv = 1;
86
+ return b0;
87
+ }
88
+ if (b0 < 0xE0) {
89
+ *adv = 2;
90
+ return (uint32_t)((b0 & 0x1Fu) << 6) | ((unsigned char)p[1] & 0x3Fu);
91
+ }
92
+ if (b0 < 0xF0) {
93
+ *adv = 3;
94
+ return (uint32_t)((b0 & 0x0Fu) << 12) |
95
+ (uint32_t)(((unsigned char)p[1] & 0x3Fu) << 6) |
96
+ ((unsigned char)p[2] & 0x3Fu);
97
+ }
98
+ *adv = 4;
99
+ return (uint32_t)((b0 & 0x07u) << 18) |
100
+ (uint32_t)(((unsigned char)p[1] & 0x3Fu) << 12) |
101
+ (uint32_t)(((unsigned char)p[2] & 0x3Fu) << 6) |
102
+ ((unsigned char)p[3] & 0x3Fu);
103
+ }
104
+
105
+ static void qs_utf8_encode(QsBuf *b, uint32_t cp) {
106
+ char tmp[4];
107
+ if (cp < 0x80) {
108
+ tmp[0] = (char)cp;
109
+ qb_append(b, tmp, 1);
110
+ } else if (cp < 0x800) {
111
+ tmp[0] = (char)(0xC0 | (cp >> 6));
112
+ tmp[1] = (char)(0x80 | (cp & 0x3F));
113
+ qb_append(b, tmp, 2);
114
+ } else if (cp < 0x10000) {
115
+ tmp[0] = (char)(0xE0 | (cp >> 12));
116
+ tmp[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
117
+ tmp[2] = (char)(0x80 | (cp & 0x3F));
118
+ qb_append(b, tmp, 3);
119
+ } else {
120
+ tmp[0] = (char)(0xF0 | (cp >> 18));
121
+ tmp[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
122
+ tmp[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
123
+ tmp[3] = (char)(0x80 | (cp & 0x3F));
124
+ qb_append(b, tmp, 4);
125
+ }
126
+ }
127
+
128
+ /* ── unescape (Node's qsUnescape) ────────────────────────────────────── */
129
+
130
+ /* The string's UTF-16 code units (astral chars split into surrogate
131
+ * pairs) — unescapeBuffer's scan is defined over these, byte-truncation
132
+ * quirk included, so the fallback builds them first. Caller frees. */
133
+ static uint16_t *qs_utf16_units(const ScrStr *s, size_t *out_n) {
134
+ /* One unit per byte is a safe cap (ASCII 1:1; multibyte shrinks). */
135
+ uint16_t *units = malloc(sizeof(uint16_t) * (s->len ? s->len : 1));
136
+ if (!units) {
137
+ fputs("scriptc: out of memory\n", stderr);
138
+ abort();
139
+ }
140
+ size_t n = 0, i = 0;
141
+ while (i < s->len) {
142
+ size_t adv;
143
+ uint32_t cp = qs_utf8_decode(s->data + i, &adv);
144
+ i += adv;
145
+ if (cp >= 0x10000) {
146
+ cp -= 0x10000;
147
+ units[n++] = (uint16_t)(0xD800 + (cp >> 10));
148
+ units[n++] = (uint16_t)(0xDC00 + (cp & 0x3FF));
149
+ } else {
150
+ units[n++] = (uint16_t)cp;
151
+ }
152
+ }
153
+ *out_n = n;
154
+ return units;
155
+ }
156
+
157
+ /* WHATWG UTF-8 decode with replacement (maximal subpart) — the semantics
158
+ * of Buffer.prototype.toString('utf8') the fallback path ends with. */
159
+ static ScrStr *qs_utf8_replace_decode(const unsigned char *p, size_t n) {
160
+ QsBuf out;
161
+ qb_init(&out);
162
+ size_t i = 0;
163
+ while (i < n) {
164
+ unsigned char b0 = p[i];
165
+ if (b0 < 0x80) {
166
+ qb_push(&out, (char)b0);
167
+ i++;
168
+ continue;
169
+ }
170
+ int need;
171
+ unsigned lo = 0x80, hi = 0xBF;
172
+ if (b0 >= 0xC2 && b0 <= 0xDF) need = 1;
173
+ else if (b0 == 0xE0) { need = 2; lo = 0xA0; }
174
+ else if (b0 == 0xED) { need = 2; hi = 0x9F; }
175
+ else if (b0 >= 0xE1 && b0 <= 0xEF) need = 2;
176
+ else if (b0 == 0xF0) { need = 3; lo = 0x90; }
177
+ else if (b0 == 0xF4) { need = 3; hi = 0x8F; }
178
+ else if (b0 >= 0xF1 && b0 <= 0xF3) need = 3;
179
+ else { /* 0x80..0xC1, 0xF5..0xFF: never a leading byte */
180
+ qs_utf8_encode(&out, 0xFFFD);
181
+ i++;
182
+ continue;
183
+ }
184
+ /* Consume as many valid continuations as exist (maximal subpart):
185
+ * an invalid or missing continuation replaces the consumed prefix
186
+ * with ONE U+FFFD and rescans at the offending byte. */
187
+ uint32_t cp = b0 & (uint32_t)(0xFF >> (need + 2));
188
+ size_t j = i + 1;
189
+ int k = 0;
190
+ bool ok = true;
191
+ for (; k < need; k++, j++) {
192
+ if (j >= n) { ok = false; break; }
193
+ unsigned c = p[j];
194
+ unsigned clo = (k == 0) ? lo : 0x80, chi = (k == 0) ? hi : 0xBF;
195
+ if (c < clo || c > chi) { ok = false; break; }
196
+ cp = (cp << 6) | (c & 0x3F);
197
+ }
198
+ if (!ok) {
199
+ qs_utf8_encode(&out, 0xFFFD);
200
+ i = j; /* prefix consumed; rescan at the offending byte */
201
+ continue;
202
+ }
203
+ qs_utf8_encode(&out, cp);
204
+ i = j;
205
+ }
206
+ return qb_take(&out);
207
+ }
208
+
209
+ /* Node's unescapeBuffer(s) (decodeSpaces false — neither qsUnescape nor
210
+ * the exported unescape passes it) over the UTF-16 units, then the
211
+ * replacement decode of the byte buffer. */
212
+ static ScrStr *qs_unescape_buffer(const ScrStr *s) {
213
+ size_t n;
214
+ uint16_t *units = qs_utf16_units(s, &n);
215
+ unsigned char *out = malloc(n ? n : 1);
216
+ if (!out) {
217
+ fputs("scriptc: out of memory\n", stderr);
218
+ abort();
219
+ }
220
+ size_t w = 0, index = 0;
221
+ /* maxLength = s.length - 2 as a signed comparison (n < 2 must refuse
222
+ * every escape, matching Node's `index < maxLength` on negatives). */
223
+ while (index < n) {
224
+ unsigned cur = units[index];
225
+ if (cur == '%' && n >= 3 && index < n - 2) {
226
+ unsigned c1 = units[index + 1];
227
+ int hexHigh = (c1 < 256) ? qs_unhex(c1) : -1;
228
+ if (hexHigh < 0) {
229
+ out[w++] = '%';
230
+ index++;
231
+ continue;
232
+ }
233
+ unsigned c2 = units[index + 2];
234
+ int hexLow = (c2 < 256) ? qs_unhex(c2) : -1;
235
+ if (hexLow < 0) {
236
+ out[w++] = '%';
237
+ index++; /* Node: ++index then index-- — net one step */
238
+ continue;
239
+ }
240
+ cur = (unsigned)(hexHigh * 16 + hexLow);
241
+ index += 2;
242
+ }
243
+ out[w++] = (unsigned char)(cur & 0xFF); /* Buffer write truncates */
244
+ index++;
245
+ }
246
+ free(units);
247
+ ScrStr *res = qs_utf8_replace_decode(out, w);
248
+ free(out);
249
+ return res;
250
+ }
251
+
252
+ ScrStr *scr_qs_unescape(const ScrStr *s) {
253
+ ScrStr *strict = scr_str_decode_uri_component_try((ScrStr *)s);
254
+ if (strict) return strict;
255
+ return qs_unescape_buffer(s);
256
+ }
257
+
258
+ /* ── parse ───────────────────────────────────────────────────────────── */
259
+
260
+ /* addKeyVal's tail: decode-if-encoded, then group into the overflow map
261
+ * (first value = the str_tag arm; a repeat REPLACES it with a two-element
262
+ * string[] in the arr_tag arm; later repeats push). key/value move in. */
263
+ static void qs_add_key_val(ScrMap *out, ScrStr *key, ScrStr *value,
264
+ bool key_encoded, bool val_encoded,
265
+ uint32_t str_tag, uint32_t arr_tag) {
266
+ if (key->len > 0 && key_encoded) {
267
+ ScrStr *dec = scr_qs_unescape(key);
268
+ scr_str_release(key);
269
+ key = dec;
270
+ }
271
+ if (value->len > 0 && val_encoded) {
272
+ ScrStr *dec = scr_qs_unescape(value);
273
+ scr_str_release(value);
274
+ value = dec;
275
+ }
276
+ ScrUnion *cell = scr_map_get_str_ref(out, key);
277
+ if (!cell) {
278
+ scr_map_set_str_ref(out, key,
279
+ scr_union_new_ref(str_tag, value, &scr_str_retain_v,
280
+ &scr_str_release_v, NULL));
281
+ scr_str_release(key);
282
+ return;
283
+ }
284
+ if (cell->tag == str_tag) {
285
+ /* obj[key] = [curValue, value] */
286
+ ScrArr *rows = scr_arr_new(SCR_ELEM_STR, 2);
287
+ scr_arr_push_ref(rows, scr_str_retain_v(scr_union_peek(cell)));
288
+ scr_arr_push_ref(rows, value);
289
+ scr_map_set_str_ref(out, key,
290
+ scr_union_new_ref(arr_tag, rows, &scr_arr_retain_v,
291
+ &scr_arr_release_v, NULL));
292
+ } else {
293
+ scr_arr_push_ref((ScrArr *)scr_union_peek(cell), value);
294
+ }
295
+ scr_union_release(cell);
296
+ scr_str_release(key);
297
+ }
298
+
299
+ void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
300
+ const ScrStr *eq, double max_keys, uint32_t str_tag,
301
+ uint32_t arr_tag) {
302
+ if (qs->len == 0) return;
303
+ /* Node's falsy rule: undefined/null/'' all mean the default. */
304
+ const char *sep_b = (sep && sep->len) ? sep->data : "&";
305
+ size_t sep_len = (sep && sep->len) ? sep->len : 1;
306
+ const char *eq_b = (eq && eq->len) ? eq->data : "=";
307
+ size_t eq_len = (eq && eq->len) ? eq->len : 1;
308
+
309
+ /* pairs: Node's `maxKeys > 0 ? maxKeys : -1` as a double so Infinity
310
+ * decrements forever, exactly like Node's -1 sentinel. */
311
+ double pairs = max_keys > 0 ? max_keys : -1;
312
+
313
+ const char *p = qs->data;
314
+ size_t n = qs->len;
315
+ QsBuf key, value;
316
+ qb_init(&key);
317
+ qb_init(&value);
318
+ size_t last_pos = 0, sep_idx = 0, eq_idx = 0;
319
+ bool key_encoded = false, val_encoded = false;
320
+ int encode_check = 0;
321
+ bool returned = false;
322
+
323
+ for (size_t i = 0; i < n; ++i) {
324
+ unsigned char code = (unsigned char)p[i];
325
+
326
+ /* Try matching the pair separator (e.g. '&'). */
327
+ if (code == (unsigned char)sep_b[sep_idx]) {
328
+ if (++sep_idx == sep_len) {
329
+ /* Key/value pair separator match. */
330
+ size_t end = i - sep_idx + 1;
331
+ if (eq_idx < eq_len) {
332
+ /* No (entire) key/value separator seen. */
333
+ if (last_pos < end) {
334
+ qb_append(&key, p + last_pos, end - last_pos);
335
+ } else if (key.len == 0) {
336
+ /* An empty substring between separators. */
337
+ if (--pairs == 0) { returned = true; break; }
338
+ last_pos = i + 1;
339
+ sep_idx = eq_idx = 0;
340
+ continue;
341
+ }
342
+ } else if (last_pos < end) {
343
+ qb_append(&value, p + last_pos, end - last_pos);
344
+ }
345
+
346
+ qs_add_key_val(out, scr_str_new(key.data, key.len),
347
+ scr_str_new(value.data, value.len), key_encoded,
348
+ val_encoded, str_tag, arr_tag);
349
+
350
+ if (--pairs == 0) { returned = true; break; }
351
+ key_encoded = val_encoded = false;
352
+ key.len = value.len = 0;
353
+ encode_check = 0;
354
+ last_pos = i + 1;
355
+ sep_idx = eq_idx = 0;
356
+ }
357
+ } else {
358
+ sep_idx = 0;
359
+ /* Try matching the key/value separator (e.g. '=') if we haven't. */
360
+ if (eq_idx < eq_len) {
361
+ if (code == (unsigned char)eq_b[eq_idx]) {
362
+ if (++eq_idx == eq_len) {
363
+ /* Key/value separator match. */
364
+ size_t end = i - eq_idx + 1;
365
+ if (last_pos < end) qb_append(&key, p + last_pos, end - last_pos);
366
+ encode_check = 0;
367
+ last_pos = i + 1;
368
+ }
369
+ continue;
370
+ }
371
+ eq_idx = 0;
372
+ if (!key_encoded) {
373
+ /* Match a valid encoded byte once, to minimize decode calls. */
374
+ if (code == '%') {
375
+ encode_check = 1;
376
+ continue;
377
+ } else if (encode_check > 0) {
378
+ if (qs_unhex(code) >= 0) {
379
+ if (++encode_check == 3) key_encoded = true;
380
+ continue;
381
+ } else {
382
+ encode_check = 0;
383
+ }
384
+ }
385
+ }
386
+ if (code == '+') {
387
+ if (last_pos < i) qb_append(&key, p + last_pos, i - last_pos);
388
+ qb_push(&key, ' ');
389
+ last_pos = i + 1;
390
+ continue;
391
+ }
392
+ }
393
+ if (code == '+') {
394
+ if (last_pos < i) qb_append(&value, p + last_pos, i - last_pos);
395
+ qb_push(&value, ' ');
396
+ last_pos = i + 1;
397
+ } else if (!val_encoded) {
398
+ if (code == '%') {
399
+ encode_check = 1;
400
+ } else if (encode_check > 0) {
401
+ if (qs_unhex(code) >= 0) {
402
+ if (++encode_check == 3) val_encoded = true;
403
+ } else {
404
+ encode_check = 0;
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ if (!returned) {
412
+ /* Leftover key or value data. */
413
+ bool ended_empty = false;
414
+ if (last_pos < n) {
415
+ if (eq_idx < eq_len) qb_append(&key, p + last_pos, n - last_pos);
416
+ else if (sep_idx < sep_len) qb_append(&value, p + last_pos, n - last_pos);
417
+ } else if (eq_idx == 0 && key.len == 0) {
418
+ ended_empty = true; /* ended on an empty substring */
419
+ }
420
+ if (!ended_empty) {
421
+ qs_add_key_val(out, scr_str_new(key.data, key.len),
422
+ scr_str_new(value.data, value.len), key_encoded,
423
+ val_encoded, str_tag, arr_tag);
424
+ }
425
+ }
426
+ free(key.data);
427
+ free(value.data);
428
+ }
429
+
430
+ /* ── stringify ───────────────────────────────────────────────────────── */
431
+
432
+ /* Node's encodeStringified over one DOM value: strings escape, finite
433
+ * numbers render then escape, booleans are bare, everything else is the
434
+ * empty value. Appends to b. */
435
+ static void qs_stringify_value(QsBuf *b, const ScrDyn *v) {
436
+ switch (v->kind) {
437
+ case SCR_DYN_STR: {
438
+ ScrStr *enc = scr_str_encode_uri_component(v->v.str);
439
+ qb_append_str(b, enc);
440
+ scr_str_release(enc);
441
+ return;
442
+ }
443
+ case SCR_DYN_NUM: {
444
+ if (!isfinite(v->v.num)) return;
445
+ ScrStr *num = scr_f64_to_scrstr(v->v.num);
446
+ ScrStr *enc = scr_str_encode_uri_component(num);
447
+ qb_append_str(b, enc);
448
+ scr_str_release(enc);
449
+ scr_str_release(num);
450
+ return;
451
+ }
452
+ case SCR_DYN_BOOL:
453
+ if (v->v.b) qb_append(b, "true", 4);
454
+ else qb_append(b, "false", 5);
455
+ return;
456
+ default:
457
+ return; /* null/undefined/objects/arrays/functions → '' */
458
+ }
459
+ }
460
+
461
+ ScrStr *scr_qs_stringify(const ScrDyn *obj, const ScrStr *sep,
462
+ const ScrStr *eq) {
463
+ const char *sep_b = (sep && sep->len) ? sep->data : "&";
464
+ size_t sep_len = (sep && sep->len) ? sep->len : 1;
465
+ const char *eq_b = (eq && eq->len) ? eq->data : "=";
466
+ size_t eq_len = (eq && eq->len) ? eq->len : 1;
467
+ if (!obj || obj->kind != SCR_DYN_OBJ) return scr_str_new("", 0);
468
+
469
+ QsBuf fields;
470
+ qb_init(&fields);
471
+ /* JS own-key order (array-index keys ascending first) — ObjectKeys. */
472
+ ScrDyn *keys = scr_dyn_obj_keys((ScrDyn *)obj);
473
+ for (size_t i = 0; i < keys->v.arr.len; i++) {
474
+ const ScrDyn *kd = keys->v.arr.items[i];
475
+ const ScrStr *k = kd->v.str;
476
+ const ScrDyn *v = scr_dyn_obj_get(obj, k->data, k->len);
477
+ if (!v) continue; /* unreachable: keys came from the object */
478
+ ScrStr *ks = scr_str_encode_uri_component((ScrStr *)k);
479
+ if (v->kind == SCR_DYN_ARR) {
480
+ for (size_t j = 0; j < v->v.arr.len; j++) {
481
+ if (fields.len) qb_append(&fields, sep_b, sep_len);
482
+ qb_append_str(&fields, ks);
483
+ qb_append(&fields, eq_b, eq_len);
484
+ qs_stringify_value(&fields, v->v.arr.items[j]);
485
+ }
486
+ } else {
487
+ if (fields.len) qb_append(&fields, sep_b, sep_len);
488
+ qb_append_str(&fields, ks);
489
+ qb_append(&fields, eq_b, eq_len);
490
+ qs_stringify_value(&fields, v);
491
+ }
492
+ scr_str_release(ks);
493
+ }
494
+ scr_dyn_release(keys);
495
+ return qb_take(&fields);
496
+ }
package/src/scr_runtime.h CHANGED
@@ -484,6 +484,64 @@ ScrStr *scr_str_encode_uri_component(ScrStr *s);
484
484
  * malformed") and returns NULL. Borrows s; result +1. */
485
485
  ScrStr *scr_str_decode_uri_component(ScrStr *s);
486
486
 
487
+ /* The non-throwing core of decodeURIComponent: NULL on malformed input
488
+ * instead of the URIError — the try/catch shape node:querystring's
489
+ * unescape wraps around decodeURIComponent (scr_qs.c's strict pass). */
490
+ ScrStr *scr_str_decode_uri_component_try(ScrStr *s);
491
+
492
+ /* ── node:querystring (scr_qs.c — LINK-GATED by moduleUsesQs; the
493
+ * scr_url_params.c precedent: pure data transforms, no loop hooks).
494
+ * querystring.escape needs no entry here: Node's qsEscape encodes exactly
495
+ * the component unreserved set, so the frontend lowers it to
496
+ * scr_str_encode_uri_component (always linked; a program using only
497
+ * escape never pulls this unit). */
498
+
499
+ /* querystring.unescape — Node's qsUnescape: strict decodeURIComponent
500
+ * first, and on failure the lenient legacy unescapeBuffer(s).toString()
501
+ * (valid %XX escapes decode to their byte, malformed escapes copy
502
+ * literally, every non-escape UTF-16 CODE UNIT truncates to its low byte
503
+ * — Node's Buffer element write — and the byte buffer decodes as UTF-8
504
+ * with U+FFFD replacement per maximal subpart, Buffer.toString's rule).
505
+ * Borrows s; result +1; never throws. */
506
+ ScrStr *scr_qs_unescape(const ScrStr *s);
507
+
508
+ /* querystring.parse — Node v24's scan state machine, byte-wise over the
509
+ * UTF-8 storage (equivalent to the code-unit scan for well-formed input:
510
+ * the machine compares sep/eq sequences positionally and treats '%'/'+'/
511
+ * hex as ASCII, and multibyte alignment is preserved). Decoded pairs land
512
+ * in `out` — the PURE-index-signature Dict record's overflow map (the
513
+ * emitters pass rec->sc_ovf) — grouped like Node's addKeyVal: a first
514
+ * value stores the `str_tag` union arm, a repeat REPLACES it with a
515
+ * two-element string[] wrapped in `arr_tag`, later repeats push. sep/eq
516
+ * fall back to "&"/"=" when empty (Node's falsy rule; the frontend
517
+ * completes omitted/null arguments to the defaults). max_keys is Node's
518
+ * rule exactly: > 0 caps the PAIR count (empty skipped segments count,
519
+ * like Node's --pairs), anything else (0, negatives, NaN) is unlimited;
520
+ * the frontend completes the omitted option to 1000. Only the DEFAULT
521
+ * decoder runs (custom decodeURIComponent options fence at compile time),
522
+ * so '+' means ' ' and segments decode with scr_qs_unescape's semantics
523
+ * only when they carry a full valid %XX triple (Node's encodeCheck).
524
+ * Borrows everything; never throws. */
525
+ typedef struct ScrMap ScrMap; /* full definition below (C11 repeat) */
526
+ void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
527
+ const ScrStr *eq, double max_keys, uint32_t str_tag,
528
+ uint32_t arr_tag);
529
+
530
+ /* querystring.stringify — Node's stringify over a borrowed DOM value (the
531
+ * frontend dynFroms the typed record; JS-world dyn values pass straight
532
+ * through). Non-object DOMs answer "" like Node; object keys iterate in
533
+ * JS own-key order (scr_dyn_obj_keys — array-index keys ascending first,
534
+ * then insertion order, Node's ObjectKeys). Values serialize per Node's
535
+ * encodeStringified: strings escape, finite numbers render shortest-
536
+ * roundtrip then escape ('1e+21' → '1e%2B21'), booleans are bare
537
+ * true/false, arrays expand to repeated keys (empty arrays emit NOTHING,
538
+ * key included), and everything else (null, undefined, nested objects/
539
+ * arrays, functions) is the empty value — key and eq still emitted.
540
+ * sep/eq fall back to "&"/"=" when empty (Node's `sep ||= '&'`). Result
541
+ * +1; never throws. */
542
+ ScrStr *scr_qs_stringify(const struct ScrDyn *obj, const ScrStr *sep,
543
+ const ScrStr *eq);
544
+
487
545
  /* encodeURI — the same ECMA-262 Encode() with the reserved set and '#'
488
546
  * kept unescaped (scr_encode_uri_impl's keep_reserved arm). Total by the
489
547
  * well-formed-UTF-8 invariant. Borrows s; result +1. */
@@ -1675,6 +1733,9 @@ double scr_process_umask(double mask);
1675
1733
  * has no answer). scr_active_resources (scr_async.c) walks the loop's
1676
1734
  * own bookkeeping — 'Timeout'/'Immediate' strings, +1. */
1677
1735
  double scr_process_uptime(void);
1736
+ /* perf_hooks performance.now(): fractional ms since process start (the
1737
+ * uptime anchor — Node's timeOrigin for a compiled program). */
1738
+ double scr_perf_now(void);
1678
1739
  double scr_cpu_user(void);
1679
1740
  double scr_cpu_system(void);
1680
1741
  double scr_cpu_user_diff(double prev);
package/src/scr_string.c CHANGED
@@ -1212,7 +1212,12 @@ static int scr_uri_hex_byte(const char *p, size_t rem) {
1212
1212
  return (hi << 4) | lo;
1213
1213
  }
1214
1214
 
1215
- ScrStr *scr_str_decode_uri_component(ScrStr *s) {
1215
+ /* The non-throwing core of decodeURIComponent: NULL on malformed input
1216
+ * (bad hex, invalid UTF-8 octets) instead of the URIError — the
1217
+ * querystring unit's unescape needs exactly the try/catch shape Node's
1218
+ * qsUnescape wraps around decodeURIComponent (scr_qs.c), and the throwing
1219
+ * entry point below stays byte-identical by rethrowing over NULL. */
1220
+ ScrStr *scr_str_decode_uri_component_try(ScrStr *s) {
1216
1221
  /* Decoding only ever shrinks (%XX → 1 byte), so len is a safe cap. */
1217
1222
  ScrStr *out = scr_str_alloc_raw(0, s->len);
1218
1223
  size_t w = 0;
@@ -1258,7 +1263,15 @@ ScrStr *scr_str_decode_uri_component(ScrStr *s) {
1258
1263
  return out;
1259
1264
  malformed:
1260
1265
  scr_str_release(out);
1261
- scr_uri_malformed();
1262
1266
  return NULL;
1263
1267
  }
1264
1268
 
1269
+ ScrStr *scr_str_decode_uri_component(ScrStr *s) {
1270
+ ScrStr *out = scr_str_decode_uri_component_try(s);
1271
+ if (!out) {
1272
+ scr_uri_malformed();
1273
+ return NULL;
1274
+ }
1275
+ return out;
1276
+ }
1277
+
@@ -3155,7 +3155,7 @@ int mbedtls_ecp_gen_keypair(mbedtls_ecp_group *grp,
3155
3155
  }
3156
3156
 
3157
3157
  /*
3158
- * Generate a keypair, prettier wrapper
3158
+ * Generate a keypair, the formatter wrapper
3159
3159
  */
3160
3160
  int mbedtls_ecp_gen_key(mbedtls_ecp_group_id grp_id, mbedtls_ecp_keypair *key,
3161
3161
  int (*f_rng)(void *, unsigned char *, size_t), void *p_rng)