@amritk/nish-aarch64-darwin 0.25.0 → 0.27.2
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 +30 -4
- package/package.json +1 -1
- package/runtime/core/host.d.ts +4 -0
- package/runtime/core/host.mjs +24 -0
- package/runtime/nish.d.ts +53 -5
- package/runtime/nish.h +43 -4
- package/runtime/nish.mjs +4 -0
- package/runtime/node.mjs +79 -0
- package/runtime/runtime-os.c +257 -22
- package/runtime/runtime-simd.c +63 -2
- package/runtime/runtime-wasm.c +18 -0
- package/scripts/build.sh +16 -1
- package/std/README.md +3 -2
- package/std/crypto/sha256.ts +335 -188
- package/std/net/http-client.ts +1645 -0
- package/std/net/postgres.ts +425 -29
package/runtime/runtime-os.c
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
#include <unistd.h>
|
|
50
50
|
#ifndef __wasi__
|
|
51
51
|
/* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
|
|
52
|
+
#include <poll.h>
|
|
52
53
|
#include <signal.h>
|
|
53
54
|
#include <spawn.h>
|
|
54
55
|
#include <sys/resource.h>
|
|
@@ -170,20 +171,9 @@ nish_str *nish_read_file(const nish_str *path) {
|
|
|
170
171
|
return s;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
/* `
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
its source, so a link planted at that name in a checkout would otherwise
|
|
177
|
-
truncate any file the user can write. A link among the directories on the
|
|
178
|
-
way is still followed.
|
|
179
|
-
|
|
180
|
-
`O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
|
|
181
|
-
as the read retries it (RT-13): a write cut short is finished, and one that
|
|
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). */
|
|
174
|
+
/* All `n` bytes of `p` to `fd`, answering 0 or the negative errno of the
|
|
175
|
+
write that failed (-EIO for one that wrote nothing). A write cut short is
|
|
176
|
+
finished and `EINTR` retried (RT-13). Every writer below shares it. */
|
|
187
177
|
static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
|
|
188
178
|
for (uint64_t done = 0; done < n;) {
|
|
189
179
|
ssize_t k = write(fd, p + done, n - done);
|
|
@@ -193,6 +183,17 @@ static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
|
|
|
193
183
|
}
|
|
194
184
|
return 0;
|
|
195
185
|
}
|
|
186
|
+
|
|
187
|
+
/* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
|
|
188
|
+
component of the path is refused, as an unwritable file is, rather than
|
|
189
|
+
followed to whatever file it names. `nish file.ts` writes `file.ll` beside
|
|
190
|
+
its source, so a link planted at that name in a checkout would otherwise
|
|
191
|
+
truncate any file the user can write. A link among the directories on the
|
|
192
|
+
way is still followed.
|
|
193
|
+
|
|
194
|
+
`O_CLOEXEC` for the reason the read has it (RT-12), and the bytes go out
|
|
195
|
+
through `nish_put_all`, so a write that fails is `cannot write`, never a
|
|
196
|
+
silently short file. */
|
|
196
197
|
static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
|
|
197
198
|
int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
|
|
198
199
|
if (fd < 0 || nish_put_all(fd, data->data, data->len) < 0) nish_io_fail("write ", path);
|
|
@@ -248,6 +249,73 @@ int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes) {
|
|
|
248
249
|
return r < 0 ? r : (int32_t)bytes->len;
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/* ---- Files and directories with an explicit mode (docs/security/runtime.md,
|
|
253
|
+
RT-24 to RT-26): `writeFileSyncMode`, `createFileExclusiveSync` and
|
|
254
|
+
`mkdirSyncMode`. Each answers a value and never exits, as the writers just
|
|
255
|
+
above do. A mode outside 0..0777 is -EINVAL before any system call: the
|
|
256
|
+
setuid, setgid and sticky bits are refused, not applied, so the mode asked
|
|
257
|
+
for is the mode the file has. WASI
|
|
258
|
+
has no file modes to apply, so there each answers -ENOSYS and creates
|
|
259
|
+
nothing rather than a file whose mode was not set. */
|
|
260
|
+
|
|
261
|
+
/* The two file writers' shared body: `open(p, O_WRONLY | O_CREAT |
|
|
262
|
+
O_NOFOLLOW | O_CLOEXEC | flags, mode)`, then `fchmod(fd, mode)` before a
|
|
263
|
+
byte is written, so the file is exactly `mode` whatever the umask and
|
|
264
|
+
whatever mode a file already there had: no reader that opens it while it is
|
|
265
|
+
written finds it wider (RT-24). Then the data through `nish_put_all` and a
|
|
266
|
+
checked `close`, as `nish_write_file_bytes` does. The open truncates
|
|
267
|
+
before the `fchmod`, so a file there that this process may write but does
|
|
268
|
+
not own is emptied and then answers -EPERM. When `flags` holds `O_EXCL`,
|
|
269
|
+
any failure after the create unlinks the file this call made, so a lock
|
|
270
|
+
whose write failed does not stay taken. */
|
|
271
|
+
static int32_t nish_put_file_mode(const nish_str *path, const nish_str *data, int flags, int32_t mode) {
|
|
272
|
+
const char *p = nish_cpath(path);
|
|
273
|
+
if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
|
|
274
|
+
if (data->len > NISH_LENGTH_MAX) return -EFBIG;
|
|
275
|
+
#ifdef __wasi__
|
|
276
|
+
(void)flags;
|
|
277
|
+
return -ENOSYS;
|
|
278
|
+
#else
|
|
279
|
+
int fd = open(p, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, (mode_t)mode);
|
|
280
|
+
if (fd < 0) return -errno;
|
|
281
|
+
int32_t r = fchmod(fd, (mode_t)mode) == 0 ? 0 : -errno;
|
|
282
|
+
if (r == 0) r = nish_put_all(fd, data->data, data->len);
|
|
283
|
+
if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
|
|
284
|
+
if (r < 0 && (flags & O_EXCL)) unlink(p);
|
|
285
|
+
return r < 0 ? r : (int32_t)data->len;
|
|
286
|
+
#endif
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* `writeFileSyncMode(path, data, mode)`: `writeFileSync` with the mode given
|
|
290
|
+
and an answer instead of an exit. A link at the path is refused (-ELOOP on
|
|
291
|
+
Linux) and the file it names is untouched (RT-25). */
|
|
292
|
+
int32_t nish_write_file_mode(const nish_str *path, const nish_str *data, int32_t mode) {
|
|
293
|
+
return nish_put_file_mode(path, data, O_TRUNC, mode);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* `createFileExclusiveSync(path, data, mode)`: `O_EXCL`, so the path is
|
|
297
|
+
created by this call or the call answers -EEXIST, a link there included,
|
|
298
|
+
dangling or not. Of N processes racing for one path exactly one wins, which
|
|
299
|
+
is what a lock file needs; a winner whose write then fails removes the file
|
|
300
|
+
and answers the error. */
|
|
301
|
+
int32_t nish_create_file_exclusive(const nish_str *path, const nish_str *data, int32_t mode) {
|
|
302
|
+
return nish_put_file_mode(path, data, O_EXCL, mode);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* `mkdirSyncMode(path, mode)`: mkdir(2) with `mode`, one directory, not
|
|
306
|
+
recursive. A path already there is -EEXIST and its mode is left as it was.
|
|
307
|
+
The kernel applies the umask, so the directory is `mode & ~umask`: 0700 is
|
|
308
|
+
exact under any umask that leaves the owner's bits (RT-26). */
|
|
309
|
+
int32_t nish_mkdir_mode(const nish_str *path, int32_t mode) {
|
|
310
|
+
const char *p = nish_cpath(path);
|
|
311
|
+
if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
|
|
312
|
+
#ifdef __wasi__
|
|
313
|
+
return -ENOSYS;
|
|
314
|
+
#else
|
|
315
|
+
return mkdir(p, (mode_t)mode) == 0 ? 0 : -errno;
|
|
316
|
+
#endif
|
|
317
|
+
}
|
|
318
|
+
|
|
251
319
|
/* `writeBytes(bytes, off, len)` (nish:io): the bytes `[off, off + len)` of a
|
|
252
320
|
`u8[]` on stdout, through `nish_put_all`, so a short write is finished and `EINTR` retried. The
|
|
253
321
|
emitter has range-checked `off` and `len` against `bytes->len`
|
|
@@ -290,17 +358,19 @@ _Bool nish_mkdir(const nish_str *path) {
|
|
|
290
358
|
value rather than through a child that has already run.
|
|
291
359
|
|
|
292
360
|
`out` and `err` are paths for the child's stdout and stderr, or NULL to let
|
|
293
|
-
it inherit this process's
|
|
361
|
+
it inherit this process's; `dup`, when not NULL, is three descriptors the
|
|
362
|
+
child takes as its stdin, stdout and stderr instead (`spawnPiped`'s pipes). Every spawn builtin starts its child here, so the
|
|
294
363
|
argv vector and the redirects are written once; it is `static`, so a program
|
|
295
364
|
that spawns nothing loses it to `--gc-sections`. The answer is the child's
|
|
296
365
|
pid, or a negative errno: -EINVAL for an empty argv or an argument holding a
|
|
297
366
|
NUL, and `posix_spawnp`'s own error otherwise (-ENOENT for a command that is
|
|
298
367
|
not on PATH, on glibc). */
|
|
299
|
-
static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
368
|
+
static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup) {
|
|
300
369
|
#ifdef __wasi__
|
|
301
370
|
(void)argv;
|
|
302
371
|
(void)out;
|
|
303
372
|
(void)err;
|
|
373
|
+
(void)dup;
|
|
304
374
|
return -ENOSYS;
|
|
305
375
|
#else
|
|
306
376
|
if (!argv->len) return -EINVAL; /* argv[0] would read past an empty array */
|
|
@@ -323,6 +393,13 @@ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, con
|
|
|
323
393
|
posix_spawn_file_actions_t fa;
|
|
324
394
|
int failed = posix_spawn_file_actions_init(&fa);
|
|
325
395
|
if (failed) return -failed;
|
|
396
|
+
/* A `dup2` action that could not be recorded would start the child on this
|
|
397
|
+
process's own stream in its place, so it fails the spawn. */
|
|
398
|
+
for (int k = 0; dup && k < 3 && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], k);
|
|
399
|
+
if (failed) {
|
|
400
|
+
posix_spawn_file_actions_destroy(&fa);
|
|
401
|
+
return -failed;
|
|
402
|
+
}
|
|
326
403
|
const nish_str *to[2] = { out, err };
|
|
327
404
|
for (int k = 0; k < 2; k++) {
|
|
328
405
|
/* The child opens the file, not this process: a redirect that this process
|
|
@@ -348,7 +425,7 @@ static int32_t nish_exit_code(int status) {
|
|
|
348
425
|
/* The two blocking builtins: start, then wait for that one child. A failed
|
|
349
426
|
start is -1, the answer they have always given. */
|
|
350
427
|
static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
351
|
-
int32_t pid = nish_spawn_start(argv, out, err);
|
|
428
|
+
int32_t pid = nish_spawn_start(argv, out, err, 0);
|
|
352
429
|
if (pid < 0) return -1;
|
|
353
430
|
#ifdef __wasi__
|
|
354
431
|
return -1;
|
|
@@ -379,7 +456,160 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
|
|
|
379
456
|
/* `spawn(argv)`: the child's pid, or a negative errno, without waiting. The
|
|
380
457
|
child is this process's until a `waitPid` reaps it; one never reaped stays
|
|
381
458
|
a zombie until the program exits, as it would in C. */
|
|
382
|
-
int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0); }
|
|
459
|
+
int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0); }
|
|
460
|
+
|
|
461
|
+
/* `spawnPiped(argv, stdin, stdout, stderr)`: run the child as `nish_spawn`
|
|
462
|
+
does, with `stdin`'s bytes on its standard input and its standard output
|
|
463
|
+
and error appended to `stdout` and `stderr`, and answer its status. -1 is
|
|
464
|
+
`nish_spawn`'s failure (an empty argv, a NUL, a child that cannot start or
|
|
465
|
+
be waited for, a pipe or `poll` that fails), and -2 is a stream that passed
|
|
466
|
+
NISH_PIPED_CAP bytes: the child is killed with SIGKILL and reaped, and the
|
|
467
|
+
array holds the first NISH_PIPED_CAP bytes of that stream.
|
|
468
|
+
|
|
469
|
+
The three pipes are close-on-exec, so no other child this process starts
|
|
470
|
+
inherits an end, and each end is moved off descriptors 0 to 2, where a
|
|
471
|
+
`dup2` onto itself would leave close-on-exec set and the child without
|
|
472
|
+
that stream. Darwin has no `pipe2`, so there an end is close-on-exec only
|
|
473
|
+
once `F_DUPFD_CLOEXEC` has moved it: a child another thread starts in
|
|
474
|
+
that window inherits it (docs/security/runtime.md, RT-30).
|
|
475
|
+
|
|
476
|
+
One `poll` loop over non-blocking ends writes stdin and reads both outputs
|
|
477
|
+
as each is ready, so a child that fills its stdout before it reads all its
|
|
478
|
+
stdin, or the reverse, cannot deadlock against this process. A child that
|
|
479
|
+
exits, or closes its stdin, before reading everything ends the writing
|
|
480
|
+
with EPIPE rather than SIGPIPE: the signal is blocked on this thread once
|
|
481
|
+
the child has started, the one a write raised is taken off the pending
|
|
482
|
+
set, and the mask is put back before the answer (docs/security/runtime.md,
|
|
483
|
+
RT-29). Ignoring it instead would be process-wide, and every child
|
|
484
|
+
started meanwhile, this one included, would inherit SIGPIPE ignored and
|
|
485
|
+
never die of a reader that has gone.
|
|
486
|
+
|
|
487
|
+
The bytes are written from the caller's array as they stand, and nothing
|
|
488
|
+
copies them: the only copies are the kernel's pipe buffer and the child.
|
|
489
|
+
The length is read once, so `stdout` being the same array as `stdin`
|
|
490
|
+
appends to it without feeding the output back in, and the pointer at each
|
|
491
|
+
write, because that array moves when it grows and its stdin bytes stay
|
|
492
|
+
its first `n`. */
|
|
493
|
+
#define NISH_PIPED_CAP (16u << 20)
|
|
494
|
+
#ifndef __wasi__
|
|
495
|
+
static int nish_pipe(int *p) {
|
|
496
|
+
#ifdef __linux__
|
|
497
|
+
if (pipe2(p, O_CLOEXEC)) return -1;
|
|
498
|
+
#else
|
|
499
|
+
if (pipe(p)) return -1;
|
|
500
|
+
#endif
|
|
501
|
+
for (int k = 0; k < 2; k++) {
|
|
502
|
+
#ifdef __linux__
|
|
503
|
+
if (p[k] > 2) continue;
|
|
504
|
+
#endif
|
|
505
|
+
int moved = fcntl(p[k], F_DUPFD_CLOEXEC, 3);
|
|
506
|
+
close(p[k]);
|
|
507
|
+
p[k] = moved;
|
|
508
|
+
if (moved < 0) return -1;
|
|
509
|
+
}
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#endif
|
|
514
|
+
|
|
515
|
+
int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_array *out, nish_array *err) {
|
|
516
|
+
#ifdef __wasi__
|
|
517
|
+
(void)argv;
|
|
518
|
+
(void)in;
|
|
519
|
+
(void)out;
|
|
520
|
+
(void)err;
|
|
521
|
+
return -1;
|
|
522
|
+
#else
|
|
523
|
+
uint64_t n = in->len, sent = 0, got[3] = { 0, 0, 0 };
|
|
524
|
+
int p[6] = { -1, -1, -1, -1, -1, -1 };
|
|
525
|
+
int32_t rc = -1;
|
|
526
|
+
if (nish_pipe(p) || nish_pipe(p + 2) || nish_pipe(p + 4)) goto done;
|
|
527
|
+
int child[3] = { p[0], p[3], p[5] };
|
|
528
|
+
int32_t pid = nish_spawn_start(argv, 0, 0, child);
|
|
529
|
+
/* The child holds its ends now; this process keeps only its own. */
|
|
530
|
+
for (int k = 0; k < 3; k++) {
|
|
531
|
+
close(child[k]);
|
|
532
|
+
p[k ? 2 * k + 1 : 0] = -1;
|
|
533
|
+
}
|
|
534
|
+
if (pid < 0) goto done;
|
|
535
|
+
sigset_t pipe_sig, mask, pending;
|
|
536
|
+
sigemptyset(&pipe_sig);
|
|
537
|
+
sigaddset(&pipe_sig, SIGPIPE);
|
|
538
|
+
pthread_sigmask(SIG_BLOCK, &pipe_sig, &mask);
|
|
539
|
+
sigpending(&pending);
|
|
540
|
+
int queued = sigismember(&pending, SIGPIPE);
|
|
541
|
+
struct pollfd f[3] = { { p[1], POLLOUT, 0 }, { p[2], POLLIN, 0 }, { p[4], POLLIN, 0 } };
|
|
542
|
+
p[1] = p[2] = p[4] = -1;
|
|
543
|
+
/* A blocking end could stall the loop on one stream while the child fills
|
|
544
|
+
another, so an end that stays blocking fails the run: the loop is skipped
|
|
545
|
+
and the child killed. */
|
|
546
|
+
rc = 0;
|
|
547
|
+
for (int k = 0; k < 3; k++)
|
|
548
|
+
if (fcntl(f[k].fd, F_SETFL, O_NONBLOCK) < 0) rc = -1;
|
|
549
|
+
if (!n) {
|
|
550
|
+
close(f[0].fd);
|
|
551
|
+
f[0].fd = -1;
|
|
552
|
+
}
|
|
553
|
+
nish_array *to[3] = { 0, out, err };
|
|
554
|
+
while (!rc && (f[0].fd >= 0 || f[1].fd >= 0 || f[2].fd >= 0)) {
|
|
555
|
+
if (poll(f, 3, -1) < 0) {
|
|
556
|
+
if (errno != EINTR) rc = -1;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (f[0].revents) {
|
|
560
|
+
ssize_t k = write(f[0].fd, in->data + sent, n - sent);
|
|
561
|
+
if (k > 0) sent += k;
|
|
562
|
+
if (sent == n || (k < 0 && errno != EINTR && errno != EAGAIN)) {
|
|
563
|
+
close(f[0].fd);
|
|
564
|
+
f[0].fd = -1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (int s = 1; s < 3 && !rc; s++) {
|
|
568
|
+
if (!f[s].revents) continue;
|
|
569
|
+
nish_array *a = to[s];
|
|
570
|
+
/* Grown as `push` grows it, so the array keeps the shape every other
|
|
571
|
+
array has, and an ASan build tracks the block the same way. */
|
|
572
|
+
if (a->len == a->cap) nish_array_grow(a, 1);
|
|
573
|
+
/* One byte past the cap at most, which is how a stream that ends at
|
|
574
|
+
exactly the cap is told from one that goes on. */
|
|
575
|
+
uint64_t room = a->cap - a->len, left = NISH_PIPED_CAP + 1 - got[s];
|
|
576
|
+
ssize_t k = read(f[s].fd, a->data + a->len, room < left ? room : left);
|
|
577
|
+
if (k > 0) {
|
|
578
|
+
a->len += k;
|
|
579
|
+
got[s] += k;
|
|
580
|
+
if (got[s] > NISH_PIPED_CAP) {
|
|
581
|
+
a->len--;
|
|
582
|
+
rc = -2;
|
|
583
|
+
}
|
|
584
|
+
} else if (!k || (errno != EINTR && errno != EAGAIN)) {
|
|
585
|
+
close(f[s].fd);
|
|
586
|
+
f[s].fd = -1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
sigpending(&pending);
|
|
591
|
+
if (!queued && sigismember(&pending, SIGPIPE)) {
|
|
592
|
+
int sig;
|
|
593
|
+
sigwait(&pipe_sig, &sig);
|
|
594
|
+
}
|
|
595
|
+
pthread_sigmask(SIG_SETMASK, &mask, 0);
|
|
596
|
+
if (rc) kill(pid, SIGKILL);
|
|
597
|
+
for (int k = 0; k < 3; k++)
|
|
598
|
+
if (f[k].fd >= 0) close(f[k].fd);
|
|
599
|
+
int status;
|
|
600
|
+
while (waitpid(pid, &status, 0) < 0) {
|
|
601
|
+
if (errno != EINTR) {
|
|
602
|
+
rc = -1;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!rc) rc = nish_exit_code(status);
|
|
607
|
+
done:
|
|
608
|
+
for (int k = 0; k < 6; k++)
|
|
609
|
+
if (p[k] >= 0) close(p[k]);
|
|
610
|
+
return rc;
|
|
611
|
+
#endif
|
|
612
|
+
}
|
|
383
613
|
|
|
384
614
|
/* `kill(pid, sig)`: 0, or a negative errno. Only the four signals a program
|
|
385
615
|
asks a child to stop or report with are sent -- 0 (is it there), SIGINT,
|
|
@@ -545,15 +775,20 @@ int64_t nish_total_memory(void) {
|
|
|
545
775
|
}
|
|
546
776
|
|
|
547
777
|
/* `setNice(n)`: this process's nice value set to `n`, 0 or a negative errno.
|
|
548
|
-
|
|
549
|
-
below
|
|
550
|
-
|
|
551
|
-
so it is the calling thread's and every thread started after it. */
|
|
778
|
+
Lower-only, even as root: `n` takes 0..19 (-EINVAL outside it, with no system
|
|
779
|
+
call), and a value below the current one is -EACCES (Linux, Darwin) without
|
|
780
|
+
a `setpriority` call, so the priority never rises. Linux keeps the value per
|
|
781
|
+
thread, so it is the calling thread's and every thread started after it. */
|
|
552
782
|
int32_t nish_set_nice(int32_t n) {
|
|
553
783
|
#ifdef __wasi__
|
|
554
784
|
(void)n;
|
|
555
785
|
return -ENOSYS;
|
|
556
786
|
#else
|
|
787
|
+
if (n < 0 || n > 19) return -EINVAL;
|
|
788
|
+
errno = 0;
|
|
789
|
+
int current = getpriority(PRIO_PROCESS, 0);
|
|
790
|
+
if (current == -1 && errno != 0) return -errno;
|
|
791
|
+
if (n < current) return -EACCES;
|
|
557
792
|
return setpriority(PRIO_PROCESS, 0, n) == 0 ? 0 : -errno;
|
|
558
793
|
#endif
|
|
559
794
|
}
|
package/runtime/runtime-simd.c
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Nish runtime, the vector half: byte searches that read sixteen or thirty-two
|
|
2
|
-
* bytes at a time (WP38 S1, docs/wp38-simd.md §3.1).
|
|
2
|
+
* bytes at a time (WP38 S1, docs/wp38-simd.md §3.1), and the SSE4.1 rounding
|
|
3
|
+
* behind `Math.floor`, `Math.ceil` and `Math.trunc` (#943, at the end).
|
|
3
4
|
*
|
|
4
5
|
* A translation unit of its own for the reason runtime-host.c is one: each file
|
|
5
6
|
* carries its own measured `.text*` ceiling in tests/runtime-budgets.js, and
|
|
@@ -176,10 +177,17 @@ static int nish_cpu_has_avx2(void) {
|
|
|
176
177
|
`base` never reads the CPU's feature bits at all. */
|
|
177
178
|
enum { NISH_PATH_SCALAR, NISH_PATH_BASE, NISH_PATH_AVX2 };
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
/* The path `NISH_SIMD` pins, or -1 when it pins none. */
|
|
181
|
+
static int nish_simd_pin(void) {
|
|
180
182
|
const char *pin = getenv("NISH_SIMD");
|
|
181
183
|
if (pin != NULL && strcmp(pin, "scalar") == 0) return NISH_PATH_SCALAR;
|
|
182
184
|
if (pin != NULL && strcmp(pin, "base") == 0) return NISH_PATH_BASE;
|
|
185
|
+
return -1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static int nish_simd_choose(void) {
|
|
189
|
+
int pin = nish_simd_pin();
|
|
190
|
+
if (pin >= 0) return pin;
|
|
183
191
|
#if defined(NISH_SIMD_SSE2)
|
|
184
192
|
if (nish_cpu_has_avx2()) return NISH_PATH_AVX2;
|
|
185
193
|
#endif
|
|
@@ -425,3 +433,56 @@ int64_t nish_regex_scan(const nish_str *s, const nish_array *masks, int64_t dept
|
|
|
425
433
|
int64_t from) {
|
|
426
434
|
return __atomic_load_n(&nish_scan, __ATOMIC_RELAXED)(s, masks, depth, off, width, from);
|
|
427
435
|
}
|
|
436
|
+
|
|
437
|
+
/* `Math.floor`, `Math.ceil` and `Math.trunc`, and the floor `Math.round` takes
|
|
438
|
+
(#943), the contract in nish.h. SSE4.1's `roundsd` answers each in one
|
|
439
|
+
instruction, but the x86-64 baseline is SSE2, where LLVM lowers
|
|
440
|
+
`llvm.floor.f64` to a call of libm's `floor` through the GOT. So a module
|
|
441
|
+
for x86-64 calls these instead (`roundingCallee`, src/emit-builtins.ts) and
|
|
442
|
+
registers a constructor that hands `nish_round_init` libm's three routines;
|
|
443
|
+
it asks the CPU once, before `main`, and keeps them only when the answer is
|
|
444
|
+
no. The runtime names no libm symbol itself, so a link without `-lm` (a C
|
|
445
|
+
host, a program that never rounds) still resolves: the module that rounds
|
|
446
|
+
brings the reference, as it did before. A null slot is the instruction and
|
|
447
|
+
is never written after the constructor, so on a CPU without SSE4.1 each
|
|
448
|
+
call is an indirect jump to libm, the path a module took before.
|
|
449
|
+
`NISH_SIMD=scalar` keeps libm, the forced fallback a test runs. `base` does
|
|
450
|
+
not: the instruction gate counts under it, and glibc's own `floor` takes
|
|
451
|
+
`roundsd` there as well, so the count is of the instruction both call.
|
|
452
|
+
Built for a machine that has SSE4.1 already (`--cpu x86-64-v3`), or for any
|
|
453
|
+
other, each is the builtin, which is the one instruction there, and the
|
|
454
|
+
constructor keeps nothing. The immediate is the rounding mode, 1 down, 2 up,
|
|
455
|
+
3 toward zero, plus 8, which keeps the precision exception quiet as libm's
|
|
456
|
+
`floor` does. */
|
|
457
|
+
#if defined(NISH_SIMD_SSE2) && !defined(__SSE4_1__)
|
|
458
|
+
static double (*nish_round_libm[3])(double);
|
|
459
|
+
|
|
460
|
+
void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
|
|
461
|
+
unsigned a, b, c, d;
|
|
462
|
+
if (nish_simd_pin() != NISH_PATH_SCALAR && __get_cpuid(1, &a, &b, &c, &d) && (c & bit_SSE4_1)) return;
|
|
463
|
+
nish_round_libm[0] = floor_fn;
|
|
464
|
+
nish_round_libm[1] = ceil_fn;
|
|
465
|
+
nish_round_libm[2] = trunc_fn;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#define NISH_ROUNDING(name, base, slot, mode) \
|
|
469
|
+
double name(double x) { \
|
|
470
|
+
double (*libm)(double) = nish_round_libm[slot]; \
|
|
471
|
+
if (libm) return libm(x); \
|
|
472
|
+
__asm__("roundsd $" #mode ", %0, %0" : "+x"(x)); \
|
|
473
|
+
return x; \
|
|
474
|
+
}
|
|
475
|
+
#else
|
|
476
|
+
void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
|
|
477
|
+
(void)floor_fn;
|
|
478
|
+
(void)ceil_fn;
|
|
479
|
+
(void)trunc_fn;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#define NISH_ROUNDING(name, base, slot, mode) \
|
|
483
|
+
double name(double x) { return __builtin_##base(x); }
|
|
484
|
+
#endif
|
|
485
|
+
|
|
486
|
+
NISH_ROUNDING(nish_floor, floor, 0, 9)
|
|
487
|
+
NISH_ROUNDING(nish_ceil, ceil, 1, 10)
|
|
488
|
+
NISH_ROUNDING(nish_trunc, trunc, 2, 11)
|
package/runtime/runtime-wasm.c
CHANGED
|
@@ -128,3 +128,21 @@ void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
|
|
|
128
128
|
void nish_panic(const void *s) { (void)s; __builtin_trap(); }
|
|
129
129
|
/* Output has nowhere to go either. */
|
|
130
130
|
void nish_write(const void *s, int32_t fd, _Bool newline) { (void)s; (void)fd; (void)newline; }
|
|
131
|
+
|
|
132
|
+
/* ---- Rounding (#943): IR compiled for the host calls these and registers a
|
|
133
|
+
* constructor that passes `nish_round_init` libm's three routines
|
|
134
|
+
* (runtime-simd.c has the native ones). On wasm32 each is the one
|
|
135
|
+
* instruction, f64.floor, f64.ceil or f64.trunc, and there is no CPU to ask.
|
|
136
|
+
* The link is freestanding, so the libm names the constructor takes are
|
|
137
|
+
* defined here too, as the same instructions. */
|
|
138
|
+
void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
|
|
139
|
+
(void)floor_fn;
|
|
140
|
+
(void)ceil_fn;
|
|
141
|
+
(void)trunc_fn;
|
|
142
|
+
}
|
|
143
|
+
double nish_floor(double x) { return __builtin_floor(x); }
|
|
144
|
+
double nish_ceil(double x) { return __builtin_ceil(x); }
|
|
145
|
+
double nish_trunc(double x) { return __builtin_trunc(x); }
|
|
146
|
+
double floor(double x) { return __builtin_floor(x); }
|
|
147
|
+
double ceil(double x) { return __builtin_ceil(x); }
|
|
148
|
+
double trunc(double x) { return __builtin_trunc(x); }
|
package/scripts/build.sh
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
# threads, <dir>/runtime-cross.c, the copies that carry a string or an array
|
|
11
11
|
# from one thread's arena to another's, <dir>/runtime-host.c, the wall clock, entropy, file times and
|
|
12
12
|
# signals, <dir>/runtime-net.c, the sockets of `nish:net`,
|
|
13
|
-
# <dir>/runtime-simd.c, the byte searches that read a vector at a time
|
|
13
|
+
# <dir>/runtime-simd.c, the byte searches that read a vector at a time and
|
|
14
|
+
# the SSE4.1 rounding of `Math.floor` and its kin,
|
|
14
15
|
# <dir>/runtime-aes.c, AES-GCM on the CPU's AES instructions,
|
|
15
16
|
# <dir>/runtime-term.c, stdin and the terminal of `nish:term`, and
|
|
16
17
|
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`. Each of those
|
|
@@ -665,6 +666,20 @@ case "$profile" in
|
|
|
665
666
|
if [ "$bytes_wanted" = 1 ] && [ "$bytes_named" = 0 ]; then
|
|
666
667
|
inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-bytes.c")
|
|
667
668
|
fi
|
|
669
|
+
# IR compiled for the host rounds through runtime-simd.c's `nish_floor` and its
|
|
670
|
+
# kin behind the constructor `nish_round_init` (#943). runtime-wasm.c has their
|
|
671
|
+
# wasm32 bodies, and it is added the same way when such IR does not name it.
|
|
672
|
+
wasm_named=0
|
|
673
|
+
round_wanted=0
|
|
674
|
+
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
675
|
+
case "$i" in
|
|
676
|
+
*/runtime-wasm.c|runtime-wasm.c) wasm_named=1 ;;
|
|
677
|
+
*.ll) if grep -q '@nish_round_init' "$i"; then round_wanted=1; fi ;;
|
|
678
|
+
esac
|
|
679
|
+
done
|
|
680
|
+
if [ "$round_wanted" = 1 ] && [ "$wasm_named" = 0 ]; then
|
|
681
|
+
inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-wasm.c")
|
|
682
|
+
fi
|
|
668
683
|
# -mbulk-memory lowers llvm.memset/memcpy (`new Array<T>(n)`, `push` growth) to the
|
|
669
684
|
# memory.fill/memory.copy instructions instead of libc calls the freestanding link lacks.
|
|
670
685
|
"$CC" -Wno-override-module --target=wasm32-unknown-unknown -Oz -nostdlib -mbulk-memory \
|
package/std/README.md
CHANGED
|
@@ -225,6 +225,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
225
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 |
|
|
226
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
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`) |
|
|
228
|
+
| [`net/http-client.ts`](./net/http-client.ts) | A blocking HTTP/1.1 client for command-line programs: `httpFetch(request, options)` looks the host up, connects, sends an `HttpRequest` (`method`, `url`, `header(name, value)`, `body`) and answers an `HttpResponse` (`status`, `reason`, `url`, `header(name)`) once its head is in, its body read through `body.read` or `body.readAll(limit)`; null on a failure. `HttpClient(options)` does the same and keeps one connection alive between fetches, sending an idempotent request once more when a kept connection turns out to be gone, and says why a fetch failed in `error` (an `HTTP_E_*` code, `httpErrorName`) and `errorText`. `HttpOptions` bounds every wait (`connectTimeoutMs`, `readTimeoutMs`, `totalTimeoutMs`) and every body (`maxBodyBytes`), follows redirects only when asked (`followRedirects`, `maxRedirects`, `crossOriginRedirects`, never https to http, credentials dropped across origins), and reads `http_proxy`, `https_proxy` and `no_proxy` when `useEnvProxy` is set (`HTTP_PROXY` ignored under CGI; https through `CONNECT`). Every byte goes through `nish/net/http1-client`. Native only. Record: [`docs/security/http-client.md`](../docs/security/http-client.md) | RFC 9110 §9.2.2 and §15.4, RFC 9112; a loopback server and proxy in the test itself: keep-alive, retries, redirects, proxies, timeouts and body caps (`tests/link/net_http_fetch`) |
|
|
228
229
|
| [`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 |
|
|
229
230
|
| [`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 |
|
|
230
231
|
| [`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 |
|
|
@@ -239,7 +240,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
239
240
|
| [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
|
|
240
241
|
| [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`). Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
|
|
241
242
|
| [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
|
|
242
|
-
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
243
|
+
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
243
244
|
|
|
244
245
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
245
246
|
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|
|
@@ -496,7 +497,7 @@ have, and all three have since landed as builtins:
|
|
|
496
497
|
| Was missing | Now |
|
|
497
498
|
| --- | --- |
|
|
498
499
|
| a directory listing | `readdirSync(path): string[] \| null`, sorted by bytes, because there is no `sort` for a caller to reach for |
|
|
499
|
-
| a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting |
|
|
500
|
+
| a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting; `spawnPiped(argv, stdin, stdout, stderr)`, stdin from a `u8[]` and both outputs into `u8[]`s, for a secret the child reads on stdin |
|
|
500
501
|
| a clock | `monotonicNanos(): i64` |
|
|
501
502
|
|
|
502
503
|
So the driver exists, in the language, and it is
|