/*****************************************************************************
 * h2frame.c: HTTP/2 frame formatting
 *****************************************************************************
 * Copyright (C) 2015 Rémi Denis-Courmont
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <vlc_common.h>

#include "conn.h"
#include "hpack.h"
#include "h2frame.h"

static struct vlc_h2_frame *
vlc_h2_frame_alloc(uint_fast8_t type, uint_fast8_t flags,
                   uint_fast32_t stream_id, size_t length)
{
    assert((stream_id >> 31) == 0);

    if (unlikely(length > VLC_H2_MAX_MAX_FRAME))
    {
        errno = EINVAL;
        return NULL;
    }

    struct vlc_h2_frame *f = malloc(sizeof (*f) + 9 + length);
    if (unlikely(f == NULL))
        return NULL;

    f->next = NULL;
    f->data[0] = length >> 16;
    f->data[1] = length >> 8;
    f->data[2] = length;
    f->data[3] = type;
    f->data[4] = flags;
    SetDWBE(f->data + 5, stream_id);
    return f;
}

#define vlc_h2_frame_payload(f) ((f)->data + 9)

static uint_fast32_t vlc_h2_frame_length(const struct vlc_h2_frame *f)
{
    const uint8_t *buf = f->data;
    return (buf[0] << 16) | (buf[1] << 8) | buf[2];
}

size_t vlc_h2_frame_size(const struct vlc_h2_frame *f)
{
    return 9 + vlc_h2_frame_length(f);
}

static uint_fast8_t vlc_h2_frame_type(const struct vlc_h2_frame *f)
{
    return f->data[3];
}

static uint_fast8_t vlc_h2_frame_flags(const struct vlc_h2_frame *f)
{
    return f->data[4];
}

static uint_fast32_t vlc_h2_frame_id(const struct vlc_h2_frame *f)
{
    return GetDWBE(f->data + 5) & 0x7FFFFFFF;
}

enum {
    VLC_H2_FRAME_DATA,
    VLC_H2_FRAME_HEADERS,
    VLC_H2_FRAME_PRIORITY,
    VLC_H2_FRAME_RST_STREAM,
    VLC_H2_FRAME_SETTINGS,
    VLC_H2_FRAME_PUSH_PROMISE,
    VLC_H2_FRAME_PING,
    VLC_H2_FRAME_GOAWAY,
    VLC_H2_FRAME_WINDOW_UPDATE,
    VLC_H2_FRAME_CONTINUATION,
};

static const char *vlc_h2_type_name(uint_fast8_t type)
{
    static const char names[][14] = {
        [VLC_H2_FRAME_DATA]          = "DATA",
        [VLC_H2_FRAME_HEADERS]       = "HEADERS",
        [VLC_H2_FRAME_PRIORITY]      = "PRIORITY",
        [VLC_H2_FRAME_RST_STREAM]    = "RST_STREAM",
        [VLC_H2_FRAME_SETTINGS]      = "SETTINGS",
        [VLC_H2_FRAME_PUSH_PROMISE]  = "PUSH_PROMISE",
        [VLC_H2_FRAME_PING]          = "PING",
        [VLC_H2_FRAME_GOAWAY]        = "GOAWAY",
        [VLC_H2_FRAME_WINDOW_UPDATE] = "WINDOW_UPDATE",
        [VLC_H2_FRAME_CONTINUATION]  = "CONTINUATION",
    };

    if (type >= ARRAY_SIZE(names) || names[type][0] == '\0')
        return "<unknown>";
    return names[type];
}

enum {
    VLC_H2_DATA_END_STREAM = 0x01,
    VLC_H2_DATA_PADDED     = 0x08,
};

enum {
    VLC_H2_HEADERS_END_STREAM  = 0x01,
    VLC_H2_HEADERS_END_HEADERS = 0x04,
    VLC_H2_HEADERS_PADDED      = 0x08,
    VLC_H2_HEADERS_PRIORITY    = 0x20,
};

enum {
    VLC_H2_SETTINGS_ACK = 0x01,
};

enum {
    VLC_H2_PUSH_PROMISE_END_HEADERS = 0x04,
    VLC_H2_PUSH_PROMISE_PADDED      = 0x08,
};

enum {
    VLC_H2_PING_ACK = 0x01,
};

enum {
    VLC_H2_CONTINUATION_END_HEADERS = 0x04,
};

struct vlc_h2_frame *
vlc_h2_frame_headers(uint_fast32_t stream_id, uint_fast32_t mtu, bool eos,
                     unsigned count, const char *const headers[][2])
{
    struct vlc_h2_frame *f;
    uint8_t flags = eos ? VLC_H2_HEADERS_END_STREAM : 0;

    size_t len = hpack_encode(NULL, 0, headers, count);

    if (likely(len <= mtu))
    {   /* Most common case: single frame - with zero copy */
        flags |= VLC_H2_HEADERS_END_HEADERS;

        f = vlc_h2_frame_alloc(VLC_H2_FRAME_HEADERS, flags, stream_id, len);
        if (unlikely(f == NULL))
            return NULL;

        hpack_encode(vlc_h2_frame_payload(f), len, headers, count);
        return f;
    }

    /* Edge case: HEADERS frame then CONTINUATION frame(s) */
    uint8_t *payload = malloc(len);
    if (unlikely(payload == NULL))
        return NULL;

    hpack_encode(payload, len, headers, count);

    struct vlc_h2_frame **pp = &f, *n;
    const uint8_t *offset = payload;
    uint_fast8_t type = VLC_H2_FRAME_HEADERS;

    f = NULL;

    while (len > mtu)
    {
        n = vlc_h2_frame_alloc(type, flags, stream_id, mtu);
        if (unlikely(n == NULL))
            goto error;

        memcpy(vlc_h2_frame_payload(n), offset, mtu);
        *pp = n;
        pp = &n->next;

        type = VLC_H2_FRAME_CONTINUATION;
        flags = 0;
        offset += mtu;
        len -= mtu;
    }

    flags |= VLC_H2_CONTINUATION_END_HEADERS;

    n = vlc_h2_frame_alloc(type, flags, stream_id, len);
    if (unlikely(n == NULL))
        goto error;

    memcpy(vlc_h2_frame_payload(n), offset, len);
    *pp = n;

    free(payload);
    return f;

