/*
 * ABOUTME: SVCB (RFC 9460) and HTTPS RR parsing, validation, wire
 * ABOUTME: encoding, and presentation. Covers all nine RFC 9460/9461/9540 keys.
 *
 * Part of DNS zone file validator `validns`.
 *
 * Copyright 2025-2026 OARC, Inc.
 * Copyright 2011-2026 Anton Berezin <tobez@tobez.org>
 * Modified BSD license.
 * (See LICENSE file in the distribution.)
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "common.h"
#include "textparse.h"
#include "mempool.h"
#include "carp.h"
#include "rr.h"
#include "base64.h"

/* Forward decls for per-key parsers (filled in by later tasks). */
static int parse_port(struct svcb_param* p, struct binary_data v);
static int parse_dohpath(struct svcb_param* p, struct binary_data v);
static int parse_ech(struct svcb_param* p, struct binary_data v);
static int parse_ipv4hint(struct svcb_param* p, struct binary_data v);
static int parse_ipv6hint(struct svcb_param* p, struct binary_data v);
static int parse_alpn(struct svcb_param* p, struct binary_data v);
static int parse_mandatory(struct svcb_param* p, struct binary_data v);
static int parse_generic(struct svcb_param* p, struct binary_data v);

/* Look up a key mnemonic. Returns the SVCB_KEY_* value, or:
 *  -1  : unknown name (caller should check for keyNNNNN form)
 */
static int lookup_svcb_key(const char* name)
{
    if (strcmp(name, "mandatory") == 0)
        return SVCB_KEY_MANDATORY;
    if (strcmp(name, "alpn") == 0)
        return SVCB_KEY_ALPN;
    if (strcmp(name, "no-default-alpn") == 0)
        return SVCB_KEY_NO_DEFAULT_ALPN;
    if (strcmp(name, "port") == 0)
        return SVCB_KEY_PORT;
    if (strcmp(name, "ipv4hint") == 0)
        return SVCB_KEY_IPV4HINT;
    if (strcmp(name, "ech") == 0)
        return SVCB_KEY_ECH;
    if (strcmp(name, "ipv6hint") == 0)
        return SVCB_KEY_IPV6HINT;
    if (strcmp(name, "dohpath") == 0)
        return SVCB_KEY_DOHPATH;
    if (strcmp(name, "ohttp") == 0)
        return SVCB_KEY_OHTTP;
    if (strncmp(name, "key", 3) == 0) {
        char* endp;
        long  n = strtol(name + 3, &endp, 10);
        if (*endp == '\0' && n >= 0 && n <= 65535)
            return (int)n;
    }
    return -1;
}

/* Extract one SvcParam token "key" or "key=value" from input.
 *
 * Respects RFC 1035 §5.1 char-string syntax (contiguous-no-spaces or
 * "quoted-with-\-escapes"), but PRESERVES backslashes in the value
 * bytes — per-key parsers apply their own escape semantics (alpn needs
 * \, distinguished from ,; mandatory does not; etc.).
 *
 * Returns 1 on success, 0 on end-of-input, -1 on parse error
 * (bitch() already called).
 */
