/*
 * 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.)
 *
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <libgen.h>

#include "config.h"

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

struct globals    G;
struct file_info* file_info = NULL;

int  read_zone_file(void);
void open_zone_file(char* fname);

static void
concat_generate_template(char* buf, int bufsz, int val, struct generate_template_piece* t)
{
    char sval[40];

    while (t) {
        if (t->constant_string) {
            mystrlcat(buf, t->constant_string, bufsz);
        } else {
            snprintf(sval, 40, "%d", val);
            mystrlcat(buf, sval, bufsz);
        }
        t = t->next;
    }
}

static struct generate_template_piece*
free_generate_template(struct generate_template_piece* t)
{
    struct generate_template_piece* n;
    while (t) {
        n = t->next;
        free(t);
        t = n;
    }
    return NULL;
}

static void
create_generate_template_piece(struct generate_template_piece** generate_template, char* s)
{
    if (s && *s == 0)
        return;

    struct generate_template_piece* p = malloc(sizeof(struct generate_template_piece));

    p->constant_string = s;
    p->next            = NULL;

    if (*generate_template) {
        struct generate_template_piece* t = *generate_template;
        while (t->next)
            t = t->next;
        t->next = p;
    } else {
        *generate_template = p;
    }
}

static struct generate_template_piece*
prepare_generate_template(char* t)
{
    char*                           s = t;
    struct generate_template_piece* r = NULL;

    while (1) {
        while (*t && *t != '$')
            t++;
        if (!*t) {
            create_generate_template_piece(&r, s);
            break;
        } else {
            *t = 0;
            create_generate_template_piece(&r, s);
            create_generate_template_piece(&r, NULL);
            t++;
            s = t;
        }
    }

    return r;
}

static char* process_directive(char* s)
{
    char* d = s + 1;
    if (*(s + 1) == 'O' && strncmp(s, "$ORIGIN", 7) == 0) {
        char* o;
        s += 7;
        if (!isspace(*s)) {
            if (isalnum(*s))
                goto unrecognized_directive;
            return bitch("bad $ORIGIN format");
        }
        s = skip_white_space(s);
        o = extract_name(&s, "$ORIGIN value", 0);
        if (!o) {
            return NULL;
        }
        if (*s) {
            return bitch("garbage after valid $ORIGIN directive");
        }
        file_info->current_origin = o;
        if (G.opt.verbose) {
            fprintf(stderr, "-> %s:%d: ", file_info->name, file_info->line);
            fprintf(stderr, "origin is now %s\n", o);
        }
    } else if (*(s + 1) == 'T' && strncmp(s, "$TTL", 4) == 0) {
        s += 4;
        if (!isspace(*s)) {
            if (isalnum(*s))
                goto unrecognized_directive;
            return bitch("bad $TTL format");
        }
        s             = skip_white_space(s);
        G.default_ttl = extract_timevalue(&s, "$TTL value");
        if (G.default_ttl < 0) {
            return NULL;
        }
        if (*s) {
            return bitch("garbage after valid $TTL directive");
        }
        if (G.opt.verbose) {
            fprintf(stderr, "-> %s:%d: ", file_info->name, file_info->line);
            fprintf(stderr, "default ttl is now %ld\n", G.default_ttl);
        }
    } else if (*(s + 1) == 'G' && strncmp(s, "$GENERATE", 9) == 0) {
        int   from, to;
        char *lhs, *rdtype;

        s += 9;
        if (!isspace(*s)) {
            if (isalnum(*s))
                goto unrecognized_directive;
            return bitch("bad $GENERATE format");
        }
        s = skip_white_space(s);

        from = extract_integer(&s, "generate-from", "-");
        if (from < 0)
            return NULL;
        if (*s != '-')
            return bitch("'-' between generate-from and generate-to is expected");
        s++;
        to = extract_integer(&s, "generate-to", "-");
        if (to < 0)
            return NULL;

        if (*s == '/')
            return bitch("generate-step is unsupported for now");

        lhs = extract_name(&s, "generate-lhs", KEEP_CAPITALIZATION | DOLLAR_OK_IN_NAMES);
        if (!lhs)
            return NULL;

        if (*s == '{')
            return bitch("{offset,width,type} is unsupported for now");

        rdtype = extract_label(&s, "type", NULL);
        if (!rdtype)
            return NULL;

        file_info->generate_cur  = from;
        file_info->generate_lim  = to;
        file_info->generate_type = rdtype;
        file_info->generate_lhs  = prepare_generate_template(lhs);
        file_info->generate_rhs  = prepare_generate_template(quickstrdup(s));

        return s;
    } else if (*(s + 1) == 'I' && strncmp(s, "$INCLUDE", 8) == 0) {
        char *p, *f, *new_origin = NULL;
        char  c;
        s += 8;
        if (!isspace(*s)) {
            if (isalnum(*s))
                goto unrecognized_directive;
            return bitch("bad $INCLUDE format");
        }
        s = skip_white_space(s);
        p = s;
        while (*s && !isspace(*s) && *s != ';')
            s++;
        c  = *s;
        *s = '\0';
        if (!*p) {
            return bitch("$INCLUDE directive with empty file name");
        }
        f  = quickstrdup_temp(p);
        *s = c;
        s  = skip_white_space(s);

        if (*s) {
            /* RFC 1035 §5.1: $INCLUDE <file-name> [<domain-name>] [<comment>].
             * The optional domain-name overrides $ORIGIN for the included
             * file only; relative names are relative to the current origin
             * at the $INCLUDE point. */
            new_origin = extract_name(&s, "$INCLUDE origin", 0);
            if (!new_origin)
                return NULL;
            if (*s) {
                return bitch("garbage after valid $INCLUDE directive");
            }
        }
        if (new_origin && G.opt.verbose) {
            fprintf(stderr, "-> %s:%d: ", file_info->name, file_info->line);
            fprintf(stderr, "origin for included %s is %s\n", f, new_origin);
        }
        if (*f == '/') {
            open_zone_file(f);
        } else {
            char buf[1024];

            snprintf(buf, 1024, "%s/%s", G.opt.include_path, f);
            open_zone_file(buf);
        }
        if (new_origin) {
            /* open_zone_file pushed a new file_info; set ITS origin so the
             * parent's current_origin is preserved and naturally restored
             * when the included file ends. */
            file_info->current_origin = new_origin;
        }
    } else {
    unrecognized_directive:
        s = d - 1;
        while (isalnum(*d))
            d++;
        *d = '\0';
        return bitch("unrecognized directive: %s", s);
    }
    return s;
}

