@scriptc/runtime 0.0.12 → 0.0.13

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
@@ -1613,7 +1613,7 @@ ScrPromise *scr_sp_pipeline(double n, ScrStream **streams);
1613
1613
  * and settle at the terminal point (right after 'close', the eos timing
1614
1614
  * Node's consumers share) — text answers the utf8 decode, json parses
1615
1615
  * the text (malformed input rejects with the parse's SyntaxError; the
1616
- * result is a +1 DOM tree), buffer the concatenated bytes. Stream
1616
+ * result is a +1 dyn tree), buffer the concatenated bytes. Stream
1617
1617
  * errors reject; an early close rejects ERR_STREAM_PREMATURE_CLOSE; a
1618
1618
  * stream with no readable side rejects Node's async-iterable TypeError.
1619
1619
  * Streams borrowed; +1 promises. */
@@ -2681,6 +2681,8 @@ typedef enum {
2681
2681
  SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */
2682
2682
  SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */
2683
2683
  SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */
2684
+ SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */
2685
+ SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */
2684
2686
  SCR_DYNH_COUNT,
2685
2687
  } ScrDynHandleTag;
2686
2688
 
@@ -2861,6 +2863,9 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value
2861
2863
  * non-object kinds throw Node's catchable TypeErrors (strict-mode
2862
2864
  * wording). All three operands BORROWED (the value is retained in). */
2863
2865
  void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
2866
+ /* `key in v` with a runtime key — the dynHasKey fold per value (OBJ own
2867
+ * members, ARR length/valid indices, false elsewhere). Never throws. */
2868
+ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key);
2864
2869
  /* Bare `typeof v` on a dyn value: the dyn kind's JS answer (+1 string;
2865
2870
  * null answers "object"). Never throws. */
2866
2871
  ScrStr *scr_dyn_typeof(const ScrDyn *d);
@@ -3378,16 +3383,37 @@ void scr_als_disable(double id);
3378
3383
  ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args);
3379
3384
  ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args);
3380
3385
 
3381
- /* process.on('unhandledRejection', fn): dyn listeners dispatched per
3382
- * never-observed rejection at loop end — (reason, promise), suppressing
3383
- * the default report and the exit-1 (Node's handled-event contract; the
3384
- * end-of-turn vs loop-end timing is a SEMANTICS.md divergence). Throws
3385
- * Node's ERR_INVALID_ARG_TYPE on a non-function. */
3386
- void scr_process_on_unhandled_rejection(ScrDyn *fn);
3386
+ /* process.on/once('unhandledRejection', fn): dyn listeners dispatched
3387
+ * per never-observed rejection at loop end — (reason, promise),
3388
+ * suppressing the default report and the exit-1 (Node's handled-event
3389
+ * contract; the end-of-turn vs loop-end timing is a SEMANTICS.md
3390
+ * divergence). `once` auto-removes after one delivery; off removes by
3391
+ * closure identity (the warning registry's story). Throws Node's
3392
+ * ERR_INVALID_ARG_TYPE on a non-function. */
3393
+ void scr_process_on_unhandled_rejection(ScrDyn *fn, bool once);
3394
+ void scr_process_off_unhandled_rejection(ScrDyn *fn);
3395
+ /* process.on/once/off('rejectionHandled', fn): the sibling registry.
3396
+ * Under the loop-exhaustion model the event's ONE firing window is a
3397
+ * handler attached DURING an unhandledRejection listener (a rejection
3398
+ * handled any earlier never enters the report at all — the same
3399
+ * documented divergence); dispatch is synchronous at the attach, with
3400
+ * the promise, Node's payload. */
3401
+ void scr_process_on_rejection_handled(ScrDyn *fn, bool once);
3402
+ void scr_process_off_rejection_handled(ScrDyn *fn);
3387
3403
  /* The loop-end delivery hook the registration above installs
3388
3404
  * (scr_async_dyn.c → scr_report_unhandled_rejections; NULL = default
3389
3405
  * report). Runtime-internal. */
3390
3406
  extern bool (*scr_urj_deliver_fn)(ScrPromise *p);
