@amritk/nish-x86_64-linux 0.29.0 → 0.30.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/std/net/http1.ts CHANGED
@@ -81,6 +81,7 @@
81
81
  * Written from RFC 9112 and RFC 9110, not ported from another implementation.
82
82
  */
83
83
  import { copyBytes } from "nish/bytes"
84
+ import { extract, loadU8x16, u8x16 } from "nish:simd"
84
85
 
85
86
  /** `next()`: the input so far holds no further event; feed more. */
86
87
  export const HTTP1_NEED_MORE: i32 = 0
@@ -218,7 +219,13 @@ export class Http1Parser {
218
219
  /** The line `http1TakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
219
220
  lineStart: i32 = 0
220
221
  lineEnd: i32 = 0
221
-
222
+ /**
223
+ * Whether the line being scanned holds a byte a field value may not: a
224
+ * control byte other than HTAB, or DEL (RFC 9110 §5.5). `http1TakeLine`
225
+ * sets it as it looks for the line's end, so a field line's bytes are read
226
+ * once, not once for the LF and again for the check.
227
+ */
228
+ lineControl: boolean = false
222
229
  chunked: boolean = false
223
230
  keepAlive: boolean = false
224
231
  /** Whether the request asked to switch protocols (`upgrade` is its value when `keepText`). */
@@ -498,6 +505,60 @@ const http1StartRequest = (p: Http1Parser): void => {
498
505
  p.headHeld = false
499
506
  }
500
507
 
508
+ /**
509
+ * Whether any of the eight bytes of `x` is below 32 or is 127: a control
510
+ * byte or DEL, which `http1TakeLine` stops at. Two of the bit tricks of
511
+ * "Bit Twiddling Hacks" (S. E. Anderson, public domain): a byte below `n`
512
+ * leaves its high bit set in `(x - n * ones) & ~x`, for `n` up to 128, and
513
+ * `127` is the byte that is zero in `x ^ 127 * ones`. A byte from 128 up
514
+ * has its high bit set in `x`, so `~x` clears it: obs-text never matches.
515
+ * The answer is exact for the word, though not for which byte. Each
516
+ * subtraction is masked to 64 bits because a `u64` is a BigInt under Node,
517
+ * which does not wrap; natively the mask is a no-op.
518
+ */
519
+ const http1WordHasControl = (x: u64): boolean => {
520
+ const ones32: u64 = toU64(16843009)
521
+ const ones: u64 = (ones32 << toU64(32)) | ones32
522
+ const low32: u64 = 0xffffffff
523
+ const all: u64 = (low32 << toU64(32)) | low32
524
+ const high: u64 = ones * toU64(128)
525
+ const below: u64 = (x - ones * toU64(32)) & all & ~x & high
526
+ const y: u64 = x ^ (ones * toU64(127))
527
+ const del: u64 = (y - ones) & all & ~y & high
528
+ return (below | del) !== toU64(0)
529
+ }
530
+
531
+ /**
532
+ * Lanes `at` to `at + 7` of `v`, the first the lowest byte: eight bytes of a
533
+ * sixteen-byte load as one word, which `opt` folds to a bitcast of the
534
+ * vector's half. One load is one bounds check for sixteen bytes, where eight
535
+ * reads of `buf[i + k]` would be eight.
536
+ */
537
+ const http1Lanes = (v: u8x16, at: i32): u64 => {
538
+ if (at === 0) {
539
+ return (
540
+ toU64(extract(v, 0)) |
541
+ (toU64(extract(v, 1)) << toU64(8)) |
542
+ (toU64(extract(v, 2)) << toU64(16)) |
543
+ (toU64(extract(v, 3)) << toU64(24)) |
544
+ (toU64(extract(v, 4)) << toU64(32)) |
545
+ (toU64(extract(v, 5)) << toU64(40)) |
546
+ (toU64(extract(v, 6)) << toU64(48)) |
547
+ (toU64(extract(v, 7)) << toU64(56))
548
+ )
549
+ }
550
+ return (
551
+ toU64(extract(v, 8)) |
552
+ (toU64(extract(v, 9)) << toU64(8)) |
553
+ (toU64(extract(v, 10)) << toU64(16)) |
554
+ (toU64(extract(v, 11)) << toU64(24)) |
555
+ (toU64(extract(v, 12)) << toU64(32)) |
556
+ (toU64(extract(v, 13)) << toU64(40)) |
557
+ (toU64(extract(v, 14)) << toU64(48)) |
558
+ (toU64(extract(v, 15)) << toU64(56))
559
+ )
560
+ }
561
+
501
562
  /** `http1TakeLine`'s answer when a whole line is ready; never an event. */
502
563
  const HTTP1_LINE: i32 = -1
503
564
 
@@ -505,14 +566,42 @@ const HTTP1_LINE: i32 = -1
505
566
  * Finds the next line: answers `HTTP1_LINE` with it in `lineStart ..
506
567
  * lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A line ends in CRLF and in
507
568
  * nothing else; a LF without its CR is a 400. A line that has not ended by
508
- * `limit` bytes is refused with `overStatus`.
569
+ * `limit` bytes is refused with `overStatus`. On the way it sets
570
+ * `lineControl` when the line holds a byte no field value may, which
571
+ * `http1FieldLine` reads instead of scanning the value again.
509
572
  */
510
573
  const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
511
574
  const buf: u8[] = p.buf
512
575
  const end: i32 = p.end
513
- let i: i32 = p.scan > p.start ? p.scan : p.start
514
- while (i >= 0 && i < end && i < toI32(buf.length)) {
515
- if (buf[i] === 10) {
576
+ let i: i32 = p.start
577
+ if (p.scan > p.start) {
578
+ i = p.scan
579
+ } else {
580
+ p.lineControl = false
581
+ }
582
+ const stop: i32 = end < toI32(buf.length) ? end : toI32(buf.length)
583
+ if (i < 0) {
584
+ i = 0
585
+ }
586
+ while (i < stop) {
587
+ // Most bytes are printable: sixteen at a time while a whole vector is,
588
+ // then one at a time, with no test in either loop but the bytes' and the
589
+ // bound's.
590
+ while (i >= 0 && i <= stop - 16 && i <= toI32(buf.length) - 16) {
591
+ const v: u8x16 = loadU8x16(buf, i)
592
+ if (http1WordHasControl(http1Lanes(v, 0)) || http1WordHasControl(http1Lanes(v, 8))) {
593
+ break
594
+ }
595
+ i += 16
596
+ }
597
+ while (i >= 0 && i < stop && i < toI32(buf.length) && buf[i] >= 32 && buf[i] !== 127) {
598
+ i += 1
599
+ }
600
+ if (i < 0 || i >= stop || i >= toI32(buf.length)) {
601
+ break
602
+ }
603
+ const c: i32 = toI32(buf[i])
604
+ if (c === 10) {
516
605
  if (i === p.start || buf[i - 1] !== 13) {
517
606
  return http1Fail(p, 400, "a line ended in a bare LF")
518
607
  }
@@ -525,31 +614,49 @@ const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
525
614
  p.scan = i + 1
526
615
  return HTTP1_LINE
527
616
  }
617
+ if (c === 13 && i + 1 >= stop) {
618
+ // A CR at the end of the bytes so far: whether it ends the line is
619
+ // the next byte's to say, so the next call looks at it again.
620
+ break
621
+ }
622
+ if (c !== 9 && !(c === 13 && i + 1 < toI32(buf.length) && buf[i + 1] === 10)) {
623
+ p.lineControl = true
624
+ }
528
625
  i += 1
529
626
  }
530
- p.scan = end
627
+ p.scan = i
531
628
  if (end - p.start > limit + 1) {
532
629
  return http1Fail(p, overStatus, "a line over its limit")
533
630
  }
534
631
  return HTTP1_NEED_MORE
535
632
  }
536
633
 
634
+ /**
635
+ * The tchars of RFC 9110 §5.6.2 as three 32-bit masks, bit `c - 32`,
636
+ * `c - 64` and `c - 96` of each: `!` `#`-`'` `*` `+` `-` `.` and the digits
637
+ * in the first (0x03ff6cfa); the upper-case letters, `^` and `_` in the
638
+ * second (0xc7fffffe); `` ` ``, the lower-case letters, `|` and `~` in the
639
+ * third (0x57ffffff). No byte below 33 or above 126 is a tchar. One shift
640
+ * and mask replaces a chain of fourteen comparisons on the parser's hottest
641
+ * path (`bench/techempower`).
642
+ */
643
+ const HTTP1_TCHAR_32: i32 = 67071226
644
+ const HTTP1_TCHAR_64: i32 = -939524098
645
+ const HTTP1_TCHAR_96: i32 = 1476395007
646
+
537
647
  /** Whether `c` is a tchar of RFC 9110 §5.6.2, the bytes a token is made of. */
538
- export const http1IsTokenByte = (c: i32): boolean =>
539
- (c >= 97 && c <= 122) ||
540
- (c >= 65 && c <= 90) ||
541
- (c >= 48 && c <= 57) ||
542
- c === 33 ||
543
- (c >= 35 && c <= 39) ||
544
- c === 42 ||
545
- c === 43 ||
546
- c === 45 ||
547
- c === 46 ||
548
- c === 94 ||
549
- c === 95 ||
550
- c === 96 ||
551
- c === 124 ||
552
- c === 126
648
+ export const http1IsTokenByte = (c: i32): boolean => {
649
+ if (c < 32 || c > 127) {
650
+ return false
651
+ }
652
+ let mask: i32 = HTTP1_TCHAR_96
653
+ if (c < 64) {
654
+ mask = HTTP1_TCHAR_32
655
+ } else if (c < 96) {
656
+ mask = HTTP1_TCHAR_64
657
+ }
658
+ return ((mask >> (c & 31)) & 1) !== 0
659
+ }
553
660
 
554
661
  /** Whether `c` may appear in a field value (RFC 9110 §5.5): HTAB, SP, VCHAR or obs-text. */
555
662
  export const http1IsValueByte = (c: i32): boolean => c === 9 || (c >= 32 && c !== 127)
@@ -720,6 +827,57 @@ const http1Tally = (p: Http1Parser, name: string, word: string): Http1ListTally
720
827
  return tally
721
828
  }
722
829
 
830
+ /** `http1ConnectionOptions`: the Connection list names `close`. */
831
+ const HTTP1_CONNECTION_CLOSE: i32 = 1
832
+ /** `http1ConnectionOptions`: the Connection list names `keep-alive`. */
833
+ const HTTP1_CONNECTION_KEEP_ALIVE: i32 = 2
834
+ /** `http1ConnectionOptions`: the Connection list names `upgrade`. */
835
+ const HTTP1_CONNECTION_UPGRADE: i32 = 4
836
+
837
+ /**
838
+ * Which of `close`, `keep-alive` and `upgrade` the Connection fields name,
839
+ * in any case, as `HTTP1_CONNECTION_` bits: one walk of every Connection
840
+ * field's list, where three `http1Tally` walks would each read it all. An
841
+ * element is trimmed of OWS and compared whole, as `http1TallyRun` compares.
842
+ */
843
+ const http1ConnectionOptions = (p: Http1Parser): i32 => {
844
+ const spans: i32[] = p.spans
845
+ const head: u8[] = p.head
846
+ let found: i32 = 0
847
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
848
+ if (!http1HeadIs(head, spans[4 * i], spans[4 * i + 1], "connection", false)) {
849
+ continue
850
+ }
851
+ const to: i32 = spans[4 * i + 3]
852
+ let start: i32 = spans[4 * i + 2]
853
+ for (let k: i32 = start; k <= to; k += 1) {
854
+ const c: i32 = k < to && k >= 0 && k < toI32(head.length) ? toI32(head[k]) : 44
855
+ if (c !== 44) {
856
+ continue
857
+ }
858
+ let a: i32 = start
859
+ let b: i32 = k
860
+ while (a >= 0 && a < b && a < toI32(head.length) && http1IsOws(toI32(head[a]))) {
861
+ a += 1
862
+ }
863
+ while (b > a && b - 1 >= 0 && b - 1 < toI32(head.length) && http1IsOws(toI32(head[b - 1]))) {
864
+ b -= 1
865
+ }
866
+ // The length picks the one word an element could be.
867
+ const n: i32 = b - a
868
+ if (n === 5 && http1HeadIs(head, a, b, "close", true)) {
869
+ found = found | HTTP1_CONNECTION_CLOSE
870
+ } else if (n === 10 && http1HeadIs(head, a, b, "keep-alive", true)) {
871
+ found = found | HTTP1_CONNECTION_KEEP_ALIVE
872
+ } else if (n === 7 && http1HeadIs(head, a, b, "upgrade", true)) {
873
+ found = found | HTTP1_CONNECTION_UPGRADE
874
+ }
875
+ start = k + 1
876
+ }
877
+ }
878
+ return found
879
+ }
880
+
723
881
  /**
724
882
  * Whether the buffered start of a request line holds more than
725
883
  * `HTTP1_MAX_METHOD` bytes before its first space (or line end).
@@ -828,10 +986,10 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
828
986
  return http1Fail(p, 400, "a malformed field name")
829
987
  }
830
988
  const nameEnd: i32 = i
831
- for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
832
- if (!http1IsValueByte(toI32(buf[k]))) {
833
- return http1Fail(p, 400, "a control character in a field value")
834
- }
989
+ // The name is tchars and the colon is field text, so a byte no value may
990
+ // hold anywhere on the line is one in the value.
991
+ if (p.lineControl) {
992
+ return http1Fail(p, 400, "a control character in a field value")
835
993
  }
836
994
  p.fieldCount += 1
837
995
  if (p.fieldCount > p.maxHeaders) {
@@ -856,9 +1014,13 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
856
1014
  }
857
1015
  const base: i32 = 4 * p.headerCount
858
1016
  http1SpanSet(p, base + 3, at + (valueEnd - a))
859
- http1SpanSet(p, base, at)
860
- http1SpanSet(p, base + 1, at + (nameEnd - a))
861
- http1SpanSet(p, base + 2, at + (valueStart - a))
1017
+ // The last of the four is set first, so the array has room for them all.
1018
+ const spans: i32[] = p.spans
1019
+ if (base >= 0 && base + 2 < toI32(spans.length)) {
1020
+ spans[base] = at
1021
+ spans[base + 1] = at + (nameEnd - a)
1022
+ spans[base + 2] = at + (valueStart - a)
1023
+ }
862
1024
  p.headerCount += 1
863
1025
  if (p.keepText) {
864
1026
  p.names.push(http1Text(buf, a, nameEnd, true))
@@ -938,12 +1100,12 @@ const http1EndOfHead = (p: Http1Parser): i32 => {
938
1100
  }
939
1101
  p.contentLength = size
940
1102
  }
941
- // Each tally is read before the next one reuses it, and none is walked
942
- // for a request without a Connection field, which is most of them.
943
- const listed: boolean = connectionFields > 0
944
- const close: boolean = listed && http1Tally(p, "connection", "close").matches > 0
945
- const keep: boolean = listed && http1Tally(p, "connection", "keep-alive").matches > 0
946
- const upgradeAsked: boolean = listed && http1Tally(p, "connection", "upgrade").matches > 0
1103
+ // One walk of the Connection list finds all three options, and none is
1104
+ // made for a request without a Connection field.
1105
+ const options: i32 = connectionFields > 0 ? http1ConnectionOptions(p) : 0
1106
+ const close: boolean = (options & HTTP1_CONNECTION_CLOSE) !== 0
1107
+ const keep: boolean = (options & HTTP1_CONNECTION_KEEP_ALIVE) !== 0
1108
+ const upgradeAsked: boolean = (options & HTTP1_CONNECTION_UPGRADE) !== 0
947
1109
  p.keepAlive = p.minor === 1 ? !close : keep && !close
948
1110
  // The fields of one name joined with ", " are empty only when there is one
949
1111
  // field and it is empty: the same test, without the join.
@@ -1405,9 +1567,18 @@ export const http1BytesAre = (bytes: u8[], word: string): boolean =>
1405
1567
 
1406
1568
  /** How many decimal digits `n`, which is not negative, takes. */
1407
1569
  const http1DecimalDigits = (n: i32): i32 => {
1408
- let digits: i32 = 1
1409
- let v: i32 = n
1410
- while (v >= 10) {
1570
+ if (n < 10) {
1571
+ return 1
1572
+ }
1573
+ if (n < 100) {
1574
+ return 2
1575
+ }
1576
+ if (n < 1000) {
1577
+ return 3
1578
+ }
1579
+ let digits: i32 = 4
1580
+ let v: i32 = n / 10000
1581
+ while (v > 0) {
1411
1582
  v = v / 10
1412
1583
  digits += 1
1413
1584
  }
@@ -1425,9 +1596,31 @@ export const http1PutText = (out: u8[], at: i32, text: string): i32 => {
1425
1596
  return at + n
1426
1597
  }
1427
1598
 
1428
- /** Writes `bytes` at `out[at ..)`, likewise. */
1599
+ /** Writes CR LF at `out[at ..)`, likewise: two stores, where `http1PutText` walks a string. */
1600
+ const http1PutCrlf = (out: u8[], at: i32): i32 => {
1601
+ if (at >= 0 && at < toI32(out.length) - 1) {
1602
+ out[at] = 13
1603
+ out[at + 1] = 10
1604
+ }
1605
+ return at + 2
1606
+ }
1607
+
1608
+ /** Writes `: ` at `out[at ..)`, likewise. */
1609
+ const http1PutColonSpace = (out: u8[], at: i32): i32 => {
1610
+ if (at >= 0 && at < toI32(out.length) - 1) {
1611
+ out[at] = 58
1612
+ out[at + 1] = 32
1613
+ }
1614
+ return at + 2
1615
+ }
1616
+
1617
+ /** Writes `bytes` at `out[at ..)`, likewise, as one copy when it fits. */
1429
1618
  const http1PutBytes = (out: u8[], at: i32, bytes: u8[]): i32 => {
1430
1619
  const n: i32 = toI32(bytes.length)
1620
+ if (at >= 0 && at <= toI32(out.length) - n) {
1621
+ copyBytes(out, at, bytes, 0, n)
1622
+ return at + n
1623
+ }
1431
1624
  for (let k: i32 = 0; k < n && at + k < toI32(out.length); k += 1) {
1432
1625
  if (at + k >= 0) {
1433
1626
  out[at + k] = bytes[k]
@@ -1525,32 +1718,42 @@ export const http1WriteResponseHead = (
1525
1718
  return HTTP1_NO_ROOM
1526
1719
  }
1527
1720
  let end: i32 = http1PutText(out, at, "HTTP/1.1 ")
1528
- end = http1PutDecimal(out, end, status)
1529
- end = http1PutText(out, end, " ")
1721
+ // A status is three digits, checked above: no division loop for them.
1722
+ if (end >= 0 && end < toI32(out.length) - 3) {
1723
+ const hundreds: i32 = status / 100
1724
+ const tens: i32 = status / 10
1725
+ const tensDigit: i32 = tens % 10
1726
+ const ones: i32 = status % 10
1727
+ out[end] = toU8(48 + hundreds)
1728
+ out[end + 1] = toU8(48 + tensDigit)
1729
+ out[end + 2] = toU8(48 + ones)
1730
+ out[end + 3] = 32
1731
+ }
1732
+ end = end + 4
1530
1733
  end = http1PutText(out, end, reason)
1531
- end = http1PutText(out, end, "\r\n")
1734
+ end = http1PutCrlf(out, end)
1532
1735
  for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
1533
1736
  end = http1PutBytes(out, end, names[i])
1534
- end = http1PutText(out, end, ": ")
1737
+ end = http1PutColonSpace(out, end)
1535
1738
  end = http1PutBytes(out, end, values[i])
1536
- end = http1PutText(out, end, "\r\n")
1739
+ end = http1PutCrlf(out, end)
1537
1740
  }
1538
1741
  if (sendDate) {
1539
1742
  end = http1PutText(out, end, "Date: ")
1540
1743
  end = http1PutBytes(out, end, date)
1541
- end = http1PutText(out, end, "\r\n")
1744
+ end = http1PutCrlf(out, end)
1542
1745
  }
1543
1746
  if (bodyLength >= 0) {
1544
1747
  end = http1PutText(out, end, "Content-Length: ")
1545
1748
  end = http1PutDecimal(out, end, bodyLength)
1546
- end = http1PutText(out, end, "\r\n")
1749
+ end = http1PutCrlf(out, end)
1547
1750
  } else if (bodyLength === HTTP1_CHUNKED) {
1548
1751
  end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
1549
1752
  }
1550
1753
  if (toI32(connection.length) > 0) {
1551
1754
  end = http1PutText(out, end, "Connection: ")
1552
1755
  end = http1PutText(out, end, connection)
1553
- end = http1PutText(out, end, "\r\n")
1756
+ end = http1PutCrlf(out, end)
1554
1757
  }
1555
- return http1PutText(out, end, "\r\n")
1758
+ return http1PutCrlf(out, end)
1556
1759
  }