static int extract_svcparam(char** input, char** key, struct binary_data* value)
{
    char* s = *input;
    char* k;
    int   klen = 0;
    char* buf;
    int   blen = 0;
    int   max  = 65535;
    int   i;

    *key          = NULL;
    value->data   = NULL;
    value->length = 0;

    while (*s && isspace((unsigned char)*s))
        s++;
    if (!*s) {
        *input = s;
        return 0;
    }

    /* Key: [a-z0-9-]+, lowercased. Up to '=' or whitespace or end. */
    k = s;
    while (*s && *s != '=' && !isspace((unsigned char)*s)) {
        if (!(isalnum((unsigned char)*s) || *s == '-')) {
            bitch("invalid character in SvcParamKey name");
            return -1;
        }
        klen++;
        s++;
    }
    if (klen == 0) {
        bitch("SvcParamKey expected");
        return -1;
    }
    *key = getmem_temp(klen + 1);
    for (i = 0; i < klen; i++)
        (*key)[i] = (char)tolower((unsigned char)k[i]);
    (*key)[klen] = '\0';

    if (*s != '=') {
        *input = s;
        return 1; /* key only, no value */
    }
    s++; /* consume '=' */

    /* Value: either "quoted" (with \X escapes preserved as 2 bytes)
     *        or contiguous (no whitespace). */
    buf = getmem_temp(max + 1);
    if (*s == '"') {
        s++;
        while (*s && *s != '"') {
            if (*s == '\\') {
                if (blen + 2 > max) {
                    bitch("SvcParamValue too long");
                    return -1;
                }
                buf[blen++] = '\\';
                s++;
                if (!*s) {
                    bitch("unterminated SvcParamValue");
                    return -1;
                }
                buf[blen++] = *s++;
            } else {
                if (blen + 1 > max) {
                    bitch("SvcParamValue too long");
                    return -1;
                }
                buf[blen++] = *s++;
            }
        }
        if (*s != '"') {
            bitch("unterminated SvcParamValue");
            return -1;
        }
        s++;
    } else {
        while (*s && !isspace((unsigned char)*s)) {
            if (blen + 1 > max) {
                bitch("SvcParamValue too long");
                return -1;
            }
            buf[blen++] = *s++;
        }
    }
    value->data = getmem(blen);
    if (blen > 0)
        memcpy(value->data, buf, blen);
    value->length = blen;
    *input        = s;
    return 1;
}

/* port=DECIMAL */
static int parse_port(struct svcb_param* p, struct binary_data v)
{
    char  buf[16];
    long  val;
    char* endp;

    if (v.data == NULL || v.length == 0) {
        bitch("port value expected");
        return -1;
    }
    if (v.length >= (int)sizeof(buf)) {
        bitch("port value too long");
        return -1;
    }
    memcpy(buf, v.data, v.length);
    buf[v.length] = '\0';
    val           = strtol(buf, &endp, 10);
    if (*endp != '\0' || val < 0 || val > 65535) {
        bitch("port value out of range");
        return -1;
    }
    p->v.port = (uint16_t)val;
    return 0;
}

/* dohpath / key65500 / generic: store opaque bytes from the raw value.
 * extract_svcparam preserves backslashes; we apply standard \DDD and
 * \X unescaping here since the value is just bytes with no further
 * structure. */
static struct binary_data unescape_value(struct binary_data v)
{
    struct binary_data r;
    char*              out = getmem(v.length + 1);
    int                o = 0, i = 0;

    while (i < v.length) {
        if (v.data[i] == '\\' && i + 1 < v.length) {
            if (isdigit((unsigned char)v.data[i + 1])) {
                int code = 0, k = 0;
                i++;
                while (k < 3 && i < v.length && isdigit((unsigned char)v.data[i])) {
                    code = code * 10 + (v.data[i] - '0');
                    i++;
                    k++;
                }
                if (code > 255)
                    code = 255;
                out[o++] = (char)code;
            } else {
                out[o++] = v.data[i + 1];
                i += 2;
            }
        } else {
            out[o++] = v.data[i++];
        }
    }
    r.data   = out;
    r.length = o;
    return r;
}

static int parse_dohpath(struct svcb_param* p, struct binary_data v)
{
    p->v.dohpath = unescape_value(v);
    return 0;
}

static int parse_generic(struct svcb_param* p, struct binary_data v)
{
    p->v.generic = unescape_value(v);
    return 0;
}

static int parse_ech(struct svcb_param* p, struct binary_data v)
{
    /* RFC 9460 §7.5: ech value in presentation is base64. Use the existing
     * extract_base64_binary_data helper by pointing it at a NUL-terminated
     * copy of the raw value bytes; it consumes from a char** input. */
    char*              buf;
    char*              cursor;
    struct binary_data dec;

    if (v.data == NULL || v.length == 0) {
        bitch("ech value expected");
        return -1;
    }
    buf = getmem_temp(v.length + 1);
    memcpy(buf, v.data, v.length);
    buf[v.length] = '\0';
    cursor        = buf;
    dec           = extract_base64_binary_data(&cursor, "ech");
    if (dec.length < 0) {
        /* extract_base64_binary_data has already bitched. */
        return -1;
    }
    p->v.ech = dec;
    return 0;
}

