//
// File loading code for Mini-XML, a small XML file parsing library.
//
// https://www.msweet.org/mxml
//
// Copyright © 2003-2026 by Michael R Sweet.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//

#include "mxml-private.h"


//
// Local types...
//

typedef enum _mxml_encoding_e		// Character encoding
{
  _MXML_ENCODING_UTF8,			// UTF-8
  _MXML_ENCODING_UTF16BE,		// UTF-16 Big-Endian
  _MXML_ENCODING_UTF16LE		// UTF-16 Little-Endian
} _mxml_encoding_t;

typedef struct _mxml_stringbuf_s	// String buffer
{
  char		*buffer,		// Buffer
		*bufptr;		// Pointer into buffer
  size_t	bufsize;		// Size of buffer
  bool		bufalloc;		// Allocate buffer?
} _mxml_stringbuf_t;


//
// Macro to test for a bad XML character...
//

#define mxml_bad_char(ch) ((ch) < ' ' && (ch) != '\n' && (ch) != '\r' && (ch) != '\t')


//
// Local functions...
//

static bool		mxml_add_char(mxml_options_t *options, int ch, char **ptr, char **buffer, size_t *bufsize);
static int		mxml_get_entity(mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata, _mxml_encoding_t *encoding, mxml_node_t *parent, int *line);
static int		mxml_getc(mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata, _mxml_encoding_t *encoding);
static inline int	mxml_isspace(int ch)
			{
			  return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
			}
static mxml_node_t	*mxml_load_data(mxml_node_t *top, mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata);
static int		mxml_parse_element(mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata, mxml_node_t *node, _mxml_encoding_t *encoding, int *line);
static size_t		mxml_read_cb_fd(int *fd, void *buffer, size_t bytes);
static size_t		mxml_read_cb_file(FILE *fp, void *buffer, size_t bytes);
static size_t		mxml_read_cb_string(_mxml_stringbuf_t *sb, void *buffer, size_t bytes);
static double		mxml_strtod(mxml_options_t *options, const char *buffer, char **bufptr);
static size_t		mxml_io_cb_fd(int *fd, void *buffer, size_t bytes);
static size_t		mxml_io_cb_file(FILE *fp, void *buffer, size_t bytes);
static size_t		mxml_io_cb_string(_mxml_stringbuf_t *sb, void *buffer, size_t bytes);
static int		mxml_write_node(mxml_node_t *node, mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata, int col);
static int		mxml_write_string(const char *s, mxml_io_cb_t io_cb, void *io_cbdata, bool use_entities, int col);
static int		mxml_write_ws(mxml_node_t *node, mxml_options_t *options, mxml_io_cb_t io_cb, void *io_cbdata, mxml_ws_t ws, int col);


//
// 'mxmlLoadFd()' - Load a file descriptor into an XML node tree.
//
// This function loads the file descriptor `fd` into an XML node tree.  The
// nodes in the specified file are added to the specified node `top` - if `NULL`
// the XML file MUST be well-formed with a single parent processing instruction
// node like `<?xml version="1.0"?>` at the start of the file.
//
// Load options are provides via the `options` argument.  If `NULL`, all values
// will be loaded into `MXML_TYPE_TEXT` nodes.  Use the @link mxmlOptionsNew@
// function to create options when loading XML data.
//

mxml_node_t *				// O - First node or `NULL` if the file could not be read.
mxmlLoadFd(
    mxml_node_t    *top,		// I - Top node
    mxml_options_t *options,		// I - Options
    int            fd)			// I - File descriptor to read from
{
  // Range check input...
  if (fd < 0)
    return (NULL);

  // Read the XML data...
  return (mxml_load_data(top, options, (mxml_io_cb_t)mxml_read_cb_fd, &fd));
}


//
// 'mxmlLoadFile()' - Load a file into an XML node tree.
//
// This function loads the `FILE` pointer `fp` into an XML node tree.  The
// nodes in the specified file are added to the specified node `top` - if `NULL`
// the XML file MUST be well-formed with a single parent processing instruction
// node like `<?xml version="1.0"?>` at the start of the file.
//
// Load options are provides via the `options` argument.  If `NULL`, all values
// will be loaded into `MXML_TYPE_TEXT` nodes.  Use the @link mxmlOptionsNew@
// function to create options when loading XML data.
//

mxml_node_t *				// O - First node or `NULL` if the file could not be read.
mxmlLoadFile(
    mxml_node_t    *top,		// I - Top node
    mxml_options_t *options,		// I - Options
    FILE           *fp)			// I - File to read from
{
  // Range check input...
  if (!fp)
    return (NULL);

  // Read the XML data...
  return (mxml_load_data(top, options, (mxml_io_cb_t)mxml_read_cb_file, fp));
}


//
// 'mxmlLoadFilename()' - Load a file into an XML node tree.
//
// This function loads the named file `filename` into an XML node tree.  The
// nodes in the specified file are added to the specified node `top` - if `NULL`
// the XML file MUST be well-formed with a single parent processing instruction
// node like `<?xml version="1.0"?>` at the start of the file.
//
// Load options are provides via the `options` argument.  If `NULL`, all values
// will be loaded into `MXML_TYPE_TEXT` nodes.  Use the @link mxmlOptionsNew@
// function to create options when loading XML data.
//

mxml_node_t *				// O - First node or `NULL` if the file could not be read.
mxmlLoadFilename(
    mxml_node_t    *top,		// I - Top node
    mxml_options_t *options,		// I - Options
    const char     *filename)		// I - File to read from
{
  FILE		*fp;			// File pointer
  mxml_node_t	*ret;			// Node


  // Range check input...
  if (!filename)
    return (NULL);

  // Open the file...
  if ((fp = fopen(filename, "r")) == NULL)
    return (NULL);

  // Read the XML data...
  ret = mxml_load_data(top, options, (mxml_io_cb_t)mxml_read_cb_file, fp);

  // Close the file and return...
  fclose(fp);

  return (ret);
}


//
// 'mxmlLoadIO()' - Load an XML node tree using a read callback.
//
// This function loads data into an XML node tree using a read callback.  The
// nodes in the specified file are added to the specified node `top` - if `NULL`
// the XML file MUST be well-formed with a single parent processing instruction
// node like `<?xml version="1.0"?>` at the start of the file.
//
// Load options are provides via the `options` argument.  If `NULL`, all values
// will be loaded into `MXML_TYPE_TEXT` nodes.  Use the @link mxmlOptionsNew@
// function to create options when loading XML data.
//
// The read callback function `io_cb` is called to read a number of bytes from
// the source.  The callback data pointer `io_cbdata` is passed to the read
// callback with a pointer to a buffer and the maximum number of bytes to read,
// for example:
//
// ```c
// size_t my_io_cb(void *cbdata, void *buffer, size_t bytes)
// {
//   ... copy up to "bytes" bytes into buffer ...
//   ... return the number of bytes "read" or 0 on error ...
// }
// ```
//

mxml_node_t *				// O - First node or `NULL` if the file could not be read.
mxmlLoadIO(
    mxml_node_t    *top,		// I - Top node
    mxml_options_t *options,		// I - Options
    mxml_io_cb_t   io_cb,		// I - Read callback function
    void           *io_cbdata)		// I - Read callback data
{
  // Range check input...
  if (!io_cb)
    return (NULL);

  // Read the XML data...
  return (mxml_load_data(top, options, io_cb, io_cbdata));
}


//
// 'mxmlLoadString()' - Load a string into an XML node tree.
//
// This function loads the string into an XML node tree.  The nodes in the
// specified file are added to the specified node `top` - if `NULL` the XML file
// MUST be well-formed with a single parent processing instruction node like
// `<?xml version="1.0"?>` at the start of the file.
//
// Load options are provides via the `options` argument.  If `NULL`, all values
// will be loaded into `MXML_TYPE_TEXT` nodes.  Use the @link mxmlOptionsNew@
// function to create options when loading XML data.
//

