/*
 * 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 <ctype.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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

int empty_line_or_comment(char* s)
{
    while (isspace(*s))
        s++;
    if (!*s)
        return 1;
    if (*s == ';')
        return 1;
    return 0;
}

char* skip_white_space(char* s)
{
    while (isspace(*s))
        s++;
    if (*s == ';') {
        while (*s)
            s++;
    }
    if (*s == 0) {
        if (file_info->paren_mode) {
            if (read_zone_line()) {
                return skip_white_space(file_info->buf);
            } else {
                return bitch("unexpected end of file");
            }
        }
    }
    if (*s == '(') {
        if (file_info->paren_mode) {
            return bitch("unexpected opening parenthesis");
        } else {
            file_info->paren_mode = 1;
            s++;
            return skip_white_space(s);
        }
    }
    if (*s == ')') {
        if (file_info->paren_mode) {
            file_info->paren_mode = 0;
            s++;
            return skip_white_space(s);
        } else {
            return bitch("unexpected closing parenthesis");
        }
    }
    return s;
}

/* Decode one backslash-escape sequence at *sp (where *sp already points
 * one byte past the leading '\\') into *tp. Returns 1 on success and
 * advances both pointers; on failure bitches and returns 0. */
static int read_backslash_escape(char** sp, char** tp, char* buf_end)
{
    char* s = *sp;
    char* t = *tp;
    int   d;

    if (isdigit((unsigned char)*s)) {
        d = *s - '0';
        s++;
        if (!isdigit((unsigned char)*s)) {
            bitch("bad escape sequence");
            return 0;
        }
        d = d * 10 + *s - '0';
        s++;
        if (!isdigit((unsigned char)*s)) {
            bitch("bad escape sequence");
            return 0;
        }
        d = d * 10 + *s - '0';
        s++;
        if (d > 255) {
            bitch("bad escape sequence");
            return 0;
        }
        if (d == '.') {
            bitch("a dot within a label is not currently supported");
            return 0;
        }
        if (t >= buf_end) {
            bitch("name too long");
            return 0;
        }
        *(unsigned char*)t++ = (unsigned char)d;
    } else if (*s == '.') {
        bitch("a dot within a label is not currently supported");
        return 0;
    } else if (*s) {
        if (t >= buf_end) {
            bitch("name too long");
            return 0;
        }
        *t++ = *s++;
    } else {
        bitch("backslash in the end of the line not parsable");
        return 0;
    }
    *sp = s;
    *tp = t;
    return 1;
}

/* Common tail used by extract_name_slow and extract_quoted_name: append
 * current_origin if the parsed name is not absolute, run leading-dot,
 * empty-label and wildcard sanity checks, advance *input past trailing
 * whitespace, lowercase unless KEEP_CAPITALIZATION, and return the
 * duplicated result (or NULL after bitching). */
static char* finalize_parsed_name(char* buf, char* t, char* s, char* what,
    int options, char** input)
{
    int   l, ol;
    char* p;

    *t = '\0';
    l  = strlen(buf);
    if (!l)
        return bitch("%s should not be empty", what);

    if (buf[l - 1] != '.') {
        if (!file_info->current_origin)
            return bitch("do not know origin to determine %s", what);
        ol = strlen(file_info->current_origin);
        if (file_info->current_origin[0] == '.') {
            if (l + ol >= 1023)
                return bitch("name too long");
            strcat(buf, file_info->current_origin);
        } else {
            if (l + ol >= 1022)
                return bitch("name too long");
            strcat(buf, ".");
            strcat(buf, file_info->current_origin);
        }
    }

    p = strchr(buf, '*');
    if (p && (p != buf || p[1] != '.'))
        return bitch("%s: bad wildcard", what);
    if (buf[0] == '.' && buf[1] != '\0')
        return bitch("%s: name cannot start with a dot", what);
    if (strstr(buf, ".."))
        return bitch("%s: empty label in a name", what);

    *input = skip_white_space(s);
    if (!*input)
        return NULL; /* bitching's done elsewhere */
    if (!(options & KEEP_CAPITALIZATION)) {
        p = buf;
        while (*p) {
            *p = tolower((unsigned char)*p);
            p++;
        }
    }
    return quickstrdup(buf);
}

/* True for bytes that may appear verbatim as DNS label content in an
 * unquoted zone-file name. Excludes the zone-file metacharacters that
 * terminate or delimit a name; '*' stays reserved so wildcard diagnostics
 * fire as before. Everything else, including '@' mid-name and raw 8-bit
 * bytes (RFC 2181 §11), is accepted. */
static int label_byte_unquoted(unsigned char uc)
{
    if (uc == 0)
        return 0;
    if (isspace(uc))
        return 0;
    switch (uc) {
    case ';':
    case '(':
    case ')':
    case '\\':
    case '"':
    case '*':
        return 0;
    }
    return 1;
}

