/* MIT License
 *
 * Copyright (c) 2023 Brad House
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 */
#include "ares_private.h"
#include "ares_buf.h"
#include <limits.h>
#ifdef HAVE_STDINT_H
#  include <stdint.h>
#endif

struct ares_buf {
  const unsigned char *data;          /*!< pointer to start of data buffer */
  size_t               data_len;      /*!< total size of data in buffer */

  unsigned char       *alloc_buf;     /*!< Pointer to allocated data buffer,
                                       *   not used for const buffers */
  size_t               alloc_buf_len; /*!< Size of allocated data buffer */

  size_t               offset;        /*!< Current working offset in buffer */
  size_t               tag_offset;    /*!< Tagged offset in buffer. Uses
                                       *   SIZE_MAX if not set. */
};

ares_buf_t *ares_buf_create(void)
{
  ares_buf_t *buf = ares_malloc_zero(sizeof(*buf));
  if (buf == NULL) {
    return NULL;
  }

  buf->tag_offset = SIZE_MAX;
  return buf;
}

ares_buf_t *ares_buf_create_const(const unsigned char *data, size_t data_len)
{
  ares_buf_t *buf;

  if (data == NULL || data_len == 0) {
    return NULL;
  }

  buf = ares_buf_create();
  if (buf == NULL) {
    return NULL;
  }

  buf->data     = data;
  buf->data_len = data_len;

  return buf;
}

void ares_buf_destroy(ares_buf_t *buf)
{
  if (buf == NULL) {
    return;
  }
  ares_free(buf->alloc_buf);
  ares_free(buf);
}

