@amritk/nish-x86_64-linux 0.27.2 → 0.28.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 +13 -8
- package/bin/nish +0 -0
- package/docs/AI.md +19 -2
- package/package.json +1 -1
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +49 -0
- package/runtime/nish.h +50 -5
- package/runtime/nish.mjs +6 -0
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-parallel.c +21 -0
- package/runtime/runtime-sandbox.c +470 -0
- package/runtime/runtime-sha256.c +239 -0
- package/scripts/build.sh +44 -23
- package/std/README.md +15 -10
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-value.ts +510 -17
- package/std/net/http-compress.ts +259 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +61 -12
- package/std/net/postgres.ts +405 -0
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/term-io.ts +35 -8
- package/std/yaml-json.ts +841 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/* Nish runtime, the SHA-256 half: the block function of FIPS 180-4 §6.2.2 on
|
|
2
|
+
* the CPU's own SHA-256 instructions. std/crypto/sha256.ts is the hash; this is
|
|
3
|
+
* a faster body for its private `sha256Compress`, which the compiler calls in
|
|
4
|
+
* that function's place when `nish_sha256_hw` says this machine can run it
|
|
5
|
+
* (src/kernels.ts).
|
|
6
|
+
*
|
|
7
|
+
* A translation unit of its own for the reason runtime-aes.c is one: its own
|
|
8
|
+
* measured `.text*` ceiling in tests/runtime-budgets.js, and a program that
|
|
9
|
+
* hashes links no AES and one that encrypts no SHA-256. Section GC means a
|
|
10
|
+
* program that never calls the kernel links none of this file.
|
|
11
|
+
*
|
|
12
|
+
* There are two paths and no portable one. x86-64 runs the SHA extensions
|
|
13
|
+
* (SHA-NI: `sha256rnds2`, `sha256msg1`, `sha256msg2`) with SSSE3's byte shuffle
|
|
14
|
+
* and SSE4.1's word blend, compiled for those extensions alone with
|
|
15
|
+
* `target(...)` so the binary stays a baseline binary; AArch64 runs the ARMv8
|
|
16
|
+
* Cryptography Extension's SHA-256 instructions, likewise with
|
|
17
|
+
* `target("+sha2")`. A machine without them, or a process with
|
|
18
|
+
* `NISH_SHA256=soft`, runs the Nish code in std/crypto/sha256.ts instead: the
|
|
19
|
+
* compiler keeps that code and branches on `nish_sha256_hw` at each call. Every
|
|
20
|
+
* other target, wasm32 included, compiles `nish_sha256_hw` to `return 0` and
|
|
21
|
+
* the kernel to a trap nothing reaches, so a link that names it still resolves.
|
|
22
|
+
*
|
|
23
|
+
* The kernel checks the guard `sha256Compress` runs its body under and returns
|
|
24
|
+
* 0 without touching anything when it fails, so the caller runs the Nish
|
|
25
|
+
* function, which panics with its own message: both paths fail the same way.
|
|
26
|
+
*
|
|
27
|
+
* Constant time: the kernel's branches are on array headers and `at`, which are
|
|
28
|
+
* public, and its loads and stores are at addresses fixed by them; the
|
|
29
|
+
* round-constant table is read whole, in order. tests/ct-asm-runtime.js reads
|
|
30
|
+
* the `clang -O2` assembly of this file for both targets and holds the kernel
|
|
31
|
+
* to that. The one branch on anything else is `nish_sha256_hw`'s, on the CPU's
|
|
32
|
+
* feature bits and `NISH_SHA256` (docs/security/ct-verification.md).
|
|
33
|
+
*
|
|
34
|
+
* Wiping: the kernel keeps no buffer of its own; the state and the message
|
|
35
|
+
* words live in vector registers between the load from `src` and the store to
|
|
36
|
+
* `h`. What sits in those registers, and what the register allocator spills,
|
|
37
|
+
* no store from C can reach (docs/security/crypto-sha256-hw.md).
|
|
38
|
+
*
|
|
39
|
+
* The algorithm is FIPS 180-4 §6.2.2; the instruction sequences are the ones
|
|
40
|
+
* the instructions are defined for, written here from Intel's "Intel SHA
|
|
41
|
+
* Extensions" (S. Gulley et al., 2013) and Arm's description of SHA256H,
|
|
42
|
+
* SHA256H2, SHA256SU0 and SHA256SU1 (Arm ARM, DDI 0487).
|
|
43
|
+
*/
|
|
44
|
+
#include <stddef.h>
|
|
45
|
+
#include <stdint.h>
|
|
46
|
+
|
|
47
|
+
#include "nish.h"
|
|
48
|
+
|
|
49
|
+
#if defined(__x86_64__)
|
|
50
|
+
#include <cpuid.h>
|
|
51
|
+
#include <immintrin.h>
|
|
52
|
+
#define NISH_SHA256_X86 1
|
|
53
|
+
#elif defined(__aarch64__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
|
54
|
+
#include <arm_neon.h>
|
|
55
|
+
#define NISH_SHA256_ARM 1
|
|
56
|
+
#endif
|
|
57
|
+
|
|
58
|
+
#if defined(NISH_SHA256_X86) || defined(NISH_SHA256_ARM)
|
|
59
|
+
|
|
60
|
+
/* Named here rather than through <stdlib.h>, for runtime-aes.c's reason: the
|
|
61
|
+
constant-time check compiles this file for targets whose libc headers the
|
|
62
|
+
host does not have. */
|
|
63
|
+
char *getenv(const char *name);
|
|
64
|
+
|
|
65
|
+
#if defined(__has_attribute) && __has_attribute(no_builtin)
|
|
66
|
+
#define NISH_SHA256_NO_LIBC no_builtin
|
|
67
|
+
#else
|
|
68
|
+
#define NISH_SHA256_NO_LIBC
|
|
69
|
+
#endif
|
|
70
|
+
#if defined(NISH_SHA256_X86)
|
|
71
|
+
#define NISH_SHA256_TARGET __attribute__((target("sha,sse4.1,ssse3"), NISH_SHA256_NO_LIBC))
|
|
72
|
+
#else
|
|
73
|
+
#define NISH_SHA256_TARGET __attribute__((target("+sha2"), NISH_SHA256_NO_LIBC))
|
|
74
|
+
#endif
|
|
75
|
+
#define NISH_SHA256_INLINE static inline __attribute__((always_inline)) NISH_SHA256_TARGET
|
|
76
|
+
|
|
77
|
+
/* K of FIPS 180-4 §4.2.2: the first 32 bits of the fractional parts of the cube
|
|
78
|
+
roots of the first sixty-four primes. */
|
|
79
|
+
static const uint32_t nish_sha256_k[64] __attribute__((aligned(16))) = {
|
|
80
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
81
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
82
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
83
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
84
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
85
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
86
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
87
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
#if defined(NISH_SHA256_X86)
|
|
91
|
+
/* One block. `sha256rnds2` runs two rounds on the state split as ABEF and CDGH
|
|
92
|
+
(the words high lane to low), taking W + K for the two rounds from its third
|
|
93
|
+
operand's low two lanes, so the eight words are shuffled into that order
|
|
94
|
+
once, on the way in, and back on the way out. */
|
|
95
|
+
NISH_SHA256_INLINE void nish_sha256_block(char *state, const char *in) {
|
|
96
|
+
/* Each of the sixteen bytes' places in a big-endian word read little-endian. */
|
|
97
|
+
const __m128i bswap = _mm_set_epi64x(0x0c0d0e0f08090a0bLL, 0x0405060700010203LL);
|
|
98
|
+
__m128i dcba = _mm_loadu_si128((const __m128i *)state);
|
|
99
|
+
__m128i hgfe = _mm_loadu_si128((const __m128i *)(state + 16));
|
|
100
|
+
__m128i cdab = _mm_shuffle_epi32(dcba, 0xb1);
|
|
101
|
+
__m128i efgh = _mm_shuffle_epi32(hgfe, 0x1b);
|
|
102
|
+
__m128i abef = _mm_alignr_epi8(cdab, efgh, 8);
|
|
103
|
+
__m128i cdgh = _mm_blend_epi16(efgh, cdab, 0xf0);
|
|
104
|
+
const __m128i abef0 = abef;
|
|
105
|
+
const __m128i cdgh0 = cdgh;
|
|
106
|
+
/* W in four-word groups, a ring of the last four: group i is W[4i .. 4i + 4). */
|
|
107
|
+
__m128i w[4];
|
|
108
|
+
for (int i = 0; i < 16; i++) {
|
|
109
|
+
if (i < 4) {
|
|
110
|
+
w[i] = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(in + 16 * i)), bswap);
|
|
111
|
+
} else {
|
|
112
|
+
/* (6.2) for t = 4i .. 4i + 3: msg1 adds σ0 of the next word to W[t - 16],
|
|
113
|
+
the align brings in W[t - 7], and msg2 adds σ1 of W[t - 2], two of
|
|
114
|
+
which it computes itself from the words before. */
|
|
115
|
+
__m128i w7 = _mm_alignr_epi8(w[(i - 1) & 3], w[(i - 2) & 3], 4);
|
|
116
|
+
w[i & 3] = _mm_sha256msg2_epu32(_mm_add_epi32(_mm_sha256msg1_epu32(w[i & 3], w[(i - 3) & 3]), w7),
|
|
117
|
+
w[(i - 1) & 3]);
|
|
118
|
+
}
|
|
119
|
+
__m128i wk = _mm_add_epi32(w[i & 3], _mm_load_si128((const __m128i *)&nish_sha256_k[4 * i]));
|
|
120
|
+
cdgh = _mm_sha256rnds2_epu32(cdgh, abef, wk);
|
|
121
|
+
abef = _mm_sha256rnds2_epu32(abef, cdgh, _mm_shuffle_epi32(wk, 0x0e));
|
|
122
|
+
}
|
|
123
|
+
abef = _mm_add_epi32(abef, abef0);
|
|
124
|
+
cdgh = _mm_add_epi32(cdgh, cdgh0);
|
|
125
|
+
__m128i feba = _mm_shuffle_epi32(abef, 0x1b);
|
|
126
|
+
__m128i dchg = _mm_shuffle_epi32(cdgh, 0xb1);
|
|
127
|
+
_mm_storeu_si128((__m128i *)state, _mm_blend_epi16(feba, dchg, 0xf0));
|
|
128
|
+
_mm_storeu_si128((__m128i *)(state + 16), _mm_alignr_epi8(dchg, feba, 8));
|
|
129
|
+
}
|
|
130
|
+
#else
|
|
131
|
+
/* One block. SHA256H and SHA256H2 run four rounds on ABCD and EFGH together,
|
|
132
|
+
each giving one half of the new state, with W + K for the four in their
|
|
133
|
+
third operand. */
|
|
134
|
+
NISH_SHA256_INLINE void nish_sha256_block(char *state, const char *in) {
|
|
135
|
+
uint32x4_t abcd = vld1q_u32((const uint32_t *)state);
|
|
136
|
+
uint32x4_t efgh = vld1q_u32((const uint32_t *)(state + 16));
|
|
137
|
+
const uint32x4_t abcd0 = abcd;
|
|
138
|
+
const uint32x4_t efgh0 = efgh;
|
|
139
|
+
uint32x4_t w[4];
|
|
140
|
+
for (int i = 0; i < 16; i++) {
|
|
141
|
+
if (i < 4) {
|
|
142
|
+
w[i] = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8((const uint8_t *)(in + 16 * i))));
|
|
143
|
+
} else {
|
|
144
|
+
/* (6.2) for t = 4i .. 4i + 3: SU0 adds σ0 to W[t - 16], SU1 adds W[t - 7]
|
|
145
|
+
and σ1 of W[t - 2]. */
|
|
146
|
+
w[i & 3] = vsha256su1q_u32(vsha256su0q_u32(w[i & 3], w[(i - 3) & 3]), w[(i - 2) & 3], w[(i - 1) & 3]);
|
|
147
|
+
}
|
|
148
|
+
uint32x4_t wk = vaddq_u32(w[i & 3], vld1q_u32(&nish_sha256_k[4 * i]));
|
|
149
|
+
uint32x4_t before = abcd;
|
|
150
|
+
abcd = vsha256hq_u32(abcd, efgh, wk);
|
|
151
|
+
efgh = vsha256h2q_u32(efgh, before, wk);
|
|
152
|
+
}
|
|
153
|
+
vst1q_u32((uint32_t *)state, vaddq_u32(abcd, abcd0));
|
|
154
|
+
vst1q_u32((uint32_t *)(state + 16), vaddq_u32(efgh, efgh0));
|
|
155
|
+
}
|
|
156
|
+
#endif
|
|
157
|
+
|
|
158
|
+
/* `sha256Compress`'s own guard, then the block. That guard reads the source's
|
|
159
|
+
length through `toI32`, which saturates at 2^31 - 1, so it refuses a block
|
|
160
|
+
ending past that offset whatever the length: `at > INT32_MAX - 64`. */
|
|
161
|
+
NISH_SHA256_TARGET int32_t nish_sha256_compress(nish_array *h, const nish_array *w, const nish_array *src,
|
|
162
|
+
int32_t at) {
|
|
163
|
+
if (h->len < 8 || w->len < 64 || at < 0 || at > INT32_MAX - 64 || src->len < 64 || (uint64_t)at > src->len - 64)
|
|
164
|
+
return 0;
|
|
165
|
+
nish_sha256_block(h->data, src->data + at);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#if defined(NISH_SHA256_X86)
|
|
170
|
+
/* Whether this CPU runs the kernel: CPUID leaf 7's EBX has SHA (bit 29), and
|
|
171
|
+
leaf 1's ECX SSE4.1 (bit 19) and SSSE3 (bit 9). Asked through `cpuid.h`, for
|
|
172
|
+
runtime-aes.c's reason; `__get_cpuid_count` answers 0 when leaf 7 is past
|
|
173
|
+
the CPU's highest. */
|
|
174
|
+
static int32_t nish_sha256_cpu(void) {
|
|
175
|
+
unsigned a, b, c, d;
|
|
176
|
+
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
|
|
177
|
+
if ((c & bit_SSE4_1) == 0 || (c & bit_SSSE3) == 0) return 0;
|
|
178
|
+
if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
|
|
179
|
+
return (b & bit_SHA) != 0;
|
|
180
|
+
}
|
|
181
|
+
#elif defined(__linux__)
|
|
182
|
+
/* Linux reports the ARMv8 SHA-256 instructions in the auxiliary vector:
|
|
183
|
+
AT_HWCAP is 16, and its HWCAP_SHA2 bit is 6
|
|
184
|
+
(arch/arm64/include/uapi/asm/hwcap.h). */
|
|
185
|
+
unsigned long getauxval(unsigned long type);
|
|
186
|
+
|
|
187
|
+
static int32_t nish_sha256_cpu(void) {
|
|
188
|
+
return (getauxval(16) & (1ul << 6)) != 0;
|
|
189
|
+
}
|
|
190
|
+
#elif defined(__APPLE__)
|
|
191
|
+
/* macOS names the feature in sysctl (macOS 12 and later). A system that does
|
|
192
|
+
not know the name answers as a CPU without it: the Nish path. */
|
|
193
|
+
int sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen);
|
|
194
|
+
|
|
195
|
+
static int32_t nish_sha256_cpu(void) {
|
|
196
|
+
int32_t has = 0;
|
|
197
|
+
size_t size = sizeof has;
|
|
198
|
+
if (sysctlbyname("hw.optional.arm.FEAT_SHA256", &has, &size, 0, 0) != 0) return 0;
|
|
199
|
+
return has != 0;
|
|
200
|
+
}
|
|
201
|
+
#else
|
|
202
|
+
/* Another AArch64 system: no question asked, so the Nish path. */
|
|
203
|
+
static int32_t nish_sha256_cpu(void) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
#endif
|
|
207
|
+
|
|
208
|
+
/* The answer: 0 until the first call, then 1 for the kernel or 2 for the Nish
|
|
209
|
+
path, with runtime-aes.c's relaxed atomics for the same reason. */
|
|
210
|
+
static int32_t nish_sha256_answer;
|
|
211
|
+
|
|
212
|
+
int32_t nish_sha256_hw(void) {
|
|
213
|
+
int32_t answer = __atomic_load_n(&nish_sha256_answer, __ATOMIC_RELAXED);
|
|
214
|
+
if (answer == 0) {
|
|
215
|
+
/* `soft` pins the Nish path and never asks the CPU; anything else, or
|
|
216
|
+
nothing, means the best this machine has. */
|
|
217
|
+
const char *pin = getenv("NISH_SHA256");
|
|
218
|
+
int soft = pin != 0 && pin[0] == 's' && pin[1] == 'o' && pin[2] == 'f' && pin[3] == 't' && pin[4] == 0;
|
|
219
|
+
answer = !soft && nish_sha256_cpu() ? 1 : 2;
|
|
220
|
+
__atomic_store_n(&nish_sha256_answer, answer, __ATOMIC_RELAXED);
|
|
221
|
+
}
|
|
222
|
+
return answer == 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#else
|
|
226
|
+
|
|
227
|
+
/* A target with no SHA-256 instructions this file uses, wasm32 among them: the
|
|
228
|
+
Nish path always, and a kernel that a program whose every call is behind
|
|
229
|
+
`nish_sha256_hw` never reaches. */
|
|
230
|
+
int32_t nish_sha256_hw(void) {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
int32_t nish_sha256_compress(nish_array *h, const nish_array *w, const nish_array *src, int32_t at) {
|
|
235
|
+
(void)h, (void)w, (void)src, (void)at;
|
|
236
|
+
__builtin_trap();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#endif
|
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 thirteen 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
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
# <dir>/runtime-simd.c, the byte searches that read a vector at a time and
|
|
14
14
|
# the SSE4.1 rounding of `Math.floor` and its kin,
|
|
15
15
|
# <dir>/runtime-aes.c, AES-GCM on the CPU's AES instructions,
|
|
16
|
-
# <dir>/runtime-
|
|
17
|
-
# <dir>/runtime-
|
|
16
|
+
# <dir>/runtime-sha256.c, SHA-256 on the CPU's SHA instructions,
|
|
17
|
+
# <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
|
|
18
|
+
# <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
|
|
19
|
+
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`, and
|
|
20
|
+
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies. Each of those
|
|
18
21
|
# files says why they are compiled and measured apart.
|
|
19
22
|
#
|
|
20
23
|
# Profiles:
|
|
@@ -169,6 +172,15 @@ case "$profile" in
|
|
|
169
172
|
if grep -E '^define [^@]*@memory\(' "$i" | grep -Evq '^define (internal|private) '; then
|
|
170
173
|
echo "error: $i defines a function named \`memory\`, the name the $profile profile's module exports its linear memory under; rename the function" >&2
|
|
171
174
|
exit 2
|
|
175
|
+
fi
|
|
176
|
+
# `--sandbox` IR compiled for the host confines through Linux system
|
|
177
|
+
# calls a wasm module has none of; the compiler emits no call for a
|
|
178
|
+
# wasm target, so only IR that never told it reaches this. The match
|
|
179
|
+
# is the call instruction, not the name: the compiler's own IR holds
|
|
180
|
+
# the name as string data.
|
|
181
|
+
if grep -Eq '^[[:space:]]+call void @nish_sandbox\(' "$i"; then
|
|
182
|
+
echo "error: $i was compiled with --sandbox, and a $profile module has no sandbox; compile it with --target wasm32 and without --sandbox" >&2
|
|
183
|
+
exit 2
|
|
172
184
|
fi ;;
|
|
173
185
|
esac
|
|
174
186
|
done ;;
|
|
@@ -195,15 +207,17 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
|
|
|
195
207
|
fi
|
|
196
208
|
[ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
197
209
|
|
|
198
|
-
# The runtime is
|
|
210
|
+
# The runtime is thirteen translation units, and a caller names one: whoever passes
|
|
199
211
|
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
|
|
200
212
|
# <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
|
|
201
|
-
# <dir>/runtime-aes.c, <dir>/runtime-
|
|
213
|
+
# <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
|
|
214
|
+
# <dir>/runtime-bytes.c and <dir>/runtime-sandbox.c compiled beside it, on every profile,
|
|
202
215
|
# wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
|
|
203
216
|
# unless the build adds -msimd128,
|
|
204
|
-
# runtime-aes.c
|
|
205
|
-
# because the compiler emits no call to
|
|
206
|
-
# runtime-term.c reads stdin and answers "not a terminal"
|
|
217
|
+
# runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
|
|
218
|
+
# stubs that nothing calls, because the compiler emits no call to their kernels for wasm32, and
|
|
219
|
+
# runtime-term.c reads stdin and answers "not a terminal", and runtime-sandbox.c
|
|
220
|
+
# compiles to two functions that refuse to run, which no wasm build calls. They were one file until the
|
|
207
221
|
# operating-system half was split out for its own size budget, and the parallel
|
|
208
222
|
# half followed for the same reason (each file's header comment says why), and
|
|
209
223
|
# a link line is where those splits
|
|
@@ -213,7 +227,7 @@ fi
|
|
|
213
227
|
# here keeps every one of those correct, and keeps "the runtime" one thing to
|
|
214
228
|
# name from the outside. A caller that names one itself is left alone, because
|
|
215
229
|
# naming one object twice is a duplicate-symbol error.
|
|
216
|
-
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-term.c runtime-bytes.c"
|
|
230
|
+
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"
|
|
217
231
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
218
232
|
case "$i" in
|
|
219
233
|
*/runtime.c|runtime.c)
|
|
@@ -635,22 +649,29 @@ case "$profile" in
|
|
|
635
649
|
exit 2
|
|
636
650
|
fi
|
|
637
651
|
# IR compiled for the host rather than with `--target wasm32` calls std/crypto/aes.ts's
|
|
638
|
-
# AES kernels behind `nish_aes_hw
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
652
|
+
# AES kernels behind `nish_aes_hw`, std/crypto/sha256.ts's block function behind
|
|
653
|
+
# `nish_sha256_hw` and std/compress/checksum.ts's CRC-32 kernel behind
|
|
654
|
+
# `nish_crc32_hw` (src/kernels.ts). runtime-aes.c's, runtime-sha256.c's and
|
|
655
|
+
# runtime-checksum.c's wasm32 builds answer 0 there, so the Nish code runs, and
|
|
656
|
+
# each is added only for such IR: every other module links the bytes it always did.
|
|
657
|
+
# Each pair is a unit and the kernel its probe names.
|
|
658
|
+
# A caller that names the unit itself is left alone, as `halves` leaves it:
|
|
642
659
|
# one object named twice is a duplicate-symbol error.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
660
|
+
for pair in aes:aes sha256:sha256 checksum:crc32; do
|
|
661
|
+
unit=${pair%%:*}
|
|
662
|
+
kernel=${pair#*:}
|
|
663
|
+
named=0
|
|
664
|
+
wanted=0
|
|
665
|
+
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
666
|
+
case "$i" in
|
|
667
|
+
*/runtime-"$unit".c|runtime-"$unit".c) named=1 ;;
|
|
668
|
+
*.ll) if grep -q "@nish_${kernel}_hw(" "$i"; then wanted=1; fi ;;
|
|
669
|
+
esac
|
|
670
|
+
done
|
|
671
|
+
if [ "$wanted" = 1 ] && [ "$named" = 0 ]; then
|
|
672
|
+
inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-$unit.c")
|
|
673
|
+
fi
|
|
650
674
|
done
|
|
651
|
-
if [ "$aes_wanted" = 1 ] && [ "$aes_named" = 0 ]; then
|
|
652
|
-
inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-aes.c")
|
|
653
|
-
fi
|
|
654
675
|
# IR of either kind that imports std/json or calls nish/bytes's byte-class runs
|
|
655
676
|
# calls runtime-bytes.c's kernels (src/kernels.ts), which compile freestanding:
|
|
656
677
|
# the scalar path, or the 128-bit one under -msimd128. It is added the same way,
|
package/std/README.md
CHANGED
|
@@ -12,12 +12,17 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
12
12
|
| [`testing.ts`](./testing.ts) | a test runner: a `Suite` a program drives with straight-line assertions, printing the `PASS` / `FAIL` / `SKIP` lines the repository's own harness prints, and answering the exit code |
|
|
13
13
|
| [`text.ts`](./text.ts) | the string operations a program would otherwise write inline: `splitLines`, `splitWhitespace`, `trim` and its halves, `contains`, `replaceAll`, `firstDifference` over two arrays of lines, `codePointLength`, the number of Unicode code points (what JSON Schema's `minLength` counts, where `.length` counts UTF-8 bytes natively and UTF-16 units under Node; it answers the same on both by asking which it is reading), and `indexOfAny(text, bytes, from)`, the first index at or after `from` whose byte is one of 1 to 16 ASCII `bytes`. Its walker is the meaning and what runs under Node; natively the compiler replaces the one call to it with the runtime kernel `nish_str_index_of_any`, which reads 16 or 32 bytes at a time and is variable-time ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#byte-search-indexofany-in-nishtext)). `tests/link/std_text_index_of_any*` are its programs |
|
|
14
14
|
| [`math.ts`](./math.ts) | `Math.tan`, `Math.atan2` and `Math.hypot` on `f64`, ported from fdlibm (`LICENSE-fdlibm`) so that every target, wasm included, answers alike without libm. A program never imports it: a call to one of the three loads it, and the compiler copies the functions the call reaches into the calling module as `internal` ones. JavaScript's answer for every NaN, signed zero and infinity, and within one ulp elsewhere (0.83, 0.84 and 0.93 ulp measured against mpmath). `tests/cases/math_std_edges` pins the special values |
|
|
15
|
-
| [`regex.ts`](./regex.ts) | Regular expressions in JavaScript's syntax with the `u` flag's strictness, matched by a Pike VM in time linear in the input whatever the pattern; finding every match is one search per match, so `findAll` and its kin can be quadratic when a long branch is preferred to a short one that wins ([`docs/security/regex.md`](../docs/security/regex.md)). `regexCompile(pattern, flags)` answers a `Regex` or the error text; `test`, `exec` into a reused `RegexMatch`, `find`, `findAll`, `count`, `replace` and `replaceAll` with `$&`, `$1` and `$<name>`, and `split`. Flags `i`
|
|
15
|
+
| [`regex.ts`](./regex.ts) | Regular expressions in JavaScript's syntax with the `u` flag's strictness, matched by a Pike VM in time linear in the input whatever the pattern; finding every match is one search per match, so `findAll` and its kin can be quadratic when a long branch is preferred to a short one that wins ([`docs/security/regex.md`](../docs/security/regex.md)). `regexCompile(pattern, flags)` answers a `Regex` or the error text; `test`, `exec` into a reused `RegexMatch`, `find`, `findAll`, `count`, `replace` and `replaceAll` with `$&`, `$1` and `$<name>`, and `split`. Flags `i`, `m`, `s` and `u`; `i` folds by Unicode's simple case folding, ASCII letters only to ASCII. `\p{…}` and `\P{…}` take Unicode 16.0's General_Category, Script, Script_Extensions and binary properties by JavaScript's names, from tables `scripts/gen-regex-unicode.mjs` generates into the module and checks against Node's `RegExp`; backreferences and lookaround are refused at compile time. Offsets are UTF-8 bytes. A search allocates nothing beyond its answer: the thread lists and capture tables are the `Regex`'s, so one `Regex` serves one thread at a time. A leading literal is found with `indexOf`, a small leading set with `indexOfAny`. `tests/link/std_regex` and `tests/link/std_regex_unicode` check it against JavaScript's own answers |
|
|
16
16
|
| [`json.ts`](./json.ts) | `jsonField(object, name)`: the value of one field of one flat JSON object, which is the shape the compiler's own `--json` diagnostics have. A reader and not a parser — it answers text, answers `null` for a field that is not there, and does not validate. `JsonWriter` is the other direction: any JSON value written into a caller-owned `u8[]`, commas and colons included, byte for byte what `JSON.stringify` writes, and allocating nothing per call except an `f64`'s digits, a temporary string the call gives back before it returns (`text()` and `jsonStringifyString` build strings by design); misuse and a full buffer set a sticky negative `JSON_WRITE_*` code in `failed` rather than panicking. `setIndent(n)` and `setIndentTab()` write what `JSON.stringify(v, null, n)` or `(v, null, "\t")` writes, with no allocation for the indent, and `setStrict(true)` makes NaN and the infinities the sticky `JSON_WRITE_NON_FINITE` instead of `null`; both modes are off by default and kept by `reset`. `jsonStringifyString(s)` is the string rule answered as a string. `stringBytes(bytes, off, len)` and `keyBytes(bytes, off, len)` write a window of a byte array as `string` and `key` write a string of those bytes, allocating nothing, and refuse a window outside the array with `JSON_WRITE_BAD_WINDOW` |
|
|
17
17
|
| [`json-tokenizer.ts`](./json-tokenizer.ts) | `JsonTokenizer`: a validating, streaming tokenizer for RFC 8259 JSON held in a window of a `u8[]`. `next()` answers one `JSON_TOK_*` token at a time — object and array openers and closers, keys, strings, numbers and the three literals — then `JSON_TOK_END`, or a negative `JSON_TOK_ERR_*` code that sticks in `failed`, with the byte in `errorAt`. Strings are checked as UTF-8 and a lone surrogate escape is refused; `stringInto` decodes the current string into the caller's array and `keyEquals` compares it with a name, neither allocating. `numberF64` is correctly rounded; `isInteger` and `numberI64` read an integer that fits an `i64`, and `isU64` and `numberU64` one that fits a `u64`. `isIntegral` asks whether a number's value is whole however it is written (`1.0`, `1e2`, `1e400`), JSON Schema's "integer", from the text and at any exponent. `skipValue` steps over a value. `JsonLimits` caps nesting depth, string, number and total bytes, each with its own code; `reset` reuses the tokenizer, so a reused body keeps the arena flat. Duplicate keys are reported as tokens. No input panics and no byte outside the window is read; [`docs/security/json.md`](../docs/security/json.md) is its record, and `tests/link/std_json_tokenizer` runs JSONTestSuite and a seeded fuzz |
|
|
18
18
|
| [`json-typed.ts`](./json-typed.ts) | `jsonDecode(d, out)` and `jsonEncode(w, value)`: one JSON object read into a class and written back, with code the compiler writes at each call from the class's fields — scalars (`u64` exact to 2^64 - 1), strings, numeric enums, nested classes, arrays of those, `Map<string, V>` of those and `\| null` — through `JsonDecoder` (a `JsonTokenizer` and a sticky error) and `JsonWriter`. A class's one `static readonly json` literal renames a field's key and makes a key optional, a missing one setting its field to the initializer; otherwise every field is required, each key once, an unknown key refused unless `ignoreUnknown`, and every outcome a sticky `JSON_DECODE_*` or `JSON_TOK_ERR_*` code with its byte in `errorAt`; decoding into a reused object reuses its objects, arrays and equal strings, so the arena stays flat (a map's entries are made anew, at most `maxMapKeys` over all the maps of one decode, 4,096 by default); a key is sent to its field by its length, then one byte. A `JsonNumber` field keeps a number's text byte for byte. Under Node the types are erased and both calls panic ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#typed-json-nishjson-typed), [`docs/security/json.md`](../docs/security/json.md)) |
|
|
19
|
-
| [`json-value.ts`](./json-value.ts) | `JsonDocument`: any JSON document parsed into a tree of `JsonValue` handles (`i32`s into the document), read with `kind`, `boolean`, `string`, `stringEquals`, `length`, `at`, `keyAt` and `get`, built with the `new*` methods, `push` and `set`, and written back through `JsonWriter` with `write`, or as text with `stringify`. A number keeps its source text and is read on request as an `i64` or `u64` that fits (`isI64`, `isU64`), the nearest `f64`, or the text, and `isIntegral` asks whether its value is whole however it is written (`1.0`, `1e2`), so integers and decimals wider than any type round-trip byte for byte. Objects keep insertion order; a repeated key is `JSON_VALUE_ERR_DUPLICATE` unless `duplicates` is `JSON_VALUE_DUPLICATE_LAST`, `JSON.parse`'s rule. Every parse error and limit is the tokenizer's, with the depth held to the writer's 64 so that whatever parses writes back, nothing panics, and a value is in at most one container, so a tree built through `push` and `set` cannot hold itself. A handle carries the generation of the document it came from, so one kept across a `parse` or `clear` reads as nothing; `maxValues` (262,144 by default, `JSON_VALUE_ERR_VALUES` past it) bounds the memory a document keeps. Each `parse` reuses the document's arrays, so a reused document keeps the arena flat. `tests/link/std_json_value` runs JSONTestSuite through it ([`docs/security/json.md`](../docs/security/json.md)) |
|
|
19
|
+
| [`json-value.ts`](./json-value.ts) | `JsonDocument`: any JSON document parsed into a tree of `JsonValue` handles (`i32`s into the document), read with `kind`, `boolean`, `string`, `stringEquals`, `length`, `at`, `keyAt` and `get`, built with the `new*` methods, `push` and `set`, and written back through `JsonWriter` with `write`, or as text with `stringify`. A number keeps its source text and is read on request as an `i64` or `u64` that fits (`isI64`, `isU64`), the nearest `f64`, or the text, and `isIntegral` asks whether its value is whole however it is written (`1.0`, `1e2`), so integers and decimals wider than any type round-trip byte for byte. Objects keep insertion order; a repeated key is `JSON_VALUE_ERR_DUPLICATE` unless `duplicates` is `JSON_VALUE_DUPLICATE_LAST`, `JSON.parse`'s rule. Every parse error and limit is the tokenizer's, with the depth held to the writer's 64 so that whatever parses writes back, nothing panics, and a value is in at most one container, so a tree built through `push` and `set` cannot hold itself. A handle carries the generation of the document it came from, so one kept across a `parse` or `clear` reads as nothing; `maxValues` (262,144 by default, `JSON_VALUE_ERR_VALUES` past it) bounds the memory a document keeps. `parseRanged` and `parseTextRanged` also record each value's byte range (`start`, `end`) and each key's (`keyStart`, `keyEnd`), 20 bytes more per value, and `setRange` and `setKeyRange` give built values theirs; after `parse` or `parseText` every range reads `-1`. Each `parse` reuses the document's arrays, so a reused document keeps the arena flat. `tests/link/std_json_value` runs JSONTestSuite through it ([`docs/security/json.md`](../docs/security/json.md)) |
|
|
20
|
+
| [`json-pointer.ts`](./json-pointer.ts) | RFC 6901 JSON Pointers over a `JsonDocument`: `jsonPointerParse` (the decoded tokens, or `null`), `jsonPointerEscape` (one token, `~0` and `~1`), and `jsonPointerResolve` (the value, `JSON_POINTER_NONE` when the pointer names nothing, `JSON_POINTER_ERR_SYNTAX` when it is not one); `isJsonPointer` and `isRelativeJsonPointer` check JSON Schema's `json-pointer` and `relative-json-pointer` forms, reading the text once and allocating nothing. A resolve costs the pointer's length plus each object step's size. `tests/link/std_json_pointer` runs the suite's two format files through it |
|
|
21
|
+
| [`json-equal.ts`](./json-equal.ts) | JSON Schema's value equality for `const`, `enum` and `uniqueItems`: `jsonEqual` across two documents (numbers by value, `1` is `1.0`; member order ignored; `true` is not `1`), `jsonEqualCanonical` (a text equal for exactly the equal values), `jsonEqualHash` (64-bit FNV-1a of it, unkeyed) and `jsonEqualFirstDuplicate` (`-1` when the items are unique). Nothing recurses, so any depth a document holds compares; a call costs the values' sizes plus sorting object keys. `tests/link/std_json_equal` runs the suite's `const`, `enum` and `uniqueItems` |
|
|
22
|
+
| [`json-number.ts`](./json-number.ts) | Exact arithmetic on JSON number texts, never through `f64`: `jsonNumberCompare` (`-1`, `0`, `1`), `jsonNumberCanonical` (digits and exponent, equal for equal values) and `jsonNumberMultipleOf` (`1` or `0`). It refuses rather than guesses: `JSON_NUMBER_ERR_SYNTAX`, `JSON_NUMBER_ERR_EXPONENT` past `JSON_NUMBER_MAX_EXPONENT` (10^15), `JSON_NUMBER_ERR_DIVISOR` for zero, and `JSON_NUMBER_ERR_GAP` when `multipleOf` would take more than `JSON_NUMBER_MAX_STEPS` (2^20) digit steps (`1e1000000000` by `7`). A compare costs the texts' digits. `tests/link/std_json_number` runs the suite's `multipleOf`, bounds, `bignum` and `float-overflow` |
|
|
23
|
+
| [`json-format.ts`](./json-format.ts) | `jsonFormatCheck(id, value)` and `jsonFormatValidate(name, value)`: JSON Schema draft 2020-12's `format` checks, answering `JSON_FORMAT_VALID`, `JSON_FORMAT_INVALID`, or `JSON_FORMAT_UNKNOWN` for `idn-email`, `idn-hostname`, `json-pointer`, `relative-json-pointer` and any other name; `jsonFormatId(name)` resolves a name once per schema. Each format is a predicate too: `isDateTime`, `isDate`, `isTime`, `isDuration` (RFC 3339, the duration ABNF as written, so `P1Y1D` is invalid), `isEmail` (RFC 5321), `isHostname` (RFC 1123), `isIpv4`, `isIpv6`, `isUuid`, `isUri`, `isUriReference`, `isIri`, `isIriReference` (RFC 3986 and 3987, walked here because `nish/net/url` refuses valid URIs a client should not follow), `isUriTemplate` (RFC 6570) and `isRegex` (what `nish/regex` compiles, so lookaround and backreferences are false negatives). All but `isRegex` are linear and allocate nothing. `tests/link/std_json_format` runs JSON-Schema-Test-Suite's `optional/format` through it |
|
|
20
24
|
| [`yaml.ts`](./yaml.ts) | `parseYaml(text)` and `parseYamlWith(text, options)`: a YAML 1.2 stream parsed into a `YamlStream` of documents, nodes and diagnostics, struct of arrays with `i32` handles and `-1` for none. Every node and diagnostic keeps its byte range; a scalar is decoded (`text`) and resolved against the core schema (`type`) only when read. `keyText`, `pathNode`, `pathPair` and `pathMatch` find a key by path through aliases and `<<` merge keys (the map's own keys first, then each merged map in order). A parse never fails or panics on its input: errors and warnings carry the `YAML_ERR_*` codes other YAML tooling reports (`yamlErrorName`), with `maxDepth`, `uniqueKeys`, `merge`, `keepComments` and `firstDocumentOnly` in `YamlOptions`. A merge key's value must name maps (`BAD_MERGE`); tabs are not indentation. |
|
|
25
|
+
| [`yaml-json.ts`](./yaml-json.ts) | `YamlJsonLoader.load(stream, d, doc)`: document `d` of a `YamlStream` loaded into a `json-value` `JsonDocument`, with `setRange` and `setKeyRange` from the nodes' byte ranges. Core-schema scalars; hex and octal ints as exact decimal; `.inf` and `.nan` refused (`NOT_FINITE`); aliases copied and `<<` merges applied, own keys winning; a non-string key as its source text. The expansion is counted before anything is built, so an alias bomb is `EXPANSION` at the alias, past `maxValues` or `maxTotalBytes`. A failed load keeps nothing and answers a `YAML_JSON_ERR_*` code with its range. |
|
|
21
26
|
| [`yaml-lines.ts`](./yaml-lines.ts) | `YamlLineIndex`: byte offsets turned into 1-based `line` and `column` (characters) or `byteColumn`, by binary search over line starts found once with `indexOf`. LF, CR LF and a lone CR each break a line; an offset is clamped to the text first. |
|
|
22
27
|
| [`yaml-write.ts`](./yaml-write.ts) | `stringifyYaml(doc, v)`: a `json-value` value written as block-style YAML that `parseYaml` reads back as the same tree. A string YAML would read as something else (a null, bool or number word, an indicator, a `: ` or ` #`, an edge space, a control character) is double-quoted and escaped; a key over 1024 bytes is an explicit `? key`. `""` for a no-value handle or a tree deeper than `JSON_WRITER_MAX_DEPTH`. |
|
|
23
28
|
| [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
|
|
@@ -80,7 +85,7 @@ is listed in [`SECURITY.md`](../SECURITY.md#threat-model).
|
|
|
80
85
|
|
|
81
86
|
| Module | What it is | Reproduces |
|
|
82
87
|
| --- | --- | --- |
|
|
83
|
-
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK`. `sha256` panics on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.2 |
|
|
88
|
+
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK`. `sha256` panics on an array longer than 2^31 − 1 bytes. Natively, on a CPU with SHA-NI (and SSE4.1 and SSSE3) or the ARMv8 SHA-256 instructions, the compiler sends each block's compression to the runtime's kernel instead (`runtime/runtime-sha256.c`, WP38), and `NISH_SHA256=soft` pins the portable code | FIPS 180-4 §6.2 |
|
|
84
89
|
| [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK`. The one-shot `sha512` and `sha384` panic on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.4, §6.5 |
|
|
85
90
|
| [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual`. The one-shot functions panic on a message longer than 2^31 − 1 bytes. `HmacSha256Scratch` / `HmacSha384Scratch` are the same tag over hashers the caller allocates once — `begin(key, off, len)`, `update`, `finishInto(out, at)`, `wipe` — so a loop of them inside `using a = arena()` leaves the arena where it was; `hmacCopySha256` / `hmacCopySha384` copy a hasher's state into one the caller holds. `pbkdf2HmacSha256(password, salt, iterations, dkLen)` is PBKDF2 with HMAC-SHA-256, for SCRAM-SHA-256: the password goes in and the key comes out as a `Secret<u8[]>`, every intermediate is wiped, and fewer than one iteration or a `dkLen` below one answers `null` | RFC 2104, RFC 4231 §4, RFC 8018 §5.2 |
|
|
86
91
|
| [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen, for an `info` longer than 2^31 − 1 bytes, and for a PRK shorter than HashLen. `hkdfExpandLabelSha256` / `hkdfExpandLabelSha384` are TLS 1.3's HKDF-Expand-Label, which QUIC uses too; the label is given without its `"tls13 "` prefix, and a length outside 0 to 255, a label outside 1 to 249 bytes, a context past 255 bytes or a secret shorter than HashLen panics, since each is the program's choice rather than the peer's. `hkdfExtractInto`, `hkdfExpandInto` (`false` where the others answer `null`) and `hkdfExpandLabelInto` compute the same bytes in an `HkdfScratch` the caller owns, over the hash a length of 32 or 48 names, into the caller's array, and wipe the scratch before they return. A key held as a `Secret<u8[]>` (`nish:secret`) goes through the `*Secret` twins, which read it only inside `exposeWith` and answer a `Secret`: `hkdfSha256Secret` / `hkdfSha384Secret`, each step's, and `hkdfExpandLabelSha256Secret(prk, label, context, length)` / `hkdfExpandLabelSha384Secret`, which panic where HKDF-Expand-Label does. A destination already held as a `Secret` is derived into in place by `hkdfExpandLabelSha256Held(dst, prk, label, context)` / `hkdfExpandLabelSha384Held`, as many bytes as `dst` holds, the plain output copied in inside `exposeWith` and wiped; `dst` may be `prk`. `hkdfExpandLabelHeldInto(hashLength, prk, label, context, out, at, length)` writes a plain window from a held PRK, for output that is plain by design, a record key or IV | RFC 5869 §2, Appendix A; RFC 8448 §3 and RFC 9001 A.1 for the label |
|
|
@@ -181,10 +186,10 @@ libdeflate: `bench/gzip/`; against Go, Rust, Java, Node, Bun and Python:
|
|
|
181
186
|
|
|
182
187
|
| Module | What it is | Reproduces |
|
|
183
188
|
| --- | --- | --- |
|
|
184
|
-
| [`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`) |
|
|
185
|
-
| [`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`) |
|
|
189
|
+
| [`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`). `FixedDeflater` is the small-body encoder: one fixed-Huffman block, a 16 KB window, nothing kept between calls, two to four times faster than `DEFLATE_FAST` at about 1.35 times its size; `fixedDeflateBound(n)` is the most it writes | 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`) |
|
|
190
|
+
| [`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`; `GzipFixedCompressor`, the same framing round `FixedDeflater` (`gzipFixedBound(n)`); `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`) |
|
|
186
191
|
| [`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`) |
|
|
187
|
-
| [`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`) |
|
|
192
|
+
| [`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`. Natively the compiler sends `update`'s walk to `runtime/runtime-checksum.c` where the CPU has PCLMULQDQ or the ARMv8 CRC32 instructions, chosen once per process; `NISH_CRC32=soft` pins the tables | The check values of "123456789", pieces against the whole, Adler-32's deferred reduction against its definition (`std_compress`); every kernel against a bit-at-a-time reference (`tests/simd/crc32.c`); both paths on the same programs (`tests/link/std_compress_crc_paths`, `std_compress_crc_diff`) |
|
|
188
193
|
|
|
189
194
|
## `nish/net` — the protocol stack
|
|
190
195
|
|
|
@@ -224,10 +229,10 @@ reads one, and it still takes its randomness as an argument.
|
|
|
224
229
|
| [`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 |
|
|
225
230
|
| [`net/qpack.ts`](./net/qpack.ts) | QPACK, HTTP/3's field compression, with a dynamic table of capacity 0: `QpackEncoder` and `QpackDecoder`, one per connection. The encoder writes a field section that starts `00 00` (Required Insert Count 0, Base 0) with `beginSection`, then one field line per `encodeField(out, name, off, len, value, off, len, neverIndexed, huffman)`: an Indexed Field Line when the 99-entry static table holds the field, else a literal with the name by static index when it can, each string Huffman-coded when that is shorter and `huffman` is set, and a never-indexed field always a literal with the N bit. The decoder answers a section's fields as windows into its own `bytes` (`nameStart`, `nameLength`, `valueStart`, `valueLength`, `neverIndexed`, `count`), its arrays reused from section to section. It answers QPACK_DECOMPRESSION_FAILED, and stays spent, for a truncated section, an integer past 2^62 − 1, a non-zero Required Insert Count, a negative Base, any dynamic-table reference, a static index past 98 and bad Huffman; a section past `maxFieldSectionSize` answers `QPACK_SECTION_TOO_LARGE`, which is not fatal. `receiveEncoderStream` accepts Set Dynamic Table Capacity 0 and answers QPACK_ENCODER_STREAM_ERROR for any other capacity, Insert or Duplicate; the encoder's `receiveDecoderStream` accepts Stream Cancellations, split anywhere, and answers QPACK_DECODER_STREAM_ERROR for a Section Acknowledgment or Insert Count Increment; `reason` names the rule. `qpackPushStreamCancellation` is the one instruction it may send. The Huffman code is `nish/net/hpack`'s. A window outside its buffer, a negative limit and a stream ID outside 0 to 2^62 − 1 panic | RFC 9204 Appendix B.1, every Appendix A entry by its index, B.2 to B.5's instructions refused, and a request matched byte for byte with pylsqpack's encoding |
|
|
226
231
|
| [`net/http1.ts`](./net/http1.ts) | `Http1Parser`, an incremental HTTP/1.1 request parser: `feed(buf, off, len)` any slice, then `next()` answers `HTTP1_NEED_MORE`, `HTTP1_HEAD` (`method`, `target`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength` or `chunked`, `keepAlive`, `upgrade`), `HTTP1_BODY` (a window onto its buffer, de-chunked), `HTTP1_END`, or one of three final events: `HTTP1_UPGRADE` (`upgradeBytes()` and `declineUpgrade()`), `HTTP1_CLOSED` and `HTTP1_ERROR` with the `status` to answer. It refuses with 400 every request-smuggling shape — `Content-Length` with `Transfer-Encoding`, a repeated or non-digit `Content-Length`, an obs-fold, a bare LF, whitespace before a colon, a malformed chunk size or terminator — and with 413, 414, 431, 501 and 505 what those mean. The head is also kept as spans in a buffer reused from request to request (`head`, `spans`, `methodIs`, `targetIs`, `headerIndex`, `headerIs`, `upgradeIs`), so a parser with `keepText` off allocates nothing per request; `maxChunk` caps each `HTTP1_BODY`, and `restart()` readies it for the next connection. The writer is `http1ResponseHead(status, reason, names, values, bodyLength)`, which adds the framing field itself and refuses (`null`) a CR, LF or other control character anywhere a caller's string lands, and `http1Chunk` and `http1LastChunk` for a chunked body; `http1WriteResponseHead` and `http1WriteChunk` write the same bytes into a caller's buffer at an offset, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED`, and the first takes a date's bytes to send as a `Date` field on a final response, unless the caller's fields hold one | RFC 9112 §2–§7 and §9.3, RFC 9110 §5, §6.2, §7.2 and §7.8; a corpus fed at every split point (`tests/link/net_http1`) |
|
|
227
|
-
| [`net/http1-client.ts`](./net/http1-client.ts) | The client side of HTTP/1.1, sans-IO. The writer is `http1RequestHead(method, target, names, values, bodyLength)`, `Host` first and the framing field added for `bodyLength` (`Content-Length`, `HTTP1_CHUNKED` or `HTTP1_NO_BODY`), and `http1WriteRequestHead` into a caller's buffer, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED` with nothing written; it refuses a method that is not a token, a target that is not a form the method may send, a CR, LF or control byte in a value, not exactly one `Host`, the caller's own framing fields and a `Connection: upgrade
|
|
232
|
+
| [`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`) |
|
|
228
233
|
| [`net/http-client.ts`](./net/http-client.ts) | A blocking HTTP/1.1 client for command-line programs: `httpFetch(request, options)` looks the host up, connects, sends an `HttpRequest` (`method`, `url`, `header(name, value)`, `body`) and answers an `HttpResponse` (`status`, `reason`, `url`, `header(name)`) once its head is in, its body read through `body.read` or `body.readAll(limit)`; null on a failure. `HttpClient(options)` does the same and keeps one connection alive between fetches, sending an idempotent request once more when a kept connection turns out to be gone, and says why a fetch failed in `error` (an `HTTP_E_*` code, `httpErrorName`) and `errorText`. `HttpOptions` bounds every wait (`connectTimeoutMs`, `readTimeoutMs`, `totalTimeoutMs`) and every body (`maxBodyBytes`), follows redirects only when asked (`followRedirects`, `maxRedirects`, `crossOriginRedirects`, never https to http, credentials dropped across origins), and reads `http_proxy`, `https_proxy` and `no_proxy` when `useEnvProxy` is set (`HTTP_PROXY` ignored under CGI; https through `CONNECT`). Every byte goes through `nish/net/http1-client`. Native only. Record: [`docs/security/http-client.md`](../docs/security/http-client.md) | RFC 9110 §9.2.2 and §15.4, RFC 9112; a loopback server and proxy in the test itself: keep-alive, retries, redirects, proxies, timeouts and body caps (`tests/link/net_http_fetch`) |
|
|
229
234
|
| [`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 |
|
|
230
|
-
| [`net/websocket.ts`](./net/websocket.ts) | `WsDecoder(expectMasked, maxMessage)`, the same `feed` and `next()` over WebSocket frames: `WS_MESSAGE` (fragments joined, text checked as UTF-8 fragment by fragment), `WS_PING`, `WS_PONG`, and the final `WS_CLOSE` and `WS_ERROR`, whose `closeCode` is the 1002, 1007 or 1009 to close with. `websocketFrame` encodes a frame with the mask key the caller supplies (`websocketWriteFrame` into a caller's buffer, `websocketFrameSize` its length), `reset()` readies a decoder for the next connection, `websocketClosePayload` a close frame's code and reason, and `websocketIsUtf8` checks a window. The handshake is `websocketRequestKey(parser)` over an `Http1Parser`'s head, `websocketAcceptKey`, `websocketKeyIsValid` and `websocketUpgradeResponse`, the 101 | RFC 6455 §1.3's accept key, the §5.7 example frames byte for byte, §5.5's control-frame rules, §7.4's close codes |
|
|
235
|
+
| [`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 |
|
|
231
236
|
| [`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 |
|
|
232
237
|
| [`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`) |
|
|
233
238
|
| [`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`) |
|
|
@@ -238,9 +243,9 @@ reads one, and it still takes its randomness as an argument.
|
|
|
238
243
|
| [`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`) |
|
|
239
244
|
| [`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 |
|
|
240
245
|
| [`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`) |
|
|
241
|
-
| [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`). Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
|
|
246
|
+
| [`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`) |
|
|
242
247
|
| [`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_*`) |
|
|
243
|
-
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
|
|
248
|
+
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. `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`) |
|
|
244
249
|
|
|
245
250
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
246
251
|
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|