char* read_zone_line(void)
{
    char* r;

    if (file_info->generate_lhs) {
        if (file_info->generate_cur <= file_info->generate_lim) {
            file_info->buf[0] = 0;
            concat_generate_template(file_info->buf, LINEBUFSZ, file_info->generate_cur, file_info->generate_lhs);
            mystrlcat(file_info->buf, " ", LINEBUFSZ);
            mystrlcat(file_info->buf, file_info->generate_type, LINEBUFSZ);
            mystrlcat(file_info->buf, " ", LINEBUFSZ);
            concat_generate_template(file_info->buf, LINEBUFSZ, file_info->generate_cur, file_info->generate_rhs);
            file_info->generate_cur++;
            return file_info->buf;
        } else {
            /* Done with this $GENERATE */
            file_info->generate_cur  = 0;
            file_info->generate_lim  = 0;
            file_info->generate_type = NULL;
            free_generate_template(file_info->generate_lhs);
            file_info->generate_lhs = NULL;
            free_generate_template(file_info->generate_rhs);
            file_info->generate_rhs = NULL;
        }
    }

    r = fgets(file_info->buf, LINEBUFSZ, file_info->file);
    if (r)
        file_info->line++;
    return r;
}

int read_zone_file(void)
{
    char* s;
    char *name = NULL, *class, *rdtype;
    long  ttl  = -1;
    while (file_info) {
        while (read_zone_line()) {
            freeall_temp();
            file_info->paren_mode = 0;
            rdtype                = NULL;
            if (empty_line_or_comment(file_info->buf))
                continue;

            s = file_info->buf;
            if (!isspace(*s)) {
                /* <domain-name>, $INCLUDE, $ORIGIN */
                if (*s == '$') {
                    process_directive(s);
                    continue;
                } else {
                    /* <domain-name> */
                    name = extract_name(&s, "record name", 0);
                    if (!name)
                        continue;
                }
            } else {
                s = skip_white_space(s);
            }
            if (!s)
                continue;
            if (!name) {
                bitch("cannot assume previous name for it is not known");
                continue;
            }
            if (G.default_ttl >= 0)
                ttl = G.default_ttl;
            if (isdigit(*s)) {
                ttl = extract_timevalue(&s, "TTL");
                if (ttl < 0)
                    continue;
                class = extract_label(&s, "class or type", "temporary");
                if (!class)
                    continue;
                if (*class == 'i' && *(class + 1) == 'n' && *(class + 2) == 0) {
                } else if (*class == 'c' && *(class + 1) == 's' && *(class + 2) == 0) {
                    bitch("CSNET class is not supported");
                    continue;
                } else if (*class == 'c' && *(class + 1) == 'h' && *(class + 2) == 0) {
                    bitch("CHAOS class is not supported");
                    continue;
                } else if (*class == 'h' && *(class + 1) == 's' && *(class + 2) == 0) {
                    bitch("HESIOD class is not supported");
                    continue;
                } else {
                    rdtype = class;
                }
            } else {
                class = extract_label(&s, "class or type", "temporary");
                if (!class)
                    continue;
                if (*class == 'i' && *(class + 1) == 'n' && *(class + 2) == 0) {
                    if (isdigit(*s)) {
                        ttl = extract_timevalue(&s, "TTL");
                        if (ttl < 0)
                            continue;
                    }
                } else if (*class == 'c' && *(class + 1) == 's' && *(class + 2) == 0) {
                    bitch("CSNET class is not supported");
                    continue;
                } else if (*class == 'c' && *(class + 1) == 'h' && *(class + 2) == 0) {
                    bitch("CHAOS class is not supported");
                    continue;
                } else if (*class == 'h' && *(class + 1) == 's' && *(class + 2) == 0) {
                    bitch("HESIOD class is not supported");
                    continue;
                } else {
                    rdtype = class;
                }
            }
            if (!rdtype) {
                rdtype = extract_label(&s, "type", "temporary");
            }
            if (!rdtype) {
                continue;
            }
            if (ttl < 0) {
                ttl = G.default_ttl;
            }

            {
                int        is_generic;
                int        type = str2rdtype(rdtype, &is_generic);
                struct rr* rr;

                if (type <= 0)
                    continue;

                if (ttl < 0 && !(G.opt.soa_minttl_as_default_ttl && type == T_SOA)) {
                    bitch("ttl not specified and default is not known");
                    continue;
                }

                if (is_generic)
                    rr = rr_parse_any(name, ttl, type, s);
                else if (type > T_MAX)
                    rr = rr_parse_any(name, ttl, type, s);
                else if (rr_methods[type].rr_parse)
                    rr = rr_methods[type].rr_parse(name, ttl, type, s);
                else
                    rr = rr_parse_any(name, ttl, type, s);

                if (type == T_SOA && ttl < 0 && rr) {
                    struct rr_soa* soa = (struct rr_soa*)rr;
                    soa->rr.ttl = G.default_ttl = soa->minimum;
                    if (G.opt.verbose) {
                        fprintf(stderr, "-> %s:%d: ", file_info->name, file_info->line);
                        fprintf(stderr, "no ttl specified; using SOA MINTTL (%ld) instead\n", G.default_ttl);
                    }
                }
            }
        }
        if (ferror(file_info->file))
            croak(1, "read error for %s", file_info->name);
        file_info = file_info->next;
    }
    return 0;
}