static char* extract_name_slow(char** input, char* what, int options)
{
    char        buf[1024];
    char* const buf_end = buf + 1022;
    char*       t       = buf;
    char*       s       = *input;

    while (1) {
        if (*s == '\\') {
            s++;
            if (!read_backslash_escape(&s, &t, buf_end))
                return NULL;
        } else if (label_byte_unquoted((unsigned char)*s)) {
            if (t >= buf_end)
                return bitch("name too long");
            *t++ = *s++;
        } else {
            break;
        }
    }
    if (*s && !isspace((unsigned char)*s) && *s != ';' && *s != ')')
        return bitch("%s is not valid", what);

    return finalize_parsed_name(buf, t, s, what, options, input);
}

/* Parse a quoted-string form of a record name: "...". Inside the quotes
 * the same backslash escapes as the unquoted form are honored, but
 * whitespace, commas, etc. are taken as literal label bytes. Matches
 * BIND/ldns behavior. */
static char* extract_quoted_name(char** input, char* what, int options)
{
    char        buf[1024];
    char* const buf_end = buf + 1022;
    char*       t       = buf;
    char*       s       = *input;

    s++; /* skip opening '"' */
    while (*s != '"') {
        if (*s == '\\') {
            s++;
            if (!read_backslash_escape(&s, &t, buf_end))
                return NULL;
        } else if (!*s) {
            return bitch("closing quote not found while parsing %s", what);
        } else {
            if (t >= buf_end)
                return bitch("name too long");
            *t++ = *s++;
        }
    }
    s++; /* skip closing '"' */
    if (*s && !isspace((unsigned char)*s) && *s != ';' && *s != ')')
        return bitch("%s is not valid", what);

    return finalize_parsed_name(buf, t, s, what, options, input);
}

char* extract_name(char** input, char* what, int options)
{
    char* s   = *input;
    char* r   = NULL;
    char* end = NULL;
    char  c;
    int   wildcard = 0;

    if (*s == '@') {
        s++;
        if (*s && !isspace((unsigned char)*s) && *s != ';' && *s != ')') {
            return bitch("literal @ in %s is not all by itself", what);
        }
        if (!file_info->current_origin) {
            return bitch("do not know origin to expand @ in %s", what);
        }
        r = quickstrdup(file_info->current_origin);
    } else if (*s == '"') {
        return extract_quoted_name(input, what, options);
    } else {
        if (!(isalnum((unsigned char)*s) || *s == '_' || *s == '.' || *s == '/' || ((options & DOLLAR_OK_IN_NAMES) && *s == '$')
                || (G.opt.policy_checks[POLICY_PERMIT_STARTING_HYPHEN] && *s == '-'))) {
            if (*s == '*') {
                wildcard = 1;
            } else if (*s && !isspace((unsigned char)*s) && *s != ';' && *s != ')'
                       && *s != '-' && *s != '$') {
                /* '-' and '$' are gated by policy/options above; if we
                 * landed here they were not permitted as a leading char,
                 * so bitch instead of falling through to the slow path. */
                return extract_name_slow(input, what, options);
            } else {
                return bitch("%s expected", what);
            }
        }
        s++;
        while (isalnum((unsigned char)*s) || *s == '.' || *s == '-' || *s == '_' || *s == '/' || ((options & DOLLAR_OK_IN_NAMES) && *s == '$'))
            s++;
        if (*s && !isspace((unsigned char)*s) && *s != ';' && *s != ')') {
            return extract_name_slow(input, what, options);
        }
        if (!*s)
            end = s;
        c  = *s;
        *s = '\0';
        if (*(s - 1) == '.') {
            r = quickstrdup(*input);
        } else {
            if (!file_info->current_origin) {
                return bitch("do not know origin to determine %s", what);
            }
            r = getmem(strlen(*input) + 1 + strlen(file_info->current_origin) + 1);
            if (file_info->current_origin[0] == '.') {
                strcpy(mystpcpy(r, *input), file_info->current_origin);
            } else {
                strcpy(mystpcpy(mystpcpy(r, *input), "."), file_info->current_origin);
            }
        }
        *s = c;
    }
    if (end) {
        *input = end;
    } else {
        *input = skip_white_space(s);
        if (!*input)
            return NULL; /* bitching's done elsewhere */
    }
    if (!(options & KEEP_CAPITALIZATION)) {
        s = r;
        while (*s) {
            *s = tolower((unsigned char)*s);
            s++;
        }
    }
    if (wildcard && r[1] != '.') {
        return bitch("%s: bad wildcard", what);
    } else if (r[0] == '.' && r[1] != '\0') {
        return bitch("%s: name cannot start with a dot", what);
    }
    return r;
}

