/*
    GSK - a library to write servers
    Copyright (C) 2001 Dave Benson

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA

    Contact:
        daveb@ffem.org <Dave Benson>
*/

#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include "gskdns.h"
#include "../gskmacros.h"
#include "../gskghelpers.h"

/* SAFE_STRLEN: Determine the length of a string, or 0 for NULL. */
#define SAFE_STRLEN(str)     ((str) != NULL ? strlen(str) : 0)

/* SAFE_BUFFER_APPEND_STR0: Append a length-prefixed string,
 *                          up to 255 characters.
 */
#define SAFE_BUFFER_APPEND_STR0(buffer, str) 			\
      G_STMT_START{						\
	if ((str) != NULL)					\
	  {							\
	    guint len = strlen (str);				\
	    guint8 len8;					\
	    if (len > 255)					\
	      len = 255;					\
	    len8 = (guint8) len;				\
	    gsk_buffer_append (buffer, &len8, 1);		\
	    gsk_buffer_append (buffer, (str), len8);		\
	  }							\
	else							\
	  gsk_buffer_append_char (buffer, 0);			\
      }G_STMT_END

/* Maximum offset allowed by the compression scheme. */
#define MAX_OFFSET			((1<<14) - 1)

/* How many characters to make the OWNER column for textual representation. */
#define COLUMN_OWNER_WIDTH		32

/* Maximum number of dotted components in a domain name we can handle. */
#define MAX_COMPONENTS			128

/* Protocol-limited number of characters in a component of a domain name. */
#define MAX_COMPONENT_LENGTH		63

/* Maximum length for a TXT record. */
#define MAX_TEXT_STRING_SIZE           1024

#define GSK_SKIP_DIGITS(char_ptr)	GSK_SKIP_CHAR_TYPE(char_ptr, isdigit)

static char * strncpy_nul_internal (char *dest, const char *mem, int len)
{
  strncpy (dest, mem, len);
  dest[len] = 0;
  return dest;
}

static int compute_maybe_suffixed_length (const char *start,
					  const char *end,
					  const char *origin)
{
  if (start >= end)
    return 1;
  if (end[-1] == '.')
    return strlen (start) + 2;
  else
    return strlen (start) + strlen (origin) + 2;
}
static char *suffix_and_copy  (char           *rv,
			       const char     *start,
			       const char     *end,
			       const char     *origin)
{
  if (start >= end)
    {
      *rv = 0;
      return rv;
    }
  memcpy (rv, start, end - start);
  if (end[-1] == '.')
    {
      rv[end - start - 1] = 0;
      return rv;
    }
  if (strcmp (origin, ".") == 0)
    rv[end - start] = 0;
  else
    {
      rv[end - start] = '.';
      strcpy (rv + (end - start + 1), origin);
    }
  return rv;
}

#define CUT_ONTO_STACK(var,start,end) \
	G_STMT_START{ \
	  const char *_str = start; \
	  guint _len = end - start; \
	  char *_tmp; \
	  _tmp = alloca (_len + 1);	\
	  var = strncpy_nul_internal (_tmp, _str, _len); \
	}G_STMT_END

#define SUFFIXED_CUT_ONTO_STACK(var,start,end,suffix) \
  	G_STMT_START{ \
	  char *_tmp; \
	  _tmp = alloca (compute_maybe_suffixed_length (start, end, suffix)); \
	  var = suffix_and_copy (_tmp, start, end, suffix); \
  	}G_STMT_END

/* Support some of the extensions that BIND supports.  */
#define SUPPORT_BIND_ENHANCEMENTS	1

/* TEST ONLY */
#if 1
#define PARSE_FAIL(note)	gsk_g_debug ("NOTE: parse error: %s", note)
#else
#define PARSE_FAIL(note)	((void) (note))
#endif

/* Parse the time field for an SOA record. */
static int
parse_into_seconds (const char     *str,
		    char          **endp)
{
  int rv;
#if SUPPORT_BIND_ENHANCEMENTS
  char *tmp = (char *) str;
  rv = 0;
  GSK_SKIP_WHITESPACE (tmp);
  while (TRUE)
    {
      char *end;
      int v;
      int scale = 1;
      if (*tmp == 0)
	break;
      v = strtol (tmp, &end, 10);
      if (tmp == end)
	break;
      GSK_SKIP_DIGITS (tmp);
      switch (*tmp)
	{
	case 'm': case 'M': scale = 60; tmp++; break;
	case 'h': case 'H': scale = 60 * 60; tmp++; break;
	case 'd': case 'D': scale = 60 * 60 * 24; tmp++; break;
	case 'w': case 'W': scale = 60 * 60 * 24 * 7; tmp++; break;
	default: break;
	}
      rv += v * scale;
      if (*tmp == '\0')
	break;
      if (isspace (*tmp))
	break;
    }
  if (endp != NULL)
    *endp = tmp;
  return rv;
#else  /* !SUPPORT_BIND_ENHANCEMENTS */
  return (int) strtol (str, endp, 10);
#endif  /* !SUPPORT_BIND_ENHANCEMENTS */
}

/* --- pooling GskDnsMessage's --- */
static GMemChunk *gsk_dns_message_chunk = NULL;
G_LOCK_DEFINE_STATIC (gsk_dns_message_chunk);

static inline GskDnsMessage  *
gsk_dns_message_alloc   ()
{
  GskDnsMessage *rv;
  G_LOCK (gsk_dns_message_chunk);
  if (gsk_dns_message_chunk == NULL)
    gsk_dns_message_chunk = g_mem_chunk_create (GskDnsMessage,
						16,
						G_ALLOC_AND_FREE);
  rv = g_mem_chunk_alloc (gsk_dns_message_chunk);
  G_UNLOCK (gsk_dns_message_chunk);

  memset (rv, 0, sizeof (GskDnsMessage));

  rv->qr_pool = g_mem_chunk_new ("DNS (Resource and Question) Pool",
				 MAX (sizeof (GskDnsResourceRecord),
				      sizeof (GskDnsQuestion)),
				 2048,
				 G_ALLOC_ONLY);
  rv->str_pool = g_string_chunk_new (2048);
  rv->ref_count = 1;

  return rv;
}
static inline void
gsk_dns_message_free (GskDnsMessage *gsk_dns_message)
{
  g_string_chunk_free (gsk_dns_message->str_pool);
  g_mem_chunk_destroy (gsk_dns_message->qr_pool);

  G_LOCK (gsk_dns_message_chunk);
  g_mem_chunk_free (gsk_dns_message_chunk, gsk_dns_message);
  G_UNLOCK (gsk_dns_message_chunk);
}

/* --- Dns Message parsing.  (See RFC 1035, 4.1.1.) --- */

/* The Compression Algorithm
 * 
 * Each component 
 *     (length-prefixed string)+ (0 | pointer-to-end-of-another string)
 * for example:
 *     a.hello.com.
 * might be stored:
 *     \001a\005hello\003com\0
 * the compression stunt is that since the length of each
 * component is limited to 63 characters, the upper
 * two bits of the length are used as a flag:
 *
 *    - if the top two bits are both one, then the next 14 bits
 *      are a pointer to a byte offset of a terminating string
 *
 *      eg if "hello.com" had already been stored at offset 64 (==\100),
 *      then a.hello.com could be stored:
 *    \001a\300\100
 */


/* --- decompressing (RFC 1035, 4.1.4) --- */
static char *
parse_domain_name (GskBufferIterator *iterator,
		   GskDnsMessage     *message)
{
  char tmp_buf[63 + 1];
  char rv_buf[1024];
  int len = 0;
  guint i;
  GString *string = NULL;

  /* We need to store other offset pairs,
   * which will be entered into the compression table
   * after the string has been formed in all its dotted glory...
   */
  int str_offsets[MAX_COMPONENTS];
  int buffer_offsets[MAX_COMPONENTS];
  guint num_offsets = 0;

  char *rv;
  gboolean stop = FALSE;

  rv_buf[0] = 0;

  while (!stop)
    {
      guint8 tmp_len;
      guint piece_len;
      gchar *str;
      guint component_offset = gsk_buffer_iterator_offset (iterator);
      if (gsk_buffer_iterator_read (iterator, &tmp_len, 1) != 1)
	return NULL;
      piece_len = tmp_len;

      if ((piece_len >> 6) == 3)
	{
	  guint8 ptr_byte_2;
	  guint offset;
	  if (gsk_buffer_iterator_read (iterator, &ptr_byte_2, 1) != 1)
	    return NULL;
	  offset = (((guint)piece_len & 0x3f) << 8) | ((guint)ptr_byte_2);
	  str = g_hash_table_lookup (message->offset_to_str,
				     GUINT_TO_POINTER (offset));
	  if (str == NULL)
	    {
	      PARSE_FAIL ("offset not found (for compression)");
	      return NULL;
	    }

	  /* XXX: SECURITY: this needs to enforce MAX_COMPONENTS!!! */
	  piece_len = strlen (str);
	  stop = TRUE;
	}
      else if ((piece_len >> 6) != 0)
	{
	  /* Not allowed ``reserved for future use'' (rfc1035) */
	  /* XXX: parse error? */
	  PARSE_FAIL ("bad bit sequence at start of string");
	  return NULL;
	}
      else if (piece_len == 0)
	{
	  break;
	}
      else
	{
	  str = tmp_buf;
	  g_assert (piece_len < 64);
	  if (gsk_buffer_iterator_read (iterator,
					tmp_buf,
					piece_len) != piece_len)
	    {
	      /* XXX: parse error? */
	      PARSE_FAIL ("data shorter than header byte indicated");
	      return NULL;
	    }
	  tmp_buf[piece_len] = 0;
	}

      /* Add this offset for this compression table... */
      if (num_offsets == MAX_COMPONENTS)
	{
	  g_warning ("too many dotted components for compile time limit (%d)?",
		     MAX_COMPONENTS);
	  return NULL;
	}
      str_offsets[num_offsets] = (len == 0 ? 0 : len + 1);
      buffer_offsets[num_offsets] = component_offset;
      num_offsets++;

      if (string == NULL && piece_len + len >= (sizeof (rv_buf) - 2))
	{
	  rv_buf[len] = 0;
	  string = g_string_new (rv_buf);
	}
      if (string != NULL)
	{
	  if (len > 0)
	    g_string_append_c (string, '.');
	  g_string_append (string, str);
	}
      else
	{
	  if (len > 0)
	    rv_buf[len++] = '.';
	  memcpy (rv_buf + len, str, piece_len);
	  rv_buf[len + piece_len] = 0;
	}
      len += piece_len;
    }

  if (string == NULL)
    {
      rv = g_string_chunk_insert (message->str_pool, rv_buf);
    }
  else
    {
      rv = g_string_chunk_insert (message->str_pool, string->str);
      g_string_free (string, TRUE);
    }

  /* Register the returned string, for future decompressions. */
  for (i = 0; i < num_offsets; i++)
    {
      g_hash_table_insert (message->offset_to_str,
			   GUINT_TO_POINTER (buffer_offsets[i]),
			   rv + str_offsets[i]);
    }

  return rv;
}