error:
    while (f != NULL)
    {
        n = f->next;
        free(f);
        f = n;
    }
    free(payload);
    return NULL;
}

struct vlc_h2_frame *
vlc_h2_frame_data(uint_fast32_t stream_id, const void *buf, size_t len,
                  bool eos)
{
    struct vlc_h2_frame *f;
    uint8_t flags = eos ? VLC_H2_DATA_END_STREAM : 0;

    f = vlc_h2_frame_alloc(VLC_H2_FRAME_DATA, flags, stream_id, len);
    if (len > 0 && likely(f != NULL))
        memcpy(vlc_h2_frame_payload(f), buf, len);
    return f;
}

struct vlc_h2_frame *
vlc_h2_frame_rst_stream(uint_fast32_t stream_id, uint_fast32_t error_code)
{
    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_RST_STREAM, 0,
                                                stream_id, 4);
    if (likely(f != NULL))
        SetDWBE(vlc_h2_frame_payload(f), error_code);
    return f;
}

struct vlc_h2_frame *vlc_h2_frame_settings(void)
{
    unsigned n = (VLC_H2_MAX_HEADER_TABLE != VLC_H2_DEFAULT_MAX_HEADER_TABLE)
               + 1 /* ENABLE_PUSH */
#if defined(VLC_H2_MAX_STREAMS)
               + 1
#endif
               + (VLC_H2_INIT_WINDOW != VLC_H2_DEFAULT_INIT_WINDOW)
               + (VLC_H2_MAX_FRAME != VLC_H2_DEFAULT_MAX_FRAME)
#if defined(VLC_H2_MAX_HEADER_LIST)
               + 1
#endif
    ;
    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_SETTINGS, 0, 0,
                                                n * 6);
    if (unlikely(f == NULL))
        return NULL;

    uint8_t *p = vlc_h2_frame_payload(f);

