@scriptc/runtime 0.0.21 → 0.0.22

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/src/scr_runtime.h CHANGED
@@ -2683,9 +2683,15 @@ typedef enum {
2683
2683
  * a silent wrong answer. The dyn→cell edge is NOT visible to the cycle
2684
2684
  * collector (the dyn→closure stance): a cycle dyn → cell → engine
2685
2685
  * object → host closure → dyn is merely never collected (the
2686
- * documented cross-boundary-cycle divergence). Enum position: LAST —
2687
- * the LLVM backend hardcodes the preceding kind numbers. */
2686
+ * documented cross-boundary-cycle divergence). */
2688
2687
  SCR_DYN_JSVAL,
2688
+ /* A compiler-owned typed reference in transit through a native Web
2689
+ * stream. Unlike an ordinary typed→unknown conversion, this capsule
2690
+ * retains the original value so a statically typed reader can recover
2691
+ * its identity. `materialize` supplies the ordinary deep-copy view for
2692
+ * consumers such as fetch request bodies that need a dyn chunk. Enum
2693
+ * position: LAST — LLVM hardcodes every preceding kind number. */
2694
+ SCR_DYN_TYPED_REF,
2689
2695
  } ScrDynKind;
2690
2696
 
2691
2697
  /* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the
@@ -2701,12 +2707,27 @@ typedef enum {
2701
2707
  SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */
2702
2708
  SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */
2703
2709
  SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */
2710
+ SCR_DYNH_ABORT_SIGNAL, /* native static-fetch AbortSignal */
2711
+ SCR_DYNH_WEB_STREAM, /* native WHATWG ReadableStream */
2712
+ SCR_DYNH_WEB_READER, /* native ReadableStreamDefaultReader */
2713
+ SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */
2714
+ SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */
2715
+ SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */
2716
+ SCR_DYNH_EVENT, /* native static-fetch abort Event */
2704
2717
  SCR_DYNH_COUNT,
2705
2718
  } ScrDynHandleTag;
2706
2719
 
2707
2720
  typedef struct ScrDyn ScrDyn;
2708
2721
  typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
2709
2722
  typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */
2723
+ typedef struct ScrDynTypedCast {
2724
+ const char *type_key;
2725
+ size_t type_key_len;
2726
+ void *ptr;
2727
+ void *(*retain)(void *);
2728
+ void (*release)(void *);
2729
+ struct ScrDynTypedCast *next;
2730
+ } ScrDynTypedCast;
2710
2731
  typedef struct ScrJsval ScrJsval; /* opaque island cell (C11 repeat; the
2711
2732
  * always-linked dyn core never touches
2712
2733
  * its engine value — only the gated ops
@@ -2752,7 +2773,19 @@ struct ScrDyn {
2752
2773
  ScrStr *str; /* owned */
2753
2774
  ScrBytes *bytes; /* owned (SCR_DYN_BYTES) */
2754
2775
  struct { size_t len; size_t cap; ScrDyn **items; } arr; /* owned */
2755
- struct { size_t len; size_t cap; ScrDynEntry *entries; } obj; /* owned */
2776
+ struct {
2777
+ size_t len;
2778
+ size_t cap;
2779
+ ScrDynEntry *entries;
2780
+ /* Optional identity of the typed record that produced this ordinary
2781
+ * deep-copy snapshot. EventTarget uses it to recognize the same
2782
+ * EventListenerObject across repeated static→dyn crossings without
2783
+ * changing generic dyn-object `===` semantics. */
2784
+ /* Owned through source_access(source_identity, false). Callback
2785
+ * interfaces may request a fresh snapshot with the true arm. */
2786
+ void *source_identity;
2787
+ ScrDyn *(*source_access)(void *, bool materialize);
2788
+ } obj; /* owned */
2756
2789
  /* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig`
2757
2790
  * and `name` are static compiler-emitted literals (never freed); name
2758
2791
  * may be NULL (anonymous — inspect prints [Function (anonymous)]).
@@ -2767,6 +2800,23 @@ struct ScrDyn {
2767
2800
  * so a listener capturing its own boxed handle never cycles past the
2768
2801
  * settle — the settle-releases-listeners story. */
2769
2802
  struct { void *ptr; ScrDynHandleTag tag; } handle;