static char *
parse_char_single_string (GskBufferIterator *iterator,
		          GskDnsMessage     *message,
		          int                max_iterate)
{
  guint8 len;
  char *buf = alloca (max_iterate + 1);
  if (gsk_buffer_iterator_read (iterator, &len, 1) != 1)
    return NULL;
  max_iterate--;
  if (len == 0)
    return NULL;
  if (len > max_iterate)
    return NULL;
  if (gsk_buffer_iterator_read (iterator, buf, len) != len)
    return NULL;
  buf[len] = 0;
  return g_string_chunk_insert (message->str_pool, buf);
}

static char *
parse_char_string (GskBufferIterator *iterator,
		   GskDnsMessage     *message,
		   int                max_iterate)
{
  char *buf;
  int out_len = 0;
  g_return_val_if_fail (max_iterate > 0, NULL);
  buf = alloca (max_iterate + 1);
  while (max_iterate > 0)
    {
      guint8 len;
      if (gsk_buffer_iterator_read (iterator, &len, 1) != 1)
	break;
      max_iterate--;
      if (len == 0)
	break;
      if (len > max_iterate)
	break;
      if (gsk_buffer_iterator_read (iterator, buf + out_len, len) != len)
	return NULL;
      out_len += len;
      max_iterate -= len;
    }
  buf[out_len] = 0;
  return g_string_chunk_insert (message->str_pool, buf);
}

/* Duplicate a string, either from the stringpool of a message,
   or off the heap. */
#define ALLOCATOR_STRDUP(dns_message, str)				\
    ( ((str) == NULL)							\
       ? (NULL)								\
       : ((dns_message == NULL) 					\
	 ? (g_strdup (str))						\
	 : (g_string_chunk_insert ((dns_message)->str_pool, (str)))) )

/**
 * gsk_test_domain_name_validity:
 * @domain_name: a name which is supposed to be a domain name to test for validity.
 *
 * Verify that the domain_name meets certain required to be a hostname
 * on the internet.  In particular, all domain names MUST have <= 128 parts
 * each with <= 63 characters.
 *
 * returns: whether the domain name was valid.
 */
gboolean
gsk_test_domain_name_validity (const char *domain_name)
{
  /* Test that domain_name consists of MAX_COMPONENTS components
   * each with less than MAX_COMPONENT_LENGTH (==63 by very hardcoded limits)
   * characters.
   */
  int max_remaining = MAX_COMPONENTS;
  while (max_remaining > 0)
    {
      guint max_char = MAX_COMPONENT_LENGTH;
      while (*domain_name != '\0' && *domain_name != '.' && max_char > 0)
	{
	  domain_name++;
	  max_char--;
	}
      if (*domain_name != '\0' && *domain_name != '.')
	{
	  /* component was too long. */
	  return FALSE;
	}
      if (*domain_name == '\0')
	return TRUE;

      /* skip past the period. */
      g_assert (*domain_name == '.');
      domain_name++;
      max_remaining--;
    }

  /* too many components. */
  return FALSE;
}

/* --- building/allocating resource-records --- */
/**
 * gsk_dns_rr_new_generic:
 * @allocator: a message from which to draw the resource-records memory.
 * @owner: the owner field, which is common to all types of resource-records.
 * @ttl: the time-to-live for this record.
 *
 * Allocate a new blank record of the given type.
 *
 * The returned resource-record will probably not be valid,
 * since most records have non-optional type-specific fields
 * that the caller must initialize.
 *
 * returns: the newly allocated Resource Record.
 */ 
GskDnsResourceRecord *
gsk_dns_rr_new_generic (GskDnsMessage *allocator,
			const char    *owner,
			guint32        ttl)
{
  GskDnsResourceRecord *rv;
  if (allocator != NULL)
    rv = g_mem_chunk_alloc0 (allocator->qr_pool);
  else
    rv = g_new0 (GskDnsResourceRecord, 1);
  rv->record_class = GSK_DNS_CLASS_INTERNET;
  if (owner != NULL)
    rv->owner = ALLOCATOR_STRDUP (allocator, owner);
  rv->time_to_live = ttl;
  rv->allocator = allocator;
  return rv;
}

/**
 * gsk_dns_rr_new_a:
 * @owner: hostname whose ip address is given.
 * @ttl: the time-to-live for this record.
 *     This is maximum time for this record to be stored on a remote host,
 *     in seconds.
 * @ip_address: 4-byte IP address to contact @owner.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new A record.  It is a mapping from domain name
 * to IP address.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_a       (const char    *owner,
			guint32        ttl,
			const guint8  *ip_address,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_HOST_ADDRESS;
  memcpy (rv->rdata.a.ip_address, ip_address, 4);
  return rv;
}

/**
 * gsk_dns_rr_new_aaaa:
 * @owner: hostname whose IPv6 address is given.
 * @ttl: the time-to-live for this record.
 *     This is maximum time for this record to be stored on a remote host,
 *     in seconds.
 * @ip_address: 16-byte IP address to contact @owner.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new AAAA record.  It is a mapping from domain name
 * to IP address.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_aaaa    (const char    *owner,
			guint32        ttl,
			const guint8  *ip_address,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_HOST_ADDRESS_IPV6;
  memcpy (rv->rdata.aaaa.address, ip_address, 4);
  return rv;
}

/**
 * gsk_dns_rr_new_ns:
 * @owner: hostname or domain name whose nameserver is given by this record.
 * @ttl: the time-to-live for this record.
 *    This is maximum time for this record to be stored on a remote host, in seconds.
 * @name_server: the name of a nameserver responsible for this hostname or domainname.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new NS record.  It gives the name of a nameserver that can provide information about
 * @owner.
 *
 * If you are encountering this in response to a query,
 * you typically also get the address of a nameserver is given in an Additional
 * record of the message as an A record; otherwise, you'd probably have to look up
 * the nameserver in a separate query.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_ns      (const char    *owner,
			guint32        ttl,
			const char    *name_server,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || ! gsk_test_domain_name_validity (name_server))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_NAME_SERVER;
  rv->rdata.domain_name = ALLOCATOR_STRDUP (allocator, name_server);
  return rv;
}

/**
 * gsk_dns_rr_new_cname:
 * @owner: hostname or domain name whose canonical name is given by this record.
 * @ttl: the time-to-live for this record.
 *     This is maximum time for this record to be stored on a remote host, in seconds.
 * @canonical_name: the canonical name of @owner.  The canonical name in some sense should
 * be the most preferred name for this host, but in practice it's no different than
 * a way to alias one name for another.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new CNAME record.  It gives the canonical name of a record.
 * @owner.
 *
 * Information about the canonical host may be given in the Additional section.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_cname   (const char    *owner,
			guint32        ttl,
			const char    *canonical_name,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || ! gsk_test_domain_name_validity (canonical_name))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_CANONICAL_NAME;
  rv->rdata.domain_name = ALLOCATOR_STRDUP (allocator, canonical_name);
  return rv;
}

/**
 * gsk_dns_rr_new_ptr:
 * @owner: hostname or domain name whose PTR record is being looked up.
 *    In practice, this name is almost always in the .arpa domain.
 * @ttl: the time-to-live for this record.
 *    This is maximum time for this record to be stored on a remote host,
 *    in seconds.
 * @ptr: a hostname which @owner points to.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new PTR record.  It gives ``a pointer to another part of the domain name space''
 * according to [RFC 1034, Section 3.6]
 *
 * In practice, it is almost always used to do reverse-DNS.
 * That is because, by RFC 1034 and 1035, if you have an IP address aa.bb.cc.dd,
 * then looking up a PTR record for dd.cc.bb.aa.IN-ADDR.ARPA
 * should give the name for the host.
 *
 * Of course, reverse DNS is a bit flaky, and is principally for debugging information.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_ptr     (const char    *owner,
			guint32        ttl,
			const char    *ptr,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || !gsk_test_domain_name_validity (ptr))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_POINTER;
  rv->rdata.domain_name = ALLOCATOR_STRDUP (allocator, ptr);
  return rv;
}

/**
 * gsk_dns_rr_new_mx:
 * @owner: hostname or domain name whose PTR record is being looked up.
 * @ttl: the time-to-live for this record.
 * @preference: ???
 * @mail_host: host responsible for mail for this domain.
 * @allocator: an optional message to get memory from; this can prevent you
 *
 * Allocate a mail-exchange record.
 * This gives a hostname which is responsible for main for the @owner
 * of this record.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_mx      (const char    *owner,
			guint32        ttl,
			int            preference,
			const char    *mail_host,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || !gsk_test_domain_name_validity (mail_host))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_MAIL_EXCHANGE;
  rv->rdata.mx.preference_value = preference;
  rv->rdata.mx.mail_exchange_host_name = ALLOCATOR_STRDUP (allocator,
							   mail_host);
  return rv;
}

/**
 * gsk_dns_rr_new_hinfo:
 * @owner: hostname or domain name whose PTR record is being looked up.
 *    In practice, this name is almost always in the .arpa domain.
 * @ttl: the time-to-live for this record.
 *    This is maximum time for this record to be stored on a remote host,
 *    in seconds.
 * @cpu: CPU name for the host.
 * @os: OS for the host.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new HINFO record.  It ``identifies the CPU and OS used by a host'',
 * see [RFC 1034, Section 3.6]
 *
 * In practice, it is never used.
 * It is provided for completeness, and also for experimental use.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_hinfo   (const char    *owner,
			guint32        ttl,
			const char    *cpu,
			const char    *os,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || !gsk_test_domain_name_validity (cpu)
   || !gsk_test_domain_name_validity (os))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_HOST_INFO;
  rv->rdata.hinfo.cpu = ALLOCATOR_STRDUP (allocator, cpu);
  rv->rdata.hinfo.os = ALLOCATOR_STRDUP (allocator, os);
  return rv;
}

/**
 * gsk_dns_rr_new_soa:
 * @owner: hostname or domain name whose authority is being stated.
 * @ttl: the time-to-live for this record.
 *    This is maximum time for this record to be stored on a remote host, in seconds.
 * @mname:
 * The domain-name of the name server that was the
 * original or primary source of data for this zone.
 * @rname:
 * A domain-name which specifies the mailbox of the
 * person responsible for this zone.
 * @serial:
 * The unsigned 32 bit version number of the original copy
 * of the zone.  Zone transfers preserve this value.  This
 * value wraps and should be compared using sequence space
 * arithmetic.
 * @refresh_time:
 * A 32 bit time interval before the zone should be
 * refreshed.
 * @retry_time:
 * A 32 bit time interval that should elapse before a
 * failed refresh should be retried.
 * @expire_time:
 * A 32 bit time value that specifies the upper limit on
 * the time interval that can elapse before the zone is no
 * longer authoritative.
 * @minimum_time:
 * The unsigned 32 bit minimum TTL field that should be
 * exported with any RR from this zone.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new SOA record.  It ``identifies the start of a zone of authority'',
 * see [RFC 1034, Section 3.6]

 * [The field descriptions come from RFC 1035, section 3.3.13]
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_soa     (const char    *owner,
			guint32        ttl,
			const char    *mname,
			const char    *rname,
			guint32        serial,
			guint32        refresh_time,
			guint32        retry_time,
			guint32        expire_time,
			guint32        minimum_time,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || !gsk_test_domain_name_validity (mname)
   || !gsk_test_domain_name_validity (rname))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_START_OF_AUTHORITY;
  rv->rdata.soa.mname = ALLOCATOR_STRDUP (allocator, mname);
  rv->rdata.soa.rname = ALLOCATOR_STRDUP (allocator, rname);
  rv->rdata.soa.serial = serial;
  rv->rdata.soa.refresh_time = refresh_time;
  rv->rdata.soa.retry_time = retry_time;
  rv->rdata.soa.expire_time = expire_time;
  rv->rdata.soa.minimum_time = minimum_time;
  return rv;
}

/**
 * gsk_dns_rr_new_txt:
 * @owner: hostname or domain name whose authority is being stated.
 * @ttl: the time-to-live for this record.
 *    This is maximum time for this record to be stored on a remote host, in seconds.
 * @text: text about the owner.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * TXT RRs are used to hold descriptive text.  The semantics of the text
 * depends on the domain where it is found.
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_txt     (const char    *owner,
			guint32        ttl,
			const char    *text,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner)
   || strlen (text) > MAX_TEXT_STRING_SIZE)
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_START_OF_AUTHORITY;
  rv->rdata.txt = ALLOCATOR_STRDUP (allocator, text);
  return rv;
}

/* queries only */

