@amritk/nish-x86_64-linux 0.28.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.
@@ -538,9 +538,22 @@ export class Http1ResponseParser {
538
538
  // --- the response: valid from its HEAD or INFORMATIONAL until the next ----
539
539
  /** The reason phrase, which RFC 9112 §4 says a client ignores; for a log. */
540
540
  reason: string = ""
541
- /** The field names, in lowercase, in the order they arrived; `values` beside them, OWS trimmed. */
541
+ /**
542
+ * The field names, in lowercase, in the order they arrived; `values` beside
543
+ * them, OWS trimmed. These and `reason` are filled only while the parser
544
+ * keeps strings, which it does until `fieldsAsSpans()`; `fields()`,
545
+ * `fieldName`, `fieldValue`, `reasonText` and `header` answer in either mode.
546
+ */
542
547
  names: string[]
543
548
  values: string[]
549
+ /**
550
+ * The response's reason phrase and kept fields, copied: `head[reasonStart ..
551
+ * reasonEnd)` is the reason, and field `i` is `head[spans[4i] ..
552
+ * spans[4i + 1])`, its name in lowercase, then `head[spans[4i + 2] ..
553
+ * spans[4i + 3])`, its value. Both arrays are reused from response to response.
554
+ */
555
+ head: u8[]
556
+ spans: i32[]
544
557
  /** The buffer the last `HTTP1_BODY`'s bytes are in, at `bodyOff`, `bodyLen` long. */
545
558
  body: u8[]
546
559
  /** What was wrong, once `next()` said `HTTP1_ERROR`; for a log. */
@@ -595,6 +608,13 @@ export class Http1ResponseParser {
595
608
  wasUpgrade: boolean = false
596
609
  /** Whether `finish()` said the server closed the connection. */
597
610
  closed: boolean = false
611
+ /** Whether `names`, `values` and `reason` are filled; `fieldsAsSpans()` clears it. */
612
+ keepStrings: boolean = true
613
+ /** Bytes of `head` in use, and the fields recorded in `spans`. */
614
+ headUsed: i32 = 0
615
+ spanCount: i32 = 0
616
+ reasonStart: i32 = 0
617
+ reasonEnd: i32 = 0
598
618
 
599
619
  /**
600
620
  * Each `i32` limit is clamped into 0 to 2^30 and `maxBody` into 0 to 2^53,
@@ -614,6 +634,46 @@ export class Http1ResponseParser {
614
634
  this.body = this.buf
615
635
  this.names = []
616
636
  this.values = []
637
+ this.head = []
638
+ this.spans = []
639
+ }
640
+
641
+ /**
642
+ * Stops filling `names`, `values` and `reason`, which then stay empty: a
643
+ * response's fields are read through `fields()`, `fieldName`, `fieldValue`,
644
+ * `reasonText` and `header`, which make a string only when asked. A parser
645
+ * kept for many responses allocates nothing per response this way.
646
+ */
647
+ fieldsAsSpans(): void {
648
+ this.keepStrings = false
649
+ this.names = []
650
+ this.values = []
651
+ this.reason = ""
652
+ }
653
+
654
+ /**
655
+ * Makes the parser as it was new, for the next connection, keeping its
656
+ * limits, its mode and its buffers: what was fed and not read is dropped,
657
+ * and an error or a close is forgotten.
658
+ */
659
+ reset(): void {
660
+ this.state = HTTP1_CLIENT_S_STATUS_LINE
661
+ this.start = 0
662
+ this.end = 0
663
+ this.scan = 0
664
+ this.lineStart = 0
665
+ this.lineEnd = 0
666
+ this.closed = false
667
+ this.error = 0
668
+ this.errorText = ""
669
+ this.informational = 0
670
+ this.wasHead = false
671
+ this.wasConnect = false
672
+ this.wasUpgrade = false
673
+ http1ClientStartHead(this)
674
+ this.left = toI64(0)
675
+ this.bodyOff = 0
676
+ this.body = this.buf
617
677
  }
618
678
 
619
679
  /**
@@ -704,16 +764,43 @@ export class Http1ResponseParser {
704
764
 
705
765
  /** The value of the first field named `name` (any case), or `null`. */
706
766
  header(name: string): string | null {
707
- const names: string[] = this.names
708
- const n: i32 = toI32(name.length)
709
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(this.values.length); i += 1) {
710
- if (http1SameWord(name, 0, n, names[i])) {
711
- return this.values[i]
767
+ for (let i: i32 = 0; i < this.spanCount; i += 1) {
768
+ if (http1ClientHeadSameWord(this.head, this.spans[i * 4], this.spans[i * 4 + 1], name)) {
769
+ return http1Text(this.head, this.spans[i * 4 + 2], this.spans[i * 4 + 3], false)
712
770
  }
713
771
  }
714
772
  return null
715
773
  }
716
774
 
775
+ /** How many fields the response last read has; trailers are not kept. */
776
+ fields(): i32 {
777
+ return this.spanCount
778
+ }
779
+
780
+ /** Field `i`'s name, in lowercase, as a fresh string; "" past the last field. */
781
+ fieldName(i: i32): string {
782
+ if (i < 0 || i >= this.spanCount) {
783
+ return ""
784
+ }
785
+ return http1Text(this.head, this.spans[i * 4], this.spans[i * 4 + 1], false)
786
+ }
787
+
788
+ /** Field `i`'s value, OWS trimmed, as a fresh string; "" past the last field. */
789
+ fieldValue(i: i32): string {
790
+ if (i < 0 || i >= this.spanCount) {
791
+ return ""
792
+ }
793
+ return http1Text(this.head, this.spans[i * 4 + 2], this.spans[i * 4 + 3], false)
794
+ }
795
+
796
+ /** The reason phrase, as a fresh string. */
797
+ reasonText(): string {
798
+ if (this.reasonEnd <= this.reasonStart) {
799
+ return ""
800
+ }
801
+ return http1Text(this.head, this.reasonStart, this.reasonEnd, false)
802
+ }
803
+
717
804
  /** How many bytes were fed and not consumed yet. */
718
805
  buffered(): i32 {
719
806
  return this.end - this.start
@@ -777,10 +864,17 @@ const http1ClientFail = (p: Http1ResponseParser, code: i32, text: string): i32 =
777
864
  /** Clears the per-response fields for the status line just taken. */
778
865
  const http1ClientStartHead = (p: Http1ResponseParser): void => {
779
866
  p.status = 0
780
- p.reason = ""
781
867
  p.minor = 1
782
- p.names = []
783
- p.values = []
868
+ // Fresh arrays, not cleared ones: a caller may hold the last response's.
869
+ if (p.keepStrings) {
870
+ p.reason = ""
871
+ p.names = []
872
+ p.values = []
873
+ }
874
+ p.headUsed = 0
875
+ p.spanCount = 0
876
+ p.reasonStart = 0
877
+ p.reasonEnd = 0
784
878
  p.contentLength = toI64(-1)
785
879
  p.chunked = false
786
880
  p.persistent = false
@@ -872,7 +966,13 @@ const http1ClientStatusLine = (p: Http1ResponseParser): i32 => {
872
966
  }
873
967
  p.minor = buf[a + 7] === 48 ? 0 : 1
874
968
  p.status = status
875
- p.reason = b - a > 13 ? http1Text(buf, a + 13, b, false) : ""
969
+ if (b - a > 13) {
970
+ p.reasonStart = http1ClientKeep(p, buf, a + 13, b, false)
971
+ p.reasonEnd = p.headUsed
972
+ if (p.keepStrings) {
973
+ p.reason = http1Text(buf, a + 13, b, false)
974
+ }
975
+ }
876
976
  p.state = HTTP1_CLIENT_S_FIELDS
877
977
  return HTTP1_NEED_MORE
878
978
  }
@@ -925,8 +1025,13 @@ const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
925
1025
  ) {
926
1026
  valueEnd -= 1
927
1027
  }
928
- p.names.push(http1Text(buf, a, nameEnd, true))
929
- p.values.push(http1Text(buf, valueStart, valueEnd, false))
1028
+ const name: i32 = http1ClientKeep(p, buf, a, nameEnd, true)
1029
+ const value: i32 = http1ClientKeep(p, buf, valueStart, valueEnd, false)
1030
+ http1ClientSpan(p, name, value, value + (valueEnd - valueStart))
1031
+ if (p.keepStrings) {
1032
+ p.names.push(http1Text(buf, a, nameEnd, true))
1033
+ p.values.push(http1Text(buf, valueStart, valueEnd, false))
1034
+ }
930
1035
  }
931
1036
  return HTTP1_NEED_MORE
932
1037
  }
@@ -940,20 +1045,20 @@ const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
940
1045
  * value past 2^53 is saturated to 2^53 + 1: over any `maxBody`. Saturated
941
1046
  * values cannot be told apart, so one seen with any other value is -2.
942
1047
  */
943
- const http1ClientLengthList = (value: string, seen: i64): i64 => {
944
- const n: i32 = toI32(value.length)
1048
+ const http1ClientLengthList = (head: u8[], from: i32, to: i32, seen: i64): i64 => {
1049
+ const n: i32 = to < toI32(head.length) ? to : toI32(head.length)
945
1050
  let found: i64 = seen
946
- let start: i32 = 0
947
- for (let k: i32 = 0; k <= n; k += 1) {
948
- if (k === n || http1ClientIsComma(value, k)) {
949
- const a: i32 = http1ClientTrimStart(value, start, k)
950
- const b: i32 = http1ClientTrimEnd(value, a, k)
1051
+ let start: i32 = from
1052
+ for (let k: i32 = from; k <= n; k += 1) {
1053
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1054
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1055
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
951
1056
  if (a === b) {
952
1057
  return toI64(-1)
953
1058
  }
954
1059
  let v: i64 = toI64(0)
955
- for (let j: i32 = a; j >= 0 && j < b && j < n; j += 1) {
956
- const c: i32 = toI32(value.charCodeAt(j))
1060
+ for (let j: i32 = a; j >= 0 && j < b && j < toI32(head.length); j += 1) {
1061
+ const c: i32 = toI32(head[j])
957
1062
  if (c < 48 || c > 57) {
958
1063
  return toI64(-1)
959
1064
  }
@@ -982,20 +1087,19 @@ const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i
982
1087
  let elements: i32 = 0
983
1088
  let matches: i32 = 0
984
1089
  let last: boolean = false
985
- const names: string[] = p.names
986
- const values: string[] = p.values
987
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
988
- if (names[i] === name) {
989
- const value: string = values[i]
990
- const n: i32 = toI32(value.length)
991
- let start: i32 = 0
992
- for (let k: i32 = 0; k <= n; k += 1) {
993
- if (k === n || http1ClientIsComma(value, k)) {
994
- const a: i32 = http1ClientTrimStart(value, start, k)
995
- const b: i32 = http1ClientTrimEnd(value, a, k)
1090
+ const head: u8[] = p.head
1091
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1092
+ if (http1ClientHeadIs(head, p.spans[i * 4], p.spans[i * 4 + 1], name)) {
1093
+ const from: i32 = p.spans[i * 4 + 2]
1094
+ const n: i32 = p.spans[i * 4 + 3] < toI32(head.length) ? p.spans[i * 4 + 3] : toI32(head.length)
1095
+ let start: i32 = from
1096
+ for (let k: i32 = from; k <= n; k += 1) {
1097
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1098
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1099
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
996
1100
  if (b > a) {
997
1101
  elements += 1
998
- last = http1SameWord(value, a, b, word)
1102
+ last = http1ClientHeadSameWord(head, a, b, word)
999
1103
  if (last) {
1000
1104
  matches += 1
1001
1105
  }
@@ -1010,15 +1114,139 @@ const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i
1010
1114
 
1011
1115
  /** Whether a `Connection` field of the response lists `word`. */
1012
1116
  const http1ClientConnectionHas = (p: Http1ResponseParser, word: string): boolean => {
1013
- const names: string[] = p.names
1014
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
1015
- if (names[i] === "connection" && http1ClientListHas(p.values[i], word)) {
1117
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1118
+ if (
1119
+ http1ClientHeadIs(p.head, p.spans[i * 4], p.spans[i * 4 + 1], "connection") &&
1120
+ http1ClientHeadListHas(p.head, p.spans[i * 4 + 2], p.spans[i * 4 + 3], word)
1121
+ ) {
1016
1122
  return true
1017
1123
  }
1018
1124
  }
1019
1125
  return false
1020
1126
  }
1021
1127
 
1128
+ /**
1129
+ * Copies `buf[a .. b)` to the end of `p.head`, lowercased when `lower`, and
1130
+ * answers where the copy starts. `head` grows by doubling and is kept, so a
1131
+ * parser reused for many responses stops growing at the largest head.
1132
+ */
1133
+ const http1ClientKeep = (p: Http1ResponseParser, buf: u8[], a: i32, b: i32, lower: boolean): i32 => {
1134
+ const n: i32 = b - a
1135
+ const at: i32 = p.headUsed
1136
+ if (n > toI32(p.head.length) - at) {
1137
+ let size: i32 = toI32(p.head.length) < 256 ? 256 : toI32(p.head.length)
1138
+ while (size - at < n) {
1139
+ if (size > 1073741823) {
1140
+ panic("Http1ResponseParser: more than 2^30 bytes of head")
1141
+ }
1142
+ size = size * 2
1143
+ }
1144
+ const grown: u8[] = new Array<u8>(size)
1145
+ copyBytes(grown, 0, p.head, 0, at)
1146
+ p.head = grown
1147
+ }
1148
+ copyBytes(p.head, at, buf, a, n)
1149
+ if (lower) {
1150
+ const head: u8[] = p.head
1151
+ for (let k: i32 = at; k >= 0 && k < at + n && k < toI32(head.length); k += 1) {
1152
+ const c: i32 = toI32(head[k])
1153
+ if (c >= 65 && c <= 90) {
1154
+ head[k] = toU8(c + 32)
1155
+ }
1156
+ }
1157
+ }
1158
+ p.headUsed = at + n
1159
+ return at
1160
+ }
1161
+
1162
+ /** Records the field whose name starts at `name` and whose value is `head[value .. valueEnd)`. */
1163
+ const http1ClientSpan = (p: Http1ResponseParser, name: i32, value: i32, valueEnd: i32): void => {
1164
+ const at: i32 = p.spanCount * 4
1165
+ if (at + 4 > toI32(p.spans.length)) {
1166
+ p.spans.push(name)
1167
+ p.spans.push(value)
1168
+ p.spans.push(value)
1169
+ p.spans.push(valueEnd)
1170
+ } else {
1171
+ p.spans[at] = name
1172
+ p.spans[at + 1] = value
1173
+ p.spans[at + 2] = value
1174
+ p.spans[at + 3] = valueEnd
1175
+ }
1176
+ p.spanCount += 1
1177
+ }
1178
+
1179
+ /** Whether `head[a .. b)` is `word`, byte for byte. */
1180
+ const http1ClientHeadIs = (head: u8[], a: i32, b: i32, word: string): boolean => {
1181
+ const n: i32 = toI32(word.length)
1182
+ if (b - a !== n || a < 0 || b > toI32(head.length)) {
1183
+ return false
1184
+ }
1185
+ for (let k: i32 = 0; k < n; k += 1) {
1186
+ if (toI32(head[a + k]) !== toI32(word.charCodeAt(k))) {
1187
+ return false
1188
+ }
1189
+ }
1190
+ return true
1191
+ }
1192
+
1193
+ /** Whether `head[a .. b)` is `word` in any case, ASCII letters folded. */
1194
+ const http1ClientHeadSameWord = (head: u8[], a: i32, b: i32, word: string): boolean => {
1195
+ const n: i32 = toI32(word.length)
1196
+ if (b - a !== n || a < 0 || b > toI32(head.length)) {
1197
+ return false
1198
+ }
1199
+ for (let k: i32 = 0; k < n; k += 1) {
1200
+ let x: i32 = toI32(head[a + k])
1201
+ let y: i32 = toI32(word.charCodeAt(k))
1202
+ if (x >= 65 && x <= 90) {
1203
+ x += 32
1204
+ }
1205
+ if (y >= 65 && y <= 90) {
1206
+ y += 32
1207
+ }
1208
+ if (x !== y) {
1209
+ return false
1210
+ }
1211
+ }
1212
+ return true
1213
+ }
1214
+
1215
+ /** Where `head[a .. b)` starts once its leading OWS is dropped. */
1216
+ const http1ClientHeadTrimStart = (head: u8[], a: i32, b: i32): i32 => {
1217
+ let i: i32 = a
1218
+ while (i >= 0 && i < b && i < toI32(head.length) && http1IsOws(toI32(head[i]))) {
1219
+ i += 1
1220
+ }
1221
+ return i
1222
+ }
1223
+
1224
+ /** Where `head[a .. b)` ends once its trailing OWS is dropped. */
1225
+ const http1ClientHeadTrimEnd = (head: u8[], a: i32, b: i32): i32 => {
1226
+ let i: i32 = b
1227
+ while (i > a && i > 0 && i <= toI32(head.length) && http1IsOws(toI32(head[i - 1]))) {
1228
+ i -= 1
1229
+ }
1230
+ return i
1231
+ }
1232
+
1233
+ /** Whether the comma-separated list in `head[from .. to)` holds `word` in any case, each element trimmed of OWS. */
1234
+ const http1ClientHeadListHas = (head: u8[], from: i32, to: i32, word: string): boolean => {
1235
+ const n: i32 = to < toI32(head.length) ? to : toI32(head.length)
1236
+ let start: i32 = from
1237
+ for (let k: i32 = from; k <= n; k += 1) {
1238
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1239
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1240
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
1241
+ if (http1ClientHeadSameWord(head, a, b, word)) {
1242
+ return true
1243
+ }
1244
+ start = k + 1
1245
+ }
1246
+ }
1247
+ return false
1248
+ }
1249
+
1022
1250
  /**
1023
1251
  * A header section has ended: answer `HTTP1_INFORMATIONAL` for a 1xx, or
1024
1252
  * decide the final response's framing and the connection's fate (RFC 9112
@@ -1052,12 +1280,13 @@ const http1ClientEndOfHead = (p: Http1ResponseParser): i32 => {
1052
1280
  }
1053
1281
  let codings: boolean = false
1054
1282
  let length: i64 = toI64(-1)
1055
- const names: string[] = p.names
1056
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
1057
- if (names[i] === "transfer-encoding") {
1283
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1284
+ const a: i32 = p.spans[i * 4]
1285
+ const b: i32 = p.spans[i * 4 + 1]
1286
+ if (http1ClientHeadIs(p.head, a, b, "transfer-encoding")) {
1058
1287
  codings = true
1059
- } else if (names[i] === "content-length") {
1060
- length = http1ClientLengthList(p.values[i], length)
1288
+ } else if (http1ClientHeadIs(p.head, a, b, "content-length")) {
1289
+ length = http1ClientLengthList(p.head, p.spans[i * 4 + 2], p.spans[i * 4 + 3], length)
1061
1290
  if (length === toI64(-2)) {
1062
1291
  return http1ClientFail(
1063
1292
  p,