/* Split v on unescaped commas. Returns count; fills out[] with binary_data
 * slices into a fresh buffer. Each slice is unescaped on the way out
 * (\X -> X, \DDD -> byte). out[] is caller-allocated and sized for the
 * upper bound (one element per byte of v). */
static int split_unescaped(struct binary_data v, struct binary_data* out)
{
    char* buf = getmem(v.length + 1);
    int   n   = 0;
    int   o   = 0;
    int   i   = 0;

    out[n].data = buf + o;
    while (i < v.length) {
        if (v.data[i] == '\\' && i + 1 < v.length) {
            if (isdigit((unsigned char)v.data[i + 1])) {
                int code = 0, k = 0;
                i++;
                while (k < 3 && i < v.length && isdigit((unsigned char)v.data[i])) {
                    code = code * 10 + (v.data[i] - '0');
                    i++;
                    k++;
                }
                if (code > 255)
                    code = 255;
                buf[o++] = (char)code;
            } else {
                buf[o++] = v.data[i + 1];
                i += 2;
            }
        } else if (v.data[i] == ',') {
            out[n].length = (buf + o) - out[n].data;
            n++;
            i++;
            out[n].data = buf + o;
        } else {
            buf[o++] = v.data[i++];
        }
    }
    out[n].length = (buf + o) - out[n].data;
    n++;
    return n;
}

static int parse_ipv4hint(struct svcb_param* p, struct binary_data v)
{
    struct binary_data* elems;
    char                addrstr[64];
    int                 n, i;

    if (v.length == 0) {
        bitch("ipv4hint value expected");
        return -1;
    }
    elems               = getmem_temp(sizeof(*elems) * (v.length + 1));
    n                   = split_unescaped(v, elems);
    p->v.ipv4hint.count = n;
    p->v.ipv4hint.addrs = getmem(sizeof(struct in_addr) * n);
    for (i = 0; i < n; i++) {
        if (elems[i].length >= (int)sizeof(addrstr) || elems[i].length == 0) {
            bitch("ipv4hint address malformed");
            return -1;
        }
        memcpy(addrstr, elems[i].data, elems[i].length);
        addrstr[elems[i].length] = '\0';
        if (inet_pton(AF_INET, addrstr, &p->v.ipv4hint.addrs[i]) != 1) {
            bitch("ipv4hint address \"%s\" is not valid", addrstr);
            return -1;
        }
    }
    return 0;
}

static int parse_ipv6hint(struct svcb_param* p, struct binary_data v)
{
    struct binary_data* elems;
    char                addrstr[64];
    int                 n, i;

    if (v.length == 0) {
        bitch("ipv6hint value expected");
        return -1;
    }
    elems               = getmem_temp(sizeof(*elems) * (v.length + 1));
    n                   = split_unescaped(v, elems);
    p->v.ipv6hint.count = n;
    p->v.ipv6hint.addrs = getmem(sizeof(struct in6_addr) * n);
    for (i = 0; i < n; i++) {
        if (elems[i].length >= (int)sizeof(addrstr) || elems[i].length == 0) {
            bitch("ipv6hint address malformed");
            return -1;
        }
        memcpy(addrstr, elems[i].data, elems[i].length);
        addrstr[elems[i].length] = '\0';
        if (inet_pton(AF_INET6, addrstr, &p->v.ipv6hint.addrs[i]) != 1) {
            bitch("ipv6hint address \"%s\" is not valid", addrstr);
            return -1;
        }
    }
    return 0;
}

static int parse_alpn(struct svcb_param* p, struct binary_data v)
{
    struct binary_data* elems;
    int                 n, i;

    if (v.length == 0) {
        bitch("alpn value expected (non-empty list)");
        return -1;
    }
    elems           = getmem_temp(sizeof(*elems) * (v.length + 1));
    n               = split_unescaped(v, elems);
    p->v.alpn.count = n;
    p->v.alpn.ids   = getmem(sizeof(struct binary_data) * n);
    for (i = 0; i < n; i++) {
        if (elems[i].length < 1 || elems[i].length > 255) {
            bitch("alpn protocol-id length must be 1..255");
            return -1;
        }
        p->v.alpn.ids[i] = elems[i];
    }
    return 0;
}

