@amritk/nish-x86_64-linux 0.17.0 → 0.20.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.
Files changed (61) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +231 -20
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +1376 -42
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1412 -70
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -0,0 +1,412 @@
1
+ /**
2
+ * `nish/net/prefork` — run a server on every core: a supervisor process that
3
+ * starts one worker process per core, restarts one that dies, and stops them
4
+ * all on SIGTERM or SIGINT (`docs/wp42-multicore-server.md` §2.3).
5
+ *
6
+ * import { preforkWorkers } from "nish/net/prefork";
7
+ *
8
+ * export const main = (): number => {
9
+ * const worker: i32 = preforkWorkers(process.argv);
10
+ * if (worker < 0) { return worker === -1 ? 0 : 1; } // the supervisor, done
11
+ * const listener = tcpListenFlags("0.0.0.0", 8080, 1024, 1); // SO_REUSEPORT
12
+ * // ... the single-threaded readiness loop, unchanged ...
13
+ * };
14
+ *
15
+ * **One program, two roles.** `preforkWorkers(process.argv)` answers in two kinds of
16
+ * process. In a worker it answers the worker's index, `0` to `N - 1`, at once,
17
+ * and the program goes on to bind its listener and serve. In the process the
18
+ * user started it supervises, and answers only once every worker has exited:
19
+ * `-1` after a shutdown, `-2` when the workers could not be started or the
20
+ * supervisor could not listen for signals. Each worker binds its own listener
21
+ * with `tcpListenFlags`' flag 1 (`SO_REUSEPORT`), so on Linux the kernel
22
+ * spreads new connections across them and no state is shared:
23
+ * a worker is the single-threaded server it was before, `Arena.used()` flat
24
+ * per worker.
25
+ *
26
+ * **How a worker is told.** The supervisor runs the program again with
27
+ * `--nish-prefork-worker=<index>` appended to its arguments, through `spawn`;
28
+ * on Linux it runs `/proc/self/exe`, so a program started through `PATH` or a
29
+ * relative path finds itself. `preforkArgs(process.argv)` answers the
30
+ * arguments without that flag, for a worker that reads its own; the module
31
+ * takes `process.argv` from its caller because only a program with a `main`
32
+ * has one. The environment is inherited whole, and every descriptor the
33
+ * supervisor holds is close-on-exec, so a worker starts with none of them
34
+ * (`docs/security/runtime.md`, RT-17, RT-19).
35
+ *
36
+ * **How many.** `NISH_WORKERS` when it is a positive whole number, else
37
+ * `PreforkConfig.workers` when that is positive, else
38
+ * `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
39
+ * `SO_REUSEPORT` port without spreading connections across it. The count has
40
+ * no upper bound, so `NISH_WORKERS` is the operator's to set, not a client's.
41
+ *
42
+ * **Restarts.** A worker that exits, by a signal or with any code, is started
43
+ * again at the same index while the supervisor is not stopping. One that dies
44
+ * within `quickExitMs` of starting waits before its restart: `backoffMs` the
45
+ * first time, twice the last wait after that, up to `backoffCapMs`; one that
46
+ * ran longer starts again at once and clears its wait, so a crash loop costs a
47
+ * spawn every `backoffCapMs`, not a core. A restart `spawn` refuses waits on
48
+ * the same backoff and is tried again; only the first start of a worker that
49
+ * fails ends the supervisor, with `PREFORK_SPAWN_FAILED`.
50
+ *
51
+ * **Shutdown.** The supervisor listens through `signalFd()`, made before the
52
+ * first spawn so no signal falls between. On SIGTERM or SIGINT it stops
53
+ * restarting, sends every worker SIGTERM, reaps them for `graceMs`, sends what
54
+ * is left SIGKILL and reaps that. A second signal during the grace period sends
55
+ * SIGKILL at once. The workers get the default dispositions back from `exec`,
56
+ * so a worker that never asked for `signalFd()` ends on the SIGTERM. A
57
+ * supervisor that dies any other way (SIGKILL, the OOM killer, or the default
58
+ * action of SIGHUP or SIGQUIT, which `signalFd()` does not catch) leaves its
59
+ * workers running, reparented to init.
60
+ *
61
+ * Private helpers share the importing program's flat symbol namespace
62
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
63
+ */
64
+ import { netClose, pollAdd, pollCreate, pollWait } from "nish:net"
65
+ import {
66
+ availableParallelism,
67
+ getenv,
68
+ kill,
69
+ monotonicNanos,
70
+ platform,
71
+ readSignal,
72
+ signalFd,
73
+ spawn,
74
+ waitPid,
75
+ } from "nish:process"
76
+
77
+ /** The argument a worker is started with, its index after the `=`. */
78
+ export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
79
+
80
+ /** What `preforkWith` answers in the supervisor once its workers are gone. */
81
+ export const PREFORK_STOPPED: i32 = -1
82
+ /**
83
+ * What `preforkWith` answers when a worker could not be started, or when the
84
+ * supervisor could not listen for SIGTERM and so starts none.
85
+ */
86
+ export const PREFORK_SPAWN_FAILED: i32 = -2
87
+
88
+ /** The supervisor's numbers; every one is checked by `preforkWith`. */
89
+ export class PreforkConfig {
90
+ /** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
91
+ workers: i32 = 0
92
+ /** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
93
+ tickMs: i32 = 100
94
+ /** The first wait before restarting a worker that died young. */
95
+ backoffMs: i32 = 100
96
+ /** The longest wait before a restart. */
97
+ backoffCapMs: i32 = 5000
98
+ /** A worker that exits within this many milliseconds of starting died young. */
99
+ quickExitMs: i32 = 1000
100
+ /** How long the workers get to exit after SIGTERM before SIGKILL. */
101
+ graceMs: i32 = 5000
102
+ }
103
+
104
+ /** True when `s` is one or more ASCII digits and nothing else. */
105
+ const preforkDigits = (s: string): boolean => {
106
+ if (s.length === 0 || s.length > 9) {
107
+ return false
108
+ }
109
+ for (let i: i32 = 0; i < toI32(s.length); i++) {
110
+ const c: i32 = toI32(s.charCodeAt(i))
111
+ if (c < 48 || c > 57) {
112
+ return false
113
+ }
114
+ }
115
+ return true
116
+ }
117
+
118
+ /**
119
+ * How many workers to start: `env` (`NISH_WORKERS`, `null` when unset) when it
120
+ * is a positive whole number, else `requested` when positive, else the
121
+ * platform's default.
122
+ */
123
+ export const preforkWorkerCount = (requested: i32, env: string | null): i32 => {
124
+ if (env !== null && preforkDigits(env)) {
125
+ const n: i32 = parseInt(env)
126
+ if (n > 0) {
127
+ return n
128
+ }
129
+ }
130
+ if (requested > 0) {
131
+ return requested
132
+ }
133
+ if (platform !== "linux") {
134
+ return 1
135
+ }
136
+ const cores: i32 = availableParallelism()
137
+ return cores > 0 ? cores : 1
138
+ }
139
+
140
+ /** The worker index `args` carries in its last element, or -1 when none. */
141
+ export const preforkIndex = (args: string[]): i32 => {
142
+ if (args.length < 2) {
143
+ return -1
144
+ }
145
+ const last: string = args[args.length - 1]
146
+ if (!last.startsWith(PREFORK_WORKER_FLAG)) {
147
+ return -1
148
+ }
149
+ const digits: string = last.substring(PREFORK_WORKER_FLAG.length)
150
+ return preforkDigits(digits) ? parseInt(digits) : -1
151
+ }
152
+
153
+ /** `args` (`process.argv`) without the worker flag, the same in every process. */
154
+ export const preforkArgs = (args: string[]): string[] => {
155
+ const keep: i32 = preforkIndex(args) >= 0 ? toI32(args.length) - 1 : toI32(args.length)
156
+ const out: string[] = []
157
+ for (let i: i32 = 0; i < toI32(args.length); i++) {
158
+ if (i < keep) {
159
+ out.push(args[i])
160
+ }
161
+ }
162
+ return out
163
+ }
164
+
165
+ /** Milliseconds on the monotonic clock. */
166
+ const preforkNowMs = (): i64 => monotonicNanos() / 1000000
167
+
168
+ /** The signals the supervisor stops its workers with. */
169
+ const PREFORK_SIGTERM: i32 = 15
170
+ const PREFORK_SIGKILL: i32 = 9
171
+
172
+ /** One worker's slot in the supervisor. */
173
+ class PreforkSlot {
174
+ /** When the running worker started, or when the waiting one may start. */
175
+ at: i64 = 0
176
+ /** The running worker's pid, 0 while it waits to be started again. */
177
+ pid: i32 = 0
178
+ /** The wait before the last restart, 0 after a worker that ran long. */
179
+ delay: i32 = 0
180
+ }
181
+
182
+ /** The supervisor's state while it runs. */
183
+ class PreforkSupervisor {
184
+ config: PreforkConfig
185
+ program: string
186
+ args: string[]
187
+ slots: PreforkSlot[]
188
+ running: i32 = 0
189
+
190
+ constructor(config: PreforkConfig, args: string[], count: i32) {
191
+ this.config = config
192
+ this.args = preforkArgs(args)
193
+ const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
194
+ this.program = self !== null ? self : this.args[0]
195
+ this.slots = []
196
+ for (let i: i32 = 0; i < count; i++) {
197
+ this.slots.push(new PreforkSlot())
198
+ }
199
+ }
200
+
201
+ /** Start worker `index`; false when `spawn` refused. */
202
+ start(index: i32): boolean {
203
+ const argv: string[] = [this.program]
204
+ for (let i: i32 = 1; i < toI32(this.args.length); i++) {
205
+ argv.push(this.args[i])
206
+ }
207
+ argv.push(`${PREFORK_WORKER_FLAG}${index}`)
208
+ const pid: i32 = spawn(argv)
209
+ if (pid <= 0) {
210
+ return false
211
+ }
212
+ const slot: PreforkSlot = this.slots[index]
213
+ slot.pid = pid
214
+ slot.at = preforkNowMs()
215
+ this.running = this.running + 1
216
+ return true
217
+ }
218
+
219
+ /** The slot `pid` ran in, or -1. */
220
+ find(pid: i32): i32 {
221
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
222
+ if (this.slots[i].pid === pid) {
223
+ return i
224
+ }
225
+ }
226
+ return -1
227
+ }
228
+
229
+ /**
230
+ * Give `slot` the time it may start again: at once after a worker that ran
231
+ * at least `quickExitMs`, else, or when `failed` (`spawn` refused it), after
232
+ * a wait that starts at `backoffMs` and doubles up to `backoffCapMs`.
233
+ */
234
+ backoff(slot: PreforkSlot, now: i64, failed: boolean): void {
235
+ if (failed || now - slot.at < toI64(this.config.quickExitMs)) {
236
+ const cap: i32 = this.config.backoffCapMs
237
+ if (slot.delay === 0) {
238
+ slot.delay = this.config.backoffMs
239
+ } else {
240
+ // Compared before doubling, so a cap near the i32 limit cannot overflow.
241
+ slot.delay = slot.delay > cap / 2 ? cap : slot.delay * 2
242
+ }
243
+ } else {
244
+ slot.delay = 0
245
+ }
246
+ slot.at = now + toI64(slot.delay)
247
+ }
248
+
249
+ /**
250
+ * Reap every worker that has exited. Unless `stopping`, each one's slot is
251
+ * given the time it may start again. A child that is not a worker (one the
252
+ * program spawned itself before calling `preforkWith`) is reaped too, and
253
+ * its status is discarded. Answers false when `waitPid` fails, which means
254
+ * no child is left to wait for.
255
+ */
256
+ reap(stopping: boolean): boolean {
257
+ while (this.running > 0) {
258
+ const done: i32[] = waitPid(-1, 1)
259
+ if (done[0] === -4) {
260
+ continue
261
+ }
262
+ if (done[0] <= 0) {
263
+ return done[0] === 0
264
+ }
265
+ const index: i32 = this.find(done[0])
266
+ if (index < 0) {
267
+ continue
268
+ }
269
+ const slot: PreforkSlot = this.slots[index]
270
+ this.running = this.running - 1
271
+ slot.pid = 0
272
+ if (!stopping) {
273
+ this.backoff(slot, preforkNowMs(), false)
274
+ }
275
+ }
276
+ return true
277
+ }
278
+
279
+ /**
280
+ * Start every waiting worker whose time has come. When `first`, answer false
281
+ * at the first `spawn` that fails; otherwise a failed start waits on the
282
+ * same backoff as a worker that exited at once, and the answer is true.
283
+ */
284
+ restart(first: boolean): boolean {
285
+ const now: i64 = preforkNowMs()
286
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
287
+ const slot: PreforkSlot = this.slots[i]
288
+ if (slot.pid === 0 && slot.at <= now && !this.start(i)) {
289
+ if (first) {
290
+ return false
291
+ }
292
+ console.error(`nish/net/prefork: cannot restart worker ${i}`)
293
+ this.backoff(slot, now, true)
294
+ }
295
+ }
296
+ return true
297
+ }
298
+
299
+ /** Send `sig` to every running worker. */
300
+ signal(sig: i32): void {
301
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
302
+ if (this.slots[i].pid > 0) {
303
+ kill(this.slots[i].pid, sig)
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * SIGTERM, reap until `graceMs` or a second signal, SIGKILL, reap until no
310
+ * worker is left. Two signals that arrive before the supervisor reads the
311
+ * first count as the second here too. Every reap goes through `reap`, so a
312
+ * child that is not a worker is skipped in both phases, and the SIGKILL
313
+ * phase ends only when the last worker is reaped or no child is left.
314
+ */
315
+ stop(loop: i32, signals: i32, ready: i32[]): void {
316
+ this.signal(PREFORK_SIGTERM)
317
+ const deadline: i64 = preforkNowMs() + toI64(this.config.graceMs)
318
+ this.reap(true)
319
+ let forced = false
320
+ while (this.running > 0 && !forced && preforkNowMs() < deadline) {
321
+ const n: i32 = pollWait(loop, ready, this.config.tickMs)
322
+ if (n > 0) {
323
+ readSignal(signals)
324
+ forced = true
325
+ }
326
+ this.reap(true)
327
+ }
328
+ if (this.running > 0) {
329
+ this.signal(PREFORK_SIGKILL)
330
+ while (this.reap(true) && this.running > 0) {
331
+ if (pollWait(loop, ready, this.config.tickMs) > 0) {
332
+ readSignal(signals)
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ /** The first field of `config` the supervisor cannot run with, or "" when none. */
340
+ const preforkInvalid = (config: PreforkConfig): string => {
341
+ if (config.workers < 0) {
342
+ return "workers below 0"
343
+ }
344
+ if (config.tickMs <= 0) {
345
+ return "tickMs not above 0"
346
+ }
347
+ if (config.backoffMs <= 0 || config.backoffCapMs < config.backoffMs) {
348
+ return "backoffMs not above 0 or above backoffCapMs"
349
+ }
350
+ if (config.quickExitMs < 0 || config.graceMs < 0) {
351
+ return "quickExitMs or graceMs below 0"
352
+ }
353
+ return ""
354
+ }
355
+
356
+ /**
357
+ * In a worker, its index. In the supervisor, run the workers until SIGTERM or
358
+ * SIGINT and answer `PREFORK_STOPPED`, or `PREFORK_SPAWN_FAILED` when a worker's
359
+ * first start failed (those already started are stopped first) or no signal
360
+ * listener could be made (no worker is started). Panics on a config
361
+ * `preforkInvalid` names a field of.
362
+ */
363
+ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
364
+ const invalid: string = preforkInvalid(config)
365
+ if (invalid !== "") {
366
+ panic(`nish/net/prefork: ${invalid}`)
367
+ }
368
+ const index: i32 = preforkIndex(args)
369
+ if (index >= 0) {
370
+ return index
371
+ }
372
+ const signals: i32 = signalFd()
373
+ const loop: i32 = pollCreate()
374
+ if (signals < 0 || loop < 0 || pollAdd(loop, signals, 1, 0) < 0) {
375
+ // `signals` is the process's one signal pipe, which every later
376
+ // `signalFd()` answers, so only the loop is closed.
377
+ console.error("nish/net/prefork: cannot listen for SIGTERM")
378
+ if (loop >= 0) {
379
+ netClose(loop)
380
+ }
381
+ return PREFORK_SPAWN_FAILED
382
+ }
383
+ const supervisor = new PreforkSupervisor(
384
+ config,
385
+ args,
386
+ preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
387
+ )
388
+ const ready: i32[] = new Array<i32>(2)
389
+ let result: i32 = PREFORK_STOPPED
390
+ if (!supervisor.restart(true)) {
391
+ console.error(`nish/net/prefork: cannot start ${supervisor.program}`)
392
+ result = PREFORK_SPAWN_FAILED
393
+ } else {
394
+ let running = true
395
+ while (running) {
396
+ const n: i32 = pollWait(loop, ready, config.tickMs)
397
+ if (n > 0) {
398
+ readSignal(signals)
399
+ running = false
400
+ } else {
401
+ supervisor.reap(false)
402
+ supervisor.restart(false)
403
+ }
404
+ }
405
+ }
406
+ supervisor.stop(loop, signals, ready)
407
+ netClose(loop)
408
+ return result
409
+ }
410
+
411
+ /** `preforkWith` with the default `PreforkConfig`. */
412
+ export const preforkWorkers = (args: string[]): i32 => preforkWith(args, new PreforkConfig())
@@ -126,6 +126,42 @@ export class QuicTransportParameters {
126
126
  }
127
127
  }
128
128
 
129
+ /** Empties `to`, keeping its room. */
130
+ const quicParamsEmpty = (to: u8[]): void => {
131
+ while (to.length > 0) {
132
+ to.pop()
133
+ }
134
+ }
135
+
136
+ /** Sets `p` back to what a new `QuicTransportParameters` holds, keeping its arrays, emptied. */
137
+ const quicParamsDefaults = (p: QuicTransportParameters): void => {
138
+ p.error = 0
139
+ p.maxIdleTimeout = 0
140
+ p.maxUdpPayloadSize = 65527
141
+ p.initialMaxData = 0
142
+ p.initialMaxStreamDataBidiLocal = 0
143
+ p.initialMaxStreamDataBidiRemote = 0
144
+ p.initialMaxStreamDataUni = 0
145
+ p.initialMaxStreamsBidi = 0
146
+ p.initialMaxStreamsUni = 0
147
+ p.ackDelayExponent = 3
148
+ p.maxAckDelay = 25
149
+ p.activeConnectionIdLimit = 2
150
+ p.maxDatagramFrameSize = 0
151
+ p.errorParameter = 0
152
+ quicParamsEmpty(p.originalDcid)
153
+ quicParamsEmpty(p.statelessResetToken)
154
+ quicParamsEmpty(p.preferredAddress)
155
+ quicParamsEmpty(p.initialScid)
156
+ quicParamsEmpty(p.retryScid)
157
+ p.hasOriginalDcid = false
158
+ p.hasStatelessResetToken = false
159
+ p.hasPreferredAddress = false
160
+ p.hasInitialScid = false
161
+ p.hasRetryScid = false
162
+ p.disableActiveMigration = false
163
+ }
164
+
129
165
  /** Appends one parameter whose value is a varint, when it differs from `fallback`, its default. */
130
166
  const quicParamsPushVarint = (out: u8[], id: i32, value: i64, fallback: i64): void => {
131
167
  if (value === fallback || quicVarintSize(value) === 0) {
@@ -158,6 +194,18 @@ const quicParamsPushBytes = (out: u8[], id: i32, bytes: u8[], present: boolean):
158
194
  */
159
195
  export const quicEncodeTransportParameters = (p: QuicTransportParameters): u8[] => {
160
196
  const out: u8[] = []
197
+ quicEncodeTransportParametersInto(p, out)
198
+ return out
199
+ }
200
+
201
+ /**
202
+ * `quicEncodeTransportParameters` into `out`, which is emptied first: the
203
+ * encoding is pushed into the room the array already has, so a connection
204
+ * slot that keeps one array for its parameters grows it only when they
205
+ * outgrow every earlier connection's.
206
+ */
207
+ export const quicEncodeTransportParametersInto = (p: QuicTransportParameters, out: u8[]): void => {
208
+ quicParamsEmpty(out)
161
209
  quicParamsPushBytes(out, QUIC_TP_ORIGINAL_DCID, p.originalDcid, p.hasOriginalDcid)
162
210
  quicParamsPushVarint(out, QUIC_TP_MAX_IDLE_TIMEOUT, p.maxIdleTimeout, 0)
163
211
  quicParamsPushBytes(out, QUIC_TP_STATELESS_RESET_TOKEN, p.statelessResetToken, p.hasStatelessResetToken)
@@ -177,19 +225,20 @@ export const quicEncodeTransportParameters = (p: QuicTransportParameters): u8[]
177
225
  quicParamsPushBytes(out, QUIC_TP_INITIAL_SCID, p.initialScid, p.hasInitialScid)
178
226
  quicParamsPushBytes(out, QUIC_TP_RETRY_SCID, p.retryScid, p.hasRetryScid)
179
227
  quicParamsPushVarint(out, QUIC_TP_MAX_DATAGRAM_FRAME_SIZE, p.maxDatagramFrameSize, 0)
180
- return out
181
228
  }
182
229
 
183
- /** The bytes `data[from .. from + length)`, copied; the caller has checked the window. */
184
- const quicParamsCopy = (data: u8[], from: i32, length: i32): u8[] => {
185
- const out: u8[] = new Array<u8>(length)
186
- const n: i32 = toI32(out.length)
187
- for (let k: i32 = 0; k < n; k += 1) {
230
+ /**
231
+ * `bytes[from .. from + length)` into `to`, which is emptied first and
232
+ * refilled in the room it has: a parameters object reused for connection
233
+ * after connection keeps its arrays. The caller has checked the window.
234
+ */
235
+ const quicParamsCopyInto = (to: u8[], data: u8[], from: i32, length: i32): void => {
236
+ quicParamsEmpty(to)
237
+ for (let k: i32 = 0; k < length; k += 1) {
188
238
  if (from + k >= 0 && from + k < toI32(data.length)) {
189
- out[k] = data[from + k]
239
+ to.push(data[from + k])
190
240
  }
191
241
  }
192
- return out
193
242
  }
194
243
 
195
244
  /** Fails `p` over parameter `id` and answers it. */
@@ -293,30 +342,35 @@ const quicParamsStoreBytes = (
293
342
  at: i32,
294
343
  length: i32
295
344
  ): boolean => {
345
+ // A connection ID or reset token is copied only once its length is one the
346
+ // parameter allows, so a reused object's arrays never grow past it whatever
347
+ // a peer sends. `preferred_address` is server-only and so never read from a
348
+ // client into a server's slot; its length is the server's own.
349
+ const cidFits: boolean = length <= QUIC_MAX_CID_LENGTH
296
350
  switch (id) {
297
351
  case QUIC_TP_ORIGINAL_DCID:
298
- p.originalDcid = quicParamsCopy(bytes, at, length)
352
+ quicParamsCopyInto(p.originalDcid, bytes, at, cidFits ? length : 0)
299
353
  p.hasOriginalDcid = true
300
- return length <= QUIC_MAX_CID_LENGTH
354
+ return cidFits
301
355
  case QUIC_TP_STATELESS_RESET_TOKEN:
302
- p.statelessResetToken = quicParamsCopy(bytes, at, length)
356
+ quicParamsCopyInto(p.statelessResetToken, bytes, at, length === QUIC_RESET_TOKEN_SIZE ? length : 0)
303
357
  p.hasStatelessResetToken = true
304
358
  return length === QUIC_RESET_TOKEN_SIZE
305
359
  case QUIC_TP_DISABLE_ACTIVE_MIGRATION:
306
360
  p.disableActiveMigration = true
307
361
  return length === 0
308
362
  case QUIC_TP_PREFERRED_ADDRESS:
309
- p.preferredAddress = quicParamsCopy(bytes, at, length)
363
+ quicParamsCopyInto(p.preferredAddress, bytes, at, length)
310
364
  p.hasPreferredAddress = true
311
365
  return true
312
366
  case QUIC_TP_INITIAL_SCID:
313
- p.initialScid = quicParamsCopy(bytes, at, length)
367
+ quicParamsCopyInto(p.initialScid, bytes, at, cidFits ? length : 0)
314
368
  p.hasInitialScid = true
315
- return length <= QUIC_MAX_CID_LENGTH
369
+ return cidFits
316
370
  default:
317
- p.retryScid = quicParamsCopy(bytes, at, length)
371
+ quicParamsCopyInto(p.retryScid, bytes, at, cidFits ? length : 0)
318
372
  p.hasRetryScid = true
319
- return length <= QUIC_MAX_CID_LENGTH
373
+ return cidFits
320
374
  }
321
375
  }
322
376
 
@@ -336,6 +390,22 @@ const quicParamsIsVarint = (id: i32): boolean =>
336
390
  */
337
391
  export const quicParseTransportParameters = (bytes: u8[], fromServer: boolean): QuicTransportParameters => {
338
392
  const p: QuicTransportParameters = new QuicTransportParameters()
393
+ quicParseTransportParametersInto(p, bytes, fromServer)
394
+ return p
395
+ }
396
+
397
+ /**
398
+ * `quicParseTransportParameters` into `p`, which is set back to every
399
+ * default first, its byte-valued fields emptied and refilled in the room
400
+ * their arrays have: a connection slot parses each client's parameters into
401
+ * one object of its own. Answers `p`.
402
+ */
403
+ export const quicParseTransportParametersInto = (
404
+ p: QuicTransportParameters,
405
+ bytes: u8[],
406
+ fromServer: boolean
407
+ ): QuicTransportParameters => {
408
+ quicParamsDefaults(p)
339
409
  const end: i32 = toI32(bytes.length)
340
410
  // The known IDs are 0 to 16, so one bit each of an i32 records which were
341
411
  // seen; RFC 9221's 0x20 has a flag of its own.