3407
+ /* The late-handled hook (scr_async_dyn.c installs it when a
3408
+ * 'rejectionHandled' listener registers): scr_async.c calls it when a
3409
+ * promise the report already delivered as unhandled gains a handler.
3410
+ * Runtime-internal. */
3411
+ extern void (*scr_rjh_notify_fn)(ScrPromise *p);
3412
+ /* The attach-time handled mark (a dyn then/catch carrying a rejection
3413
+ * handler): marks an already-rejected source observed — Node's attach
3414
+ * moment — firing the late-handled hook when the report already
3415
+ * delivered it. Runtime-internal. */
3416
+ void scr_promise_mark_handled(ScrPromise *p);
3391
3417
  /* A rejected promise's reason as a dyn value (identity-preserving for
3392
3418
  * dyn payloads and %Error instances). +1. */
3393
3419
  ScrDyn *scr_promise_reason_dyn(const ScrPromise *p);
@@ -4819,6 +4845,22 @@ void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
4819
4845
  void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
4820
4846
  void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
4821
4847
  void scr_net_sock_destroy(ScrNetSocket *s);
4848
+ /* Flow control (pause/resume — see the struct's flag comments), the
4849
+ * FIN-flushed destroy (destroySoon), TCP_NODELAY, the deferred write/
4850
+ * finish callbacks (write(chunk, cb) / end(cb) — sweep-fired), and the
4851
+ * counters/flags the compat surface reads. */
4852
+ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */
4853
+ ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */
4854
+ ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */
4855
+ void scr_net_sock_destroy_soon(ScrNetSocket *s);
4856
+ void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/);
4857
+ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/);
4858
+ double scr_net_sock_bytes_written(ScrNetSocket *s);
4859
+ bool scr_net_sock_readable(ScrNetSocket *s);
4860
+ /* The deferred dial (the http agent's maxSockets queue): the socket
4861
+ * registers "connecting" and buffers writes; dial_start runs the dial. */
4862
+ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/); /* +1 */
4863
+ void scr_net_sock_dial_start(ScrNetSocket *s);
4822
4864
  void scr_net_sock_set_timeout(ScrNetSocket *s, double ms);