/**
 * gsk_dns_rr_new_wildcard:
 * @owner: hostname or domain name whose information is given by this record.
 * @ttl: the time-to-live for this record.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Create a new wildcard ("*") record.  This should never need to be done, I guess.
 *
 * TODO: delete this function
 *
 * returns: the newly allocated Resource Record.
 */
GskDnsResourceRecord *
gsk_dns_rr_new_wildcard(const char    *owner,
			guint          ttl,
			GskDnsMessage *allocator)
{
  GskDnsResourceRecord *rv;
  if (!gsk_test_domain_name_validity (owner))
    return NULL;
  rv = gsk_dns_rr_new_generic (allocator, owner, ttl);
  rv->type = GSK_DNS_RR_WILDCARD;
  return rv;
}

/* freeing */

/**
 * gsk_dns_rr_free:
 * @record: the record to free.
 *
 * De-allocate memory associated with a resource record.
 */
void
gsk_dns_rr_free        (GskDnsResourceRecord *record)
{
  if (record->allocator != NULL)
    {
      /* XXX: maybe deleting is viable, but more importantly
	      deal with things which *need* dynamic allocation */
      /* XXX: g_mem_chunk_free only needed for --disable-mem-pool support (see glib) */
      g_mem_chunk_free (record->allocator->qr_pool, record);
      return;
    }

  switch (record->type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      break;
    case GSK_DNS_RR_NAME_SERVER:
    case GSK_DNS_RR_CANONICAL_NAME:
    case GSK_DNS_RR_POINTER:
      g_free (record->rdata.domain_name);
      break;
    case GSK_DNS_RR_HOST_INFO:
      g_free (record->rdata.hinfo.os);
      g_free (record->rdata.hinfo.cpu);
      break;
    case GSK_DNS_RR_MAIL_EXCHANGE:
      g_free (record->rdata.mx.mail_exchange_host_name);
      break;
    case GSK_DNS_RR_START_OF_AUTHORITY:
      g_free (record->rdata.soa.mname);
      g_free (record->rdata.soa.rname);
      break;
    case GSK_DNS_RR_TEXT:
      g_free (record->rdata.txt);
      break;
    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      g_warning ("XXX: unimplemented");
      break;
    case GSK_DNS_RR_ZONE_TRANSFER:
      g_warning ("XXX: unimplemented");
      break;
    case GSK_DNS_RR_ZONE_MAILB:
      g_warning ("XXX: unimplemented");
      break;
    case GSK_DNS_RR_WILDCARD:
      break;
    default:
      g_warning ("unknown DNS record type: %d", record->type);
      break;
    }
  g_free (record->owner);
  g_free (record);
}

/* copying */
/**
 * gsk_dns_rr_copy:
 * @record: the record to copy.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the record: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Make a copy of a resource record, possibly coming from a given #GskDnsMessage's
 * allocator.
 *
 * returns: the new copy of the resource record.
 */
GskDnsResourceRecord *
gsk_dns_rr_copy        (GskDnsResourceRecord *record,
			GskDnsMessage        *allocator)
{
  switch (record->type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
      return gsk_dns_rr_new_a (record->owner,
			       record->time_to_live,
			       record->rdata.a.ip_address,
			       allocator);
    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      return gsk_dns_rr_new_aaaa (record->owner,
			          record->time_to_live,
			          record->rdata.aaaa.address,
			          allocator);
    case GSK_DNS_RR_NAME_SERVER:
      return gsk_dns_rr_new_ns (record->owner,
				record->time_to_live,
				record->rdata.domain_name,
				allocator);
    case GSK_DNS_RR_CANONICAL_NAME:
      return gsk_dns_rr_new_cname (record->owner,
				   record->time_to_live,
				   record->rdata.domain_name,
				   allocator);
    case GSK_DNS_RR_POINTER:
      return gsk_dns_rr_new_ptr (record->owner,
				 record->time_to_live,
				 record->rdata.domain_name,
				 allocator);
    case GSK_DNS_RR_HOST_INFO:
      return gsk_dns_rr_new_hinfo (record->owner,
				   record->time_to_live,
				   record->rdata.hinfo.cpu,
				   record->rdata.hinfo.os,
				   allocator);
    case GSK_DNS_RR_MAIL_EXCHANGE:
      return gsk_dns_rr_new_mx (record->owner,
				record->time_to_live,
				record->rdata.mx.preference_value,
				record->rdata.mx.mail_exchange_host_name,
				allocator);
    case GSK_DNS_RR_START_OF_AUTHORITY:
      return gsk_dns_rr_new_soa (record->owner,
				 record->time_to_live,
				 record->rdata.soa.mname,
				 record->rdata.soa.rname,
				 record->rdata.soa.serial,
				 record->rdata.soa.refresh_time,
				 record->rdata.soa.retry_time,
				 record->rdata.soa.expire_time,
				 record->rdata.soa.minimum_time,
				 allocator);
    case GSK_DNS_RR_TEXT:
      return gsk_dns_rr_new_txt (record->owner,
				 record->time_to_live,
				 record->rdata.txt,
				 allocator);
    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      g_warning ("XXX: unimplemented");
      return NULL;
    case GSK_DNS_RR_ZONE_TRANSFER:
      g_warning ("XXX: unimplemented");
      return NULL;
    case GSK_DNS_RR_ZONE_MAILB:
      g_warning ("XXX: unimplemented");
      return NULL;
    case GSK_DNS_RR_WILDCARD:
      return gsk_dns_rr_new_wildcard (record->owner,
				      record->time_to_live,
				      allocator);
    default:
      g_warning ("dns rr copy: unknown DNS record type: %d", record->type);
      return NULL;
    }
  return NULL;
}


/* --- questions --- */
/**
 * gsk_dns_question_new:
 * @query_name: the name for which information is being saught.
 * @query_type: the type of resource-record that is saught.
 * @query_class: the address namespace for the query.
 * @allocator: an optional message to get memory from; this can prevent you
 *    from having to destroy the question: it will automatically get freed as
 *    part of the #GskDnsMessage.
 *
 * Allocate a new question.
 *
 * returns: the new question.
 */
GskDnsQuestion *
gsk_dns_question_new (const char               *query_name,
		      GskDnsResourceRecordType  query_type,
		      GskDnsResourceClass       query_class,
		      GskDnsMessage            *allocator)
{
  GskDnsQuestion *question;
  if (allocator == NULL)
    question = g_new (GskDnsQuestion, 1);
  else
    question = g_mem_chunk_alloc (allocator->qr_pool);
  question->query_name = ALLOCATOR_STRDUP (allocator, query_name);
  question->query_type = query_type;
  question->query_class = query_class;
  question->allocator = allocator;
  return question;
}

/**
 * gsk_dns_question_copy:
 * @question: the question to make a copy of.
 * @allocator: an optional message to get memory from.
 *
 * Make a copy of a question, optionally drawing its memory
 * from the given message's pool.
 *
 * returns: the new question.
 */
GskDnsQuestion *
gsk_dns_question_copy(GskDnsQuestion           *question,
		      GskDnsMessage            *allocator)
{
  return gsk_dns_question_new (question->query_name,
			       question->query_type,
			       question->query_class,
			       allocator);
}

/**
 * gsk_dns_question_free:
 * @question: the question to deallocate.
 *
 * Deallocate a question (unless it was drawn from a GskDnsMessage,
 * in which case it will be destroyed automatically when
 * the message is destroyed.
 */
void
gsk_dns_question_free(GskDnsQuestion           *question)
{
  if (question->allocator != NULL)
    {
      /* XXX: g_mem_chunk_free only needed for --disable-mem-pool support (see glib) */
      g_mem_chunk_free (question->allocator->qr_pool, question);
    }
  else
    {
      g_free (question->query_name);
      g_free (question);
    }
}

/* --- parsing binary messages --- */

/* questions (RFC 1035, 4.1.2) */
static GskDnsQuestion *
parse_question (GskBufferIterator *iterator,
		GskDnsMessage     *message)
{
  GskDnsQuestion *question;
  guint16 qarray[2];
  char *name;

  name = parse_domain_name (iterator, message);

  if (gsk_buffer_iterator_read (iterator, qarray, 4) != 4)
    return NULL;
  qarray[0] = GUINT16_FROM_BE (qarray[0]);
  qarray[1] = GUINT16_FROM_BE (qarray[1]);

  question = gsk_dns_question_new (NULL, qarray[0], qarray[1], message);
  question->query_name = name;
  return question;
}

