/*
 * 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 <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <Judy.h>

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

/* Signed-zone completeness (issue #16) and delegation-glue policy checks
 * (issues #24 and #13), all sharing one per-name walk.  Every RR set that
 * ought to be signed must have a covering RRSIG; under the
 * all-algorithms-signed policy, coverage is additionally required for
 * every algorithm present in the apex DNSKEY RR set (RFC 4035, section
 * 2.2).  The occluded-records policy reports RR sets hidden below zone
 * cuts, exempting glue; the missing-glue policy reports in-bailiwick
 * nameserver names lacking address records.  Signature verification
 * remains rrsig_validate's job. */

static struct cbtree ns_targets; /* every NS target named from a non-occluded owner */

static char* lowercase_name(char* name)
{
    char* s = quickstrdup_temp(name);
    char* p;

    for (p = s; *p; p++)
        if (*p >= 'A' && *p <= 'Z')
            *p |= 0x20;
    return s;
}

void collect_ns_targets(struct named_rr* named_rr)
{
    struct rr_set** rr_set_p;
    struct rr*      rr;

    JLG(rr_set_p, named_rr->rr_sets, T_NS);
    if (!rr_set_p)
        return;
    rr = (*rr_set_p)->tail;
    while (rr) {
        if (!cbtree_insert(&ns_targets, lowercase_name(((struct rr_ns*)rr)->nsdname)))
            croak(2, "collect_ns_targets: tree insertion failed");
        rr = rr->next;
    }
}

static int ns_set_contains(struct named_rr* owner, char* name)
{
    struct rr_set** rr_set_p;
    struct rr*      rr;

    JLG(rr_set_p, owner->rr_sets, T_NS);
    if (!rr_set_p)
        return 0;
    rr = (*rr_set_p)->tail;
    while (rr) {
        if (strcasecmp(((struct rr_ns*)rr)->nsdname, name) == 0)
            return 1;
        rr = rr->next;
    }
    return 0;
}

static int name_in_bailiwick(char* name, char* cut)
{
    size_t ln = strlen(name);
    size_t lc = strlen(cut);

    if (ln == lc)
        return strcasecmp(name, cut) == 0;
    return ln > lc && name[ln - lc - 1] == '.' && strcasecmp(name + ln - lc, cut) == 0;
}

static void check_missing_glue(struct named_rr* named_rr)
{
    struct rr_set** rr_set_p;
    struct rr*      rr;

    JLG(rr_set_p, named_rr->rr_sets, T_NS);
    if (!rr_set_p)
        return;
    rr = (*rr_set_p)->tail;
    while (rr) {
        char* nsdname = ((struct rr_ns*)rr)->nsdname;

        if (name_in_bailiwick(nsdname, named_rr->name)) {
            struct named_rr* target = find_named_rr(nsdname);

            if (!target
                || (!find_rr_set_in_named_rr(target, T_A)
                    && !find_rr_set_in_named_rr(target, T_AAAA)))
                moan(rr->file_name, rr->line,
                    "%s: missing glue for nameserver %s",
                    named_rr->name, nsdname);
        }
        rr = rr->next;
    }
}

static void check_occluded_sets(struct named_rr* named_rr, struct named_rr* cut, struct named_rr* apex)
{
    struct rr_set** rr_set_p;
    Word_t          rdtype = 0;

    JLF(rr_set_p, named_rr->rr_sets, rdtype);
    while (rr_set_p) {
        struct rr_set* rr_set     = *rr_set_p;
        int            is_address = rr_set->rdtype == T_A || rr_set->rdtype == T_AAAA;

        if (is_address
            && (ns_set_contains(cut, named_rr->name) || ns_set_contains(apex, named_rr->name))) {
            /* glue for the cut above it, or for the zone's own apex NS set */
        } else if (is_address && cbtree_find(&ns_targets, lowercase_name(named_rr->name))) {
            if (!G.opt.policy_checks[POLICY_NO_STRICT_GLUE])
                moan(rr_set->tail->file_name, rr_set->tail->line,
                    "%s %s: glue not referenced by its delegation %s",
                    named_rr->name, rdtype2str(rr_set->rdtype), cut->name);
        } else {
            moan(rr_set->tail->file_name, rr_set->tail->line,
                "%s %s: occluded by delegation %s",
                named_rr->name, rdtype2str(rr_set->rdtype), cut->name);
        }
        JLN(rr_set_p, named_rr->rr_sets, rdtype);
    }
}