mxml_node_t *				// O - First node or `NULL` if the string has errors.
mxmlLoadString(
    mxml_node_t    *top,		// I - Top node
    mxml_options_t *options,		// I - Options
    const char     *s)			// I - String to load
{
  _mxml_stringbuf_t	sb;		// String buffer


  // Range check input...
  if (!s)
    return (NULL);

  // Setup string buffer...
  sb.buffer   = (char *)s;
  sb.bufptr   = (char *)s;
  sb.bufsize  = strlen(s);
  sb.bufalloc = false;

  // Read the XML data...
  return (mxml_load_data(top, options, (mxml_io_cb_t)mxml_read_cb_string, &sb));
}


//
// 'mxmlSaveAllocString()' - Save an XML tree to an allocated string.
//
// This function saves the XML tree `node` to an allocated string.  The string
// should be freed using `free` (or the string free callback set using
// @link mxmlSetStringCallbacks@) when you are done with it.
//
// `NULL` is returned if the node would produce an empty string or if the string
// cannot be allocated.
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

char *					// O - Allocated string or `NULL`
mxmlSaveAllocString(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options)		// I - Options
{
  _mxml_stringbuf_t	sb;		// String buffer


  // Setup a string buffer
  if ((sb.buffer = malloc(1024)) == NULL)
    return (NULL);

  sb.bufptr   = sb.buffer;
  sb.bufsize  = 1024;
  sb.bufalloc = true;

  // Write the top node...
  if (mxml_write_node(node, options, (mxml_io_cb_t)mxml_io_cb_string, &sb, 0) < 0)
  {
    free(sb.buffer);
    return (NULL);
  }

  // Nul-terminate the string...
  *(sb.bufptr) = '\0';

  // Return the allocated string...
  return (sb.buffer);
}


//
// 'mxmlSaveFd()' - Save an XML tree to a file descriptor.
//
// This function saves the XML tree `node` to a file descriptor.
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

bool					// O - `true` on success, `false` on error.
mxmlSaveFd(mxml_node_t    *node,	// I - Node to write
           mxml_options_t *options,	// I - Options
           int            fd)		// I - File descriptor to write to
{
  int		col;			// Final column


  // Write the node...
  if ((col = mxml_write_node(node, options, (mxml_io_cb_t)mxml_io_cb_fd, &fd, 0)) < 0)
    return (false);

  // Make sure the file ends with a newline...
  if (col > 0)
  {
    if (write(fd, "\n", 1) < 0)
      return (false);
  }

  return (true);
}


//
// 'mxmlSaveFile()' - Save an XML tree to a file.
//
// This function saves the XML tree `node` to a stdio `FILE`.
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

bool					// O - `true` on success, `false` on error.
mxmlSaveFile(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options,		// I - Options
    FILE           *fp)			// I - File to write to
{
  int		col;			// Final column


  // Write the node...
  if ((col = mxml_write_node(node, options, (mxml_io_cb_t)mxml_io_cb_file, fp, 0)) < 0)
    return (false);

  // Make sure the file ends with a newline...
  if (col > 0)
  {
    if (putc('\n', fp) < 0)
      return (false);
  }

  return (true);
}


//
// 'mxmlSaveFilename()' - Save an XML tree to a file.
//
// This function saves the XML tree `node` to a named file.
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

bool					// O - `true` on success, `false` on error.
mxmlSaveFilename(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options,		// I - Options
    const char     *filename)		// I - File to write to
{
  bool		ret = true;		// Return value
  FILE		*fp;			// File pointer
  int		col;			// Final column


  // Open the file...
  if ((fp = fopen(filename, "w")) == NULL)
    return (false);

  // Write the node...
  if ((col = mxml_write_node(node, options, (mxml_io_cb_t)mxml_io_cb_file, fp, 0)) < 0)
  {
    ret = false;
  }
  else if (col > 0)
  {
    // Make sure the file ends with a newline...
    if (putc('\n', fp) < 0)
      ret = false;
  }

  fclose(fp);

  return (ret);
}


//
// 'mxmlSaveIO()' - Save an XML tree using a callback.
//
// This function saves the XML tree `node` using a write callback function
// `io_cb`.  The write callback is called with the callback data pointer
// `io_cbdata`, a buffer pointer, and the number of bytes to write, for
// example:
//
// ```c
// size_t my_io_cb(void *cbdata, const void *buffer, size_t bytes)
// {
//   ... write/copy bytes from buffer to the output ...
//   ... return the number of bytes written/copied or 0 on error ...
// }
// ```
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

bool					// O - `true` on success, `false` on error.
mxmlSaveIO(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options,		// I - Options
    mxml_io_cb_t   io_cb,		// I - Write callback function
    void           *io_cbdata)		// I - Write callback data
{
  int		col;			// Final column


  // Range check input...
  if (!node || !io_cb)
    return (false);

  // Write the node...
  if ((col = mxml_write_node(node, options, io_cb, io_cbdata, 0)) < 0)
    return (false);

  if (col > 0)
  {
    // Make sure the file ends with a newline...
    if ((io_cb)(io_cbdata, "\n", 1) != 1)
      return (false);
  }

  return (true);
}


//
// 'mxmlSaveString()' - Save an XML node tree to a string.
//
// This function saves the XML tree `node` to a fixed-size string buffer.
//
// Save options are provides via the `options` argument.  If `NULL`, the XML
// output will be wrapped at column 72 with no additional whitespace.  Use the
// @link mxmlOptionsNew@ function to create options for saving XML data.
//

size_t					// O - Size of string
mxmlSaveString(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options,		// I - Options
    char           *buffer,		// I - String buffer
    size_t         bufsize)		// I - Size of string buffer
{
  _mxml_stringbuf_t	sb;		// String buffer


  // Setup the string buffer...
  sb.buffer   = buffer;
  sb.bufptr   = buffer;
  sb.bufsize  = bufsize;
  sb.bufalloc = false;

  // Write the node...
  if (mxml_write_node(node, options, (mxml_io_cb_t)mxml_io_cb_string, &sb, 0) < 0)
    return (false);

  // Nul-terminate the string...
  if (sb.bufptr < (sb.buffer + sb.bufsize))
    *(sb.bufptr) = '\0';

  // Return the number of characters...
  return ((size_t)(sb.bufptr - sb.buffer));
}


//
// 'mxml_add_char()' - Add a character to a buffer, expanding as needed.
//

static bool				// O  - `true` on success, `false` on error
mxml_add_char(mxml_options_t *options,	// I - Options
              int            ch,	// I  - Character to add
              char           **bufptr,	// IO - Current position in buffer
	      char           **buffer,	// IO - Current buffer
	      size_t         *bufsize)	// IO - Current buffer size
{
  char	*newbuffer;			// New buffer value


  if (*bufptr >= (*buffer + *bufsize - 4))
  {
    // Increase the size of the buffer...
    if (*bufsize < 1024)
      (*bufsize) *= 2;
    else
      (*bufsize) += 1024;

    if ((newbuffer = realloc(*buffer, *bufsize)) == NULL)
    {
      _mxml_error(options, "Unable to expand string buffer to %lu bytes.", (unsigned long)*bufsize);

      return (false);
    }

    *bufptr = newbuffer + (*bufptr - *buffer);
    *buffer = newbuffer;
  }

  if (ch < 0x80)
  {
    // Single byte ASCII...
    *(*bufptr)++ = ch;
  }
  else if (ch < 0x800)
  {
    // Two-byte UTF-8...
    *(*bufptr)++ = 0xc0 | (ch >> 6);
    *(*bufptr)++ = 0x80 | (ch & 0x3f);
  }
  else if (ch < 0x10000)
  {
    // Three-byte UTF-8...
    *(*bufptr)++ = 0xe0 | (ch >> 12);
    *(*bufptr)++ = 0x80 | ((ch >> 6) & 0x3f);
    *(*bufptr)++ = 0x80 | (ch & 0x3f);
  }
  else
  {
    // Four-byte UTF-8...
    *(*bufptr)++ = 0xf0 | (ch >> 18);
    *(*bufptr)++ = 0x80 | ((ch >> 12) & 0x3f);
    *(*bufptr)++ = 0x80 | ((ch >> 6) & 0x3f);
    *(*bufptr)++ = 0x80 | (ch & 0x3f);
  }

  return (true);
}