char* extract_label(char** input, char* what, void* is_temporary)
{
    char* s   = *input;
    char* r   = NULL;
    char* end = NULL;

    if (!isalpha(*s)) {
        return bitch("%s expected", what);
    }
    s++;
    while (isalnum(*s))
        s++;
    if (*s && !isspace(*s)) {
        return bitch("%s is not valid", what);
    }
    if (!*s)
        end = s;
    *s++ = '\0';
    if (is_temporary) {
        r = quickstrdup_temp(*input);
    } else {
        r = quickstrdup(*input);
    }

    if (end) {
        *input = end;
    } else {
        *input = skip_white_space(s);
        if (!*input)
            return NULL; /* bitching's done elsewhere */
    }
    s = r;
    while (*s) {
        *s = tolower(*s);
        s++;
    }
    return r;
}

long long extract_integer(char** input, char* what, const char* extra_delimiters)
{
    char*     s   = *input;
    long long r   = -1;
    char*     end = NULL;
    char      c;

    if (!isdigit(*s)) {
        bitch("%s expected", what);
        return -1;
    }
    s++;
    while (isdigit(*s))
        s++;
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        if (!extra_delimiters || strchr(extra_delimiters, *s) == NULL) {
            bitch("%s is not valid", what);
            return -1;
        }
    }
    if (!*s)
        end = s;
    c  = *s;
    *s = '\0';
    r  = strtoll(*input, NULL, 10);
    *s = c;

    if (end) {
        *input = end;
    } else {
        *input = skip_white_space(s);
        if (!*input)
            return -1; /* bitching's done elsewhere */
    }
    return r;
}

int extract_double(char** input, char* what, double* val, int skip_m)
{
    char* s   = *input;
    char* end = NULL;
    char* stop;
    char  c;
    int   saw_m = 0;

    while (isdigit(*s) || *s == '+' || *s == '-' || *s == '.')
        s++;
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        if (skip_m && (*s == 'm' || *s == 'M')) {
            saw_m = 1;
        } else {
            bitch("%s is not valid", what);
            return -1;
        }
    }
    if (!*s)
        end = s;
    c    = *s;
    *s   = '\0';
    *val = strtod(*input, &stop);
    if (*stop != '\0') {
        *s = c;
        bitch("%s is not valid", what);
        return -1;
    }
    *s = c;

    if (saw_m) {
        s++;
        if (*s && !isspace(*s) && *s != ';' && *s != ')') {
            bitch("%s is not valid", what);
            return -1;
        }
    }

    if (end) {
        *input = end;
    } else {
        *input = skip_white_space(s);
        if (!*input)
            return -1; /* bitching's done elsewhere */
    }
    return 1;
}

long extract_timevalue(char** input, char* what)
{
    char* s = *input;
    int   r = 0, acc = 0;

    if (!isdigit(*s)) {
        bitch("%s expected", what);
        return -1;
    }
next_component:
    r = 0;
    while (isdigit(*s)) {
        r *= 10;
        r += *s - '0';
        s++;
    }
    if (tolower(*s) == 's') {
        s++;
    } else if (tolower(*s) == 'm') {
        r *= 60;
        s++;
    } else if (tolower(*s) == 'h') {
        r *= 3600;
        s++;
    } else if (tolower(*s) == 'd') {
        r *= 86400;
        s++;
    } else if (tolower(*s) == 'w') {
        r *= 604800;
        s++;
    }
    acc += r;
    if (isdigit(*s))
        goto next_component;

    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
    *input = skip_white_space(s);
    if (!*input)
        return -1; /* bitching's done elsewhere */
    return acc;
}