struct coverage_ctx {
    void*            apex_algorithms;
    struct named_rr* apex;
};

static int check_one_name(const char* name, intptr_t* data, void* p)
{
    struct named_rr*     named_rr        = *((struct named_rr**)data);
    struct coverage_ctx* ctx             = p;
    void*                apex_algorithms = ctx->apex_algorithms;
    struct named_rr*     cut;
    struct rr_set**      rr_set_p;
    void*                covered_types = NULL;
    void*                covered_pairs = NULL;
    Word_t               rdtype;
    Word_t               rcw;
    int                  rc;
    int                  is_delegation;

    freeall_temp();
    if ((named_rr->flags & NAME_FLAG_APEX_PARENT) != 0)
        return 1;
    cut = find_occluding_delegation(named_rr);
    if (G.opt.policy_checks[POLICY_OCCLUDED_RECORDS] && cut && named_rr->rr_sets)
        check_occluded_sets(named_rr, cut, ctx->apex);
    if (G.opt.policy_checks[POLICY_MISSING_GLUE] && !cut
        && (named_rr->flags & (NAME_FLAG_DELEGATION | NAME_FLAG_APEX)) == NAME_FLAG_DELEGATION)
        check_missing_glue(named_rr);
    if (!G.dnssec_active)
        return 1;
    if ((named_rr->flags & NAME_FLAG_NOT_AUTHORITATIVE) != 0)
        return 1;
    is_delegation = (named_rr->flags & NAME_FLAG_DELEGATION) != 0;

    JLG(rr_set_p, named_rr->rr_sets, T_RRSIG);
    if (rr_set_p) {
        struct rr* rr = (*rr_set_p)->tail;
        while (rr) {
            struct rr_rrsig* sig = (struct rr_rrsig*)rr;
            J1S(rc, covered_types, sig->type_covered);
            if (apex_algorithms)
                J1S(rc, covered_pairs, (Word_t)sig->type_covered * 256 + sig->algorithm);
            rr = rr->next;
        }
    }

    rdtype = 0;
    JLF(rr_set_p, named_rr->rr_sets, rdtype);
    while (rr_set_p) {
        struct rr_set* rr_set = *rr_set_p;

        if (rdtype == T_RRSIG)
            goto next_set;
        /* At a delegation, only DS and NSEC are authoritative; NS and
         * glue at the owner are the parent's non-authoritative copies,
         * the same exemption check_typemap makes. */
        if (is_delegation && rdtype != T_DS && rdtype != T_NSEC)
            goto next_set;
        J1T(rc, covered_types, rdtype);
        if (!rc) {
            moan(rr_set->tail->file_name, rr_set->tail->line,
                "%s %s: no covering RRSIG in a signed zone",
                named_rr->name, rdtype2str(rr_set->rdtype));
        } else if (apex_algorithms) {
            Word_t alg = 0;
            J1F(rc, apex_algorithms, alg);
            while (rc) {
                int covered;
                J1T(covered, covered_pairs, rdtype * 256 + alg);
                if (!covered) {
                    moan(rr_set->tail->file_name, rr_set->tail->line,
                        "%s %s: no covering RRSIG with algorithm %d, which the DNSKEY RR set contains",
                        named_rr->name, rdtype2str(rr_set->rdtype), (int)alg);
                }
                J1N(rc, apex_algorithms, alg);
            }
        }
    next_set:
        JLN(rr_set_p, named_rr->rr_sets, rdtype);
    }
    J1FA(rcw, covered_types);
    J1FA(rcw, covered_pairs);
    return 1;
}

void validate_coverage_and_glue(void)
{
    struct coverage_ctx ctx = { NULL, NULL };
    Word_t              rcw;
    int                 rc;

    ctx.apex = find_named_rr(zone_apex);
    if (!ctx.apex)
        return;
    if (G.dnssec_active && G.opt.policy_checks[POLICY_ALL_ALGORITHMS_SIGNED]) {
        struct rr_set* dnskey_set = find_rr_set(T_DNSKEY, zone_apex);
        if (dnskey_set) {
            struct rr* rr = dnskey_set->tail;
            while (rr) {
                struct rr_dnskey* key = (struct rr_dnskey*)rr;
                J1S(rc, ctx.apex_algorithms, key->algorithm);
                rr = rr->next;
            }
        }
    }
    cbtree_allprefixed(&zone_data, "", check_one_name, &ctx);
    J1FA(rcw, ctx.apex_algorithms);
}