void open_zone_file(char* fname)
{
    FILE*             f;
    struct file_info* new_file_info;

    if (strcmp(fname, "-") == 0) {
        f     = stdin;
        fname = "stdin";
    } else {
        f = fopen(fname, "r");
        if (!file_info && !G.opt.include_path_specified) {
            G.opt.include_path = quickstrdup(dirname(quickstrdup_temp(fname)));
        }
    }
    if (!f)
        croak(1, "open %s", fname);
    new_file_info = calloc(1, sizeof(*new_file_info) + strlen(fname) + 1);
    if (!new_file_info)
        croak(1, "calloc(file_info), %s", fname);
    new_file_info->next = file_info;
    new_file_info->file = f;
    new_file_info->line = 0;
    strcpy(new_file_info->name, fname);
    if (file_info) {
        new_file_info->current_origin = file_info->current_origin;
    } else {
        new_file_info->current_origin = G.opt.first_origin;
    }
    file_info = new_file_info;
}

void usage(char* err)
{
    if (err)
        fprintf(stderr, "%s\n", err);
    fprintf(stderr, "Usage:\n");
    fprintf(stderr, "    %s -h\n", thisprogname());
    fprintf(stderr, "    %s [options] zone-file\n", thisprogname());
    fprintf(stderr, "Usage parameters:\n");
    fprintf(stderr, "\t-h\t\tproduce usage text and quit\n");
    fprintf(stderr, "\t-f\t\tquit on first validation error\n");

    fprintf(stderr, "\t-p name\tperform policy check <name>\n");
    fprintf(stderr, "\t\t\tsingle-ns\n");
    fprintf(stderr, "\t\t\tcname-other-data\n");
    fprintf(stderr, "\t\t\tdname\n");
    fprintf(stderr, "\t\t\tnsec3param-not-apex\n");
    fprintf(stderr, "\t\t\tmx-alias\n");
    fprintf(stderr, "\t\t\tns-alias\n");
    fprintf(stderr, "\t\t\trp-txt-exists\n");
    fprintf(stderr, "\t\t\ttlsa-host\n");
    fprintf(stderr, "\t\t\tksk-exists\n");
    fprintf(stderr, "\t\t\tsmimea-host\n");
    fprintf(stderr, "\t\t\tall (except permit-starting-hyphen)\n");
    fprintf(stderr, "\t\t\tpermit-starting-hyphen\n");

    fprintf(stderr, "\t-n N\t\tuse N worker threads\n");
    fprintf(stderr, "\t-q\t\tquiet - do not produce any output\n");
    fprintf(stderr, "\t-s\t\tprint validation summary/stats\n");
    fprintf(stderr, "\t-v\t\tbe extra verbose\n");
    fprintf(stderr, "\t-I path\tuse this path for $INCLUDE files\n");
    fprintf(stderr, "\t-z origin\tuse this origin as initial $ORIGIN\n");
    fprintf(stderr, "\t-t epoch-time\tuse this time instead of \"now\"\n");
    fprintf(stderr, "\t-V\t\tprint version and exit\n");
    exit(1);
}

