/* rbldnsd_dnvset.c.
 * Zone type which consists of a set of (possible wildcarded)
 * domain names together with (A,TXT) result for each.
 */

#include <syslog.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "rbldnsd.h"
#include "dns.h"
#include "mempool.h"

struct entry {
  const unsigned char *dn; /* mp-allocated */
  ip4addr_t r_a;
  const char *r_txt;  /* sp-allocated */
};

/* zone data and operations is the same as in rbldnsd_dnset,
 * except of instead array of just domain names we store array
 * of structures for each entry.  Also, we allow duplicated entries. */

struct zonedata {
  unsigned n[2]; /* number of entries */
  unsigned a[2]; /* entries allocated (used only when loading) */
  struct entry *e[2]; /* entries: plain and wildcard */
  unsigned minlab[2]; /* min level of labels */
  unsigned maxlab[2]; /* max level of labels */
  ip4addr_t r_a; /* default result: address */
  const char *r_txt; /* default result: text (sp-allocated) */
  struct mempool mpdn; /* mempool for domain names (no dup checking) */
  struct strpool sptx; /* strpool for TXT RRs */
};

/* indexes */
#define EP 0
#define EW 1

void dnvset_free(struct zonedata *z) {
  if (z) {
    sp_free(&z->sptx);
    mp_free(&z->mpdn);
    if (z->e[EP]) free(z->e[EP]);
    if (z->e[EW]) free(z->e[EW]);
    free(z);
  }
}

static int
dnvset_parseline(struct zonedata *z, char *line, int lineno, int llines) {
  char *p;
  ip4addr_t a;
  unsigned char dn[DNS_MAXDN];
  struct entry *e;
  unsigned idx, labels;
  int not;

  if (!llines && line[0] == ':') {
    if (!addrtxt(line, &a, &p)) {
      zlog(LOG_WARNING, lineno, "invalid default entry");
      return 1;
    }
    if (a) z->r_a = a;
    if (*p && !(z->r_txt = sp_estrdup(&z->sptx, p)))
      return 0;
    return 1;
  }

  if (*line == '!') {
    not = 1;
    do ++line;
    while(*line == ' ' || *line == '\t');
  }
  else
    not = 0;
  for(p = line; *p; ++p)
    if (*p == ' ' || *p == '\t' || *p == '#') break;
  if (*p) *p++ = '\0';
  if (*line == '.') { idx = EW; ++line; }
  else if (line[0] == '*' && line[1] == '.') { idx = EW; line += 2; }
  else idx = EP;
  if (!dns_ptodn(line, dn, DNS_MAXDN)) {
    zlog(LOG_WARNING, lineno, "invalid domain name");
    return 1;
  }
  dns_dntol(dn, dn);
  if (not)
    a = 0;
  else {
    while(*p == ' ' || *p == '\t') ++p;
    if (!*p)
      a = z->r_a;
    else if (!addrtxt(p, &a, &p)) {
      zlog(LOG_WARNING, lineno, "invalid value");
      return 1;
    }
    else if (!a)
      a = z->r_a;
  }

  e = z->e[idx];
  if (z->n[idx] >= z->a[idx]) {
    z->a[idx] = z->a[idx] ? z->a[idx] << 1 : 32;
    e = (struct entry *)erealloc(e, z->a[idx] * sizeof(*e));
    if (!e) return 0;
    z->e[idx] = e;
  }
  e += z->n[idx]++;
  e->r_a = a;
  if (not) e->r_txt = NULL;
  else if (!*p) e->r_txt = z->r_txt;
  else if (!(e->r_txt = sp_estrdup(&z->sptx, p))) return 0;
  if (!(e->dn = (const unsigned char*)mp_estrdup(&z->mpdn, dn)))
    return 0;
  labels = dns_dnlabels(dn);
  if (z->maxlab[idx] < labels) z->maxlab[idx] = labels;
  if (z->minlab[idx] > labels) z->minlab[idx] = labels;

  return 1;
}

static int dnvset_cmpent(const struct entry *a, const struct entry *b) {
  int r = strcmp(a->dn, b->dn);
  if (r) return r;
  if (a->r_a < b->r_a) return -1;
  if (a->r_a > b->r_a) return 1;
  return 0;
}

struct zonedata *dnvset_load(FILE *f) {
  int r;
  struct zonedata *z = (struct zonedata *)emalloc(sizeof(*z));
  if (!z) return NULL;
  memset(z, 0, sizeof(*z));
  z->r_a = R_A_DEFAULT;
  z->minlab[EP] = z->minlab[EW] = 255;
  if (!readzlines(f, z, dnvset_parseline)) {
    dnvset_free(z);
    return NULL;
  }
  for(r = 0; r < 2; ++r) {
    if (!z->n[r]) continue;
    if (z->a[r] != z->n[r])
      z->e[r] = (struct entry *)
        realloc(z->e[r], z->n[r] * sizeof(struct entry));
    qsort(z->e[r], z->n[r], sizeof(struct entry),
          (int(*)(const void*, const void*))dnvset_cmpent);
  }
  zloaded("e/w=%u/%u", z->n[EP], z->n[EW]);
  return z;
}

static const struct entry *
dnvset_find(const struct entry *e, int b, const unsigned char *q) {
  int a = 0, m, r;
  while(a <= b) {
    m = (a + b) >> 1;
    r = strcmp(e[m].dn, q);
    if (!r) {
      while(m && strcmp(e[m-1].dn, q) == 0) --m;
      return e + m;
    }
    else if (r < 0) a = m + 1;
    else b = m - 1;
  }
  return NULL;
}

int dnvset_query(const struct zonedata *const z, struct dnspacket *p,
                 const unsigned char *const query, unsigned qflg)
{
  const unsigned char *dn = query;
  unsigned labels = dns_dnlabels(dn);
  const struct entry *e, *t;
  char name[DNS_MAXDOMAIN+1];
  if (!labels) return 0;
  if (labels > z->maxlab[EP] || labels < z->minlab[EP] ||
      !(e = dnvset_find(z->e[EP], z->n[EP] - 1, dn))) {
    /* try wildcard; require at least 1 label on the left */
    do
      --labels, dn += 1 + *dn;
    while(labels > z->maxlab[EW]);
    for(;;) {
      if (labels < z->minlab[EW]) return 0;
      if ((e = dnvset_find(z->e[EW], z->n[EW]-1, dn)) != NULL) break;
      dn += 1 + *dn;
      --labels;
    }
    t = z->e[EW] + z->n[EW];
  }
  else
    t = z->e[EP] + z->n[EP];

  if (!e->r_a) return 0;

  if (qflg & QUERY_TXT) dns_dntop(query, name, sizeof(name));
  do {
    if (qflg & QUERY_A) addrec_a(p, e->r_a);
    if (qflg & QUERY_TXT && e->r_txt) addrec_txt(p, e->r_txt, name);
  } while(++e < t && strcmp(e->dn, dn) == 0);

  return 1;
}