static ares_bool_t ares_buf_is_const(const ares_buf_t *buf)
{
  if (buf == NULL) {
    return ARES_FALSE; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (buf->data != NULL && buf->alloc_buf == NULL) {
    return ARES_TRUE;
  }

  return ARES_FALSE;
}

void ares_buf_reclaim(ares_buf_t *buf)
{
  size_t prefix_size;
  size_t data_size;

  if (buf == NULL) {
    return;
  }

  if (ares_buf_is_const(buf)) {
    return; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  /* Silence coverity.  All lengths are zero so would bail out later but
   * coverity doesn't know this */
  if (buf->alloc_buf == NULL) {
    return;
  }

  if (buf->tag_offset != SIZE_MAX && buf->tag_offset < buf->offset) {
    prefix_size = buf->tag_offset;
  } else {
    prefix_size = buf->offset;
  }

  if (prefix_size == 0) {
    return;
  }

  data_size = buf->data_len - prefix_size;

  memmove(buf->alloc_buf, buf->alloc_buf + prefix_size, data_size);
  buf->data      = buf->alloc_buf;
  buf->data_len  = data_size;
  buf->offset   -= prefix_size;
  if (buf->tag_offset != SIZE_MAX) {
    buf->tag_offset -= prefix_size;
  }
}

static ares_status_t ares_buf_ensure_space(ares_buf_t *buf, size_t needed_size)
{
  size_t         remaining_size;
  size_t         alloc_size;
  unsigned char *ptr;

  if (buf == NULL) {
    return ARES_EFORMERR;
  }

  if (ares_buf_is_const(buf)) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  /* When calling ares_buf_finish_str() we end up adding a null terminator,
   * so we want to ensure the size is always sufficient for this as we don't
   * want an ARES_ENOMEM at that point */
  needed_size++;

  /* No need to do an expensive move operation, we have enough to just append */
  remaining_size = buf->alloc_buf_len - buf->data_len;
  if (remaining_size >= needed_size) {
    return ARES_SUCCESS;
  }

  /* See if just moving consumed data frees up enough space */
  ares_buf_reclaim(buf);

  remaining_size = buf->alloc_buf_len - buf->data_len;
  if (remaining_size >= needed_size) {
    return ARES_SUCCESS;
  }

  alloc_size = buf->alloc_buf_len;

  /* Not yet started */
  if (alloc_size == 0) {
    alloc_size = 16; /* Always shifts 1, so ends up being 32 minimum */
  }

  /* Increase allocation by powers of 2 */
  do {
    alloc_size     <<= 1;
    remaining_size   = alloc_size - buf->data_len;
  } while (remaining_size < needed_size);

  ptr = ares_realloc(buf->alloc_buf, alloc_size);
  if (ptr == NULL) {
    return ARES_ENOMEM;
  }

  buf->alloc_buf     = ptr;
  buf->alloc_buf_len = alloc_size;
  buf->data          = ptr;

  return ARES_SUCCESS;
}

ares_status_t ares_buf_set_length(ares_buf_t *buf, size_t len)
{
  if (buf == NULL || ares_buf_is_const(buf)) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (len >= buf->alloc_buf_len - buf->offset) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  buf->data_len = len + buf->offset;
  return ARES_SUCCESS;
}

ares_status_t ares_buf_append(ares_buf_t *buf, const unsigned char *data,
                              size_t data_len)
{
  ares_status_t status;

  if (data == NULL && data_len != 0) {
    return ARES_EFORMERR;
  }

  if (data_len == 0) {
    return ARES_SUCCESS;
  }

  status = ares_buf_ensure_space(buf, data_len);
  if (status != ARES_SUCCESS) {
    return status;
  }

  memcpy(buf->alloc_buf + buf->data_len, data, data_len);
  buf->data_len += data_len;
  return ARES_SUCCESS;
}

ares_status_t ares_buf_append_byte(ares_buf_t *buf, unsigned char b)
{
  return ares_buf_append(buf, &b, 1);
}

ares_status_t ares_buf_append_be16(ares_buf_t *buf, unsigned short u16)
{
  ares_status_t status;

  status = ares_buf_append_byte(buf, (unsigned char)((u16 >> 8) & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_buf_append_byte(buf, (unsigned char)(u16 & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  return ARES_SUCCESS;
}

ares_status_t ares_buf_append_be32(ares_buf_t *buf, unsigned int u32)
{
  ares_status_t status;

  status = ares_buf_append_byte(buf, ((unsigned char)(u32 >> 24) & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_buf_append_byte(buf, ((unsigned char)(u32 >> 16) & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_buf_append_byte(buf, ((unsigned char)(u32 >> 8) & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_buf_append_byte(buf, ((unsigned char)u32 & 0xff));
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  return ARES_SUCCESS;
}

unsigned char *ares_buf_append_start(ares_buf_t *buf, size_t *len)
{
  ares_status_t status;

  if (len == NULL || *len == 0) {
    return NULL;
  }

  status = ares_buf_ensure_space(buf, *len);
  if (status != ARES_SUCCESS) {
    return NULL;
  }

  /* -1 for possible null terminator for ares_buf_finish_str() */
  *len = buf->alloc_buf_len - buf->data_len - 1;
  return buf->alloc_buf + buf->data_len;
}

void ares_buf_append_finish(ares_buf_t *buf, size_t len)
{
  if (buf == NULL) {
    return;
  }

  buf->data_len += len;
}

unsigned char *ares_buf_finish_bin(ares_buf_t *buf, size_t *len)
{
  unsigned char *ptr = NULL;
  if (buf == NULL || len == NULL || ares_buf_is_const(buf)) {
    return NULL;
  }

  ares_buf_reclaim(buf);

  /* We don't want to return NULL except on failure, may be zero-length */
  if (buf->alloc_buf == NULL && ares_buf_ensure_space(buf, 1) != ARES_SUCCESS) {
    return NULL; /* LCOV_EXCL_LINE: OutOfMemory */
  }
  ptr  = buf->alloc_buf;
  *len = buf->data_len;
  ares_free(buf);
  return ptr;
}

char *ares_buf_finish_str(ares_buf_t *buf, size_t *len)
{
  char  *ptr;
  size_t mylen;

  ptr = (char *)ares_buf_finish_bin(buf, &mylen);
  if (ptr == NULL) {
    return NULL;
  }

  if (len != NULL) {
    *len = mylen;
  }

  /* NOTE: ensured via ares_buf_ensure_space() that there is always at least
   *       1 extra byte available for this specific use-case */
  ptr[mylen] = 0;

  return ptr;
}

void ares_buf_tag(ares_buf_t *buf)
{
  if (buf == NULL) {
    return;
  }

  buf->tag_offset = buf->offset;
}

ares_status_t ares_buf_tag_rollback(ares_buf_t *buf)
{
  if (buf == NULL || buf->tag_offset == SIZE_MAX) {
    return ARES_EFORMERR;
  }

  buf->offset     = buf->tag_offset;
  buf->tag_offset = SIZE_MAX;
  return ARES_SUCCESS;
}

ares_status_t ares_buf_tag_clear(ares_buf_t *buf)
{
  if (buf == NULL || buf->tag_offset == SIZE_MAX) {
    return ARES_EFORMERR;
  }

  buf->tag_offset = SIZE_MAX;
  return ARES_SUCCESS;
}

const unsigned char *ares_buf_tag_fetch(const ares_buf_t *buf, size_t *len)
{
  if (buf == NULL || buf->tag_offset == SIZE_MAX || len == NULL) {
    return NULL;
  }

  *len = buf->offset - buf->tag_offset;
  return buf->data + buf->tag_offset;
}

size_t ares_buf_tag_length(const ares_buf_t *buf)
{
  if (buf == NULL || buf->tag_offset == SIZE_MAX) {
    return 0;
  }
  return buf->offset - buf->tag_offset;
}

ares_status_t ares_buf_tag_fetch_bytes(const ares_buf_t *buf,
                                       unsigned char *bytes, size_t *len)
{
  size_t               ptr_len = 0;
  const unsigned char *ptr     = ares_buf_tag_fetch(buf, &ptr_len);

  if (ptr == NULL || bytes == NULL || len == NULL) {
    return ARES_EFORMERR;
  }

  if (*len < ptr_len) {
    return ARES_EFORMERR;
  }

  *len = ptr_len;

  if (ptr_len > 0) {
    memcpy(bytes, ptr, ptr_len);
  }
  return ARES_SUCCESS;
}

ares_status_t ares_buf_tag_fetch_constbuf(const ares_buf_t *buf,
                                          ares_buf_t      **newbuf)
{
  size_t               ptr_len = 0;
  const unsigned char *ptr     = ares_buf_tag_fetch(buf, &ptr_len);

  if (ptr == NULL || newbuf == NULL) {
    return ARES_EFORMERR;
  }

  *newbuf = ares_buf_create_const(ptr, ptr_len);
  if (*newbuf == NULL) {
    return ARES_ENOMEM;
  }
  return ARES_SUCCESS;
}

ares_status_t ares_buf_tag_fetch_string(const ares_buf_t *buf, char *str,
                                        size_t len)
{
  size_t        out_len;
  ares_status_t status;
  size_t        i;

  if (str == NULL || len == 0) {
    return ARES_EFORMERR;
  }

  /* Space for NULL terminator */
  out_len = len - 1;

  status = ares_buf_tag_fetch_bytes(buf, (unsigned char *)str, &out_len);
  if (status != ARES_SUCCESS) {
    return status;
  }

  /* NULL terminate */
  str[out_len] = 0;

  /* Validate string is printable */
  for (i = 0; i < out_len; i++) {
    if (!ares_isprint(str[i])) {
      return ARES_EBADSTR;
    }
  }

  return ARES_SUCCESS;
}

ares_status_t ares_buf_tag_fetch_strdup(const ares_buf_t *buf, char **str)
{
  size_t               ptr_len = 0;
  const unsigned char *ptr     = ares_buf_tag_fetch(buf, &ptr_len);

  if (ptr == NULL || str == NULL) {
    return ARES_EFORMERR;
  }

  if (!ares_str_isprint((const char *)ptr, ptr_len)) {
    return ARES_EBADSTR;
  }

  *str = ares_malloc(ptr_len + 1);
  if (*str == NULL) {
    return ARES_ENOMEM;
  }

  if (ptr_len > 0) {
    memcpy(*str, ptr, ptr_len);
  }
  (*str)[ptr_len] = 0;
  return ARES_SUCCESS;
}

static const unsigned char *ares_buf_fetch(const ares_buf_t *buf, size_t *len)
{
  if (len != NULL) {
    *len = 0;
  }

  if (buf == NULL || len == NULL || buf->data == NULL) {
    return NULL;
  }

  *len = buf->data_len - buf->offset;
  if (*len == 0) {
    return NULL;
  }

  return buf->data + buf->offset;
}

ares_status_t ares_buf_consume(ares_buf_t *buf, size_t len)
{
  size_t remaining_len = ares_buf_len(buf);

  if (remaining_len < len) {
    return ARES_EBADRESP;
  }

  buf->offset += len;
  return ARES_SUCCESS;
}

ares_status_t ares_buf_fetch_be16(ares_buf_t *buf, unsigned short *u16)
{
  size_t               remaining_len;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);
  unsigned int         u32;

  if (buf == NULL || u16 == NULL || remaining_len < sizeof(*u16)) {
    return ARES_EBADRESP;
  }

  /* Do math in an unsigned int in order to prevent warnings due to automatic
   * conversion by the compiler from short to int during shifts */
  u32  = ((unsigned int)(ptr[0]) << 8 | (unsigned int)ptr[1]);
  *u16 = (unsigned short)(u32 & 0xFFFF);

  return ares_buf_consume(buf, sizeof(*u16));
}

ares_status_t ares_buf_fetch_be32(ares_buf_t *buf, unsigned int *u32)
{
  size_t               remaining_len;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);

  if (buf == NULL || u32 == NULL || remaining_len < sizeof(*u32)) {
    return ARES_EBADRESP;
  }

  *u32 = ((unsigned int)(ptr[0]) << 24 | (unsigned int)(ptr[1]) << 16 |
          (unsigned int)(ptr[2]) << 8 | (unsigned int)(ptr[3]));

  return ares_buf_consume(buf, sizeof(*u32));
}

ares_status_t ares_buf_fetch_bytes(ares_buf_t *buf, unsigned char *bytes,
                                   size_t len)
{
  size_t               remaining_len;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);

  if (buf == NULL || bytes == NULL || len == 0 || remaining_len < len) {
    return ARES_EBADRESP;
  }

  memcpy(bytes, ptr, len);
  return ares_buf_consume(buf, len);
}

ares_status_t ares_buf_fetch_bytes_dup(ares_buf_t *buf, size_t len,
                                       ares_bool_t     null_term,
                                       unsigned char **bytes)
{
  size_t               remaining_len;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);

  if (buf == NULL || bytes == NULL || len == 0 || remaining_len < len) {
    return ARES_EBADRESP;
  }

  *bytes = ares_malloc(null_term ? len + 1 : len);
  if (*bytes == NULL) {
    return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  memcpy(*bytes, ptr, len);
  if (null_term) {
    (*bytes)[len] = 0;
  }
  return ares_buf_consume(buf, len);
}

ares_status_t ares_buf_fetch_str_dup(ares_buf_t *buf, size_t len, char **str)
{
  size_t               remaining_len;
  size_t               i;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);

  if (buf == NULL || str == NULL || len == 0 || remaining_len < len) {
    return ARES_EBADRESP;
  }

  /* Validate string is printable */
  for (i = 0; i < len; i++) {
    if (!ares_isprint(ptr[i])) {
      return ARES_EBADSTR;
    }
  }

  *str = ares_malloc(len + 1);
  if (*str == NULL) {
    return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  memcpy(*str, ptr, len);
  (*str)[len] = 0;

  return ares_buf_consume(buf, len);
}

ares_status_t ares_buf_fetch_bytes_into_buf(ares_buf_t *buf, ares_buf_t *dest,
                                            size_t len)
{
  size_t               remaining_len;
  const unsigned char *ptr = ares_buf_fetch(buf, &remaining_len);
  ares_status_t        status;

  if (buf == NULL || dest == NULL || len == 0 || remaining_len < len) {
    return ARES_EBADRESP;
  }

  status = ares_buf_append(dest, ptr, len);
  if (status != ARES_SUCCESS) {
    return status;
  }

  return ares_buf_consume(buf, len);
}

static ares_bool_t ares_is_whitespace(unsigned char c,
                                      ares_bool_t   include_linefeed)
{
  switch (c) {
    case '\r':
    case '\t':
    case ' ':
    case '\v':
    case '\f':
      return ARES_TRUE;
    case '\n':
      return include_linefeed;
    default:
      break;
  }
  return ARES_FALSE;
}

size_t ares_buf_consume_whitespace(ares_buf_t *buf,
                                   ares_bool_t include_linefeed)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  size_t               i;

  if (ptr == NULL) {
    return 0;
  }

  for (i = 0; i < remaining_len; i++) {
    if (!ares_is_whitespace(ptr[i], include_linefeed)) {
      break;
    }
  }

  if (i > 0) {
    ares_buf_consume(buf, i);
  }
  return i;
}

size_t ares_buf_consume_nonwhitespace(ares_buf_t *buf)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  size_t               i;

  if (ptr == NULL) {
    return 0;
  }

  for (i = 0; i < remaining_len; i++) {
    if (ares_is_whitespace(ptr[i], ARES_TRUE)) {
      break;
    }
  }

  if (i > 0) {
    ares_buf_consume(buf, i);
  }
  return i;
}

size_t ares_buf_consume_line(ares_buf_t *buf, ares_bool_t include_linefeed)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  size_t               i;

  if (ptr == NULL) {
    return 0;
  }

  for (i = 0; i < remaining_len; i++) {
    if (ptr[i] == '\n') {
      goto done;
    }
  }

done:
  if (include_linefeed && i < remaining_len && ptr[i] == '\n') {
    i++;
  }

  if (i > 0) {
    ares_buf_consume(buf, i);
  }
  return i;
}

size_t ares_buf_consume_until_charset(ares_buf_t          *buf,
                                      const unsigned char *charset, size_t len,
                                      ares_bool_t require_charset)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  size_t               pos;
  ares_bool_t          found = ARES_FALSE;

  if (ptr == NULL || charset == NULL || len == 0) {
    return 0;
  }

  /* Optimize for single character searches */
  if (len == 1) {
    const unsigned char *p = memchr(ptr, charset[0], remaining_len);
    if (p != NULL) {
      found = ARES_TRUE;
      pos   = (size_t)(p - ptr);
    } else {
      pos = remaining_len;
    }
    goto done;
  }

  for (pos = 0; pos < remaining_len; pos++) {
    size_t j;
    for (j = 0; j < len; j++) {
      if (ptr[pos] == charset[j]) {
        found = ARES_TRUE;
        goto done;
      }
    }
  }

done:
  if (require_charset && !found) {
    return SIZE_MAX;
  }

  if (pos > 0) {
    ares_buf_consume(buf, pos);
  }
  return pos;
}

size_t ares_buf_consume_until_seq(ares_buf_t *buf, const unsigned char *seq,
                                  size_t len, ares_bool_t require_seq)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  const unsigned char *p;
  size_t               consume_len = 0;

  if (ptr == NULL || seq == NULL || len == 0) {
    return 0;
  }

  p = ares_memmem(ptr, remaining_len, seq, len);
  if (require_seq && p == NULL) {
    return SIZE_MAX;
  }

  if (p != NULL) {
    consume_len = (size_t)(p - ptr);
  } else {
    consume_len = remaining_len;
  }

  if (consume_len > 0) {
    ares_buf_consume(buf, consume_len);
  }

  return consume_len;
}

size_t ares_buf_consume_charset(ares_buf_t *buf, const unsigned char *charset,
                                size_t len)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);
  size_t               i;

  if (ptr == NULL || charset == NULL || len == 0) {
    return 0;
  }

  for (i = 0; i < remaining_len; i++) {
    size_t j;
    for (j = 0; j < len; j++) {
      if (ptr[i] == charset[j]) {
        break;
      }
    }
    /* Not found */
    if (j == len) {
      break;
    }
  }

  if (i > 0) {
    ares_buf_consume(buf, i);
  }
  return i;
}

static void ares_buf_destroy_cb(void *arg)
{
  ares_buf_t **buf = arg;
  ares_buf_destroy(*buf);
}

static ares_bool_t ares_buf_split_isduplicate(ares_array_t        *arr,
                                              const unsigned char *val,
                                              size_t               len,
                                              ares_buf_split_t     flags)
{
  size_t i;
  size_t num = ares_array_len(arr);

  for (i = 0; i < num; i++) {
    ares_buf_t         **bufptr = ares_array_at(arr, i);
    const ares_buf_t    *buf    = *bufptr;
    size_t               plen   = 0;
    const unsigned char *ptr    = ares_buf_peek(buf, &plen);

    /* Can't be duplicate if lengths mismatch */
    if (plen != len) {
      continue;
    }

    if (flags & ARES_BUF_SPLIT_CASE_INSENSITIVE) {
      if (ares_memeq_ci(ptr, val, len)) {
        return ARES_TRUE;
      }
    } else {
      if (ares_memeq(ptr, val, len)) {
        return ARES_TRUE;
      }
    }
  }

  return ARES_FALSE;
}

ares_status_t ares_buf_split(ares_buf_t *buf, const unsigned char *delims,
                             size_t delims_len, ares_buf_split_t flags,
                             size_t max_sections, ares_array_t **arr)
{
  ares_status_t status = ARES_SUCCESS;
  ares_bool_t   first  = ARES_TRUE;

  if (buf == NULL || delims == NULL || delims_len == 0 || arr == NULL) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  *arr = ares_array_create(sizeof(ares_buf_t *), ares_buf_destroy_cb);
  if (*arr == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  while (ares_buf_len(buf)) {
    size_t               len = 0;
    const unsigned char *ptr;

    if (first) {
      /* No delimiter yet, just tag the start */
      ares_buf_tag(buf);
    } else {
      if (flags & ARES_BUF_SPLIT_KEEP_DELIMS) {
        /* tag then eat delimiter so its first byte in buffer */
        ares_buf_tag(buf);
        ares_buf_consume(buf, 1);
      } else {
        /* throw away delimiter */
        ares_buf_consume(buf, 1);
        ares_buf_tag(buf);
      }
    }

    if (max_sections && ares_array_len(*arr) >= max_sections - 1) {
      ares_buf_consume(buf, ares_buf_len(buf));
    } else {
      ares_buf_consume_until_charset(buf, delims, delims_len, ARES_FALSE);
    }

    ptr = ares_buf_tag_fetch(buf, &len);

    /* Shouldn't be possible */
    if (ptr == NULL) {
      status = ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
      goto done;
    }

    if (flags & ARES_BUF_SPLIT_LTRIM) {
      size_t i;
      for (i = 0; i < len; i++) {
        if (!ares_is_whitespace(ptr[i], ARES_TRUE)) {
          break;
        }
      }
      ptr += i;
      len -= i;
    }

    if (flags & ARES_BUF_SPLIT_RTRIM) {
      while (len > 0 && ares_is_whitespace(ptr[len - 1], ARES_TRUE)) {
        len--;
      }
    }

    if (len != 0 || flags & ARES_BUF_SPLIT_ALLOW_BLANK) {
      ares_buf_t *data;

      if (!(flags & ARES_BUF_SPLIT_NO_DUPLICATES) ||
          !ares_buf_split_isduplicate(*arr, ptr, len, flags)) {
        /* Since we don't allow const buffers of 0 length, and user wants
         * 0-length buffers, swap what we do here */
        if (len) {
          data = ares_buf_create_const(ptr, len);
        } else {
          data = ares_buf_create();
        }

        if (data == NULL) {
          status = ARES_ENOMEM;
          goto done;
        }

        status = ares_array_insertdata_last(*arr, &data);
        if (status != ARES_SUCCESS) {
          ares_buf_destroy(data);
          goto done;
        }
      }
    }

    first = ARES_FALSE;
  }

done:
  if (status != ARES_SUCCESS) {
    ares_array_destroy(*arr);
    *arr = NULL;
  }

  return status;
}

static void ares_free_split_array(void *arg)
{
  void **ptr = arg;
  ares_free(*ptr);
}

ares_status_t ares_buf_split_str_array(ares_buf_t          *buf,
                                       const unsigned char *delims,
                                       size_t               delims_len,
                                       ares_buf_split_t     flags,
                                       size_t max_sections, ares_array_t **arr)
{
  ares_status_t status;
  ares_array_t *split = NULL;
  size_t        i;
  size_t        len;

  if (arr == NULL) {
    return ARES_EFORMERR;
  }

  *arr = NULL;

  status = ares_buf_split(buf, delims, delims_len, flags, max_sections, &split);
  if (status != ARES_SUCCESS) {
    goto done;
  }

  *arr = ares_array_create(sizeof(char *), ares_free_split_array);
  if (*arr == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  len = ares_array_len(split);
  for (i = 0; i < len; i++) {
    ares_buf_t **bufptr = ares_array_at(split, i);
    ares_buf_t  *lbuf   = *bufptr;
    char        *str    = NULL;

    status = ares_buf_fetch_str_dup(lbuf, ares_buf_len(lbuf), &str);
    if (status != ARES_SUCCESS) {
      goto done;
    }

    status = ares_array_insertdata_last(*arr, &str);
    if (status != ARES_SUCCESS) {
      ares_free(str);
      goto done;
    }
  }

done:
  ares_array_destroy(split);
  if (status != ARES_SUCCESS) {
    ares_array_destroy(*arr);
    *arr = NULL;
  }
  return status;
}

ares_status_t ares_buf_split_str(ares_buf_t *buf, const unsigned char *delims,
                                 size_t delims_len, ares_buf_split_t flags,
                                 size_t max_sections, char ***strs,
                                 size_t *nstrs)
{
  ares_status_t status;
  ares_array_t *arr = NULL;

  if (strs == NULL || nstrs == NULL) {
    return ARES_EFORMERR;
  }

  *strs  = NULL;
  *nstrs = 0;

  status = ares_buf_split_str_array(buf, delims, delims_len, flags,
                                    max_sections, &arr);

  if (status != ARES_SUCCESS) {
    goto done;
  }

done:
  if (status == ARES_SUCCESS) {
    *strs = ares_array_finish(arr, nstrs);
  } else {
    ares_array_destroy(arr);
  }
  return status;
}

ares_bool_t ares_buf_begins_with(const ares_buf_t    *buf,
                                 const unsigned char *data, size_t data_len)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);

  if (ptr == NULL || data == NULL || data_len == 0) {
    return ARES_FALSE;
  }

  if (data_len > remaining_len) {
    return ARES_FALSE;
  }

  if (memcmp(ptr, data, data_len) != 0) {
    return ARES_FALSE;
  }

  return ARES_TRUE;
}

size_t ares_buf_len(const ares_buf_t *buf)
{
  if (buf == NULL) {
    return 0;
  }

  return buf->data_len - buf->offset;
}

const unsigned char *ares_buf_peek(const ares_buf_t *buf, size_t *len)
{
  return ares_buf_fetch(buf, len);
}

ares_status_t ares_buf_replace(ares_buf_t *buf, const unsigned char *srch,
                               size_t srch_size, const unsigned char *rplc,
                               size_t rplc_size)
{
  size_t        processed_len = 0;
  ares_status_t status;

  if (buf->alloc_buf == NULL || srch == NULL || srch_size == 0 ||
      (rplc == NULL && rplc_size != 0)) {
    return ARES_EFORMERR;
  }

  while (1) {
    unsigned char *ptr           = buf->alloc_buf + buf->offset + processed_len;
    size_t         remaining_len = buf->data_len - buf->offset - processed_len;
    size_t         found_offset  = 0;
    size_t         move_data_len;

    /* Find pattern */
    ptr = ares_memmem(ptr, remaining_len, srch, srch_size);
    if (ptr == NULL) {
      break;
    }

    /* Store the offset this was found because our actual pointer might be
     * switched out from under us by the call to ensure_space() if the
     * replacement pattern is larger than the search pattern */
    found_offset   = (size_t)(ptr - (size_t)(buf->alloc_buf + buf->offset));
    if (rplc_size > srch_size) {
      status = ares_buf_ensure_space(buf, rplc_size - srch_size);
      if (status != ARES_SUCCESS) {
        return status;
      }
    }

    /* Impossible, but silence clang */
    if (buf->alloc_buf == NULL) {
      return ARES_ENOMEM;
    }

    /* Recalculate actual pointer */
    ptr = buf->alloc_buf + buf->offset + found_offset;

    /* Move the data */
    move_data_len = buf->data_len - buf->offset - found_offset - srch_size;
    memmove(ptr + rplc_size,
            ptr + srch_size,
            move_data_len);

    /* Copy in the replacement data */
    if (rplc != NULL && rplc_size > 0) {
      memcpy(ptr, rplc, rplc_size);
    }

    if (rplc_size > srch_size) {
      buf->data_len += rplc_size - srch_size;
    } else {
      buf->data_len -= srch_size - rplc_size;
    }

    processed_len = found_offset + rplc_size;
  }

  return ARES_SUCCESS;
}

ares_status_t ares_buf_peek_byte(const ares_buf_t *buf, unsigned char *b)
{
  size_t               remaining_len = 0;
  const unsigned char *ptr           = ares_buf_fetch(buf, &remaining_len);

  if (buf == NULL || b == NULL) {
    return ARES_EFORMERR;
  }

  if (remaining_len == 0) {
    return ARES_EBADRESP;
  }
  *b = ptr[0];
  return ARES_SUCCESS;
}

size_t ares_buf_get_position(const ares_buf_t *buf)
{
  if (buf == NULL) {
    return 0;
  }
  return buf->offset;
}

ares_status_t ares_buf_set_position(ares_buf_t *buf, size_t idx)
{
  if (buf == NULL) {
    return ARES_EFORMERR;
  }

  if (idx > buf->data_len) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  buf->offset = idx;
  return ARES_SUCCESS;
}

static ares_status_t
  ares_buf_parse_dns_binstr_int(ares_buf_t *buf, size_t remaining_len,
                                unsigned char **bin, size_t *bin_len,
                                ares_bool_t validate_printable)
{
  unsigned char len;
  ares_status_t status = ARES_EBADRESP;
  ares_buf_t   *binbuf = NULL;

  if (buf == NULL) {
    return ARES_EFORMERR;
  }

  if (remaining_len == 0) {
    return ARES_EBADRESP;
  }

  binbuf = ares_buf_create();
  if (binbuf == NULL) {
    return ARES_ENOMEM;
  }

  status = ares_buf_fetch_bytes(buf, &len, 1);
  if (status != ARES_SUCCESS) {
    goto done; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  remaining_len--;

  if (len > remaining_len) {
    status = ARES_EBADRESP;
    goto done;
  }

  if (len) {
    /* When used by the _str() parser, it really needs to be validated to
     * be a valid printable ascii string.  Do that here */
    if (validate_printable && ares_buf_len(buf) >= len) {
      size_t      mylen;
      const char *data = (const char *)ares_buf_peek(buf, &mylen);
      if (!ares_str_isprint(data, len)) {
        status = ARES_EBADSTR;
        goto done;
      }
    }

    if (bin != NULL) {
      status = ares_buf_fetch_bytes_into_buf(buf, binbuf, len);
    } else {
      status = ares_buf_consume(buf, len);
    }
  }

done:
  if (status != ARES_SUCCESS) {
    ares_buf_destroy(binbuf);
  } else {
    if (bin != NULL) {
      size_t mylen = 0;
      /* NOTE: we use ares_buf_finish_str() here as we guarantee NULL
       *       Termination even though we are technically returning binary data.
       */
      *bin     = (unsigned char *)ares_buf_finish_str(binbuf, &mylen);
      *bin_len = mylen;
    }
  }

  return status;
}

ares_status_t ares_buf_parse_dns_binstr(ares_buf_t *buf, size_t remaining_len,
                                        unsigned char **bin, size_t *bin_len)
{
  return ares_buf_parse_dns_binstr_int(buf, remaining_len, bin, bin_len,
                                       ARES_FALSE);
}

ares_status_t ares_buf_parse_dns_str(ares_buf_t *buf, size_t remaining_len,
                                     char **str)
{
  size_t len;

  return ares_buf_parse_dns_binstr_int(buf, remaining_len,
                                       (unsigned char **)str, &len, ARES_TRUE);
}

ares_status_t ares_buf_append_num_dec(ares_buf_t *buf, size_t num, size_t len)
{
  size_t i;
  size_t mod;

  if (len == 0) {
    len = ares_count_digits(num);
  }

  mod = ares_pow(10, len);

  for (i = len; i > 0; i--) {
    size_t        digit = (num % mod);
    ares_status_t status;

    mod /= 10;

    /* Silence coverity.  Shouldn't be possible since we calculate it above */
    if (mod == 0) {
      return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
    }

    digit  /= mod;
    status  = ares_buf_append_byte(buf, '0' + (unsigned char)(digit & 0xFF));
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }
  }
  return ARES_SUCCESS;
}

ares_status_t ares_buf_append_num_hex(ares_buf_t *buf, size_t num, size_t len)
{
  size_t                     i;
  static const unsigned char hexbytes[] = "0123456789ABCDEF";

  if (len == 0) {
    len = ares_count_hexdigits(num);
  }

  for (i = len; i > 0; i--) {
    ares_status_t status;
    status = ares_buf_append_byte(buf, hexbytes[(num >> ((i - 1) * 4)) & 0xF]);
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }
  }
  return ARES_SUCCESS;
}

ares_status_t ares_buf_append_str(ares_buf_t *buf, const char *str)
{
  return ares_buf_append(buf, (const unsigned char *)str, ares_strlen(str));
}

static ares_status_t ares_buf_hexdump_line(ares_buf_t *buf, size_t idx,
                                           const unsigned char *data,
                                           size_t               len)
{
  size_t        i;
  ares_status_t status;

  /* Address */
  status = ares_buf_append_num_hex(buf, idx, 6);
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  /* | */
  status = ares_buf_append_str(buf, " | ");
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  for (i = 0; i < 16; i++) {
    if (i >= len) {
      status = ares_buf_append_str(buf, "  ");
    } else {
      status = ares_buf_append_num_hex(buf, data[i], 2);
    }
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }

    status = ares_buf_append_byte(buf, ' ');
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }
  }

  /* | */
  status = ares_buf_append_str(buf, " | ");
  if (status != ARES_SUCCESS) {
    return status; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  for (i = 0; i < 16; i++) {
    if (i >= len) {
      break;
    }
    status = ares_buf_append_byte(buf, ares_isprint(data[i]) ? data[i] : '.');
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }
  }

  return ares_buf_append_byte(buf, '\n');
}