2803
+ /* SCR_DYN_TYPED_REF: original static value + its compiler-emitted
2804
+ * identity tag, RC adapters, and ordinary dyn snapshot adapter. */
2805
+ struct {
2806
+ void *ptr;
2807
+ void *(*retain)(void *);
2808
+ void (*release)(void *);
2809
+ const char *type_key;
2810
+ size_t type_key_len;
2811
+ ScrDyn *(*materialize)(void *);
2812
+ void (*commit)(void *, const ScrDyn *);
2813
+ /* Both caches belong to this capsule. ReadableStream canonicalizes
2814
+ * capsules for repeated source references, so the refreshed dyn view
2815
+ * and every structurally converted static view keep JS reference
2816
+ * identity while the source reference remains observable. */
2817
+ ScrDyn *materialized;
2818
+ ScrDynTypedCast *casts;
2819
+ } typed_ref;
2770
2820
  /* SCR_DYN_PROMISE: the retained promise. The boundary contract: it
2771
2821
  * settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a
2772
2822
  * void fulfillment awaiters read as the undefined value) — the
@@ -2841,6 +2891,14 @@ ScrDyn *scr_dyn_new_num(double n);
2841
2891
  ScrDyn *scr_dyn_new_str(ScrStr *s);
2842
2892
  ScrDyn *scr_dyn_new_arr(void);
2843
2893
  ScrDyn *scr_dyn_new_obj(void);
2894
+ /* The ordinary deep-copy object plus a retained typed source. source_access
2895
+ * releases that source when materialize=false and returns a fresh dyn snapshot
2896
+ * when true. Generic dyn member reads and strict equality remain snapshot/node
2897
+ * based; callback-interface consumers opt into the live arm explicitly. */
2898
+ ScrDyn *scr_dyn_new_obj_with_identity(
2899
+ void *source, void *(*source_retain)(void *),
2900
+ ScrDyn *(*source_access)(void *, bool materialize));
2901
+ bool scr_dyn_obj_same_source(const ScrDyn *a, const ScrDyn *b);
2844
2902
  /* Object.create(null): the fresh null-prototype dictionary (see the
2845
2903
  * null_proto flavor flag above). */
2846
2904
  ScrDyn *scr_dyn_new_obj_null_proto(void);
@@ -2850,6 +2908,26 @@ ScrDyn *scr_dyn_new_bytes_copy(const ScrBytes *b);
2850
2908
  /* The Buffer-flavored twin (stream chunks): string coercion/toString
2851
2909
  * decode utf8 instead of joining elements. */
2852
2910
  ScrDyn *scr_dyn_new_buffer_copy(const ScrBytes *b);
2911
+ /* Identity-preserving transit capsule used by ReadableStream.from over
2912
+ * typed arrays. The constructor retains `ptr`; matching unbox returns +1.
2913
+ * materialize lazily creates and then retains one detached dyn snapshot.
2914
+ * The cast cache lets a non-exact dynCheck reuse one safe, compiler-built
2915
+ * target view instead of raw-casting structurally different layouts. */
2916
+ ScrDyn *scr_dyn_new_typed_ref(
2917
+ void *ptr, void *(*retain)(void *), void (*release)(void *),
2918
+ const char *type_key, size_t type_key_len,
2919
+ ScrDyn *(*materialize)(void *),
2920
+ void (*commit)(void *, const ScrDyn *));
2921
+ bool scr_dyn_typed_ref_is(
2922
+ const ScrDyn *d, const char *type_key, size_t type_key_len);
2923
+ void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */
2924
+ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */
2925
+ void scr_dyn_typed_ref_commit(ScrDyn *d);
2926
+ void *scr_dyn_typed_ref_cached_cast(
2927
+ const ScrDyn *d, const char *type_key, size_t type_key_len); /* +1/NULL */
2928
+ void scr_dyn_typed_ref_cache_cast(
2929
+ ScrDyn *d, const char *type_key, size_t type_key_len, void *ptr,
2930
+ void *(*retain)(void *), void (*release)(void *));
2853
2931
  /* A fresh u8 COPY of a SCR_DYN_BYTES payload (+1) — the dynCheck
2854
2932
  * extraction (`u as Uint8Array`). */
2855
2933
  ScrBytes *scr_dyn_bytes_copy_out(const ScrDyn *d);
@@ -3137,9 +3215,10 @@ ScrDyn *scr_dyn_alloc_jsval(ScrJsval *cell, const ScrDynJsvalOps *ops);
3137
3215
  /* The installed ops (traps on a missing install — impossible unless a
3138
3216
  * JSVAL node was forged without the constructor). */
3139
3217
  const ScrDynJsvalOps *scr_dyn_jsval_ops(void);
3140
- /* dynTest arms that need the ENGINE's answer on a JSVAL node. Each
3141
- * answers false for every other kind (callers test unconditionally —
3142
- * the emitted narrowing tests stay branch-free). Never throw. */
3218
+ /* dynTest arms that need the ENGINE's answer on a JSVAL node, or the
3219
+ * materialized answer for a live typed-reference capsule. Each answers
3220
+ * false for every other kind (callers test unconditionally — the emitted
3221
+ * narrowing tests stay branch-free). Never throw. */
3143
3222
  bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name);
3144
3223
  bool scr_dyn_isl_is_array(const ScrDyn *d);
3145
3224
  bool scr_dyn_isl_is_error(const ScrDyn *d);
@@ -3819,6 +3898,33 @@ ScrGen *scr_gen_of_fiber(ScrFiber *f);
3819
3898
  long scr_promise_live_count(void);
3820
3899
  #endif
3821
3900
 
