quico 0.1.3 → 0.3.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/src/utils.js ADDED
@@ -0,0 +1,345 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+
16
+ // ============================================================
17
+ // Debug flag — set QUICO_DEBUG=1 to enable verbose logging.
18
+ // Read once at module load. Call sites guard with `if (DEBUG)`
19
+ // so the (expensive) log-string construction is skipped entirely
20
+ // when debugging is off.
21
+ // ============================================================
22
+
23
+ var DEBUG = !!(typeof process !== 'undefined' && process.env && process.env.QUICO_DEBUG);
24
+
25
+
26
+ // ============================================================
27
+ // Emitter — lightweight event emitter (single shared copy)
28
+ // ============================================================
29
+
30
+ function Emitter() {
31
+ var listeners = {};
32
+ return {
33
+ on: function (name, fn) {
34
+ (listeners[name] = listeners[name] || []).push(fn);
35
+ },
36
+ off: function (name, fn) {
37
+ var arr = listeners[name];
38
+ if (arr) {
39
+ var idx = arr.indexOf(fn);
40
+ if (idx !== -1) arr.splice(idx, 1);
41
+ }
42
+ },
43
+ emit: function (name) {
44
+ var args = Array.prototype.slice.call(arguments, 1);
45
+ var arr = listeners[name] || [];
46
+ for (var i = 0; i < arr.length; i++) {
47
+ try { arr[i].apply(null, args); } catch (e) { /* swallow */ }
48
+ }
49
+ }
50
+ };
51
+ }
52
+
53
+
54
+ // ============================================================
55
+ // QUIC Variable-Length Integer (RFC 9000 §16)
56
+ // ============================================================
57
+
58
+ function writeVarInt(value) {
59
+ if (value < 0x40) {
60
+ return new Uint8Array([value]);
61
+ }
62
+ if (value < 0x4000) {
63
+ return new Uint8Array([
64
+ 0x40 | (value >> 8),
65
+ value & 0xff
66
+ ]);
67
+ }
68
+ if (value < 0x40000000) {
69
+ return new Uint8Array([
70
+ 0x80 | (value >> 24),
71
+ (value >> 16) & 0xff,
72
+ (value >> 8) & 0xff,
73
+ value & 0xff
74
+ ]);
75
+ }
76
+ if (value <= Number.MAX_SAFE_INTEGER) {
77
+ var hi = Math.floor(value / 2 ** 32);
78
+ var lo = value >>> 0;
79
+ return new Uint8Array([
80
+ 0xc0 | (hi >> 24),
81
+ (hi >> 16) & 0xff,
82
+ (hi >> 8) & 0xff,
83
+ hi & 0xff,
84
+ (lo >> 24) & 0xff,
85
+ (lo >> 16) & 0xff,
86
+ (lo >> 8) & 0xff,
87
+ lo & 0xff
88
+ ]);
89
+ }
90
+ throw new Error('Value too large for QUIC VarInt');
91
+ }
92
+
93
+
94
+ function readVarInt(array, offset) {
95
+ if (offset >= array.length) return null;
96
+
97
+ var first = array[offset];
98
+ var prefix = first >> 6;
99
+
100
+ if (prefix === 0) {
101
+ return { value: first & 0x3f, byteLength: 1 };
102
+ }
103
+ if (prefix === 1) {
104
+ if (offset + 1 >= array.length) return null;
105
+ return {
106
+ value: ((first & 0x3f) << 8) | array[offset + 1],
107
+ byteLength: 2
108
+ };
109
+ }
110
+ if (prefix === 2) {
111
+ if (offset + 3 >= array.length) return null;
112
+ return {
113
+ value: (
114
+ ((first & 0x3f) << 24) |
115
+ (array[offset + 1] << 16) |
116
+ (array[offset + 2] << 8) |
117
+ array[offset + 3]
118
+ ) >>> 0,
119
+ byteLength: 4
120
+ };
121
+ }
122
+ if (prefix === 3) {
123
+ if (offset + 7 >= array.length) return null;
124
+ var hi = (
125
+ ((first & 0x3f) << 24) |
126
+ (array[offset + 1] << 16) |
127
+ (array[offset + 2] << 8) |
128
+ array[offset + 3]
129
+ ) >>> 0;
130
+ var lo = (
131
+ (array[offset + 4] << 24) |
132
+ (array[offset + 5] << 16) |
133
+ (array[offset + 6] << 8) |
134
+ array[offset + 7]
135
+ ) >>> 0;
136
+ var full = BigInt(hi) * 4294967296n + BigInt(lo);
137
+ // Always return a Number. QUIC fields (lengths, offsets, IDs, limits) never
138
+ // legitimately exceed 2^53, and a malicious 8-byte value that does will be
139
+ // rejected by the parsers' bounds checks anyway. Returning a BigInt here
140
+ // would throw "Cannot mix BigInt and other types" the moment the value is
141
+ // combined with a Number offset during parsing.
142
+ return { value: Number(full), byteLength: 8 };
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+
149
+ // ============================================================
150
+ // Binary helpers
151
+ // ============================================================
152
+
153
+ function concatUint8Arrays(arrays) {
154
+ var totalLength = 0;
155
+ for (var i = 0; i < arrays.length; i++) {
156
+ totalLength += arrays[i].length;
157
+ }
158
+ var result = new Uint8Array(totalLength);
159
+ var offset = 0;
160
+ for (var i = 0; i < arrays.length; i++) {
161
+ result.set(arrays[i], offset);
162
+ offset += arrays[i].length;
163
+ }
164
+ return result;
165
+ }
166
+
167
+
168
+ function uint8Equal(a, b) {
169
+ if (a === b) return true;
170
+ if (!a || !b) return false;
171
+ if (a.byteLength !== b.byteLength) return false;
172
+ for (var i = 0; i < a.byteLength; i++) {
173
+ if (a[i] !== b[i]) return false;
174
+ }
175
+ return true;
176
+ }
177
+
178
+
179
+ // ============================================================
180
+ // WebTransport framing helpers (draft-ietf-webtrans-http3 / RFC 9297)
181
+ //
182
+ // Both client (webtransport.js) and server (h3.js) build the same
183
+ // byte layouts, so they live here to avoid duplicating the prefix
184
+ // construction across the stream and datagram send paths.
185
+ // ============================================================
186
+
187
+ /**
188
+ * Build a WebTransport stream header: VarInt(streamType) + VarInt(sessionId) [+ data].
189
+ * streamType is 0x41 for a bidirectional WT stream, 0x54 for a unidirectional one.
190
+ * The header prefixes the FIRST chunk on a WT stream (or a lone FIN frame); pass
191
+ * `data` for "header + payload", omit it for a header-only (e.g. FIN) frame.
192
+ */
193
+ function wt_stream_header(isBidi, sessionId, data) {
194
+ var typePrefix = writeVarInt(isBidi ? 0x41 : 0x54);
195
+ var sessionPrefix = writeVarInt(sessionId);
196
+ var dataLen = (data && data.byteLength) ? data.byteLength : 0;
197
+ var out = new Uint8Array(typePrefix.length + sessionPrefix.length + dataLen);
198
+ out.set(typePrefix, 0);
199
+ out.set(sessionPrefix, typePrefix.length);
200
+ if (dataLen) out.set(data, typePrefix.length + sessionPrefix.length);
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * Build a WebTransport datagram payload: VarInt(quarter_stream_id) + data.
206
+ * The quarter-stream-id is the session's CONNECT stream id divided by 4
207
+ * (RFC 9297 §2.1). Pass the session stream id; the division is done here.
208
+ */
209
+ function wt_datagram_payload(sessionStreamId, data) {
210
+ var prefix = writeVarInt(Math.floor(sessionStreamId / 4));
211
+ var dataLen = (data && data.byteLength) ? data.byteLength : 0;
212
+ var out = new Uint8Array(prefix.length + dataLen);
213
+ out.set(prefix, 0);
214
+ if (dataLen) out.set(data, prefix.length);
215
+ return out;
216
+ }
217
+
218
+
219
+ // ============================================================
220
+ // ACK range helpers (flat [from,to, from,to, ...] format)
221
+ // ============================================================
222
+
223
+ function ack_frame_to_ranges(ackFrame) {
224
+ var flat = [];
225
+ if (!ackFrame || ackFrame.type !== 'ack') return flat;
226
+
227
+ var largest = Number(ackFrame.largest);
228
+ var firstRange = Number(ackFrame.firstRange);
229
+ if (!isFinite(largest) || !isFinite(firstRange)) return flat;
230
+
231
+ var rangesDesc = [];
232
+
233
+ // First range
234
+ var rangeEnd = largest;
235
+ var rangeStart = rangeEnd - firstRange;
236
+ if (rangeStart <= rangeEnd) {
237
+ rangesDesc.push([rangeStart, rangeEnd]);
238
+ }
239
+
240
+ // Additional ranges
241
+ var more = ackFrame.ranges || [];
242
+ var prevStart = rangeStart;
243
+ for (var i = 0; i < more.length; i++) {
244
+ var gap = Number(more[i].gap);
245
+ var length = Number(more[i].length);
246
+ if (!isFinite(gap) || !isFinite(length)) continue;
247
+
248
+ rangeEnd = prevStart - gap - 2;
249
+ rangeStart = rangeEnd - length;
250
+ if (rangeStart <= rangeEnd) {
251
+ rangesDesc.push([rangeStart, rangeEnd]);
252
+ prevStart = rangeStart;
253
+ }
254
+ }
255
+
256
+ // Sort ascending, merge, and flatten
257
+ rangesDesc.sort(function (a, b) { return a[0] - b[0]; });
258
+
259
+ var merged = [];
260
+ for (var j = 0; j < rangesDesc.length; j++) {
261
+ var s = rangesDesc[j][0], e = rangesDesc[j][1];
262
+ if (merged.length === 0) {
263
+ merged.push([s, e]);
264
+ } else {
265
+ var last = merged[merged.length - 1];
266
+ if (s <= last[1] + 1) {
267
+ if (e > last[1]) last[1] = e;
268
+ } else {
269
+ merged.push([s, e]);
270
+ }
271
+ }
272
+ }
273
+
274
+ for (var k = 0; k < merged.length; k++) {
275
+ flat.push(merged[k][0], merged[k][1]);
276
+ }
277
+ return flat;
278
+ }
279
+
280
+
281
+ function ranges_to_ack_frame(flatRanges, ecnStats, ackDelay) {
282
+ if (!flatRanges || flatRanges.length === 0) return null;
283
+ if (flatRanges.length % 2 !== 0) throw new Error('flatRanges must be [from,to,...] pairs');
284
+
285
+ // flat_ranges uses exclusive end: [from, to) → convert to inclusive [start, end]
286
+ var ranges = [];
287
+ for (var i = 0; i < flatRanges.length; i += 2) {
288
+ if (flatRanges[i + 1] > flatRanges[i]) {
289
+ ranges.push({ start: flatRanges[i], end: flatRanges[i + 1] - 1 });
290
+ }
291
+ }
292
+
293
+ if (ranges.length === 0) return null;
294
+
295
+ ranges.sort(function (a, b) { return b.end - a.end; });
296
+
297
+ // Merge overlapping/adjacent
298
+ var merged = [ranges[0]];
299
+ for (var i = 1; i < ranges.length; i++) {
300
+ var last = merged[merged.length - 1];
301
+ var curr = ranges[i];
302
+ if (curr.end >= last.start - 1) {
303
+ last.start = Math.min(last.start, curr.start);
304
+ } else {
305
+ merged.push(curr);
306
+ }
307
+ }
308
+
309
+ var largest = merged[0].end;
310
+ var firstRange = largest - merged[0].start;
311
+ var ackRanges = [];
312
+
313
+ for (var i = 1; i < merged.length; i++) {
314
+ var gap = merged[i - 1].start - merged[i].end - 2;
315
+ var length = merged[i].end - merged[i].start;
316
+ ackRanges.push({ gap: gap, length: length });
317
+ }
318
+
319
+ return {
320
+ type: 'ack',
321
+ largest: largest,
322
+ delay: ackDelay || 0,
323
+ firstRange: firstRange,
324
+ ranges: ackRanges,
325
+ ecn: ecnStats ? {
326
+ ect0: ecnStats.ect0 || 0,
327
+ ect1: ecnStats.ect1 || 0,
328
+ ce: ecnStats.ce || 0
329
+ } : null
330
+ };
331
+ }
332
+
333
+
334
+ export {
335
+ DEBUG,
336
+ Emitter,
337
+ writeVarInt,
338
+ readVarInt,
339
+ concatUint8Arrays,
340
+ uint8Equal,
341
+ wt_stream_header,
342
+ wt_datagram_payload,
343
+ ack_frame_to_ranges,
344
+ ranges_to_ack_frame
345
+ };