@amritk/nish-x86_64-linux 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.h +20 -6
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +11 -0
- package/runtime/runtime.c +20 -10
- package/scripts/build.sh +13 -9
- package/std/README.md +9 -8
- package/std/crypto/p256.ts +473 -10
- package/std/json.ts +297 -41
- package/std/net/http3-server.ts +22 -16
- package/std/net/quic-conn-params.ts +86 -16
- package/std/net/quic-listener.ts +489 -213
- package/std/net/quic-packet.ts +226 -2
- package/std/net/quic-stream.ts +10 -4
- package/std/net/quic.ts +484 -120
- package/std/net/tls-tcp.ts +8 -4
- package/std/net/tls.ts +114 -45
- package/std/text.ts +77 -0
package/std/json.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `std/json` — the value of
|
|
2
|
+
* `std/json` — the value of a field, or of several, of one flat JSON object.
|
|
3
3
|
*
|
|
4
4
|
* It exists because the compiler's own machine-readable surface is JSON: under
|
|
5
5
|
* `--json` every diagnostic is one flat object on a line of stdout, whose `code`
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
* whether the whole line was well-formed is asking a question this does not
|
|
27
27
|
* answer.
|
|
28
28
|
*
|
|
29
|
-
* There
|
|
30
|
-
* the program that imports it and the symbol namespace is flat
|
|
29
|
+
* There are two exported functions, and few on purpose. A `std/` module is
|
|
30
|
+
* compiled into the program that imports it and the symbol namespace is flat
|
|
31
31
|
* (`std/README.md`), so every name here is a name its importer cannot use: the
|
|
32
|
-
* helpers are `json`-prefixed and the
|
|
32
|
+
* helpers are `json`-prefixed and the readers are the only exports. Splitting a
|
|
33
33
|
* stream into lines and picking the ones that start with `{` is two calls the
|
|
34
34
|
* caller already has — `splitLines` from `std/text` and `startsWith` — and
|
|
35
35
|
* duplicating them here would cost more names than it saves.
|
|
@@ -46,6 +46,16 @@
|
|
|
46
46
|
* }
|
|
47
47
|
* }
|
|
48
48
|
*
|
|
49
|
+
* `jsonField` scans from the start of the line on every call, so a caller that
|
|
50
|
+
* wants three fields of each line reads each line three times. `jsonFields`
|
|
51
|
+
* reads it once: `jsonFields(line, ["code", "line", "message"])` answers an
|
|
52
|
+
* array whose slot `k` is exactly what `jsonField(line, names[k])` answers —
|
|
53
|
+
* absent, first-of-a-duplicate and malformed input included — because the two
|
|
54
|
+
* share the step that reads one member (`jsonReadMember`) and stop at the same
|
|
55
|
+
* one. Its array goes back to the caller with the values in it, so a loop that
|
|
56
|
+
* calls it keeps its per-pass arena release, as one calling `jsonField` does;
|
|
57
|
+
* `jsonFields`' own comment says when that holds.
|
|
58
|
+
*
|
|
49
59
|
* Every offset here is a **byte** offset and every width is spelled, with each
|
|
50
60
|
* length and byte a builtin answers read through `toI32` — `.length` and
|
|
51
61
|
* `charCodeAt` answer `number`, which is `f64` under `--number-mode f64`, so the
|
|
@@ -53,14 +63,17 @@
|
|
|
53
63
|
* (`docs/wp26-stdlib.md` §4).
|
|
54
64
|
*/
|
|
55
65
|
|
|
66
|
+
const JSON_BACKSPACE: i32 = 8
|
|
56
67
|
const JSON_TAB: i32 = 9
|
|
57
68
|
const JSON_NEWLINE: i32 = 10
|
|
69
|
+
const JSON_FORM_FEED: i32 = 12
|
|
58
70
|
const JSON_CARRIAGE_RETURN: i32 = 13
|
|
59
71
|
const JSON_SPACE: i32 = 32
|
|
60
72
|
const JSON_QUOTE: i32 = 34
|
|
61
73
|
const JSON_COMMA: i32 = 44
|
|
62
74
|
const JSON_COLON: i32 = 58
|
|
63
75
|
const JSON_BACKSLASH: i32 = 92
|
|
76
|
+
const JSON_LETTER_U: i32 = 117
|
|
64
77
|
const JSON_OPEN_BRACKET: i32 = 91
|
|
65
78
|
const JSON_CLOSE_BRACKET: i32 = 93
|
|
66
79
|
const JSON_OPEN_BRACE: i32 = 123
|
|
@@ -261,6 +274,37 @@ const jsonUtf8 = (code: i32): string => {
|
|
|
261
274
|
return `${lead}${middle}${String.fromCharCode(JSON_UTF8_CONTINUATION + (code & JSON_UTF8_LOW_SIX))}`
|
|
262
275
|
}
|
|
263
276
|
|
|
277
|
+
/** The number of bytes `jsonUtf8(code)` answers. */
|
|
278
|
+
const jsonUtf8Width = (code: i32): i32 => {
|
|
279
|
+
if (code < JSON_UTF8_TWO_BYTE_FLOOR) {
|
|
280
|
+
return 1
|
|
281
|
+
}
|
|
282
|
+
if (code < JSON_UTF8_THREE_BYTE_FLOOR) {
|
|
283
|
+
return 2
|
|
284
|
+
}
|
|
285
|
+
return 3
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Byte `k` of the `width` bytes `jsonUtf8(code)` answers: its arithmetic, one
|
|
290
|
+
* byte at a time, for a caller that compares the bytes rather than keeps them.
|
|
291
|
+
*/
|
|
292
|
+
const jsonUtf8Byte = (code: i32, width: i32, k: i32): i32 => {
|
|
293
|
+
if (width === 1) {
|
|
294
|
+
return code
|
|
295
|
+
}
|
|
296
|
+
if (k === 0) {
|
|
297
|
+
if (width === 2) {
|
|
298
|
+
return JSON_UTF8_TWO_BYTE_LEAD + (code >> 6)
|
|
299
|
+
}
|
|
300
|
+
return JSON_UTF8_THREE_BYTE_LEAD + (code >> 12)
|
|
301
|
+
}
|
|
302
|
+
if (k === width - 1) {
|
|
303
|
+
return JSON_UTF8_CONTINUATION + (code & JSON_UTF8_LOW_SIX)
|
|
304
|
+
}
|
|
305
|
+
return JSON_UTF8_CONTINUATION + ((code >> 6) & JSON_UTF8_LOW_SIX)
|
|
306
|
+
}
|
|
307
|
+
|
|
264
308
|
/**
|
|
265
309
|
* The bytes of `text[at..end)` with the JSON escapes decoded — the body of a
|
|
266
310
|
* string literal, without its quotes.
|
|
@@ -343,6 +387,190 @@ const jsonUnescape = (text: string, at: i32, end: i32): string => {
|
|
|
343
387
|
return parts.join("")
|
|
344
388
|
}
|
|
345
389
|
|
|
390
|
+
/** Whether byte `j` of `name` is `code`; an index past either end is not. */
|
|
391
|
+
const jsonNameHas = (name: string, j: i32, code: i32): boolean =>
|
|
392
|
+
j >= 0 && j < toI32(name.length) && toI32(name.charCodeAt(j)) === code
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Whether `jsonUnescape(text, at, end)` would answer `name`, decided without
|
|
396
|
+
* building it.
|
|
397
|
+
*
|
|
398
|
+
* `jsonField` and `jsonFields` ask this of every key they walk past, and almost
|
|
399
|
+
* every answer is no, so the question is answered the way it is asked: the literal and `name`
|
|
400
|
+
* are walked together, each escape decoded where it stands, and the first byte
|
|
401
|
+
* that differs ends the walk. Every escape reads as `jsonUnescape` reads it —
|
|
402
|
+
* the short forms, a `\u` as the bytes `jsonUtf8` would build, a broken `\u`
|
|
403
|
+
* as the two bytes `\u`, a backslash with no letter after it as itself, and
|
|
404
|
+
* any other escape as the byte it escapes — so a key matches here exactly when
|
|
405
|
+
* its decoded text would.
|
|
406
|
+
*/
|
|
407
|
+
const jsonKeyEquals = (text: string, at: i32, end: i32, name: string): boolean => {
|
|
408
|
+
// `jsonUnescape`'s guard, for the same reason: every caller passes the
|
|
409
|
+
// inside of a key `jsonReadMember` found with `jsonEndOfString`.
|
|
410
|
+
if (at < 0 || end > toI32(text.length)) {
|
|
411
|
+
return false
|
|
412
|
+
}
|
|
413
|
+
let i: i32 = at
|
|
414
|
+
let j: i32 = 0
|
|
415
|
+
while (i < end) {
|
|
416
|
+
const code: i32 = toI32(text.charCodeAt(i))
|
|
417
|
+
if (code !== JSON_BACKSLASH) {
|
|
418
|
+
if (!jsonNameHas(name, j, code)) {
|
|
419
|
+
return false
|
|
420
|
+
}
|
|
421
|
+
i += 1
|
|
422
|
+
j += 1
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
const letter: i32 = i + 1 < end ? toI32(text.charCodeAt(i + 1)) : 0
|
|
426
|
+
if (letter === JSON_LETTER_U) {
|
|
427
|
+
const point: i32 = jsonHex4(text, i + 2, end)
|
|
428
|
+
if (point < 0) {
|
|
429
|
+
if (!jsonNameHas(name, j, JSON_BACKSLASH) || !jsonNameHas(name, j + 1, JSON_LETTER_U)) {
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
i += 2
|
|
433
|
+
j += 2
|
|
434
|
+
continue
|
|
435
|
+
}
|
|
436
|
+
const width: i32 = jsonUtf8Width(point)
|
|
437
|
+
let k: i32 = 0
|
|
438
|
+
while (k < width) {
|
|
439
|
+
if (!jsonNameHas(name, j + k, jsonUtf8Byte(point, width, k))) {
|
|
440
|
+
return false
|
|
441
|
+
}
|
|
442
|
+
k += 1
|
|
443
|
+
}
|
|
444
|
+
i += 6
|
|
445
|
+
j += width
|
|
446
|
+
continue
|
|
447
|
+
}
|
|
448
|
+
// Every other escape decodes to one byte.
|
|
449
|
+
let decoded: i32 = letter
|
|
450
|
+
if (letter === 110) {
|
|
451
|
+
decoded = JSON_NEWLINE
|
|
452
|
+
} else if (letter === 116) {
|
|
453
|
+
decoded = JSON_TAB
|
|
454
|
+
} else if (letter === 114) {
|
|
455
|
+
decoded = JSON_CARRIAGE_RETURN
|
|
456
|
+
} else if (letter === 98) {
|
|
457
|
+
decoded = JSON_BACKSPACE
|
|
458
|
+
} else if (letter === 102) {
|
|
459
|
+
decoded = JSON_FORM_FEED
|
|
460
|
+
} else if (letter === 0) {
|
|
461
|
+
decoded = JSON_BACKSLASH
|
|
462
|
+
}
|
|
463
|
+
if (!jsonNameHas(name, j, decoded)) {
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
// A backslash with no letter after it is one byte of the literal and every
|
|
467
|
+
// other escape two. The steps are constants rather than a variable because
|
|
468
|
+
// a cursor moved by a variable is one the bounds proof stops following.
|
|
469
|
+
if (letter === 0) {
|
|
470
|
+
i += 1
|
|
471
|
+
} else {
|
|
472
|
+
i += 2
|
|
473
|
+
}
|
|
474
|
+
j += 1
|
|
475
|
+
}
|
|
476
|
+
return j === toI32(name.length)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Where one member of an object lies, as `jsonReadMember` found it: the inside
|
|
481
|
+
* of its key literal, its value, and the opening quote of the key after it.
|
|
482
|
+
*
|
|
483
|
+
* It is a class so that one step can answer the four offsets together, and
|
|
484
|
+
* each reader allocates exactly one, which never leaves the reader: that is
|
|
485
|
+
* what keeps it a stack slot rather than an arena allocation.
|
|
486
|
+
*/
|
|
487
|
+
class JsonMember {
|
|
488
|
+
keyAt: i32 = 0
|
|
489
|
+
keyEnd: i32 = 0
|
|
490
|
+
valueAt: i32 = 0
|
|
491
|
+
valueEnd: i32 = 0
|
|
492
|
+
/** The opening quote of the next key, or `-1` when no comma follows the value. */
|
|
493
|
+
next: i32 = -1
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The opening quote of the first key of `object`, or `-1` when `object` does
|
|
498
|
+
* not start, past any blanks, with a `{`.
|
|
499
|
+
*/
|
|
500
|
+
const jsonFirstKey = (object: string): i32 => {
|
|
501
|
+
const length: i32 = toI32(object.length)
|
|
502
|
+
const i: i32 = jsonSkipBlank(object, 0)
|
|
503
|
+
if (i >= length || toI32(object.charCodeAt(i)) !== JSON_OPEN_BRACE) {
|
|
504
|
+
return -1
|
|
505
|
+
}
|
|
506
|
+
return jsonSkipBlank(object, i + 1)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Reads the member whose key's opening quote is at `at` into `member`, and
|
|
511
|
+
* answers whether there was one.
|
|
512
|
+
*
|
|
513
|
+
* This is the per-key step `jsonField` and `jsonFields` share, so they cannot
|
|
514
|
+
* disagree about where a member ends: `false` for anything that is not a key,
|
|
515
|
+
* a colon and a whole value in that order, which is the point at which both
|
|
516
|
+
* stop. A value need not be followed by a comma — a truncated object still
|
|
517
|
+
* answers the fields before the cut — so a missing one only sets `next` to
|
|
518
|
+
* `-1`, and the read that follows answers `false`.
|
|
519
|
+
*/
|
|
520
|
+
const jsonReadMember = (object: string, at: i32, member: JsonMember): boolean => {
|
|
521
|
+
const length: i32 = toI32(object.length)
|
|
522
|
+
// `jsonSkipBlank` never answers a negative index for a non-negative one, but
|
|
523
|
+
// the bounds proof does not look inside a callee, so every cursor it answers
|
|
524
|
+
// is tested for `i < 0` beside `i >= length`: the pair is what lets each read
|
|
525
|
+
// below go without a check.
|
|
526
|
+
if (at < 0 || at >= length || toI32(object.charCodeAt(at)) !== JSON_QUOTE) {
|
|
527
|
+
return false
|
|
528
|
+
}
|
|
529
|
+
const keyEnd: i32 = jsonEndOfString(object, at)
|
|
530
|
+
if (keyEnd < 0) {
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
const colon: i32 = jsonSkipBlank(object, keyEnd)
|
|
534
|
+
if (colon < 0 || colon >= length || toI32(object.charCodeAt(colon)) !== JSON_COLON) {
|
|
535
|
+
return false
|
|
536
|
+
}
|
|
537
|
+
const valueAt: i32 = jsonSkipBlank(object, colon + 1)
|
|
538
|
+
const valueEnd: i32 = jsonEndOfValue(object, valueAt)
|
|
539
|
+
if (valueEnd < 0) {
|
|
540
|
+
return false
|
|
541
|
+
}
|
|
542
|
+
member.keyAt = at + 1
|
|
543
|
+
member.keyEnd = keyEnd - 1
|
|
544
|
+
member.valueAt = valueAt
|
|
545
|
+
member.valueEnd = valueEnd
|
|
546
|
+
const comma: i32 = jsonSkipBlank(object, valueEnd)
|
|
547
|
+
if (comma < 0 || comma >= length || toI32(object.charCodeAt(comma)) !== JSON_COMMA) {
|
|
548
|
+
member.next = -1
|
|
549
|
+
} else {
|
|
550
|
+
member.next = jsonSkipBlank(object, comma + 1)
|
|
551
|
+
}
|
|
552
|
+
return true
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The text a member's value answers: a string unquoted and unescaped, anything
|
|
557
|
+
* else as the bytes it was written with.
|
|
558
|
+
*/
|
|
559
|
+
const jsonValueText = (object: string, member: JsonMember): string => {
|
|
560
|
+
const valueAt: i32 = member.valueAt
|
|
561
|
+
const valueEnd: i32 = member.valueEnd
|
|
562
|
+
// `jsonReadMember` only fills in a value `jsonEndOfValue` ended, which starts
|
|
563
|
+
// inside the object and never ends past it, so this changes no answer: it is
|
|
564
|
+
// the range the value's first byte and its `substring` are proved in.
|
|
565
|
+
if (valueAt < 0 || valueAt >= valueEnd || valueEnd > toI32(object.length)) {
|
|
566
|
+
return ""
|
|
567
|
+
}
|
|
568
|
+
if (toI32(object.charCodeAt(valueAt)) === JSON_QUOTE) {
|
|
569
|
+
return jsonUnescape(object, valueAt + 1, valueEnd - 1)
|
|
570
|
+
}
|
|
571
|
+
return object.substring(valueAt, valueEnd)
|
|
572
|
+
}
|
|
573
|
+
|
|
346
574
|
/**
|
|
347
575
|
* The value of `name` in `object`, or `null` when the object does not have that
|
|
348
576
|
* field.
|
|
@@ -358,45 +586,73 @@ const jsonUnescape = (text: string, at: i32, end: i32): string => {
|
|
|
358
586
|
* and it is the shape the compiler's `--json` line has.
|
|
359
587
|
*/
|
|
360
588
|
export const jsonField = (object: string, name: string): string | null => {
|
|
361
|
-
const
|
|
362
|
-
let
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// is tested for `i < 0` beside `i >= length`: the pair is what lets each read
|
|
370
|
-
// below go without a check.
|
|
371
|
-
while (i >= 0 && i < length && toI32(object.charCodeAt(i)) === JSON_QUOTE) {
|
|
372
|
-
const keyEnd: i32 = jsonEndOfString(object, i)
|
|
373
|
-
if (keyEnd < 0) {
|
|
374
|
-
return null
|
|
375
|
-
}
|
|
376
|
-
const key: string = jsonUnescape(object, i + 1, keyEnd - 1)
|
|
377
|
-
i = jsonSkipBlank(object, keyEnd)
|
|
378
|
-
if (i < 0 || i >= length || toI32(object.charCodeAt(i)) !== JSON_COLON) {
|
|
379
|
-
return null
|
|
589
|
+
const member: JsonMember = new JsonMember()
|
|
590
|
+
let at: i32 = jsonFirstKey(object)
|
|
591
|
+
while (jsonReadMember(object, at, member)) {
|
|
592
|
+
// The key is compared where it stands rather than unescaped into a string
|
|
593
|
+
// first: every key before the one asked for is a no, and a no should not
|
|
594
|
+
// cost an allocation.
|
|
595
|
+
if (jsonKeyEquals(object, member.keyAt, member.keyEnd, name)) {
|
|
596
|
+
return jsonValueText(object, member)
|
|
380
597
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
598
|
+
at = member.next
|
|
599
|
+
}
|
|
600
|
+
return null
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* The value of every one of `names` in `object`, in the order of `names`: slot
|
|
605
|
+
* `k` is exactly what `jsonField(object, names[k])` answers.
|
|
606
|
+
*
|
|
607
|
+
* It is for a caller that wants several fields of one line — a diagnostic's
|
|
608
|
+
* `code`, `line` and `message` — and would otherwise scan the line once per
|
|
609
|
+
* field. This scans it once, left to right, and stops as soon as every name has
|
|
610
|
+
* answered: a speed path and nothing more, since a slot is filled only once
|
|
611
|
+
* and reading on could change no answer. Each slot keeps the **first** field of its name, as `jsonField`
|
|
612
|
+
* does; a name asked for twice answers in both slots; and a malformed object
|
|
613
|
+
* answers in each slot what `jsonField` answers for that name, because both
|
|
614
|
+
* stop at the same member.
|
|
615
|
+
*
|
|
616
|
+
* The answer is a fresh array and the only allocation besides the values. Each
|
|
617
|
+
* value is stored into it by a plain `values[k] = text` statement, and the
|
|
618
|
+
* array is only stored into, tested and returned, so the values travel with
|
|
619
|
+
* the array: the call lets nothing else out (LANGUAGE.md, "Memory model", a
|
|
620
|
+
* value stored into an array that is returned with it). A loop calling this
|
|
621
|
+
* keeps its per-pass release on a line older than the pass, as `jsonField`'s
|
|
622
|
+
* does, and a `using a = arena()` block may hold the call. On a line built in
|
|
623
|
+
* the pass neither reader keeps the release (LANGUAGE.md, "Memory model").
|
|
624
|
+
* Reading `values` back, passing it to a function, or storing it anywhere
|
|
625
|
+
* before the return would undo this; `tests/link/std_json` pins the flat
|
|
626
|
+
* arena and the `using` block.
|
|
627
|
+
*/
|
|
628
|
+
export const jsonFields = (object: string, names: string[]): (string | null)[] => {
|
|
629
|
+
const count: i32 = toI32(names.length)
|
|
630
|
+
const values: (string | null)[] = new Array<string | null>(count)
|
|
631
|
+
const member: JsonMember = new JsonMember()
|
|
632
|
+
let missing: i32 = count
|
|
633
|
+
let at: i32 = jsonFirstKey(object)
|
|
634
|
+
while (missing > 0 && jsonReadMember(object, at, member)) {
|
|
635
|
+
// A value is built once per member however many names it answers, and
|
|
636
|
+
// only when one of them does.
|
|
637
|
+
let text: string | null = null
|
|
638
|
+
let k: i32 = 0
|
|
639
|
+
// Bounded by both lengths, which are the same, because the bounds proof
|
|
640
|
+
// follows each array by its own length and not by `count`.
|
|
641
|
+
while (k < toI32(values.length) && k < toI32(names.length)) {
|
|
642
|
+
if (values[k] === null && jsonKeyEquals(object, member.keyAt, member.keyEnd, names[k])) {
|
|
643
|
+
if (text === null) {
|
|
644
|
+
text = jsonValueText(object, member)
|
|
645
|
+
}
|
|
646
|
+
// Always true: the loop's own test. A call stands between it and the
|
|
647
|
+
// store, and a call is where the bounds proof forgets a length.
|
|
648
|
+
if (k < toI32(values.length)) {
|
|
649
|
+
values[k] = text
|
|
650
|
+
}
|
|
651
|
+
missing -= 1
|
|
392
652
|
}
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
i = jsonSkipBlank(object, valueEnd)
|
|
396
|
-
if (i < 0 || i >= length || toI32(object.charCodeAt(i)) !== JSON_COMMA) {
|
|
397
|
-
return null
|
|
653
|
+
k += 1
|
|
398
654
|
}
|
|
399
|
-
|
|
655
|
+
at = member.next
|
|
400
656
|
}
|
|
401
|
-
return
|
|
657
|
+
return values
|
|
402
658
|
}
|
package/std/net/http3-server.ts
CHANGED
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
* configuration's streams and buffers and the `Http3Config`'s field section,
|
|
54
54
|
* body chunk and DATA frame. The constructor makes every slot's connections;
|
|
55
55
|
* a datagram an established connection reads, and every datagram it sends,
|
|
56
|
-
* allocates nothing
|
|
57
|
-
*
|
|
58
|
-
* `docs/security/http3.md`
|
|
56
|
+
* allocates nothing; nor does a datagram no slot owns, which the listener
|
|
57
|
+
* reads where it lies and answers into scratch made once, nor a handshake,
|
|
58
|
+
* whose state is the slot's — `docs/security/http3.md` (H3-1, H3-3).
|
|
59
59
|
*
|
|
60
60
|
* Written from RFC 9114 §3 and RFC 9000 §5.2, not ported from another
|
|
61
61
|
* implementation. Private names carry the `http3Server` prefix
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
import { QUIC_MAX_CID_LENGTH } from "nish/net/quic-packet"
|
|
77
77
|
import {
|
|
78
78
|
QUIC_LISTEN_ACCEPT,
|
|
79
|
+
QUIC_LISTENER_ANSWER_SIZE,
|
|
79
80
|
QUIC_LISTENER_FLIGHT_MAX,
|
|
80
81
|
QUIC_LISTENER_MIN_RESET,
|
|
81
82
|
QuicFlight,
|
|
@@ -84,7 +85,7 @@ import {
|
|
|
84
85
|
quicListenerPaceTime,
|
|
85
86
|
quicListenerTakeFlight,
|
|
86
87
|
} from "nish/net/quic-listener"
|
|
87
|
-
import {
|
|
88
|
+
import { TlsP256Signer } from "nish/net/tls"
|
|
88
89
|
|
|
89
90
|
/** The timer wheel's buckets, one millisecond each: its horizon. */
|
|
90
91
|
export const H3_SERVER_WHEEL: i32 = 2048
|
|
@@ -397,6 +398,8 @@ export class Http3Wheel {
|
|
|
397
398
|
export class Http3Server {
|
|
398
399
|
quicConfig: QuicServerConfig
|
|
399
400
|
listener: QuicListener
|
|
401
|
+
/** What the listener answers into, made once: its reply and IDs are refilled in their own room. */
|
|
402
|
+
answer: QuicListenerAnswer
|
|
400
403
|
quics: QuicConnection[]
|
|
401
404
|
conns: Http3Connection[]
|
|
402
405
|
index: Http3CidIndex
|
|
@@ -410,6 +413,8 @@ export class Http3Server {
|
|
|
410
413
|
meta: i32[]
|
|
411
414
|
entropy: u8[]
|
|
412
415
|
flight: QuicFlight
|
|
416
|
+
/** Signs every slot's CertificateVerify in `serve`, one for the server, keeping nothing per signature. */
|
|
417
|
+
signer: TlsP256Signer
|
|
413
418
|
/** Each slot's connection-ID index signature, so it is refiled only when its IDs changed. */
|
|
414
419
|
signatures: i64[]
|
|
415
420
|
state: i32[]
|
|
@@ -450,6 +455,7 @@ export class Http3Server {
|
|
|
450
455
|
this.fd = fd
|
|
451
456
|
this.index = new Http3CidIndex(size, listenerEntropy)
|
|
452
457
|
this.listener = new QuicListener(quicConfig, listenerEntropy)
|
|
458
|
+
this.answer = new QuicListenerAnswer(QUIC_LISTENER_ANSWER_SIZE)
|
|
453
459
|
this.wheel = new Http3Wheel(size)
|
|
454
460
|
this.quics = []
|
|
455
461
|
this.conns = []
|
|
@@ -467,6 +473,7 @@ export class Http3Server {
|
|
|
467
473
|
this.from = new Array<u8>(H3_SERVER_ADDRESS)
|
|
468
474
|
this.meta = [H3_SERVER_ZERO, H3_SERVER_ZERO]
|
|
469
475
|
this.flight = new QuicFlight()
|
|
476
|
+
this.signer = new TlsP256Signer()
|
|
470
477
|
this.signatures = new Array<i64>(size)
|
|
471
478
|
this.state = new Array<i32>(size)
|
|
472
479
|
this.readyRing = new Array<i32>(size)
|
|
@@ -605,17 +612,17 @@ export class Http3Server {
|
|
|
605
612
|
this.unowned(buf, at, len, address, now, key)
|
|
606
613
|
}
|
|
607
614
|
|
|
608
|
-
/**
|
|
615
|
+
/**
|
|
616
|
+
* A datagram no slot owns: the listener decides, reading it where it lies
|
|
617
|
+
* in `buf` and answering into `answer`, and may make a connection in a
|
|
618
|
+
* free slot, which reads the same window. Nothing is copied (H3-3).
|
|
619
|
+
*/
|
|
609
620
|
unowned(buf: u8[], at: i32, len: i32, address: u8[], now: i64, key: Secret<u8[]>): void {
|
|
610
621
|
if (!this.worthListening(buf, at, len, now)) {
|
|
611
622
|
return
|
|
612
623
|
}
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
datagram[k] = buf[at + k]
|
|
616
|
-
}
|
|
617
|
-
const answer: QuicListenerAnswer = this.listener.handle(datagram, address, now)
|
|
618
|
-
if (answer.kind !== QUIC_LISTEN_ACCEPT) {
|
|
624
|
+
const answer: QuicListenerAnswer = this.answer
|
|
625
|
+
if (this.listener.handleWindow(buf, at, len, address, now, answer) !== QUIC_LISTEN_ACCEPT) {
|
|
619
626
|
const reply: i32 = toI32(answer.reply.length)
|
|
620
627
|
if (reply > 0) {
|
|
621
628
|
this.send(answer.reply, H3_SERVER_ZERO, reply, address, H3_SERVER_ZERO)
|
|
@@ -646,9 +653,8 @@ export class Http3Server {
|
|
|
646
653
|
|
|
647
654
|
/**
|
|
648
655
|
* Whether the listener could answer `buf[at .. at + len)` at all, read
|
|
649
|
-
* from its first bytes: what it would drop without a word
|
|
650
|
-
*
|
|
651
|
-
* (H3-3). That is a short header too short for a stateless reset; a long
|
|
656
|
+
* from its first bytes: what it would drop without a word is dropped
|
|
657
|
+
* here, before the listener parses it. That is a short header too short for a stateless reset; a long
|
|
652
658
|
* one shorter than a full Initial, which neither Version Negotiation nor a
|
|
653
659
|
* new connection answers (RFC 9000 §14.1, §6.1); version 0; a version 1
|
|
654
660
|
* packet that is not an Initial, or whose DCID is under 8 bytes (§7.2);
|
|
@@ -686,7 +692,7 @@ export class Http3Server {
|
|
|
686
692
|
* `H3_SERVER_ANSWER_BURST` at once, then one per
|
|
687
693
|
* `H3_SERVER_ANSWER_INTERVAL` milliseconds — topping it up first. Past it
|
|
688
694
|
* the datagram is dropped before the listener sees it, so a flood of
|
|
689
|
-
* garbage costs
|
|
695
|
+
* garbage costs no answer and no MAC past the budget (H3-3); counted in `limited`.
|
|
690
696
|
*/
|
|
691
697
|
takeAnswer(now: i64): boolean {
|
|
692
698
|
if (this.answerRefilled < 0 || now < this.answerRefilled) {
|
|
@@ -713,7 +719,7 @@ export class Http3Server {
|
|
|
713
719
|
quic.receiveWindow(buf, at, len, now)
|
|
714
720
|
const input: u8[] | null = quic.signatureInput()
|
|
715
721
|
if (input !== null) {
|
|
716
|
-
const signature: u8[] | null =
|
|
722
|
+
const signature: u8[] | null = this.signer.sign(key, input)
|
|
717
723
|
if (signature !== null) {
|
|
718
724
|
quic.sign(signature)
|
|
719
725
|
}
|
|
@@ -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
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
352
|
+
quicParamsCopyInto(p.originalDcid, bytes, at, cidFits ? length : 0)
|
|
299
353
|
p.hasOriginalDcid = true
|
|
300
|
-
return
|
|
354
|
+
return cidFits
|
|
301
355
|
case QUIC_TP_STATELESS_RESET_TOKEN:
|
|
302
|
-
p.statelessResetToken
|
|
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
|
|
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
|
|
367
|
+
quicParamsCopyInto(p.initialScid, bytes, at, cidFits ? length : 0)
|
|
314
368
|
p.hasInitialScid = true
|
|
315
|
-
return
|
|
369
|
+
return cidFits
|
|
316
370
|
default:
|
|
317
|
-
p.retryScid
|
|
371
|
+
quicParamsCopyInto(p.retryScid, bytes, at, cidFits ? length : 0)
|
|
318
372
|
p.hasRetryScid = true
|
|
319
|
-
return
|
|
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.
|