3901
+ /* ── fetch (scr_fetch.c) ──────────────────────────────────────────────
3902
+ * The native bridge over scr_net + scr_tls + scr_http's client parser.
3903
+ * Static fetch(url) resolves at the response head with native Response
3904
+ * and ReadableStream handles; Response.json consumes that same stream.
3905
+ * AbortSignal and streaming request bodies stay native too. Under
3906
+ * --dynamic the same TU boots the broader engine web surface. The
3907
+ * emitted main calls scr_fetch_install in either form. */
3908
+ void scr_fetch_install(void);
3909
+ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response handle> */
3910
+ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
3911
+ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
3912
+ ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */
3913
+ /* Borrowed number; +1 AbortSignal handle or NULL pending. */
3914
+ ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay);
3915
+ ScrDyn *scr_fetch_abort_now(ScrDyn *reason); /* borrowed reason; +1 handle */
3916
+ ScrDyn *scr_fetch_abort_any(ScrDyn *signals); /* borrowed array; +1 handle or NULL pending */
3917
+ ScrDyn *scr_fetch_stream_new(ScrDyn *source); /* borrowed source; +1 handle or NULL pending */
3918
+ ScrDyn *scr_fetch_stream_from(ScrDyn *iterable); /* borrowed iterable; +1 handle or NULL pending */
3919
+ ScrDyn *scr_fetch_stream_from_array(
3920
+ ScrArr *iterable,
3921
+ ScrDyn *(*item)(ScrArr *, double)); /* borrowed array/callback; +1 handle */
3922
+ ScrDyn *scr_fetch_stream_from_bytes(
3923
+ ScrBytes *iterable); /* borrowed bytes; +1 handle */
3924
+ ScrDyn *scr_fetch_stream_from_string(
3925
+ ScrStr *iterable); /* borrowed string; +1 handle */
3926
+ ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */
3927
+
3822
3928
  /* ── dynamic island (scr_island.c; --dynamic builds ONLY) ────────────
3823
3929
  * The embedded QuickJS-ng engine. Compiled and linked only under
3824
3930
  * -DSCR_DYNAMIC; static builds never reference these symbols (nor the
@@ -3948,18 +4054,12 @@ bool scr_island_timers_due(void);
3948
4054
  bool scr_island_timers_fire_due(void); /* true = fired at least one */
3949
4055
  void scr_island_timers_teardown(void);
3950
4056
 
3951
- /* ── fetch (scr_fetch.c) ──────────────────────────────────────────────
3952
- * The native bridge behind the island's fetch (scr_net + scr_tls +
3953
- * scr_http's client parser + zlib), compiled and linked ONLY into
3954
- * --dynamic builds whose graph references fetch. The emitted main calls
3955
- * scr_fetch_install BEFORE any island entry; install registers the
3956
- * bridge's hooks with the island, which boots the fetch glue with the
3957
- * engine. The native bridge registers NO pending/poll hooks (its
4057
+ /* ── dynamic fetch hooks ──────────────────────────────────────────────
4058
+ * The native bridge registers NO pending/poll hooks (its
3958
4059
  * transfers live on real sockets the loop's poller sleeps on); the curl
3959
4060
  * reference implementation (scr_fetch_curl.c, SCRIPTC_FETCH_CURL=1 —
3960
4061
  * one release as the flip's reference) still registers all four so the
3961
4062
  * loop can sleep on curl's fds. */
3962
- void scr_fetch_install(void);
3963
4063
  void scr_island_set_fetch(void (*boot)(void *jsctx), bool (*pending)(void),
3964
4064
  void (*poll)(double max_wait_ms), void (*teardown)(void));
3965
4065
 
@@ -4078,6 +4178,7 @@ ScrJsval *scr_jsval_destr_check(ScrJsval *v, const char *spell, const char *firs
4078
4178
  ScrJsval *scr_jsval_iter_n(ScrJsval *v, double n);
4079
4179
  int scr_jsval_set_idx(ScrJsval *o, ScrJsval *key, ScrJsval *v);
4080
4180
  ScrJsval *scr_jsval_call_method(ScrJsval *o, const ScrStr *name, int argc, ScrJsval **argv);
4181
+ ScrJsval *scr_jsval_call_this(ScrJsval *f, ScrJsval *receiver, int argc, ScrJsval **argv);
4081
4182
  /* `o.name?.(...)`: a nullish member answers the engine's undefined;
4082
4183
  * anything else calls with this = o (non-callables throw in the engine). */
4083
4184
  ScrJsval *scr_jsval_opt_call_method(ScrJsval *o, const ScrStr *name, int argc, ScrJsval **argv);
@@ -4468,6 +4569,9 @@ void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet
4468
4569
  * toward zero, wrap mod 2^8/2^32), f32 by double→float rounding. */
4469
4570
  double scr_bytes_get(const ScrBytes *b, double i);
4470
4571
  void scr_bytes_set(ScrBytes *b, double i, double v);
4572
+ /* Replace a live typed-array capsule's fixed-size payload from its dyn
4573
+ * snapshot. Source and target have the same compiler-checked bytes type. */
4574
+ void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src);
4471
4575
 
