lsquic_qdec_hdl.c revision f4841319
1/* Copyright (c) 2017 - 2020 LiteSpeed Technologies Inc.  See LICENSE. */
2/*
3 * lsquic_qdec_hdl.c -- QPACK decoder streams handler
4 */
5
6#include <assert.h>
7#include <errno.h>
8#include <inttypes.h>
9#include <stdlib.h>
10#include <string.h>
11#include <sys/queue.h>
12
13#include "lsquic.h"
14#include "lsquic_types.h"
15#include "lsquic_int_types.h"
16#include "lsquic_sfcw.h"
17#include "lsquic_varint.h"
18#include "lsquic_hq.h"
19#include "lsquic_hash.h"
20#include "lsquic_stream.h"
21#include "lsquic_frab_list.h"
22#include "lsqpack.h"
23#include "lsquic_http1x_if.h"
24#include "lsquic_qdec_hdl.h"
25#include "lsquic_mm.h"
26#include "lsquic_engine_public.h"
27#include "lsquic_headers.h"
28#include "lsquic_conn.h"
29
30#define LSQUIC_LOGGER_MODULE LSQLM_QDEC_HDL
31#define LSQUIC_LOG_CONN_ID lsquic_conn_log_cid(qdh->qdh_conn)
32#include "lsquic_logger.h"
33
34static void
35qdh_hblock_unblocked (void *);
36
37
38static int
39qdh_write_decoder (struct qpack_dec_hdl *qdh, const unsigned char *buf,
40                                                                size_t sz)
41{
42    ssize_t nw;
43
44    if (!(qdh->qdh_dec_sm_out && lsquic_frab_list_empty(&qdh->qdh_fral)))
45    {
46  write_to_frab:
47        if (0 == lsquic_frab_list_write(&qdh->qdh_fral,
48                                                (unsigned char *) buf, sz))
49        {
50            LSQ_DEBUG("wrote %zu bytes to frab list", sz);
51            lsquic_stream_wantwrite(qdh->qdh_dec_sm_out, 1);
52            return 0;
53        }
54        else
55        {
56            LSQ_INFO("error writing to frab list");
57            return -1;
58        }
59    }
60
61    nw = lsquic_stream_write(qdh->qdh_dec_sm_out, buf, sz);
62    if (nw < 0)
63    {
64        LSQ_INFO("error writing to outgoing QPACK decoder stream: %s",
65                                                        strerror(errno));
66        return -1;
67    }
68    LSQ_DEBUG("wrote %zd bytes to outgoing QPACK decoder stream", nw);
69
70    if ((size_t) nw == sz)
71        return 0;
72
73    buf = buf + nw;
74    sz -= (size_t) nw;
75    goto write_to_frab;
76}
77
78
79static int
80qdh_write_type (struct qpack_dec_hdl *qdh)
81{
82    int s;
83
84#ifndef NDEBUG
85    const char *env = getenv("LSQUIC_RND_VARINT_LEN");
86    if (env && atoi(env))
87    {
88        s = rand() & 3;
89        LSQ_DEBUG("writing %d-byte stream type", 1 << s);
90    }
91    else
92#endif
93        s = 0;
94
95    switch (s)
96    {
97    case 0:
98        return qdh_write_decoder(qdh,
99                                (unsigned char []) { HQUST_QPACK_DEC }, 1);
100    case 1:
101        return qdh_write_decoder(qdh,
102                            (unsigned char []) { 0x40, HQUST_QPACK_DEC }, 2);
103    case 2:
104        return qdh_write_decoder(qdh,
105                (unsigned char []) { 0x80, 0x00, 0x00, HQUST_QPACK_DEC }, 4);
106    default:
107        return qdh_write_decoder(qdh,
108                (unsigned char []) { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
109                                                        HQUST_QPACK_DEC }, 8);
110    }
111}
112
113
114static void
115qdh_begin_out (struct qpack_dec_hdl *qdh)
116{
117    if (0 != qdh_write_type(qdh))
118    {
119        LSQ_WARN("%s: could not write to decoder", __func__);
120        qdh->qdh_conn->cn_if->ci_internal_error(qdh->qdh_conn,
121                                        "cannot write to decoder stream");
122    }
123}
124
125
126int
127lsquic_qdh_init (struct qpack_dec_hdl *qdh, struct lsquic_conn *conn,
128                    int is_server, const struct lsquic_engine_public *enpub,
129                    unsigned dyn_table_size, unsigned max_risked_streams)
130{
131    qdh->qdh_conn = conn;
132    lsquic_frab_list_init(&qdh->qdh_fral, 0x400, NULL, NULL, NULL);
133    lsqpack_dec_init(&qdh->qdh_decoder, (void *) conn, dyn_table_size,
134                        max_risked_streams, qdh_hblock_unblocked);
135    qdh->qdh_flags |= QDH_INITIALIZED;
136    qdh->qdh_enpub = enpub;
137    if (qdh->qdh_enpub->enp_hsi_if == lsquic_http1x_if)
138    {
139        qdh->qdh_h1x_ctor_ctx = (struct http1x_ctor_ctx) {
140            .conn           = conn,
141            .max_headers_sz = MAX_HTTP1X_HEADERS_SIZE,
142            .is_server      = is_server,
143        };
144        qdh->qdh_hsi_ctx = &qdh->qdh_h1x_ctor_ctx;
145    }
146    else
147        qdh->qdh_hsi_ctx = qdh->qdh_enpub->enp_hsi_ctx;
148    if (qdh->qdh_dec_sm_out)
149        qdh_begin_out(qdh);
150    if (qdh->qdh_enc_sm_in)
151        lsquic_stream_wantread(qdh->qdh_enc_sm_in, 1);
152    LSQ_DEBUG("initialized");
153    return 0;
154}
155
156
157void
158lsquic_qdh_cleanup (struct qpack_dec_hdl *qdh)
159{
160    if (qdh->qdh_flags & QDH_INITIALIZED)
161    {
162        LSQ_DEBUG("cleanup");
163        lsqpack_dec_cleanup(&qdh->qdh_decoder);
164        lsquic_frab_list_cleanup(&qdh->qdh_fral);
165        qdh->qdh_flags &= ~QDH_INITIALIZED;
166    }
167}
168
169static lsquic_stream_ctx_t *
170qdh_out_on_new (void *stream_if_ctx, struct lsquic_stream *stream)
171{
172    struct qpack_dec_hdl *const qdh = stream_if_ctx;
173    qdh->qdh_dec_sm_out = stream;
174    if (qdh->qdh_flags & QDH_INITIALIZED)
175        qdh_begin_out(qdh);
176    LSQ_DEBUG("initialized outgoing decoder stream");
177    return (void *) qdh;
178}
179
180
181static void
182qdh_out_on_write (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
183{
184    struct qpack_dec_hdl *const qdh = (void *) ctx;
185    struct lsquic_reader reader;
186    ssize_t nw;
187    unsigned char buf[LSQPACK_LONGEST_ICI];
188
189    if (lsqpack_dec_ici_pending(&qdh->qdh_decoder))
190    {
191        nw = lsqpack_dec_write_ici(&qdh->qdh_decoder, buf, sizeof(buf));
192        if (nw > 0)
193        {
194            if (0 == qdh_write_decoder(qdh, buf, nw))
195                LSQ_DEBUG("wrote %zd-byte TSS instruction", nw);
196            else
197                goto err;
198        }
199        else if (nw < 0)
200        {
201            LSQ_WARN("could not generate TSS instruction");
202            goto err;
203        }
204    }
205
206    if (lsquic_frab_list_empty(&qdh->qdh_fral))
207    {
208        LSQ_DEBUG("%s: nothing to write", __func__);
209        lsquic_stream_wantwrite(stream, 0);
210        return;
211    }
212
213    reader = (struct lsquic_reader) {
214        .lsqr_read  = lsquic_frab_list_read,
215        .lsqr_size  = lsquic_frab_list_size,
216        .lsqr_ctx   = &qdh->qdh_fral,
217    };
218
219    nw = lsquic_stream_writef(stream, &reader);
220    if (nw >= 0)
221    {
222        LSQ_DEBUG("wrote %zd bytes to stream", nw);
223        (void) lsquic_stream_flush(stream);
224        if (lsquic_frab_list_empty(&qdh->qdh_fral))
225        {
226            lsquic_stream_wantwrite(stream, 0);
227            if (qdh->qdh_on_dec_sent_func)
228            {
229                LSQ_DEBUG("buffered data written: call callback");
230                qdh->qdh_on_dec_sent_func(qdh->qdh_on_dec_sent_ctx);
231                qdh->qdh_on_dec_sent_func = NULL;
232                qdh->qdh_on_dec_sent_ctx = NULL;
233            }
234        }
235    }
236    else
237    {
238        LSQ_WARN("cannot write to stream: %s", strerror(errno));
239  err:
240        lsquic_stream_wantwrite(stream, 0);
241        qdh->qdh_conn->cn_if->ci_internal_error(qdh->qdh_conn,
242                                        "cannot write to stream");
243    }
244}
245
246
247static void
248qdh_out_on_close (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
249{
250    struct qpack_dec_hdl *const qdh = (void *) ctx;
251    qdh->qdh_dec_sm_out = NULL;
252    LSQ_DEBUG("closed outgoing decoder stream");
253}
254
255
256static void
257qdh_out_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
258{
259    assert(0);
260}
261
262
263static const struct lsquic_stream_if qdh_dec_sm_out_if =
264{
265    .on_new_stream  = qdh_out_on_new,
266    .on_read        = qdh_out_on_read,
267    .on_write       = qdh_out_on_write,
268    .on_close       = qdh_out_on_close,
269};
270const struct lsquic_stream_if *const lsquic_qdh_dec_sm_out_if =
271                                                    &qdh_dec_sm_out_if;
272
273
274static lsquic_stream_ctx_t *
275qdh_in_on_new (void *stream_if_ctx, struct lsquic_stream *stream)
276{
277    struct qpack_dec_hdl *const qdh = stream_if_ctx;
278    qdh->qdh_enc_sm_in = stream;
279    if (qdh->qdh_flags & QDH_INITIALIZED)
280        lsquic_stream_wantread(qdh->qdh_enc_sm_in, 1);
281    LSQ_DEBUG("initialized incoming encoder stream");
282    return (void *) qdh;
283}
284
285
286static size_t
287qdh_read_encoder_stream (void *ctx, const unsigned char *buf, size_t sz,
288                                                                    int fin)
289{
290    struct qpack_dec_hdl *const qdh = (void *) ctx;
291    const struct lsqpack_dec_err *qerr;
292    int s;
293
294    if (fin)
295    {
296        LSQ_INFO("encoder stream is closed");
297        qdh->qdh_conn->cn_if->ci_abort_error(qdh->qdh_conn, 1,
298            HEC_CLOSED_CRITICAL_STREAM, "Peer closed QPACK encoder stream");
299        goto end;
300    }
301
302    s = lsqpack_dec_enc_in(&qdh->qdh_decoder, buf, sz);
303    if (s != 0)
304    {
305        LSQ_INFO("error reading encoder stream");
306        qerr = lsqpack_dec_get_err_info(&qdh->qdh_decoder);
307        qdh->qdh_conn->cn_if->ci_abort_error(qdh->qdh_conn, 1,
308            HEC_QPACK_DECODER_STREAM_ERROR, "Error interpreting QPACK encoder "
309            "stream; offset %"PRIu64", line %d", qerr->off, qerr->line);
310        goto end;
311    }
312    if (qdh->qdh_dec_sm_out
313                    && lsqpack_dec_ici_pending(&qdh->qdh_decoder))
314        lsquic_stream_wantwrite(qdh->qdh_dec_sm_out, 1);
315
316    LSQ_DEBUG("successfully fed %zu bytes to QPACK decoder", sz);
317
318  end:
319    return sz;
320}
321
322
323static void
324qdh_in_on_read (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
325{
326    struct qpack_dec_hdl *const qdh = (void *) ctx;
327    ssize_t nread;
328
329    nread = lsquic_stream_readf(stream, qdh_read_encoder_stream, qdh);
330    if (nread <= 0)
331    {
332        if (nread < 0)
333        {
334            LSQ_WARN("cannot read from encoder stream: %s", strerror(errno));
335            qdh->qdh_conn->cn_if->ci_internal_error(qdh->qdh_conn,
336                                        "cannot read from encoder stream");
337        }
338        else
339        {
340            LSQ_INFO("encoder stream closed by peer: abort connection");
341            qdh->qdh_conn->cn_if->ci_abort_error(qdh->qdh_conn, 1,
342                HEC_CLOSED_CRITICAL_STREAM, "encoder stream closed");
343        }
344        lsquic_stream_wantread(stream, 0);
345    }
346}
347
348
349static void
350qdh_in_on_close (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
351{
352    struct qpack_dec_hdl *const qdh = (void *) ctx;
353    LSQ_DEBUG("closed incoming encoder stream");
354    qdh->qdh_enc_sm_in = NULL;
355}
356
357
358static void
359qdh_in_on_write (struct lsquic_stream *stream, lsquic_stream_ctx_t *ctx)
360{
361    assert(0);
362}
363
364
365static const struct lsquic_stream_if qdh_enc_sm_in_if =
366{
367    .on_new_stream  = qdh_in_on_new,
368    .on_read        = qdh_in_on_read,
369    .on_write       = qdh_in_on_write,
370    .on_close       = qdh_in_on_close,
371};
372const struct lsquic_stream_if *const lsquic_qdh_enc_sm_in_if =
373                                                    &qdh_enc_sm_in_if;
374
375
376static void
377qdh_hblock_unblocked (void *stream_p)
378{
379    struct lsquic_stream *const stream = stream_p;
380    struct qpack_dec_hdl *const qdh = lsquic_stream_get_qdh(stream);
381
382    LSQ_DEBUG("header block for stream %"PRIu64" unblocked", stream->id);
383    lsquic_stream_qdec_unblocked(stream);
384}
385
386
387struct cont_len
388{
389    unsigned long long      value;
390    int                     has;    /* 1: set, 0: not set, -1: invalid */
391};
392
393
394static void
395process_content_length (const struct qpack_dec_hdl *qdh /* for logging */,
396            struct cont_len *cl, const char *val /* not NUL-terminated */,
397                                                                unsigned len)
398{
399    char *endcl, cont_len_buf[30];
400
401    if (0 == cl->has)
402    {
403        if (len >= sizeof(cont_len_buf))
404        {
405            LSQ_DEBUG("content-length has invalid value `%.*s'",
406                                                            (int) len, val);
407            cl->has = -1;
408            return;
409        }
410        memcpy(cont_len_buf, val, len);
411        cont_len_buf[len] = '\0';
412        cl->value = strtoull(cont_len_buf, &endcl, 10);
413        if (*endcl == '\0' && !(ULLONG_MAX == cl->value && ERANGE == errno))
414        {
415            cl->has = 1;
416            LSQ_DEBUG("content length is %llu", cl->value);
417        }
418        else
419        {
420            cl->has = -1;
421            LSQ_DEBUG("content-length has invalid value `%.*s'",
422                (int) len, val);
423        }
424    }
425    else if (cl->has > 0)
426    {
427        LSQ_DEBUG("header set has two content-length: ambiguous, "
428            "turn off checking");
429        cl->has = -1;
430    }
431}
432
433
434static int
435is_content_length (const struct lsqpack_header *header)
436{
437    return ((header->qh_flags & QH_ID_SET) && header->qh_static_id == 4)
438        || (header->qh_name_len == 14 && header->qh_name[0] == 'c'
439                    && 0 == memcmp(header->qh_name + 1, "ontent-length", 13))
440        ;
441}
442
443
444static int
445qdh_supply_hset_to_stream (struct qpack_dec_hdl *qdh,
446            struct lsquic_stream *stream, struct lsqpack_header_list *qlist)
447{
448    const struct lsquic_hset_if *const hset_if = qdh->qdh_enpub->enp_hsi_if;
449    const unsigned hpack_static_table_size = 61;
450    struct uncompressed_headers *uh = NULL;
451    const struct lsqpack_header *header;
452    enum lsquic_header_status st;
453    int push_promise;
454    unsigned i;
455    void *hset;
456    struct cont_len cl;
457
458    push_promise = lsquic_stream_header_is_pp(stream);
459    hset = hset_if->hsi_create_header_set(qdh->qdh_hsi_ctx, push_promise);
460    if (!hset)
461    {
462        LSQ_INFO("call to hsi_create_header_set failed");
463        return -1;
464    }
465
466    LSQ_DEBUG("got header set for stream %"PRIu64, stream->id);
467
468    cl.has = 0;
469    for (i = 0; i < qlist->qhl_count; ++i)
470    {
471        header = qlist->qhl_headers[i];
472        LSQ_DEBUG("%.*s: %.*s", header->qh_name_len, header->qh_name,
473                                        header->qh_value_len, header->qh_value);
474        st = hset_if->hsi_process_header(hset,
475                    header->qh_flags & QH_ID_SET ?
476                        hpack_static_table_size + 1 + header->qh_static_id : 0,
477                    header->qh_name, header->qh_name_len,
478                    header->qh_value, header->qh_value_len);
479        if (st != LSQUIC_HDR_OK)
480        {
481            LSQ_INFO("header process returned non-OK code %u", (unsigned) st);
482            goto err;
483        }
484        if (is_content_length(header))
485            process_content_length(qdh, &cl, header->qh_value,
486                                                        header->qh_value_len);
487    }
488
489    lsqpack_dec_destroy_header_list(qlist);
490    qlist = NULL;
491    st = hset_if->hsi_process_header(hset, 0, 0, 0, 0, 0);
492    if (st != LSQUIC_HDR_OK)
493        goto err;
494
495    uh = calloc(1, sizeof(*uh));
496    if (!uh)
497        goto err;
498    uh->uh_stream_id = stream->id;
499    uh->uh_oth_stream_id = 0;
500    uh->uh_weight = 0;
501    uh->uh_exclusive = -1;
502    if (hset_if == lsquic_http1x_if)
503        uh->uh_flags    |= UH_H1H;
504    uh->uh_hset = hset;
505    if (0 != lsquic_stream_uh_in(stream, uh))
506        goto err;
507    LSQ_DEBUG("converted qlist to hset and gave it to stream %"PRIu64,
508                                                                stream->id);
509    if (cl.has > 0)
510        (void) lsquic_stream_verify_len(stream, cl.value);
511    return 0;
512
513  err:
514    if (qlist)
515        lsqpack_dec_destroy_header_list(qlist);
516    hset_if->hsi_discard_header_set(hset);
517    free(uh);
518    return -1;
519}
520
521
522/* Releases qlist */
523static int
524qdh_process_qlist (struct qpack_dec_hdl *qdh,
525            struct lsquic_stream *stream, struct lsqpack_header_list *qlist)
526{
527    if (!lsquic_stream_header_is_trailer(stream))
528        return qdh_supply_hset_to_stream(qdh, stream, qlist);
529    else
530    {
531        LSQ_DEBUG("discard trailer header set");
532        lsqpack_dec_destroy_header_list(qlist);
533        return 0;
534    }
535}
536
537
538static enum lsqpack_read_header_status
539qdh_header_read_results (struct qpack_dec_hdl *qdh,
540        struct lsquic_stream *stream, enum lsqpack_read_header_status rhs,
541        struct lsqpack_header_list *qlist, const unsigned char *dec_buf,
542        size_t dec_buf_sz)
543{
544    const struct lsqpack_dec_err *qerr;
545
546    if (rhs == LQRHS_DONE)
547    {
548        if (qlist)
549        {
550            if (0 != qdh_process_qlist(qdh, stream, qlist))
551                return LQRHS_ERROR;
552            if (qdh->qdh_dec_sm_out)
553            {
554                if (dec_buf_sz
555                    && 0 != qdh_write_decoder(qdh, dec_buf, dec_buf_sz))
556                {
557                    return LQRHS_ERROR;
558                }
559                if (dec_buf_sz || lsqpack_dec_ici_pending(&qdh->qdh_decoder))
560                    lsquic_stream_wantwrite(qdh->qdh_dec_sm_out, 1);
561            }
562        }
563        else
564        {
565            LSQ_WARN("read header status is DONE but header list is not set");
566            assert(0);
567            return LQRHS_ERROR;
568        }
569    }
570    else if (rhs == LQRHS_ERROR)
571    {
572        qerr = lsqpack_dec_get_err_info(&qdh->qdh_decoder);
573        qdh->qdh_conn->cn_if->ci_abort_error(qdh->qdh_conn, 1,
574            HEC_QPACK_DECOMPRESSION_FAILED, "QPACK decompression error; "
575            "stream %"PRIu64", offset %"PRIu64", line %d", qerr->stream_id,
576            qerr->off, qerr->line);
577    }
578
579    return rhs;
580}
581
582
583enum lsqpack_read_header_status
584lsquic_qdh_header_in_begin (struct qpack_dec_hdl *qdh,
585                        struct lsquic_stream *stream, uint64_t header_size,
586                        const unsigned char **buf, size_t bufsz)
587{
588    enum lsqpack_read_header_status rhs;
589    struct lsqpack_header_list *qlist;
590    size_t dec_buf_sz;
591    unsigned char dec_buf[LSQPACK_LONGEST_HEADER_ACK];
592
593    if (qdh->qdh_flags & QDH_INITIALIZED)
594    {
595        dec_buf_sz = sizeof(dec_buf);
596        rhs = lsqpack_dec_header_in(&qdh->qdh_decoder, stream, stream->id,
597                        header_size, buf, bufsz, &qlist, dec_buf, &dec_buf_sz);
598        return qdh_header_read_results(qdh, stream, rhs, qlist, dec_buf,
599                                                                dec_buf_sz);
600    }
601    else
602    {
603        LSQ_WARN("not initialized: cannot process header block");
604        return LQRHS_ERROR;
605    }
606
607}
608
609
610enum lsqpack_read_header_status
611lsquic_qdh_header_in_continue (struct qpack_dec_hdl *qdh,
612        struct lsquic_stream *stream, const unsigned char **buf, size_t bufsz)
613{
614    enum lsqpack_read_header_status rhs;
615    struct lsqpack_header_list *qlist;
616    size_t dec_buf_sz;
617    unsigned char dec_buf[LSQPACK_LONGEST_HEADER_ACK];
618
619    if (qdh->qdh_flags & QDH_INITIALIZED)
620    {
621        dec_buf_sz = sizeof(dec_buf);
622        rhs = lsqpack_dec_header_read(&qdh->qdh_decoder, stream,
623                                    buf, bufsz, &qlist, dec_buf, &dec_buf_sz);
624        return qdh_header_read_results(qdh, stream, rhs, qlist, dec_buf,
625                                                                dec_buf_sz);
626    }
627    else
628    {
629        LSQ_WARN("not initialized: cannot process header block");
630        return LQRHS_ERROR;
631    }
632}
633
634
635void
636lsquic_qdh_unref_stream (struct qpack_dec_hdl *qdh,
637                                                struct lsquic_stream *stream)
638{
639    if (0 == lsqpack_dec_unref_stream(&qdh->qdh_decoder, stream))
640        LSQ_DEBUG("unreffed stream %"PRIu64, stream->id);
641    else
642        LSQ_WARN("cannot unref stream %"PRIu64, stream->id);
643}
644
645
646void
647lsquic_qdh_cancel_stream (struct qpack_dec_hdl *qdh,
648                                                struct lsquic_stream *stream)
649{
650    ssize_t nw;
651    unsigned char buf[LSQPACK_LONGEST_CANCEL];
652
653    nw = lsqpack_dec_cancel_stream(&qdh->qdh_decoder, stream, buf, sizeof(buf));
654    if (nw > 0)
655    {
656        if (0 == qdh_write_decoder(qdh, buf, nw))
657            LSQ_DEBUG("cancelled stream %"PRIu64" and wrote %zd-byte Cancel "
658                "Stream instruction to the decoder stream", stream->id, nw);
659    }
660    else if (nw == 0)
661        LSQ_WARN("cannot cancel stream %"PRIu64" -- not found", stream->id);
662    else
663    {
664        LSQ_WARN("cannot cancel stream %"PRIu64" -- not enough buffer space "
665            "to encode Cancel Stream instructin", stream->id);
666        lsquic_qdh_unref_stream(qdh, stream);
667    }
668}
669
670
671int
672lsquic_qdh_arm_if_unsent (struct qpack_dec_hdl *qdh, void (*func)(void *),
673                                                                    void *ctx)
674{
675    size_t bytes;
676
677    /* Use size of a single frab list buffer as an arbitrary threshold */
678    bytes = lsquic_frab_list_size(&qdh->qdh_fral);
679    if (bytes <= qdh->qdh_fral.fl_buf_size)
680        return 0;
681    else
682    {
683        LSQ_DEBUG("have %zu bytes of unsent QPACK decoder stream data: set "
684            "up callback", bytes);
685        qdh->qdh_on_dec_sent_func = func;
686        qdh->qdh_on_dec_sent_ctx  = ctx;
687        return 1;
688    }
689}
690