/* resource records (RR's) (RFC 1035, 4.1.3) */
static GskDnsResourceRecord *
parse_resource_record (GskBufferIterator *iterator,
		       GskDnsMessage     *message)
{
  char *owner;
  guint16 type;
  guint16 class;
  guint32 ttl;
  guint16 rdlength;
  guint8 header[10];
  GskDnsResourceRecord *rr;

  owner = parse_domain_name (iterator, message);
  if (owner == NULL)
    return NULL;

  if (gsk_buffer_iterator_read (iterator, header, 10) != 10)
    return NULL;

  type     = ((guint16)header[0] << 8)  | ((guint16)header[1] << 0);
  class    = ((guint16)header[2] << 8)  | ((guint16)header[3] << 0);
  ttl      = ((guint32)header[4] << 24) | ((guint32)header[5] << 16)
           | ((guint32)header[6] << 8)  | ((guint32)header[7] << 0);
  rdlength = ((guint16)header[8] << 8)  | ((guint16)header[9] << 0);

  rr = gsk_dns_rr_new_generic (message, owner, ttl);
  rr->type = type;
  rr->record_class = class;
  /* TODO: verify class=INTERNET */

  switch (rr->type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
      if (class == GSK_DNS_CLASS_INTERNET)
	{
	  if (rdlength != 4)
	    {
	      g_warning ("only 4 byte internet addresses are supported");
              gsk_dns_rr_free (rr);
	      return NULL;
	    }
	  if (gsk_buffer_iterator_read (iterator,
					&rr->rdata.a.ip_address, 4) != 4)
            {
              gsk_dns_rr_free (rr);
	      return NULL;
            }
	}
      else
	{
	  g_warning ("class != INTERNET not supported yet, sorry");
          gsk_dns_rr_free (rr);
	  return NULL;
	}
      break;
    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      if (class == GSK_DNS_CLASS_INTERNET)
	{
	  if (rdlength != 16)
	    {
	      g_warning ("only 16 byte internet addresses are supported");
              gsk_dns_rr_free (rr);
	      return NULL;
	    }
	  if (gsk_buffer_iterator_read (iterator,
					&rr->rdata.aaaa.address, 16) != 16)
            {
              gsk_dns_rr_free (rr);
	      return NULL;
            }
	}
      else
	{
	  g_warning ("class != INTERNET not supported yet, sorry");
          gsk_dns_rr_free (rr);
	  return NULL;
	}
      break;


    case GSK_DNS_RR_NAME_SERVER:
    case GSK_DNS_RR_CANONICAL_NAME:
    case GSK_DNS_RR_POINTER:
      /* XXX: what to do if this reads too much or too little compared
       *      to rdlen???
       */
      rr->rdata.domain_name = parse_domain_name (iterator, message);
      break;

    case GSK_DNS_RR_MAIL_EXCHANGE:
      {
	guint preference_value;
	if (gsk_buffer_iterator_read (iterator, &preference_value, 2) != 2)
	  return NULL;
	rr->rdata.mx.preference_value = GUINT16_FROM_BE (preference_value);
	rr->rdata.mx.mail_exchange_host_name
	  = parse_domain_name (iterator, message);
	if (rr->rdata.mx.mail_exchange_host_name == NULL)
          {
            gsk_dns_rr_free (rr);
	    return NULL;
          }
	break;
      }

    case GSK_DNS_RR_HOST_INFO:
      {
	guint init_offset = gsk_buffer_iterator_offset (iterator);
	guint used_len;
	rr->rdata.hinfo.cpu
	  = parse_char_single_string (iterator, message, rdlength);
	used_len
	  = gsk_buffer_iterator_offset (iterator) - init_offset;
	rr->rdata.hinfo.os
	  = parse_char_single_string (iterator, message, rdlength - used_len);
	if (rr->rdata.hinfo.cpu == NULL || rr->rdata.hinfo.os == NULL)
          {
            gsk_dns_rr_free (rr);
	    return NULL;
          }
	break;
      }

    case GSK_DNS_RR_START_OF_AUTHORITY:
      {
	guint32 intervals[5];
	guint init_offset = gsk_buffer_iterator_offset (iterator);
	guint used_len;
	/* XXX: either of these domain names could go past rdlength!!! */
	rr->rdata.soa.mname = parse_domain_name (iterator, message);
        if (rr->rdata.soa.mname == NULL)
          return NULL;
	rr->rdata.soa.rname = parse_domain_name (iterator, message);
        if (rr->rdata.soa.rname == NULL)
          return NULL;
	used_len = gsk_buffer_iterator_offset (iterator) - init_offset;
	if (rdlength < used_len + 20)
	  return NULL;
	if (gsk_buffer_iterator_read (iterator, intervals, 20) != 20)
	  return NULL;
	rr->rdata.soa.serial = GUINT32_FROM_BE (intervals[0]);
	rr->rdata.soa.refresh_time = GUINT32_FROM_BE (intervals[1]);
	rr->rdata.soa.retry_time = GUINT32_FROM_BE (intervals[2]);
	rr->rdata.soa.expire_time = GUINT32_FROM_BE (intervals[3]);
	rr->rdata.soa.minimum_time = GUINT32_FROM_BE (intervals[4]);

	g_assert(gsk_buffer_iterator_offset (iterator) - init_offset == rdlength);
      }
      break;

    case GSK_DNS_RR_TEXT:
      rr->rdata.txt = parse_char_string (iterator, message, rdlength);
      break;

    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      g_warning ("XXX: unimplemented");
      gsk_dns_rr_free (rr);
      return NULL;

    case GSK_DNS_RR_ZONE_TRANSFER:
      g_warning ("XXX: unimplemented");
      gsk_dns_rr_free (rr);
      return NULL;

    case GSK_DNS_RR_ZONE_MAILB:
      g_warning ("XXX: unimplemented");
      gsk_dns_rr_free (rr);
      return NULL;

    case GSK_DNS_RR_WILDCARD:
      break;

    default:
      g_warning ("HMM.  Don't know how to deal with RTYPE==%d", rr->type);
      gsk_dns_rr_free (rr);
      return NULL;
    }
  return rr;
}

/* parse_resource_record_list:
 *   fetch a list of GskDnsResourceRecord's into *(LIST-OUT)
 *   by scanning through ITERATOR.  COUNT
 *   resource-records are expected.
 *
 *   SECTION is used for diagnostic messages;
 *   MESSAGE is used to allocation.
 */

static gboolean
parse_resource_record_list (GskBufferIterator *iterator,
			    guint              count,
			    GSList           **list_out,
			    const char        *section,
			    GskDnsMessage     *message)
{
  g_return_val_if_fail (*list_out == NULL, FALSE);
  while (count-- != 0)
    {
      GskDnsResourceRecord *rr = parse_resource_record (iterator, message);
      if (rr == NULL)
	{
	  PARSE_FAIL (section);
	  return FALSE;
	}
      *list_out = g_slist_prepend (*list_out, rr);
    }
  *list_out = g_slist_reverse (*list_out);
  return TRUE;
}

/**
 * gsk_dns_message_new:
 * @id: the identifier used to match client requests to server responses.
 * @is_request: whether the message is a client request.
 * If FALSE, then the message is a server response.
 *
 * Allocate a new blank GskDnsMessage.
 *
 * returns: the new message.
 */
GskDnsMessage *
gsk_dns_message_new (guint16            id,
		     gboolean           is_request)
{
  GskDnsMessage *message = gsk_dns_message_alloc ();
  message->id = id;
  message->is_query = is_request ? 1 : 0;
  return message;
}

static GskDnsMessage *
gsk_dns_parse_buffer_internal (GskBuffer    *buffer,
			       guint        *num_bytes_parsed)
{
  guint16 header[6];
  guint i;
  guint question_count;
  guint answer_count;
  guint auth_count;
  guint addl_count;
  GskDnsMessage *message = NULL;
  GskBufferIterator iterator;

  gsk_buffer_iterator_construct (&iterator, buffer);
  if (gsk_buffer_iterator_read (&iterator, header, 12) != 12)
    return NULL;

  for (i = 0; i < 6; i++)
    header[i] = GUINT16_FROM_BE (header[i]);

  message = gsk_dns_message_alloc ();


  message->id                  =  header[0];
  message->is_query            = (header[1] & (1<<15)) ? 0 : 1;
  message->is_authoritative    = (header[1] & (1<<10)) ? 1 : 0;
  message->is_truncated        = (header[1] & (1<<9))  ? 1 : 0;
  message->recursion_desired   = (header[1] & (1<<8))  ? 1 : 0;
  message->recursion_available = (header[1] & (1<<7))  ? 1 : 0;
  message->error_code          = (header[1] & 0x000f) >> 0;
  question_count               =  header[2];
  answer_count                 =  header[3];
  auth_count                   =  header[4];
  addl_count                   =  header[5];
  message->offset_to_str       = g_hash_table_new (NULL, NULL);

  /* question section */
  for (i = 0; i < question_count; i++)
    {
      GskDnsQuestion *question = parse_question (&iterator, message);
      if (question == NULL)
	{
	  PARSE_FAIL ("question section");
	  goto fail;
	}
      message->questions = g_slist_prepend (message->questions, question);
    }
  message->questions = g_slist_reverse (message->questions);

  /* the other three sections are the same: a list of resource-records */
  if (!parse_resource_record_list (&iterator, answer_count,
				   &message->answers, "answer", message))
    goto fail;
  if (!parse_resource_record_list (&iterator, auth_count,
				   &message->authority, "authority", message))
    goto fail;
  if (!parse_resource_record_list (&iterator, addl_count,
				   &message->additional, "additional", message))
    goto fail;

  g_assert (g_slist_length (message->questions) == question_count);
  g_assert (g_slist_length (message->answers) == answer_count);
  g_assert (g_slist_length (message->authority) == auth_count);
  g_assert (g_slist_length (message->additional) == addl_count);

  if (num_bytes_parsed != NULL)
    *num_bytes_parsed = gsk_buffer_iterator_offset (&iterator);
  return message;

fail:
  if (message != NULL)
    gsk_dns_message_unref (message);
  return NULL;
}

/**
 * gsk_dns_message_parse_buffer:
 * @buffer: the buffer to parse to get a binary DNS message.
 *
 * Parse a GskDnsMessage from a buffer, removing the binary data
 * from the buffer.
 *
 * returns: the new DNS message, or NULL if a parse error occurs.
 */
GskDnsMessage *gsk_dns_message_parse_buffer  (GskBuffer    *buffer)
{
  guint len;
  GskDnsMessage *rv = gsk_dns_parse_buffer_internal (buffer, &len);
  if (rv == NULL)
    return rv;
  gsk_buffer_discard (buffer, len);
  return rv;
}

/**
 * gsk_dns_message_parse_data:
 * @data: binary data to parse into a DNS message.
 * @length: length of @data in bytes.
 * @bytes_used_out: number of bytes of @data actually used to make the
 *   returned message, or NULL if you don't care.
 *
 * Parse a GskDnsMessage from a buffer.
 *
 * returns: the new DNS message, or NULL if a parse error occurs.
 */
GskDnsMessage *gsk_dns_message_parse_data    (const guint8 *data,
				              guint         length,
				              guint        *bytes_used_out)
{
  GskBuffer buffer;
  GskDnsMessage *message;
  guint len;
  gsk_buffer_construct (&buffer);
  gsk_buffer_append_foreign (&buffer, data, length, NULL, NULL);
  message = gsk_dns_parse_buffer_internal (&buffer, &len);
  gsk_buffer_destruct (&buffer);
  if (message == NULL)
    return NULL;
  if (bytes_used_out != NULL)
    *bytes_used_out = len;
  return message;
}

/* --- writing binary messages --- */
typedef struct _SerializeInfo SerializeInfo;
struct _SerializeInfo
{
  gboolean    compress;
  GHashTable *str_to_offset;
  GskBuffer  *buffer;
  gint        init_buffer_size;
};