4472
4576
  /* TypedArray.prototype.slice(start, end): relative indices clamp like
4473
4577
  * string/array slice (ToIntegerOrInfinity, negatives from the end); the
@@ -5125,9 +5229,12 @@ long scr_secure_ctx_live_count(void);
5125
5229
  * /etc/ssl/cert.pem probe order scr_tls.c documents) stands in for both
5126
5230
  * Node's compiled-in Mozilla roots ('bundled', rootCertificates) and the
5127
5231
  * platform store ('system') — the established SEMANTICS divergence,
5128
- * extended to introspection; 'extra' reads NODE_EXTRA_CA_CERTS. Arrays
5129
- * are cached per type (+1 retained answers each call — Node's own
5130
- * caching, and the identity the suite pins with strictEqual). */
5232
+ * extended to introspection; 'extra' uses the NODE_EXTRA_CA_CERTS file
5233
+ * captured before user code runs. Arrays are cached per type (+1 retained
5234
+ * answers each call — Node's own caching, and the identity the suite pins
5235
+ * with strictEqual). */
5236
+ void scr_tls_ca_install(void); /* snapshots NODE_EXTRA_CA_CERTS + file bytes */
5237
+ bool scr_tls_ca_extra_pem(const char **pem, size_t *len); /* borrowed launch snapshot */
5131
5238
  ScrArr *scr_tls_ca_get(ScrStr *type); /* +1; throws ERR_INVALID_ARG_VALUE on unknown types */
5132
5239
  ScrArr *scr_tls_ca_root(void); /* +1; === getCACertificates("bundled") */
5133
5240
  /* Replaces the 'default' set: entries filter to their PEM certificate
package/src/scr_tls.c CHANGED
@@ -336,17 +336,14 @@ typedef struct ScrTlsCli {
336
336
  } ScrTlsCli;
337
337
 
338
338
  /* The default trust anchors when no `ca` option is given: the system
339
- * bundle, standing in for Node's compiled-in Mozilla roots. Probed once
340
- * across the distro spellings /etc/ssl/cert.pem first (macOS ships it;
341
- * Alpine links it), then Debian/Ubuntu's ca-certificates.crt,
342
- * Fedora/RHEL's ca-bundle.crt, and openSUSE's ca-bundle.pem. On macOS
343
- * only the first path exists, so the probe order changes nothing there.
344
- * A host with none leaves the chain empty: verification fails, exactly
345
- * the no-roots behavior /etc/ssl/cert.pem-only had. Windows is that host
346
- * by construction (no PEM bundle ships; the OS store is cert-database-
347
- * shaped) — a no-`ca` client there fails verification like a bundle-less
348
- * Unix host, which agrees with the oracle wherever the fixtures tread
349
- * (their CAs are local, never in Node's Mozilla roots either). */
339
+ * bundle, standing in for Node's compiled-in Mozilla roots, plus
340
+ * NODE_EXTRA_CA_CERTS. The system bundle is probed once across the distro
341
+ * spellings — /etc/ssl/cert.pem first (macOS ships it; Alpine links it),
342
+ * then Debian/Ubuntu's ca-certificates.crt, Fedora/RHEL's ca-bundle.crt,
343
+ * and openSUSE's ca-bundle.pem. On macOS only the first path exists, so
344
+ * the probe order changes nothing there. A host with none leaves only the
345
+ * optional extra chain. Windows is that host by construction (no PEM
346
+ * bundle ships; the OS store is cert-database-shaped). */
350
347
  static mbedtls_x509_crt scr_tls_system_roots;
351
348
  static bool scr_tls_system_roots_loaded = false;
352
349
 
@@ -386,6 +383,13 @@ static mbedtls_x509_crt *scr_tls_system_ca(void) {
386
383
  for (size_t i = 0; i < sizeof bundles / sizeof bundles[0]; i++) {
387
384
  if (mbedtls_x509_crt_parse_file(&scr_tls_system_roots, bundles[i]) == 0) break;
388
385
  }
386
+ const char *extra = NULL;
387
+ size_t extra_len = 0;
388
+ if (scr_tls_ca_extra_pem(&extra, &extra_len) && extra_len > 0) {
389
+ (void)mbedtls_x509_crt_parse(
390
+ &scr_tls_system_roots, (const unsigned char *)extra,
391
+ extra_len + 1);
392
+ }
389
393
  }
390
394
  return &scr_tls_system_roots;
391
395
  }
package/src/scr_tls_ca.c CHANGED
@@ -32,6 +32,9 @@ static ScrArr *scr_ca_bundled = NULL;
32
32
  static ScrArr *scr_ca_system = NULL;
33
33
  static ScrArr *scr_ca_extra = NULL;
34
34
  static ScrArr *scr_ca_default = NULL;
35
+ static char *scr_ca_extra_buf = NULL;
36
+ static size_t scr_ca_extra_len = 0;
37
+ static bool scr_ca_installed = false;
35
38
  static char *scr_ca_override_buf = NULL;
36
39
  static size_t scr_ca_override_len = 0;
37
40
  static uint64_t scr_ca_override_gen = 0; /* 0 = never set */