4823
4865
  void scr_net_sock_on_timeout(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
4824
4866
  ScrStr *scr_net_sock_remote_address(ScrNetSocket *s); /* +1 or NULL (undefined arm) */
@@ -4845,6 +4887,7 @@ void scr_net_sock_unshift_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
4845
4887
  void scr_net_server_emit_connection(ScrNetServer *srv, ScrNetSocket *sock /*borrowed*/);
4846
4888
  void scr_net_data_thunk0(ScrClosure *cb, ScrBytes *chunk);
4847
4889
  void scr_net_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
4890
+ void scr_net_data_thunk_str(ScrClosure *cb, ScrBytes *chunk);
4848
4891
  /* The dynCheck-adapted listener's flavor: boxes the chunk as a
4849
4892
  * Buffer-flavored dyn (toString decodes utf8, Node's 'data' payload). */
4850
4893
  void scr_net_data_thunk_dyn(ScrClosure *cb, ScrBytes *chunk);
@@ -5025,6 +5068,29 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move
5025
5068
  long scr_secure_ctx_live_count(void);
5026
5069
  #endif
5027
5070
 
5071
+ /* ── node:tls, the CA-store introspection slice (scr_tls_ca.c — its own
5072
+ * unit and link gate; plain PEM-block bookkeeping, NO mbedTLS, so a
5073
+ * getCACertificates-only binary never builds the archive; cc.ts also
5074
+ * compiles it whenever scr_tls.c does). The HOST bundle (the
5075
+ * /etc/ssl/cert.pem probe order scr_tls.c documents) stands in for both
5076
+ * Node's compiled-in Mozilla roots ('bundled', rootCertificates) and the
5077
+ * platform store ('system') — the established SEMANTICS divergence,
5078
+ * extended to introspection; 'extra' reads NODE_EXTRA_CA_CERTS. Arrays
5079
+ * are cached per type (+1 retained answers each call — Node's own
5080
+ * caching, and the identity the suite pins with strictEqual). */
5081
+ ScrArr *scr_tls_ca_get(ScrStr *type); /* +1; throws ERR_INVALID_ARG_VALUE on unknown types */
5082
+ ScrArr *scr_tls_ca_root(void); /* +1; === getCACertificates("bundled") */
5083
+ /* Replaces the 'default' set: entries filter to their PEM certificate
5084
+ * blocks (deduped byte-exactly — Node dedupes by X509 identity); a
5085
+ * non-empty array yielding no blocks throws Node's
5086
+ * ERR_CRYPTO_OPERATION_FAILED and leaves the set unchanged. Borrows. */
5087
+ void scr_tls_ca_set_default(ScrArr *certs);
5088
+ /* scr_tls.c's anchor consult: true iff setDefaultCACertificates ran;
5089
+ * *pem/*len then carry the concatenated NUL-terminated blocks (len 0 =
5090
+ * the empty set — verification fails, Node's own consequence), and *gen
5091
+ * a counter that bumps per set so the parsed chain re-parses on change. */
5092
+ bool scr_tls_ca_default_override(const char **pem, size_t *len, uint64_t *gen);
5093
+
5028
5094
  /* ── node:http, the server slice (scr_http.c — compiled only when the
5029
5095
  * program uses it; rides scr_net.c wholesale, design note atop the
5030
5096
  * file). http.createServer returns an ORDINARY ScrNetServer (listen/
@@ -5180,6 +5246,23 @@ void scr_http_upgrade_thunk3(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock
5180
5246
  double scr_http_req_status(ScrHttpReq *r); /* < 0 = the undefined arm (server request) */
5181
5247
  ScrNetSocket *scr_http_req_socket(ScrHttpReq *r); /* +1 */
5182
5248
  void scr_http_req_resume(ScrHttpReq *r);
5249
+ /* pause()/resume() hold and drain 'data'/'end' delivery (the parser keeps
5250
+ * consuming; the drain rides the emit queue); setTimeout delegates to the
5251
+ * socket's idle timer; the flags back req.destroyed/req.readable. */
5252
+ void scr_http_req_pause(ScrHttpReq *r);
5253
+ void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/);
5254
+ bool scr_http_req_destroyed_flag(ScrHttpReq *r);
5255
+ bool scr_http_req_readable(ScrHttpReq *r);
5256
+ /* flushHeaders/cork/uncork/writableCorked, the res.req backref, the
5257
+ * socket-delegated setTimeout, and write(chunk, cb)'s deferred callback. */
5258
+ void scr_http_res_flush_headers(ScrHttpRes *r);
5259
+ void scr_http_res_cork(ScrHttpRes *r);
5260
+ void scr_http_res_uncork(ScrHttpRes *r);
5261
+ double scr_http_res_writable_corked(ScrHttpRes *r);
5262
+ bool scr_http_res_destroyed_flag(ScrHttpRes *r);
5263
+ void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed, nullable*/);
5264
+ void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/);
5265
+ void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/);
5183
5266
  /* req.setEncoding(enc) — the socket twin's contract; may throw. */
5184
5267
  void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/);
5185
5268
  void scr_http_req_destroy(ScrHttpReq *r);
@@ -5244,6 +5327,30 @@ ScrHttpClientReq *scr_http_request_ex(ScrStr *host /*borrowed*/, double port,
5244
5327
  ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
5245
5328
  bool auto_end, ScrClosure *cb /*moves, nullable*/,
5246
5329
  ScrHttpRespFn fn); /* +1 */
5330
+ /* ── the http Agent (new http.Agent(opts) — option surface, getName, and
5331
+ * the maxSockets queue over one-dial-per-request connections; keep-alive
5332
+ * POOLING is not modeled: keepAlive: true fences at construction).
5333
+ * agent_new answers the SCR_DYNH_HTTP_AGENT handle (+1 dyn) or throws.
5334
+ * max_sockets/max_free arrive < 0 for "unset" (Infinity / 256);
5335
+ * timeout_ms < 0 = no idle timer. request_agent_ex threads the agent dyn
5336
+ * (undefined/null = the default path; false = one-shot with Connection:
5337
+ * close; an Agent handle = queue accounting); port < 0 means "no port
5338
+ * option" — the agent's (settable) defaultPort, then the scheme's. */
5339
+ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
5340
+ double max_sockets, double max_free, double timeout_ms,
5341
+ double port /* < 0 = unset: the option-merge default */); /* +1 */
5342
+ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
5343
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
5344
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
5345
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
5346
+ ScrClosure *cb /*moves, nullable*/,
5347
+ ScrHttpRespFn fn, int default_port,
5348
+ void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx); /* +1 */
5349
+ ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
5350
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
5351
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
5352
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
5353
+ ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
5247
5354
  #ifdef SCR_RC_AUDIT