//
// 'mxml_get_entity()' - Get the character corresponding to an entity...
//

static int				// O  - Character value or `EOF` on error
mxml_get_entity(
    mxml_options_t   *options,		// I  - Options
    mxml_io_cb_t     io_cb,		// I  - Read callback function
    void             *io_cbdata,	// I  - Read callback data
    _mxml_encoding_t *encoding,		// IO - Character encoding
    mxml_node_t      *parent,		// I  - Parent node
    int              *line)		// IO - Current line number
{
  int	ch;				// Current character
  char	entity[64],			// Entity string
	*entptr;			// Pointer into entity


  // Read a HTML character entity of the form "&NAME;", "&#NUMBER;", or "&#xHEX"...
  entptr = entity;

  while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
  {
    if (ch > 126 || (!isalnum(ch) && ch != '#'))
    {
      break;
    }
    else if (entptr < (entity + sizeof(entity) - 1))
    {
      *entptr++ = ch;
    }
    else
    {
      _mxml_error(options, "Entity name too long under parent <%s> on line %d.", mxmlGetElement(parent), *line);
      break;
    }
  }

  *entptr = '\0';

  if (ch != ';')
  {
    _mxml_error(options, "Character entity '%s' not terminated under parent <%s> on line %d.", entity, mxmlGetElement(parent), *line);

    if (ch == '\n')
      (*line)++;

    return (EOF);
  }

  if ((ch = _mxml_entity_value(options, entity)) < 0)
  {
    _mxml_error(options, "Entity '&%s;' not supported under parent <%s> on line %d.", entity, mxmlGetElement(parent), *line);
    return (EOF);
  }

  if (mxml_bad_char(ch))
  {
    _mxml_error(options, "Bad control character 0x%02x under parent <%s> on line %d not allowed by XML standard.", ch, mxmlGetElement(parent), *line);
    return (EOF);
  }

  return (ch);
}


//
// 'mxml_getc()' - Read a character from a file descriptor.
//

static int				// O  - Character or `EOF`
mxml_getc(mxml_options_t   *options,	// I  - Options
          mxml_io_cb_t     io_cb,	// I  - Read callback function
          void             *io_cbdata,	// I  - Read callback data
          _mxml_encoding_t *encoding)	// IO - Encoding
{
  int		ch;			// Current character
  unsigned char	buffer[4];		// Read buffer


  // Grab the next character...
  read_first_byte:

  if ((io_cb)(io_cbdata, buffer, 1) != 1)
    return (EOF);

  ch = buffer[0];

  switch (*encoding)
  {
    case _MXML_ENCODING_UTF8 :
        // Got a UTF-8 character; convert UTF-8 to Unicode and return...
	if (!(ch & 0x80))
	{
	  // ASCII
	  break;
        }
	else if (ch == 0xfe)
	{
	  // UTF-16 big-endian BOM?
	  if ((io_cb)(io_cbdata, buffer + 1, 1) != 1)
	    return (EOF);

	  if (buffer[1] != 0xff)
	    return (EOF);

          // Yes, switch to UTF-16 BE and try reading again...
	  *encoding = _MXML_ENCODING_UTF16BE;

	  goto read_first_byte;
	}
	else if (ch == 0xff)
	{
	  // UTF-16 little-endian BOM?
	  if ((io_cb)(io_cbdata, buffer + 1, 1) != 1)
	    return (EOF);

	  if (buffer[1] != 0xfe)
	    return (EOF);

          // Yes, switch to UTF-16 LE and try reading again...
	  *encoding = _MXML_ENCODING_UTF16LE;

	  goto read_first_byte;
	}
	else if ((ch & 0xe0) == 0xc0)
	{
	  // Two-byte value...
	  if ((io_cb)(io_cbdata, buffer + 1, 1) != 1)
	    return (EOF);

	  if ((buffer[1] & 0xc0) != 0x80)
	    return (EOF);

	  ch = ((ch & 0x1f) << 6) | (buffer[1] & 0x3f);

	  if (ch < 0x80)
	  {
	    _mxml_error(options, "Invalid UTF-8 sequence for character 0x%04x.", ch);
	    return (EOF);
	  }
	}
	else if ((ch & 0xf0) == 0xe0)
	{
	  // Three-byte value...
	  if ((io_cb)(io_cbdata, buffer + 1, 2) != 2)
	    return (EOF);

	  if ((buffer[1] & 0xc0) != 0x80 || (buffer[2] & 0xc0) != 0x80)
	    return (EOF);

	  ch = ((ch & 0x0f) << 12) | ((buffer[1] & 0x3f) << 6) | (buffer[2] & 0x3f);

	  if (ch < 0x800)
	  {
	    _mxml_error(options, "Invalid UTF-8 sequence for character 0x%04x.", ch);
	    return (EOF);
	  }

          // Ignore (strip) Byte Order Mark (BOM)...
	  if (ch == 0xfeff)
	    goto read_first_byte;
	}
	else if ((ch & 0xf8) == 0xf0)
	{
	  // Four-byte value...
	  if ((io_cb)(io_cbdata, buffer + 1, 3) != 3)
	    return (EOF);

	  if ((buffer[1] & 0xc0) != 0x80 || (buffer[2] & 0xc0) != 0x80 || (buffer[3] & 0xc0) != 0x80)
	    return (EOF);

	  ch = ((ch & 0x07) << 18) | ((buffer[1] & 0x3f) << 12) | ((buffer[2] & 0x3f) << 6) | (buffer[3] & 0x3f);

	  if (ch < 0x10000)
	  {
	    _mxml_error(options, "Invalid UTF-8 sequence for character 0x%04x.", ch);
	    return (EOF);
	  }
	}
	else
	{
	  return (EOF);
	}
	break;

    case _MXML_ENCODING_UTF16BE :
        // Read UTF-16 big-endian char...
	if ((io_cb)(io_cbdata, buffer + 1, 1) != 1)
	  return (EOF);

	ch = (ch << 8) | buffer[1];

        if (ch >= 0xd800 && ch <= 0xdbff)
	{
	  // Multi-word UTF-16 char...
          int lch;			// Lower bits

	  if ((io_cb)(io_cbdata, buffer + 2, 2) != 2)
	    return (EOF);

	  lch = (buffer[2] << 8) | buffer[3];

          if (lch < 0xdc00 || lch >= 0xdfff)
	    return (EOF);

          ch = (((ch & 0x3ff) << 10) | (lch & 0x3ff)) + 0x10000;
	}
	break;

    case _MXML_ENCODING_UTF16LE :
        // Read UTF-16 little-endian char...
	if ((io_cb)(io_cbdata, buffer + 1, 1) != 1)
	  return (EOF);

	ch |= buffer[1] << 8;

        if (ch >= 0xd800 && ch <= 0xdbff)
	{
	  // Multi-word UTF-16 char...
          int lch;			// Lower bits

	  if ((io_cb)(io_cbdata, buffer + 2, 2) != 2)
	    return (EOF);

	  lch = (buffer[3] << 8) | buffer[2];

          if (lch < 0xdc00 || lch >= 0xdfff)
	    return (EOF);

          ch = (((ch & 0x3ff) << 10) | (lch & 0x3ff)) + 0x10000;
	}
	break;
  }

  if (mxml_bad_char(ch))
  {
    _mxml_error(options, "Bad control character 0x%02x not allowed by XML standard.", ch);
    return (EOF);
  }

  return (ch);
}


//
// 'mxml_load_data()' - Load data into an XML node tree.
//

