/*
 * Part of DNS zone file validator `validns`.
 *
 * Copyright 2025-2026 OARC, Inc.
 * Copyright 2011-2025 Anton Berezin <tobez@tobez.org>
 * Modified BSD license.
 * (See LICENSE file in the distribution.)
 *
 */
#include <sys/types.h>
#include <stdio.h>
#include <ctype.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

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

/*
 * Per-tag value validator.
 *   tag     — tag name (lowercase, NUL-terminated, max 15 chars per RFC 8659 §4.1 SHOULD).
 *   validate — function called with the parsed value; returns 0 on success, or -1
 *              after calling bitch() with an appropriate message. May be NULL to
 *              mean "no validation beyond the length>0 check already enforced".
 */
struct caa_tag_entry {
    const char* tag;
    int (*validate)(struct binary_data value);
};

static int caa_issue_validate_with_tag(struct binary_data value, const char* tag_name);

static int caa_issue_validate(struct binary_data value)
{
    return caa_issue_validate_with_tag(value, "issue");
}

static int caa_issuewild_validate(struct binary_data value)
{
    return caa_issue_validate_with_tag(value, "issuewild");
}

static int is_ldh(int c)
{
    return isalnum(c) || c == '-';
}

static int is_param_value_char(int c)
{
    /* RFC 8659 §4.2 value = *(%x21-3A / %x3C-7E)  — printable ASCII minus ';'. */
    return c >= 0x21 && c <= 0x7E && c != ';';
}

static int caa_issue_validate_with_tag(struct binary_data value, const char* tag_name)
{
    char* buf = getmem_temp(value.length + 1);
    memcpy(buf, value.data, value.length);
    buf[value.length] = '\0';

    char* p = buf;
    /* Skip leading whitespace. */
    while (*p == ' ' || *p == '\t')
        p++;

    /* Domain name part. May be empty (means "no CA"). */
    if (*p && *p != ';') {
        /* Parse one label, then optional '.'-separated labels. */
        if (!isalnum((unsigned char)*p)) {
            bitch("CAA %s value is not a valid domain or parameter list", tag_name);
            return -1;
        }
        while (*p && *p != ';' && *p != ' ' && *p != '\t') {
            if (*p == '.') {
                p++;
                if (!isalnum((unsigned char)*p)) {
                    bitch("CAA %s value is not a valid domain or parameter list", tag_name);
                    return -1;
                }
                continue;
            }
            if (!is_ldh((unsigned char)*p)) {
                bitch("CAA %s value is not a valid domain or parameter list", tag_name);
                return -1;
            }
            p++;
        }
        /* Skip whitespace before optional ';'. */
        while (*p == ' ' || *p == '\t')
            p++;
    }

    if (*p == '\0')
        return 0; /* domain-only, no params */

    if (*p != ';') {
        bitch("CAA %s value is not a valid domain or parameter list", tag_name);
        return -1;
    }
    p++; /* consume ';' */

    /* Zero-or-more parameters: skip WS, parse tag, '=', value, repeat. */
    for (;;) {
        while (*p == ' ' || *p == '\t')
            p++;
        if (*p == '\0')
            return 0; /* trailing ';' or all-whitespace is OK */

        /* tag */
        if (!isalnum((unsigned char)*p)) {
            bitch("CAA %s parameter is missing a tag", tag_name);
            return -1;
        }
        while (is_ldh((unsigned char)*p))
            p++;
        while (*p == ' ' || *p == '\t')
            p++;
        if (*p != '=') {
            bitch("CAA %s parameter is missing a value", tag_name);
            return -1;
        }
        p++; /* consume '=' */
        while (*p == ' ' || *p == '\t')
            p++;

        /* value: one-or-more printable-non-semicolon chars, ended by WS or NUL */
        char* val_start = p;
        while (*p && *p != ' ' && *p != '\t') {
            if (!is_param_value_char((unsigned char)*p)) {
                bitch("CAA %s parameter contains a forbidden character", tag_name);
                return -1;
            }
            p++;
        }
        if (p == val_start) {
            bitch("CAA %s parameter is missing a value", tag_name);
            return -1;
        }
    }
}

static int caa_iodef_validate(struct binary_data value)
{
    /* Pragmatic URI check: alpha-led scheme, ":", at least one char of rest. */
    if (value.length < 3) {
        bitch("CAA iodef value is not a URI");
        return -1;
    }
    const unsigned char* p   = (const unsigned char*)value.data;
    const unsigned char* end = p + value.length;
    if (!isalpha(*p)) {
        bitch("CAA iodef value is not a URI");
        return -1;
    }
    p++;
    while (p < end && (isalnum(*p) || *p == '+' || *p == '-' || *p == '.'))
        p++;
    if (p >= end || *p != ':') {
        bitch("CAA iodef value is not a URI");
        return -1;
    }
    p++;
    if (p >= end) {
        bitch("CAA iodef value is not a URI");
        return -1;
    }
    return 0;
}