static void compress_string (SerializeInfo *ser_info,
			     const char    *str)
{
  GHashTable *offsets = ser_info->str_to_offset;
  const char *at = str;
  const char *component = at;
  guint offset = 0;

  while (*component != 0)
    {
      const char *end_component;
      guint len;
      guint buf_offset;
      offset = GPOINTER_TO_UINT (g_hash_table_lookup (offsets, component));
      if (offset != 0)
	break;

      buf_offset = ser_info->buffer->size - ser_info->init_buffer_size;
      if (buf_offset <= MAX_OFFSET)
	g_hash_table_insert (ser_info->str_to_offset,
			     (gpointer) component,
			     GUINT_TO_POINTER (buf_offset));

      end_component = strchr (component, '.');
      if (end_component != NULL)
	len = end_component - component;
      else
	len = strlen (component);

      /* Skip extraneous `.'s */
      if (len == 0)
	{
	  component++;
	  continue;
	}

      /* Hmm, warning??? */
      if (len > MAX_COMPONENT_LENGTH)
	len = MAX_COMPONENT_LENGTH;
      gsk_buffer_append_char (ser_info->buffer, len);
      gsk_buffer_append (ser_info->buffer, component, len);
      
      if (end_component == NULL)
	{
	  component = NULL;
	  break;
	}
      else
	component = end_component + 1;
    }
  if (offset == 0)
    gsk_buffer_append_char (ser_info->buffer, 0);
  else
    {
      gsk_buffer_append_char (ser_info->buffer, (offset >> 8) | 0xc0);
      gsk_buffer_append_char (ser_info->buffer, offset & 0xff);
    }
}

static void
write_question_to_buffer (gpointer    list_data,
			  gpointer    ser_data)
{
  SerializeInfo *ser_info = ser_data;
  GskDnsQuestion *question = list_data;
  guint16 data[2];

  compress_string (ser_info, question->query_name);
  data[0] = GUINT16_TO_BE (question->query_type);
  data[1] = GUINT16_TO_BE (question->query_class);
  gsk_buffer_append (ser_info->buffer, data, 4);
}

static void
append_char_string (GskBuffer  *buffer,
		    const char *str)
{
  int len = strlen (str);
  if (len > 63)
    len = 63;
  gsk_buffer_append_char (buffer, len);
  gsk_buffer_append (buffer, str, len);
}

static void
write_rr_to_buffer (gpointer    list_data,
		    gpointer    ser_data)
{
  SerializeInfo *ser_info = ser_data;
  GskDnsResourceRecord *rr = list_data;
  GskBuffer *buffer = ser_info->buffer;
  guint16 data[5];
  GskBuffer tmp_buffer;
  gsk_buffer_construct (&tmp_buffer);
  data[0] = GUINT16_TO_BE (rr->type);
  data[1] = GUINT16_TO_BE (rr->record_class);
  data[2] = GUINT16_TO_BE (rr->time_to_live >> 16);
  data[3] = GUINT16_TO_BE ((guint16) rr->time_to_live);

  compress_string (ser_info, rr->owner);

  switch (rr->type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
      if (rr->record_class == GSK_DNS_CLASS_INTERNET)
	{
	  data[4] = GUINT16_TO_BE (4);
	  gsk_buffer_append (buffer, data, 10);
	  gsk_buffer_append (buffer, rr->rdata.a.ip_address, 4);
	}
      else
	{
	  g_warning ("cannot serialize DnsClasses beside `INTERNET'");
	  return;
	}
      break;

    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      if (rr->record_class == GSK_DNS_CLASS_INTERNET)
	{
	  data[4] = GUINT16_TO_BE (16);
	  gsk_buffer_append (buffer, data, 10);
	  gsk_buffer_append (buffer, rr->rdata.aaaa.address, 16);
	}
      else
	{
	  g_warning ("cannot serialize DnsClasses beside `INTERNET'");
	  return;
	}
      break;

    case GSK_DNS_RR_NAME_SERVER:
    case GSK_DNS_RR_CANONICAL_NAME:
    case GSK_DNS_RR_POINTER:
      {
	GskBuffer tmp_buffer;
	gint old_init_buf_size = ser_info->init_buffer_size;
	gsk_buffer_construct (&tmp_buffer);
	ser_info->buffer = &tmp_buffer;
	ser_info->init_buffer_size = old_init_buf_size
				   - buffer->size
				   - 10;
	compress_string (ser_info, rr->rdata.domain_name);
	data[4] = tmp_buffer.size;
	data[4] = GUINT16_TO_BE (data[4]);
	gsk_buffer_append (buffer, data, 10);
	gsk_buffer_drain (buffer, &tmp_buffer);
	ser_info->buffer = buffer;
	ser_info->init_buffer_size = old_init_buf_size;
	break;
      }

    case GSK_DNS_RR_HOST_INFO:
      data[4] = SAFE_STRLEN (rr->rdata.hinfo.cpu)
	      + SAFE_STRLEN (rr->rdata.hinfo.os) + 2;
      gsk_buffer_append (buffer, data, 10);
      append_char_string (buffer, rr->rdata.hinfo.cpu);
      append_char_string (buffer, rr->rdata.hinfo.os);
      break;

    case GSK_DNS_RR_MAIL_EXCHANGE:
      {
	guint16 pref = GUINT16_TO_BE (rr->rdata.mx.preference_value);
	gint old_init_buf_size = ser_info->init_buffer_size;
	ser_info->buffer = &tmp_buffer;
	ser_info->init_buffer_size = old_init_buf_size
				   - buffer->size
				   - 10;
	gsk_buffer_append (&tmp_buffer, &pref, 2);
	compress_string (ser_info, rr->rdata.mx.mail_exchange_host_name);
	data[4] = tmp_buffer.size;
	data[4] = GUINT16_TO_BE (data[4]);
	gsk_buffer_append (buffer, data, 10);
	gsk_buffer_drain (buffer, &tmp_buffer);
	ser_info->buffer = buffer;
	ser_info->init_buffer_size = old_init_buf_size;
      }
      break;

    case GSK_DNS_RR_START_OF_AUTHORITY:
      {
	gint old_init_buf_size = ser_info->init_buffer_size;
	ser_info->buffer = &tmp_buffer;
	ser_info->init_buffer_size = old_init_buf_size
				   - buffer->size
				   - 10;

	compress_string (ser_info, rr->rdata.soa.mname);
	compress_string (ser_info, rr->rdata.soa.rname);
	{
	  guint32 int_buf[5];
	  int_buf[0] = GUINT32_TO_BE (rr->rdata.soa.serial);
	  int_buf[1] = GUINT32_TO_BE (rr->rdata.soa.refresh_time);
	  int_buf[2] = GUINT32_TO_BE (rr->rdata.soa.retry_time);
	  int_buf[3] = GUINT32_TO_BE (rr->rdata.soa.expire_time);
	  int_buf[4] = GUINT32_TO_BE (rr->rdata.soa.minimum_time);
	  gsk_buffer_append (buffer, int_buf, sizeof (int_buf));
	}

	data[4] = tmp_buffer.size;
	data[4] = GUINT16_TO_BE (data[4]);

	gsk_buffer_append (buffer, data, 10);
	gsk_buffer_drain (buffer, &tmp_buffer);
	ser_info->buffer = buffer;
	ser_info->init_buffer_size = old_init_buf_size;
	break;
      }

    case GSK_DNS_RR_TEXT:
      {
	char *text = rr->rdata.txt;
	int remaining = strlen (text);
	while (remaining > 0)
	  {
	    int to_write = MIN (remaining, 255);
	    gsk_buffer_append_char (buffer, to_write);
	    gsk_buffer_append (buffer, text, to_write);
	    remaining -= to_write;
	    text += to_write;
	  }
	break;
      }

    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      g_warning ("XXX: writing DNS WKS RR's not supported");
      data[4] = 0;
      gsk_buffer_append (buffer, data, 10);
      break;

    case GSK_DNS_RR_ZONE_TRANSFER:
      g_warning ("XXX: writing DNS AXFR RR's not supported");
      break;

    case GSK_DNS_RR_ZONE_MAILB:
      g_warning ("XXX: writing DNS MAILB RR's not supported");
      break;

    default:
      data[4] = 0;
      gsk_buffer_append (buffer, data, 10);
      break;
    }
}

/**
 * gsk_dns_message_write_buffer:
 * @message: the DNS message to serialize.
 * @buffer: where to write the message.
 * @compress: whether to use compression.  always specify TRUE!
 *
 * Create a serialized message to send in a packet, or
 * whatever transport.
 *
 * XXX:  technically, DNS is supposed to support really crappy
 * transport media, which only allow a very short message.
 * We have no real control over how long the message will be,
 * a priori, and we just ignore the problem...
 * We casually try to send whatever packet a caller wants,
 * even if it probably won't work in the transport layer.
 */
void
gsk_dns_message_write_buffer  (GskDnsMessage *message,
		               GskBuffer     *buffer,
		               gboolean       compress)
{
  SerializeInfo ser_info;

  ser_info.compress = compress;
  ser_info.buffer = buffer;
  if (compress)
    ser_info.str_to_offset = g_hash_table_new (g_str_hash, g_str_equal);
  else
    ser_info.str_to_offset = NULL;
  ser_info.init_buffer_size = buffer->size;

  {
    guint16 dns_header[6];
    guint i;
    dns_header[0] = message->id;
    dns_header[1] = (message->is_query            ? 0 : (1 << 15))
                  | (message->is_authoritative    ? (1 << 10) : 0)
		  | (message->is_truncated        ? (1 << 9)  : 0)
		  | (message->recursion_desired   ? (1 << 8)  : 0)
		  | (message->recursion_available ? (1 << 7)  : 0)
		  | (message->error_code & 0x000f);
    dns_header[2] = g_slist_length (message->questions);
    dns_header[3] = g_slist_length (message->answers);
    dns_header[4] = g_slist_length (message->authority);
    dns_header[5] = g_slist_length (message->additional);
    for (i = 0; i < 6; i++)
      dns_header[i] = GUINT16_FROM_BE (dns_header[i]);
    gsk_buffer_append (buffer, dns_header, 12);
  }
  g_slist_foreach (message->questions, write_question_to_buffer, &ser_info);
  g_slist_foreach (message->answers, write_rr_to_buffer, &ser_info);
  g_slist_foreach (message->authority, write_rr_to_buffer, &ser_info);
  g_slist_foreach (message->additional, write_rr_to_buffer, &ser_info);

  if (ser_info.str_to_offset != NULL)
    g_hash_table_destroy (ser_info.str_to_offset);
}

/**
 * gsk_dns_message_to_packet:
 * @message: a DNS message which is going to be sent out.
 *
 * Convert a DNS message into a packet.
 * Typically then the packet will be added to a packet queue.
 *
 * returns: the new packet containing the binary data.
 */
GskPacket *
gsk_dns_message_to_packet (GskDnsMessage *message)
{
  GskBuffer buffer;
  gsize size;
  gpointer slab;
  gsk_buffer_construct (&buffer);
  gsk_dns_message_write_buffer (message, &buffer, TRUE);
  size = buffer.size;
  slab = g_malloc (size);
  gsk_buffer_read (&buffer, slab, size);
  return gsk_packet_new (slab, size, (GskPacketDestroyFunc) g_free, slab);
}