struct rr_methods rr_methods[T_MAX + 1];

static void initialize_globals(void)
{
    int i;

    setenv("TZ", "GMT0", 1);
    tzset();
    memset(&G, 0, sizeof(G));
    memset(&G.opt, 0, sizeof(G.opt));
    memset(&G.stats, 0, sizeof(G.stats));
    memset(rr_counts, 0, sizeof(rr_counts[0]) * (T_MAX + 1));
    G.default_ttl           = -1; /* XXX orly? */
    G.opt.times_to_check[0] = time(NULL);
    G.opt.n_times_to_check  = 0;
    G.opt.include_path      = ".";

    for (i = 0; i <= T_MAX; i++) {
        rr_methods[i] = unknown_methods;
    }
    rr_methods[T_AAAA]       = aaaa_methods;
    rr_methods[T_A]          = a_methods;
    rr_methods[T_AFSDB]      = afsdb_methods;
    rr_methods[T_CAA]        = caa_methods;
    rr_methods[T_CDNSKEY]    = cdnskey_methods;
    rr_methods[T_CDS]        = cds_methods;
    rr_methods[T_CERT]       = cert_methods;
    rr_methods[T_CNAME]      = cname_methods;
    rr_methods[T_DHCID]      = dhcid_methods;
    rr_methods[T_DLV]        = dlv_methods;
    rr_methods[T_DNAME]      = dname_methods;
    rr_methods[T_DNSKEY]     = dnskey_methods;
    rr_methods[T_DS]         = ds_methods;
    rr_methods[T_HINFO]      = hinfo_methods;
    rr_methods[T_HTTPS]      = svcb_methods;
    rr_methods[T_IPSECKEY]   = ipseckey_methods;
    rr_methods[T_ISDN]       = isdn_methods;
    rr_methods[T_KX]         = kx_methods;
    rr_methods[T_L32]        = l32_methods;
    rr_methods[T_L64]        = l64_methods;
    rr_methods[T_LOC]        = loc_methods;
    rr_methods[T_LP]         = lp_methods;
    rr_methods[T_MB]         = mb_methods;
    rr_methods[T_MG]         = mg_methods;
    rr_methods[T_MINFO]      = minfo_methods;
    rr_methods[T_MR]         = mr_methods;
    rr_methods[T_MX]         = mx_methods;
    rr_methods[T_NAPTR]      = naptr_methods;
    rr_methods[T_NID]        = nid_methods;
    rr_methods[T_NSAP]       = nsap_methods;
    rr_methods[T_NSEC3PARAM] = nsec3param_methods;
    rr_methods[T_NSEC3]      = nsec3_methods;
    rr_methods[T_NSEC]       = nsec_methods;
    rr_methods[T_NS]         = ns_methods;
    rr_methods[T_PTR]        = ptr_methods;
    rr_methods[T_PX]         = px_methods;
    rr_methods[T_RP]         = rp_methods;
    rr_methods[T_RT]         = rt_methods;
    rr_methods[T_RRSIG]      = rrsig_methods;
    rr_methods[T_SMIMEA]     = smimea_methods;
    rr_methods[T_SOA]        = soa_methods;
    rr_methods[T_SPF]        = spf_methods;
    rr_methods[T_SRV]        = srv_methods;
    rr_methods[T_SSHFP]      = sshfp_methods;
    rr_methods[T_SVCB]       = svcb_methods;
    rr_methods[T_TLSA]       = tlsa_methods;
    rr_methods[T_TXT]        = txt_methods;
    rr_methods[T_WALLET]     = wallet_methods;
    rr_methods[T_X25]        = x25_methods;
    rr_methods[T_ZONEMD]     = zonemd_methods;
}