static int is_leap_year(int year)
{
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

long long extract_timestamp(char** input, char* what)
{
    static const int days_before_month[12] = {
        0, 31, 59, 90, 120, 151,
        181, 212, 243, 273, 304, 334
    };
    char*     s      = *input;
    int       year   = 0;
    int       month  = 0;
    int       day    = 0;
    int       hour   = 0;
    int       minute = 0;
    int       second = 0;
    long long epoch  = 0;
    long long days_since_epoch;
    int       y;

    if (!isdigit(*s)) {
        bitch("%s expected", what);
        return -1;
    }
    year  = year * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    year  = year * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    year  = year * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    year  = year * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    month = month * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    month = month * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    day   = day * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    day   = day * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    hour  = hour * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    hour  = hour * 10 + *s - '0';
    epoch = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    minute = minute * 10 + *s - '0';
    epoch  = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    minute = minute * 10 + *s - '0';
    epoch  = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    second = second * 10 + *s - '0';
    epoch  = epoch * 10 + *s - '0';
    s++;
    if (!isdigit(*s))
        goto looks_like_epoch;
    second = second * 10 + *s - '0';
    s++;
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
    if (second > 60 || minute > 59 || hour > 23 || day < 1 || day > 31 || month < 1 || month > 12 || year < 1970 || year > 2106) {
        bitch("%s is not valid", what);
        return -1;
    }
    /*
     * RFC 4034 §3.1.5 specifies RRSIG inception/expiration as a 32-bit
     * unsigned count of seconds since 1970-01-01 00:00:00 UTC.  Compute
     * the epoch arithmetically so the result is in UTC (mktime() would
     * interpret the broken-down time in the local zone) and is not bounded
     * by a possibly 32-bit time_t.
     */
    days_since_epoch = 0;
    for (y = 1970; y < year; y++)
        days_since_epoch += is_leap_year(y) ? 366 : 365;
    days_since_epoch += days_before_month[month - 1];
    if (month > 2 && is_leap_year(year))
        days_since_epoch += 1;
    days_since_epoch += day - 1;
    epoch = ((days_since_epoch * 24 + hour) * 60 + minute) * 60 + second;
    if (epoch > 0xFFFFFFFFLL) {
        bitch("%s is not valid", what);
        return -1;
    }

    goto done;

looks_like_epoch:
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
done:
    *input = skip_white_space(s);
    if (!*input)
        return -1; /* bitching's done elsewhere */
    return epoch;
}

/*
 * Render an RFC 4034 timestamp (seconds since 1970-01-01 00:00:00 UTC)
 * in YYYYMMDDHHMMSS presentation form.  Computed arithmetically, like
 * extract_timestamp() above, so the result is in UTC (not the local
 * zone) and is not bounded by a possibly 32-bit time_t.
 */
char* timestamp2str(uint32_t epoch)
{
    static const int days_per_month[12] = {
        31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    };
    unsigned long long t = epoch;
    int                second, minute, hour, year, month, month_days;
    char               s[15];

    second = t % 60;
    t /= 60;
    minute = t % 60;
    t /= 60;
    hour = t % 24;
    t /= 24; /* t is now whole days since 1970-01-01 */
    year = 1970;
    while (t >= (unsigned)(is_leap_year(year) ? 366 : 365)) {
        t -= is_leap_year(year) ? 366 : 365;
        year++;
    }
    month = 0;
    for (;;) {
        month_days = days_per_month[month];
        if (month == 1 && is_leap_year(year))
            month_days++;
        if (t < (unsigned)month_days)
            break;
        t -= month_days;
        month++;
    }
    snprintf(s, sizeof(s), "%04d%02d%02d%02d%02d%02d",
        year, month + 1, (int)t + 1, hour, minute, second);
    return quickstrdup_temp(s);
}

int extract_ipv4(char** input, char* what, struct in_addr* addr)
{
    char* s = *input;
    char  c;

    while (isdigit(*s) || *s == '.') {
        s++;
    }
    if (s == *input) {
        bitch("%s is not valid", what);
        return -1;
    }
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
    c  = *s;
    *s = 0;
    if (inet_pton(AF_INET, *input, addr) != 1) {
        *s = c;
        bitch("cannot parse %s", what);
        return -1;
    }
    *s     = c;
    *input = skip_white_space(s);
    if (!*input) {
        return -1; /* bitching's done elsewhere */
    }
    return 1;
}

int extract_ipv6(char** input, char* what, struct in6_addr* addr)
{
    char* s = *input;
    char  c;

    while (isdigit(*s) || *s == ':' || *s == '.' || (*s >= 'a' && *s <= 'f') || (*s >= 'A' && *s <= 'F')) {
        s++;
    }
    if (s == *input) {
        bitch("%s is not valid", what);
        return -1;
    }
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
    c  = *s;
    *s = 0;
    if (inet_pton(AF_INET6, *input, addr) != 1) {
        *s = c;
        bitch("cannot parse %s", what);
        return -1;
    }
    *s     = c;
    *input = skip_white_space(s);
    if (!*input) {
        return -1; /* bitching's done elsewhere */
    }
    return 1;
}

int extract_u64(char** input, char* what, uint64_t* r)
{
    char*    s      = *input;
    uint64_t result = 0;
    unsigned u;

#define GETHEXBLOCK                                       \
    if (!isxdigit(*s)) {                                  \
        bitch("%s is not valid", what);                   \
        return -1;                                        \
    }                                                     \
    u = 0;                                                \
    while (isxdigit(*s)) {                                \
        if (isdigit(*s)) {                                \
            u = (u << 4) | (*s - '0');                    \
        } else if (*s >= 'a' && *s <= 'f') {              \
            u = (u << 4) | (*s - 'a' + 10);               \
        } else {                                          \
            u = (u << 4) | (*s - 'A' + 10);               \
        }                                                 \
        s++;                                              \
    }                                                     \
    if (u > 0xffff) {                                     \
        bitch("%s is not valid, hex out of range", what); \
        return -1;                                        \
    }                                                     \
    result = (result << 16) | u;
#define SKIPCOLON                       \
    if (*s != ':') {                    \
        bitch("%s is not valid", what); \
        return -1;                      \
    }                                   \
    s++;

    GETHEXBLOCK;
    SKIPCOLON;
    GETHEXBLOCK;
    SKIPCOLON;
    GETHEXBLOCK;
    SKIPCOLON;
    GETHEXBLOCK;
    *r = result;

#undef GETHEXBLOCK
#undef SKIPCOLON

    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        bitch("%s is not valid", what);
        return -1;
    }
    *input = skip_white_space(s);
    if (!*input) {
        return -1; /* bitching's done elsewhere */
    }
    return 1;
}