5248
5355
  long scr_http_live_count(void);
5249
5356
  #endif
@@ -5288,6 +5395,19 @@ ScrNetServer *scr_http2_create_server_req(ScrClosure *handler /*moves*/, ScrHttp
5288
5395
  * the same session machinery behind an mbedTLS handshake whose ALPN
5289
5396
  * advertises h2 alone — protocol-identical after establishment. */
5290
5397
  ScrNetServer *scr_http2_create_secure_server(const char *cert, size_t cert_len, const char *key, size_t key_len); /* +1 */
5398
+ /* createSecureServer(options, handler): the eager COMPAT handler as the
5399
+ * first 'request' listener over the ALPN=h2 server (the createServerReq
5400
+ * route, secured). */
5401
+ ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 */
5402
+ /* createSecureServer with a RUNTIME options record (the divergence-66
5403
+ * stance): allowHTTP1 picks the flavor at runtime, cert/key ride the
5404
+ * shared TLS server walk (out-of-bounds members throw the catchable
5405
+ * fence), h2 session-tuning keys drop like the literal walk ignores
5406
+ * them. +1, or NULL with the exception pending. */
5407
+ ScrNetServer *scr_http2_create_secure_server_dyn(const struct ScrDyn *opts /*borrowed*/, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn);
5408
+ /* The TLS server-options walk (scr_tls.c) — runtime-internal, shared
5409
+ * with scr_http2.c's createSecureServerDyn. */
5410
+ bool scr_tls_srv_opts_walk(const struct ScrDyn *opts, const char *api, ScrBytes **cert_out, ScrBytes **key_out);
5291
5411
  void scr_http2_server_on_stream(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH2StreamFn fn, bool once);
5292
5412
  void scr_http2_server_on_session(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH2SessionFn fn, bool once);
5293
5413
 
@@ -5394,6 +5514,14 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
5394
5514
  bool auto_end, bool reject_unauthorized,
5395
5515
  const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
5396
5516
  ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
5517
+ /* The agent-threaded twin (the scr_http_request_agent story over TLS). */
5518
+ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port,
5519
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
5520
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
5521
+ bool auto_end, bool reject_unauthorized,
5522
+ const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
5523
+ const struct ScrDyn *agent /*borrowed*/,
5524
+ ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
5397
5525
  /* The fetch unit's https leg (defined in scr_tls.c): a client transport
5398
5526
  * context (SNI/verify against the URL hostname — the DIALED address may
5399
5527
  * be a resolved IP) plus the wrap hook scr_http_request_ex installs on
package/src/scr_tls.c CHANGED
@@ -350,7 +350,30 @@ typedef struct ScrTlsCli {
350
350
  static mbedtls_x509_crt scr_tls_system_roots;
351
351
  static bool scr_tls_system_roots_loaded = false;
352
352
 
353
+ /* setDefaultCACertificates' replacement anchors (scr_tls_ca.c — always
354
+ * compiled alongside this unit): parsed lazily per generation, so each
355
+ * set re-parses once and every later dial reuses the chain. An EMPTY
356
+ * replacement keeps an initialized, certificate-free chain — every
357
+ * verification fails, Node's own consequence of trusting nothing. */
358
+ static mbedtls_x509_crt scr_tls_override_roots;
359
+ static uint64_t scr_tls_override_parsed_gen = 0;
360
+
353
361
  static mbedtls_x509_crt *scr_tls_system_ca(void) {
362
+ const char *pem = NULL;
363
+ size_t pem_len = 0;
364
+ uint64_t gen = 0;
365
+ if (scr_tls_ca_default_override(&pem, &pem_len, &gen)) {
366
+ if (gen != scr_tls_override_parsed_gen) {
367
+ if (scr_tls_override_parsed_gen != 0) mbedtls_x509_crt_free(&scr_tls_override_roots);
368
+ mbedtls_x509_crt_init(&scr_tls_override_roots);
369
+ /* +1 for the NUL — mbedTLS's PEM mode wants it counted. A parse
370
+ * failure inside one block leaves the chain with the blocks that
371
+ * did parse (the scr_tls_ca.c divergence note). */
372
+ if (pem_len > 0) (void)mbedtls_x509_crt_parse(&scr_tls_override_roots, (const unsigned char *)pem, pem_len + 1);
373
+ scr_tls_override_parsed_gen = gen;
374
+ }
375
+ return &scr_tls_override_roots;
376
+ }
354
377
  if (!scr_tls_system_roots_loaded) {
355
378
  scr_tls_system_roots_loaded = true;
356
379
  mbedtls_x509_crt_init(&scr_tls_system_roots);
@@ -983,6 +1006,18 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
983
1006
  fn, 443, &scr_tls_cli_wrap, cli);
984
1007
  }
985
1008
 
1009
+ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port,
1010
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
1011
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
1012
+ bool auto_end, bool reject_unauthorized,
1013
+ const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
1014
+ const ScrDyn *agent /*borrowed*/,
1015
+ ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
1016
+ ScrTlsCli *cli = scr_tls_cli_new(host, reject_unauthorized, ca, ca_len);
1017
+ return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
1018
+ agent, cb, fn, 443, &scr_tls_cli_wrap, cli);
1019
+ }
1020
+
986
1021
  /* ── RUNTIME options records (the divergence-66 stance) ────────────────
987
1022
  * A non-literal options value — the checked-dynamic JS lane's dyn record
988
1023
  * — reads its members at RUNTIME. Members the literal path lowers take
@@ -1219,9 +1254,11 @@ static bool scr_tls_opts_validate(const ScrDyn *opts) {
1219
1254
  }
1220
1255
 
1221
1256
  /* The server-options walk. Fills the cert/key out-params (+1 each) from a
1222
- * dyn options record; false = exception pending (partial results released). */
1223
- static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
1224
- ScrBytes **key_out) {
1257
+ * dyn options record; false = exception pending (partial results released).
1258
+ * Exported runtime-internally: scr_http2.c's createSecureServerDyn rides
1259
+ * the same walk (scr_http2.c always links alongside this unit). */
1260
+ bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
1261
+ ScrBytes **key_out) {
1225
1262
  *cert_out = NULL;
1226
1263
  *key_out = NULL;
1227
1264
  if (opts == NULL || opts->kind != SCR_DYN_OBJ) {
@@ -0,0 +1,288 @@
1
+ /* node:tls — the CA-store introspection slice: getCACertificates,
2
+ * rootCertificates, and setDefaultCACertificates (scr_runtime.h has the
3
+ * contracts). Its own link gate ("tlsca." libCalls), deliberately free
4
+ * of mbedTLS: everything here is PEM-BLOCK bookkeeping over the host
5
+ * bundle, so a program that only inspects the CA store keeps the lean
6
+ * link line. scr_tls.c (when it links — cc.ts compiles this unit
7
+ * alongside it) consults scr_tls_ca_default_override for its client
8
+ * trust anchors, which is what makes setDefaultCACertificates LIVE
9
+ * semantics: the next https/tls dial verifies against the replaced set.
10
+ *
11
+ * Divergences, documented: the host bundle stands in for Node's
12
+ * compiled-in Mozilla roots ('bundled' and rootCertificates) AND for the
13
+ * platform store ('system') — the same /etc/ssl/cert.pem stance
14
+ * scr_tls.c's anchors established; and set-time validation is PEM-BLOCK
15
+ * shaped (a well-formed block with corrupt DER inside is kept here where
16
+ * Node's X509 parse would drop it — such a cert then fails verification
17
+ * instead, the same observable outcome one step later). Deduplication is
18
+ * byte-exact over the extracted block where Node compares parsed X509
19
+ * identities; re-encoded duplicates of one certificate stay distinct
20
+ * entries, which no equality the tests observe can distinguish without a
21
+ * parser. */
22
+ #include "scr_runtime.h"
23
+
24
+ #include <stdio.h>
25
+ #include <stdlib.h>
26
+ #include <string.h>
27
+
28
+ /* The per-type caches: +1 held here for the process lifetime (an atexit
29
+ * teardown releases them so the RC audit stays clean). 'default' also
30
+ * keeps the concatenated PEM buffer scr_tls.c parses into anchors. */
31
+ static ScrArr *scr_ca_bundled = NULL;
32
+ static ScrArr *scr_ca_system = NULL;
33
+ static ScrArr *scr_ca_extra = NULL;
34
+ static ScrArr *scr_ca_default = NULL;
35
+ static char *scr_ca_override_buf = NULL;
36
+ static size_t scr_ca_override_len = 0;
37
+ static uint64_t scr_ca_override_gen = 0; /* 0 = never set */
38
+
39
+ static void scr_ca_oom(void) {
40
+ fputs("scriptc: out of memory\n", stderr);
41
+ abort();
42
+ }
43
+
44
+ static void scr_ca_teardown(void) {
45
+ if (scr_ca_bundled != NULL) scr_arr_release(scr_ca_bundled);
46
+ if (scr_ca_system != NULL) scr_arr_release(scr_ca_system);
47
+ if (scr_ca_extra != NULL) scr_arr_release(scr_ca_extra);
48
+ if (scr_ca_default != NULL) scr_arr_release(scr_ca_default);
49
+ scr_ca_bundled = scr_ca_system = scr_ca_extra = scr_ca_default = NULL;
50
+ free(scr_ca_override_buf);
51
+ scr_ca_override_buf = NULL;
52
+ }
53
+
54
+ static bool scr_ca_teardown_armed = false;
55
+
56
+ static void scr_ca_arm_teardown(void) {
57
+ if (!scr_ca_teardown_armed) {
58
+ scr_ca_teardown_armed = true;
59
+ atexit(scr_ca_teardown);
60
+ }
61
+ }
62
+
63
+ /* Portable bounded substring search (memmem is GNU/BSD-only; win32 has
64
+ * neither). Needles here are short constant markers — the naive scan is
65
+ * plenty. */
66
+ static const char *scr_ca_find(const char *hay, size_t hay_len, const char *needle, size_t needle_len) {
67
+ if (needle_len == 0 || hay_len < needle_len) return NULL;
68
+ for (size_t i = 0; i + needle_len <= hay_len; i++) {
69
+ if (hay[i] == needle[0] && memcmp(hay + i, needle, needle_len) == 0) return hay + i;
70
+ }
71
+ return NULL;
72
+ }
73
+
74
+ /* Every "-----BEGIN CERTIFICATE-----" ... "-----END CERTIFICATE-----"
75
+ * block of `text` (other PEM kinds — keys, CRLs — skip, like Node's CA
76
+ * loaders), pushed onto `arr` as one string each, END line included.
77
+ * Returns the number of blocks found. */
78
+ static size_t scr_ca_push_pem_blocks(ScrArr *arr, const char *text, size_t len) {
79
+ static const char BEGIN[] = "-----BEGIN CERTIFICATE-----";
80
+ static const char END[] = "-----END CERTIFICATE-----";
81
+ size_t found = 0;
82
+ const char *p = text;
83
+ const char *limit = text + len;
84
+ while (p < limit) {
85
+ const char *b = scr_ca_find(p, (size_t)(limit - p), BEGIN, sizeof BEGIN - 1);
86
+ if (b == NULL) break;
87
+ const char *e = scr_ca_find(b, (size_t)(limit - b), END, sizeof END - 1);
88
+ if (e == NULL) break;
89
+ const char *stop = e + (sizeof END - 1);
90
+ /* Node's entries carry the block's trailing newline when the source
91
+ * had one — keep it so round-trips through writeFileSync + parse
92
+ * stay concatenation-safe. */
93
+ if (stop < limit && *stop == '\r') stop++;
94
+ if (stop < limit && *stop == '\n') stop++;
95
+ scr_arr_push_ref(arr, scr_str_new(b, (size_t)(stop - b)));
96
+ found++;
97
+ p = stop;
98
+ }
99
+ return found;
100
+ }
101
+
102
+ /* One whole file, malloc'd (NULL on any failure — absent bundles answer
103
+ * empty stores, the same silence Node has on hosts without the file). */
104
+ static char *scr_ca_read_file(const char *path, size_t *out_len) {
105
+ FILE *f = fopen(path, "rb");
106
+ if (f == NULL) return NULL;
107
+ if (fseek(f, 0, SEEK_END) != 0) {
108
+ fclose(f);
109
+ return NULL;
110
+ }
111
+ long sz = ftell(f);
112
+ if (sz < 0) {
113
+ fclose(f);
114
+ return NULL;
115
+ }
116
+ rewind(f);
117
+ char *buf = malloc((size_t)sz + 1);
118
+ if (buf == NULL) scr_ca_oom();
119
+ size_t got = fread(buf, 1, (size_t)sz, f);
120
+ fclose(f);
121
+ buf[got] = '\0';
122
+ *out_len = got;
123
+ return buf;
124
+ }
125
+
126
+ /* The host bundle, probed in scr_tls.c's documented order. */
127
+ static ScrArr *scr_ca_load_host_bundle(void) {
128
+ ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 128);
129
+ static const char *const bundles[] = {
130
+ "/etc/ssl/cert.pem", /* macOS, Alpine */
131
+ "/etc/ssl/certs/ca-certificates.crt", /* Debian/Ubuntu */
132
+ "/etc/pki/tls/certs/ca-bundle.crt", /* Fedora/RHEL */
133
+ "/etc/ssl/ca-bundle.pem", /* openSUSE */
134
+ };
135
+ for (size_t i = 0; i < sizeof bundles / sizeof bundles[0]; i++) {
136
+ size_t len = 0;
137
+ char *text = scr_ca_read_file(bundles[i], &len);
138
+ if (text == NULL) continue;
139
+ scr_ca_push_pem_blocks(arr, text, len);
140
+ free(text);
141
+ break; /* first bundle wins, like the anchor probe */
142
+ }
143
+ return arr;
144
+ }
145
+
146
+ static ScrArr *scr_ca_bundled_arr(void) {
147
+ if (scr_ca_bundled == NULL) {
148
+ scr_ca_arm_teardown();
149
+ scr_ca_bundled = scr_ca_load_host_bundle();
150
+ }
151
+ return scr_ca_bundled;
152
+ }
153
+
154
+ static ScrArr *scr_ca_system_arr(void) {
155
+ if (scr_ca_system == NULL) {
156
+ scr_ca_arm_teardown();
157
+ scr_ca_system = scr_ca_load_host_bundle();
158
+ }
159
+ return scr_ca_system;
160
+ }
161
+
162
+ static ScrArr *scr_ca_extra_arr(void) {
163
+ if (scr_ca_extra == NULL) {
164
+ scr_ca_arm_teardown();
165
+ 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
+ }
174
+ }
175
+ }
176
+ return scr_ca_extra;
177
+ }
178
+
179
+ /* 'default' with no override: bundled ∪ extra (Node's own composition
180
+ * when no --use-*-ca flag redirects it). */
181
+ static ScrArr *scr_ca_default_arr(void) {
182
+ if (scr_ca_default == NULL) {
183
+ scr_ca_arm_teardown();
184
+ ScrArr *bundled = scr_ca_bundled_arr();
185
+ ScrArr *extra = scr_ca_extra_arr();
186
+ size_t nb = (size_t)scr_arr_len(bundled);
187
+ size_t ne = (size_t)scr_arr_len(extra);
188
+ scr_ca_default = scr_arr_new(SCR_ELEM_STR, nb + ne > 0 ? nb + ne : 1);
189
+ for (size_t i = 0; i < nb; i++) scr_arr_push_ref(scr_ca_default, scr_arr_get_ref(bundled, (double)i));
190
+ for (size_t i = 0; i < ne; i++) scr_arr_push_ref(scr_ca_default, scr_arr_get_ref(extra, (double)i));
191
+ }
192
+ return scr_ca_default;
193
+ }
194
+
195
+ ScrArr *scr_tls_ca_get(ScrStr *type) {
196
+ ScrArr *arr = NULL;
197
+ if (strcmp(type->data, "default") == 0) arr = scr_ca_default_arr();
198
+ else if (strcmp(type->data, "bundled") == 0) arr = scr_ca_bundled_arr();
199
+ else if (strcmp(type->data, "system") == 0) arr = scr_ca_system_arr();
200
+ else if (strcmp(type->data, "extra") == 0) arr = scr_ca_extra_arr();
201
+ if (arr == NULL) {
202
+ /* Node's ERR_INVALID_ARG_VALUE TypeError, message-exact. */
203
+ char msg[256];
204
+ int n = snprintf(msg, sizeof msg, "The argument 'type' is invalid. Received '%s'", type->data);
205
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, n < 0 ? 0 : (size_t)n, "ERR_INVALID_ARG_VALUE");
206
+ return NULL;
207
+ }
208
+ return scr_arr_retain(arr);
209
+ }
210
+
211
+ ScrArr *scr_tls_ca_root(void) { return scr_arr_retain(scr_ca_bundled_arr()); }
212
+
213
+ void scr_tls_ca_set_default(ScrArr *certs) {
214
+ size_t n = (size_t)scr_arr_len(certs);
215
+ /* Extract + dedupe first — a throw must leave the current set intact
216
+ * (Node's own error contract, pinned by the suite's error test). */
217
+ ScrArr *next = scr_arr_new(SCR_ELEM_STR, n > 0 ? n : 1);
218
+ size_t blocks = 0;
219
+ for (size_t i = 0; i < n; i++) {
220
+ ScrStr *entry = scr_arr_get_ref(certs, (double)i); /* +1 */
221
+ size_t before = (size_t)scr_arr_len(next);
222
+ blocks += scr_ca_push_pem_blocks(next, entry->data, entry->len);
223
+ /* Dedupe the freshly pushed blocks against everything before them. */
224
+ size_t after = (size_t)scr_arr_len(next);
225
+ for (size_t j = after; j > before; j--) {
226
+ ScrStr *cand = scr_arr_get_ref(next, (double)(j - 1)); /* +1 */
227
+ bool dup = false;
228
+ for (size_t k = 0; k + 1 < j && !dup; k++) {
229
+ ScrStr *prev = scr_arr_get_ref(next, (double)k); /* +1 */
230
+ dup = prev->len == cand->len && memcmp(prev->data, cand->data, cand->len) == 0;
231
+ scr_str_release(prev);
232
+ }
233
+ scr_str_release(cand);
234
+ if (dup) {
235
+ /* Drop the duplicate: rebuild without index j-1 (rare path —
236
+ * a simple splice via slice would copy anyway; do it in place). */
237
+ ScrArr *trimmed = scr_arr_new(SCR_ELEM_STR, after);
238
+ for (size_t k = 0; k < (size_t)scr_arr_len(next); k++) {
239
+ if (k == j - 1) continue;
240
+ scr_arr_push_ref(trimmed, scr_arr_get_ref(next, (double)k));
241
+ }
242
+ scr_arr_release(next);
243
+ next = trimmed;
244
+ }
245
+ }
246
+ scr_str_release(entry);
247
+ }
248
+ if (n > 0 && blocks == 0) {
249
+ scr_arr_release(next);
250
+ static const char MSG[] = "No valid certificates found in the provided array";
251
+ scr_throw_error_msg_code(SCR_ERR_ERROR, MSG, sizeof MSG - 1, "ERR_CRYPTO_OPERATION_FAILED");
252
+ return;
253
+ }
254
+ /* Commit: the cached 'default' array and the concatenated anchor
255
+ * buffer scr_tls.c parses (NUL-terminated for mbedTLS's PEM mode). */
256
+ scr_ca_arm_teardown();
257
+ if (scr_ca_default != NULL) scr_arr_release(scr_ca_default);
258
+ scr_ca_default = next;
259
+ size_t total = 0;
260
+ size_t count = (size_t)scr_arr_len(next);
261
+ for (size_t i = 0; i < count; i++) {
262
+ ScrStr *s = scr_arr_get_ref(next, (double)i);
263
+ total += s->len;
264
+ scr_str_release(s);
265
+ }
266
+ char *buf = malloc(total + 1);
267
+ if (buf == NULL) scr_ca_oom();
268
+ size_t off = 0;
269
+ for (size_t i = 0; i < count; i++) {
270
+ ScrStr *s = scr_arr_get_ref(next, (double)i);
271
+ memcpy(buf + off, s->data, s->len);
272
+ off += s->len;
273
+ scr_str_release(s);
274
+ }
275
+ buf[off] = '\0';
276
+ free(scr_ca_override_buf);
277
+ scr_ca_override_buf = buf;
278
+ scr_ca_override_len = off;
279
+ scr_ca_override_gen++;
280
+ }
281
+
282
+ bool scr_tls_ca_default_override(const char **pem, size_t *len, uint64_t *gen) {
283
+ if (scr_ca_override_gen == 0) return false;
284
+ *pem = scr_ca_override_buf;
285
+ *len = scr_ca_override_len;
286
+ *gen = scr_ca_override_gen;
287
+ return true;
288
+ }