/* --- adjusting GskDnsMessage's --- */
/**
 * gsk_dns_message_append_question:
 * @message: the message to affect.
 * @question: a question to add to the message.
 *
 * Add a question to the QUESTION section of a GskDnsMessage.
 * For client requests, the message usually consists of
 * just questions.  For server responses, the message will
 * have copies the questions that the message deals with.
 *
 * The question will be free'd by the message.
 */
void
gsk_dns_message_append_question (GskDnsMessage        *message,
				 GskDnsQuestion       *question)
{
  message->questions = g_slist_append (message->questions,
				       question);
}

/**
 * gsk_dns_message_append_answer:
 * @message: the message to affect.
 * @answer: a resource-record to add to the answer section of the message.
 *
 * Add an answer to the DNS message.
 * This is done by servers when they have a direct
 * answer to a question (either a real answer or a reference to
 * a nameserver which can answer it).
 *
 * @answer will be freed when @message is freed.
 */
void
gsk_dns_message_append_answer   (GskDnsMessage        *message,
				 GskDnsResourceRecord *answer)
{
  message->answers = g_slist_append (message->answers, answer);
}

/**
 * gsk_dns_message_append_auth:
 * @message: the message to affect.
 * @auth: a resource-record to add to the authority section of the message.
 *
 * Add an authority-record to the DNS message.
 * This is done by servers to indicate who
 * is in charge of certain names and caching information.
 * 
 * @auth will be freed when @message is freed.
 */
void
gsk_dns_message_append_auth     (GskDnsMessage        *message,
				 GskDnsResourceRecord *auth)
{
  message->authority = g_slist_append (message->authority, auth);
}

/**
 * gsk_dns_message_append_addl:
 * @message: the message to affect.
 * @addl: a resource-record to add to the additional section of the message.
 *
 * Add an additional record to the DNS message.
 * This may be done by servers as a courtesy and optimization.
 * The most common use is to give the numeric IP address 
 * when a nameserver referenced in the answers section.
 * 
 * @auth will be freed when @message is freed.
 */
void
gsk_dns_message_append_addl     (GskDnsMessage        *message,
				 GskDnsResourceRecord *addl)
{
  message->additional = g_slist_append (message->additional, addl);
}

/**
 * gsk_dns_message_remove_question:
 * @message: a DNS message.
 * @question: a question in the message.
 *
 * Remove the question from the message's list,
 * and delete the question.
 */
void
gsk_dns_message_remove_question (GskDnsMessage        *message,
				 GskDnsQuestion       *question)
{
  g_return_if_fail (g_slist_find (message->questions, question) != NULL);
  message->questions = g_slist_remove (message->questions, question);
  gsk_dns_question_free (question);
}

/**
 * gsk_dns_message_remove_answer:
 * @message: a DNS message.
 * @answer: an answer in the message.
 *
 * Remove the record from the message's answer list.
 * This frees the record.
 */
void
gsk_dns_message_remove_answer   (GskDnsMessage        *message,
				 GskDnsResourceRecord *answer)
{
  g_return_if_fail (g_slist_find (message->answers, answer) != NULL);
  message->answers = g_slist_remove (message->answers, answer);
  gsk_dns_rr_free (answer);
}

/**
 * gsk_dns_message_remove_auth:
 * @message: a DNS message.
 * @auth: an authority record in the message.
 *
 * Remove the record from the message's authority list.
 * This frees the record.
 */
void
gsk_dns_message_remove_auth     (GskDnsMessage        *message,
				 GskDnsResourceRecord *auth)
{
  g_return_if_fail (g_slist_find (message->authority, auth) != NULL);
  message->authority = g_slist_remove (message->authority, auth);
  gsk_dns_rr_free (auth);
}

/**
 * gsk_dns_message_remove_addl:
 * @message: a DNS message.
 * @addl: an additional record in the message.
 *
 * Remove the record from the message's additional list.
 * This frees the record.
 */
void
gsk_dns_message_remove_addl     (GskDnsMessage        *message,
				 GskDnsResourceRecord *addl)
{
  g_return_if_fail (g_slist_find (message->additional, addl) != NULL);
  message->additional = g_slist_remove (message->additional, addl);
  gsk_dns_rr_free (addl);
}

/* --- refcounting messages --- */
/**
 * gsk_dns_message_unref:
 * @message: the message to stop referencing.
 *
 * Decrease the reference-count on the message.
 * The message will be destroyed once its ref-count
 * gets to 0.
 */
void
gsk_dns_message_unref           (GskDnsMessage        *message)
{
  g_return_if_fail (message->ref_count > 0);
  --(message->ref_count);
  if (message->ref_count == 0)
    {
      g_slist_foreach (message->questions, (GFunc) gsk_dns_question_free, NULL);
      g_slist_free (message->questions);
      g_slist_foreach (message->answers, (GFunc) gsk_dns_rr_free, NULL);
      g_slist_free (message->answers);
      g_slist_foreach (message->authority, (GFunc) gsk_dns_rr_free, NULL);
      g_slist_free (message->authority);
      g_slist_foreach (message->additional, (GFunc) gsk_dns_rr_free, NULL);
      g_slist_free (message->additional);
      if (message->offset_to_str != NULL)
	g_hash_table_destroy (message->offset_to_str);
      gsk_dns_message_free (message);
    }
}

/**
 * gsk_dns_message_ref:
 * @message: the message to add a reference to.
 *
 * Increase the reference count on the message by one.
 * The message will not be destroyed until its ref-count
 * gets to 0.
 */
void
gsk_dns_message_ref             (GskDnsMessage        *message)
{
  g_return_if_fail (message->ref_count > 0);
  ++(message->ref_count);
}

/* --- parsing text resource-records (RFC 1034, 3.6.1) --- */
/**
 * gsk_dns_parse_ip_address:
 * @pat: pointer which starts at a numeric IP address.  *@pat
 * will be updated to past the IP address.
 * @ip_addr_out: the 4-byte IP address.
 *
 * Parse a numeric IP address, in the standard fashion (RFC 1034, 3.6.1).
 *
 * returns: whether the address was parsed successfully.
 */
gboolean
gsk_dns_parse_ip_address (const char **pat,
		          guint8      *ip_addr_out)
{
  /* XXX: clean this one  up */
  const char *at = *pat;
  char *endp;
  ip_addr_out[0] = (guint) strtoul (at, &endp, 10);
  if (at == endp || *endp != '.')
    return FALSE;
  at = endp + 1;
  ip_addr_out[1] = (guint) strtoul (at, &endp, 10);
  if (at == endp || *endp != '.')
    return FALSE;
  at = endp + 1;
  ip_addr_out[2] = (guint) strtoul (at, &endp, 10);
  if (at == endp || *endp != '.')
    return FALSE;
  at = endp + 1;
  ip_addr_out[3] = (guint) strtoul (at, &endp, 10);
  if (at == endp)
    return FALSE;
  GSK_SKIP_WHITESPACE (endp);
  *pat = endp;
  return TRUE;
}

/**
 * gsk_dns_parse_ipv6_address:
 * @pat: pointer which starts at a numeric IP address.  *@pat
 * will be updated to past the IP address.
 * @ip_addr_out: the 4-byte IP address.
 *
 * Parse a numeric IP address, in the standard fashion (RFC 1034, 3.6.1).
 *
 * returns: whether the address was parsed successfully.
 */
gboolean
gsk_dns_parse_ipv6_address (const char **pat,
		            guint8      *ip_addr_out)
{
  guint i;
  const char *at = *pat;
  char *endp;
  for (i = 0; i < 7; i++)
    {
      guint16 word = (guint16) strtoul (at, &endp, 16);
      ip_addr_out[2 * i + 0] = word >> 8;
      ip_addr_out[2 * i + 1] = word & 0xff;
      if (at == endp || *endp != ':')
        return FALSE;
      at = endp + 1;
    }
  {
    guint16 word = (guint16) strtoul (at, &endp, 16);
    ip_addr_out[2 * i + 0] = word >> 8;
    ip_addr_out[2 * i + 1] = word & 0xff;
    if (at == endp)
      return FALSE;
  }
  GSK_SKIP_WHITESPACE (endp);
  *pat = endp;
  return TRUE;
}


static gboolean
parse_rr_type (const char               *type_str,
               GskDnsResourceRecordType *type_out)
{
#define TEST_STRING(str, rv)			\
  G_STMT_START{					\
    if (strcasecmp (type_str, str) == 0)	\
      {						\
	*type_out = rv;				\
	return TRUE;				\
      }						\
  }G_STMT_END
  switch (*type_str)
    {
    case 'a': case 'A':
      TEST_STRING("a", GSK_DNS_RR_HOST_ADDRESS);
      TEST_STRING("aaaa", GSK_DNS_RR_HOST_ADDRESS_IPV6);
      TEST_STRING("axfr", GSK_DNS_RR_ZONE_TRANSFER);
      break;

    case 'n': case 'N':
      TEST_STRING ("ns", GSK_DNS_RR_NAME_SERVER);
      break;

    case 'h': case 'H':
      TEST_STRING ("hinfo", GSK_DNS_RR_HOST_INFO);
      break;

    case 'c': case 'C':
      TEST_STRING ("cname", GSK_DNS_RR_CANONICAL_NAME);
      break;

    case 'm': case 'M':
      TEST_STRING ("mx", GSK_DNS_RR_MAIL_EXCHANGE);
      break;

    case 'p': case 'P':
      TEST_STRING ("ptr", GSK_DNS_RR_POINTER);
      break;

    case 's': case 'S':
      TEST_STRING ("soa", GSK_DNS_RR_START_OF_AUTHORITY);
      break;

    case 'w': case 'W':
      TEST_STRING ("wks", GSK_DNS_RR_WELL_KNOWN_SERVICE);
      break;

    case '*':
      TEST_STRING ("*", GSK_DNS_RR_WILDCARD);
      break;
    }
#undef TEST_STRING
  /*gsk_g_debug ("Unknown RRTYPE parsed from text: %s", type_str);*/
  return FALSE;
}

static gboolean
parse_rr_class (const char          *class_str,
                GskDnsResourceClass *class_out)
{
#define TEST_STRING(second_chars, rv)		\
  G_STMT_START{					\
    if (class_str[1] == second_chars[0] 	\
     || class_str[1] == second_chars[1])	\
      {						\
	*class_out = rv;			\
	return TRUE;				\
      }						\
  }G_STMT_END
  switch (*class_str)
    {
    case 'i': case 'I':
      TEST_STRING ("nN", GSK_DNS_CLASS_INTERNET);
      break;
    case 'c': case 'C':
      TEST_STRING ("hH", GSK_DNS_CLASS_CHAOS);
      break;
    case 'h': case 'H':
      TEST_STRING ("sS", GSK_DNS_CLASS_HESIOD);
      break;
    }
#undef TEST_STRING
  /*gsk_g_debug ("Unknown RRCLASS parsed from text: %s", class_str);*/
  return FALSE;
}