static int eui_hexval(int c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    return c - 'A' + 10; /* isxdigit guaranteed by caller */
}

int extract_eui(char** input, char* what, uint8_t* out, int n_octets)
{
    char* s = *input;
    int   i;

    for (i = 0; i < n_octets; i++) {
        if (i > 0) {
            if (*s != '-') {
                bitch("%s is not valid", what);
                return -1;
            }
            s++;
        }
        if (!isxdigit((unsigned char)s[0]) || !isxdigit((unsigned char)s[1])) {
            bitch("%s is not valid", what);
            return -1;
        }
        out[i] = (eui_hexval((unsigned char)s[0]) << 4)
               | eui_hexval((unsigned char)s[1]);
        s += 2;
    }
    *input = skip_white_space(s);
    if (!*input) {
        return -1; /* bitching's done elsewhere */
    }
    return 1;
}

struct binary_data bad_binary_data(void)
{
    struct binary_data r;
    r.length = -1;
    r.data   = NULL;
    return r;
}

void dump_binary_data(FILE* f, struct binary_data d)
{
    char* s       = d.data;
    int   mem_len = d.length;
    int   i;
    char  o[69];
    int   pos[] = { 0, 3, 6, 9, 12, 15, 18, 21, 25, 28, 31, 34, 37, 40, 43, 46 };
    char  hex[] = "0123456789abcdef";

    if (mem_len < 0) {
        fprintf(f, "<BAD DATA>\n");
        return;
    }

    while (mem_len) {
        memset(o, ' ', 67);
        o[67] = '\n';
        o[68] = 0;
        for (i = 0; i < 16 && mem_len > 0; i++, mem_len--, s++) {
            o[pos[i]]     = hex[*s >> 4];
            o[pos[i] + 1] = hex[*s & 0x0f];
            o[51 + i]     = isprint(*s) ? *s : '.';
        }
        fprintf(f, "%s", o);
    }
}

struct binary_data extract_base64_binary_data(char** input, char* what)
{
    char               b64[4096];
    int                l64 = 0;
    char*              s   = *input;
    struct binary_data r   = bad_binary_data();
    int                bl;

    while (s && *s) {
        if (!isalnum(*s) && *s != '=' && *s != '+' && *s != '/') {
            bitch("%s expected", what);
            return r;
        }
        while (isalnum(*s) || *s == '=' || *s == '+' || *s == '/') {
            if (l64 >= 4095) {
                bitch("%s is too long", what);
                return r;
            }
            b64[l64++] = *s++;
        }
        s = skip_white_space(s);
    }
    *input = s;
    if (!s)
        return r;
    b64[l64] = 0;
    bl       = (l64 * 3 + 3) / 4;
    r.data   = getmem(bl);
    r.length = decode_base64(r.data, b64, bl);
    if (r.length < 0) {
        bitch("error decoding base64 %s", what);
        return r;
    }
    return r;
}

struct binary_data extract_base32hex_binary_data(char** input, char* what)
{
    char               b32[4096];
    int                l32 = 0;
    char*              s   = *input;
    struct binary_data r   = bad_binary_data();
    int                bl;

    while (
        (*s >= 'A' && *s <= 'V') || (*s >= 'a' && *s <= 'v') || (*s >= '0' && *s <= '9') || *s == '=') {
        if (l32 >= 4095) {
            bitch("%s is too long", what);
            return r;
        }
        b32[l32++] = *s++;
    }
    if (l32 <= 0) {
        bitch("%s expected", what);
        return r;
    }

    s      = skip_white_space(s);
    *input = s;
    if (!s)
        return r;

    b32[l32] = 0;
    bl       = (l32 * 5 + 7) / 8;
    r.data   = getmem(bl);
    r.length = decode_base32hex(r.data, b32, bl);
    if (r.length < 0) {
        bitch("error decoding base32hex %s", what);
        return r;
    }
    return r;
}

struct binary_data extract_text(char** input, char* what)
{
    char*              s = *input;
    struct binary_data r = bad_binary_data();
    char*              o = getmem_temp(65536);
    int                l = 0;
    int                c;