static int parse_mandatory(struct svcb_param* p, struct binary_data v)
{
    struct binary_data* elems;
    char                name[32];
    int                 n, i;

    if (v.length == 0) {
        bitch("mandatory value expected (non-empty list)");
        return -1;
    }
    elems                = getmem_temp(sizeof(*elems) * (v.length + 1));
    n                    = split_unescaped(v, elems);
    p->v.mandatory.count = n;
    p->v.mandatory.keys  = getmem(sizeof(uint16_t) * n);
    for (i = 0; i < n; i++) {
        int k;
        if (elems[i].length >= (int)sizeof(name) || elems[i].length == 0) {
            bitch("mandatory key name malformed");
            return -1;
        }
        memcpy(name, elems[i].data, elems[i].length);
        name[elems[i].length] = '\0';
        for (char* c = name; *c; c++)
            *c = (char)tolower((unsigned char)*c);
        k = lookup_svcb_key(name);
        if (k < 0) {
            bitch("mandatory references unknown key \"%s\"", name);
            return -1;
        }
        p->v.mandatory.keys[i] = (uint16_t)k;
    }
    /* Sort ascending and reject duplicates. */
    for (i = 0; i + 1 < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if (p->v.mandatory.keys[j] < p->v.mandatory.keys[i]) {
                uint16_t t             = p->v.mandatory.keys[i];
                p->v.mandatory.keys[i] = p->v.mandatory.keys[j];
                p->v.mandatory.keys[j] = t;
            }
        }
    }
    for (i = 0; i + 1 < n; i++) {
        if (p->v.mandatory.keys[i] == p->v.mandatory.keys[i + 1]) {
            bitch("mandatory has duplicate keys");
            return -1;
        }
    }
    return 0;
}

static struct rr* svcb_parse(char* name, long ttl, int type, char* s)
{
    struct rr_svcb* rr = getmem(sizeof(*rr));
    long long       pr;
    char*           t;

    pr = extract_integer(&s, "priority", NULL);
    if (pr < 0)
        return NULL;
    if (pr > 65535)
        return bitch("priority range is not valid");
    rr->priority = (uint16_t)pr;

    t = extract_name(&s, "target", KEEP_CAPITALIZATION);
    if (!t)
        return NULL;
    rr->target = t;

    rr->n_params = 0;
    rr->params   = NULL;

