@amritk/nish-x86_64-linux 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nish +0 -0
- package/docs/AI.md +26 -19
- package/package.json +1 -1
- package/runtime/core/arrays.d.ts +0 -2
- package/runtime/core/arrays.mjs +0 -2
- package/runtime/core/numbers.mjs +2 -3
- package/runtime/nish.d.ts +0 -4
- package/runtime/nish.mjs +2 -5
- package/runtime/runtime-net.c +1 -1
- package/runtime/runtime-term.c +2 -2
- package/runtime/runtime.c +1 -1
- package/std/README.md +20 -0
- package/std/compress/checksum.ts +178 -0
- package/std/compress/deflate.ts +2400 -0
- package/std/compress/gzip.ts +747 -0
- package/std/compress/zlib.ts +168 -0
- package/std/crypto/sha256.ts +2 -3
- package/std/crypto/x25519.ts +3 -4
- package/std/net/http-compress.ts +393 -0
- package/std/net/http1-server.ts +95 -0
package/bin/nish
CHANGED
|
Binary file
|
package/docs/AI.md
CHANGED
|
@@ -90,8 +90,7 @@ so it refuses `-o` and `--link`. It rewrites your files in place, with no
|
|
|
90
90
|
backup, so run it on a committed or backed-up tree. Run it first, then read
|
|
91
91
|
what remains. A warning can carry a fix too, and `--fix` applies it only when the program
|
|
92
92
|
compiles and the warning would be printed: `NL9007`'s inserts the guard that
|
|
93
|
-
proves an index in range (not under `--no-warn-performance`)
|
|
94
|
-
migrates a site off `--unchecked-indexing` (below).
|
|
93
|
+
proves an index in range (not under `--no-warn-performance`).
|
|
95
94
|
|
|
96
95
|
When it compiles, **check the native build against the program's TypeScript
|
|
97
96
|
reading**. Run the project's tests natively; where it has none, write the
|
|
@@ -247,14 +246,9 @@ the site: `uncheckedGet(xs, i)` and `uncheckedSet(xs, i, v)` on an array of
|
|
|
247
246
|
numbers with an `i32` index (out of range is undefined behaviour), and
|
|
248
247
|
`wrappingAdd` / `wrappingSub` / `wrappingMul` on two `i32`s or two `i64`s.
|
|
249
248
|
The sixth export, `readCString`, copies a C string out of a `CPtr`
|
|
250
|
-
([Calling C](#calling-c)). Calling one without the import is NL2456.
|
|
251
|
-
`--
|
|
252
|
-
|
|
253
|
-
`nish --unchecked-indexing --fix` on your files: each index the flag leaves
|
|
254
|
-
unchecked is a `deprecation` warning of its own (NL7002), and where the site
|
|
255
|
-
is a number array with an `i32` index its `fix` rewrites it to `uncheckedGet`
|
|
256
|
-
or `uncheckedSet` and adds the import. A site with no rewrite keeps its
|
|
257
|
-
warning; dropping the flag gives it back its bounds check.
|
|
249
|
+
([Calling C](#calling-c)). Calling one without the import is NL2456. No flag turns the checks off for a
|
|
250
|
+
whole program: `--unchecked-indexing` and `--wrapping` were removed before 1.0
|
|
251
|
+
and are refused with exit 2.
|
|
258
252
|
|
|
259
253
|
```ts nish:ok
|
|
260
254
|
import { uncheckedGet, wrappingAdd, wrappingMul } from "nish:unsafe";
|
|
@@ -280,8 +274,7 @@ where it can prove the result fits — a loop counter under `i < n`, a byte
|
|
|
280
274
|
widened with `toI32`, an `integer<Lo, Hi>`, a value masked with `& c`. Arithmetic
|
|
281
275
|
that is *meant* to wrap — a hash, a PRNG, a checksum — goes in `u32` or `u64`,
|
|
282
276
|
which always wrap and are never checked; convert back with `toI32` (free), or
|
|
283
|
-
call `wrappingAdd` / `wrappingSub` / `wrappingMul` at the site.
|
|
284
|
-
`--wrapping` makes every signed operation of your own package wrap instead.
|
|
277
|
+
call `wrappingAdd` / `wrappingSub` / `wrappingMul` at the site.
|
|
285
278
|
|
|
286
279
|
```ts nish:ok
|
|
287
280
|
// FNV-1a: the multiply overflows on purpose, so the state is a `u32`.
|
|
@@ -2209,7 +2202,8 @@ unread is reported again. Add `signalFd()` to the loop to wake on SIGTERM. None
|
|
|
2209
2202
|
exists on a wasm target, and under Node each throws.
|
|
2210
2203
|
|
|
2211
2204
|
**Arena.** `Arena.mark()`, `Arena.used()` and `using a = arena()` — see below.
|
|
2212
|
-
`Arena.release(m)` and `Arena.reset()`
|
|
2205
|
+
`Arena.release(m)` and `Arena.reset()` were removed before 1.0: write
|
|
2206
|
+
`using a = arena()`.
|
|
2213
2207
|
|
|
2214
2208
|
### Map and Set
|
|
2215
2209
|
|
|
@@ -2494,10 +2488,9 @@ anything that runs for a long time, should compile with `--deny-retention`,
|
|
|
2494
2488
|
which makes this warning and the three like it errors.
|
|
2495
2489
|
|
|
2496
2490
|
Everything else is bumped from the arena, which is released when `main`
|
|
2497
|
-
returns.
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
with a `deprecation` warning (NL7001) pointing to `using a = arena()`.
|
|
2491
|
+
returns. To give memory back sooner, scope it with `using a = arena()`;
|
|
2492
|
+
`Arena.release` and `Arena.reset` are gone, and a call to either is an unknown
|
|
2493
|
+
builtin (NL2244).
|
|
2501
2494
|
|
|
2502
2495
|
## Recipes for what is missing
|
|
2503
2496
|
|
|
@@ -2584,6 +2577,21 @@ most 100 pairs. (No block, as for `nish/text`; `tests/link/std_url` is its
|
|
|
2584
2577
|
program, and the user guide's [standard-library
|
|
2585
2578
|
page](guide/using-nish/standard-library.md#nishneturl) its walk-through.)
|
|
2586
2579
|
|
|
2580
|
+
**Gzip a response, or read a gzipped body** — there is no `node:zlib` and no
|
|
2581
|
+
`CompressionStream`: `nish/compress/gzip` (and `zlib`, `deflate`) compress
|
|
2582
|
+
and decompress into buffers you own. Build a `GzipCompressor(DEFLATE_FAST)`
|
|
2583
|
+
once and call `compress(src, off, len, out, at)`, which answers the end or -1
|
|
2584
|
+
when `out` is shorter than `compressBound(len)`; `GzipDecompressor`'s
|
|
2585
|
+
`decompress(..., limit)` never writes more than `limit` bytes and answers a
|
|
2586
|
+
negative `DEFLATE_ERR_*` / `GZIP_ERR_*` code for any malformed stream, so test
|
|
2587
|
+
it before reading `out`. `GzipWriter` and `GzipReader` stream. On HTTP/1.1,
|
|
2588
|
+
`conn.respondEncoded(status, names, values, body, off, len, enc,
|
|
2589
|
+
conn.acceptEncoding())` with an `HttpCompressor` from `nish/net/http-compress`
|
|
2590
|
+
negotiates, compresses and sets the fields. Do not compress a secret beside
|
|
2591
|
+
text the client chose (BREACH). (No block, as for `nish/text`;
|
|
2592
|
+
`tests/link/std_compress` is its program, and the user guide's
|
|
2593
|
+
[compression page](guide/using-nish/compression.md) its walk-through.)
|
|
2594
|
+
|
|
2587
2595
|
**Pass a list to a PostgreSQL statement** — there is no driver that expands
|
|
2588
2596
|
`IN (...)` and no `pg`'s array serialiser: in `nish/net/postgres`, write
|
|
2589
2597
|
`= any($1::int8[])` or `unnest($1::text[])` and bind the list as one
|
|
@@ -2731,8 +2739,7 @@ export const at = (xs: i32[], i: i32): i32 => xs[i];
|
|
|
2731
2739
|
A call into the standard library or a dependency that may panic is refused at
|
|
2732
2740
|
the call (NL2458), because only its own author can prove it: call a function
|
|
2733
2741
|
that cannot panic, or make the call from a module outside the scope.
|
|
2734
|
-
|
|
2735
|
-
still refused. `uncheckedGet` and `uncheckedSet` are not proofs either, but
|
|
2742
|
+
`uncheckedGet` and `uncheckedSet` are not proofs, but
|
|
2736
2743
|
they cannot panic, and their `nish:unsafe` import is the module's visible
|
|
2737
2744
|
opt-in to an index out of range being undefined behaviour, so the scope allows
|
|
2738
2745
|
them (as `"allowed": true` sites) and a call into them is never refused:
|
package/package.json
CHANGED
package/runtime/core/arrays.d.ts
CHANGED
|
@@ -15,8 +15,6 @@ export declare function arraySetAt<T>(dst: T[], src: readonly T[], offset?: numb
|
|
|
15
15
|
export declare function newArray<T>(n: number | bigint, zero: unknown): T[];
|
|
16
16
|
export declare function arenaUsed(): i64;
|
|
17
17
|
export declare function arenaMark(): i64;
|
|
18
|
-
export declare function arenaRelease(m?: unknown): void;
|
|
19
|
-
export declare function arenaReset(): void;
|
|
20
18
|
/**
|
|
21
19
|
* The protocol `using` reads, declared here as `nish.d.ts` does, so `using a =
|
|
22
20
|
* arena()` type-checks with no `"esnext.disposable"` in a program's `lib`.
|
package/runtime/core/arrays.mjs
CHANGED
|
@@ -145,8 +145,6 @@ export function newArray(n, zero) {
|
|
|
145
145
|
// that is when this file grows a second arena to keep count of.
|
|
146
146
|
export function arenaUsed() { return 0n; }
|
|
147
147
|
export function arenaMark() { return 0n; }
|
|
148
|
-
export function arenaRelease() {}
|
|
149
|
-
export function arenaReset() {}
|
|
150
148
|
/** `using a = arena()`: a disposable whose disposal does nothing, because there is no arena to release. */
|
|
151
149
|
export function arena() {
|
|
152
150
|
return { [Symbol.dispose]() {} };
|
package/runtime/core/numbers.mjs
CHANGED
|
@@ -90,7 +90,7 @@ export function ctEq(a, b) {
|
|
|
90
90
|
// ---- Checked arithmetic (wp33 R4) ----------------------------------------------------
|
|
91
91
|
//
|
|
92
92
|
// What `--emit-ts` writes for a signed `+ - *` and unary minus the compiler
|
|
93
|
-
//
|
|
93
|
+
// did not prove in range: the exact
|
|
94
94
|
// answer, or the panic `nish_panic_overflow` prints, in its words. An i32 is
|
|
95
95
|
// exact in a double until it leaves the range, and `Math.imul` is the
|
|
96
96
|
// wrapped product, so a product that differs from the double one overflowed.
|
|
@@ -121,8 +121,7 @@ function checkI64(r, op) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// `wrappingAdd`, `wrappingSub` and `wrappingMul` from `nish:unsafe`: the
|
|
124
|
-
// two's-complement answer at the operands' width, i32 or i64
|
|
125
|
-
// `--wrapping` says.
|
|
124
|
+
// two's-complement answer at the operands' width, i32 or i64.
|
|
126
125
|
export function wrappingAdd(a, b) {
|
|
127
126
|
return typeof a === "bigint" ? BigInt.asIntN(64, a + b) : (a + b) | 0;
|
|
128
127
|
}
|
package/runtime/nish.d.ts
CHANGED
|
@@ -617,10 +617,6 @@ declare module "nish:secret" {
|
|
|
617
617
|
declare const Arena: {
|
|
618
618
|
/** The current bump address. */
|
|
619
619
|
mark(): i64;
|
|
620
|
-
/** Free everything allocated since `m`. */
|
|
621
|
-
release(m: i64): void;
|
|
622
|
-
/** Recycle everything in O(1), keeping the newest chunk. */
|
|
623
|
-
reset(): void;
|
|
624
620
|
/** Bytes bumped in the current chunk. */
|
|
625
621
|
used(): i64;
|
|
626
622
|
};
|
package/runtime/nish.mjs
CHANGED
|
@@ -265,15 +265,12 @@ globalThis.parseFloat = shim.parseFloat;
|
|
|
265
265
|
// script under Node, which is one index later than Node's own `argv`.
|
|
266
266
|
process.argv = shim.argv();
|
|
267
267
|
|
|
268
|
-
// There is no arena. `mark
|
|
269
|
-
//
|
|
270
|
-
// zero, which is the honest number for a host that is not bump-allocating.
|
|
268
|
+
// There is no arena. `mark()` answers zero rather than failing so that a
|
|
269
|
+
// program which reads it still runs; `used()` answers zero too, which is the honest number for a host that is not bump-allocating.
|
|
271
270
|
// Delegated like everything else here rather than written inline, so that the
|
|
272
271
|
// answers cannot drift from the ones the rewritten runner gives.
|
|
273
272
|
provide("Arena", {
|
|
274
273
|
mark: shim.arenaMark,
|
|
275
|
-
release: shim.arenaRelease,
|
|
276
|
-
reset: shim.arenaReset,
|
|
277
274
|
used: shim.arenaUsed,
|
|
278
275
|
});
|
|
279
276
|
// `using a = arena()` releases nothing here either: what the block allocated
|
package/runtime/runtime-net.c
CHANGED
|
@@ -359,7 +359,7 @@ int32_t nish_connect_result(int32_t fd) {
|
|
|
359
359
|
|
|
360
360
|
/* `netRead(fd, buf, off, len)`: at most `len` bytes into `buf` from `off`.
|
|
361
361
|
The compiled call has already checked `0 <= off <= off + len <=
|
|
362
|
-
buf.length
|
|
362
|
+
buf.length`. */
|
|
363
363
|
int32_t nish_net_read(int32_t fd, nish_array *buf, int64_t off, int64_t len) {
|
|
364
364
|
ssize_t n = recv(fd, buf->data + off, (size_t)len, 0);
|
|
365
365
|
return n < 0 ? nish_net_fail() : (int32_t)n;
|
package/runtime/runtime-term.c
CHANGED
|
@@ -145,8 +145,8 @@ static int nish_stdin_ready(int32_t timeout_ms) {
|
|
|
145
145
|
|
|
146
146
|
/* `readStdin(buf, off, len)`: at most `len` bytes into `buf` from `off`, the
|
|
147
147
|
count, 0 at the end of the stream and -11 when nothing is waiting. The
|
|
148
|
-
compiled call has already checked `0 <= off <= off + len <= buf.length
|
|
149
|
-
|
|
148
|
+
compiled call has already checked `0 <= off <= off + len <= buf.length`.
|
|
149
|
+
A `read` that a
|
|
150
150
|
signal interrupts is asked again; one that would block (a descriptor some
|
|
151
151
|
other program made non-blocking) is -11 too, and so is a readiness check a
|
|
152
152
|
signal interrupts, which would otherwise fall through to a `read` that
|
package/runtime/runtime.c
CHANGED
|
@@ -1354,7 +1354,7 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
|
|
|
1354
1354
|
return a;
|
|
1355
1355
|
}
|
|
1356
1356
|
|
|
1357
|
-
/* process.argv: malloc, not the arena, so
|
|
1357
|
+
/* process.argv: malloc, not the arena, so no arena scope can free it. Built once by @main.
|
|
1358
1358
|
Deliberately *not* NISH_TLS (WP20 T0 §3.2): `nish_argv_init` writes it once
|
|
1359
1359
|
before the program's `main` body runs and nothing writes it afterwards, so
|
|
1360
1360
|
every thread may read the one copy. That holds only while no thread is
|
package/std/README.md
CHANGED
|
@@ -158,6 +158,25 @@ Three rules hold across the modules:
|
|
|
158
158
|
whose one secret, a private key, is base64-decoded by range masks and
|
|
159
159
|
otherwise only copied and handed to `crypto/p256.ts`.
|
|
160
160
|
|
|
161
|
+
## `nish/compress` — gzip, zlib and DEFLATE
|
|
162
|
+
|
|
163
|
+
Compression for a server that gzips its responses and reads compressed
|
|
164
|
+
bodies, in pure Nish. Every call takes caller-owned windows and answers where
|
|
165
|
+
its output ends; the objects (`Deflater`, `Inflater`, `GzipCompressor`, ...)
|
|
166
|
+
allocate their tables once and nothing per call, and a free function
|
|
167
|
+
(`gzipCompress`, ...) builds one for a single call. The inner loops use
|
|
168
|
+
`nish:unsafe`'s unchecked access, each site justified in
|
|
169
|
+
`docs/security/compress.md`, so a program built with `--deny unsafe` cannot
|
|
170
|
+
import them. Written from the RFCs, not ported. Speed against zlib and
|
|
171
|
+
libdeflate: `bench/gzip/`. Guide: [Compression](../docs/guide/using-nish/compression.md).
|
|
172
|
+
|
|
173
|
+
| Module | What it is | Reproduces |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| [`compress/deflate.ts`](./compress/deflate.ts) | `Deflater(level)` and `Inflater`, raw DEFLATE. Levels 0 (stored), 1 to 3 (greedy on a four-byte hash, `DEFLATE_FAST` = 1) and 4 to 9 (lazy, `DEFLATE_DEFAULT` = 6); each block is the smallest of dynamic, fixed and stored by its exact size. `compress(src, off, len, out, at)` answers the end or `DEFLATE_NO_ROOM` (-1); `deflateBound(n)` / `compressBound(n)` are the most any level writes. `decompress(src, off, len, out, at, limit)` answers the end or a `DEFLATE_ERR_*` code (truncated, block type, stored length, Huffman, symbol, distance, output limit, trailing) and never panics on a stream; `inflate` is the resumable step under it (`INFLATE_DONE`, `INFLATE_NEED_INPUT`, `INFLATE_NEED_OUTPUT`) | RFC 1951: every level against Node's zlib both ways (`tests/link/std_compress_node`), round trips of empty to 1 MB inputs, a 1,500-input fuzz, every prefix and random corruption refused (`tests/link/std_compress`) |
|
|
176
|
+
| [`compress/gzip.ts`](./compress/gzip.ts) | `GzipCompressor(level)` / `GzipDecompressor` one-shot (the decompressor reads concatenated members, refuses trailing bytes, checks the header CRC, CRC-32 and length: `GZIP_ERR_HEADER`, `GZIP_ERR_CRC`, `GZIP_ERR_SIZE`), `gzipCompress` / `gzipDecompress`; `GzipWriter` (`write`, `flush`, `finish`, `bound`) and `GzipReader` (`feed` or `inputBuffer` / `commit`, `endInput`, `read` answering bytes, `GZIP_NEED_INPUT`, `GZIP_END` or an error, with a total `limit`) for bodies that arrive in pieces | RFC 1952: Node's gzip at every level, strategy and window, through both readers; the writer's output through Node's gunzip (`std_compress_node`, `std_compress`) |
|
|
177
|
+
| [`compress/zlib.ts`](./compress/zlib.ts) | `ZlibCompressor(level)` / `ZlibDecompressor`, `zlibCompress` / `zlibDecompress`: the two-byte header and Adler-32 trailer (`ZLIB_ERR_HEADER`, `ZLIB_ERR_ADLER`, `ZLIB_ERR_DICTIONARY` for `FDICT`) | RFC 1950, against Node's `deflateSync` / `inflateSync` (`std_compress_node`) |
|
|
178
|
+
| [`compress/checksum.ts`](./compress/checksum.ts) | `Crc32` (slicing-by-8 tables built once; `update(crc, buf, off, len)`), `crc32` for a one-off, `adler32`, `adler32Update` | The check values of "123456789", pieces against the whole, Adler-32's deferred reduction against its definition (`std_compress`) |
|
|
179
|
+
|
|
161
180
|
## `nish/net` — the protocol stack
|
|
162
181
|
|
|
163
182
|
The protocol lanes of [WP34](../docs/wp34-hosting-cs.md) §5, written on
|
|
@@ -201,6 +220,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
201
220
|
| [`net/http3.ts`](./net/http3.ts) | `Http3Connection`, the server side of HTTP/3 over a `QuicConnection`'s streams: `next()` answers `H3_REQUEST` (`stream`, `fields`), `H3_DATA` (a window onto `data`, at most `bodyChunk`), `H3_TRAILERS`, `H3_END`, `H3_RESET`, `H3_WRITABLE`, `H3_GOAWAY` or the final `H3_ERROR`; `respond`, `writeData` (takes what the send buffer has room for), `writeTrailers`, `reset` and `goaway` write. It opens its control stream with SETTINGS and its QPACK streams (capacity 0, through `nish/net/qpack`), checks every field section with `nish/net/http-fields`, answers a section past `maxFieldSectionSize` with a 431, sends one `date` field from the connection's `date` bytes on every final head (straight after `:status`, counted against the client's field-section limit, never on trailers) unless the program's fields hold one, and closes the connection with RFC 9114's code for each connection error. No server push. A warm connection allocates nothing per request (`docs/security/http3.md`) | RFC 9114 §4–§8 and RFC 9204 §4.2: a Nish client over QUIC, requests with bodies both ways, trailers, the date and its limit, GOAWAY, cancellation, every error code (`tests/link/net_http3`, `net_http3_errors`) |
|
|
202
221
|
| [`net/http3-server.ts`](./net/http3-server.ts) | `Http3Server`: HTTP/3 over QUIC on one UDP socket for a program that owns its loop — a pool of `QuicConnection` slots with an `Http3Connection` each, a slot's built on the first Initial accepted into it, slots 0 up, and reused, while the connection-ID index, the timer wheel and the rings are sized from the cap at start-up; a `QuicListener` in front; datagrams routed by a hashed index of every slot's connection IDs; `receive(now, key)`, `tick(now)`, `ready()` for the next slot with news, `flush(now)` sending paced GSO flights, `timeout(now)` from a timer wheel. A handshake that did not choose ALPN `h3` is closed with no_application_protocol (RFC 9114 §3.1); a slot done after GOAWAY is closed and freed. Every slot's final responses carry the server's one `HttpDate` (`date`), refreshed at `connection(slot)` and `ready()` and formatted at most once a second. `stop()` releases every connection and the listener and wipes the configuration's stateless reset and Retry token keys, which are the server's once handed in, and its last answer; the keys are zeroed in place, so a `QuicServerConfig` serves one server (QUIC-5). Native only, as `nish:net` is | A Nish client across loopback (`tests/link/net_http3_server`): requests, a paced 300 KB response, GOAWAY and the slot freed, ALPN, a full pool, the arena over connections through one slot while the pinned date moves a second on for each, one date formatted for two slots; every key read back as zero after `stop()` (`tests/link/net_quic_server_wipes`); aioquic and `curl --http3` against its `serve` mode |
|
|
203
222
|
| [`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`) |
|
|
223
|
+
| [`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`) |
|
|
204
224
|
| [`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_*`) |
|
|
205
225
|
| [`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`) |
|
|
206
226
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/compress/checksum` — the two checksums the compressed formats carry:
|
|
3
|
+
* CRC-32 (ISO 3309, the polynomial gzip's trailer uses, RFC 1952 §8) and
|
|
4
|
+
* Adler-32 (zlib's trailer, RFC 1950 §9).
|
|
5
|
+
*
|
|
6
|
+
* import { Crc32 } from "nish/compress/checksum";
|
|
7
|
+
*
|
|
8
|
+
* const crc = new Crc32(); // the tables, once
|
|
9
|
+
* let c: u32 = crc.update(toU32(0), body, 0, 100);
|
|
10
|
+
* c = crc.update(c, body, 100, toI32(body.length) - 100); // the same as one call
|
|
11
|
+
*
|
|
12
|
+
* A module has no global state, so CRC-32's tables live in a `Crc32` object:
|
|
13
|
+
* build one and keep it, because building it is 2,048 table entries (a few
|
|
14
|
+
* microseconds) and an update of a 5 KB body is about as long again. `crc32`
|
|
15
|
+
* is the one-off convenience that builds a table for one call.
|
|
16
|
+
*
|
|
17
|
+
* CRC-32 is slicing-by-8: eight 256-entry tables, eight bytes per step with
|
|
18
|
+
* one table read per byte and no dependency between the reads of a step
|
|
19
|
+
* (Kounavis and Berry, "A Systematic Approach to Building High Performance
|
|
20
|
+
* Software-based CRC Generators", 2005). Adler-32 defers its two modulo
|
|
21
|
+
* reductions to once every 5,552 bytes, the most that cannot overflow 32 bits
|
|
22
|
+
* (RFC 1950 §9 and zlib's `NMAX`).
|
|
23
|
+
*
|
|
24
|
+
* Neither is a cryptographic hash: both detect accidental corruption only.
|
|
25
|
+
* Written from the specifications, not ported from another implementation.
|
|
26
|
+
*
|
|
27
|
+
* The table reads in `update` are `nish:unsafe`'s `uncheckedGet`: every index
|
|
28
|
+
* is a byte plus a multiple of 256 below 2,048, and the table is built here at
|
|
29
|
+
* exactly that length and never replaced, which is the whole proof. The input
|
|
30
|
+
* window is checked once, before the loop.
|
|
31
|
+
*/
|
|
32
|
+
import { uncheckedGet, uncheckedSet } from "nish:unsafe"
|
|
33
|
+
|
|
34
|
+
/** The reflected CRC-32 polynomial, x^32 + x^26 + ... + 1 (RFC 1952 §8). */
|
|
35
|
+
const CRC32_POLY: i64 = 0xedb88320
|
|
36
|
+
|
|
37
|
+
/** The entries of the eight slicing tables, 256 each. */
|
|
38
|
+
const CRC32_TABLE_SIZE: i32 = 2048
|
|
39
|
+
|
|
40
|
+
/** zlib's `NMAX`: the most bytes Adler-32's sums take before they must be reduced. */
|
|
41
|
+
const ADLER32_NMAX: i32 = 5552
|
|
42
|
+
|
|
43
|
+
/** The Adler-32 modulus, the largest prime below 2^16 (RFC 1950 §9). */
|
|
44
|
+
const ADLER32_BASE: i32 = 65521
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The little-endian 64-bit word at `buf[p .. p + 8)`, which the caller has
|
|
48
|
+
* checked. `p`'s range makes each `p + k` an `add nsw`, which lets LLVM load
|
|
49
|
+
* the word at once.
|
|
50
|
+
*/
|
|
51
|
+
const checksumLoad64 = (buf: u8[], p: integer<0, 2147483639>): u64 =>
|
|
52
|
+
toU64(uncheckedGet(buf, p)) |
|
|
53
|
+
(toU64(uncheckedGet(buf, p + 1)) << toU64(8)) |
|
|
54
|
+
(toU64(uncheckedGet(buf, p + 2)) << toU64(16)) |
|
|
55
|
+
(toU64(uncheckedGet(buf, p + 3)) << toU64(24)) |
|
|
56
|
+
(toU64(uncheckedGet(buf, p + 4)) << toU64(32)) |
|
|
57
|
+
(toU64(uncheckedGet(buf, p + 5)) << toU64(40)) |
|
|
58
|
+
(toU64(uncheckedGet(buf, p + 6)) << toU64(48)) |
|
|
59
|
+
(toU64(uncheckedGet(buf, p + 7)) << toU64(56))
|
|
60
|
+
|
|
61
|
+
/** Panics unless `buf[off .. off + len)` lies inside `buf`. */
|
|
62
|
+
const checksumWindow = (what: string, buf: u8[], off: i32, len: i32): void => {
|
|
63
|
+
if (off < 0 || len < 0 || off > toI32(buf.length) - len) {
|
|
64
|
+
panic(`${what}: the window lies outside its array`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* CRC-32's lookup tables, built once and read by every `update`. The object
|
|
70
|
+
* holds nothing else, so one may be shared by every compressor and reader in
|
|
71
|
+
* a thread, and by threads that only read it.
|
|
72
|
+
*/
|
|
73
|
+
export class Crc32 {
|
|
74
|
+
/** `table[k * 256 + b]`: the CRC of byte `b` followed by `k` zero bytes. */
|
|
75
|
+
readonly table: u32[]
|
|
76
|
+
|
|
77
|
+
/** Builds the eight tables. */
|
|
78
|
+
constructor() {
|
|
79
|
+
const t: u32[] = new Array<u32>(CRC32_TABLE_SIZE)
|
|
80
|
+
for (let i: i32 = 0; i < 256; i += 1) {
|
|
81
|
+
let c: u32 = toU32(i)
|
|
82
|
+
for (let k: i32 = 0; k < 8; k += 1) {
|
|
83
|
+
c = (c & 1) !== 0 ? toU32(CRC32_POLY) ^ (c >>> 1) : c >>> 1
|
|
84
|
+
}
|
|
85
|
+
uncheckedSet(t, i, c)
|
|
86
|
+
}
|
|
87
|
+
for (let i: i32 = 256; i < CRC32_TABLE_SIZE; i += 1) {
|
|
88
|
+
const prev: u32 = uncheckedGet(t, i - 256)
|
|
89
|
+
uncheckedSet(t, i, (prev >>> 8) ^ uncheckedGet(t, toI32(prev & 255)))
|
|
90
|
+
}
|
|
91
|
+
this.table = t
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The CRC-32 of the bytes `crc` was the CRC of, followed by
|
|
96
|
+
* `buf[off .. off + len)`. Start a new checksum from 0. A window outside
|
|
97
|
+
* `buf` panics.
|
|
98
|
+
*/
|
|
99
|
+
update(crc: u32, buf: u8[], off: i32, len: i32): u32 {
|
|
100
|
+
checksumWindow("Crc32.update", buf, off, len)
|
|
101
|
+
const t: u32[] = this.table
|
|
102
|
+
if (toI32(t.length) !== CRC32_TABLE_SIZE) {
|
|
103
|
+
panic("Crc32.update: the table is not its size")
|
|
104
|
+
}
|
|
105
|
+
let c: u32 = ~crc
|
|
106
|
+
let p: i32 = off
|
|
107
|
+
const end: i32 = off + len
|
|
108
|
+
const whole: i32 = off + (len - (len & 7))
|
|
109
|
+
while (p < whole && p >= 0 && p <= 2147483639) {
|
|
110
|
+
const w: u64 = checksumLoad64(buf, p)
|
|
111
|
+
const lo: u32 = c ^ toU32(w)
|
|
112
|
+
const hi: u32 = toU32(w >> toU64(32))
|
|
113
|
+
c =
|
|
114
|
+
uncheckedGet(t, 1792 + toI32(lo & 255)) ^
|
|
115
|
+
uncheckedGet(t, 1536 + toI32((lo >>> 8) & 255)) ^
|
|
116
|
+
uncheckedGet(t, 1280 + toI32((lo >>> 16) & 255)) ^
|
|
117
|
+
uncheckedGet(t, 1024 + toI32(lo >>> 24)) ^
|
|
118
|
+
uncheckedGet(t, 768 + toI32(hi & 255)) ^
|
|
119
|
+
uncheckedGet(t, 512 + toI32((hi >>> 8) & 255)) ^
|
|
120
|
+
uncheckedGet(t, 256 + toI32((hi >>> 16) & 255)) ^
|
|
121
|
+
uncheckedGet(t, toI32(hi >>> 24))
|
|
122
|
+
p += 8
|
|
123
|
+
}
|
|
124
|
+
while (p < end) {
|
|
125
|
+
c = uncheckedGet(t, toI32((c ^ toU32(uncheckedGet(buf, p))) & 255)) ^ (c >>> 8)
|
|
126
|
+
p += 1
|
|
127
|
+
}
|
|
128
|
+
return ~c
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The CRC-32 of `buf[off .. off + len)`. It builds a `Crc32` for the one
|
|
134
|
+
* call, which costs about as much as checksumming 5 KB: keep a `Crc32` for
|
|
135
|
+
* anything repeated. A window outside `buf` panics.
|
|
136
|
+
*/
|
|
137
|
+
export const crc32 = (buf: u8[], off: i32, len: i32): u32 => new Crc32().update(toU32(0), buf, off, len)
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The Adler-32 of the bytes `adler` was the Adler-32 of, followed by
|
|
141
|
+
* `buf[off .. off + len)` (RFC 1950 §9). Start a new checksum from 1. A
|
|
142
|
+
* window outside `buf` panics.
|
|
143
|
+
*/
|
|
144
|
+
export const adler32Update = (adler: u32, buf: u8[], off: i32, len: i32): u32 => {
|
|
145
|
+
checksumWindow("adler32Update", buf, off, len)
|
|
146
|
+
let s1: u32 = adler & 0xffff
|
|
147
|
+
let s2: u32 = adler >>> 16
|
|
148
|
+
let p: i32 = off
|
|
149
|
+
const end: i32 = off + len
|
|
150
|
+
while (p < end) {
|
|
151
|
+
let stop: i32 = p + ADLER32_NMAX
|
|
152
|
+
if (stop > end) {
|
|
153
|
+
stop = end
|
|
154
|
+
}
|
|
155
|
+
while (p + 4 <= stop) {
|
|
156
|
+
s1 += toU32(uncheckedGet(buf, p))
|
|
157
|
+
s2 += s1
|
|
158
|
+
s1 += toU32(uncheckedGet(buf, p + 1))
|
|
159
|
+
s2 += s1
|
|
160
|
+
s1 += toU32(uncheckedGet(buf, p + 2))
|
|
161
|
+
s2 += s1
|
|
162
|
+
s1 += toU32(uncheckedGet(buf, p + 3))
|
|
163
|
+
s2 += s1
|
|
164
|
+
p += 4
|
|
165
|
+
}
|
|
166
|
+
while (p < stop) {
|
|
167
|
+
s1 += toU32(uncheckedGet(buf, p))
|
|
168
|
+
s2 += s1
|
|
169
|
+
p += 1
|
|
170
|
+
}
|
|
171
|
+
s1 = s1 % toU32(ADLER32_BASE)
|
|
172
|
+
s2 = s2 % toU32(ADLER32_BASE)
|
|
173
|
+
}
|
|
174
|
+
return (s2 << 16) | s1
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The Adler-32 of `buf[off .. off + len)`. A window outside `buf` panics. */
|
|
178
|
+
export const adler32 = (buf: u8[], off: i32, len: i32): u32 => adler32Update(toU32(1), buf, off, len)
|