    if (*s != '"') {
        while (*s && !isspace((unsigned char)*s)) {
            if (l >= 65534) {
                bitch("%s string too long", what);
                return r;
            }
            o[l++] = *s++;
        }
        *input = skip_white_space(s);
        if (!*input)
            return r; /* bitching's done elsewhere */

        o[l]     = 0;
        r.data   = getmem(l + 1);
        r.length = l;
        memcpy(r.data, o, l + 1);
        return r;
    }
    s++;
more_text:
    while (*s && *s != '"') {
        if (*s == '\\') {
            s++;
            if (*s == 0) {
                bitch("bad backslash quoting of %s", what);
                return r;
            } else if (isdigit(*s)) {
                int i;
                c = 0;
                for (i = 0; i < 3; i++) {
                    if (!isdigit(*s)) {
                        bitch("bad \\DDD escape in %s", what);
                        return r;
                    }
                    c = c * 10 + *s - '0';
                    s++;
                }
                if (c > 255) {
                    bitch("bad \\DDD escape in %s", what);
                    return r;
                }
                if (l >= 65534) {
                    bitch("%s string too long", what);
                    return r;
                }
                o[l++] = (unsigned char)c;
            } else {
                o[l] = *s;
                goto new_char;
            }
        } else {
            o[l] = *s;
        new_char:
            if (l >= 65534) {
                bitch("%s string too long", what);
                return r;
            }
            l++;
            s++;
        }
    }
    if (!*s) {
        if (read_zone_line()) {
            s = file_info->buf;
            goto more_text;
        } else {
            bitch("closing quote not found while parsing %s", what);
            return r;
        }
    }
    s++;
    *input = skip_white_space(s);
    if (!*input)
        return r; /* bitching's done elsewhere */

    o[l]     = 0;
    r.data   = getmem(l + 1);
    r.length = l;
    memcpy(r.data, o, l + 1);
    return r;
}

char* text2str(struct binary_data text)
{
    char*         o = getmem_temp(4 * text.length + 3);
    int           l = 0;
    int           i;
    unsigned char c;

    o[l++] = '"';
    for (i = 0; i < text.length; i++) {
        c = (unsigned char)text.data[i];
        if (c == '"' || c == '\\') {
            o[l++] = '\\';
            o[l++] = c;
        } else if (c < 0x20 || c > 0x7e) {
            sprintf(o + l, "\\%03d", c);
            l += 4;
        } else {
            o[l++] = c;
        }
    }
    o[l++] = '"';
    o[l]   = 0;
    return o;
}

struct binary_data extract_hex_binary_data(char** input, char* what, int eat_whitespace)
{
    char               hex[4096];
    char*              s = *input;
    struct binary_data r = bad_binary_data();
    int                hl, hi, hb;

    hex[0] = '0';
    hl     = 1;

    if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
        s += 2;
    if (eat_whitespace == EXTRACT_DONT_EAT_WHITESPACE) {
        while (isxdigit(*s)) {
            if (hl >= 4095) {
                bitch("%s is too long", what);
                return r;
            }
            hex[hl] = *s;
            s++;
            hl++;
        }
        if (*s && !isspace(*s) && *s != ';' && *s != ')') {
            bitch("%s is not valid", what);
            return r;
        }
        *input = skip_white_space(s);
    } else if (eat_whitespace == EXTRACT_EAT_WHITESPACE) {
        while (s && *s) {
            if (!isxdigit(*s)) {
                bitch("%s expected", what);
                return r;
            }
            while (isxdigit(*s)) {
                if (hl >= 4095) {
                    bitch("%s is too long", what);
                    return r;
                }
                hex[hl++] = *s++;
            }
            s = skip_white_space(s);
        }
        *input = s;
    } else {
        bitch("%s: internal: invalid eat_whitespace", what);
    }

    if (!*input)
        return r; /* bitching's done elsewhere */

    hb = hl % 2 ? 1 : 0;
    if (hb == 0)
        bitch("%s: hex data does not represent whole number of bytes", what);
    r.data   = getmem(hl / 2);
    r.length = hl / 2;
    memset(r.data, 0, r.length);
    for (hi = 0; hi < hl - hb; hi++) {
        r.data[hi / 2] <<= 4;
        r.data[hi / 2] |= 0x0f & (isdigit(hex[hi + hb]) ? hex[hi + hb] - '0' : tolower(hex[hi + hb]) - 'a' + 10);
    }
    return r;
}

struct binary_data new_set(void)
{
    struct binary_data set;
    set.length = 256 * (1 + 1 + 32);
    set.data   = getmem_temp(set.length);
    memset(set.data, 0, set.length);
    return set;
}

void add_bit_to_set(struct binary_data* set, int bit)
{
    int map;
    int map_base;
    int byte;

    if (bit < 0 || bit > 65535)
        croakx(1, "bitmap index out of range");
    map                 = bit / 256;
    map_base            = map * (1 + 1 + 32);
    set->data[map_base] = map;
    bit                 = bit & 0xff;
    byte                = bit / 8;
    if (set->data[map_base + 1] <= byte)
        set->data[map_base + 1] = byte + 1;
    set->data[map_base + 2 + byte] |= 0x80 >> (bit & 0x07);
}

struct binary_data compressed_set(struct binary_data* set)
{
    int                len = 0;
    int                map;
    int                map_base;
    struct binary_data r;

    for (map = 0; map <= 255; map++) {
        map_base = map * (1 + 1 + 32);
        if (set->data[map_base + 1]) {
            len += 2 + set->data[map_base + 1];
        }
    }
    r.length = len;
    r.data   = getmem(r.length);
    len      = 0;
    for (map = 0; map <= 255; map++) {
        map_base = map * (1 + 1 + 32);
        if (set->data[map_base + 1]) {
            memcpy(&r.data[len], &set->data[map_base], 2 + set->data[map_base + 1]);
            len += 2 + set->data[map_base + 1];
        }
    }
    return r;
}