    /* Parse SvcParams. */
    {
        struct svcb_param  tmp[64];
        int                n = 0;
        char*              key;
        struct binary_data value;
        int                r;

        while ((r = extract_svcparam(&s, &key, &value)) == 1) {
            int k = lookup_svcb_key(key);
            if (k < 0) {
                bitch("unknown SvcParamKey \"%s\"", key);
                return NULL;
            }
            if (n >= 64) {
                bitch("too many SvcParams");
                return NULL;
            }
            memset(&tmp[n], 0, sizeof(tmp[n]));
            tmp[n].key = (uint16_t)k;
            switch (k) {
            case SVCB_KEY_PORT:
                if (parse_port(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_ALPN:
                if (parse_alpn(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_NO_DEFAULT_ALPN:
            case SVCB_KEY_OHTTP:
                if (value.data != NULL && value.length > 0) {
                    bitch("SvcParamKey \"%s\" takes no value", key);
                    return NULL;
                }
                /* No value to store — presence in params[] is the flag. */
                break;
            case SVCB_KEY_DOHPATH:
                if (parse_dohpath(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_ECH:
                if (parse_ech(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_IPV4HINT:
                if (parse_ipv4hint(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_IPV6HINT:
                if (parse_ipv6hint(&tmp[n], value) < 0)
                    return NULL;
                break;
            case SVCB_KEY_MANDATORY:
                if (parse_mandatory(&tmp[n], value) < 0)
                    return NULL;
                break;
            default:
                /* Unknown / generic keyN. */
                if (parse_generic(&tmp[n], value) < 0)
                    return NULL;
                break;
            }
            n++;
        }
        if (r < 0)
            return NULL;

        /* Sort ascending by key (small n; insertion sort). */
        for (int i = 1; i < n; i++) {
            struct svcb_param k = tmp[i];
            int               j = i - 1;
            while (j >= 0 && tmp[j].key > k.key) {
                tmp[j + 1] = tmp[j];
                j--;
            }
            tmp[j + 1] = k;
        }
        /* Reject duplicates. */
        for (int i = 0; i + 1 < n; i++) {
            if (tmp[i].key == tmp[i + 1].key) {
                bitch("duplicate SvcParamKey in SVCB/HTTPS record");
                return NULL;
            }
        }

        rr->n_params = n;
        if (n > 0) {
            rr->params = getmem(sizeof(struct svcb_param) * n);
            memcpy(rr->params, tmp, sizeof(struct svcb_param) * n);
        }
    }

    if (rr->priority == 0) {
        if (rr->n_params > 0)
            return bitch("AliasMode SVCB must have no SvcParams");
        if (strcmp(rr->target, ".") == 0)
            return bitch("AliasMode target must not be the root");
    }

    /* mandatory consistency and no-default-alpn dependency. */
    {
        int                      have_alpn            = 0;
        int                      have_no_default_alpn = 0;
        const struct svcb_param* mand                 = NULL;

        for (int i = 0; i < rr->n_params; i++) {
            if (rr->params[i].key == SVCB_KEY_ALPN)
                have_alpn = 1;
            if (rr->params[i].key == SVCB_KEY_NO_DEFAULT_ALPN)
                have_no_default_alpn = 1;
            if (rr->params[i].key == SVCB_KEY_MANDATORY)
                mand = &rr->params[i];
        }
        if (have_no_default_alpn && !have_alpn)
            return bitch("no-default-alpn requires alpn to be present");

        if (mand) {
            for (int i = 0; i < mand->v.mandatory.count; i++) {
                uint16_t k       = mand->v.mandatory.keys[i];
                int      present = 0;
                if (k == SVCB_KEY_MANDATORY)
                    return bitch("mandatory may not list itself");
                if (k == SVCB_KEY_NO_DEFAULT_ALPN)
                    return bitch("mandatory may not list no-default-alpn");
                for (int j = 0; j < rr->n_params; j++) {
                    if (rr->params[j].key == k) {
                        present = 1;
                        break;
                    }
                }
                if (!present)
                    return bitch("mandatory references key %u which is not present in this RR", (unsigned)k);
            }
        }
    }

    return store_record(type, name, ttl, rr);
}

/* Render a single byte-string value into out as char-string presentation.
 * Quotes the whole value if it contains whitespace, '"', or '\'.
 * Returns the number of chars written, or -1 if the value does not fit. */
static int render_charstring(char* out, int max, const char* data, int len)
{
    int needs_quote = 0;
    int o           = 0;
    int l;

    for (int i = 0; i < len; i++) {
        unsigned char c = (unsigned char)data[i];
        if (isspace(c) || c == '"' || c == '\\' || !isprint(c)) {
            needs_quote = 1;
            break;
        }
    }
    if (needs_quote) {
        if (o + 1 >= max)
            return -1;
        out[o++] = '"';
    }
    for (int i = 0; i < len; i++) {
        unsigned char c = (unsigned char)data[i];
        if (c == '"' || c == '\\') {
            if (o + 2 >= max)
                return -1;
            out[o++] = '\\';
            out[o++] = (char)c;
        } else if (isprint(c)) {
            if (o + 1 >= max)
                return -1;
            out[o++] = (char)c;
        } else {
            if (o + 4 >= max)
                return -1;
            l = snprintf(out + o, max - o, "\\%03u", c);
            if (l < 0 || l >= max - o)
                return -1;
            o += l;
        }
    }
    if (needs_quote) {
        if (o + 1 >= max)
            return -1;
        out[o++] = '"';
    }
    return o;
}

static const char* key_name(uint16_t k)
{
    static char buf[16];
    switch (k) {
    case SVCB_KEY_MANDATORY:
        return "mandatory";
    case SVCB_KEY_ALPN:
        return "alpn";
    case SVCB_KEY_NO_DEFAULT_ALPN:
        return "no-default-alpn";
    case SVCB_KEY_PORT:
        return "port";
    case SVCB_KEY_IPV4HINT:
        return "ipv4hint";
    case SVCB_KEY_ECH:
        return "ech";
    case SVCB_KEY_IPV6HINT:
        return "ipv6hint";
    case SVCB_KEY_DOHPATH:
        return "dohpath";
    case SVCB_KEY_OHTTP:
        return "ohttp";
    default:
        snprintf(buf, sizeof(buf), "key%u", (unsigned)k);
        return buf;
    }
}

static int render_alpn(char* out, int max, const struct svcb_alpn* a)
{
    int o                = 0;
    int need_outer_quote = 0;
    for (int i = 0; i < a->count; i++) {
        for (int j = 0; j < a->ids[i].length; j++) {
            unsigned char c = (unsigned char)a->ids[i].data[j];
            if (isspace(c) || c == '"' || !isprint(c)) {
                need_outer_quote = 1;
                break;
            }
        }
        if (need_outer_quote)
            break;
    }
    if (need_outer_quote) {
        if (o + 1 >= max)
            return -1;
        out[o++] = '"';
    }
    for (int i = 0; i < a->count; i++) {
        if (i > 0) {
            if (o + 1 >= max)
                return -1;
            out[o++] = ',';
        }
        for (int j = 0; j < a->ids[i].length; j++) {
            unsigned char c = (unsigned char)a->ids[i].data[j];
            if (c == '\\' || c == ',' || c == '"') {
                if (o + 2 >= max)
                    return -1;
                out[o++] = '\\';
                out[o++] = (char)c;
            } else if (isprint(c)) {
                if (o + 1 >= max)
                    return -1;
                out[o++] = (char)c;
            } else {
                int l;
                if (o + 4 >= max)
                    return -1;
                l = snprintf(out + o, max - o, "\\%03u", c);
                if (l < 0 || l >= max - o)
                    return -1;
                o += l;
            }
        }
    }
    if (need_outer_quote) {
        if (o + 1 >= max)
            return -1;
        out[o++] = '"';
    }
    return o;
}

/* Render one SvcParam into out.  Returns the number of chars written,
 * or -1 if the parameter does not fit. */
static int render_param(char* out, int max, const struct svcb_param* p)
{
    int  o = 0;
    int  w;
    char addrstr[64];

    w = snprintf(out + o, max - o, "%s", key_name(p->key));
    if (w < 0 || w >= max - o)
        return -1;
    o += w;
    switch (p->key) {
    case SVCB_KEY_NO_DEFAULT_ALPN:
    case SVCB_KEY_OHTTP:
        return o;

    case SVCB_KEY_PORT:
        w = snprintf(out + o, max - o, "=%u", (unsigned)p->v.port);
        if (w < 0 || w >= max - o)
            return -1;
        o += w;
        return o;

    case SVCB_KEY_IPV4HINT:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        for (int i = 0; i < p->v.ipv4hint.count; i++) {
            if (i > 0) {
                if (o + 1 >= max)
                    return -1;
                out[o++] = ',';
            }
            inet_ntop(AF_INET, &p->v.ipv4hint.addrs[i], addrstr, sizeof(addrstr));
            w = snprintf(out + o, max - o, "%s", addrstr);
            if (w < 0 || w >= max - o)
                return -1;
            o += w;
        }
        return o;

    case SVCB_KEY_IPV6HINT:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        for (int i = 0; i < p->v.ipv6hint.count; i++) {
            if (i > 0) {
                if (o + 1 >= max)
                    return -1;
                out[o++] = ',';
            }
            inet_ntop(AF_INET6, &p->v.ipv6hint.addrs[i], addrstr, sizeof(addrstr));
            w = snprintf(out + o, max - o, "%s", addrstr);
            if (w < 0 || w >= max - o)
                return -1;
            o += w;
        }
        return o;

    case SVCB_KEY_ALPN:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        w = render_alpn(out + o, max - o, &p->v.alpn);
        if (w < 0)
            return -1;
        o += w;
        return o;

    case SVCB_KEY_MANDATORY:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        for (int i = 0; i < p->v.mandatory.count; i++) {
            if (i > 0) {
                if (o + 1 >= max)
                    return -1;
                out[o++] = ',';
            }
            w = snprintf(out + o, max - o, "%s",
                key_name(p->v.mandatory.keys[i]));
            if (w < 0 || w >= max - o)
                return -1;
            o += w;
        }
        return o;

    case SVCB_KEY_ECH:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        w = encode_base64((unsigned char*)p->v.ech.data,
            p->v.ech.length, out + o, max - o);
        if (w != (p->v.ech.length + 2) / 3 * 4)
            return -1; /* does not fit in full */
        o += w;
        return o;

    case SVCB_KEY_DOHPATH:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        w = render_charstring(out + o, max - o,
            p->v.dohpath.data, p->v.dohpath.length);
        if (w < 0)
            return -1;
        o += w;
        return o;

    default:
        if (o + 1 >= max)
            return -1;
        out[o++] = '=';
        w = render_charstring(out + o, max - o,
            p->v.generic.data, p->v.generic.length);
        if (w < 0)
            return -1;
        o += w;
        return o;
    }
}

static char* svcb_human(struct rr* rrv)
{
    RRCAST(svcb);
    char* buf = getmem_temp(65536);
    char* p   = buf;
    char* end = buf + 65536;
    int   n;

    n = snprintf(p, end - p, "%hu %s", rr->priority, rr->target);
    if (n < 0 || n >= end - p)
        return NULL;
    p += n;
    for (int i = 0; i < rr->n_params; i++) {
        int w;
        if (p + 1 >= end)
            return NULL;
        *p++ = ' ';
        w = render_param(p, (int)(end - p), &rr->params[i]);
        if (w < 0)
            return NULL;
        p += w;
    }
    *p = '\0';
    return quickstrdup_temp(buf);
}

static struct binary_data encode_port(uint16_t p)
{
    return compose_binary_data("2", 1, (unsigned int)p);
}

static struct binary_data encode_alpn(const struct svcb_alpn* a)
{
    struct binary_data r = { .length = 0, .data = NULL };
    for (int i = 0; i < a->count; i++)
        r = compose_binary_data("db", 1, r, a->ids[i]);
    return r;
}

static struct binary_data encode_addrs(const void* addrs, int count, int sz)
{
    struct binary_data r;
    r.length = count * sz;
    r.data   = getmem_temp(r.length);
    memcpy(r.data, addrs, r.length);
    return r;
}

static struct binary_data encode_mandatory(const struct svcb_keylist* kl)
{
    struct binary_data r = { .length = 0, .data = NULL };
    for (int i = 0; i < kl->count; i++)
        r = compose_binary_data("d2", 1, r, (unsigned int)kl->keys[i]);
    return r;
}

static struct binary_data encode_value(const struct svcb_param* p)
{
    struct binary_data empty = { .length = 0, .data = NULL };

    switch (p->key) {
    case SVCB_KEY_MANDATORY:
        return encode_mandatory(&p->v.mandatory);
    case SVCB_KEY_ALPN:
        return encode_alpn(&p->v.alpn);
    case SVCB_KEY_NO_DEFAULT_ALPN:
        return empty;
    case SVCB_KEY_PORT:
        return encode_port(p->v.port);
    case SVCB_KEY_IPV4HINT:
        return encode_addrs(p->v.ipv4hint.addrs, p->v.ipv4hint.count, 4);
    case SVCB_KEY_ECH:
        return p->v.ech;
    case SVCB_KEY_IPV6HINT:
        return encode_addrs(p->v.ipv6hint.addrs, p->v.ipv6hint.count, 16);
    case SVCB_KEY_DOHPATH:
        return p->v.dohpath;
    case SVCB_KEY_OHTTP:
        return empty;
    default:
        return p->v.generic;
    }
}

static struct binary_data wrap_param(uint16_t key, struct binary_data value)
{
    if (value.length < 0 || value.length > 65535)
        return bad_binary_data();
    return compose_binary_data("22d", 1,
        (unsigned int)key,
        (unsigned int)value.length,
        value);
}

static struct binary_data svcb_wirerdata(struct rr* rrv)
{
    RRCAST(svcb);
    struct binary_data params_blob = { .length = 0, .data = NULL };

    for (int i = 0; i < rr->n_params; i++) {
        struct binary_data wrapped = wrap_param(rr->params[i].key, encode_value(&rr->params[i]));
        if (wrapped.length < 0)
            return bad_binary_data();
        params_blob = compose_binary_data("dd", 1, params_blob, wrapped);
    }

    return compose_binary_data("2dd", 1,
        (unsigned int)rr->priority,
        name2wire_name(rr->target),
        params_blob);
}

struct rr_methods svcb_methods = { svcb_parse, svcb_human, svcb_wirerdata, NULL, NULL };