static mxml_node_t *			// O - First node or `NULL` if the XML could not be read.
mxml_load_data(
    mxml_node_t     *top,		// I - Top node
    mxml_options_t  *options,		// I - Options
    mxml_io_cb_t    io_cb,		// I - Read callback function
    void            *io_cbdata)		// I - Read callback data
{
  mxml_node_t	*node = NULL,		// Current node
		*first = NULL,		// First node added
		*parent = NULL;		// Current parent node
  int		line = 1,		// Current line number
		ch;			// Character from file
  bool		whitespace = false;	// Whitespace seen?
  char		*buffer,		// String buffer
		*bufptr;		// Pointer into buffer
  size_t	bufsize;		// Size of buffer
  mxml_type_t	type;			// Current node type
  _mxml_encoding_t encoding = _MXML_ENCODING_UTF8;
					// Character encoding
  static const char * const types[] =	// Type strings...
		{
		  "MXML_TYPE_CDATA",	// CDATA
		  "MXML_TYPE_COMMENT",	// Comment
		  "MXML_TYPE_DECLARATION",// Declaration
		  "MXML_TYPE_DIRECTIVE",// Processing instruction/directive
		  "MXML_TYPE_ELEMENT",	// XML element with attributes
		  "MXML_TYPE_INTEGER",	// Integer value
		  "MXML_TYPE_OPAQUE",	// Opaque string
		  "MXML_TYPE_REAL",	// Real value
		  "MXML_TYPE_TEXT",	// Text fragment
		  "MXML_TYPE_CUSTOM"	// Custom data
		};


  // Read elements and other nodes from the file...
  if ((buffer = malloc(64)) == NULL)
  {
    _mxml_error(options, "Unable to allocate string buffer.");
    return (NULL);
  }

  bufsize    = 64;
  bufptr     = buffer;
  parent     = top;
  first      = NULL;

  if (options && options->type_cb && parent)
    type = (options->type_cb)(options->type_cbdata, parent);
  else if (options && !options->type_cb)
    type = options->type_value;
  else
    type = MXML_TYPE_IGNORE;

  if ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) == EOF)
  {
    free(buffer);
    return (NULL);
  }
  else if (ch != '<' && !top)
  {
    free(buffer);
    _mxml_error(options, "XML does not start with '<' (saw '%c').", ch);
    return (NULL);
  }

  do
  {
    if ((ch == '<' || (mxml_isspace(ch) && type != MXML_TYPE_OPAQUE && type != MXML_TYPE_CUSTOM)) && bufptr > buffer)
    {
      // Add a new value node...
      *bufptr = '\0';

      switch (type)
      {
	case MXML_TYPE_INTEGER :
            node = mxmlNewInteger(parent, strtol(buffer, &bufptr, 0));
	    break;

	case MXML_TYPE_OPAQUE :
            node = mxmlNewOpaque(parent, buffer);
	    break;

	case MXML_TYPE_REAL :
            node = mxmlNewReal(parent, mxml_strtod(options, buffer, &bufptr));
	    break;

	case MXML_TYPE_TEXT :
            node = mxmlNewText(parent, whitespace, buffer);
	    break;

	case MXML_TYPE_CUSTOM :
	    if (options && options->custload_cb)
	    {
	      // Use the callback to fill in the custom data...
              node = mxmlNewCustom(parent, /*data*/NULL, /*free_cb*/NULL, /*free_cbdata*/NULL);

	      if (!(options->custload_cb)(options->cust_cbdata, node, buffer))
	      {
	        _mxml_error(options, "Bad custom value '%s' in parent <%s> on line %d.", buffer, parent ? parent->value.element.name : "null", line);
		mxmlDelete(node);
		node = NULL;
	      }
	      break;
	    }

        default : // Ignore...
	    node = NULL;
	    break;
      }

      if (*bufptr)
      {
        // Bad integer/real number value...
        _mxml_error(options, "Bad %s value '%s' in parent <%s> on line %d.", type == MXML_TYPE_INTEGER ? "integer" : "real", buffer, parent ? parent->value.element.name : "null", line);
	break;
      }

      MXML_DEBUG("mxml_load_data: node=%p(%s), parent=%p\n", node, buffer, parent);

      bufptr     = buffer;
      whitespace = mxml_isspace(ch) && type == MXML_TYPE_TEXT;

      if (!node && type != MXML_TYPE_IGNORE)
      {
        // Print error and return...
	_mxml_error(options, "Unable to add value node of type %s to parent <%s> on line %d.", types[type], parent ? parent->value.element.name : "null", line);
	goto error;
      }

      if (options && options->sax_cb)
      {
        if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_DATA))
          goto error;

        if (!mxmlRelease(node))
          node = NULL;
      }

      if (!first && node)
        first = node;
    }
    else if (mxml_isspace(ch) && type == MXML_TYPE_TEXT)
    {
      whitespace = true;
    }

    if (ch == '\n')
      line ++;

    // Add lone whitespace node if we have an element and existing whitespace...
    if (ch == '<' && whitespace && type == MXML_TYPE_TEXT)
    {
      if (parent)
      {
	node = mxmlNewText(parent, whitespace, "");

	if (options && options->sax_cb)
	{
	  if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_DATA))
	    goto error;

	  if (!mxmlRelease(node))
	    node = NULL;
	}

	if (!first && node)
	  first = node;
      }

      whitespace = false;
    }

    if (ch == '<')
    {
      // Start of open/close tag...
      bufptr = buffer;

      while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF)
      {
        if (mxml_isspace(ch) || ch == '>' || (ch == '/' && bufptr > buffer))
        {
	  break;
	}
	else if (ch == '<')
	{
	  _mxml_error(options, "Bare < in element.");
	  goto error;
	}
	else if (ch == '&')
	{
	  if ((ch = mxml_get_entity(options, io_cb, io_cbdata, &encoding, parent, &line)) == EOF)
	    goto error;

	  if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	    goto error;
	}
	else if (ch < '0' && ch != '!' && ch != '-' && ch != '.' && ch != '/')
	{
	  goto error;
	}
	else if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	{
	  goto error;
	}
	else if (((bufptr - buffer) == 1 && buffer[0] == '?') || ((bufptr - buffer) == 3 && !strncmp(buffer, "!--", 3)) || ((bufptr - buffer) == 8 && !strncmp(buffer, "![CDATA[", 8)))
	{
	  break;
	}

	if (ch == '\n')
	  line ++;
      }

      *bufptr = '\0';

      if (!strcmp(buffer, "!--"))
      {
        // Gather rest of comment...
	while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF)
	{
	  if (ch == '>' && bufptr > (buffer + 4) && bufptr[-3] != '-' && bufptr[-2] == '-' && bufptr[-1] == '-')
	    break;
	  else if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	    goto error;

	  if (ch == '\n')
	    line ++;
	}

        // Error out if we didn't get the whole comment...
        if (ch != '>')
	{
	  // Print error and return...
	  _mxml_error(options, "Early EOF in comment node on line %d.", line);
	  goto error;
	}

        // Otherwise add this as an element under the current parent...
	bufptr[-2] = '\0';

        if (!parent && first)
	{
	  // There can only be one root element!
	  _mxml_error(options, "<%s--> cannot be a second root node after <%s> on line %d.", buffer, first->value.element.name, line);
          goto error;
	}

	if ((node = mxmlNewComment(parent, buffer + 3)) == NULL)
	{
	  // Just print error for now...
	  _mxml_error(options, "Unable to add comment node to parent <%s> on line %d.", parent ? parent->value.element.name : "null", line);
	  break;
	}

	MXML_DEBUG("mxml_load_data: node=%p(<%s-->), parent=%p\n", node, buffer, parent);

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_COMMENT))
	    goto error;

          if (!mxmlRelease(node))
            node = NULL;
        }

	if (node && !first)
	  first = node;
      }
      else if (!strcmp(buffer, "![CDATA["))
      {
        // Gather CDATA section...
	while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF)
	{
	  if (ch == '>' && !strncmp(bufptr - 2, "]]", 2))
	  {
	    // Drop terminator from CDATA string...
	    bufptr[-2] = '\0';
	    break;
	  }
	  else if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	  {
	    goto error;
	  }

	  if (ch == '\n')
	    line ++;
	}

        // Error out if we didn't get the whole comment...
        if (ch != '>')
	{
	  // Print error and return...
	  _mxml_error(options, "Early EOF in CDATA node on line %d.", line);
	  goto error;
	}

        // Otherwise add this as an element under the current parent...
	bufptr[-2] = '\0';

        if (!parent && first)
	{
	  // There can only be one root element!
	  _mxml_error(options, "<%s]]> cannot be a second root node after <%s> on line %d.", buffer, first->value.element.name, line);
          goto error;
	}

	if ((node = mxmlNewCDATA(parent, buffer + 8)) == NULL)
	{
	  // Print error and return...
	  _mxml_error(options, "Unable to add CDATA node to parent <%s> on line %d.", parent ? parent->value.element.name : "null", line);
	  goto error;
	}

	MXML_DEBUG("mxml_load_data: node=%p(<%s]]>), parent=%p\n", node, buffer, parent);

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_CDATA))
	    goto error;

          if (!mxmlRelease(node))
            node = NULL;
        }

	if (node && !first)
	  first = node;
      }
      else if (buffer[0] == '?')
      {
        // Gather rest of processing instruction...
	while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF)
	{
	  if (ch == '>' && bufptr > buffer && bufptr[-1] == '?')
	    break;
	  else if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	    goto error;

	  if (ch == '\n')
	    line ++;
	}

        // Error out if we didn't get the whole processing instruction...
        if (ch != '>')
	{
	  // Print error and return...
	  _mxml_error(options, "Early EOF in processing instruction node on line %d.", line);
	  goto error;
	}

        // Otherwise add this as an element under the current parent...
	bufptr[-1] = '\0';

        if (!parent && first)
	{
	  // There can only be one root element!
	  _mxml_error(options, "<%s?> cannot be a second root node after <%s> on line %d.", buffer, first->value.element.name, line);
          goto error;
	}

	if ((node = mxmlNewDirective(parent, buffer + 1)) == NULL)
	{
	  // Print error and return...
	  _mxml_error(options, "Unable to add processing instruction node to parent <%s> on line %d.", parent ? parent->value.element.name : "null", line);
	  goto error;
	}

	MXML_DEBUG("mxml_load_data: node=%p(<%s?>), parent=%p\n", node, buffer, parent);

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_DIRECTIVE))
	    goto error;

          if (strncmp(node->value.directive, "xml ", 4) && !mxmlRelease(node))
            node = NULL;
        }

        if (node)
	{
	  if (!first)
            first = node;

	  if (!parent)
	  {
	    parent = node;

	    if (options && options->type_cb)
	      type = (options->type_cb)(options->type_cbdata, parent);
	    else if (options)
	      type = options->type_value;
	    else
	      type = MXML_TYPE_TEXT;
	  }
	}
      }
      else if (buffer[0] == '!')
      {
        // Gather rest of declaration...
	do
	{
	  if (ch == '>')
	  {
	    break;
	  }
	  else
	  {
            if (ch == '&')
            {
	      if ((ch = mxml_get_entity(options, io_cb, io_cbdata, &encoding, parent, &line)) == EOF)
		goto error;
            }

	    if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	      goto error;
	  }

	  if (ch == '\n')
	    line ++;
	}
        while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF);

        // Error out if we didn't get the whole declaration...
        if (ch != '>')
	{
	  // Print error and return...
	  _mxml_error(options, "Early EOF in declaration node on line %d.", line);
	  goto error;
	}

        // Otherwise add this as an element under the current parent...
	*bufptr = '\0';

        if (!parent && first)
	{
	  // There can only be one root element!
	  _mxml_error(options, "<%s> cannot be a second root node after <%s> on line %d.", buffer, first->value.element.name, line);
          goto error;
	}

	if ((node = mxmlNewDeclaration(parent, buffer + 1)) == NULL)
	{
	  // Print error and return...
	  _mxml_error(options, "Unable to add declaration node to parent <%s> on line %d.", parent ? parent->value.element.name : "null", line);
	  goto error;
	}

	MXML_DEBUG("mxml_load_data: node=%p(<%s>), parent=%p\n", node, buffer, parent);

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_DECLARATION))
	    goto error;

          if (!mxmlRelease(node))
            node = NULL;
        }

        if (node)
	{
	  if (!first)
            first = node;

	  if (!parent)
	  {
	    parent = node;

	    if (options && options->type_cb)
	      type = (options->type_cb)(options->type_cbdata, parent);
	    else if (options)
	      type = options->type_value;
	    else
	      type = MXML_TYPE_TEXT;
	  }
	}
      }
      else if (buffer[0] == '/')
      {
        // Handle close tag...
	MXML_DEBUG("mxml_load_data: <%s>, parent=%p\n", buffer, parent);

        if (!parent || strcmp(buffer + 1, parent->value.element.name))
	{
	  // Close tag doesn't match tree; print an error for now...
	  _mxml_error(options, "Mismatched close tag <%s> under parent <%s> on line %d.", buffer, parent ? parent->value.element.name : "(null)", line);
          goto error;
	}

        // Keep reading until we see >...
        while (ch != '>' && ch != EOF)
	  ch = mxml_getc(options, io_cb, io_cbdata, &encoding);

        node   = parent;
        parent = parent->parent;

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_ELEMENT_CLOSE))
	    goto error;

          if (!mxmlRelease(node))
          {
            if (first == node)
	      first = NULL;

	    node = NULL;
	  }
        }

        // Ascend into the parent and set the value type as needed...
	if (options && options->type_cb && parent)
	  type = (options->type_cb)(options->type_cbdata, parent);
	else if (options && !options->type_cb)
	  type = options->type_value;
      }
      else
      {
        // Handle open tag...
        if (!parent && first)
	{
	  // There can only be one root element!
	  _mxml_error(options, "<%s> cannot be a second root node after <%s> on line %d.", buffer, first->value.element.name, line);
          goto error;
	}

        if ((node = mxmlNewElement(parent, buffer)) == NULL)
	{
	  // Just print error for now...
	  _mxml_error(options, "Unable to add element node to parent <%s> on line %d.", parent ? parent->value.element.name : "null", line);
	  goto error;
	}

        if (mxml_isspace(ch))
        {
	  MXML_DEBUG("mxml_load_data: node=%p(<%s...>), parent=%p\n", node, buffer, parent);

	  if ((ch = mxml_parse_element(options, io_cb, io_cbdata, node, &encoding, &line)) == EOF)
	    goto error;
        }
        else if (ch == '/')
	{
	  MXML_DEBUG("mxml_load_data: node=%p(<%s/>), parent=%p\n", node, buffer, parent);

	  if ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != '>')
	  {
	    _mxml_error(options, "Expected > but got '%c' instead for element <%s/> on line %d.", ch, buffer, line);
            goto error;
	  }

	  ch = '/';
	}

        if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_ELEMENT_OPEN))
	    goto error;
	}

        if (!first)
	  first = node;

	if (ch == EOF)
	  break;

        if (ch != '/')
	{
	  // Descend into this node, setting the value type as needed...
	  parent = node;

	  if (options && options->type_cb && parent)
	    type = (options->type_cb)(options->type_cbdata, parent);
	  else if (options && !options->type_cb)
	    type = options->type_value;
	  else
	    type = MXML_TYPE_TEXT;
	}
        else if (options && options->sax_cb)
        {
          if (!(options->sax_cb)(options->sax_cbdata, node, MXML_SAX_EVENT_ELEMENT_CLOSE))
	    goto error;

          if (!mxmlRelease(node))
          {
            if (first == node)
	      first = NULL;

	    node = NULL;
	  }
        }
      }

      bufptr  = buffer;
    }
    else if (ch == '&')
    {
      // Add character entity to current buffer...
      if ((ch = mxml_get_entity(options, io_cb, io_cbdata, &encoding, parent, &line)) == EOF)
	goto error;

      if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	goto error;
    }
    else if (type == MXML_TYPE_OPAQUE || type == MXML_TYPE_CUSTOM || !mxml_isspace(ch))
    {
      // Add character to current buffer...
      if (!mxml_add_char(options, ch, &bufptr, &buffer, &bufsize))
	goto error;
    }
  }
  while ((ch = mxml_getc(options, io_cb, io_cbdata, &encoding)) != EOF);

  // Free the string buffer - we don't need it anymore...
  free(buffer);

  // Find the top element and return it...
  if (parent)
  {
    node = parent;

    while (parent != top && parent->parent)
      parent = parent->parent;

    if (node != parent)
    {
      _mxml_error(options, "Missing close tag </%s> under parent <%s> on line %d.", mxmlGetElement(node), node->parent ? node->parent->value.element.name : "(null)", line);

      mxmlDelete(first);

      return (NULL);
    }
  }

  if (parent)
    return (parent);
  else
    return (first);

  // Common error return...
  error:

  if (node && node != first)
    mxmlDelete(node);

  mxmlDelete(first);

  free(buffer);

  return (NULL);
}