/**
 * gsk_dns_rr_text_parse:
 * @record: the record as a string.
 * @last_owner: the last parsed resource-record's "owner"
 * field.  Spaces at the beginning of the line are
 * taken to represent the same owner.
 * @origin: the origin server for this record.
 * @err_msg: optional place to put an allocated error message.
 * @allocator: a message from which to draw the resource-records memory.
 *
 * Parse a text representation of a message,
 * as from a zone file or 'dig'.
 *
 * returns: the new resource-record, or NULL if an error occurred.
 */
GskDnsResourceRecord *
gsk_dns_rr_text_parse     (const char       *record,
			   const char       *last_owner,
			   const char       *origin,
			   char            **err_msg,
			   GskDnsMessage    *allocator)
{
  gboolean start_with_space;
  const char *at = record;
  const char *endp;
  const char *owner = NULL;
  GskDnsResourceRecord *rr = NULL;
  guint ttl;
  char *rrtype_str;
  char *rclass_str;
  GskDnsResourceRecordType rtype;
  GskDnsResourceClass rclass;

  g_return_val_if_fail (origin != NULL, NULL);

  if (err_msg)
    *err_msg = NULL;

  if (*at == 0)
    return NULL;

  start_with_space = isspace (*at);
  GSK_SKIP_WHITESPACE (at);

  /* XXX: is this a good comment character? */
  if (*at == ';')
    return NULL;

  if (!start_with_space)
    {
      endp = at;
      GSK_SKIP_NONWHITESPACE (endp);
      if (endp == at + 1 && *at == '.')
	{
	  owner = origin;
	  if (origin == NULL)
	    {
	      if (err_msg != NULL)
		*err_msg = g_strdup ("@ specified as RR-owner, but no origin");
	      return NULL;
	    }
	}
      else
	{
	  SUFFIXED_CUT_ONTO_STACK (owner, at, endp, origin);
	}
      at = endp;
      GSK_SKIP_WHITESPACE (at);
    }
  else if (last_owner == NULL)
    {
      if (err_msg != NULL)
	*err_msg = g_strdup ("line began with SPACE but last-owner was NULL");
      goto fail;
    }
  else
    {
      owner = last_owner;
    }

  /* TTL */
  if (!isdigit (*at))
    {
      if (err_msg != NULL)
	*err_msg = g_strdup ("TTL was not a number");
      goto fail;
    }
  ttl = parse_into_seconds (at, (char **) &endp);
  if (at == endp)
    {
      if (err_msg != NULL)
	*err_msg = g_strdup ("TTL was not a number");
      goto fail;
    }
  at = endp;
  GSK_SKIP_WHITESPACE (at);

  /* Type (MX, A, etc.) */
  endp = at;
  GSK_SKIP_NONWHITESPACE (endp);
  if (endp == at || *endp == 0)
    {
      if (err_msg != NULL)
        *err_msg = g_strdup ("end-of-record before Class/Type");
      goto fail;
    }
  CUT_ONTO_STACK (rrtype_str, at, endp);

  /* Class */
  at = endp;
  GSK_SKIP_WHITESPACE (at);
  endp = at;
  GSK_SKIP_NONWHITESPACE (endp);
  if (endp == at)
    {
      if (err_msg != NULL)
	*err_msg = g_strdup ("end-of-record before Class/Type");
      goto fail;
    }
  CUT_ONTO_STACK (rclass_str, at, endp);

  {
    gboolean rrtype_succeeded, rclass_succeded;

    rrtype_succeeded = parse_rr_type (rrtype_str, &rtype);
    rclass_succeded = parse_rr_class (rclass_str, &rclass);
#if SUPPORT_BIND_ENHANCEMENTS
    /* BIND allows RRCLASS AND RRTYPE to be backward. */
    if (!rrtype_succeeded && !rclass_succeded)
      {
	char *tmp = rrtype_str;
	rrtype_str = rclass_str;
	rclass_str = tmp;
	rrtype_succeeded = parse_rr_type (rrtype_str, &rtype);
	rclass_succeded = parse_rr_class (rclass_str, &rclass);
      }
#endif
    if (!rrtype_succeeded)
      {
	if (err_msg != NULL)
	  *err_msg = g_strdup ("unknown RTYPE");
	goto fail;
      }
    if (!rclass_succeded)
      {
	if (err_msg != NULL)
	  *err_msg = g_strdup ("unknown RCLASS");
	goto fail;
      }
  }
  at = endp;
  GSK_SKIP_WHITESPACE (at);

  /* Rdata */
  switch (rtype)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
      if (rclass == GSK_DNS_CLASS_INTERNET)
	{
	  guint8 ip_address[4];
	  if (!gsk_dns_parse_ip_address (&at, ip_address))
	    {
	      if (err_msg != NULL)
		*err_msg = g_strdup ("error parsing IP address for A record");
	      goto fail;
	    }
	  rr = gsk_dns_rr_new_a (owner, ttl, ip_address, allocator);
	}
      else
	{
	  /* TODO: maybe add a way to register more classes externally? */
	  if (err_msg != NULL)
	    *err_msg = g_strdup ("Only `IN' class `A' records can be parsed");
	  goto fail;
	}
      break;

    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      if (rclass == GSK_DNS_CLASS_INTERNET)
	{
	  guint8 address[16];
	  if (!gsk_dns_parse_ipv6_address (&at, address))
	    {
	      if (err_msg != NULL)
		*err_msg = g_strdup ("error parsing IP address for AAAA record");
	      goto fail;
	    }
	  rr = gsk_dns_rr_new_aaaa (owner, ttl, address, allocator);
	}
      else
	{
	  /* TODO: maybe add a way to register more classes externally? */
	  if (err_msg != NULL)
	    *err_msg = g_strdup ("Only `IN' class `AAAA' records can be parsed");
	  goto fail;
	}
      break;

    case GSK_DNS_RR_NAME_SERVER:
    case GSK_DNS_RR_CANONICAL_NAME:
    case GSK_DNS_RR_POINTER:
      endp = at;
      GSK_SKIP_NONWHITESPACE (endp);
      {
	char *name;

	SUFFIXED_CUT_ONTO_STACK (name, at, endp, origin);
	if (rtype == GSK_DNS_RR_NAME_SERVER)
	  rr = gsk_dns_rr_new_ns (owner, ttl, name, allocator);
	else if (rtype == GSK_DNS_RR_CANONICAL_NAME)
	  rr = gsk_dns_rr_new_cname (owner, ttl, name, allocator);
	else if (rtype == GSK_DNS_RR_POINTER)
	  rr = gsk_dns_rr_new_ptr (owner, ttl, name, allocator);
      }
      break;

    /* A `HINFO' record: identifies the CPU and OS used by a host */
    case GSK_DNS_RR_HOST_INFO:
      /* TODO: fix this */
      if (err_msg != NULL)
	*err_msg = g_strdup ("HINFO text messages not supported");
      goto fail;

    /* A `MX' record */
    case GSK_DNS_RR_MAIL_EXCHANGE:
      {
	/* parse a preference, then a host */
	int preference = (int) strtol (at, (char **) &endp, 10);
	char *name;
	if (at == endp)
	  {
	    if (err_msg != NULL)
	      *err_msg = g_strdup ("first field of MX wasn't an int `pref'");
	    goto fail;
	  }
	GSK_SKIP_WHITESPACE (endp);
	at = endp;
	GSK_SKIP_NONWHITESPACE (endp);
	if (at == endp)
	  {
	    if (err_msg != NULL)
	      *err_msg = g_strdup ("no name (last field) in MX record");
	    goto fail;
	  }
	SUFFIXED_CUT_ONTO_STACK (name, at, endp, origin);
	rr = gsk_dns_rr_new_mx (owner, ttl, preference, name, allocator);
	break;
      }

    /* A `SOA' record:  identifies the start of a zone of authority [???] */
    case GSK_DNS_RR_START_OF_AUTHORITY:
      {
	/* The fields in an SOA entry */
	guint32 int_array[5];
	char *mname;
	char *rname;
	guint i;
	endp = at;
	GSK_SKIP_NONWHITESPACE (endp);
	SUFFIXED_CUT_ONTO_STACK (mname, at, endp, origin);
	at = endp;
	GSK_SKIP_WHITESPACE (at);
	endp = at;
	GSK_SKIP_NONWHITESPACE (endp);
	SUFFIXED_CUT_ONTO_STACK (rname, at, endp, origin);
	at = endp;
	GSK_SKIP_WHITESPACE (at);

	/* we ignore a left-paren */
	if (*at == '(')
	  {
	    at++;
	    GSK_SKIP_WHITESPACE (at);
	  }

	/* parse the 5 int32s into int_array */
	int_array[0] = (int) strtol (at, (char **) &endp, 10);
	if (at == endp)
	  {
	    if (err_msg != NULL)
	      *err_msg = g_strdup_printf ("error parsing int from SOA record");
	    goto fail;
	  }
	GSK_SKIP_WHITESPACE (endp);
	at = endp;
	for (i = 1; i < 5; i++)
	  {
	    int_array[i] = (int) parse_into_seconds (at, (char **) &endp);
	    if (at == endp)
	      {
		if (err_msg != NULL)
		  *err_msg
		    = g_strdup_printf ("error parsing time %d from SOA at %s",
				       i, endp);
		goto fail;
	      }
	    GSK_SKIP_WHITESPACE (endp);
	    at = endp;
	  }

	/* map int_array to the soa member */
	rr = gsk_dns_rr_new_soa (owner, ttl, mname, rname,
				 int_array[0], int_array[1],
				 int_array[2], int_array[3],
				 int_array[4], allocator);
      }
      break;

    /* A `TXT' record:  miscellaneous text */
    case GSK_DNS_RR_TEXT:
      rr = gsk_dns_rr_new_txt (owner, ttl, at, allocator);
      break;

    /* A `WKS' record:  description of a well-known service */
    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      if (err_msg != NULL)
	*err_msg = g_strdup ("TODO: can't deal with parsing WKS text fields yet");
      break;

    default:
      if (err_msg != NULL)
	*err_msg = g_strdup_printf ("could not convert RTYPE %d to text", rtype);
      goto fail;
    }

  return rr;
fail:
  if (rr != NULL)
    gsk_dns_rr_free (rr);
  return NULL;
}

static void
append_spaces (GString *str, int n)
{
  char *spaces = alloca (n + 1);
  memset (spaces, ' ', n);
  spaces[n] = 0;
  g_string_append (str, spaces);
}

static const char *
gsk_resource_record_type_to_string (GskDnsResourceRecordType type)
{
  switch (type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:       return "A";
    case GSK_DNS_RR_HOST_ADDRESS_IPV6:  return "AAAA";
    case GSK_DNS_RR_NAME_SERVER:        return "NS";
    case GSK_DNS_RR_CANONICAL_NAME:     return "CNAME";
    case GSK_DNS_RR_HOST_INFO:          return "HINFO";
    case GSK_DNS_RR_MAIL_EXCHANGE:      return "MX";
    case GSK_DNS_RR_POINTER:            return "PTR";
    case GSK_DNS_RR_START_OF_AUTHORITY: return "SOA";
    case GSK_DNS_RR_TEXT:               return "TXT";
    case GSK_DNS_RR_WELL_KNOWN_SERVICE: return "WKS";
    case GSK_DNS_RR_ZONE_TRANSFER:      return "AXFR";
    case GSK_DNS_RR_ZONE_MAILB:         return "MAILB";
    case GSK_DNS_RR_WILDCARD:           return "*";
    default:                            return "UNKNOWN-RTYPE";
    }
}