#if (VLC_H2_MAX_HEADER_TABLE != VLC_H2_DEFAULT_MAX_HEADER_TABLE)
    SetWBE(p, VLC_H2_SETTING_HEADER_TABLE_SIZE);
    SetDWBE(p + 2, VLC_H2_MAX_HEADER_TABLE);
    p += 6;
#endif

    SetWBE(p, VLC_H2_SETTING_ENABLE_PUSH);
    SetDWBE(p + 2, 0);
    p += 6;

#if defined(VLC_H2_MAX_STREAMS)
    SetWBE(p, VLC_H2_SETTING_MAX_CONCURRENT_STREAMS);
    SetDWBE(p + 2, VLC_H2_MAX_STREAMS);
    p += 6;
#endif

#if (VLC_H2_INIT_WINDOW != VLC_H2_DEFAULT_INIT_WINDOW)
    static_assert (VLC_H2_INIT_WINDOW < 2147483648,
                   "Illegal initial window value");
    SetWBE(p, VLC_H2_SETTING_INITIAL_WINDOW_SIZE);
    SetDWBE(p + 2, VLC_H2_INIT_WINDOW);
    p += 6;
#endif

#if (VLC_H2_MAX_FRAME != VLC_H2_DEFAULT_MAX_FRAME)
    static_assert (VLC_H2_MAX_FRAME >= 16384 && VLC_H2_MAX_FRAME < 16777216,
                   "Illegal maximum frame size");
    SetWBE(p, VLC_H2_SETTING_MAX_FRAME_SIZE);
    SetDWBE(p + 2, VLC_H2_MAX_FRAME);
    p += 6;
#endif

#if defined(VLC_H2_MAX_HEADER_LIST)
    SetWBE(p, VLC_H2_SETTING_MAX_HEADER_LIST_SIZE);
    SetDWBE(p + 2, VLC_H2_MAX_HEADER_LIST);
    p += 6;
#endif

    return f;
}

struct vlc_h2_frame *vlc_h2_frame_settings_ack(void)
{
    return vlc_h2_frame_alloc(VLC_H2_FRAME_SETTINGS, VLC_H2_SETTINGS_ACK, 0,
                              0);
}

const char *vlc_h2_setting_name(uint_fast16_t id)
{
    static const char names[][20] = {
        [0]                                     = "Unknown setting",
        [VLC_H2_SETTING_HEADER_TABLE_SIZE]      = "Header table size",
        [VLC_H2_SETTING_ENABLE_PUSH]            = "Enable push",
        [VLC_H2_SETTING_MAX_CONCURRENT_STREAMS] = "Concurrent streams",
        [VLC_H2_SETTING_INITIAL_WINDOW_SIZE]    = "Initial window size",
        [VLC_H2_SETTING_MAX_FRAME_SIZE]         = "Frame size",
        [VLC_H2_SETTING_MAX_HEADER_LIST_SIZE]   = "Header list size",
    };

    if (id >= ARRAY_SIZE(names) || names[id][0] == '\0')
        id = 0;
    return names[id];
}

struct vlc_h2_frame *vlc_h2_frame_ping(uint64_t opaque)
{
    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_PING, 0, 0, 8);
    if (likely(f != NULL))
        memcpy(vlc_h2_frame_payload(f), &opaque, 8);
    return f;
}

struct vlc_h2_frame *vlc_h2_frame_pong(uint64_t opaque)
{
    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_PING,
                                                VLC_H2_PING_ACK, 0, 8);
    if (likely(f != NULL))
        memcpy(vlc_h2_frame_payload(f), &opaque, 8);
    return f;
}

struct vlc_h2_frame *
vlc_h2_frame_goaway(uint_fast32_t last_stream_id, uint_fast32_t error_code)
{
    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_GOAWAY, 0, 0, 8);
    if (likely(f != NULL))
    {
        uint8_t *p = vlc_h2_frame_payload(f);

        SetDWBE(p, last_stream_id);
        SetDWBE(p + 4, error_code);
    }
    return f;
}

struct vlc_h2_frame *
vlc_h2_frame_window_update(uint_fast32_t stream_id, uint_fast32_t credit)
{
    assert((stream_id >> 31) == 0);

    struct vlc_h2_frame *f = vlc_h2_frame_alloc(VLC_H2_FRAME_WINDOW_UPDATE,
                                                0, stream_id, 4);
    if (likely(f != NULL))
    {
        uint8_t *p = vlc_h2_frame_payload(f);

        SetDWBE(p, credit);
    }
    return f;
}

