@amritk/nish-x86_64-linux 0.24.0 → 0.25.0
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/bin/nish +0 -0
- package/docs/AI.md +24 -4
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/host.d.ts +8 -0
- package/runtime/core/host.mjs +50 -2
- package/runtime/nish.d.ts +100 -0
- package/runtime/nish.h +34 -0
- package/runtime/nish.mjs +21 -0
- package/runtime/node.mjs +57 -6
- package/runtime/runtime-net.c +44 -3
- package/runtime/runtime-os.c +115 -12
- package/scripts/build.sh +13 -3
- package/std/README.md +25 -8
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +132 -20
- package/std/compress/deflate.ts +638 -248
- package/std/compress/gzip.ts +20 -6
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +7 -2
- package/std/net/http1.ts +9 -9
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- package/std/yaml-write.ts +415 -0
package/runtime/runtime-net.c
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* errno to it: where `signalFd` and `readSignal` answer a plain -1, a call
|
|
18
18
|
* here answers the failure's errno, negated. The codes a loop branches on are Linux's numbers on every platform (-11
|
|
19
19
|
* would block, -95 unsupported, -32 the peer is gone, -104 reset, -98 the
|
|
20
|
-
* address is in use, -111 refused, -110 timed out, -22 a bad argument
|
|
20
|
+
* address is in use, -111 refused, -110 timed out, -22 a bad argument, -88
|
|
21
|
+
* not a socket), so
|
|
21
22
|
* `nish_net_err` translates Darwin's; any other failure is the host's own
|
|
22
23
|
* `-errno`. Nothing here allocates: the addresses a call reads or writes are
|
|
23
24
|
* the caller's `u8[]`.
|
|
@@ -29,13 +30,14 @@
|
|
|
29
30
|
* (`EAFNOSUPPORT`, as in many containers) `::` falls back to IPv4's
|
|
30
31
|
* `0.0.0.0`.
|
|
31
32
|
*
|
|
32
|
-
* Linux and Darwin differ in
|
|
33
|
+
* Linux and Darwin differ in six places: `SOCK_NONBLOCK | SOCK_CLOEXEC` and
|
|
33
34
|
* `accept4` against a `socket` or `accept` followed by `fcntl`, `MSG_NOSIGNAL`
|
|
34
35
|
* against the `SO_NOSIGPIPE` socket option (a write to a gone peer is -32 on
|
|
35
36
|
* both, never SIGPIPE), the errno numbers, UDP's segmentation offload
|
|
36
37
|
* (`UDP_SEGMENT`, `UDP_GRO`) and ECN marks, which Linux has and for which
|
|
37
38
|
* Darwin answers -95, and the readiness loop, which is epoll on Linux and
|
|
38
|
-
* kqueue on Darwin
|
|
39
|
+
* kqueue on Darwin, and `TCP_QUICKACK`, which only Linux has (-95 on
|
|
40
|
+
* Darwin). Only the sending side and the GRO bind say -95 there: a
|
|
39
41
|
* receive leaves `meta` at 0, 0, and a `meta[1]` of 0 is also what a real
|
|
40
42
|
* Not-ECT datagram reads as, so a program learns that Darwin has no ECN from
|
|
41
43
|
* the send, never from the receive. The Darwin branch is compiled by CI's Darwin bootstrap
|
|
@@ -100,6 +102,7 @@ static int32_t nish_net_err(int e) {
|
|
|
100
102
|
if (e == EADDRINUSE) return -98;
|
|
101
103
|
if (e == ECONNREFUSED) return -111;
|
|
102
104
|
if (e == ETIMEDOUT) return -110;
|
|
105
|
+
if (e == ENOTSOCK) return -88;
|
|
103
106
|
#endif
|
|
104
107
|
return -e;
|
|
105
108
|
}
|
|
@@ -383,6 +386,44 @@ int32_t nish_net_shutdown(int32_t fd, int32_t how) {
|
|
|
383
386
|
promises on Linux, so a failure is information rather than a retry. */
|
|
384
387
|
int32_t nish_net_close(int32_t fd) { return close(fd) != 0 ? nish_net_fail() : 0; }
|
|
385
388
|
|
|
389
|
+
/* `netSetOption(fd, option, value)`: one option from a whitelist, named by
|
|
390
|
+
the runtime's own id rather than a level and an option number, so a
|
|
391
|
+
program can set these five and nothing else. `value` is 0 or 1 for the
|
|
392
|
+
flags and a byte count for the buffers, passed to the kernel as it is
|
|
393
|
+
(Linux stores twice a buffer size, to cover its bookkeeping). Linux does
|
|
394
|
+
not keep quick-ACK mode: the kernel leaves it of its own accord, so a
|
|
395
|
+
server that wants it on or off sets it again after each response. */
|
|
396
|
+
int32_t nish_net_set_option(int32_t fd, int32_t option, int32_t value) {
|
|
397
|
+
int level = SOL_SOCKET;
|
|
398
|
+
int name;
|
|
399
|
+
switch (option) {
|
|
400
|
+
case 1:
|
|
401
|
+
level = IPPROTO_TCP;
|
|
402
|
+
name = TCP_NODELAY;
|
|
403
|
+
break;
|
|
404
|
+
case 2:
|
|
405
|
+
#if defined(__linux__)
|
|
406
|
+
level = IPPROTO_TCP;
|
|
407
|
+
name = TCP_QUICKACK;
|
|
408
|
+
break;
|
|
409
|
+
#else
|
|
410
|
+
return -95;
|
|
411
|
+
#endif
|
|
412
|
+
case 3:
|
|
413
|
+
name = SO_KEEPALIVE;
|
|
414
|
+
break;
|
|
415
|
+
case 4:
|
|
416
|
+
name = SO_SNDBUF;
|
|
417
|
+
break;
|
|
418
|
+
case 5:
|
|
419
|
+
name = SO_RCVBUF;
|
|
420
|
+
break;
|
|
421
|
+
default:
|
|
422
|
+
return -22;
|
|
423
|
+
}
|
|
424
|
+
return setsockopt(fd, level, name, &value, sizeof value) != 0 ? nish_net_fail() : 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
386
427
|
/* `udpBind(host, port, flags)`: a datagram socket, non-blocking and
|
|
387
428
|
close-on-exec, bound like `tcpListen`'s, with `SO_REUSEPORT` for flag 1
|
|
388
429
|
and `UDP_GRO` for flag 2; any other bit is -22. */
|
package/runtime/runtime-os.c
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
/* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
|
|
52
52
|
#include <signal.h>
|
|
53
53
|
#include <spawn.h>
|
|
54
|
+
#include <sys/resource.h>
|
|
54
55
|
#include <sys/wait.h>
|
|
55
56
|
extern char **environ;
|
|
56
57
|
#endif
|
|
@@ -178,23 +179,89 @@ nish_str *nish_read_file(const nish_str *path) {
|
|
|
178
179
|
|
|
179
180
|
`O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
|
|
180
181
|
as the read retries it (RT-13): a write cut short is finished, and one that
|
|
181
|
-
fails is `cannot write`, never a silently short file.
|
|
182
|
+
fails is `cannot write`, never a silently short file.
|
|
183
|
+
|
|
184
|
+
The loop is `nish_put_all`, which `writeFileBytesSync` and `writeBytes`
|
|
185
|
+
share: all `n` bytes of `p` to `fd`, answering 0 or the negative errno of
|
|
186
|
+
the write that failed (-EIO for one that wrote nothing). */
|
|
187
|
+
static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
|
|
188
|
+
for (uint64_t done = 0; done < n;) {
|
|
189
|
+
ssize_t k = write(fd, p + done, n - done);
|
|
190
|
+
if (k > 0) done += k;
|
|
191
|
+
else if (k == 0) return -EIO;
|
|
192
|
+
else if (errno != EINTR) return -errno;
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
182
196
|
static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
|
|
183
197
|
int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
|
|
184
|
-
if (fd < 0) nish_io_fail("write ", path);
|
|
185
|
-
for (uint64_t done = 0; done < data->len;) {
|
|
186
|
-
ssize_t n = write(fd, data->data + done, data->len - done);
|
|
187
|
-
if (n > 0) {
|
|
188
|
-
done += n;
|
|
189
|
-
} else if (n == 0 || errno != EINTR) {
|
|
190
|
-
nish_io_fail("write ", path);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
198
|
+
if (fd < 0 || nish_put_all(fd, data->data, data->len) < 0) nish_io_fail("write ", path);
|
|
193
199
|
close(fd);
|
|
194
200
|
}
|
|
195
201
|
void nish_write_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_TRUNC); }
|
|
196
202
|
void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_APPEND); }
|
|
197
203
|
|
|
204
|
+
/* ---- Replacing and removing files: `renameSync`, `unlinkSync` and
|
|
205
|
+
`writeFileBytesSync`. Unlike the writers above, none exits: each answers 0
|
|
206
|
+
(or the bytes written) on success and a negative errno otherwise, so a
|
|
207
|
+
program that writes a file beside its target and renames it over the
|
|
208
|
+
target owns its own diagnostic. A path holding a NUL answers -EINVAL before
|
|
209
|
+
any system call, rather than reaching the file its prefix names
|
|
210
|
+
(docs/security/runtime.md, RT-3). */
|
|
211
|
+
|
|
212
|
+
/* `renameSync(from, to)`: rename(2). On one file system it replaces `to`
|
|
213
|
+
atomically, so a reader sees the old file or the new one and never a part;
|
|
214
|
+
across two it answers -EXDEV and moves nothing, since a copy and a delete
|
|
215
|
+
would not be atomic. A link at `from` is moved as a link, not followed. */
|
|
216
|
+
int32_t nish_rename(const nish_str *from, const nish_str *to) {
|
|
217
|
+
const char *f = nish_cpath(from), *t = nish_cpath(to);
|
|
218
|
+
if (f != from->data || t != to->data) return -EINVAL;
|
|
219
|
+
return rename(f, t) == 0 ? 0 : -errno;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* `unlinkSync(path)`: unlink(2). A directory is refused with whatever the
|
|
223
|
+
kernel answers (-EISDIR on Linux, -EPERM elsewhere) and is never handed to
|
|
224
|
+
rmdir. A link is removed, not the file it names. */
|
|
225
|
+
int32_t nish_unlink(const nish_str *path) {
|
|
226
|
+
const char *p = nish_cpath(path);
|
|
227
|
+
if (p != path->data) return -EINVAL;
|
|
228
|
+
return unlink(p) == 0 ? 0 : -errno;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* `writeFileBytesSync(path, bytes)`: every element of a `u8[]` to the file,
|
|
232
|
+
opened as `writeFileSync` opens it (`O_NOFOLLOW`, `O_CLOEXEC`, `O_TRUNC`,
|
|
233
|
+
0644 before the umask) and written through `nish_put_all`, a short write
|
|
234
|
+
finished and `EINTR` retried. It answers the length written or the negative
|
|
235
|
+
errno of the `open` or `write` that failed (-EIO for a write that wrote
|
|
236
|
+
nothing); a failed write leaves the file truncated to what got out. */
|
|
237
|
+
int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes) {
|
|
238
|
+
const char *p = nish_cpath(path);
|
|
239
|
+
if (p != path->data) return -EINVAL;
|
|
240
|
+
if (bytes->len > NISH_LENGTH_MAX) return -EFBIG;
|
|
241
|
+
int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0644);
|
|
242
|
+
if (fd < 0) return -errno;
|
|
243
|
+
int32_t r = nish_put_all(fd, bytes->data, bytes->len);
|
|
244
|
+
/* A write NFS or a quota refuses can surface only at `close`, and the file
|
|
245
|
+
is then short: an answer of the full length would let the caller rename
|
|
246
|
+
it over the one it replaces. */
|
|
247
|
+
if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
|
|
248
|
+
return r < 0 ? r : (int32_t)bytes->len;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* `writeBytes(bytes, off, len)` (nish:io): the bytes `[off, off + len)` of a
|
|
252
|
+
`u8[]` on stdout, through `nish_put_all`, so a short write is finished and `EINTR` retried. The
|
|
253
|
+
emitter has range-checked `off` and `len` against `bytes->len`
|
|
254
|
+
(`emitNetCall`). It answers `len`, every byte out, or the negative errno of
|
|
255
|
+
the write that failed (`-EIO` for one that wrote nothing), and never exits:
|
|
256
|
+
how much of a failed call reached the descriptor is not said. A pipe whose
|
|
257
|
+
reader has gone raises SIGPIPE, as it does for `write`. */
|
|
258
|
+
int32_t nish_write_bytes(const nish_array *bytes, int64_t off, int64_t len) {
|
|
259
|
+
/* An empty array's data may be NULL, and NULL + 0 is undefined. */
|
|
260
|
+
if (len == 0) return 0;
|
|
261
|
+
int32_t r = nish_put_all(1, bytes->data + off, (uint64_t)len);
|
|
262
|
+
return r < 0 ? r : (int32_t)len;
|
|
263
|
+
}
|
|
264
|
+
|
|
198
265
|
/* ---- Directories and subprocesses (WP14 D4): what a driver needs to create
|
|
199
266
|
its own output directory and shell out for `--link`. Both answer a value
|
|
200
267
|
instead of exiting, as `nish_read_file_or_null` does — there are no
|
|
@@ -477,6 +544,36 @@ int64_t nish_total_memory(void) {
|
|
|
477
544
|
#endif
|
|
478
545
|
}
|
|
479
546
|
|
|
547
|
+
/* `setNice(n)`: this process's nice value set to `n`, 0 or a negative errno.
|
|
548
|
+
The kernel clamps `n` to its range (-20 to 19), so only lowering the value
|
|
549
|
+
below what the process has can fail: -EACCES (Linux, Darwin) or -EPERM
|
|
550
|
+
without the privilege to raise a priority. Linux keeps the value per thread,
|
|
551
|
+
so it is the calling thread's and every thread started after it. */
|
|
552
|
+
int32_t nish_set_nice(int32_t n) {
|
|
553
|
+
#ifdef __wasi__
|
|
554
|
+
(void)n;
|
|
555
|
+
return -ENOSYS;
|
|
556
|
+
#else
|
|
557
|
+
return setpriority(PRIO_PROCESS, 0, n) == 0 ? 0 : -errno;
|
|
558
|
+
#endif
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* `setIdlePriority()`: this process moved to `SCHED_IDLE`, which runs it only
|
|
562
|
+
when nothing else wants the CPU. Linux alone has the policy; elsewhere the
|
|
563
|
+
answer is -EOPNOTSUPP's Linux number, -95, as runtime-net.c answers it. An
|
|
564
|
+
unprivileged process cannot leave the policy again. Like the nice value, the
|
|
565
|
+
policy is the calling thread's. */
|
|
566
|
+
int32_t nish_set_idle_priority(void) {
|
|
567
|
+
#if defined(__linux__)
|
|
568
|
+
struct sched_param param = {0};
|
|
569
|
+
return sched_setscheduler(0, SCHED_IDLE, ¶m) == 0 ? 0 : -errno;
|
|
570
|
+
#elif defined(__wasi__)
|
|
571
|
+
return -ENOSYS;
|
|
572
|
+
#else
|
|
573
|
+
return -95;
|
|
574
|
+
#endif
|
|
575
|
+
}
|
|
576
|
+
|
|
480
577
|
/* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
|
|
481
578
|
|
|
482
579
|
**Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons
|
|
@@ -545,11 +642,17 @@ nish_array *nish_readdir(const nish_str *path) {
|
|
|
545
642
|
backwards and make an elapsed time negative. Only the difference between two
|
|
546
643
|
reads means anything — the origin is arbitrary and is not comparable across
|
|
547
644
|
processes or machines — and an i64 of nanoseconds is 292 years of it. */
|
|
548
|
-
int64_t
|
|
645
|
+
static int64_t nish_clock_nanos(clockid_t clock) {
|
|
549
646
|
struct timespec ts;
|
|
550
|
-
clock_gettime(
|
|
647
|
+
clock_gettime(clock, &ts);
|
|
551
648
|
return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
|
552
649
|
}
|
|
650
|
+
int64_t nish_monotonic_nanos(void) { return nish_clock_nanos(CLOCK_MONOTONIC); }
|
|
651
|
+
|
|
652
|
+
/* `wallClockNanos()`: `CLOCK_REALTIME`, nanoseconds since the Unix epoch, for
|
|
653
|
+
dating an event. It steps when the system time is set, so a duration is
|
|
654
|
+
`monotonicNanos`'s to measure. */
|
|
655
|
+
int64_t nish_wall_clock_nanos(void) { return nish_clock_nanos(CLOCK_REALTIME); }
|
|
553
656
|
|
|
554
657
|
/* A C string copied into the arena, or NULL for NULL: the shape of every
|
|
555
658
|
answer libc hands back by pointer, `getenv`'s here and `realpath`'s below. */
|
package/scripts/build.sh
CHANGED
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
# put in the .ll (line table, variables) reaches the binary. `nish
|
|
63
63
|
# --link -g` passes it through automatically.
|
|
64
64
|
#
|
|
65
|
+
# Build id: every native ELF binary carries an NT_GNU_BUILD_ID note
|
|
66
|
+
# (--build-id=sha1). A -g link hashes other bytes than a plain one, so strip
|
|
67
|
+
# the -g build to pair a binary with its debug file (docs/guide/using-nish/cli.md).
|
|
68
|
+
#
|
|
65
69
|
# CPU level (WP38 S2): `--cpu x86-64-v3` compiles and links every input --
|
|
66
70
|
# the module, the runtime and any other C -- with -march=x86-64-v3
|
|
67
71
|
# -ffp-contract=off, so the code generator may use AVX2, BMI2 and the other
|
|
@@ -281,8 +285,8 @@ case "$(uname -s)" in
|
|
|
281
285
|
# followed by SIGABRT, so the stage1 that linked went on to abort the
|
|
282
286
|
# moment the bootstrap ran it. Measured too, on the run after the one that
|
|
283
287
|
# named the UUID. An unloadable compiler is worse than any reproducibility,
|
|
284
|
-
# so the UUID stays.
|
|
285
|
-
#
|
|
288
|
+
# so the UUID stays. ELF's twin is the build-id note below, which ELF's
|
|
289
|
+
# loader never asks for and which is a hash of the output's bytes anyway.
|
|
286
290
|
#
|
|
287
291
|
# It costs nothing, because the UUID was measured stable across two links
|
|
288
292
|
# of one input to one output path, and differing on a link to another. That
|
|
@@ -294,8 +298,14 @@ case "$(uname -s)" in
|
|
|
294
298
|
strip_flag=(-Wl,-x) ;;
|
|
295
299
|
*)
|
|
296
300
|
# shellcheck disable=SC2054 # -Wl,<flag> is one argument: the comma is the linker's
|
|
297
|
-
gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2
|
|
301
|
+
gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2); strip_flag=(-s)
|
|
298
302
|
elf=(-fno-plt)
|
|
303
|
+
# A GNU build-id note on every native link, debug included, so perf, gdb
|
|
304
|
+
# and debuginfod can match a stripped binary to its debug file. sha1 is a
|
|
305
|
+
# hash of the output's own bytes in lld, GNU ld and gold, so one input
|
|
306
|
+
# links to one id and `stage3 == stage2` holds; uuid would be random.
|
|
307
|
+
# shellcheck disable=SC2054
|
|
308
|
+
common+=(-Wl,--build-id=sha1)
|
|
299
309
|
# GNU ld needs the gold plugin for LTO; prefer lld when clang can find it.
|
|
300
310
|
if command -v ld.lld >/dev/null 2>&1; then common+=(-fuse-ld=lld); lld=1; fi ;;
|
|
301
311
|
esac
|
package/std/README.md
CHANGED
|
@@ -19,13 +19,16 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
19
19
|
| [`json-value.ts`](./json-value.ts) | `JsonDocument`: any JSON document parsed into a tree of `JsonValue` handles (`i32`s into the document), read with `kind`, `boolean`, `string`, `stringEquals`, `length`, `at`, `keyAt` and `get`, built with the `new*` methods, `push` and `set`, and written back through `JsonWriter` with `write`, or as text with `stringify`. A number keeps its source text and is read on request as an `i64` or `u64` that fits (`isI64`, `isU64`), the nearest `f64`, or the text, and `isIntegral` asks whether its value is whole however it is written (`1.0`, `1e2`), so integers and decimals wider than any type round-trip byte for byte. Objects keep insertion order; a repeated key is `JSON_VALUE_ERR_DUPLICATE` unless `duplicates` is `JSON_VALUE_DUPLICATE_LAST`, `JSON.parse`'s rule. Every parse error and limit is the tokenizer's, with the depth held to the writer's 64 so that whatever parses writes back, nothing panics, and a value is in at most one container, so a tree built through `push` and `set` cannot hold itself. A handle carries the generation of the document it came from, so one kept across a `parse` or `clear` reads as nothing; `maxValues` (262,144 by default, `JSON_VALUE_ERR_VALUES` past it) bounds the memory a document keeps. Each `parse` reuses the document's arrays, so a reused document keeps the arena flat. `tests/link/std_json_value` runs JSONTestSuite through it ([`docs/security/json.md`](../docs/security/json.md)) |
|
|
20
20
|
| [`yaml.ts`](./yaml.ts) | `parseYaml(text)` and `parseYamlWith(text, options)`: a YAML 1.2 stream parsed into a `YamlStream` of documents, nodes and diagnostics, struct of arrays with `i32` handles and `-1` for none. Every node and diagnostic keeps its byte range; a scalar is decoded (`text`) and resolved against the core schema (`type`) only when read. `keyText`, `pathNode`, `pathPair` and `pathMatch` find a key by path through aliases and `<<` merge keys (the map's own keys first, then each merged map in order). A parse never fails or panics on its input: errors and warnings carry the `YAML_ERR_*` codes other YAML tooling reports (`yamlErrorName`), with `maxDepth`, `uniqueKeys`, `merge`, `keepComments` and `firstDocumentOnly` in `YamlOptions`. A merge key's value must name maps (`BAD_MERGE`); tabs are not indentation. |
|
|
21
21
|
| [`yaml-lines.ts`](./yaml-lines.ts) | `YamlLineIndex`: byte offsets turned into 1-based `line` and `column` (characters) or `byteColumn`, by binary search over line starts found once with `indexOf`. LF, CR LF and a lone CR each break a line; an offset is clamped to the text first. |
|
|
22
|
+
| [`yaml-write.ts`](./yaml-write.ts) | `stringifyYaml(doc, v)`: a `json-value` value written as block-style YAML that `parseYaml` reads back as the same tree. A string YAML would read as something else (a null, bool or number word, an indicator, a `: ` or ` #`, an edge space, a control character) is double-quoted and escaped; a key over 1024 bytes is an explicit `? key`. `""` for a no-value handle or a tree deeper than `JSON_WRITER_MAX_DEPTH`. |
|
|
22
23
|
| [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
|
|
23
24
|
| [`collections.ts`](./collections.ts) | the global `Map<K, V>` and `Set<T>`: insertion-ordered tables whose buckets carry a hash fingerprint beside the entry index and whose entries keep their full hash, so every `get`, `set`, `add`, `has` and `delete` is one probe. `get` is not a method here: its `V | undefined` never crosses a call, so the compiler lowers it to `probe` and, where the key was found, `valueAt`. Nor are `keys()` and `values()`: an iterator is not a value, so a `for...of` over one is lowered to `walkOpen`, `walkNext`, `keyAt` or `valueAt`, and `walkClose`, and a count of live walks defers compaction until no loop is walking the table. A program never imports it: naming `Map` or `Set` loads it, and the compiler emits what a module uses of it into that module ([`docs/wp32-map.md`](../docs/wp32-map.md), [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)). Its `hashKey`, `sameKey` and `storedKey` are lowered by the compiler per key type |
|
|
24
25
|
| [`map.ts`](./map.ts) | `reserve(m, n)` and `getOrInsert(m, k, v)` for the global `Map`. Their bodies are the meaning, and what runs under Node: `reserve` does nothing, and `getOrInsert` is a `get`, and a `set` of `v` when the key was missing. Natively the compiler lowers every call in place — `reserve` to the table's `reserveSlots`, which grows the buckets once so that `n` entries fit without a rebuild, and `getOrInsert` to one `probe` and a `valueAt` or an `insertAt` through its answer — so, like `collections.ts`, it writes no `.ll` of its own ([`docs/wp32-map.md`](../docs/wp32-map.md) §9.2, [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)) |
|
|
25
26
|
| [`bytes.ts`](./bytes.ts) | `getU16LE` / `getU16BE` to `getF64LE` / `getF64BE` and the `set` twins: `u16`, `u32`, `u64`, `i32`, `i64` and `f64` read from and written into a `u8[]` at an offset, little- or big-endian. Each access is an ordinary index, so out of range is an `index` panic naming the first missing byte. In range the answers are those of the private copies `std/` keeps (`sha256LoadWord`, `chacha20Word`, `poly1305Le64`, sha512's `loadWord` / `storeWord`, `http2FramePut32`, `tlsPutU16`), none of which it replaces. A guard in the caller does not yet remove the checks after inlining. Under Node only the `u16` and `u32` functions answer as natively. Beside them are the runs a parser walks — `skipBlank`, `skipDigits`, `skipJsonText` and `skipJsonPlain`, the first index in a clamped range whose byte is not a JSON blank, a digit, a byte a JSON string holds as itself or a byte a JSON writer copies without an escape — and `copyBytes`, a window copied as one move that panics on a range outside its array. Their bodies call private walkers, which are what runs under Node; natively the compiler replaces each walker's call with a kernel of `runtime/runtime-bytes.c`, or `llvm.memmove` for the copy ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#byte-runs-skipblank-skipdigits-skipjsontext-skipjsonplain-and-copybytes-in-nishbytes)). The runs are variable-time. `tests/link/std_bytes*` are its programs |
|
|
26
27
|
| [`base64.ts`](./base64.ts) | `nish/base64`: `base64Encode(bytes)`, `base64EncodeInto(src, off, len, dst, at)` and `base64EncodedLength(n)`, padded standard base64 (RFC 4648 §4) for kitty graphics frames. It only encodes, and it indexes its alphabet by the sextet, so it is not constant-time: it is for pixels, not secrets. A strict decoder, and an encoder for key material, are `crypto/base64url.ts`'s `base64StdPadDecode` and `base64StdPadEncode` (`tests/link/std_base64`; the two modules in one program: `tests/link/std_base64_two_modules`) |
|
|
27
28
|
| [`random.ts`](./random.ts) | `Rng`, a seeded generator of pseudo-random numbers: `new Rng(seed)`, `nextI32()`, `nextF64()` and `nextInt(lo, hi)`, an unbiased integer in [lo, hi] by rejection sampling. A Weyl sequence through all 2^32 states with MurmurHash3's finaliser on each, written with `nish:unsafe`'s `wrappingAdd`, `wrappingSub` and `wrappingMul` so that every answer is the same bits natively and under Node (`tests/link/std_random`; [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#random-numbers-rng-in-nishrandom)). A parallel body or a task that makes its own steps it without a shared write. Not for cryptography |
|
|
28
|
-
| [`cli.ts`](./cli.ts) | a command-line tool's `--help`. `HelpMenu` builds it a line at a time — `header`, `usage`, `section`, `row(flag, placeholder, description)`, `note`, `value` — with descriptions lined up at the caller's columns, counted on the plain text, and answers it with `text()`. `Style` paints it in the
|
|
29
|
+
| [`cli.ts`](./cli.ts) | a command-line tool's `--help`. `HelpMenu` builds it a line at a time — `header`, `usage`, `section`, `row(flag, placeholder, description)`, `note`, `value` — with descriptions lined up at the caller's columns, counted on the plain text, and answers it with `text()`. `Style` paints it in the basic ANSI colours, bold and faint, which the terminal's theme defines, or in bold and underline alone when `TERM` declares no colours; each role it paints (`heading`, `name`, `flag`, `meta`, `dim`) is a pair of SGR lists, `<role>Themed` and `<role>Mono`, a program may set. `Style` also paints any text: the sixteen colours (`red`, `brightBlack`, ...), their backgrounds (`bgRed`, `bgBrightRed`, ...), the attributes (`bold`, `faint`, `italic`, `underline`, `inverse`, `strikethrough`, which the failsafe keeps) and `sgr(text, codes)` for any parameter list, with the `SGR_` constants spelling the codes. `styleFor(fd, agent)` is the policy `nish --help` follows: plain unless the descriptor is a terminal, `NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`, `CLICOLOR` and `TERM=dumb` honoured, plain for an agent (`agentDriven`), forced colour always the theme's; it reads the environment and, for the terminal check, forks `/bin/sh` once, which runs `tput` by absolute path. `HelpMenu` and `Style` reach no capability. The compiler's own help is built with it through a relative import, so the module may use only what the last release compiles (`tests/link/std_cli`; [guide](../docs/guide/using-nish/standard-library.md#nishcli)) |
|
|
30
|
+
| [`cli-args.ts`](./cli-args.ts) | a command tree for a large command-line tool. `Cli` declares it with a builder — `group`, `command(parent, name, id, description)`, `alias`, `flag`, `globalFlag`, `argument`, then `defaultValue`, `required`, `repeatable`, `negatable` and `epilog` on what they answer — every declaration an `i32` handle and every mistake a panic at its call. `parse(argv, start)` walks the arguments once and answers a `ParseResult`: the reached command's id and path, each flag's value with its `source` (the command line, its default or unset), the positionals and what follows `--`, or the first error with a did-you-mean. `finish` prints help (laid out by `HelpMenu`), the version or the error and exits 0 or 2. `parse` reaches no capability (`tests/link/std_cli_args`, `examples/api-cli`; [guide](../docs/guide/using-nish/standard-library.md#nishcli-args)) |
|
|
31
|
+
| [`cli-flags.ts`](./cli-flags.ts) | `nish/cli-flags`: a command line read against a declared table. `FlagTable` declares `string`, `number` (an `f64`), `boolean` (`--name` / `--no-name`) and `repeatable` flags and `command` paths of one or more words; `parse(args)` (or `parseFrom(process.argv, 1)`) answers a `ParsedFlags` with the longest matched `command`, the `positionals` and each flag by name (`str`, `num`, `bool`, `all`, `has`). `--name value`, `--name=value`, `--` and a lone `-` follow the GNU long-option grammar; there are no short options. A usage error (unknown flag, missing value, not a number, given twice, value on a boolean, unknown command) is carried as `code`, `message` and `argument`, and `exitUsage()` writes it to stderr and exits 2. `writeHelp(menu)` adds the table's rows to a `nish/cli` `HelpMenu`. Reaches `exit` only through `exitUsage` (`tests/link/std_cli_flags`, `std_cli_flags_bad_*`; [guide](../docs/guide/using-nish/standard-library.md#parsing-flags)) |
|
|
29
32
|
| [`sqlite.ts`](./sqlite.ts) | SQLite, vendored in `runtime/sqlite/` and linked into a program that imports the module, through the C adapter `runtime/sqlite/nish-sqlite.c`. `openDatabase(path, flags)` answers a `Database`: `exec`, `begin` / `commit` / `rollback`, `prepare` into a `Statement`, `lastInsertRowid`, `changes` and `close`, which finalizes the connection's statements. A `Statement` binds (`bindI64`, `bindF64`, `bindNull`, `bindText`, `bindTextBytes`, `bindBlob`), steps, resets and reads columns (`columnType`, `columnI64`, `columnF64`, `columnBytes`, `columnText`, and `columnInto`, a copy into the caller's `u8[]`), so a row allocates nothing. Every failure is a `Result` of SQLite's extended code, and `error(code)` builds the `SqliteError` with its message. A handle is an `i32` into the adapter's thread-local tables, and one used after `close` or `finalize` answers `SQLITE_MISUSE`. Reaches `ffi`; links natively and under `--profile wasi` (`tests/link/std_sqlite*`; [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#sqlite-nishsqlite)) |
|
|
30
33
|
| [`base64.ts`](./base64.ts) | standard base64 (RFC 4648 §4) with `=` padding, encode only: `base64Encode(bytes)` answers a string, `base64EncodeInto(src, off, len, dst, at)` writes into a byte buffer allocating nothing, `base64EncodedLength(n)` sizes it; a range outside its array panics before a byte is written. What kitty graphics frames are sent in; not the constant-time `crypto/base64url` (`tests/link/std_base64`, `std_base64_f64`, `std_base64_negative_*`; [guide](../docs/guide/using-nish/standard-library.md#nishbase64)) |
|
|
31
34
|
| [`term.ts`](./term.ts) | a terminal UI with no I/O of its own. `Screen` holds `width × height` cells (code point, 24-bit foreground and background, bold and inverse) for the frame being drawn and the frame last sent; `flush()` answers the string that turns one into the other — cursor moves and only the cells that changed, a style sent only where it differs — for the caller to `write`. A `Screen` made with `palette256` sends the nearest 256-colour index instead. `Canvas` draws pixels on a screen, two to a cell with the half block "▀" (`setPixel`, `fillRect`, `line`, `text`). The escapes (`enterAltScreen`, `hideCursor`, `clearScreen`, `beginSync`, …) are functions answering strings, and a `Screen` with `sync` set wraps each flush that changes anything in synchronized output (mode 2026). `FixedStep` is a fixed-step clock: `advance(nowMs)` answers the whole steps of `1000 / stepsPerSecond` ms since the last call, at most `maxSteps`, and `alpha()` the remainder, for logic at 240 Hz and drawing at 120. `KeyDecoder` turns raw-mode bytes into `KeyEvent`s (arrows as `ESC [` or `ESC O`, Enter, Space, Escape, Ctrl-C, characters), carries a sequence a read cut short into the next `feed`, and decides a lone ESC on a timeout the caller's clock measures. `feedEvents` and `pollEvents` read the same bytes as `TermEvent`s, whose `type` is an `EVENT_` constant: keys with `MOD_` bits and an `ACTION_` (legacy, modifyOtherKeys and the kitty keyboard protocol), SGR mouse reports in cells or pixels, bracketed paste, focus, and the terminal's replies to queries, which never come out as keys. `KeyState` tracks which keys are held, making up the releases a terminal without the kitty protocol never sends and turning Ctrl-C into `EVENT_QUIT`. The size, raw mode and the clock are the caller's, so the module reaches no capability and prints the same bytes under Node (`tests/link/std_term`, `std_term_f64`; [guide](../docs/guide/using-nish/standard-library.md#nishterm)) |
|
|
@@ -42,10 +45,11 @@ The first lanes of [WP34](../docs/wp34-hosting-cs.md) §5: K1's hashes, MACs and
|
|
|
42
45
|
key derivation, K2's and K3's AEADs, K4's key exchange, K5's signatures and
|
|
43
46
|
K6's certificates, in
|
|
44
47
|
pure Nish (decision S1), each module imported by its own specifier. Every one
|
|
45
|
-
is written from its specification, with
|
|
46
|
-
upstream notice: `crypto/aes.ts`'s `ghashMul32` is adapted from BearSSL,
|
|
48
|
+
is written from its specification, with three exceptions that keep their
|
|
49
|
+
upstream notice: `crypto/aes.ts`'s `ghashMul32` is adapted from BearSSL,
|
|
47
50
|
`crypto/p256.ts`'s field and scalar arithmetic is ported from fiat-crypto and
|
|
48
|
-
its nonce's safegcd inversion from libsecp256k1
|
|
51
|
+
its nonce's safegcd inversion from libsecp256k1, and `crypto/p384.ts`'s field
|
|
52
|
+
and scalar arithmetic is ported from fiat-crypto
|
|
49
53
|
([`THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md)). Each reproduces its
|
|
50
54
|
specification's published vectors in its `tests/link/crypto_*` programs. The
|
|
51
55
|
performance gate compiles every module with no diagnostics under both
|
|
@@ -89,8 +93,12 @@ is listed in [`SECURITY.md`](../SECURITY.md#threat-model).
|
|
|
89
93
|
| [`crypto/chacha20poly1305.ts`](./crypto/chacha20poly1305.ts) | `chacha20Poly1305Seal(key, nonce, aad, plaintext)`, which answers the ciphertext followed by the 16-byte tag, and `chacha20Poly1305Open`, which checks the whole tag before it decrypts anything and answers `null` for a message that does not authenticate. Beneath them `chacha20Block`, `chacha20`, `chacha20QuarterRound`, `poly1305` and `poly1305KeyGen`, and `chacha20HeaderMask`, QUIC's 5-byte header-protection mask. Every input of the wrong length answers `null`. What a call derives from the key — the ChaCha20 state, the keystream, the Poly1305 key, `r` and accumulator, and on open the tag it compared — is wiped with `wipe`'s volatile stores before the call returns, the refusal path included | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
|
|
90
94
|
| [`crypto/aes.ts`](./crypto/aes.ts) | AES-128 and AES-256, bitsliced four blocks at a time with the Boyar–Peralta S-box circuit, so no table is read; natively, on a CPU with AES-NI and PCLMULQDQ or the ARMv8 AES and PMULL instructions, the compiler sends the key schedule, the block, GCTR and GHASH to the runtime's kernels instead (`runtime/runtime-aes.c`, WP38 S5), and `NISH_AES=soft` pins the bitsliced code: `aesKey(key)` expands a 16- or 32-byte key into an `AesKey` (or `aesKeyInto(key, out)` into one the caller holds, allocating nothing that outlives the call), then `aesEncryptBlock`, `aesGcmSeal` / `aesGcmOpen` (tag last, checked in full before anything is decrypted; any non-empty IV) and `aesHeaderMask`. AES-192 is out of scope, and a wrong length answers `null`, as does an `AesKey` not made by `aesKey`; one whose H is zero opens nothing. An `AesKey` holds its round keys in `roundKeys` and H in `h`, two `u64` words (high, low); `aesKeyWipe(key)` zeroes both with `wipe`'s volatile stores, the kernels' powers of H after the schedule included, and is how a holder drops a key. The bitsliced schedule's, block's and GCTR's working copies, the tag's E(K, J0) block, the tag open compared and J0 itself are wiped the same way before they are released | FIPS 197, SP 800-38D, RFC 9001 §5.4.3, Wycheproof `aes_gcm` (all 316 cases; the 103 AES-192 ones check that the key is refused) |
|
|
91
95
|
| [`crypto/p256.ts`](./crypto/p256.ts) | ECDSA over P-256 with RFC 6979's deterministic nonces: `p256PublicKey(priv)` (65-byte uncompressed SEC1) from a `Secret<u8[]>` private key, `p256Sign` / `p256Verify` over a digest and `p256SignSha256` / `p256VerifySha256` over a message, and `p256SignSha256Into(scratch, priv, msg, off, len, out, at)`, the same signature written into the caller's window in a `P256SignScratch` made once, keeping nothing in the arena; a signature is `r` and `s`, 32 big-endian bytes each, not DER (`crypto/x509.ts`'s `x509DerSignatureRS` converts one). A malformed key, a point off the curve or an `r` or `s` out of range answers `null` or `false`, never a panic. ECDSA is malleable, and `p256Verify` accepts a high `s`: when `(r, s)` verifies, so does `(r, n − s)` | RFC 6979 A.2.5, Wycheproof `ecdsa_secp256r1_sha256`, `_sha256_p1363`, `_sha512` and `_sha512_p1363` |
|
|
92
|
-
| [`crypto/
|
|
96
|
+
| [`crypto/p384.ts`](./crypto/p384.ts) | ECDSA verification over P-384: `p384Verify(pub, digest, sig)` over a digest from any hash (one longer than 48 bytes is truncated to its leftmost 384 bits) and `p384VerifySha384(pub, msg, sig)` over a message; the key is 97-byte uncompressed SEC1 and the signature `r` and `s`, 48 big-endian bytes each, not DER. It only verifies, so it holds no `Secret`. A malformed key, a point off the curve, an `r` or `s` out of range and a sum at the identity answer `false`, never a panic. ECDSA is malleable, and `p384Verify` accepts a high `s` | RFC 6979 A.2.6, Wycheproof `ecdsa_secp384r1_sha384`, `_sha384_p1363`, `_sha256`, `_sha512` and `_sha512_p1363` |
|
|
97
|
+
| [`crypto/rsa.ts`](./crypto/rsa.ts) | RSA signature verification under a public key, never signing: `rsaParsePublicKey(der)` reads a PKCS#1 `RSAPublicKey` (strict DER, a 1,024-to-8,192-bit modulus, an odd exponent from 3 below 2^32) into an `RsaPublicKey`, and `rsaVerifyPkcs1v15(key, hash, msg, sig)` and `rsaVerifyPss(key, hash, saltLen, msg, sig)` verify RSASSA-PKCS1-v1_5 and RSASSA-PSS (MGF1 with the same hash) over a message, the hash one of `RSA_HASH_SHA256`, `RSA_HASH_SHA384` and `RSA_HASH_SHA512`. A malformed key answers `null` and a bad signature `false`, never a panic. Public inputs only, so nothing is a `Secret` and the arithmetic is not constant-time (RSA-2) | RFC 8017 §8.2.2, §9.1.2, §9.2; Wycheproof `rsa_signature_*` and `rsa_pss_*` |
|
|
98
|
+
| [`crypto/x509.ts`](./crypto/x509.ts) | DER, PEM and X.509 over P-256: `pemToDer` / `derToPem` (RFC 7468, padded standard base64, strict labels), `x509ParseP256PrivateKey` (SEC1 or PKCS#8, answering a `Secret<u8[]>`), `x509ParseCertificate` / `x509ParseChain` into an `X509Certificate`, `x509VerifySignature` (ecdsa-with-SHA256 under an issuer's key), `x509DerSignatureRS` (a DER ECDSA-Sig-Value as the `r || s` `crypto/p256.ts` takes), `x509MintSelfSigned`, the 1-to-14-day self-signed P-256 certificate WebTransport's `serverCertificateHashes` accepts, and `x509CertificateHash`, its SHA-256. DER is read strictly — non-minimal lengths and integers, indefinite lengths and trailing bytes answer `null`. `x509ParseCertificate` names the signature algorithm (`X509_SIG_*`: ECDSA with SHA-256 or SHA-384, RSA PKCS#1 v1.5 with SHA-1 to SHA-512, RSASSA-PSS with its hash and salt, Ed25519) and key type (`X509_KEY_*`: P-256, P-384, RSA, Ed25519), each in its one strict spelling, and decodes subjectAltName, basicConstraints, keyUsage, extKeyUsage, nameConstraints and the subject and authority key identifiers; a repeated or malformed extension refuses the certificate, and an unknown critical one sets `unknownCritical`. `x509NameEqual` and `x509NameIsPrefix` compare Names byte for byte, not by RFC 5280 §7.1. It parses and does not validate: there is no path validation, nothing applies the extensions to a chain, and no clock is consulted; `x509VerifySignature` checks only ecdsa-with-SHA256 under a P-256 key, and `x509DerSignatureRSWidth` reads a P-384 signature. `pemToDer` refuses a file in which any block, of any label, is malformed. `x509MintSelfSigned` refuses a common name that is not UTF-8 or holds a NUL, and its key and serial must come from `crypto.getRandomValues`, which the module cannot call itself without refusing wasm32: `crypto/x509-random.ts` draws them (X509-6) | X.690, RFC 5280, RFC 7468, RFC 5915, RFC 5208 / 5958, RFC 4055, RFC 8410; certificates checked against OpenSSL |
|
|
93
99
|
| [`crypto/x509-random.ts`](./crypto/x509-random.ts) | **Native only**: the key and serial `crypto/x509.ts`'s mint needs, drawn from `crypto.getRandomValues`, so a program that imports it does not compile for wasm32 and `crypto/x509.ts` imports nothing from it. `x509DrawP256Key()`, a `Secret<u8[]>` P-256 private key drawn again until it is a scalar in [1, n) (each refused draw wiped); `x509DrawSerial()`, twenty octets whose first is 0x40 to 0x7f, so a positive, non-zero, full-length serial; and `x509MintSelfSignedDrawn(priv, commonName, notBeforeMs, days)`, `x509MintSelfSigned` with a drawn serial | — |
|
|
100
|
+
| [`crypto/pki-roots.ts`](./crypto/pki-roots.ts) | **Native only**: the trust anchors, read where OpenSSL and Go look. `pkiLoadSystemRoots()` gives a `PkiRoots` — `roots` (parsed `X509Certificate`s, no two with the same DER), `sources` (the files read) and `skipped` — from the file `SSL_CERT_FILE` and the `:`-separated directories `SSL_CERT_DIR` name when either is set and not empty, with no fallback, and otherwise from the first of `pkiSystemBundles()` (Go's six paths, in Go's order) that exists and `/etc/ssl/certs` (`PKI_SYSTEM_DIR`). `pkiLoadRoots(file, dirs, bundles)` and `pkiLoadRootsFor(certFile, certDir, dirs, bundles)` take the paths and the variables' values given. A directory gives its `*.pem`, `*.crt` and `xxxxxxxx.N` entries; only a regular file is read; a PEM block that is not a certificate `x509ParseCertificate` accepts, a file over `PKI_ROOTS_MAX_FILE_BYTES` and an entry past `PKI_ROOTS_MAX_FILES` or `PKI_ROOTS_MAX_ROOTS` count in `skipped` and are passed over. It does not read the macOS Keychain ([PKI-R7](../docs/security/crypto-pki.md#pki-r7-low-the-macos-keychain-is-not-read)) | — |
|
|
101
|
+
| [`crypto/pki.ts`](./crypto/pki.ts) | Whether a TLS server's chain leads to a trusted root and names the host: RFC 5280 §6 path validation and RFC 6125 §6.4 hostname matching. `pkiTrustStore(roots)` indexes the roots by subject, one per DER; `pkiVerifyServer(chain, hostname, nowMs, store)` takes each certificate's DER as sent, the leaf first and the rest in any order, and gives a `PkiResult` — `code`, `PKI_OK` or one of sixteen `PKI_ERR_` reasons (`pkiReason(code)` is its sentence), and `path`, the leaf to the root. It checks every signature (ECDSA P-256 and P-384 with SHA-256 or SHA-384, Ed25519, RSA PKCS#1 v1.5 and PSS with SHA-256/384/512 under a key of `PKI_MIN_RSA_BITS` or more), validity at `nowMs`, basicConstraints, pathLenConstraint, keyUsage, extendedKeyUsage serverAuth, name constraints and unknown critical extensions, under the bounds `PKI_MAX_CHAIN_CERTS`, `PKI_MAX_CHAIN_BYTES`, `PKI_MAX_PATH`, `PKI_MAX_SIGNATURES`, `PKI_MAX_NAMES`, `PKI_MAX_SUBTREES` and `PKI_MAX_NAME_CHECKS`. `pkiMatchHostname(leaf, hostname)` and `pkiVerifySignature(cert, issuer)` are its two halves alone. Revocation, policies and a root's own constraints are not checked ([crypto-pki.md](../docs/security/crypto-pki.md#open)). Compiles on wasm32 | RFC 5280 §4.2 and §6.1, RFC 6125 §6.4 |
|
|
94
102
|
|
|
95
103
|
Three rules hold across the modules:
|
|
96
104
|
|
|
@@ -168,7 +176,8 @@ allocate their tables once and nothing per call, and a free function
|
|
|
168
176
|
`nish:unsafe`'s unchecked access, each site justified in
|
|
169
177
|
`docs/security/compress.md`, so a program built with `--deny unsafe` cannot
|
|
170
178
|
import them. Written from the RFCs, not ported. Speed against zlib and
|
|
171
|
-
libdeflate: `bench/gzip
|
|
179
|
+
libdeflate: `bench/gzip/`; against Go, Rust, Java, Node, Bun and Python:
|
|
180
|
+
`bench/gzip/compare/`. Guide: [Compression](../docs/guide/using-nish/compression.md).
|
|
172
181
|
|
|
173
182
|
| Module | What it is | Reproduces |
|
|
174
183
|
| --- | --- | --- |
|
|
@@ -190,10 +199,13 @@ reads one, and it still takes its randomness as an argument.
|
|
|
190
199
|
| --- | --- | --- |
|
|
191
200
|
| [`net/tls.ts`](./net/tls.ts) | The server side of a TLS 1.3 handshake, ClientHello through the client's Finished. `new TlsServer(config, serverRandom, x25519Private)`, then `receive(level, buf, off, len)` with what the client sent, `takeOutput(level)` for what to send, and `signatureInput()` / `sign(signature)` for the CertificateVerify hand-off (`tlsSignEcdsaP256` signs for a P-256 key, DER-encoded; a `TlsP256Signer`, made once, gives the same DER from its `sign(priv, content)` and keeps nothing per signature). A `TlsServerConfig` names the certificate chain, the signature scheme, the ALPN protocols, whether the carrier is QUIC, the server's transport parameters, and extensions to pass through in EncryptedExtensions. The suites are `TLS_AES_128_GCM_SHA256`, `TLS_CHACHA20_POLY1305_SHA256` and `TLS_AES_256_GCM_SHA384` (in the client's order), the group x25519 (one HelloRetryRequest when the client offers it without a share), and `server_name`, ALPN and `quic_transport_parameters` are read; PSKs, 0-RTT, NewSessionTicket and client authentication are not. Every refusal is the alert to send — `receive` and `sign` answer 0 or a `TLS_ALERT_*` — never a panic, and a low-order x25519 share is refused. `restart(serverRandom, x25519Private)` hands a server's buffers, made once, to the slot's next connection, so a handshake allocates nothing that outlives it under any suite (TLS-3); it copies the key into the server's own `Secret` and wipes the caller's array, and `wipe()` zeroes every secret the server holds | RFC 8446 §4, §7; RFC 8448 §3 byte for byte, and §5's transcript |
|
|
192
201
|
| [`net/tls/codec.ts`](./net/tls/codec.ts) | The handshake messages as bytes: `tlsReadClientHello` into a reusable `TlsClientHelloView` of windows into the message, and `tlsParseClientHello` into a `TlsClientHello` of copies (structure checked, an alert on a malformed or duplicated field); each message's `tlsWrite*` writer into the caller's array at an offset, and `tlsEncodeServerHello`, `tlsEncodeHelloRetryRequest`, `tlsEncodeEncryptedExtensions`, `tlsEncodeCertificate`, `tlsEncodeCertificateVerify`, `tlsEncodeFinished` and `tlsCertificateVerifyContent`. The wire's constants: handshake and extension types, versions, the x25519 group, the two signature schemes, and the `TLS_ALERT_*` descriptions | RFC 8446 §4, RFC 6066 §3, RFC 7301 §3.1, RFC 9001 §8.2 |
|
|
202
|
+
| [`net/tls/client.ts`](./net/tls/client.ts) | The client side of a TLS 1.3 handshake, ClientHello through the client's Finished, sans-IO: `new TlsClient(new TlsClientConfig(serverName, alpn), random32, x25519Key, p256Key)`, `start`, `receive(level, data, off, len)` and `takeOutput(level)` by encryption level. It offers the three TLS 1.3 suites and X25519 (and secp256r1 when the config's `groups` names it), answers one HelloRetryRequest, checks the downgrade sentinels, and stops at `TLS_CLIENT_STATE_WAIT_AUTH` with `peerCertificates`, `verifyScheme`, `verifyContent` and `verifySignature` out: the caller checks the chain and signature and calls `authenticate(ok)` or `reject(alert)`. TLS 1.2 and client certificates are refused with an alert | `tests/link/net_tls_client_handshake` against `TlsServer`; `tests/link/net_tls_client_refusals`, each refused server flight with its alert |
|
|
193
203
|
| [`net/tls/schedule.ts`](./net/tls/schedule.ts) | The key schedule over SHA-256 or SHA-384: `tlsEarlySecret`, `tlsHandshakeSecret`, `tlsMasterSecret`, `tlsDeriveSecret`, `tlsExpandLabel`, `tlsFinishedVerifyData`, and `tlsTrafficKey` / `tlsTrafficIv` for a record layer — these one-shots take every secret as a `Secret<u8[]>` and answer one (`tlsFinishedVerifyData` answers the public verify data as a plain array; transcript hashes, labels and contexts stay plain), and each wipes its derived salts and `finished_key` before it returns — with in-place forms over an `HkdfScratch` (`tlsDeriveSecretInto`, `tlsMasterSecretInto`, `tlsFinishedVerifyDataInto`, `tlsTrafficKeysInto`), and forms into a `Secret<u8[]>` the caller already holds (`tlsDeriveSecretHeld`, `tlsHandshakeSecretHeld`, `tlsMasterSecretHeld`; `dst` may be the source), `tlsFinishedVerifyDataHeld`, the plain verify data from a held base key, and `tlsTrafficKeysFromHeld`, a held secret's key and IV into plain arrays, which answers `false` for a suite or length it does not know. `TlsTranscript` is the running transcript hash, with HelloRetryRequest's `message_hash` rule, `reset` for the next connection and `hashInto` the caller's array. The suite constants and `tlsSuiteHashLength` / `tlsSuiteKeyLength` | RFC 8446 §4.4.1, §7.1, §7.3 |
|
|
194
204
|
| [`net/tls/record.ts`](./net/tls/record.ts) | The record layer (RFC 8446 §5): `TlsRecordReader` cuts a stream into records in a buffer allocated once, refusing a header of unknown type or a body past 2^14 + 256 bytes before the body is waited for; `TlsRecordProtection` is one direction, cleartext until `install(suite, secret)`, or `installHeld(suite, secret)` from a secret held as a `Secret<u8[]>`, ratcheted in place by `advanceHeld(secret)` — then `seal` and `open` a record into the caller's buffer — the nonce from the sequence number, the header as additional data, the inner content type and zero padding found without branching on the padding. `tlsNextTrafficSecret` is KeyUpdate's next secret, taken and answered as a `Secret<u8[]>`. Every refusal is the alert, negated | RFC 8448 §3's nine records byte for byte; ChaCha20-Poly1305, AES-256-GCM-SHA384 and padding against Python's `cryptography` |
|
|
195
205
|
| [`net/tls/record-server.ts`](./net/tls/record-server.ts) | `TlsRecordServer`: a TLS 1.3 server over a byte stream, sans-IO, wrapping a `TlsServer` in records. `receive` the client's bytes, send `output[outputStart .. outputEnd)` and `consume` what was sent, `read` and `write` application data, `sign` when asked, `keyUpdate`, `close`; `interest()` answers what it wants next as `TLS_RECORD_*` bits. It adds the compatibility `change_cipher_spec`, alerts both ways, KeyUpdate both ways and on a schedule, a limit of `TLS_RECORD_IDLE_LIMIT` (16) records in a row that carry nothing, a cap of `TLS_RECORD_MAX_KEY_UPDATES` (64) KeyUpdates from the client, and fixed buffers per connection; the handshake is sealed straight out of `TlsServer`'s buffers | RFC 8448 §3 replayed at every cut of the client's stream |
|
|
196
|
-
| [`net/tls-
|
|
206
|
+
| [`net/tls/record-client.ts`](./net/tls/record-client.ts) | `TlsRecordClient`: a TLS 1.3 client over a byte stream, sans-IO, wrapping a `TlsClient` in records — the mirror of `TlsRecordServer`. `receive` the server's bytes, send `output[outputStart .. outputEnd)` and `consume` what was sent, `read` and `write` application data, `keyUpdate`, `close`; `interest()` answers `TLS_RECORD_*` bits, `TLS_RECORD_AUTH` (64) when the server waits on the caller's verdict. The compatibility `change_cipher_spec`, alerts and KeyUpdate both ways, and the server's idle and KeyUpdate limits | `tests/link/net_tls_client_record` against `TlsRecordServer`: each suite, a HelloRetryRequest, data, KeyUpdate and close_notify from each side |
|
|
207
|
+
| [`net/tls/verify.ts`](./net/tls/verify.ts) | The client's authentication seam: `tlsClientAuthenticate(conn, trust, nowMs)` at `TLS_RECORD_AUTH` checks the CertificateVerify under the leaf's key (ECDSA P-256, ECDSA P-384, RSA-PSS rsae on 2048 bits or more) and then the chain to a root in `trust` (`tlsClientSystemTrust()` reads the system's), and calls `authenticate(true)` or `reject(alert)`: `bad_certificate`, `decrypt_error`, `certificate_expired` or `unknown_ca`. The chain is `nish/crypto/pki`'s `pkiVerifyServer`: a path to a root of `trust`, every certificate valid at `nowMs`, and the name in the leaf's subjectAltName; there is no switch that skips it | `net_tls_client_tcp` |
|
|
208
|
+
| [`net/tls-tcp.ts`](./net/tls-tcp.ts) | `TlsTcpServer`: TLS 1.3 over `nish:net` TCP for a program that owns its loop — a pool of `TlsRecordServer` slots, each built on the first accept into it, `accept(serverRandom, ephemeralPrivate)` into a free one, `readable` / `writable` when the loop says so, `signP256` with a `Secret` key, and `read`, `write`, `shutdown` and `close` answering as `nish:net`'s calls do. `accept` copies the server random and the ephemeral key into the slot, wipes the caller's key array, and refuses either with -22 when it is not 32 bytes. Every call answers the slot's interest, whose low two bits are `pollModify`'s events. Each slot keeps one `TlsServer`, restarted by `accept`, so a thousand connections through the pool leave the arena where the first did, under any suite (TLS-3). `TlsTcpClient` is the client over one socket: `new TlsTcpClient(addr, config, trust)` connects without blocking, `readable` / `writable` drive it, and at `TLS_RECORD_AUTH` it authenticates the server itself through `nish/net/tls/verify`; `tlsConnect(addr, serverName, alpn, timeoutMs)` runs the handshake to the end in a loop of its own, its last flight sent, and answers the client, open or with its failure (-110 on the timeout); `writeAll` and `readWait` write and read under a timeout of their own, for a program with no loop. The address is numeric and the TLS name always the caller's. Native only, as `nish:net` is | openssl s_client under all three suites and Node's `https`, from `tests/run.js`; RFC 8448 §3 over loopback from a Nish client; the client against `TlsTcpServer` (`net_tls_client_tcp`) and openssl s_server under each suite, group, HRR, ALPN, SNI and leaf key, and through an RSA and a P-384 intermediate, from `tests/run.js` |
|
|
197
209
|
| [`net/quic-packet.ts`](./net/quic-packet.ts) | QUIC version 1's packets (WP34 Q1). The variable-length integer (`quicVarintPush`, `quicVarintPushSized`, `quicVarintRead`, `quicVarintLength`, `quicVarintSize`); packet-number length choice and recovery (`quicPacketNumberLength`, `quicPacketNumberDecode`); the long and short headers (`quicLongHeader`, `quicShortHeader`, and `quicParseHeader` into a `QuicHeader`, whose `end` is where the next coalesced packet starts); the Initial secrets from the client's first DCID (`quicInitialSecrets`); the packet keys from a traffic secret held as a `Secret` from `nish:secret` (`quicKeys(slot, aead, secret)` into a `QuicKeysSlot` the caller owns, for AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305) and a key update, its secret, answered as a `Secret` (`quicKeyUpdateSecret`), and the next generation's keys with the header-protection key kept (`quicKeysUpdate(slot, keys, next)`, into a slot of its own), neither keeping anything in the arena, and `quicKeysWipe`, which zeroes a `QuicKeys`, AES schedules included; the same four in place for a connection slot, into a `QuicKeysSlot` it makes once and over its `HkdfScratch` (`quicInitialSecretsInto`, `quicKeysInto`, `quicKeyUpdateSecretInto`, `quicKeysUpdateInto`; `QuicKeysSlot.wipe` zeroes the keys and AES schedules), with `quicKeysHeldInto(slot, aead, secret)` and `quicKeyUpdateSecretHeldInto(dst, src)` for a secret held as a `Secret<u8[]>`, the update in place when `dst` is `src`; packet and header protection (`quicSealPacket`, and `quicOpenPacket` or its two halves `quicRemoveHeaderProtection` and `quicDecryptPacket` into a `QuicPacket`); and, for a connection that allocates nothing per packet (WP34 Q4), `quicParseHeaderInto` (windows, no copy), `quicPutLongHeader` / `quicPutShortHeader` / `quicVarintPut` in place, `quicSealInPlace`, and `quicUnprotectHeader` / `quicDecryptPayload`, which store only numbers into the `QuicPacket`; and the Retry integrity tag (`quicRetryIntegrityTag`, `quicRetryPacket`, `quicRetryVerify`). Every parse is bounds-checked against the datagram and answers a `QUIC_ERR_*` code rather than panicking: a truncated header, a clear fixed bit, another version (with its connection IDs read, for Version Negotiation), a connection ID over 20 bytes, a Length past the datagram, a packet too short to sample, a payload that does not authenticate, and reserved bits set in one that does; keys `quicKeys` did not make are `QUIC_ERR_KEYS`, a caller's error rather than a forgery. Long headers always carry a two-byte Length, so a header's size does not move with its payload, and `quicSealPacket` refuses a header whose Length or packet number disagrees with what it seals. The builders answer `null` for an argument out of range. The keys in a slot the caller owns are the caller's to wipe, with `QuicKeysSlot.wipe` or `quicKeysWipe` ([QUIC-1](../docs/security/quic.md)) | RFC 9000 §16, §17 and Appendix A.1–A.3; RFC 9001 §5 and Appendix A.1–A.5 (client and server Initial, Retry, ChaCha20 short header) |
|
|
198
210
|
| [`net/quic-frame.ts`](./net/quic-frame.ts) | QUIC version 1's frames (WP34 Q2). `quicParseFrame` reads any frame of RFC 9000 §19, 0x00 to 0x1e, into a reused `QuicFrame`, leaving a CRYPTO or STREAM frame's data in the packet (`dataStart`, `dataLength`) and answering 0 or the transport error the RFC names: FRAME_ENCODING_ERROR for a truncated field, a length past the packet, an ACK range below zero, an offset past 2^62 − 1, a stream count past 2^60, an empty NEW_TOKEN or a bad NEW_CONNECTION_ID, and PROTOCOL_VIOLATION for a frame type spelled in more bytes than it needs. The writers (`quicPushAck`, `quicPushCrypto`, `quicPushStream`, `quicPushValue`, `quicPushStreamValue`, `quicPushStreamError`, `quicPushNewConnectionId`, `quicPushPathData`, `quicPushConnectionClose`, `quicPushNewToken`, `quicPushTypeOnly`, `quicPushPadding`) answer `false` rather than write a frame that would not parse; `quicCryptoOverhead` and `quicStreamOverhead` size a frame's header before it is written. `quicFrameAllowed` is §12.4's table of which frames each packet type carries, `quicFrameAckEliciting` §13.2's rule, and `QUIC_ERROR_*` the transport error codes of §20.1. RFC 9221's DATAGRAM frames (0x30, 0x31) are read too (WP34 Q4); every writer has a `put` form (`quicPutAck`, `quicPutStream`, …, `quicPutDatagram`) that writes in place into a caller's buffer before an end and answers the offset past it, which the `push` form calls, and NEW_CONNECTION_ID's ID and token are left in the packet as windows | RFC 9000 §12.4, §19, §20; RFC 9221 §4; RFC 9001 A.2's and A.3's frames |
|
|
199
211
|
| [`net/quic-conn-params.ts`](./net/quic-conn-params.ts) | Transport parameters (RFC 9000 §18): a `QuicTransportParameters` that starts at every default, `quicEncodeTransportParameters` (a parameter only when it is not its default) and `quicParseTransportParameters`, with in-place forms that refill a slot's own array and object (`quicEncodeTransportParametersInto`, `quicParseTransportParametersInto`), which refuses with TRANSPORT_PARAMETER_ERROR, naming the parameter, a parameter that does not frame, a known one sent twice, a varint-valued one that is not exactly one varint, a server-only one from a client, and every value §18.2 bounds. Unknown parameters are skipped. RFC 9221's `max_datagram_frame_size` is read and written too (WP34 Q4) | RFC 9000 §7.4, §18; RFC 9221 §3; RFC 9001 A.2's client parameters |
|
|
@@ -205,13 +217,18 @@ reads one, and it still takes its randomness as an argument.
|
|
|
205
217
|
| [`net/quic-stream.ts`](./net/quic-stream.ts) | QUIC streams (WP34 Q4), sans-IO, which `nish/net/quic` drives: `QuicStreams` holds one `QuicStream` slot per stream that may be open at once (the client's bidirectional and unidirectional limits and the server's own), each with a fixed receive and send buffer, found by ID through a hash index. The send and receive state machines of §3 (`QUIC_SEND_*`, `QUIC_RECV_*`), reassembly into the receive buffer, flow control per stream and for the connection both ways (MAX_STREAM_DATA and MAX_DATA raised as the application reads, STREAM_DATA_BLOCKED and DATA_BLOCKED once per limit), the stream limits (MAX_STREAMS raised as the client's streams finish, STREAMS_BLOCKED at the client's), STOP_SENDING answered with RESET_STREAM, and the refusals STREAM_STATE_ERROR, STREAM_LIMIT_ERROR, FLOW_CONTROL_ERROR and FINAL_SIZE_ERROR. The application's answers are `QUIC_STREAM_OK`, `QUIC_STREAM_END` and `QUIC_STREAM_ERR_*` | RFC 9000 §2–§4, §19.4–§19.14 |
|
|
206
218
|
| [`net/quic-datagram.ts`](./net/quic-datagram.ts) | `QuicDatagramQueue` (WP34 Q4): a fixed ring of DATAGRAM payloads, one each way per connection, copied in (`push`) and out (`pop`, or `putFrame` as a DATAGRAM frame), refusing past a full ring and counting what it drops | RFC 9221 §5 |
|
|
207
219
|
| [`net/buffer-pool.ts`](./net/buffer-pool.ts) | `BufferPool(name, size, ceiling)`: a pool of fixed-size `u8[]` buffers that a server lends across connections. `take()` answers a free buffer, most recently given back first, or a new one while fewer than `ceiling` are lent; `give(buf, used)` zeroes the first `used` bytes and takes it back, and `giveSecret(buf, used)` wipes the whole buffer with `secureZero`; `lent()` counts the buffers out. A `size` not above 0 or a `ceiling` below 1, taking past the ceiling, giving back a buffer of the wrong length, a used extent outside 0 to `size`, and a give-back with none lent panic, naming the pool | Reuse, order, zeroing, the ceiling and a warmed loop that allocates nothing (`tests/link/net_buffer_pool`, and its `_bad_` cases) |
|
|
220
|
+
| [`net/dns-message.ts`](./net/dns-message.ts) | The DNS wire codec, sans-IO: `dnsBuildQuery(out, id, name, qtype)` writes one A or AAAA question with RD set into the caller's buffer (`DNS_QUERY_MAX_BYTES`, 271, always fits) and answers its length or a negative `DNS_ERR_*` for a name that is not one. `dnsParseReply(msg, len, query, queryLen, port, reply)` reads the reply into a reused `DnsReply`: it is refused unless its ID, QR, opcode and one question match the query's; it answers NOERROR, NXDOMAIN, SERVFAIL or REFUSED, or a `DNS_ERR_*` for a compression pointer that is not backwards, a seventeenth hop, a name past 255 bytes, a count the bytes cannot hold, and RDATA past the end or of the wrong length. The CNAME chain from the question's name is followed and the first `DNS_MAX_ANSWERS` (8) addresses on it are kept as 18-byte addresses with their TTLs, the rest counted; TC set reads no record. A window outside its array and a port or ID that is not 16 bits panic | RFC 1035 §4, RFC 2181 §8, RFC 4343; captured replies for example.com, a CNAME chain, NXDOMAIN and nine answers, and a seeded mutation pass (`tests/link/std_dns_message`) |
|
|
221
|
+
| [`net/dns-config.ts`](./net/dns-config.ts) | `/etc/resolv.conf` and `/etc/hosts`, read from bytes into fixed tables: `dnsParseResolvConf` fills a `DnsConfig` (up to 3 `nameserver`s on port 53, 6 `search` or `domain` names in 256 bytes, `options ndots timeout attempts` clamped to 15, 30 and 5; no server gives 127.0.0.1 and ::1), `dnsParseHosts` fills a `DnsHosts` (256 addresses, 1024 names in 16 KiB), and `dnsHostsFind` answers the addresses a name has, without case, in file order. What does not fit is dropped and counted, and a line over 1024 bytes is skipped and counted. `dnsParseAddress` reads a strict IPv4 or IPv6 literal (no octal, no zone) into the 18-byte form | glibc's `resolv.conf(5)` and `hosts(5)`; RFC 4291 §2.2 (`tests/link/std_dns_config`) |
|
|
222
|
+
| [`net/dns-resolver.ts`](./net/dns-resolver.ts) | The DNS stub resolver with no I/O of its own: `DnsResolver(config, hosts, maxLookups)` takes `start(name, port, now)` and answers a lookup's index, or `DNS_RESOLVE_BUSY` past `maxLookups` (at most 64). Each lookup asks A and AAAA at once; the caller sends what `nextOutgoing(out, to, meta, id)` gives (a datagram, a TCP connection or a close, with the caller's random ID), feeds what comes back to `receive` or `receiveStream`, calls `tick(now)` once `nextDeadlineNanos()` passes, and reads `result(lookup, out, ttls)`: the count of 18-byte addresses, A first, each with its TTL, or `DNS_RESOLVE_PENDING`, `NOT_FOUND`, `NO_ANSWER` or `CNAME`. Servers are tried in turn for `attempts` rounds of `timeout`; TC asks again over TCP (a reply over `DNS_TCP_MAX_REPLY`, 16 KiB, fails the server); a CNAME left unanswered is followed by a query of its own, at most 8 hops and never back to the start; the search list and `ndots` run as resolv.conf(5) says. `localhost`, an address literal and a hosts entry answer with no packet. A reply from another address or port, or with another ID or question, is dropped, counted (`droppedSource`, `droppedId`, `droppedQuestion`, `droppedMalformed`) and the query keeps waiting. It never imports `nish:net`, reads a clock or a file, and builds for `wasm32`; every buffer is made in the constructor | RFC 1035 §4.2 and §7.3; RFC 5452 (forgery resilience); RFC 7766 (DNS over TCP); resolv.conf(5); scripted packets for every drop, bound and error path (`tests/link/std_dns_resolver`) |
|
|
223
|
+
| [`net/dns.ts`](./net/dns.ts) | `nish/net/dns-resolver` on `nish:net` sockets, on the caller's loop: `DnsClient(resolver, loop, tokenBase)` opens a new UDP socket on a kernel-chosen port for each try, or a TCP connection for a TCP one, under a token of its range (`owns`), draws each query ID from `crypto.getRandomValues`, and never waits: the program's `pollWait` hands it `ready(token, events, now)`, calls `tick(now)` and waits no longer than `timeoutMs(now, own)`. `resolveHost(name, timeoutMs, port, out, ttls)` reads `/etc/resolv.conf` and `/etc/hosts` and blocks on a loop of its own, for a CLI or start-up; `dnsLookupWith` does the same with a configuration given. | A Nish DNS server on loopback: UDP, TCP, TC, CNAME, the search list, a dead server, `attempts`, the hosts file and spoofed replies (`tests/link/std_dns`); the capability report (`std_dns_caps`); a flat arena over 2,000 lookups (`std_dns_soak`) |
|
|
208
224
|
| [`net/hpack.ts`](./net/hpack.ts) | HPACK, HTTP/2's header compression: `HpackEncoder` and `HpackDecoder`, one per direction of a connection, each with its `HpackTable`; and beneath them the §5.1 integer (`hpackEncodeInteger` / `hpackDecodeInteger`), the §5.2 string (`hpackEncodeString` / `hpackDecodeString`) and Appendix B's Huffman code (`HpackHuffman`, `hpackHuffmanEncode`, `hpackHuffmanDecode`, `hpackHuffmanLength`). Names and values are `u8[]`. The encoder takes the representation and the Huffman choice per field, and sends a never-indexed field as a literal even when a table holds it. The decoder answers a negative `HPACK_ERR_*` code, and stays spent, for a truncated block, an integer past 2^31 - 1, an index of zero or out of range, bad Huffman padding, EOS, a size update above the SETTINGS limit, after a field or missing when a lowered limit requires one, and for a block that would copy more than `HPACK_COPY_FACTOR` times the larger of the table's size and the block's length into and out of the table (`HPACK_ERR_COPY_BUDGET`; `tableCopied` counts those octets); a header list past the caller's limit is decoded to the end, its excess dropped, and answered with `HPACK_LIST_TOO_LARGE`, which is not fatal. A window outside its buffer, a negative size or limit, and an integer prefix outside 1 to 8 bits are the program's mistakes and panic | RFC 7541 Appendix C (C.1–C.6, with the dynamic table after every step) and every code of Appendix B |
|
|
209
225
|
| [`net/qpack.ts`](./net/qpack.ts) | QPACK, HTTP/3's field compression, with a dynamic table of capacity 0: `QpackEncoder` and `QpackDecoder`, one per connection. The encoder writes a field section that starts `00 00` (Required Insert Count 0, Base 0) with `beginSection`, then one field line per `encodeField(out, name, off, len, value, off, len, neverIndexed, huffman)`: an Indexed Field Line when the 99-entry static table holds the field, else a literal with the name by static index when it can, each string Huffman-coded when that is shorter and `huffman` is set, and a never-indexed field always a literal with the N bit. The decoder answers a section's fields as windows into its own `bytes` (`nameStart`, `nameLength`, `valueStart`, `valueLength`, `neverIndexed`, `count`), its arrays reused from section to section. It answers QPACK_DECOMPRESSION_FAILED, and stays spent, for a truncated section, an integer past 2^62 − 1, a non-zero Required Insert Count, a negative Base, any dynamic-table reference, a static index past 98 and bad Huffman; a section past `maxFieldSectionSize` answers `QPACK_SECTION_TOO_LARGE`, which is not fatal. `receiveEncoderStream` accepts Set Dynamic Table Capacity 0 and answers QPACK_ENCODER_STREAM_ERROR for any other capacity, Insert or Duplicate; the encoder's `receiveDecoderStream` accepts Stream Cancellations, split anywhere, and answers QPACK_DECODER_STREAM_ERROR for a Section Acknowledgment or Insert Count Increment; `reason` names the rule. `qpackPushStreamCancellation` is the one instruction it may send. The Huffman code is `nish/net/hpack`'s. A window outside its buffer, a negative limit and a stream ID outside 0 to 2^62 − 1 panic | RFC 9204 Appendix B.1, every Appendix A entry by its index, B.2 to B.5's instructions refused, and a request matched byte for byte with pylsqpack's encoding |
|
|
210
226
|
| [`net/http1.ts`](./net/http1.ts) | `Http1Parser`, an incremental HTTP/1.1 request parser: `feed(buf, off, len)` any slice, then `next()` answers `HTTP1_NEED_MORE`, `HTTP1_HEAD` (`method`, `target`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength` or `chunked`, `keepAlive`, `upgrade`), `HTTP1_BODY` (a window onto its buffer, de-chunked), `HTTP1_END`, or one of three final events: `HTTP1_UPGRADE` (`upgradeBytes()` and `declineUpgrade()`), `HTTP1_CLOSED` and `HTTP1_ERROR` with the `status` to answer. It refuses with 400 every request-smuggling shape — `Content-Length` with `Transfer-Encoding`, a repeated or non-digit `Content-Length`, an obs-fold, a bare LF, whitespace before a colon, a malformed chunk size or terminator — and with 413, 414, 431, 501 and 505 what those mean. The head is also kept as spans in a buffer reused from request to request (`head`, `spans`, `methodIs`, `targetIs`, `headerIndex`, `headerIs`, `upgradeIs`), so a parser with `keepText` off allocates nothing per request; `maxChunk` caps each `HTTP1_BODY`, and `restart()` readies it for the next connection. The writer is `http1ResponseHead(status, reason, names, values, bodyLength)`, which adds the framing field itself and refuses (`null`) a CR, LF or other control character anywhere a caller's string lands, and `http1Chunk` and `http1LastChunk` for a chunked body; `http1WriteResponseHead` and `http1WriteChunk` write the same bytes into a caller's buffer at an offset, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED`, and the first takes a date's bytes to send as a `Date` field on a final response, unless the caller's fields hold one | RFC 9112 §2–§7 and §9.3, RFC 9110 §5, §6.2, §7.2 and §7.8; a corpus fed at every split point (`tests/link/net_http1`) |
|
|
227
|
+
| [`net/http1-client.ts`](./net/http1-client.ts) | The client side of HTTP/1.1, sans-IO. The writer is `http1RequestHead(method, target, names, values, bodyLength)`, `Host` first and the framing field added for `bodyLength` (`Content-Length`, `HTTP1_CHUNKED` or `HTTP1_NO_BODY`), and `http1WriteRequestHead` into a caller's buffer, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED` with nothing written; it refuses a method that is not a token, a target that is not a form the method may send, a CR, LF or control byte in a value, not exactly one `Host`, the caller's own framing fields and a `Connection: upgrade`. `Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes)` is told what was asked (`expect(head, connect)`), fed any slice, and `next()` answers `HTTP1_INFORMATIONAL`, `HTTP1_HEAD` (`status`, `reason`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength`), `HTTP1_BODY` (a window, de-chunked), `HTTP1_END` with `keepAlive()`, or the final `HTTP1_UPGRADE` (a 2xx to CONNECT, `tunnelBytes()`), `HTTP1_CLOSED` and `HTTP1_ERROR`; `finish()` reports the close that ends a body read until close, and anywhere else is `HTTP1_E_TRUNCATED`. Every smuggling shape is refused with its own `HTTP1_E_*` code, as is a 101, more than `HTTP1_MAX_INFORMATIONAL` interim responses and a chunk-size line over `HTTP1_MAX_CHUNK_LINE`. Record: [`docs/security/http1-client.md`](../docs/security/http1-client.md) | RFC 9112 §4–§7 and §9.3, RFC 9110 §9.3.6 and §15.2; a corpus fed whole, cut at every byte and a byte at a time, every refusal and limit at its edge, and the server parser reading what the writer writes (`tests/link/net_http1_client`, `_f64`, `_feed_window`) |
|
|
211
228
|
| [`net/tls/ticket.ts`](./net/tls/ticket.ts) | Session tickets a server seals for itself (RFC 8446 §4.6.1): `tlsTicketSeal` and `tlsTicketOpen` seal a `TlsTicketState` (suite, issue time, lifetime, chain start, server name, resumption PSK) under ChaCha20-Poly1305 with a borrowed `Secret<u8[]>` key, as key id ‖ nonce ‖ ciphertext ‖ tag, and wipe the plaintext on every path. `TlsTicketKeyInfo`, `tlsTicketKeyDraw` and `tlsTicketKeyDue` are a key's public record, its draw and when to replace it: a key seals for 12 h of awake time and 2^30 tickets, and opens for 14 h. `TlsTicketStrikes` is the bounded strike register (§8.1) that makes a ticket single-use, failing closed when full | `tests/link/net_tls_ticket`: every bound at its instant and 1 ns past it, a flipped bit in each region, three key rotations, replay and a full register |
|
|
212
229
|
| [`net/websocket.ts`](./net/websocket.ts) | `WsDecoder(expectMasked, maxMessage)`, the same `feed` and `next()` over WebSocket frames: `WS_MESSAGE` (fragments joined, text checked as UTF-8 fragment by fragment), `WS_PING`, `WS_PONG`, and the final `WS_CLOSE` and `WS_ERROR`, whose `closeCode` is the 1002, 1007 or 1009 to close with. `websocketFrame` encodes a frame with the mask key the caller supplies (`websocketWriteFrame` into a caller's buffer, `websocketFrameSize` its length), `reset()` readies a decoder for the next connection, `websocketClosePayload` a close frame's code and reason, and `websocketIsUtf8` checks a window. The handshake is `websocketRequestKey(parser)` over an `Http1Parser`'s head, `websocketAcceptKey`, `websocketKeyIsValid` and `websocketUpgradeResponse`, the 101 | RFC 6455 §1.3's accept key, the §5.7 example frames byte for byte, §5.5's control-frame rules, §7.4's close codes |
|
|
213
230
|
| [`net/http1-server.ts`](./net/http1-server.ts) | The server side of HTTP/1.1 (WP34 H1): `Http1Connection`, sans-IO, drives an `Http1Parser` — `feed`, then `next()` answers `H1_REQUEST` (the parser's head), `H1_BODY` (at most `chunkSize` bytes, either framing), `H1_END`, `H1_WRITE` (a held-back write may go again), `H1_WS_MESSAGE`, `H1_WS_CLOSE` or the final `H1_ERROR`; the program answers with `respond(status, names, values, bodyLength)`, `write` and `end`, or `acceptWebSocket(protocol)` and then `sendFrame` and `closeWebSocket`. Keep-alive and pipelining one exchange at a time, a response with a length or chunked (unframed to an HTTP/1.0 client), `Connection` and the framing decided by the server, every parser refusal answered with its status and a close, pings and closes answered on a WebSocket. Two slot pools for a program that owns its loop: `Http1Server` on plain TCP and `Http1TlsServer` on `nish/net/tls-tcp` with ALPN `http/1.1` or none (`accept`, `readable` / `writable`, `next(slot)`, `flush(slot)`, `close`, `expire(now)` for the idle timeout and the deadline for a whole head, `headTimeout`, against a client that trickles its head). Both build a slot's connection on the first accept into it rather than at start-up. `Http1Server` reads a socket until a short read rather than until it would block, takes a free slot off a stack of released slots or the next never-used one rather than scanning for one, and answers `rearm(slot, wants)`, whether the interest changed and needs a `pollModify` at all: a keep-alive request is one `recv` and one `send`. The slots borrow their output from one `Http1OutputPool` while they write and give it back once sent, so a pool's memory follows the connections the socket is holding back, not its size: an idle slot holds no output buffer. Every final response carries a `Date` from `HttpDate`, which reads `Date.now()` and formats at most once a second, shared by a pool's slots; `pin(unixMs)` fixes it for a test, and a program's own `date` field replaces it. Every cap is an `Http1Config` number fixed at start-up, and a warmed slot allocates nothing per request (`docs/security/http1.md`). Native only, as `nish:net` is | RFC 9112 §6, §7, §9; RFC 9110 §9.3.2, §15; RFC 6455 §4.2, §5.5. Nish clients over loopback against both carriers, requests cut at every byte, 2 MiB bodies each way, pipelining, a WebSocket echo, the arena flat over two hundred requests, the date formatted once a second, and every refusal (`tests/link/net_http1_server`); `curl` against its `serve` mode |
|
|
214
|
-
| [`net/url.ts`](./net/url.ts) | The request target's path and query, read in place: `urlQueryStart` finds the `?`, `UrlPathSegments` splits a path on the raw `/` and `UrlQuery` yields a query's pairs as offsets, both reusable by `reset`; `percentDecode` and `percentDecodePathSegment` decode a window into the caller's array and answer the length or a `URL_DECODE_ERR_*` code, `percentDecodeErrorAt` answers the offset an error is at, and `urlQueryFind` finds a key. Decoding is strict by default (a malformed escape and `%00` are refused); `URL_DECODE_FORM`, `URL_DECODE_LENIENT` and `URL_DECODE_ALLOW_NUL` together decode valid UTF-8 as `URLSearchParams` does. A query yields at most `UrlQueryLimits.maxPairs` pairs (100 by default) and then answers `URL_QUERY_ERR_PAIRS`. The byte API allocates nothing; `urlDecode` and `urlQueryGet` answer strings and refuse what is not UTF-8. Records: [`docs/security/url.md`](../docs/security/url.md) | RFC 3986 §2.1 and §3.3, the WHATWG URL Standard's percent-decode and `application/x-www-form-urlencoded` parser; `decodeURIComponent` and `URLSearchParams` on 3,000 seeded inputs |
|
|
231
|
+
| [`net/url.ts`](./net/url.ts) | The request target's path and query, read in place: `urlQueryStart` finds the `?`, `UrlPathSegments` splits a path on the raw `/` and `UrlQuery` yields a query's pairs as offsets, both reusable by `reset`; `percentDecode` and `percentDecodePathSegment` decode a window into the caller's array and answer the length or a `URL_DECODE_ERR_*` code, `percentDecodeErrorAt` answers the offset an error is at, and `urlQueryFind` finds a key. Decoding is strict by default (a malformed escape and `%00` are refused); `URL_DECODE_FORM`, `URL_DECODE_LENIENT` and `URL_DECODE_ALLOW_NUL` together decode valid UTF-8 as `URLSearchParams` does. A query yields at most `UrlQueryLimits.maxPairs` pairs (100 by default) and then answers `URL_QUERY_ERR_PAIRS`. The byte API allocates nothing; `urlDecode` and `urlQueryGet` answer strings and refuse what is not UTF-8. Whole URLs: `urlParse` splits an absolute URL into a `Url` (`scheme`, `hasAuthority`, `username`, `password`, `host`, `port`, `path`, `query`, `fragment`, each still percent-encoded, the scheme and host lower-cased), `urlResolve(base, ref)` resolves a reference against one, `urlSerialize` writes one back, and `urlDefaultPort` and `urlOrigin` answer the port and the origin a client connects to. Both refuse with null what two readers could take for different hosts: a byte outside printable ASCII, a backslash, a malformed `%`, a second `@`, a non-unreserved host byte, a numeric host that is not strict dotted-decimal IPv4, an IPv6 zone, a port past 65535. Records: [`docs/security/url.md`](../docs/security/url.md) | RFC 3986 §2.1 and §3.3, the WHATWG URL Standard's percent-decode and `application/x-www-form-urlencoded` parser; `decodeURIComponent` and `URLSearchParams` on 3,000 seeded inputs; RFC 3986 §3 and §5.2, every §5.4 example, 57 refusals and 4,000 seeded round trips in both number modes (`tests/link/net_url_parse`) |
|
|
215
232
|
| [`net/http-fields.ts`](./net/http-fields.ts) | The header model HTTP/2 and HTTP/3 share, version-neutral: `HttpFields` reads a decoded section with `readRequest(names, values, extendedConnect)`, `readResponse` or `readTrailers` into `method`, `scheme`, `authority`, `path`, `protocol`, `status`, the regular fields in order (`get(name)`) and `contentLength`, answering `HTTP_FIELDS_OK` or the first refusal: a name that is empty, not a token or not lowercase; a value with NUL, CR or LF or whitespace at an end; a connection-specific field (`te` but `trailers` included); a pseudo-header unknown, repeated, after a regular field, forbidden by the shape or missing; a pseudo-header value of the wrong syntax; a `host` that disagrees with `:authority`; a bad or disagreeing `content-length`. `:protocol` (RFC 8441) is read only where the caller enabled extended CONNECT. `httpFieldNameValid`, `httpFieldValueValid`, `httpFieldConnectionSpecific` and `httpFieldsCheckOutgoing` check one name, one value or a section a program sends. `httpDateWrite(out, at, seconds)` writes a second since 1970 as the IMF-fixdate a `Date` field holds (`HTTP_DATE_SIZE` bytes, to 9999), reading no clock, so it compiles for wasm32 | RFC 9110 §5, RFC 9113 §8.2–§8.5, RFC 9114 §4.2–§4.3, RFC 8441 §4; every refusal in `tests/link/net_http_fields`; RFC 9110 §5.6.7, every day from 1970 to 2399 (`tests/link/net_http_date`) |
|
|
216
233
|
| [`net/http2-frame.ts`](./net/http2-frame.ts) | HTTP/2's frames, sans-IO and allocation-free: `http2ReadHeader` and `http2ParseFrame` read any frame of RFC 9113 §6 into a reused `Http2Frame` (its content left in the buffer as a window), answering 0 or the connection error — FRAME_SIZE_ERROR past the reader's SETTINGS_MAX_FRAME_SIZE or for a frame of the wrong length, PROTOCOL_ERROR for a frame on the wrong stream, padding as long as its frame, or a WINDOW_UPDATE of zero on the connection — with a stream error (`streamError`) for a PRIORITY of the wrong length, a self-dependency or a WINDOW_UPDATE of zero on a stream. `http2SettingCount`, `http2SettingId`, `http2SettingValue` and `http2SettingError` read and check SETTINGS. The writers, one per type (`http2WriteData` … `http2WriteContinuation`, `http2WriteSettingsAck`), write into the caller's buffer at an offset and answer -1 when a frame does not fit | Every type against frames built by hand from §4.1 and §6, both directions, and every refusal (`tests/link/net_http2_frame`) |
|
|
217
234
|
| [`net/http2.ts`](./net/http2.ts) | `Http2Connection`, the server side of HTTP/2, sans-IO: `feed(buf, off, len)`, then `next()` answers `H2_REQUEST` (`stream`, `fields`), `H2_DATA` (a window onto the input), `H2_TRAILERS`, `H2_RESET`, `H2_WINDOW` (a send window opened), `H2_GOAWAY` or the final `H2_ERROR`; send `output[outputStart .. outputEnd)` and `consume`. `respond`, `writeData` (as far as both windows and the output allow), `writeTrailers`, `reset`, `goaway` and `restart`. The §5.1 stream state machine, connection and stream flow control with WINDOW_UPDATE both ways, SETTINGS and the acknowledgement, PING, header blocks across CONTINUATION decoded by `HpackDecoder`, a 431 past the header-list cap, extended CONNECT behind SETTINGS_ENABLE_CONNECT_PROTOCOL, and every connection and stream error of RFC 9113 as GOAWAY or RST_STREAM. Every final head carries one `date` field from the connection's `date` bytes, when it has them, straight after `:status` and never on a 1xx or trailers, unless the program's fields hold one (matched ASCII case-insensitively). Every cap is an `Http2Config` number fixed at start-up; DATA and every control frame allocate nothing ([H2-1](../docs/security/http2.md)) | RFC 9113 §3–§8, RFC 8441; requests, streamed responses, flow control stalling and resuming, concurrent streams and the date on every final head from a scripted client (`tests/link/net_http2`), and a negative for each error class (`tests/link/net_http2_errors`) |
|