@@ -47,6 +50,9 @@ static void scr_ca_teardown(void) {
47
50
  if (scr_ca_extra != NULL) scr_arr_release(scr_ca_extra);
48
51
  if (scr_ca_default != NULL) scr_arr_release(scr_ca_default);
49
52
  scr_ca_bundled = scr_ca_system = scr_ca_extra = scr_ca_default = NULL;
53
+ free(scr_ca_extra_buf);
54
+ scr_ca_extra_buf = NULL;
55
+ scr_ca_extra_len = 0;
50
56
  free(scr_ca_override_buf);
51
57
  scr_ca_override_buf = NULL;
52
58
  }
@@ -123,6 +129,31 @@ static char *scr_ca_read_file(const char *path, size_t *out_len) {
123
129
  return buf;
124
130
  }
125
131
 
132
+ /*
133
+ * Node consumes NODE_EXTRA_CA_CERTS while the process is initialized:
134
+ * changing process.env or replacing the referenced file later does not
135
+ * alter either TLS trust or getCACertificates("extra"). Generated main
136
+ * calls this install hook before user code; the lazy fallback only serves
137
+ * library/direct-runtime callers that have no executable startup hook.
138
+ */
139
+ void scr_tls_ca_install(void) {
140
+ if (scr_ca_installed) return;
141
+ scr_ca_installed = true;
142
+ scr_ca_arm_teardown();
143
+ const char *path = getenv("NODE_EXTRA_CA_CERTS");
144
+ if (path != NULL && path[0] != '\0') {
145
+ scr_ca_extra_buf = scr_ca_read_file(path, &scr_ca_extra_len);
146
+ }
147
+ }
148
+
149
+ bool scr_tls_ca_extra_pem(const char **pem, size_t *len) {
150
+ scr_tls_ca_install();
151
+ if (scr_ca_extra_buf == NULL) return false;
152
+ *pem = scr_ca_extra_buf;
153
+ *len = scr_ca_extra_len;
154
+ return true;
155
+ }
156
+
126
157
  /* The host bundle, probed in scr_tls.c's documented order. */
127
158
  static ScrArr *scr_ca_load_host_bundle(void) {
128
159
  ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 128);