const char *vlc_h2_strerror(uint_fast32_t code)
{
    static const char names[][20] = {
        [VLC_H2_NO_ERROR]            = "No error",
        [VLC_H2_PROTOCOL_ERROR]      = "Protocol error",
        [VLC_H2_INTERNAL_ERROR]      = "Internal error",
        [VLC_H2_FLOW_CONTROL_ERROR]  = "Flow control error",
        [VLC_H2_SETTINGS_TIMEOUT]    = "Settings time-out",
        [VLC_H2_STREAM_CLOSED]       = "Stream closed",
        [VLC_H2_FRAME_SIZE_ERROR]    = "Frame size error",
        [VLC_H2_REFUSED_STREAM]      = "Refused stream",
        [VLC_H2_CANCEL]              = "Cancellation",
        [VLC_H2_COMPRESSION_ERROR]   = "Compression error",
        [VLC_H2_CONNECT_ERROR]       = "CONNECT error",
        [VLC_H2_ENHANCE_YOUR_CALM]   = "Excessive load",
        [VLC_H2_INADEQUATE_SECURITY] = "Inadequate security",
        [VLC_H2_HTTP_1_1_REQUIRED]   = "Required HTTP/1.1",
    };

    if (code >= ARRAY_SIZE(names) || names[code][0] == '\0')
        return "Unknown error";
    return names[code];
}

void vlc_h2_frame_dump(void *opaque, const struct vlc_h2_frame *f,
                       const char *msg)
{
    size_t len = vlc_h2_frame_length(f);
    uint_fast8_t type = vlc_h2_frame_type(f);
    uint_fast8_t flags = vlc_h2_frame_flags(f);
    uint_fast32_t sid = vlc_h2_frame_id(f);

    if (sid != 0)
        vlc_http_dbg(opaque, "%s %s (0x%02"PRIxFAST8") frame of %zu bytes, "
                     "flags 0x%02"PRIxFAST8", stream %"PRIuFAST32, msg,
                     vlc_h2_type_name(type), type, len,  flags, sid);
    else
        vlc_http_dbg(opaque, "%s %s (0x%02"PRIxFAST8") frame of %zu bytes, "
                     "flags 0x%02"PRIxFAST8", global", msg,
                     vlc_h2_type_name(type), type, len,  flags);
}

const uint8_t *(vlc_h2_frame_data_get)(const struct vlc_h2_frame *f,
                                       size_t *restrict lenp)
{
    assert(vlc_h2_frame_type(f) == VLC_H2_FRAME_DATA);

    size_t len = vlc_h2_frame_length(f);
    uint_fast8_t flags = vlc_h2_frame_flags(f);
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    /* At this point, the frame has already been validated by the parser. */
    if (flags & VLC_H2_DATA_PADDED)
    {
        assert(len >= 1u && len >= 1u + ptr[0]);
        len -= 1u + *(ptr++);
    }

    *lenp = len;
    return ptr;
}

typedef int (*vlc_h2_parser)(struct vlc_h2_parser *, struct vlc_h2_frame *,
                             size_t, uint_fast32_t);

/** HTTP/2 incoming frames parser */
struct vlc_h2_parser
{
    void *opaque;
    const struct vlc_h2_parser_cbs *cbs;

    vlc_h2_parser parser; /*< Parser state / callback for next frame */

    struct
    {
        uint32_t sid; /*< Ongoing stream identifier */
        bool eos; /*< End of stream after headers block */
        size_t len; /*< Compressed headers buffer length */
        uint8_t *buf; /*< Compressed headers buffer base address */
        struct hpack_decoder *decoder; /*< HPACK decompressor state */
    } headers; /*< Compressed headers reception state */

    uint32_t rcwd_size; /*< Receive congestion window (bytes) */
};

static int vlc_h2_parse_generic(struct vlc_h2_parser *, struct vlc_h2_frame *,
                                size_t, uint_fast32_t);