static const char *
gsk_resource_record_class_to_string (GskDnsResourceClass class)
{
  switch (class)
    {
    case GSK_DNS_CLASS_INTERNET:        return "IN";
    case GSK_DNS_CLASS_CHAOS:           return "CH";
    case GSK_DNS_CLASS_HESIOD:          return "HS";
    case GSK_DNS_CLASS_WILDCARD:        return "*";
    default:                            return "UNKNOWN-RCLASS";
    }
}

/**
 * gsk_dns_rr_text_to_str:
 * @rr: the resource record to convert to a text string.
 * @last_owner: if non-NULL, then this should be the owner
 * of the last resource record printed out.
 * For human-readability, zone files merely indent subsequent
 * lines that refer to the same owner.
 *
 * Convert the resource record into a format which would be appropriate
 * for a zone file.
 *
 * returns: a newly allocated string.  The text of the resource-record.
 */
char *
gsk_dns_rr_text_to_str(GskDnsResourceRecord *rr,
		       const char           *last_owner)
{
  GString *rv = g_string_new ("");
  if (last_owner == NULL
   || strcmp (last_owner, rr->owner) != 0)
    {
      int owner_len = strlen (rr->owner);
      g_string_append (rv, rr->owner);
      if (owner_len <= COLUMN_OWNER_WIDTH - 1)
	append_spaces (rv, COLUMN_OWNER_WIDTH - owner_len);
      else
	g_string_append_c (rv, ' ');
    }
  else
    {
      /* Append COLUMN_OWNER_WIDTH spaces. */
      append_spaces (rv, COLUMN_OWNER_WIDTH);
    }

  /* append ttl */
  g_string_sprintfa (rv, "%-7d ", rr->time_to_live);

  /* append type */
  g_string_append (rv, gsk_resource_record_type_to_string (rr->type));
  g_string_append_c (rv, ' ');

  /* append class */
  g_string_append (rv, gsk_resource_record_class_to_string (rr->record_class));
  g_string_append_c (rv, ' ');

  /* append rdata */
  switch (rr->type)
    {
    case GSK_DNS_RR_HOST_ADDRESS:
      if (rr->record_class == GSK_DNS_CLASS_INTERNET)
	{
	  g_string_sprintfa (rv, "%d.%d.%d.%d",
			     rr->rdata.a.ip_address[0],
			     rr->rdata.a.ip_address[1],
			     rr->rdata.a.ip_address[2],
			     rr->rdata.a.ip_address[3]);
	}
      else
	{
	  g_string_sprintfa (rv,
		     "ERROR: cannot print non-internet (IN) class address");
	  goto fail;
	}
      break;

    case GSK_DNS_RR_HOST_ADDRESS_IPV6:
      if (rr->record_class == GSK_DNS_CLASS_INTERNET)
	{
	  g_string_sprintfa (rv, "%x:%x:%x:%x:%x:%x:%x:%x",
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[0]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[1]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[2]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[3]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[4]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[5]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[6]),
                 GUINT16_FROM_BE (((guint16*)rr->rdata.aaaa.address)[7]));
	}
      else
	{
	  g_string_sprintfa (rv,
		     "ERROR: cannot print non-internet (IN) class address");
	  goto fail;
	}
      break;

    case GSK_DNS_RR_NAME_SERVER:
    case GSK_DNS_RR_CANONICAL_NAME:
    case GSK_DNS_RR_POINTER:
      g_string_append (rv, rr->rdata.domain_name);
      break;

    case GSK_DNS_RR_HOST_INFO:
      g_string_append (rv, rr->rdata.hinfo.cpu);
      g_string_append_c (rv, ' ');
      g_string_append (rv, rr->rdata.hinfo.os);
      goto fail;

    case GSK_DNS_RR_MAIL_EXCHANGE:
      g_string_sprintfa (rv, "%d %s",
			 rr->rdata.mx.preference_value,
			 rr->rdata.mx.mail_exchange_host_name);
      break;

    /* A `SOA' record:  identifies the start of a zone of authority [???] */
    case GSK_DNS_RR_START_OF_AUTHORITY:
      g_string_sprintfa (rv, "%s %s %u %u %u %u %u",
			 rr->rdata.soa.mname,
			 rr->rdata.soa.rname,
			 rr->rdata.soa.serial,
			 rr->rdata.soa.refresh_time,
			 rr->rdata.soa.retry_time,
			 rr->rdata.soa.expire_time,
			 rr->rdata.soa.minimum_time);
      break;

    /* A `TXT' record:  miscellaneous text */
    case GSK_DNS_RR_TEXT:
      g_string_append (rv, rr->rdata.txt);
      break;

    case GSK_DNS_RR_WELL_KNOWN_SERVICE:
      g_warning ("WKS not printable yet");
      g_string_append (rv, "ERROR: cannot print WKS records yet");
      break;

    default:
      g_string_sprintfa (rv, "Unknown RTYPE %d", rr->type);
      break;
    }

  return g_string_free (rv, FALSE);

fail:
  g_string_free (rv, TRUE);
  g_warning ("error converting DNS record to ASCII");
  return NULL;
}

/**
 * gsk_dns_question_text_to_str:
 * @question: question to convert to text.
 *
 * Convert a question to a newly allocated string
 * containing the question roughly as it would be
 * printed by the unix program 'dig'.
 *
 * returns: the newly allocated string.
 */
char *
gsk_dns_question_text_to_str(GskDnsQuestion       *question)
{
  GString *rv;
  char *rv_str;
  int owner_len = strlen (question->query_name);
  const char *enum_str;

  rv = g_string_new ("");
  g_string_append (rv, question->query_name);
  if (owner_len <= COLUMN_OWNER_WIDTH - 1)
    append_spaces (rv, COLUMN_OWNER_WIDTH - owner_len);
  else
    g_string_append_c (rv, ' ');

  enum_str = gsk_resource_record_class_to_string (question->query_class);
  g_string_append (rv, enum_str);
  g_string_append_c (rv, ' ');
  enum_str = gsk_resource_record_type_to_string (question->query_type);
  g_string_append (rv, enum_str);

  rv_str = rv->str;
  g_string_free (rv, FALSE);
  return rv_str;
}

/**
 * gsk_dns_rr_text_write:
 * @rr: the resource record to write as text into the buffer.
 * @out_buffer: the buffer to which the text resource-record should be appended.
 * @last_owner: if non-NULL, then spaces will be printed instead
 * of @rr->owner, if it is the same as last_owner.
 *
 * Print a resource-record to a buffer as text.
 * This will look similar to a zone file.
 */
void
gsk_dns_rr_text_write     (GskDnsResourceRecord *rr,
			   GskBuffer            *out_buffer,
			   const char           *last_owner)
{
  char *txt = gsk_dns_rr_text_to_str (rr, last_owner);
  gsk_buffer_append_string (out_buffer, txt);
  gsk_buffer_append_char (out_buffer, '\n');
  g_free (txt);
}


/* --- debugging dump --- */
void
gsk_dns_dump_question_fp (GskDnsQuestion *question,
			  FILE           *fp)
{
  char *str = gsk_dns_question_text_to_str (question);
  fprintf (fp, "%s [%p]\n", str, question);
  g_free (str);
}


static void print_rr_to_fp (gpointer list_data, gpointer data)
{
  FILE *fp = data;
  GskDnsResourceRecord *rr = list_data;
  char *str = gsk_dns_rr_text_to_str (rr, NULL);
  fprintf (fp, "%s\n", str);
  g_free (str);
}

/**
 * gsk_dns_dump_message_fp:
 * @message: the message to print out.
 * @fp: the file to write the message out to.
 *
 * Dump a message to a FILE*, for debugging.
 * This output format is modelled after the somewhat
 * obscure unix utility 'dig'.
 */
void
gsk_dns_dump_message_fp (GskDnsMessage *message,
			 FILE          *fp)
{
  const char *error_str = "UNKNOWN ERROR";
  switch (message->error_code)
    {
    case GSK_DNS_RESPONSE_ERROR_NONE:
      error_str = "NO ERROR";
      break;
    case GSK_DNS_RESPONSE_ERROR_FORMAT:
      error_str = "FORMAT ERROR";
      break;
    case GSK_DNS_RESPONSE_ERROR_SERVER_FAILURE:
      error_str = "SERVER FAILURE";
      break;
    case GSK_DNS_RESPONSE_ERROR_NAME_ERROR:
      error_str = "NAME ERROR";
      break;
    case GSK_DNS_RESPONSE_ERROR_NOT_IMPLEMENTED:
      error_str = "NOT IMPLEMENTED ERROR";
      break;
    case GSK_DNS_RESPONSE_ERROR_REFUSED:
      error_str = "REFUSED";
      break;
    }

  fprintf (fp, "%s.  ID=%d. %s%s%s%s (%s)\n",
	   message->is_query ? "QUERY" : "RESPONSE",
	   (int) message->id,
	   message->is_authoritative ? " (AA)" : "",
	   message->is_truncated ? " (TRUNCATED)" : "",
	   message->recursion_available ? " (RECURSION AVAIL)" : "",
	   message->recursion_desired ? " (RECURSION DESIRED)" : "",
	   error_str);

  switch (message->error_code)
    {
    case GSK_DNS_RESPONSE_ERROR_REFUSED:
      fprintf (fp, "Response: ERROR: REFUSED\n");
      break;
    case GSK_DNS_RESPONSE_ERROR_FORMAT:
      fprintf (fp, "Response: ERROR: FORMAT\n");
      break;
    case GSK_DNS_RESPONSE_ERROR_SERVER_FAILURE:
      fprintf (fp, "Response: ERROR: SERVER FAILURE\n");
      break;
    case GSK_DNS_RESPONSE_ERROR_NAME_ERROR:
      fprintf (fp, "Response: ERROR: NAME ERROR\n");
      break;
    case GSK_DNS_RESPONSE_ERROR_NOT_IMPLEMENTED:
      fprintf (fp, "Response: ERROR: NOT IMPLEMENTED\n");
      break;
    case GSK_DNS_RESPONSE_ERROR_NONE:
      break;
    }

  if (message->questions != NULL)
    fprintf (fp, "\nQuestions:\n");
  g_slist_foreach (message->questions, (GFunc) gsk_dns_dump_question_fp, fp);

  if (message->answers != NULL)
    fprintf (fp, "\nAnswers:\n");
  g_slist_foreach (message->answers, print_rr_to_fp, fp);

  if (message->authority != NULL)
    fprintf (fp, "\nAuthority:\n");
  g_slist_foreach (message->authority, print_rr_to_fp, fp);

  if (message->additional != NULL)
    fprintf (fp, "\nAdditional:\n");
  g_slist_foreach (message->additional, print_rr_to_fp, fp);
}



syntax highlighted by Code2HTML, v. 0.9.1