@@ -163,14 +194,10 @@ static ScrArr *scr_ca_extra_arr(void) {
163
194
  if (scr_ca_extra == NULL) {
164
195
  scr_ca_arm_teardown();
165
196
  scr_ca_extra = scr_arr_new(SCR_ELEM_STR, 4);
166
- const char *path = getenv("NODE_EXTRA_CA_CERTS");
167
- if (path != NULL && path[0] != '\0') {
168
- size_t len = 0;
169
- char *text = scr_ca_read_file(path, &len);
170
- if (text != NULL) {
171
- scr_ca_push_pem_blocks(scr_ca_extra, text, len);
172
- free(text);
173
- }
197
+ const char *pem = NULL;
198
+ size_t len = 0;
199
+ if (scr_tls_ca_extra_pem(&pem, &len)) {
200
+ scr_ca_push_pem_blocks(scr_ca_extra, pem, len);
174
201
  }
175
202
  }
176
203
  return scr_ca_extra;
package/src/scr_url.c CHANGED
@@ -7,6 +7,7 @@
7
7
  * - scheme lowercasing; "Invalid URL" TypeError on schemeless input
8
8
  * - special schemes (http/https/ws/wss/ftp/file): authority parsing with
9
9
  * host lowercasing, default-port removal (leading zeros stripped),
10
+ * canonical bracketed IPv6 literals,
10
11
  * backslash-as-slash, and any run of leading slashes tolerated
11
12
  * (http:foo.com works); file: takes an authority only after exactly
12
13
  * "//" (file:/x and file:x are host-less absolute paths, like Node)
@@ -21,8 +22,8 @@
21
22
  * path kept verbatim (data:text/plain,hi there)
22
23
  *
23
24
  * DOCUMENTED DIVERGENCES (SEMANTICS.md): non-ASCII and %-escaped hosts are
24
- * rejected with "Invalid URL" (no IDNA/punycode), IPv6 hosts are rejected,
25
- * and opaque paths skip the spec's C0-encode pass (kept verbatim).
25
+ * rejected with "Invalid URL" (no IDNA/punycode), and opaque paths skip
26
+ * the spec's C0-encode pass (kept verbatim).
26
27
  *
27
28
  * Failures THROW catchable TypeErrors through the exception cell with
28
29
  * Node's messages; callers are compiler-emitted pending checks. */
@@ -33,6 +34,12 @@
33
34
  #include <stdlib.h>
34
35
  #include <string.h>
35
36
  #include <unistd.h>
37
+ #ifdef _WIN32
38
+ #include <winsock2.h>
39
+ #include <ws2tcpip.h>
40
+ #else
41
+ #include <arpa/inet.h>
42
+ #endif
36
43
 
37
44
  ScrUrl *scr_url_retain(ScrUrl *u) {
38
45
  if (u->rc != SIZE_MAX) u->rc++;
@@ -94,6 +101,65 @@ static ScrStr *ub_take(UrlBuf *b) {
94
101
  return s;
95
102
  }
96
103
 
104
+ /* WHATWG's IPv6 serializer: lowercase hexadecimal with the first longest
105
+ * run of two-or-more zero pieces compressed. inet_pton supplies the parser,
106
+ * while serializing the eight pieces here also avoids the platform-specific
107
+ * dotted-decimal spelling inet_ntop uses for IPv4-mapped addresses. */
108
+ static bool ub_append_ipv6(UrlBuf *out, const char *raw, size_t len) {
109
+ char *text = malloc(len + 1);
110
+ if (!text) scr_trap("scriptc: out of memory\n");
111
+ memcpy(text, raw, len);
112
+ text[len] = '\0';
113
+ struct in6_addr address;
114
+ bool valid = inet_pton(AF_INET6, text, &address) == 1;
115
+ free(text);
116
+ if (!valid) return false;
117
+
118
+ const unsigned char *bytes = (const unsigned char *)&address;
119
+ uint16_t pieces[8];
120
+ for (size_t i = 0; i < 8; i++) {
121
+ pieces[i] = (uint16_t)(((uint16_t)bytes[i * 2] << 8) |
122
+ bytes[i * 2 + 1]);
123
+ }
124
+ size_t best_start = 0;
125
+ size_t best_len = 0;
126
+ for (size_t i = 0; i < 8;) {
127
+ if (pieces[i] != 0) {
128
+ i++;
129
+ continue;
130
+ }
131
+ size_t start = i;
132
+ while (i < 8 && pieces[i] == 0) i++;
133
+ size_t run = i - start;
134
+ if (run > best_len) {
135
+ best_start = start;
136
+ best_len = run;
137
+ }
138
+ }
139
+ if (best_len < 2) best_len = 0;
140
+
141
+ ub_push(out, '[');
142
+ bool first = true;
143
+ size_t compressed_end = best_start + best_len;
144
+ for (size_t i = 0; i < 8;) {
145
+ if (best_len > 0 && i == best_start) {
146
+ ub_append(out, "::", 2);
147
+ first = false;
148
+ i = compressed_end;
149
+ continue;
150
+ }
151
+ if (!first && (best_len == 0 || i != compressed_end)) ub_push(out, ':');
152
+ char hex[5];
153
+ int hex_len = snprintf(hex, sizeof hex, "%x", pieces[i]);
154
+ if (hex_len <= 0 || (size_t)hex_len >= sizeof hex) return false;
155
+ ub_append(out, hex, (size_t)hex_len);
156
+ first = false;
157
+ i++;
158
+ }
159
+ ub_push(out, ']');
160
+ return true;
161
+ }
162
+
97
163
  /* ── percent-encode sets (WHATWG) ────────────────────────────────────── */
98
164
 
99
165
  static bool enc_always(unsigned char c) {
@@ -273,25 +339,48 @@ static bool parse_authority(const char *raw, size_t len, bool special, bool is_f
273
339
  *userinfo = ub_take(&ub);
274
340
  const char *hp = at >= 0 ? raw + at + 1 : raw;
275
341
  size_t hp_len = at >= 0 ? len - (size_t)at - 1 : len;
276
- /* host[:port] no IPv6 ('[' rejected below). */
342
+ /* host[:port], with IPv6 literals bracketed per WHATWG. */
277
343
  long colon = -1;
278
- for (size_t i = 0; i < hp_len; i++) {
279
- if (hp[i] == ':') colon = (long)i;
344
+ size_t host_len = hp_len;
345
+ bool ipv6 = hp_len > 0 && hp[0] == '[';
346
+ size_t ipv6_end = 0;
347
+ if (ipv6) {
348
+ while (ipv6_end < hp_len && hp[ipv6_end] != ']') ipv6_end++;
349
+ if (ipv6_end == hp_len || ipv6_end == 1) return false;
350
+ host_len = ipv6_end + 1;
351
+ if (host_len < hp_len) {
352
+ if (hp[host_len] != ':') return false;
353
+ colon = (long)host_len;
354
+ }
355
+ } else {
356
+ for (size_t i = 0; i < hp_len; i++) {
357
+ if (hp[i] == ':') colon = (long)i;
358
+ }
359
+ host_len = colon >= 0 ? (size_t)colon : hp_len;
280
360
  }
281
- size_t host_len = colon >= 0 ? (size_t)colon : hp_len;
282
361
  /* Validate + lowercase (special) the host. Divergence: non-ASCII and
283
- * %-escapes (IDNA territory) and IPv6 are rejected outright. */
362
+ * %-escapes (IDNA territory) are rejected outright. */
284
363
  UrlBuf hb;
285
364
  ub_init(&hb);
286
- for (size_t i = 0; i < host_len; i++) {
287
- unsigned char c = (unsigned char)hp[i];
288
- if (c >= 0x80 || c == '%' || c == '[' || c == ']' || c <= 0x20 || c == '#' || c == '/' ||
289
- c == '<' || c == '>' || c == '?' || c == '@' || c == '\\' || c == '^' || c == '|') {
365
+ if (ipv6) {
366
+ if (!ub_append_ipv6(&hb, hp + 1, ipv6_end - 1)) {
290
367
  free(hb.data);
291
368
  return false;
292
369
  }
293
- if (special && c >= 'A' && c <= 'Z') c = (unsigned char)(c - 'A' + 'a');
294
- ub_push(&hb, (char)c);
370
+ } else {
371
+ for (size_t i = 0; i < host_len; i++) {
372
+ unsigned char c = (unsigned char)hp[i];
373
+ if (c >= 0x80 || c == '%' || c == ':' || c == '[' || c == ']' ||
374
+ c <= 0x20 || c == '#' || c == '/' || c == '<' || c == '>' ||
375
+ c == '?' || c == '@' || c == '\\' || c == '^' || c == '|') {
376
+ free(hb.data);
377
+ return false;
378
+ }
379
+ if (special && c >= 'A' && c <= 'Z') {
380
+ c = (unsigned char)(c - 'A' + 'a');
381
+ }
382
+ ub_push(&hb, (char)c);
383
+ }
295
384
  }
296
385
  /* file: "localhost" normalizes to "" at parse time (Node). */
297
386
  if (is_file && hb.len == 9 && memcmp(hb.data, "localhost", 9) == 0) hb.len = 0;
@@ -561,8 +650,7 @@ ScrStr *scr_url_host(ScrUrl *u) {
561
650
  }
562
651
 
563
652
  /* WHATWG hostname getter: the stored port-less host verbatim ("" for
564
- * authority-less URLs). Node would keep IPv6 brackets here; the parser
565
- * rejects IPv6 hosts (documented divergence), so none reach this getter. */
653
+ * authority-less URLs); IPv6 literals retain their brackets. */
566
654
  ScrStr *scr_url_hostname(ScrUrl *u) { return scr_str_retain(u->host); }
567
655
 
568
656
  ScrStr *scr_url_href(ScrUrl *u) {
package/src/scr_web.c CHANGED
@@ -294,6 +294,62 @@ static const char web_prelude[] =
294
294
  " return it;\n"
295
295
  " }\n"
296
296
  " [Symbol.asyncIterator](options) { return this.values(options); }\n"
297
+ " static from(iterable) {\n"
298
+ " if (iterable === undefined || iterable === null) {\n"
299
+ " throw new TypeError('ReadableStream.from requires an iterable');\n"
300
+ " }\n"
301
+ " const asyncMethod = iterable[Symbol.asyncIterator];\n"
302
+ " let method;\n"
303
+ " let asyncIterator = false;\n"
304
+ " if (asyncMethod !== undefined && asyncMethod !== null) {\n"
305
+ " if (typeof asyncMethod !== 'function') {\n"
306
+ " throw new TypeError('ReadableStream.from requires an iterable');\n"
307
+ " }\n"
308
+ " method = asyncMethod;\n"
309
+ " asyncIterator = true;\n"
310
+ " } else {\n"
311
+ " method = iterable[Symbol.iterator];\n"
312
+ " if (typeof method !== 'function') {\n"
313
+ " throw new TypeError('ReadableStream.from requires an iterable');\n"
314
+ " }\n"
315
+ " }\n"
316
+ " const iterator = method.call(iterable);\n"
317
+ " if ((typeof iterator !== 'object' || iterator === null) && typeof iterator !== 'function') {\n"
318
+ " throw new TypeError('iterator method must return an object');\n"
319
+ " }\n"
320
+ " const next = iterator.next;\n"
321
+ " let finished = false;\n"
322
+ " let started = false;\n"
323
+ " return new ReadableStream({\n"
324
+ " async pull(controller) {\n"
325
+ " started = true;\n"
326
+ " const result = asyncIterator ? await next.call(iterator) : next.call(iterator);\n"
327
+ " if ((typeof result !== 'object' || result === null) && typeof result !== 'function') {\n"
328
+ " throw new TypeError('iterator result is not an object');\n"
329
+ " }\n"
330
+ " if (result.done) { finished = true; controller.close(); return; }\n"
331
+ " controller.enqueue(asyncIterator ? result.value : await result.value);\n"
332
+ " },\n"
333
+ " async cancel(reason) {\n"
334
+ " if (finished) return;\n"
335
+ " finished = true;\n"
336
+ " if (!started) return;\n"
337
+ " const finish = iterator.return;\n"
338
+ " if (finish === undefined || finish === null) return;\n"
339
+ " if (typeof finish !== 'function') throw new TypeError('iterator return is not a function');\n"
340
+ " const result = finish.call(iterator, reason);\n"
341
+ " if (!asyncIterator && ((typeof result !== 'object' || result === null) && typeof result !== 'function')) {\n"
342
+ " throw new TypeError('iterator result is not an object');\n"
343
+ " }\n"
344
+ " if (asyncIterator) {\n"
345
+ " const awaited = await result;\n"
346
+ " if ((typeof awaited !== 'object' || awaited === null) && typeof awaited !== 'function') {\n"
347
+ " throw new TypeError('iterator result is not an object');\n"
348
+ " }\n"
349
+ " }\n"
350
+ " },\n"
351
+ " });\n"
352
+ " }\n"
297
353
  " pipeThrough(transform, options) {\n"
298
354
  " if (transform === null || typeof transform !== 'object' || !transform.writable || !transform.readable) {\n"
299
355
  " throw new TypeError('pipeThrough requires a { writable, readable } pair');\n"
@@ -1342,12 +1398,14 @@ static const char web_prelude[] =
1342
1398
  " this._headers = new g.Headers(input._headers);\n"
1343
1399
  " this._body = input._body; // shared bytes; fetch copies\n"
1344
1400
  " this._signal = input._signal;\n"
1401
+ " this._redirect = input._redirect;\n"
1345
1402
  " } else {\n"
1346
1403
  " this._url = String(input);\n"
1347
1404
  " this._method = 'GET';\n"
1348
1405
  " this._headers = new g.Headers();\n"
1349
1406
  " this._body = null;\n"
1350
1407
  " this._signal = null;\n"
1408
+ " this._redirect = 'follow';\n"
1351
1409
  " }\n"
1352
1410
  " if (init.method !== undefined) this._method = normalizeMethod(init.method);\n"
1353
1411
  " if (init.headers !== undefined) this._headers = new g.Headers(init.headers);\n"
@@ -1357,19 +1415,35 @@ static const char web_prelude[] =
1357
1415
  " }\n"
1358
1416
  " this._signal = init.signal;\n"
1359
1417
  " }\n"
1418
+ " if (init.redirect !== undefined) {\n"
1419
+ " const redirect = String(init.redirect);\n"
1420
+ " if (redirect !== 'follow' && redirect !== 'error' && redirect !== 'manual') {\n"
1421
+ " throw new TypeError(`undefined: ${redirect} is not an accepted type. Expected one of follow, manual, error.`);\n"
1422
+ " }\n"
1423
+ " this._redirect = redirect;\n"
1424
+ " }\n"
1360
1425
  " if (init.body !== undefined && init.body !== null) {\n"
1361
1426
  " if (this._method === 'GET' || this._method === 'HEAD') {\n"
1362
1427
  " throw new TypeError('Request with GET/HEAD method cannot have body.');\n"
1363
1428
  " }\n"
1364
1429
  " const [bytes, ct] = coerceBodyBytes(init.body);\n"
1430
+ " if (bytes instanceof g.ReadableStream && init.duplex !== 'half') {\n"
1431
+ " throw new TypeError('RequestInit: duplex option is required when sending a body.');\n"
1432
+ " }\n"
1365
1433
  " this._body = bytes;\n"
1366
1434
  " if (ct !== null && !this._headers.has('content-type')) this._headers.set('content-type', ct);\n"
1367
1435
  " }\n"
1436
+ " // A method override still validates a body inherited from the\n"
1437
+ " // input Request, not only a body supplied by this init.\n"
1438
+ " if (this._body !== null && (this._method === 'GET' || this._method === 'HEAD')) {\n"
1439
+ " throw new TypeError('Request with GET/HEAD method cannot have body.');\n"
1440
+ " }\n"
1368
1441
  " this._bodyUsed = false;\n"
1369
1442
  " }\n"
1370
1443
  " get url() { return this._url; }\n"
1371
1444
  " get method() { return this._method; }\n"
1372
1445
  " get headers() { return this._headers; }\n"
1446
+ " get redirect() { return this._redirect; }\n"
1373
1447
  " /* Node's Request.signal is never null — a request built without one\n"
1374
1448
  " * carries an inert signal; mint it lazily on first access. */\n"
1375
1449
  " get signal() {\n"
@@ -1391,6 +1465,7 @@ static const char web_prelude[] =
1391
1465
  " this._headers = new g.Headers(init.headers);\n"
1392
1466
  " this._url = '';\n"
1393
1467
  " this._redirected = false;\n"
1468
+ " this._type = 'default';\n"
1394
1469
  " this._bodyUsed = false;\n"
1395
1470
  " if (body === undefined || body === null) {\n"
1396
1471
  " this._body = null;\n"
@@ -1406,7 +1481,7 @@ static const char web_prelude[] =
1406
1481
  " get headers() { return this._headers; }\n"
1407
1482
  " get url() { return this._url; }\n"
1408
1483
  " get redirected() { return this._redirected; }\n"
1409
- " get type() { return 'default'; }\n"
1484
+ " get type() { return this._type; }\n"
1410
1485
  " static json(data, init) {\n"
1411
1486
  " const r = new Response(JSON.stringify(data), init);\n"
1412
1487
  " r._headers.set('content-type', 'application/json');\n"
@@ -1425,6 +1500,7 @@ static const char web_prelude[] =
1425
1500
  " r._headers = headers;\n"
1426
1501
  " r._url = url;\n"
1427
1502
  " r._redirected = redirected;\n"
1503
+ " r._type = 'basic';\n"
1428
1504
  " r._body = bodyStream;\n"
1429
1505
  " return r;\n"
1430
1506
  " };\n"