static int vlc_h2_parse_headers_block(struct vlc_h2_parser *,
                                      struct vlc_h2_frame *, size_t,
                                      uint_fast32_t);

static int vlc_h2_parse_error(struct vlc_h2_parser *p, uint_fast32_t code)
{
    p->cbs->error(p->opaque, code);
    return -1;
}

static int vlc_h2_stream_error(struct vlc_h2_parser *p, uint_fast32_t id,
                               uint_fast32_t code)
{
    return p->cbs->stream_error(p->opaque, id, code);
}

static void *vlc_h2_stream_lookup(struct vlc_h2_parser *p, uint_fast32_t id)
{
    return p->cbs->stream_lookup(p->opaque, id);
}

static void vlc_h2_parse_headers_start(struct vlc_h2_parser *p,
                                       uint_fast32_t sid, bool eos)
{
    assert(sid != 0);
    assert(p->headers.sid == 0);

    p->parser = vlc_h2_parse_headers_block;
    p->headers.sid = sid;
    p->headers.eos = eos;
    p->headers.len = 0;
}

static int vlc_h2_parse_headers_append(struct vlc_h2_parser *p,
                                       const uint8_t *data, size_t len)
{
    assert(p->headers.sid != 0);

    if (p->headers.len + len > 65536)
        return vlc_h2_parse_error(p, VLC_H2_INTERNAL_ERROR);

    uint8_t *buf = realloc(p->headers.buf, p->headers.len + len);
    if (unlikely(buf == NULL))
        return vlc_h2_parse_error(p, VLC_H2_INTERNAL_ERROR);

    p->headers.buf = buf;
    memcpy(p->headers.buf + p->headers.len, data, len);
    p->headers.len += len;
    return 0;
}

static int vlc_h2_parse_headers_end(struct vlc_h2_parser *p)
{
    char *headers[VLC_H2_MAX_HEADERS][2];

    /* TODO: limit total decompressed size of the headers list */
    int n = hpack_decode(p->headers.decoder, p->headers.buf, p->headers.len,
                         headers, VLC_H2_MAX_HEADERS);
    if (n > VLC_H2_MAX_HEADERS)
    {
        for (unsigned i = 0; i < VLC_H2_MAX_HEADERS; i++)
        {
            free(headers[i][0]);
            free(headers[i][1]);
        }
        n = -1;
    }
    if (n < 0)
        return vlc_h2_parse_error(p, VLC_H2_COMPRESSION_ERROR);

    void *s = vlc_h2_stream_lookup(p, p->headers.sid);
    int val = 0;

    if (s != NULL)
    {
        const char *ch[VLC_H2_MAX_HEADERS][2];

        for (int i = 0; i < n; i++)
            ch[i][0] = headers[i][0], ch[i][1] = headers[i][1];

        p->cbs->stream_headers(s, n, ch);

        if (p->headers.eos)
            p->cbs->stream_end(s);
    }
    else
        /* NOTE: The specification implies that the error should be sent for
         * the first header frame. But we actually want to receive the whole
         * fragmented headers block, to preserve the HPACK decoder state.
         * So we send the error at the last header frame instead. */
        val = vlc_h2_stream_error(p, p->headers.sid, VLC_H2_REFUSED_STREAM);

    for (int i = 0; i < n; i++)
    {
        free(headers[i][0]);
        free(headers[i][1]);
    }

    p->parser = vlc_h2_parse_generic;
    p->headers.sid = 0;
    return val;
}

/** Parses an HTTP/2 DATA frame */
static int vlc_h2_parse_frame_data(struct vlc_h2_parser *p,
                                   struct vlc_h2_frame *f, size_t len,
                                   uint_fast32_t id)
{
    uint_fast8_t flags = vlc_h2_frame_flags(f);
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    if (id == 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_stream_error(p, id, VLC_H2_FRAME_SIZE_ERROR);
    }

    if (flags & VLC_H2_DATA_PADDED)
    {
        if (len < 1 || len < (1u + ptr[0]))
        {
            free(f);
            return vlc_h2_stream_error(p, id, VLC_H2_FRAME_SIZE_ERROR);
        }
        len -= 1 + ptr[0];
    }

    if (len > p->rcwd_size)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FLOW_CONTROL_ERROR);
    }

    p->rcwd_size -= len;
    p->cbs->window_status(p->opaque, &p->rcwd_size);

    void *s = vlc_h2_stream_lookup(p, id);
    if (s == NULL)
    {
        free(f);
        return vlc_h2_stream_error(p, id, VLC_H2_STREAM_CLOSED);
    }

    int ret = p->cbs->stream_data(s, f);
    /* Frame gets consumed here ^^ */

    if (flags & VLC_H2_DATA_END_STREAM)
        p->cbs->stream_end(s);
    return ret;
}