//
// 'mxml_parse_element()' - Parse an element for any attributes...
//

static int				// O  - Terminating character
mxml_parse_element(
    mxml_options_t   *options,		// I - Options
    mxml_io_cb_t     io_cb,		// I - Read callback function
    void             *io_cbdata,	// I - Read callback data
    mxml_node_t      *node,		// I  - Element node
    _mxml_encoding_t *encoding,		// IO - Encoding
    int              *line)		// IO - Current line number
{
  int		ch,			// Current character in file
		quote;			// Quoting character
  char		*name,			// Attribute name
		*value,			// Attribute value
		*ptr;			// Pointer into name/value
  size_t	namesize,		// Size of name string
		valsize;		// Size of value string


  // Initialize the name and value buffers...
  if ((name = malloc(64)) == NULL)
  {
    _mxml_error(options, "Unable to allocate memory for name.");
    return (EOF);
  }

  namesize = 64;

  if ((value = malloc(64)) == NULL)
  {
    free(name);
    _mxml_error(options, "Unable to allocate memory for value.");
    return (EOF);
  }

  valsize = 64;

  // Loop until we hit a >, /, ?, or EOF...
  while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
  {
    MXML_DEBUG("mxml_parse_element: ch='%c'\n", ch);

    // Skip leading whitespace...
    if (mxml_isspace(ch))
    {
      if (ch == '\n')
        (*line)++;

      continue;
    }

    // Stop at /, ?, or >...
    if (ch == '/' || ch == '?')
    {
      // Grab the > character and print an error if it isn't there...
      quote = mxml_getc(options, io_cb, io_cbdata, encoding);

      if (quote != '>')
      {
        _mxml_error(options, "Expected '>' after '%c' for element %s, but got '%c' on line %d.", ch, mxmlGetElement(node), quote, *line);
        goto error;
      }

      break;
    }
    else if (ch == '<')
    {
      _mxml_error(options, "Bare < in element %s on line %d.", mxmlGetElement(node), *line);
      goto error;
    }
    else if (ch == '>')
    {
      break;
    }

    // Read the attribute name...
    ptr = name;
    if (!mxml_add_char(options, ch, &ptr, &name, &namesize))
      goto error;

    if (ch == '\"' || ch == '\'')
    {
      // Name is in quotes, so get a quoted string...
      quote = ch;

      while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
      {
        if (ch == '&')
        {
	  if ((ch = mxml_get_entity(options, io_cb, io_cbdata, encoding, node, line)) == EOF)
	    goto error;
	}
	else if (ch == '\n')
	{
	  (*line)++;
	}

	if (!mxml_add_char(options, ch, &ptr, &name, &namesize))
	  goto error;

	if (ch == quote)
          break;
      }
    }
    else
    {
      // Grab an normal, non-quoted name...
      while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
      {
	if (mxml_isspace(ch) || ch == '=' || ch == '/' || ch == '>' || ch == '?')
	{
	  if (ch == '\n')
	    (*line)++;
          break;
        }
	else
	{
          if (ch == '&')
          {
	    if ((ch = mxml_get_entity(options, io_cb, io_cbdata, encoding, node, line)) == EOF)
	      goto error;
          }

	  if (!mxml_add_char(options, ch, &ptr, &name, &namesize))
	    goto error;
	}
      }
    }

    *ptr = '\0';

    if (mxmlElementGetAttr(node, name))
    {
      _mxml_error(options, "Duplicate attribute '%s' in element %s on line %d.", name, mxmlGetElement(node), *line);
      goto error;
    }

    while (ch != EOF && mxml_isspace(ch))
    {
      ch = mxml_getc(options, io_cb, io_cbdata, encoding);

      if (ch == '\n')
        (*line)++;
    }

    if (ch == '=')
    {
      // Read the attribute value...
      while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF && mxml_isspace(ch))
      {
        if (ch == '\n')
          (*line)++;
      }

      if (ch == EOF)
      {
        _mxml_error(options, "Missing value for attribute '%s' in element %s on line %d.", name, mxmlGetElement(node), *line);
        goto error;
      }

      if (ch == '\'' || ch == '\"')
      {
        // Read quoted value...
        quote = ch;
	ptr   = value;

        while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
        {
	  if (ch == quote)
	  {
	    break;
	  }
	  else
	  {
	    if (ch == '&')
	    {
	      if ((ch = mxml_get_entity(options, io_cb, io_cbdata, encoding, node, line)) == EOF)
	        goto error;
	    }
	    else if (ch == '\n')
	    {
	      (*line)++;
	    }

	    if (!mxml_add_char(options, ch, &ptr, &value, &valsize))
	      goto error;
	  }
	}

        *ptr = '\0';
      }
      else
      {
        // Read unquoted value...
	ptr      = value;
	if (!mxml_add_char(options, ch, &ptr, &value, &valsize))
	  goto error;

	while ((ch = mxml_getc(options, io_cb, io_cbdata, encoding)) != EOF)
	{
	  if (mxml_isspace(ch) || ch == '=' || ch == '/' || ch == '>')
	  {
	    if (ch == '\n')
	      (*line)++;

            break;
          }
	  else
	  {
	    if (ch == '&')
	    {
	      if ((ch = mxml_get_entity(options, io_cb, io_cbdata, encoding, node, line)) == EOF)
	        goto error;
	    }

	    if (!mxml_add_char(options, ch, &ptr, &value, &valsize))
	      goto error;
	  }
	}

        *ptr = '\0';
      }

      // Set the attribute with the given string value...
      mxmlElementSetAttr(node, name, value);
      MXML_DEBUG("mxml_parse_element: %s=\"%s\"\n", name, value);
    }
    else
    {
      _mxml_error(options, "Missing value for attribute '%s' in element %s on line %d.", name, mxmlGetElement(node), *line);
      goto error;
    }

    // Check the end character...
    if (ch == '/' || ch == '?')
    {
      // Grab the > character and print an error if it isn't there...
      quote = mxml_getc(options, io_cb, io_cbdata, encoding);

      if (quote != '>')
      {
        _mxml_error(options, "Expected '>' after '%c' for element %s, but got '%c' on line %d.", ch, mxmlGetElement(node), quote, *line);
        ch = EOF;
      }

      break;
    }
    else if (ch == '>')
      break;
  }

  // Free the name and value buffers and return...
  free(name);
  free(value);

  return (ch);

  // Common error return point...
  error:

  free(name);
  free(value);

  return (EOF);
}