static int caa_contactemail_validate(struct binary_data value)
{
    if (value.length == 0) {
        bitch("CAA contactemail value is not a valid email address");
        return -1;
    }
    const char* p   = value.data;
    const char* end = p + value.length;
    const char* at  = NULL;
    for (const char* q = p; q < end; q++) {
        if (*q == '@') {
            if (at) {
                bitch("CAA contactemail value is not a valid email address");
                return -1;
            }
            at = q;
        }
    }
    if (!at || at == p || at == end - 1) {
        bitch("CAA contactemail value is not a valid email address");
        return -1;
    }
    /* Domain part: LDH labels separated by '.'. */
    const char* d = at + 1;
    if (!isalnum((unsigned char)*d)) {
        bitch("CAA contactemail value is not a valid email address");
        return -1;
    }
    while (d < end) {
        if (*d == '.') {
            d++;
            if (d >= end || !isalnum((unsigned char)*d)) {
                bitch("CAA contactemail value is not a valid email address");
                return -1;
            }
            continue;
        }
        if (!isalnum((unsigned char)*d) && *d != '-') {
            bitch("CAA contactemail value is not a valid email address");
            return -1;
        }
        d++;
    }
    return 0;
}

static int caa_contactphone_validate(struct binary_data value)
{
    if (value.length < 2 || value.data[0] != '+') {
        bitch("CAA contactphone value is not a valid global number");
        return -1;
    }
    int saw_digit = 0;
    for (int i = 1; i < value.length; i++) {
        unsigned char c = (unsigned char)value.data[i];
        if (isdigit(c)) {
            saw_digit = 1;
            continue;
        }
        if (c == '-' || c == '.' || c == ' ')
            continue;
        bitch("CAA contactphone value is not a valid global number");
        return -1;
    }
    if (!saw_digit) {
        bitch("CAA contactphone value is not a valid global number");
        return -1;
    }
    return 0;
}

static const struct caa_tag_entry caa_tag_table[] = {
    { "issue", caa_issue_validate },
    { "issuewild", caa_issuewild_validate },
    { "iodef", caa_iodef_validate },
    { "contactemail", caa_contactemail_validate },
    { "contactphone", caa_contactphone_validate },
    { NULL, NULL }
};

static const struct caa_tag_entry* caa_tag_lookup(const char* tag)
{
    const struct caa_tag_entry* e;
    for (e = caa_tag_table; e->tag; e++) {
        if (strcmp(e->tag, tag) == 0)
            return e;
    }
    return NULL;
}

static struct rr* caa_parse(char* name, long ttl, int type, char* s)
{
    struct rr_caa*              rr = getmem(sizeof(*rr));
    int                         flags;
    char*                       str_tag;
    const struct caa_tag_entry* tag_entry;

    flags = extract_integer(&s, "CAA flags", NULL);
    if (flags < 0)
        return NULL;
    if (flags != 0 && flags != 128)
        return bitch("CAA unrecognized flags value");
    rr->flags = flags;

    str_tag = extract_label(&s, "CAA tag", "temporary");
    if (!str_tag)
        return NULL;

    tag_entry = caa_tag_lookup(str_tag);
    if (!tag_entry)
        return bitch("CAA unrecognized tag name");

    rr->tag   = compose_binary_data("s", 0, str_tag);
    rr->value = extract_text(&s, "CAA tag value");
    if (rr->value.length <= 0)
        return bitch("CAA missing tag value");

    if (tag_entry->validate && tag_entry->validate(rr->value) < 0)
        return NULL; /* bitching done in the validator */

    if (*s) {
        return bitch("garbage after valid CAA data");
    }
    return store_record(type, name, ttl, rr);
}

static char* caa_human(struct rr* rrv)
{
    RRCAST(caa);
    char  ss[4096];
    char* p   = ss;
    char* end = ss + sizeof(ss);
    int   n;

    /* Flags. */
    n = snprintf(p, end - p, "%u ", rr->flags);
    if (n < 0 || n >= end - p)
        return NULL;
    p += n;

    /* Tag. rr->tag is length-prefixed ('s' format): first byte is tag length,
     * then tag bytes. */
    if (rr->tag.length < 1)
        return NULL;
    int tag_len = (unsigned char)rr->tag.data[0];
    if (tag_len + 1 > rr->tag.length)
        return NULL;
    if (p + tag_len + 1 >= end)
        return NULL;
    memcpy(p, rr->tag.data + 1, tag_len);
    p += tag_len;
    *p++ = ' ';

    /* Value: emit as a quoted character-string with backslash-octal escaping. */
    if (p >= end)
        return NULL;
    *p++ = '"';
    for (int i = 0; i < rr->value.length; i++) {
        unsigned char c = (unsigned char)rr->value.data[i];
        if (c == '"' || c == '\\') {
            if (p + 2 >= end)
                return NULL;
            *p++ = '\\';
            *p++ = c;
        } else if (c >= 0x20 && c <= 0x7E) {
            if (p + 1 >= end)
                return NULL;
            *p++ = c;
        } else {
            /* \NNN — 3-digit decimal escape, matches extract_text decoder. */
            if (p + 4 >= end)
                return NULL;
            n = snprintf(p, end - p, "\\%03u", c);
            if (n < 0 || n >= end - p)
                return NULL;
            p += n;
        }
    }
    if (p + 1 >= end)
        return NULL;
    *p++ = '"';
    *p   = '\0';
    return quickstrdup_temp(ss);
}

static struct binary_data caa_wirerdata(struct rr* rrv)
{
    RRCAST(caa);

    return compose_binary_data("1dd", 1, rr->flags, rr->tag, rr->value);
}

/*
static void *caa_validate(struct rr *rrv)
{
    dump_binary_data(stderr, call_get_wired(rrv));
    return NULL;
}
*/

struct rr_methods caa_methods = { caa_parse, caa_human, caa_wirerdata, NULL, NULL };