struct binary_data compose_binary_data(const char* fmt, int tmp, ...)
{
    va_list            ap;
    const char*        args;
    int                sz;
    struct binary_data bd;
    struct binary_data r;
    char*              t;
    uint8_t            b1;
    uint16_t           b2;
    uint32_t           b4;
    uint64_t           b8;
    char*              bs;
    int                bsl;

    va_start(ap, tmp);
    args = fmt;
    sz   = 0;
    while (*args) {
        switch (*args++) {
        case '1':
            va_arg(ap, unsigned int);
            sz += 1;
            break;
        case '2':
            va_arg(ap, unsigned int);
            sz += 2;
            break;
        case '4':
            va_arg(ap, unsigned int);
            sz += 4;
            break;
        case '8':
            va_arg(ap, uint64_t);
            sz += 8;
            break;
        case 'd':
            bd = va_arg(ap, struct binary_data);
            if (bd.length < 0)
                croak(5, "compose_binary_data: negative data length");
            sz += bd.length;
            break;
        case 'b':
            bd = va_arg(ap, struct binary_data);
            if (bd.length < 0)
                croak(5, "compose_binary_data: negative data length");
            if (bd.length > 255)
                croak(5, "compose_binary_data: 'b' data too long");
            sz += bd.length + 1;
            break;
        case 'B':
            bd = va_arg(ap, struct binary_data);
            if (bd.length < 0)
                croak(5, "compose_binary_data: negative data length");
            if (bd.length > 65535)
                croak(5, "compose_binary_data: 'B' data too long");
            sz += bd.length + 2;
            break;
        case 's':
            bs  = va_arg(ap, char*);
            bsl = strlen(bs);
            if (bsl > 255)
                croak(5, "compose_binary_data: 's' string too long");
            sz += bsl + 1;
            break;
        default:
            croak(5, "compose_binary_data: bad format");
        }
    }
    va_end(ap);

    r.length = sz;
    r.data   = tmp ? getmem_temp(sz) : getmem(sz);
    t        = r.data;
    va_start(ap, tmp);
    args = fmt;
    while (*args) {
        switch (*args++) {
        case '1':
            b1 = (uint8_t)va_arg(ap, unsigned int);
            memcpy(t, &b1, 1);
            t += 1;
            break;
        case '2':
            b2 = htons(va_arg(ap, unsigned int));
            memcpy(t, &b2, 2);
            t += 2;
            break;
        case '4':
            b4 = htonl(va_arg(ap, unsigned int));
            memcpy(t, &b4, 4);
            t += 4;
            break;
        case '8':
            b8 = va_arg(ap, uint64_t);
            t[0] = (char)((b8 >> 56) & 0xff);
            t[1] = (char)((b8 >> 48) & 0xff);
            t[2] = (char)((b8 >> 40) & 0xff);
            t[3] = (char)((b8 >> 32) & 0xff);
            t[4] = (char)((b8 >> 24) & 0xff);
            t[5] = (char)((b8 >> 16) & 0xff);
            t[6] = (char)((b8 >> 8) & 0xff);
            t[7] = (char)(b8 & 0xff);
            t += 8;
            break;
        case 'd':
            bd = va_arg(ap, struct binary_data);
            if (bd.length > 0)
                memcpy(t, bd.data, bd.length);
            t += bd.length;
            break;
        case 'b':
            bd = va_arg(ap, struct binary_data);
            b1 = (uint8_t)bd.length;
            memcpy(t, &b1, 1);
            t += 1;
            if (bd.length > 0)
                memcpy(t, bd.data, bd.length);
            t += bd.length;
            break;
        case 'B':
            bd = va_arg(ap, struct binary_data);
            b2 = htons(bd.length);
            memcpy(t, &b2, 2);
            t += 2;
            if (bd.length > 0)
                memcpy(t, bd.data, bd.length);
            t += bd.length;
            break;
        case 's':
            bs  = va_arg(ap, char*);
            bsl = strlen(bs);
            b1  = (uint8_t)bsl;
            memcpy(t, &b1, 1);
            t += 1;
            memcpy(t, bs, bsl);
            t += bsl;
            break;
        default:
            croak(5, "compose_binary_data: bad format");
        }
    }
    va_end(ap);
    return r;
}

/* implementation taken from FreeBSD's libc (minus the __restrict keyword) */
char* mystpcpy(char* to, const char* from)
{
    for (; (*to = *from); ++from, ++to)
        ;
    return (to);
}