//
// 'mxml_read_cb_fd()' - Read bytes from a file descriptor.
//

static size_t				// O - Bytes read
mxml_read_cb_fd(int    *fd,		// I - File descriptor
                void   *buffer,		// I - Buffer
                size_t bytes)		// I - Bytes to read
{
#if _WIN32
  int		rbytes;			// Bytes read


  rbytes = read(*fd, buffer, bytes);

#else
  ssize_t	rbytes;			// Bytes read


  while ((rbytes = read(*fd, buffer, bytes)) < 0)
  {
    if (errno != EINTR && errno != EAGAIN)
      break;
  }
#endif // _WIN32

  if (rbytes < 0)
    return (0);
  else
    return ((size_t)rbytes);
}


//
// 'mxml_read_cb_file()' - Read bytes from a file pointer.
//

static size_t				// O - Bytes read
mxml_read_cb_file(FILE   *fp,		// I - File pointer
                  void   *buffer,	// I - Buffer
                  size_t bytes)		// I - Bytes to read
{
  if (feof(fp))
    return (0);
  else
    return (fread(buffer, 1, bytes, fp));
}


//
// 'mxml_read_cb_string()' - Read bytes from a string.
//

static size_t				// O - Bytes read
mxml_read_cb_string(
    _mxml_stringbuf_t *sb,		// I - String buffer
    void              *buffer,		// I - Buffer
    size_t            bytes)		// I - Bytes to read
{
  size_t	remaining;		// Remaining bytes in buffer


  if ((remaining = sb->bufsize - (size_t)(sb->bufptr - sb->buffer)) < bytes)
    bytes = remaining;

  if (bytes > 0)
  {
    // Copy bytes from string...
    memcpy(buffer, sb->bufptr, bytes);
    sb->bufptr += bytes;
  }

  return (bytes);
}


