@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 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`), and `NL7002`'s
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. `--unchecked-indexing` and
251
- `--wrapping` are deprecated (NL9014, NL9015) and reach only your own package,
252
- never a dependency or `nish/`. To migrate off `--unchecked-indexing`, run
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. The deprecated
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()` are deprecated (NL7001).
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. **Do not write `Arena.release` or `Arena.reset`**: releasing while
2498
- anything allocated after the mark is still referenced is undefined behaviour
2499
- the compiler does not check, so both are deprecated, and each call compiles
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
- `--unchecked-indexing` is not a proof, and an index it leaves unchecked is
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "The nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
@@ -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`.
@@ -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]() {} };
@@ -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
- // neither proved in range nor was told to wrap (`--wrapping`): the exact
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, whatever
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`/`release`/`reset` are no-ops rather than errors so
269
- // that a program which manages memory explicitly still runs; `used()` answers
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
@@ -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`, unless the program was built with `--unchecked-indexing`. */
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;
@@ -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
- unless the program was built with `--unchecked-indexing`. A `read` that a
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 Arena.reset() cannot free it. Built once by @main.
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)