int main(int argc, char** argv)
{
    int            o;
    struct timeval start, stop;

    initialize_globals();
    while ((o = getopt(argc, argv, "fhMqsvI:z:t:p:n:V")) != -1) {
        switch (o) {
        case 'h':
            usage(NULL);
            break;
        case 'f':
            G.opt.die_on_first_error = 1;
            break;
        case 'M':
            G.opt.soa_minttl_as_default_ttl = 1;
            break;
        case 'q':
            G.opt.no_output = 1;
            break;
        case 's':
            G.opt.summary++;
            break;
        case 'v':
            G.opt.verbose = 1;
            break;
        case 'p':
            if (strcmp(optarg, "all") == 0) {
                int i;
                for (i = 0; i < N_RESTRICTIVE_POLICY_CHECKS; i++) {
                    G.opt.policy_checks[i] = 1;
                }
            } else if (strcmp(optarg, "single-ns") == 0) {
                G.opt.policy_checks[POLICY_SINGLE_NS] = 1;
            } else if (strcmp(optarg, "cname-other-data") == 0) {
                G.opt.policy_checks[POLICY_CNAME_OTHER_DATA] = 1;
            } else if (strcmp(optarg, "dname") == 0) {
                G.opt.policy_checks[POLICY_DNAME] = 1;
            } else if (strcmp(optarg, "dnskey") == 0) {
                G.opt.policy_checks[POLICY_DNSKEY] = 1;
            } else if (strcmp(optarg, "nsec3param-not-apex") == 0) {
                G.opt.policy_checks[POLICY_NSEC3PARAM_NOT_APEX] = 1;
            } else if (strcmp(optarg, "mx-alias") == 0) {
                G.opt.policy_checks[POLICY_MX_ALIAS] = 1;
            } else if (strcmp(optarg, "ns-alias") == 0) {
                G.opt.policy_checks[POLICY_NS_ALIAS] = 1;
            } else if (strcmp(optarg, "rp-txt-exists") == 0) {
                G.opt.policy_checks[POLICY_RP_TXT_EXISTS] = 1;
            } else if (strcmp(optarg, "tlsa-host") == 0) {
                G.opt.policy_checks[POLICY_TLSA_HOST] = 1;
            } else if (strcmp(optarg, "smimea-host") == 0) {
                G.opt.policy_checks[POLICY_SMIMEA_HOST] = 1;
            } else if (strcmp(optarg, "ksk-exists") == 0) {
                G.opt.policy_checks[POLICY_KSK_EXISTS] = 1;
            } else if (strcmp(optarg, "permit-starting-hyphen") == 0) {
                G.opt.policy_checks[POLICY_PERMIT_STARTING_HYPHEN] = 1;
            } else {
                usage("unknown policy name");
            }
            break;
        case 'I':
            G.opt.include_path           = optarg;
            G.opt.include_path_specified = 1;
            break;
        case 'z':
            if (strlen(optarg) && *(optarg + strlen(optarg) - 1) == '.') {
                G.opt.first_origin = optarg;
            } else if (strlen(optarg)) {
                G.opt.first_origin = getmem(strlen(optarg) + 2);
                strcpy(mystpcpy(G.opt.first_origin, optarg), ".");
            } else {
                usage("origin must not be empty");
            }
            break;
        case 'n':
            G.opt.n_threads = strtol(optarg, NULL, 10);
            if (G.opt.n_threads > 256)
                usage("non-sensical number of threads requested");
            if (G.opt.verbose)
                fprintf(stderr, "using %d worker threads\n", G.opt.n_threads);
            break;
        case 't': {
            /* -t holds a 32-bit unsigned epoch (RFC 4034 §3.1.5 timeline);
             * parse it as unsigned long long so a post-2038 value is not
             * silently clamped to LONG_MAX on hosts with 32-bit long. */
            char*              end;
            unsigned long long v;
            if (G.opt.n_times_to_check >= MAX_TIMES_TO_CHECK)
                usage("too many -t specified");
            errno = 0;
            v     = strtoull(optarg, &end, 10);
            if (end == optarg || *end != '\0' || errno == ERANGE || v > 0xFFFFFFFFULL)
                usage("-t value must be an unsigned 32-bit epoch");
            G.opt.times_to_check[G.opt.n_times_to_check++] = (uint32_t)v;
            break;
        }
        case 'V':
            printf("%s\n", PACKAGE_VERSION);
            return 0;
        default:
            usage(NULL);
        }
    }
    if (G.opt.n_times_to_check <= 0)
        G.opt.n_times_to_check = 1;
    argc -= optind;
    argv += optind;
    if (argc != 1)
        usage(NULL);
    gettimeofday(&start, NULL);
    open_zone_file(argv[0]);
    read_zone_file();
    validate_zone();
    verify_all_keys();
    if (G.nsec3_present) {
        if (first_nsec3)
            nsec3_validate(&first_nsec3->rr);
        perform_remaining_nsec3checks();
    }
    if (G.dnssec_active && G.opt.policy_checks[POLICY_KSK_EXISTS]) {
        dnskey_ksk_policy_check();
    }
    gettimeofday(&stop, NULL);
    if (G.opt.summary) {
        printf("records found:       %d\n", G.stats.rr_count);
        printf("skipped dups:        %d\n", G.stats.skipped_dup_rr_count);
        printf("record sets found:   %d\n", G.stats.rrset_count);
        printf("unique names found:  %d\n", G.stats.names_count);
        printf("delegations found:   %d\n", G.stats.delegations);
        printf("    nsec3 records:   %d\n", G.stats.nsec3_count);
        /* "not authoritative names" - non-empty terminals without any authoritative records */
        /* delegation points count as authoritative, which might or might not be correct */
        printf("not authoritative names, not counting delegation points:\n"
               "                     %d\n",
            G.stats.not_authoritative);
        printf("validation errors:   %d\n", G.stats.error_count);
        printf("signatures verified: %d\n", G.stats.signatures_verified);
        printf("time taken:          %.3fs\n",
            stop.tv_sec - start.tv_sec + (stop.tv_usec - start.tv_usec) / 1000000.);
        if (G.opt.summary > 1) {
            int i;
            printf("record count by type:\n");
            for (i = 1; i <= T_MAX; i++) {
                if (rr_counts[i])
                    printf("%20s: %d\n", rdtype2str(i), rr_counts[i]);
            }
        }
    }
    return G.exit_code;
}