//
// 'mxml_strtod()' - Convert a string to a double without respect to the locale.
//

static double				// O - Real number
mxml_strtod(mxml_options_t *options,	// I - Options
            const char     *buffer,	// I - String
            char           **bufend)	// O - End of number in string
{
  const char	*bufptr;		// Pointer into buffer
  char		temp[64],		// Temporary buffer
		*tempptr;		// Pointer into temporary buffer


  // See if the locale has a special decimal point string...
  if (!options || !options->loc)
    return (strtod(buffer, bufend));

  // Copy leading sign, numbers, period, and then numbers...
  tempptr                = temp;
  temp[sizeof(temp) - 1] = '\0';
  bufptr                 = buffer;

  if (*bufptr == '-' || *bufptr == '+')
    *tempptr++ = *bufptr++;

  while (*bufptr && isdigit(*bufptr & 255))
  {
    if (tempptr < (temp + sizeof(temp) - 1))
    {
      *tempptr++ = *bufptr++;
    }
    else
    {
      *bufend = (char *)bufptr;
      return (0.0);
    }
  }

  if (*bufptr == '.')
  {
    // Convert decimal point to locale equivalent...
    size_t	declen = strlen(options->loc->decimal_point);
					// Length of decimal point
    bufptr ++;

    if (declen <= (sizeof(temp) - (size_t)(tempptr - temp)))
    {
      memcpy(tempptr, options->loc->decimal_point, declen);
      tempptr += declen;
    }
    else
    {
      *bufend = (char *)bufptr;
      return (0.0);
    }
  }

  // Copy any remaining characters...
  while (*bufptr && isdigit(*bufptr & 255))
  {
    if (tempptr < (temp + sizeof(temp) - 1))
      *tempptr++ = *bufptr++;
    else
      break;
  }

  *bufend = (char *)bufptr;

  if (*bufptr)
    return (0.0);

  // Nul-terminate the temporary string and convert the string...
  *tempptr = '\0';

  return (strtod(temp, NULL));
}


//
// 'mxml_io_cb_fd()' - Write bytes to a file descriptor.
//

static size_t				// O - Bytes written
mxml_io_cb_fd(int    *fd,		// I - File descriptor
                 void   *buffer,	// I - Buffer
                 size_t bytes)		// I - Bytes to write
{
#if _WIN32
  int		wbytes;			// Bytes written


  wbytes = write(*fd, buffer, bytes);

#else
  ssize_t	wbytes;			// Bytes written


  while ((wbytes = write(*fd, buffer, bytes)) < 0)
  {
    if (errno != EINTR && errno != EAGAIN)
      break;
  }
#endif // _WIN32

  if (wbytes < 0)
    return (0);
  else
    return ((size_t)wbytes);
}


//
// 'mxml_io_cb_file()' - Write bytes to a file pointer.
//

static size_t				// O - Bytes written
mxml_io_cb_file(FILE   *fp,		// I - File pointer
                   void   *buffer,	// I - Buffer
                   size_t bytes)	// I - Bytes to write
{
  return (fwrite(buffer, 1, bytes, fp));
}


//
// 'mxml_io_cb_string()' - Write bytes to a string buffer.
//

static size_t				// O - Bytes written
mxml_io_cb_string(
    _mxml_stringbuf_t *sb,		// I - String buffer
    void              *buffer,		// I - Buffer
    size_t            bytes)		// I - Bytes to write
{
  size_t	remaining;		// Remaining bytes


  // Expand buffer as needed...
  if ((sb->bufptr + bytes) >= (sb->buffer + sb->bufsize - 1) && sb->bufalloc)
  {
    // Reallocate buffer
    char	*temp;			// New buffer pointer
    size_t	newsize;		// New bufsize

    newsize = (size_t)(sb->bufptr - sb->buffer) + bytes + 257;
    if ((temp = realloc(sb->buffer, newsize)) == NULL)
      return (0);

    sb->bufptr  = temp + (sb->bufptr - sb->buffer);
    sb->buffer  = temp;
    sb->bufsize = newsize;
  }

  // Copy what we can...
  if (sb->bufptr >= (sb->buffer + sb->bufsize - 1))
    return (0);			// No more room
  else if ((remaining = (sb->bufsize - (size_t)(sb->bufptr - sb->buffer) - 1)) < bytes)
    bytes = remaining;

  memcpy(sb->bufptr, buffer, bytes);
  sb->bufptr += bytes;

  return (bytes);
}


//
// 'mxml_write_node()' - Save an XML node to a file.
//