ares_status_t ares_buf_hexdump(ares_buf_t *buf, const unsigned char *data,
                               size_t len)
{
  size_t i;

  /* Each line is 16 bytes */
  for (i = 0; i < len; i += 16) {
    ares_status_t status;
    status = ares_buf_hexdump_line(buf, i, data + i, len - i);
    if (status != ARES_SUCCESS) {
      return status; /* LCOV_EXCL_LINE: OutOfMemory */
    }
  }

  return ARES_SUCCESS;
}

ares_status_t ares_buf_load_file(const char *filename, ares_buf_t *buf)
{
  FILE          *fp        = NULL;
  unsigned char *ptr       = NULL;
  size_t         len       = 0;
  size_t         ptr_len   = 0;
  long           ftell_len = 0;
  ares_status_t  status;

  if (filename == NULL || buf == NULL) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  fp = fopen(filename, "rb");
  if (fp == NULL) {
    int error = errno;
    switch (error) {
      case ENOENT:
      case ESRCH:
        status = ARES_ENOTFOUND;
        goto done;
      default:
        DEBUGF(fprintf(stderr, "fopen() failed with error: %d %s\n", error,
                       strerror(error)));
        DEBUGF(fprintf(stderr, "Error opening file: %s\n", filename));
        status = ARES_EFILE;
        goto done;
    }
  }

  /* Get length portably, fstat() is POSIX, not C */
  if (fseek(fp, 0, SEEK_END) != 0) {
    status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */
    goto done;           /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  ftell_len = ftell(fp);
  if (ftell_len < 0) {
    status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */
    goto done;           /* LCOV_EXCL_LINE: DefensiveCoding */
  }
  len = (size_t)ftell_len;

  if (fseek(fp, 0, SEEK_SET) != 0) {
    status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */
    goto done;           /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (len == 0) {
    status = ARES_SUCCESS; /* LCOV_EXCL_LINE: DefensiveCoding */
    goto done;             /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  /* Read entire data into buffer */
  ptr_len = len;
  ptr     = ares_buf_append_start(buf, &ptr_len);
  if (ptr == NULL) {
    status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
    goto done;            /* LCOV_EXCL_LINE: OutOfMemory */
  }

  ptr_len = fread(ptr, 1, len, fp);
  if (ptr_len != len) {
    status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */
    goto done;           /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  ares_buf_append_finish(buf, len);
  status = ARES_SUCCESS;

done:
  if (fp != NULL) {
    fclose(fp);
  }
  return status;
}
