@amritk/nish-x86_64-linux 0.29.0 → 0.31.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/INSTALL.md +3 -3
- package/bin/nish +0 -0
- package/docs/AI.md +71 -4
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -0
- package/runtime/core/host.mjs +9 -0
- package/runtime/nish.d.ts +48 -2
- package/runtime/nish.h +31 -3
- package/runtime/nish.mjs +36 -0
- package/runtime/runtime-net.c +32 -3
- package/runtime/runtime-os.c +33 -1
- package/runtime/runtime-region.c +243 -0
- package/runtime/runtime.c +19 -2
- package/scripts/build.sh +19 -6
- package/std/README.md +10 -7
- package/std/net/{http-client.ts → fetch.ts} +17 -17
- package/std/net/handler.ts +1 -1
- package/std/net/{http1-client.ts → http1-client-codec.ts} +276 -47
- package/std/net/{http1.ts → http1-codec.ts} +252 -49
- package/std/net/http1-server.ts +72 -13
- package/std/net/outbound.ts +2 -2
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +680 -53
- package/std/net/prefork.ts +149 -13
- package/std/net/router.ts +1 -1
- package/std/net/upstream.ts +759 -0
- package/std/net/websocket.ts +5 -5
- package/std/region.ts +95 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/* Nish runtime, the region half: arenas a program makes, enters, leaves and
|
|
2
|
+
* frees one at a time, so a long-running program can give back the memory of
|
|
3
|
+
* one session, one cache entry or one connection without a garbage collector
|
|
4
|
+
* (docs/wp46-long-running-memory.md, section 2.1, "Runtime and ABI").
|
|
5
|
+
*
|
|
6
|
+
* A translation unit of its own, paired with runtime.c in scripts/build.sh's
|
|
7
|
+
* `halves` and src/run-cache.ts's `runtimeUnits()` like the others, so it has
|
|
8
|
+
* its own measured `.text*` ceiling in tests/runtime-budgets.js and a program
|
|
9
|
+
* that never makes a region links none of it. The wasm profile has no malloc
|
|
10
|
+
* to give a region its chunks, so `scripts/build.sh --profile wasm` refuses IR
|
|
11
|
+
* that names `@nish_region_`; the wasi profile links this file.
|
|
12
|
+
*
|
|
13
|
+
* **A handle is a number.** `nish_region_new` answers an `i64`: the slot's
|
|
14
|
+
* index in the low 24 bits and the slot's generation in the 40 above them.
|
|
15
|
+
* Generations start at 1, so 0 -- what a fresh `Region<T>[]` holds -- is never
|
|
16
|
+
* a handle. `nish_region_free` moves the slot's generation on, so a copy of a
|
|
17
|
+
* freed handle is stale; a slot whose generation would pass 40 bits is
|
|
18
|
+
* retired rather than reused, so no handle ever names a slot twice. Every
|
|
19
|
+
* call that takes a handle checks it first and panics on a zero handle, a
|
|
20
|
+
* stale one, and one made on another thread. The 24-bit index caps a process
|
|
21
|
+
* at 16,777,216 live regions; `nish_region_new` past that panics.
|
|
22
|
+
*
|
|
23
|
+
* **The table** is one per process: a directory of pages, each page 1,024
|
|
24
|
+
* slots allocated (zeroed) the first time a slot in it is handed out, so a
|
|
25
|
+
* program that makes ten regions holds one page. Taking a slot and giving one
|
|
26
|
+
* back take the table's lock, once per `new` and once per `free`; `enter` and
|
|
27
|
+
* `leave` take none, because a slot is only ever entered by the thread that
|
|
28
|
+
* made it and the check reads the two fields another thread's `free` could
|
|
29
|
+
* write (generation and thread) with atomic loads. Only a threads build
|
|
30
|
+
* (`-DNISH_THREADS`) has a second thread to lock against, so only it pays for
|
|
31
|
+
* the lock.
|
|
32
|
+
*
|
|
33
|
+
* **Entering swaps arenas.** A slot holds a whole `struct nish_arena`.
|
|
34
|
+
* `nish_region_enter` swaps it with the thread's `nish_arena`, so every
|
|
35
|
+
* allocation the program makes until `nish_region_leave` -- the inlined bump
|
|
36
|
+
* as well as the runtime's own -- lands in the region; `leave` swaps them
|
|
37
|
+
* back. While a region is entered its slot holds the arena it displaced, so
|
|
38
|
+
* entering a second region inside the first nests: each leave restores what
|
|
39
|
+
* its enter took. A slot that is entered cannot be entered again (it would
|
|
40
|
+
* swap its own arena with itself) or freed, and `leave` must name the region
|
|
41
|
+
* entered last on this thread.
|
|
42
|
+
*
|
|
43
|
+
* **Chunk size.** A thread's own arena grows by 64 KiB chunks, which is right
|
|
44
|
+
* for one arena a thread and wrong for a hundred thousand of them. A region's
|
|
45
|
+
* first chunk is the size of its first allocation, and each later chunk
|
|
46
|
+
* doubles, or takes the allocation's own size when that is more. The size of
|
|
47
|
+
* the next chunk is runtime.c's thread-local `nish_region_chunk`, which
|
|
48
|
+
* `nish_arena_grow` reads on its slow path and which `enter` and `leave` swap
|
|
49
|
+
* with the slot's as they swap the arena; 0 there means the thread's own
|
|
50
|
+
* arena and the 64 KiB floor. A region starts it at 8, the smallest block.
|
|
51
|
+
*
|
|
52
|
+
* **The root.** `Region<T>` is only the number, so the slot keeps the root
|
|
53
|
+
* object `region`'s function answered: `nish_region_leave` records it and
|
|
54
|
+
* `nish_region_enter` answers it.
|
|
55
|
+
*
|
|
56
|
+
* Freeing a region frees its chunks through runtime.c's `nish_free_arena`,
|
|
57
|
+
* with the region's arena swapped in for the call, so an `-DNISH_ASAN` build
|
|
58
|
+
* forgets the freed blocks as it does for the thread's arena. */
|
|
59
|
+
|
|
60
|
+
#include <stdint.h>
|
|
61
|
+
#include <stdlib.h>
|
|
62
|
+
|
|
63
|
+
#include "nish.h"
|
|
64
|
+
|
|
65
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
66
|
+
|
|
67
|
+
/* runtime.c's: the size of the next chunk `nish_arena_grow` makes for the
|
|
68
|
+
arena in `nish_arena`, or 0 for a thread's own, which takes 64 KiB. */
|
|
69
|
+
extern NISH_TLS uint64_t nish_region_chunk;
|
|
70
|
+
|
|
71
|
+
#define NISH_REGION_SLOT_BITS 24
|
|
72
|
+
#define NISH_REGION_SLOTS ((uint64_t)1 << NISH_REGION_SLOT_BITS)
|
|
73
|
+
#define NISH_REGION_GEN_END ((uint64_t)1 << 40)
|
|
74
|
+
#define NISH_REGION_PAGE 1024
|
|
75
|
+
|
|
76
|
+
typedef struct nish_region_slot {
|
|
77
|
+
struct nish_arena arena; /* the region's arena, or, while entered, the one it displaced */
|
|
78
|
+
uint64_t gen; /* the live handle's generation; NISH_REGION_GEN_END once retired */
|
|
79
|
+
uint64_t thread; /* the thread that made it, 0 while the slot is free */
|
|
80
|
+
uint64_t chunk; /* the region's next chunk size, or, while entered, the displaced one */
|
|
81
|
+
uint64_t outer; /* while entered: the handle entered before it; while free: the next free slot + 1 */
|
|
82
|
+
void *root; /* what `leave` recorded, answered by `enter` */
|
|
83
|
+
uint64_t entered;
|
|
84
|
+
} nish_region_slot;
|
|
85
|
+
|
|
86
|
+
static nish_region_slot *nish_region_pages[NISH_REGION_SLOTS / NISH_REGION_PAGE];
|
|
87
|
+
static uint64_t nish_region_used; /* slots ever handed out: the next never-used index */
|
|
88
|
+
static uint64_t nish_region_free_list; /* a free slot's index + 1, or 0 */
|
|
89
|
+
/* The handle entered last on this thread, or 0. */
|
|
90
|
+
static NISH_TLS uint64_t nish_region_current;
|
|
91
|
+
|
|
92
|
+
#ifdef NISH_THREADS
|
|
93
|
+
static int nish_region_lock_word;
|
|
94
|
+
static uint64_t nish_region_threads;
|
|
95
|
+
static NISH_TLS uint64_t nish_region_thread_id;
|
|
96
|
+
static void nish_region_lock(void) {
|
|
97
|
+
while (__atomic_exchange_n(&nish_region_lock_word, 1, __ATOMIC_ACQUIRE))
|
|
98
|
+
while (__atomic_load_n(&nish_region_lock_word, __ATOMIC_RELAXED)) {}
|
|
99
|
+
}
|
|
100
|
+
static void nish_region_unlock(void) { __atomic_store_n(&nish_region_lock_word, 0, __ATOMIC_RELEASE); }
|
|
101
|
+
/* A number no other thread of the process has: 64 bits, so it never wraps
|
|
102
|
+
back onto a live thread's. */
|
|
103
|
+
static uint64_t nish_region_me(void) {
|
|
104
|
+
if (!nish_region_thread_id) nish_region_thread_id = __atomic_add_fetch(&nish_region_threads, 1, __ATOMIC_RELAXED);
|
|
105
|
+
return nish_region_thread_id;
|
|
106
|
+
}
|
|
107
|
+
#else
|
|
108
|
+
static void nish_region_lock(void) {}
|
|
109
|
+
static void nish_region_unlock(void) {}
|
|
110
|
+
static uint64_t nish_region_me(void) { return 1; }
|
|
111
|
+
#endif
|
|
112
|
+
|
|
113
|
+
/* The panics, by number: one cold function and one table, rather than a
|
|
114
|
+
message's address loaded at each call site. Each ends in `nish_panic`, so
|
|
115
|
+
only the first panic of a process prints and atexit handlers run. */
|
|
116
|
+
enum {
|
|
117
|
+
NISH_REGION_ZERO,
|
|
118
|
+
NISH_REGION_STALE,
|
|
119
|
+
NISH_REGION_THREAD,
|
|
120
|
+
NISH_REGION_ENTERED,
|
|
121
|
+
NISH_REGION_FREE_ENTERED,
|
|
122
|
+
NISH_REGION_LEAVE,
|
|
123
|
+
NISH_REGION_FULL,
|
|
124
|
+
NISH_REGION_OOM,
|
|
125
|
+
};
|
|
126
|
+
#define NISH_REGION_MSG(text) { sizeof text - 1, text }
|
|
127
|
+
static const struct { uint64_t len; char data[72]; } nish_region_messages[] = {
|
|
128
|
+
NISH_REGION_MSG("nish: a zero region handle: no region was made for it"),
|
|
129
|
+
NISH_REGION_MSG("nish: a stale region handle: the region was freed"),
|
|
130
|
+
NISH_REGION_MSG("nish: a region handle made on another thread"),
|
|
131
|
+
NISH_REGION_MSG("nish: enter of a region that is already entered"),
|
|
132
|
+
NISH_REGION_MSG("nish: free of a region that is entered"),
|
|
133
|
+
NISH_REGION_MSG("nish: leave of a region that is not the one entered last"),
|
|
134
|
+
NISH_REGION_MSG("nish: more than 16777216 live regions"),
|
|
135
|
+
NISH_REGION_MSG("nish: out of memory"),
|
|
136
|
+
};
|
|
137
|
+
static NISH_COLD void nish_region_die(int why) {
|
|
138
|
+
nish_panic((const nish_str *)&nish_region_messages[why]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* The slot `r` names, or a panic: zero, stale, another thread's. */
|
|
142
|
+
static nish_region_slot *nish_region_slot_of(uint64_t r) {
|
|
143
|
+
if (!r) nish_region_die(NISH_REGION_ZERO);
|
|
144
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
145
|
+
nish_region_slot *page = __atomic_load_n(&nish_region_pages[index / NISH_REGION_PAGE], __ATOMIC_ACQUIRE);
|
|
146
|
+
if (!page) nish_region_die(NISH_REGION_STALE);
|
|
147
|
+
nish_region_slot *s = &page[index % NISH_REGION_PAGE];
|
|
148
|
+
uint64_t thread = __atomic_load_n(&s->thread, __ATOMIC_RELAXED);
|
|
149
|
+
if (!thread || __atomic_load_n(&s->gen, __ATOMIC_RELAXED) != r >> NISH_REGION_SLOT_BITS)
|
|
150
|
+
nish_region_die(NISH_REGION_STALE);
|
|
151
|
+
if (thread != nish_region_me()) nish_region_die(NISH_REGION_THREAD);
|
|
152
|
+
return s;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Swap the slot's arena and chunk size with the thread's: `enter` and `leave`
|
|
156
|
+
both, and `free` around its `nish_free_arena`. */
|
|
157
|
+
static void nish_region_swap(nish_region_slot *s) {
|
|
158
|
+
struct nish_arena a = nish_arena;
|
|
159
|
+
nish_arena = s->arena;
|
|
160
|
+
s->arena = a;
|
|
161
|
+
uint64_t c = nish_region_chunk;
|
|
162
|
+
nish_region_chunk = s->chunk;
|
|
163
|
+
s->chunk = c;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
uint64_t nish_region_new(void) {
|
|
167
|
+
uint64_t me = nish_region_me();
|
|
168
|
+
nish_region_lock();
|
|
169
|
+
uint64_t index;
|
|
170
|
+
nish_region_slot *s;
|
|
171
|
+
if (nish_region_free_list) {
|
|
172
|
+
index = nish_region_free_list - 1;
|
|
173
|
+
s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
|
|
174
|
+
nish_region_free_list = s->outer;
|
|
175
|
+
} else {
|
|
176
|
+
/* Each panic lets go of the lock first: an atexit handler, or another
|
|
177
|
+
thread while the exit is on its way, may still make or free a region
|
|
178
|
+
(docs/security/runtime.md, RT-46). */
|
|
179
|
+
if (nish_region_used == NISH_REGION_SLOTS) {
|
|
180
|
+
nish_region_unlock();
|
|
181
|
+
nish_region_die(NISH_REGION_FULL);
|
|
182
|
+
}
|
|
183
|
+
/* The slot is counted once its page is there: a failed page leaves the
|
|
184
|
+
count as it was (RT-46). */
|
|
185
|
+
index = nish_region_used;
|
|
186
|
+
nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
|
|
187
|
+
if (!*page) {
|
|
188
|
+
nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
|
|
189
|
+
if (!p) {
|
|
190
|
+
nish_region_unlock();
|
|
191
|
+
nish_region_die(NISH_REGION_OOM);
|
|
192
|
+
}
|
|
193
|
+
__atomic_store_n(page, p, __ATOMIC_RELEASE);
|
|
194
|
+
}
|
|
195
|
+
nish_region_used = index + 1;
|
|
196
|
+
s = &(*page)[index % NISH_REGION_PAGE];
|
|
197
|
+
__atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
|
|
198
|
+
}
|
|
199
|
+
s->chunk = 8;
|
|
200
|
+
s->outer = 0;
|
|
201
|
+
__atomic_store_n(&s->thread, me, __ATOMIC_RELAXED);
|
|
202
|
+
uint64_t r = s->gen << NISH_REGION_SLOT_BITS | index;
|
|
203
|
+
nish_region_unlock();
|
|
204
|
+
return r;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void *nish_region_enter(uint64_t r) {
|
|
208
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
209
|
+
if (s->entered) nish_region_die(NISH_REGION_ENTERED);
|
|
210
|
+
s->entered = 1;
|
|
211
|
+
s->outer = nish_region_current;
|
|
212
|
+
nish_region_current = r;
|
|
213
|
+
nish_region_swap(s);
|
|
214
|
+
return s->root;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
void nish_region_leave(uint64_t r, void *root) {
|
|
218
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
219
|
+
if (nish_region_current != r) nish_region_die(NISH_REGION_LEAVE);
|
|
220
|
+
nish_region_swap(s);
|
|
221
|
+
nish_region_current = s->outer;
|
|
222
|
+
s->entered = 0;
|
|
223
|
+
s->root = root;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
void nish_region_free(uint64_t r) {
|
|
227
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
228
|
+
if (s->entered) nish_region_die(NISH_REGION_FREE_ENTERED);
|
|
229
|
+
nish_region_swap(s);
|
|
230
|
+
nish_free_arena();
|
|
231
|
+
nish_region_swap(s);
|
|
232
|
+
s->root = 0;
|
|
233
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
234
|
+
nish_region_lock();
|
|
235
|
+
__atomic_store_n(&s->thread, 0, __ATOMIC_RELAXED);
|
|
236
|
+
uint64_t gen = s->gen + 1;
|
|
237
|
+
__atomic_store_n(&s->gen, gen, __ATOMIC_RELAXED);
|
|
238
|
+
if (gen < NISH_REGION_GEN_END) {
|
|
239
|
+
s->outer = nish_region_free_list;
|
|
240
|
+
nish_region_free_list = index + 1;
|
|
241
|
+
}
|
|
242
|
+
nish_region_unlock();
|
|
243
|
+
}
|
package/runtime/runtime.c
CHANGED
|
@@ -174,14 +174,31 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
|
|
|
174
174
|
`nish_array_grow` and `nish_str_concat`. */
|
|
175
175
|
#define NISH_ARRAY_MAX 2147483647u
|
|
176
176
|
|
|
177
|
-
/*
|
|
177
|
+
/* The size of the next chunk while a region is entered (runtime-region.c
|
|
178
|
+
swaps it in and out with the region's arena), or 0 for the thread's own
|
|
179
|
+
arena, whose chunks are at least 64 KB. Defined here, not there, so that
|
|
180
|
+
this file still links alone. */
|
|
181
|
+
NISH_TLS uint64_t nish_region_chunk;
|
|
182
|
+
|
|
183
|
+
/* The most a region's next chunk grows to: past it the chunks stop doubling,
|
|
184
|
+
so a region that keeps allocating never asks malloc for one block of
|
|
185
|
+
gigabytes when smaller ones would do (docs/security/runtime.md, RT-47). A
|
|
186
|
+
single allocation larger than this still gets a chunk of its own size. */
|
|
187
|
+
#define NISH_REGION_CHUNK_MAX ((uint64_t)1 << 20)
|
|
188
|
+
|
|
189
|
+
/* Slow path (size 8-byte rounded): push a chunk (>= 64 KB, or a region's
|
|
190
|
+
next size), bump from it. A region's chunks double from its first, up to
|
|
191
|
+
NISH_REGION_CHUNK_MAX. */
|
|
178
192
|
void *nish_arena_grow(uint64_t size) {
|
|
179
193
|
#ifdef NISH_ASAN
|
|
180
194
|
/* An exact chunk leaves off == cap, so the inline bump never fits again and
|
|
181
195
|
every block comes through here: one malloc, one redzone, per block. */
|
|
182
196
|
uint64_t cap = size;
|
|
183
197
|
#else
|
|
184
|
-
uint64_t
|
|
198
|
+
uint64_t next = nish_region_chunk;
|
|
199
|
+
uint64_t floor = next ? next : 65536;
|
|
200
|
+
uint64_t cap = size > floor ? size : floor;
|
|
201
|
+
if (next) nish_region_chunk = cap < NISH_REGION_CHUNK_MAX / 2 ? cap * 2 : NISH_REGION_CHUNK_MAX;
|
|
185
202
|
#endif
|
|
186
203
|
/* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
|
|
187
204
|
and malloc would silently take the low half of a chunk nobody can address. */
|
package/scripts/build.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
|
|
5
5
|
#
|
|
6
|
-
# The C runtime is
|
|
6
|
+
# The C runtime is fifteen translation units and is named as one: an input
|
|
7
7
|
# <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
|
|
8
8
|
# system calls (files, directories, subprocesses, the environment, the clock),
|
|
9
9
|
# <dir>/runtime-parallel.c, the half that divides a range of work across
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
# <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
|
|
18
18
|
# <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
|
|
19
19
|
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
|
|
20
|
-
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
|
|
21
|
-
# <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm
|
|
20
|
+
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
|
|
21
|
+
# <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm`, and
|
|
22
|
+
# <dir>/runtime-region.c, the arenas a program makes and frees one at a time. Each of those
|
|
22
23
|
# files says why they are compiled and measured apart.
|
|
23
24
|
#
|
|
24
25
|
# Profiles:
|
|
@@ -222,11 +223,11 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
|
|
|
222
223
|
fi
|
|
223
224
|
[ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
224
225
|
|
|
225
|
-
# The runtime is
|
|
226
|
+
# The runtime is fifteen translation units, and a caller names one: whoever passes
|
|
226
227
|
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
|
|
227
228
|
# <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
|
|
228
229
|
# <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
|
|
229
|
-
# <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c and <dir>/runtime-
|
|
230
|
+
# <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c, <dir>/runtime-shm.c and <dir>/runtime-region.c compiled beside it, on every profile,
|
|
230
231
|
# wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
|
|
231
232
|
# unless the build adds -msimd128,
|
|
232
233
|
# runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
|
|
@@ -243,7 +244,7 @@ fi
|
|
|
243
244
|
# here keeps every one of those correct, and keeps "the runtime" one thing to
|
|
244
245
|
# name from the outside. A caller that names one itself is left alone, because
|
|
245
246
|
# naming one object twice is a duplicate-symbol error.
|
|
246
|
-
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c"
|
|
247
|
+
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c runtime-region.c"
|
|
247
248
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
248
249
|
case "$i" in
|
|
249
250
|
*/runtime.c|runtime.c)
|
|
@@ -671,6 +672,18 @@ case "$profile" in
|
|
|
671
672
|
"$CC" "${common[@]}" "${optimise[@]}" ${lto_parts[@]+"${lto_parts[@]}"} \
|
|
672
673
|
"${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
|
|
673
674
|
wasm)
|
|
675
|
+
# A region's chunks come from malloc, and the freestanding wasm runtime
|
|
676
|
+
# (runtime-wasm.c) has none, so IR that makes or enters one is refused here
|
|
677
|
+
# rather than failing at the link on an undefined `nish_region_*`. The wasi
|
|
678
|
+
# profile links runtime-region.c with the rest of the runtime.
|
|
679
|
+
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
680
|
+
case "$i" in
|
|
681
|
+
*.ll) if grep -q "@nish_region_" "$i"; then
|
|
682
|
+
echo "error: $i uses regions, which the wasm profile cannot link (it has no malloc); build it with --profile wasi" >&2
|
|
683
|
+
exit 2
|
|
684
|
+
fi ;;
|
|
685
|
+
esac
|
|
686
|
+
done
|
|
674
687
|
# clang resolves wasm-ld next to its own binary first, then on PATH; ask it
|
|
675
688
|
# rather than probing PATH so a Homebrew llvm without a PATH entry still works.
|
|
676
689
|
wasm_ld=$("$CC" -print-prog-name=wasm-ld)
|
package/std/README.md
CHANGED
|
@@ -41,6 +41,7 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
41
41
|
| [`term-rate.ts`](./term-rate.ts) | how fast the link to the terminal is, and the frame rate and detail a program keeps to under it. A `RateController` is told each frame sent (`frameSent(bytes, costNs, nowMs)`) and each acknowledgement — the answer to the DSR 5 query sent after a frame — (`ackReceived(nowMs)`), keeps running averages of the round trip, the bytes a second delivered and the cost of a frame, and answers `mayDraw(nowMs)` and a `RateDecision`: the frame interval, a resolution divisor, a backend cap and a palette, stepped down a tier when frames back up and up again when the link has room; `stats()` is the `TermStats` a program can show. Pure: no capability and no clock (`tests/link/std_term_rate`, `std_term_rate_f64`; [guide](../docs/guide/using-nish/terminal.md#how-the-frame-rate-adapts)) |
|
|
42
42
|
| [`term-pixels.ts`](./term-pixels.ts) | a `Framebuffer` of `0xRRGGBB` pixels and the encoders that draw it with the backend the terminal has: `kittyFrame` (kitty graphics, one image id retransmitted each frame), `sixelFrame` (a fixed palette, optionally dithered), `drawHalfBlocks` onto a `Canvas` and `drawAscii` onto a `Screen`, each into the `PixelRect` `fitFramebuffer` letterboxes in the cells; `scaleFramebuffer` shrinks a frame by the controller's divisor. `PixelBytes` keeps the encoders' output and working arrays, so frame after frame allocates nothing. Pure (`tests/link/std_term_pixels`, `std_term_pixels_f64`; [guide](../docs/guide/using-nish/standard-library.md#nishterm-pixels)) |
|
|
43
43
|
| [`term-io.ts`](./term-io.ts) | the term module that does the I/O. `runTerminal(options, state, update, render)` is a whole program loop: it opens the terminal, steps `update` at a fixed rate, calls `render` with a `Frame` (the `Screen`, the `Framebuffer`, the step `alpha`, `TermCaps` and `TermStats`) whenever the controller lets a frame go, and closes when `update` answers false or the user presses Ctrl-C. `openTerminal(options)` answers a `Terminal` for a program's own loop (`readEvents`, `present`, `stats`, `close`). Opening refuses a pipe, enters raw mode and the alternate screen, probes the terminal and turns on the kitty keyboard, mouse, paste and focus reports the options ask for, with the runtime's restore string set before each so a panic or signal leaves the terminal as it was; `present` sends the frame in the best backend the controller allows as one write and acknowledges it with DSR 5. Reaches `tty` (`tests/term-io/relay.mjs` drives it in a pseudo-terminal from `tests/run.js`'s `term-io` section; `tests/link/std_term_io_deny`; [guide](../docs/guide/using-nish/terminal.md)) |
|
|
44
|
+
| [`region.ts`](./region.ts) | The source behind the builtin module `nish:region`, and imported only by that name: `Region<T>`, `region`, `regionWith`, `enter`, `enterWith` and `free`. A `Region<T>` is a handle, a 64-bit integer, to memory the program frees when it says so; `region(f)` allocates what `f` builds into a new region and keeps its answer as the root, `enter(r, f)` runs `f(root)` there again and answers its scalar, the `*With` forms pass one argument through, and `free(r)` gives all of it back. A handle used after `free`, entered twice or freed while entered stops the program. The bodies are the meaning; the allocator swap is the emitter's (`src/emit-region.ts`) and what may cross a region's edge is the checker's (`src/region-check.ts`), keyed on this file ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#regions-nishregion); [guide](../docs/guide/language/regions.md)) |
|
|
44
45
|
| [`secret.ts`](./secret.ts) | The source behind the builtin module `nish:secret`, and imported only by that name: `Secret<T>`, `secret`, `expose`, `exposeWith` and `wipe`. The bodies are the meaning; what makes a `Secret` one is the checker's, keyed on this file ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#secrets-nishsecret)). `wipe`'s body is written by the emitter, one volatile `llvm.memset`, and like `collections.ts` the module writes no `.ll` of its own |
|
|
45
46
|
| [`threads.ts`](./threads.ts) | `parallelMapInto(src, dst, f)` and `parallelReduce(src, f, identity)`: a function over every element of an array, on as many threads as the length is worth. Its bodies are the sequential meaning, which is what runs under Node; the compiler recognises the two templates by module and name, lowers the one loop in each onto `nish_parallel_range`, holds the function to the rules that make that safe, and compiles an importing program with `--threads` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#data-parallelism-nishthreads)). `tests/link/par_*` are its programs. `scope()`, bound by `using s = scope()`, runs unlike tasks, each on a thread of its own, and joins them at every exit of the block; `s.spawn(entry, arg, dst, at)` stores a task's answer, and `s.spawn(entry, arg)` and `s.spawn(() => ...)`, whose arrow reads the function's `const` locals, run one that answers `void` (`spawnWith` and `spawnThunk` are the methods the compiler reads them as, and under Node `spawn` itself tells them apart) ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#scoped-tasks-using-s--scope)). `scopeWithStack(bytes)` is a scope whose tasks each run on a new thread with at least `bytes` of stack, for a task that recurses deeper than a thread's default allows; under Node and without threads the size is checked and otherwise ignored (`tests/link/thread_stack_deep`). `Mutex<T>` is a lock that owns its data: the value is reachable only through the guard `using g = m.lock()` binds, which releases it at every exit of the block, so a scope's tasks can share and update one value without a data race or a deadlock ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#a-lock-that-owns-its-data-mutext); `tests/link/thread_mutex_*`, and `examples/histogram.ts` measured). `Channel<T>` carries scalars to one receiver, a task of the scope or the parent after the join, from the scope's sender tasks and from the parent before the block that spawns them, and closes when every sending task has returned ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#channels-channelt); [`docs/wp29-thread-surface.md`](../docs/wp29-thread-surface.md#44-stage-p3--channels-of-scalars) §4.4). Every body in the module is its meaning under Node, where a task runs at its `spawn` |
|
|
46
47
|
| [`sync/shm.ts`](./sync/shm.ts) | `createRegion(name, bytes)`, `openRegion(name)` and `unlinkRegion(name)`: a named region of memory that several processes of one user map at once, through `runtime/runtime-shm.c`. A `SharedRegion` is read and written through checked slots, `loadI32` to `compareExchangeI64` at an ordering the call names (`RELAXED` to `SEQ_CST`), and byte windows (`read`, `write`); an offset outside the region or not a multiple of the slot's width stops the program. `waitI32` and `wake` sleep and wake on a slot across processes (Linux futex; `SHM_UNSUPPORTED` elsewhere). Native only, and it reaches `ffi` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#shared-memory-between-processes-nishsyncshm-and-nishsyncring); [`docs/wp47-shared-memory.md`](../docs/wp47-shared-memory.md); [`docs/security/shm.md`](../docs/security/shm.md)) |
|
|
@@ -231,13 +232,13 @@ reads one, and it still takes its randomness as an argument.
|
|
|
231
232
|
| [`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`) |
|
|
232
233
|
| [`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 |
|
|
233
234
|
| [`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 |
|
|
234
|
-
| [`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`) |
|
|
235
|
-
| [`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`; `http1UpgradeRequestHead(method, target, names, values, upgrade)` writes `Upgrade` and `Connection: Upgrade` itself, with no body. `Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes)` is told what was asked (`expect(head, connect)`, then `expectUpgrade()` for a request that asked to switch protocols), 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 or a 101 asked for, `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 not asked for, 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`) |
|
|
236
|
-
| [`net/
|
|
235
|
+
| [`net/http1-codec.ts`](./net/http1-codec.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`) |
|
|
236
|
+
| [`net/http1-client-codec.ts`](./net/http1-client-codec.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`; `http1UpgradeRequestHead(method, target, names, values, upgrade)` writes `Upgrade` and `Connection: Upgrade` itself, with no body. `Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes)` is told what was asked (`expect(head, connect)`, then `expectUpgrade()` for a request that asked to switch protocols), 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 or a 101 asked for, `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 not asked for, more than `HTTP1_MAX_INFORMATIONAL` interim responses and a chunk-size line over `HTTP1_MAX_CHUNK_LINE`. Record: [`docs/security/http1-client-codec.md`](../docs/security/http1-client-codec.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`) |
|
|
237
|
+
| [`net/fetch.ts`](./net/fetch.ts) | A blocking HTTP/1.1 client for command-line programs: `fetchSync(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. `FetchSyncClient(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-codec`. Native only. Record: [`docs/security/fetch.md`](../docs/security/fetch.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`) |
|
|
237
238
|
| [`net/outbound.ts`](./net/outbound.ts) | The policy for fetching URLs your program did not choose, set as `HttpOptions.outbound`: `OutboundPolicy` reaches only public addresses (no loopback, private, link-local, CGNAT, metadata or other special-purpose range, IPv4-mapped, NAT64 and 6to4 forms judged as the IPv4 address they carry), checked on the resolved address the client then connects to, so DNS rebinding cannot move it; every redirect hop is checked again, the proxy is not used, and redirects, total time and body size are capped. `permit(cidr)` lets a trusted prefix through, `allowHost(name)` limits the names a URL or hop may carry (`HTTP_E_BLOCKED` before the lookup). Native only. Record: [`docs/security/outbound.md`](../docs/security/outbound.md) | RFC 6890, RFC 4291, RFC 6052, RFC 3056 and the IANA special-purpose address registries; every range at its edges and a loopback server fetched with and without a policy (`tests/link/net_outbound`) |
|
|
238
239
|
| [`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 |
|
|
239
240
|
| [`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`, `websocketOfferedProtocols` and `websocketUpgradeResponse`, the 101. The client's side is `websocketClientKey(nonce)` (a 16-byte nonce the caller draws; the module draws no randomness), `websocketUpgradeRequest(host, target, key, protocols)` and `websocketCheckResponse(parser, key, offered)` over an `Http1ResponseParser` told `expectUpgrade()`, answering a `WsHandshake` (`error`, a `WS_HS_*` code, and `protocol`) | 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 |
|
|
240
|
-
| [`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 |
|
|
241
|
+
| [`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-codec.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 |
|
|
241
242
|
| [`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`) |
|
|
242
243
|
| [`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`) |
|
|
243
244
|
| [`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`) |
|
|
@@ -250,7 +251,9 @@ reads one, and it still takes its randomness as an argument.
|
|
|
250
251
|
| [`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`). It sends gzip only when it pays: `minLength`, `minSaving` bytes or `minSavingPercent`, and `segmentMode` (fewer TCP segments of `mss`, head included, than as it is); `decision` says why (`HTTP_COMPRESS_SENT`, `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`, `_NO_FEWER_SEGMENTS`). Bodies up to `fixedLimit` use the fixed-Huffman encoder (`encoder`: `HTTP_ENCODER_AUTO`, `_FIXED`, `_LEVEL`). 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`); `identity;q=0` is honoured. 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`) |
|
|
251
252
|
| [`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_*`) |
|
|
252
253
|
| [`net/handler.ts`](./net/handler.ts) | `HttpHandler`, complete requests and their answers over the HTTP/1.1 carrier: it owns the readiness loop, routes each request through a `Router` at its head, gathers its body up to `HandlerLimits.http.maxBody` (1 MiB; past it the carrier answers 413), and hands it over whole. The program pulls — `wait(ms)`, `ready()`, `take()` — and switches on `req.route`; `HttpRequest` reads parameters, form-decoded query values and fields as ranges into its own buffers. Each request is answered once, now or in a later turn, by `send`, `sendFields`, `empty`, `json()` + `sendJson`, `problem` (RFC 9457), `refuse` (404, or 405 with `Allow`) or `start`/`write`/`end`; an answer whose connection closed first is `HANDLER_DROPPED` and reaches no later peer. `HEAD` takes a `GET` route. `watch`/`fired` add the program's own descriptors (`signalFd()`), and `drain` stops gracefully. Nothing is allocated per request once warm (`docs/security/handler.md`) | Routing, parameters and form-decoded queries, `HEAD`, 404 and 405 problems, chunked bodies in and out, the body cap at and one past it, pipelining order, late answers dropped across a reused slot, the arena and pool flat over two thousand requests, and the drain (`tests/link/net_handler`, and `net_http_app` over `examples/http-app`) |
|
|
253
|
-
| [`net/
|
|
254
|
+
| [`net/upstream.ts`](./net/upstream.ts) | `Upstream`, outbound HTTP/1.1 calls from a route handler that never block it: a pool of keep-alive connections to one `http:` origin on the `HttpHandler`'s own loop (watch ids `UpstreamLimits.firstId` and up). `get(req, path)` returns at once, or `UPSTREAM_E_FULL` with nothing held; each call comes back once through `ready()`/`take()` tagged with `r.req`, `ok` with its status and `body[0 .. length)`, or failed with the codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code (timeout, closed, connect, origin, lookup, blocked, request), and goes back with `release`. The origin is looked up once at start-up and pinned; `UpstreamLimits.outbound` checks it against an `OutboundPolicy`. A failed call's connection is closed. Each connection keeps one codec in spans mode, so a warm pool allocates nothing per call | A slow upstream while other clients and a second upstream are answered, the timeout, keep-alive and tagging, five codec refusals as failures, the policy refusing loopback, origin and request-target refusals, three thousand warm calls on one connection with `Arena.used()` flat (`tests/link/net_upstream`), and a second `release` panicking (`net_upstream_release`) |
|
|
255
|
+
| [`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`), binary `int4[]` / `int8[]` parameters (`paramInt4Array` / `paramInt8Array` over an `i64[]` window, or `binaryArrayStart`, `binaryArrayInt`, `binaryArrayNull` and the same `arrayEnd`; per-parameter format codes only once a binary one is bound; an `int4` element out of range panics), `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. `PgBatchClock` says when to take, from times the program passes in: once max(`minCount`, the moving average of batch sizes × `factor`) wait, the oldest has waited `maxWaitUs`, or, with the idle bypass, nothing is in flight and the load is light (its defaults are the fixed window); `PgBatcher.takeDue` takes only then, and `PgBatchTurns` lets one free connection collect while the others execute. 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`); the clock's reasons, backlog and turns (`tests/link/std_postgres_batch_clock`) |
|
|
256
|
+
| [`net/postgres-carrier.ts`](./net/postgres-carrier.ts) | `PgCarrier`, a `PgPool`'s queries on an `HttpHandler`'s loop: its sockets sit in the handler's epoll set, a route handler hands a request to `run` (or `runInt` / `runText` for one of up to 8 attached `PgBatcher`s) and gets its number at once, and `next()` answers `PG_CARRIER_BUILD` to bind a plain query on `connection()`, `PG_CARRIER_ROW`, `PG_CARRIER_COMPLETE`, `PG_CARRIER_ERROR` and one final `PG_CARRIER_DONE`, `PG_CARRIER_MISSING` or `PG_CARRIER_FAILED` (`failure`, `socketError`) with `request()` the request to answer and `tag` the caller's. It connects the pool's slots itself (TCP or a Unix socket), times out a connect after `connectTimeoutMs`, sheds batched keys when no slot can connect, and sizes every table from `PgCarrierLimits` at construction: `run` answers `PG_CARRIER_FULL` rather than grow one, and once connections are up it allocates nothing per query. Kept apart from `net/postgres.ts` so that module reaches no capability (`docs/security/postgres.md`) | A loopback backend in the test itself: routing, a slow query not stalling another client, lost and refused connections, a connect timeout, and `Arena.used()` flat over thousands of queries (`tests/link/std_postgres_carrier`); a live server by `tests/postgres/live/run.mjs` |
|
|
254
257
|
| [`net/prefork.ts`](./net/prefork.ts) | A server on every core: `preforkWorkers(process.argv)` or `preforkWith(process.argv, config)` answers a worker's index in each worker it starts again from the same program, and in the supervisor runs them until SIGTERM or SIGINT, restarting one that exits after a backoff. `PreforkConfig.supervisorChannels` gives each worker a `unixSocketPair` to the supervisor, which `preforkSupervise(args, config, state, onChannel)` hands to a callback; `serviceWorker` gives every other worker a pair to that one, and makes the workers one group that restarts together on fresh pairs. The ends arrive from descriptor 3 through `spawnWithFds`, found with `preforkSupervisorChannel`, `preforkServiceChannel` and `preforkServiceChannelOf`. Native only | `tests/link/net_prefork` (the spread, a dead worker, SIGTERM, the backoff); `tests/link/net_prefork_channels` (each worker holds exactly its own ends, a restart, the supervisor's descriptors flat) |
|
|
255
258
|
|
|
256
259
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
@@ -345,12 +348,12 @@ send, over loopback from a Nish UDP client, exchanges recorded against
|
|
|
345
348
|
aioquic by their `record.sh`: a handshake and echo, and Version Negotiation,
|
|
346
349
|
Retry, key update both ways, the idle timeout and a stateless reset.
|
|
347
350
|
|
|
348
|
-
`net/http1.ts`, `net/http1-server.ts` and `net/websocket.ts` run their checks
|
|
351
|
+
`net/http1-codec.ts`, `net/http1-server.ts` and `net/websocket.ts` run their checks
|
|
349
352
|
again under `--number-mode f64` (`net_http1_f64`, `net_http1_server_f64`,
|
|
350
353
|
`net_websocket_f64`). So does the HTTP date (`net_http_date_f64`). None
|
|
351
354
|
handles a secret but through `nish/net/tls-tcp`, so none is in the
|
|
352
355
|
constant-time check; their record is
|
|
353
|
-
[`docs/security/http1.md`](../docs/security/http1.md), written with the code
|
|
356
|
+
[`docs/security/http1-codec.md`](../docs/security/http1-codec.md), written with the code
|
|
354
357
|
rather than after an audit.
|
|
355
358
|
|
|
356
359
|
## How a program imports it
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nish/net/
|
|
3
|
-
* programs: `
|
|
4
|
-
* response's head with a reader for its body, and
|
|
2
|
+
* `nish/net/fetch` — a blocking HTTP/1.1 client for command-line
|
|
3
|
+
* programs: `fetchSync(request, options)` dials, sends, and answers the
|
|
4
|
+
* response's head with a reader for its body, and a `FetchSyncClient` does the
|
|
5
5
|
* same while keeping one connection alive between fetches.
|
|
6
6
|
*
|
|
7
|
-
* import { HttpOptions, HttpRequest, HttpResponse,
|
|
7
|
+
* import { HttpOptions, HttpRequest, HttpResponse, fetchSync } from "nish/net/fetch";
|
|
8
8
|
*
|
|
9
9
|
* const req = new HttpRequest("GET", "http://example.com/");
|
|
10
10
|
* req.header("Accept", "application/json");
|
|
11
|
-
* const res: HttpResponse | null =
|
|
11
|
+
* const res: HttpResponse | null = fetchSync(req, new HttpOptions());
|
|
12
12
|
* if (res !== null) {
|
|
13
13
|
* const body: u8[] | null = res.body.readAll(1048576);
|
|
14
14
|
* }
|
|
15
15
|
*
|
|
16
16
|
* **It blocks the thread** at every step, the way a CLI wants it to: the
|
|
17
17
|
* lookup, the connect, each write and each read. A server, which must not
|
|
18
|
-
* stall every connection for one slow peer, drives `nish/net/http1-client`'s
|
|
18
|
+
* stall every connection for one slow peer, drives `nish/net/http1-client-codec`'s
|
|
19
19
|
* parser from its own loop instead. Every wait is bounded: `connectTimeoutMs`
|
|
20
20
|
* for the lookup and the connect (one budget across every address the name
|
|
21
21
|
* has), `readTimeoutMs` for each wait on the peer, and `totalTimeoutMs`, when
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* **Framing is the parser's.** The head is written by `http1RequestHead` and
|
|
26
26
|
* every byte read goes through `Http1ResponseParser`, so a value holding a CR
|
|
27
27
|
* or LF is refused rather than sent, and every response-smuggling shape is
|
|
28
|
-
* refused with the parser's own code (see `docs/security/http1-client.md`).
|
|
28
|
+
* refused with the parser's own code (see `docs/security/http1-client-codec.md`).
|
|
29
29
|
*
|
|
30
30
|
* **Keep-alive** holds one connection, to the last origin (or proxy) used. A
|
|
31
31
|
* fetch to the same place reuses it once the last body has been read to its
|
|
@@ -70,14 +70,14 @@ import {
|
|
|
70
70
|
HTTP1_HEAD,
|
|
71
71
|
HTTP1_NEED_MORE,
|
|
72
72
|
HTTP1_UPGRADE,
|
|
73
|
-
} from "nish/net/http1"
|
|
73
|
+
} from "nish/net/http1-codec"
|
|
74
74
|
import {
|
|
75
75
|
HTTP1_E_BODY_LIMIT,
|
|
76
76
|
HTTP1_E_TRUNCATED,
|
|
77
77
|
HTTP1_INFORMATIONAL,
|
|
78
78
|
Http1ResponseParser,
|
|
79
79
|
http1RequestHead,
|
|
80
|
-
} from "nish/net/http1-client"
|
|
80
|
+
} from "nish/net/http1-client-codec"
|
|
81
81
|
import { httpFieldBytes } from "nish/net/http-fields"
|
|
82
82
|
import { Url, urlDecode, urlDefaultPort, urlOrigin, urlParse, urlResolve } from "nish/net/url"
|
|
83
83
|
import { resolveHost } from "nish/net/dns"
|
|
@@ -293,10 +293,10 @@ export class HttpResponse {
|
|
|
293
293
|
* client that made it, and only until that client starts its next fetch.
|
|
294
294
|
*/
|
|
295
295
|
export class HttpBodyReader {
|
|
296
|
-
client:
|
|
296
|
+
client: FetchSyncClient
|
|
297
297
|
generation: i32
|
|
298
298
|
|
|
299
|
-
constructor(client:
|
|
299
|
+
constructor(client: FetchSyncClient, generation: i32) {
|
|
300
300
|
this.client = client
|
|
301
301
|
this.generation = generation
|
|
302
302
|
}
|
|
@@ -371,7 +371,7 @@ export class HttpBodyReader {
|
|
|
371
371
|
* A client: the options, the proxy settings, and at most one open
|
|
372
372
|
* connection, kept between fetches. Not for two threads at once.
|
|
373
373
|
*/
|
|
374
|
-
export class
|
|
374
|
+
export class FetchSyncClient {
|
|
375
375
|
// Declared widest first, so no byte of a client is padding.
|
|
376
376
|
options: HttpOptions
|
|
377
377
|
errorText: string = ""
|
|
@@ -416,7 +416,7 @@ export class HttpClient {
|
|
|
416
416
|
lost: boolean = false
|
|
417
417
|
/** Whether the latest response's body is unread. */
|
|
418
418
|
bodyOpen: boolean = false
|
|
419
|
-
/** Whether this is `
|
|
419
|
+
/** Whether this is `fetchSync`'s client, closed whole once its one body is done with. */
|
|
420
420
|
once: boolean = false
|
|
421
421
|
|
|
422
422
|
constructor(options: HttpOptions) {
|
|
@@ -1336,7 +1336,7 @@ export class HttpClient {
|
|
|
1336
1336
|
return take
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
|
-
/** Closes the connection a body is done with, and `
|
|
1339
|
+
/** Closes the connection a body is done with, and `fetchSync`'s whole client with it. */
|
|
1340
1340
|
release(): void {
|
|
1341
1341
|
if (this.once) {
|
|
1342
1342
|
this.close()
|
|
@@ -1391,10 +1391,10 @@ export class HttpClient {
|
|
|
1391
1391
|
/**
|
|
1392
1392
|
* One fetch on a client of its own, closed once the body has been read or
|
|
1393
1393
|
* the fetch failed. Null on a failure, with no way to say why: a program
|
|
1394
|
-
* that reports its errors fetches through
|
|
1394
|
+
* that reports its errors fetches through a `FetchSyncClient`.
|
|
1395
1395
|
*/
|
|
1396
|
-
export const
|
|
1397
|
-
const client:
|
|
1396
|
+
export const fetchSync = (req: HttpRequest, options: HttpOptions): HttpResponse | null => {
|
|
1397
|
+
const client: FetchSyncClient = new FetchSyncClient(options)
|
|
1398
1398
|
client.once = true
|
|
1399
1399
|
const res: HttpResponse | null = client.fetch(req)
|
|
1400
1400
|
if (res === null) {
|
package/std/net/handler.ts
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
import { copyBytes } from "nish/bytes"
|
|
81
81
|
import { JsonWriter } from "nish/json"
|
|
82
82
|
import { httpFieldBytes } from "nish/net/http-fields"
|
|
83
|
-
import { HTTP1_CHUNKED, HTTP1_NO_BODY, http1HexValue } from "nish/net/http1"
|
|
83
|
+
import { HTTP1_CHUNKED, HTTP1_NO_BODY, http1HexValue } from "nish/net/http1-codec"
|
|
84
84
|
import {
|
|
85
85
|
H1_AGAIN,
|
|
86
86
|
H1_BODY,
|