/** Parses an HTTP/2 HEADERS frame */
static int vlc_h2_parse_frame_headers(struct vlc_h2_parser *p,
                                      struct vlc_h2_frame *f, size_t len,
                                      uint_fast32_t id)
{
    uint_fast8_t flags = vlc_h2_frame_flags(f);
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    if (id == 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    if (flags & VLC_H2_HEADERS_PADDED)
    {
        if (len < 1 || len < (1u + ptr[0]))
        {
            free(f);
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        }
        len -= 1 + ptr[0];
        ptr++;
    }

    if (flags & VLC_H2_HEADERS_PRIORITY)
    {   /* Ignore priorities for now as we do not upload anything. */
        if (len < 5)
        {
            free(f);
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        }
        ptr += 5;
        len -= 5;
    }

    vlc_h2_parse_headers_start(p, id, flags & VLC_H2_HEADERS_END_STREAM);

    int ret = vlc_h2_parse_headers_append(p, ptr, len);

    if (ret == 0 && (flags & VLC_H2_HEADERS_END_HEADERS))
        ret = vlc_h2_parse_headers_end(p);

    free(f);
    return ret;
}

/** Parses an HTTP/2 PRIORITY frame */
static int vlc_h2_parse_frame_priority(struct vlc_h2_parser *p,
                                       struct vlc_h2_frame *f, size_t len,
                                       uint_fast32_t id)
{
    free(f);

    if (id == 0)
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);

    if (len != 5)
        return vlc_h2_stream_error(p, id, VLC_H2_FRAME_SIZE_ERROR);

    /* Ignore priorities for now as we do not upload much. */
    return 0;
}

/** Parses an HTTP/2 RST_STREAM frame */
static int vlc_h2_parse_frame_rst_stream(struct vlc_h2_parser *p,
                                         struct vlc_h2_frame *f, size_t len,
                                         uint_fast32_t id)
{
    if (id == 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len != 4)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    void *s = vlc_h2_stream_lookup(p, id);
    uint_fast32_t code = GetDWBE(vlc_h2_frame_payload(f));

    free(f);

    if (s == NULL)
        return 0;
    return p->cbs->stream_reset(s, code);
}