size_t
mystrlcat(char* dst, const char* src, size_t siz)
{
    char*       d = dst;
    const char* s = src;
    size_t      n = siz;
    size_t      dlen;

    /* Find the end of dst and adjust bytes left but don't go past end */
    while (n-- != 0 && *d != '\0')
        d++;
    dlen = d - dst;
    n    = siz - dlen;

    if (n == 0)
        return (dlen + strlen(s));
    while (*s != '\0') {
        if (n != 1) {
            *d++ = *s;
            n--;
        }
        s++;
    }
    *d = '\0';

    return (dlen + (s - src)); /* count does not include NUL */
}

#ifdef TEXTPARSE_TEST_PROGRAM

#include <sys/wait.h>
#include <unistd.h>

struct globals     G;
struct file_info*  file_info = NULL;

char* read_zone_line(void)
{
    return NULL;
}

static int verbose = 0;

static int expect_croak(int testnum, const char* fmt)
{
    int     pipefd[2];
    pid_t   pid;
    char    buf[256];
    ssize_t n;
    int     status;

    if (pipe(pipefd) < 0) {
        printf("test %d: NOT OK: pipe failed\n", testnum);
        return 1;
    }
    fflush(stdout);
    pid = fork();
    if (pid < 0) {
        printf("test %d: NOT OK: fork failed\n", testnum);
        close(pipefd[0]);
        close(pipefd[1]);
        return 1;
    }
    if (pid == 0) {
        struct binary_data bad = { .length = -1, .data = NULL };
        close(pipefd[0]);
        dup2(pipefd[1], STDERR_FILENO);
        close(pipefd[1]);
        compose_binary_data(fmt, 1, bad);
        _exit(0);
    }
    close(pipefd[1]);
    n = 0;
    while (n < (ssize_t)sizeof(buf) - 1) {
        ssize_t got = read(pipefd[0], buf + n, sizeof(buf) - 1 - n);
        if (got <= 0)
            break;
        n += got;
    }
    close(pipefd[0]);
    buf[n] = 0;
    waitpid(pid, &status, 0);
    if (!WIFEXITED(status)) {
        printf("test %d: NOT OK: child did not exit normally\n", testnum);
        return 1;
    }
    if (WEXITSTATUS(status) != 5) {
        printf("test %d: NOT OK: expected exit status 5, got %d\n", testnum, WEXITSTATUS(status));
        return 1;
    }
    if (!strstr(buf, "negative data length")) {
        printf("test %d: NOT OK: expected stderr to mention \"negative data length\", got \"%s\"\n", testnum, buf);
        return 1;
    }
    if (verbose)
        printf("test %d: ok\n", testnum);
    return 0;
}

static int expect_bytes(int testnum, struct binary_data r, const unsigned char* expect, int expect_len)
{
    if (r.length != expect_len) {
        printf("test %d: NOT OK: expect size %d, got %d\n", testnum, expect_len, r.length);
        return 1;
    }
    if (expect_len > 0 && memcmp(r.data, expect, expect_len) != 0) {
        printf("test %d: NOT OK: unexpected buffer content\n", testnum);
        return 1;
    }
    if (verbose)
        printf("test %d: ok\n", testnum);
    return 0;
}

int main(int argc, char** argv)
{
    int ret = 0;
    int t   = 1;
    int i;

    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0)
            verbose = 1;
    }

    ret += expect_croak(t++, "d");
    ret += expect_croak(t++, "b");
    ret += expect_croak(t++, "B");

    {
        struct binary_data zero = { .length = 0, .data = NULL };
        ret += expect_bytes(t++, compose_binary_data("d", 1, zero), NULL, 0);
    }

    {
        struct binary_data three = { .length = 3, .data = "abc" };
        unsigned char      expect[] = { 0x01, 0x02, 0x61, 0x62, 0x63 };
        ret += expect_bytes(t++, compose_binary_data("2d", 1, 0x0102u, three), expect, sizeof(expect));
    }

    {
        struct binary_data three = { .length = 3, .data = "abc" };
        unsigned char      expect[] = { 0x03, 0x61, 0x62, 0x63 };
        ret += expect_bytes(t++, compose_binary_data("b", 1, three), expect, sizeof(expect));
    }

    {
        struct binary_data zero = { .length = 0, .data = NULL };
        unsigned char      expect[] = { 0x00 };
        ret += expect_bytes(t++, compose_binary_data("b", 1, zero), expect, sizeof(expect));
    }

    {
        struct binary_data zero = { .length = 0, .data = NULL };
        unsigned char      expect[] = { 0x00, 0x00 };
        ret += expect_bytes(t++, compose_binary_data("B", 1, zero), expect, sizeof(expect));
    }

    {
        struct binary_data three = { .length = 3, .data = "abc" };
        unsigned char      expect[] = { 0x00, 0x03, 0x61, 0x62, 0x63 };
        ret += expect_bytes(t++, compose_binary_data("B", 1, three), expect, sizeof(expect));
    }

    if (ret) {
        printf("%d/%d tests failed.\n", ret, t - 1);
    } else {
        printf("All %d tests successful.\n", t - 1);
    }

    return ret;
}
#endif