static int				// O - Column or -1 on error
mxml_write_node(
    mxml_node_t    *node,		// I - Node to write
    mxml_options_t *options,		// I - Options
    mxml_io_cb_t   io_cb,		// I - Write callback function
    void           *io_cbdata,		// I - Write callback data
    int            col)			// I - Current column
{
  mxml_node_t	*current,		// Current node
		*next;			// Next node
  size_t	i,			// Looping var
		width;			// Width of attr + value
  _mxml_attr_t	*attr;			// Current attribute
  char		s[255],			// Temporary string
		*data;			// Custom data string
  const char	*text;			// Text string
  bool		whitespace;		// Whitespace before text string?


  // Loop through this node and all of its children...
  for (current = node; current && col >= 0; current = next)
  {
    // Print the node value...
    MXML_DEBUG("mxml_write_node: current=%p(%d)\n", current, current->type);

    switch (mxmlGetType(current))
    {
      case MXML_TYPE_CDATA :
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_OPEN, col);
	  col = mxml_write_string("<![CDATA[", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(mxmlGetCDATA(current), io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string("]]>", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_OPEN, col);
          break;

      case MXML_TYPE_COMMENT :
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_OPEN, col);
	  col = mxml_write_string("<!--", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(mxmlGetComment(current), io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string("-->", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_OPEN, col);
          break;

      case MXML_TYPE_DECLARATION :
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_OPEN, col);
	  col = mxml_write_string("<!", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(mxmlGetDeclaration(current), io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(">", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_OPEN, col);
          break;

      case MXML_TYPE_DIRECTIVE :
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_OPEN, col);
	  col = mxml_write_string("<?", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(mxmlGetDirective(current), io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string("?>", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_OPEN, col);
          break;

      case MXML_TYPE_ELEMENT :
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_OPEN, col);
	  col = mxml_write_string("<", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_string(mxmlGetElement(current), io_cb, io_cbdata, /*use_entities*/true, col);

	  for (i = current->value.element.num_attrs, attr = current->value.element.attrs; i > 0 && col >= 0; i --, attr ++)
	  {
	    width = strlen(attr->name);

	    if (attr->value)
	      width += strlen(attr->value) + 3;

	    if (options && options->wrap > 0 && (col + (int)width) > options->wrap)
	      col = mxml_write_string("\n", io_cb, io_cbdata, /*use_entities*/false, col);
	    else
	      col = mxml_write_string(" ", io_cb, io_cbdata, /*use_entities*/false, col);

	    col = mxml_write_string(attr->name, io_cb, io_cbdata, /*use_entities*/true, col);

	    if (attr->value)
	    {
	      col = mxml_write_string("=\"", io_cb, io_cbdata, /*use_entities*/false, col);
	      col = mxml_write_string(attr->value, io_cb, io_cbdata, /*use_entities*/true, col);
	      col = mxml_write_string("\"", io_cb, io_cbdata, /*use_entities*/false, col);
	    }
	  }

	  col = mxml_write_string(current->child ? ">" : "/>", io_cb, io_cbdata, /*use_entities*/false, col);
	  col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_OPEN, col);
	  break;

      case MXML_TYPE_INTEGER :
	  if (current->prev)
	  {
	    // Add whitespace separator...
	    if (options && options->wrap > 0 && col > options->wrap)
	      col = mxml_write_string("\n", io_cb, io_cbdata, /*use_entities*/false, col);
	    else
	      col = mxml_write_string(" ", io_cb, io_cbdata, /*use_entities*/false, col);
	  }

          // Write integer...
	  snprintf(s, sizeof(s), "%ld", current->value.integer);
	  col = mxml_write_string(s, io_cb, io_cbdata, /*use_entities*/true, col);
	  break;

      case MXML_TYPE_OPAQUE :
	  col = mxml_write_string(mxmlGetOpaque(current), io_cb, io_cbdata, /*use_entities*/true, col);
	  break;

      case MXML_TYPE_REAL :
	  if (current->prev)
	  {
	    // Add whitespace separator...
	    if (options && options->wrap > 0 && col > options->wrap)
	      col = mxml_write_string("\n", io_cb, io_cbdata, /*use_entities*/false, col);
	    else
	      col = mxml_write_string(" ", io_cb, io_cbdata, /*use_entities*/false, col);
	  }

          // Write real number...
	  snprintf(s, sizeof(s), "%g", current->value.real);

          if (options && options->loc)
          {
            char	*sptr;		// Pointer into string

	    if ((sptr = strstr(s, options->loc->decimal_point)) != NULL)
	    {
	      // Convert locale decimal point to "."
	      if (options->loc_declen > 1)
	        memmove(sptr + 1, sptr + options->loc_declen, strlen(sptr + options->loc_declen) + 1);

	      *sptr = '.';
	    }
          }

	  col = mxml_write_string(s, io_cb, io_cbdata, /*use_entities*/true, col);
	  break;

      case MXML_TYPE_TEXT :
          text = mxmlGetText(current, &whitespace);

	  if (whitespace && col > 0)
	  {
	    // Add whitespace separator...
	    if (options && options->wrap > 0 && col > options->wrap)
	      col = mxml_write_string("\n", io_cb, io_cbdata, /*use_entities*/false, col);
	    else
	      col = mxml_write_string(" ", io_cb, io_cbdata, /*use_entities*/false, col);
	  }

	  col = mxml_write_string(text, io_cb, io_cbdata, /*use_entities*/true, col);
	  break;

      case MXML_TYPE_CUSTOM :
	  if (!options || !options->custsave_cb)
	    return (-1);

	  if ((data = (options->custsave_cb)(options->cust_cbdata, current)) == NULL)
	    return (-1);

	  col = mxml_write_string(data, io_cb, io_cbdata, /*use_entities*/true, col);

	  free(data);
	  break;

      default : // Should never happen
	  return (-1);
    }

    // Figure out the next node...
    if ((next = mxmlGetFirstChild(current)) == NULL)
    {
      if (current == node)
      {
        // Don't traverse to sibling node if we are at the "root" node...
        next = NULL;
      }
      else
      {
        // Try the next sibling, and continue traversing upwards as needed...
	while ((next = mxmlGetNextSibling(current)) == NULL)
	{
	  if (current == node || !mxmlGetParent(current))
	    break;

	  // Declarations and directives have no end tags...
	  current = mxmlGetParent(current);

	  if (mxmlGetType(current) == MXML_TYPE_ELEMENT)
	  {
	    col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_BEFORE_CLOSE, col);
	    col = mxml_write_string("</", io_cb, io_cbdata, /*use_entities*/false, col);
	    col = mxml_write_string(mxmlGetElement(current), io_cb, io_cbdata, /*use_entities*/true, col);
	    col = mxml_write_string(">", io_cb, io_cbdata, /*use_entities*/false, col);
	    col = mxml_write_ws(current, options, io_cb, io_cbdata, MXML_WS_AFTER_CLOSE, col);
	  }

	  if (current == node)
	    break;
	}
      }
    }
  }

  return (col);
}


//
// 'mxml_write_string()' - Write a string, escaping & and < as needed.
//

static int				// O - New column or `-1` on error
mxml_write_string(
    const char      *s,			// I - String to write
    mxml_io_cb_t    io_cb,		// I - Write callback function
    void            *io_cbdata,		// I - Write callback data
    bool            use_entities,	// I - Escape special characters?
    int             col)		// I - Current column
{
  const char	*frag,			// Start of current string fragment
		*ptr,			// Pointer into string
		*ent;			// Entity, if any
  size_t	fraglen;		// Length of fragment


  MXML_DEBUG("mxml_write_string(io_cb=%p, io_cbdata=%p, s=\"%s\", use_entities=%s, col=%d)\n", io_cb, io_cbdata, s, use_entities ? "true" : "false", col);

  if (col < 0)
    return (-1);

  for (frag = ptr = s; *ptr; ptr ++)
  {
    if (use_entities && (ent = _mxml_entity_string(*ptr)) != NULL)
    {
      size_t entlen = strlen(ent);	// Length of entity

      if (ptr > frag)
      {
        // Write current fragment
        fraglen = (size_t)(ptr - frag);

	if ((io_cb)(io_cbdata, (char *)frag, fraglen) != fraglen)
	  return (-1);
      }

      frag = ptr + 1;

      // Write entity
      if ((io_cb)(io_cbdata, (char *)ent, entlen) != entlen)
        return (-1);

      col ++;
    }
    else if (*ptr == '\r' || *ptr == '\n')
    {
      // CR or LF resets column
      col = 0;
    }
    else if (*ptr == '\t')
    {
      // Tab indents column
      col = col - (col % MXML_TAB) + MXML_TAB;
    }
    else
    {
      // All other characters are 1 column wide
      col ++;
    }
  }

  if (ptr > frag)
  {
    // Write final fragment
    fraglen = (size_t)(ptr - frag);

    if ((io_cb)(io_cbdata, (char *)frag, fraglen) != fraglen)
      return (-1);
  }

  return (col);
}


//
// 'mxml_write_ws()' - Do whitespace callback...
//

static int				// O - New column or `-1` on error
mxml_write_ws(
    mxml_node_t    *node,		// I - Current node
    mxml_options_t *options,		// I - Options
    mxml_io_cb_t   io_cb,		// I - Write callback function
    void           *io_cbdata,		// I - Write callback data
    mxml_ws_t      ws,			// I - Whitespace value
    int            col)			// I - Current column
{
  const char	*s;			// Whitespace string


  if (options && options->ws_cb && (s = (options->ws_cb)(options->ws_cbdata, node, ws)) != NULL)
    col = mxml_write_string(s, io_cb, io_cbdata, /*use_entities*/false, col);

  return (col);
}