/** Parses an HTTP/2 SETTINGS frame */
static int vlc_h2_parse_frame_settings(struct vlc_h2_parser *p,
                                       struct vlc_h2_frame *f, size_t len,
                                       uint_fast32_t id)
{
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    if (id != 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len % 6 || len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    if (vlc_h2_frame_flags(f) & VLC_H2_SETTINGS_ACK)
    {
        free(f);
        if (len != 0)
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        /* Ignore ACKs for now as we never change settings. */
        return 0;
    }

    for (const uint8_t *end = ptr + len; ptr < end; ptr += 6)
        p->cbs->setting(p->opaque, GetWBE(ptr), GetDWBE(ptr + 2));

    free(f);
    return p->cbs->settings_done(p->opaque);
}

/** Parses an HTTP/2 PUSH_PROMISE frame */
static int vlc_h2_parse_frame_push_promise(struct vlc_h2_parser *p,
                                           struct vlc_h2_frame *f, size_t len,
                                           uint_fast32_t id)
{
    uint8_t flags = vlc_h2_frame_flags(f);
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    if (id == 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    if (flags & VLC_H2_PUSH_PROMISE_PADDED)
    {
        if (len < 1 || len < (1u + ptr[0]))
        {
            free(f);
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        }
        len -= 1 + ptr[0];
        ptr++;
    }

    /* Not permitted by our settings. */
    free(f);
    return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
}

/** Parses an HTTP/2 PING frame */
static int vlc_h2_parse_frame_ping(struct vlc_h2_parser *p,
                                   struct vlc_h2_frame *f, size_t len,
                                   uint_fast32_t id)
{
    uint64_t opaque;

    if (id != 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len != 8)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    if (vlc_h2_frame_flags(f) & VLC_H2_PING_ACK)
    {
        free(f);
        return 0;
    }

    memcpy(&opaque, vlc_h2_frame_payload(f), 8);
    free(f);

    return p->cbs->ping(p->opaque, opaque);
}

/** Parses an HTTP/2 GOAWAY frame */
static int vlc_h2_parse_frame_goaway(struct vlc_h2_parser *p,
                                     struct vlc_h2_frame *f, size_t len,
                                     uint_fast32_t id)
{
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    if (id != 0)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len < 8 || len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    uint_fast32_t last_id = GetDWBE(ptr) & 0x7FFFFFFF;
    uint_fast32_t code = GetDWBE(ptr + 4);

    free(f);
    return p->cbs->reset(p->opaque, last_id, code);
}

/** Parses an HTTP/2 WINDOW_UPDATE frame */
static int vlc_h2_parse_frame_window_update(struct vlc_h2_parser *p,
                                            struct vlc_h2_frame *f, size_t len,
                                            uint_fast32_t id)
{
    if (len != 4)
    {
        free(f);

        if (id == 0)
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        return vlc_h2_stream_error(p, id, VLC_H2_FRAME_SIZE_ERROR);
    }

    uint_fast32_t credit = GetDWBE(vlc_h2_frame_payload(f)) & 0x7fffffffu;

    free(f);

    if (credit == 0)
    {
        if (id == 0)
            return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
        return vlc_h2_stream_error(p, id, VLC_H2_PROTOCOL_ERROR);
    }

    if (id == 0)
        p->cbs->window_update(p->opaque, credit);
    else
    {
        void *s = vlc_h2_stream_lookup(p, id);

        if (s != NULL)
            p->cbs->stream_window_update(s, credit);
    }
    return 0;
}

/** Parses an HTTP/2 CONTINUATION frame */
static int vlc_h2_parse_frame_continuation(struct vlc_h2_parser *p,
                                           struct vlc_h2_frame *f, size_t len,
                                           uint_fast32_t id)
{
    const uint8_t *ptr = vlc_h2_frame_payload(f);

    /* Stream ID must match with the previous frame. */
    if (id == 0 || id != p->headers.sid)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    if (len > VLC_H2_MAX_FRAME)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
    }

    int ret = vlc_h2_parse_headers_append(p, ptr, len);

    if (ret == 0 && (vlc_h2_frame_flags(f) & VLC_H2_CONTINUATION_END_HEADERS))
        ret = vlc_h2_parse_headers_end(p);

    free(f);
    return 0;
}

/** Parses an HTTP/2 frame of unknown type */
static int vlc_h2_parse_frame_unknown(struct vlc_h2_parser *p,
                                      struct vlc_h2_frame *f, size_t len,
                                      uint_fast32_t id)
{
    free(f);

    if (len > VLC_H2_MAX_FRAME)
    {
        if (id == 0)
            return vlc_h2_parse_error(p, VLC_H2_FRAME_SIZE_ERROR);
        return vlc_h2_stream_error(p, id, VLC_H2_FRAME_SIZE_ERROR);
    }

    /* Ignore frames of unknown type as specified. */
    return 0;
}

static const vlc_h2_parser vlc_h2_parsers[] = {
    [VLC_H2_FRAME_DATA]          = vlc_h2_parse_frame_data,
    [VLC_H2_FRAME_HEADERS]       = vlc_h2_parse_frame_headers,
    [VLC_H2_FRAME_PRIORITY]      = vlc_h2_parse_frame_priority,
    [VLC_H2_FRAME_RST_STREAM]    = vlc_h2_parse_frame_rst_stream,
    [VLC_H2_FRAME_SETTINGS]      = vlc_h2_parse_frame_settings,
    [VLC_H2_FRAME_PUSH_PROMISE]  = vlc_h2_parse_frame_push_promise,
    [VLC_H2_FRAME_PING]          = vlc_h2_parse_frame_ping,
    [VLC_H2_FRAME_GOAWAY]        = vlc_h2_parse_frame_goaway,
    [VLC_H2_FRAME_WINDOW_UPDATE] = vlc_h2_parse_frame_window_update,
    [VLC_H2_FRAME_CONTINUATION]  = vlc_h2_parse_frame_continuation,
};

/** Parses the HTTP/2 connection preface. */
static int vlc_h2_parse_preface(struct vlc_h2_parser *p,
                                struct vlc_h2_frame *f, size_t len,
                                uint_fast32_t id)
{
    /* The length must be within the specification default limits. */
    if (len > VLC_H2_DEFAULT_MAX_FRAME
    /* The type must SETTINGS. */
     || vlc_h2_frame_type(f) != VLC_H2_FRAME_SETTINGS
    /* The SETTINGS ACK flag must be clear. */
     || (vlc_h2_frame_flags(f) & VLC_H2_SETTINGS_ACK))
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    p->parser = vlc_h2_parse_generic;

    return vlc_h2_parse_frame_settings(p, f, len, id);
}

/** Parses any HTTP/2 frame. */
static int vlc_h2_parse_generic(struct vlc_h2_parser *p,
                                struct vlc_h2_frame *f, size_t len,
                                uint_fast32_t id)
{
    uint_fast8_t type = vlc_h2_frame_type(f);
    vlc_h2_parser func = vlc_h2_parse_frame_unknown;

    assert(p->headers.sid == 0);

    if (type < ARRAY_SIZE(vlc_h2_parsers) && vlc_h2_parsers[type] != NULL)
        func = vlc_h2_parsers[type];

    return func(p, f, len, id);
}

static int vlc_h2_parse_headers_block(struct vlc_h2_parser *p,
                                      struct vlc_h2_frame *f, size_t len,
                                      uint_fast32_t id)
{
    assert(p->headers.sid != 0);

    /* After a HEADER, PUSH_PROMISE of CONTINUATION frame without the
     * END_HEADERS flag, must come a CONTINUATION frame. */
    if (vlc_h2_frame_type(f) != VLC_H2_FRAME_CONTINUATION)
    {
        free(f);
        return vlc_h2_parse_error(p, VLC_H2_PROTOCOL_ERROR);
    }

    return vlc_h2_parse_frame_continuation(p, f, len, id);
}

static int vlc_h2_parse_failed(struct vlc_h2_parser *p, struct vlc_h2_frame *f,
                               size_t len, uint_fast32_t id)
{
    free(f);
    (void) p; (void) len; (void) id;
    return -1;
}

int vlc_h2_parse(struct vlc_h2_parser *p, struct vlc_h2_frame *f)
{
    int ret = 0;

    while (f != NULL)
    {
        struct vlc_h2_frame *next = f->next;
        size_t len = vlc_h2_frame_length(f);
        uint_fast32_t id = vlc_h2_frame_id(f);

        f->next = NULL;
        ret = p->parser(p, f, len, id);
        if (ret)
            p->parser = vlc_h2_parse_failed;
        f = next;
    }

    return ret;
}

struct vlc_h2_parser *vlc_h2_parse_init(void *ctx,
                                        const struct vlc_h2_parser_cbs *cbs)
{
    struct vlc_h2_parser *p = malloc(sizeof (*p));
    if (unlikely(p == NULL))
        return NULL;

    p->opaque = ctx;
    p->cbs = cbs;
    p->parser = vlc_h2_parse_preface;
    p->headers.sid = 0;
    p->headers.buf = NULL;
    p->headers.len = 0;
    p->headers.decoder = hpack_decode_init(VLC_H2_MAX_HEADER_TABLE);
    if (unlikely(p->headers.decoder == NULL))
    {
        free(p);
        return NULL;
    }
    p->rcwd_size = 65535; /* initial per-connection value */
    return p;
}

void vlc_h2_parse_destroy(struct vlc_h2_parser *p)
{
    hpack_decode_destroy(p->headers.decoder);
    free(p->headers.buf);
    free(p);
}
