/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 2016-2025 Free Software Foundation, Inc.

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

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

   You should have received a copy of the GNU General Public License
   along with GNU Mailutils.  If not, see <http://www.gnu.org/licenses/>. */

#include <config.h>
#include <stdlib.h>
#include <string.h>
#include <mailutils/cstr.h>
#include <mailutils/errno.h>

/*
 * Returns length of the buffer necessary to accommodate a copy of STR with
 * characters from CHR escaped (without terminating zero).
 */
size_t
mu_c_str_escape_length (char const *str, char const *chr)
{
  size_t n;
  if (!str)
    return 0;
  n = strlen (str);
  if (chr)
    n += mu_str_count (str, chr, NULL);
  return n;
}

/*
 * Copies STR into buffer BUF, escaping each occurrence of characters
 * from CHR.  Copying stops when zero byte in STR is reached or SIZE-1
 * bytes in BUF have been filled, whichever happens first.
 *
 * Escaping is done so, that each character CHR[i] is replaced by a
 * backslash, followed by XTAB[i].
 *
 * If CHR is NULL, STR is copied to BUF verbatim, truncating it if necessary.
 *
 * If XTAB is NULL, XTAB = CHR is assumed.
 *
 * For consistency, callers are advised to include backslash into both CHR
 * and XTAB.
 *
 * Unless ENDP is NULL, upon return it receives a pointer to the character
 * in STR at which copying stopped.  Thus, if SIZE was sufficient, *ENDP will
 * point to '\0' in STR.
 *
 * Upon successful return, the function returns the number of characters copied
 * to BUF (excluding the terminating '\0' byte).
 *
 * The function does not write more than SIZE byte to the buffer (including the
 * terminating '\0' byte).  If the output was truncated due to this limit, the
 * return value is the number of bytes (excluding the terminating '\0') which
 * would have been written to BUF.  Thus, a return value of SIZE or more means
 * that the output was truncated.
 *
 * If STR or CHR is NULL, or SIZE is 0, returns -1 and sets errno to EINVAL.
 *
 * Example:
 *
 * Escape each occurrence of backslash and double quote:
 *
 *   mu_c_str_escape_buf (str, "\\\"", NULL, buf, size, &end)
 */
ssize_t
mu_c_str_escape_buf (char const *str, char const *chr, char const *xtab,
		     char *buf, size_t size, char const **endp)
{
  int c;
  size_t i;

  if (!str || !buf || size == 0)
    {
      errno = EINVAL;
      return -1;
    }
  if (str[0] == 0)
    {
      buf[0] = 0;
      return 0;
    }

  if (!chr)
    {
      strncpy (buf, str, size);
      return strlen (buf);
    }

  if (xtab)
    {
      if (strlen (xtab) != strlen (chr))
	{
	  errno = EINVAL;
	  return -1;
	}
    }
  else
    xtab = chr;

  i = 0;
  size--;
  while ((c = *str) != 0)
    {
      char *p = strchr (chr, c);

      if (p)
	{
	  if (size - i < 2)
	    break;
	  buf[i++] = '\\';
	  buf[i++] = xtab[p - chr];
	}
      else if (size == i)
	break;
      else
	buf[i++] = c;
      str++;
    }
  buf[i] = 0;
  if (endp)
    *endp = str;
  if (*str)
    i += mu_c_str_escape_length (str, chr);

  return i;
}

/*
 * Same as above, but use a previously allocated buffer, pointed to
 * by PBUF, and its size, pointed to by PSIZE.  If size is not sufficient,
 * or *PBUF is NULL, the buffer will be reallocated as necessary.
 */
int
mu_c_str_escape_mem (char const *str, char const *chr, char const *xtab,
		     char **pbuf, size_t *psize)
{
  char *buf;
  size_t size;
  size_t n;
  int c;

  if (!pbuf || !psize)
    return MU_ERR_OUT_PTR_NULL;

  if (!str)
    {
      if (!*pbuf || *psize == 0)
	{
	  if ((*pbuf = malloc (1)) == NULL)
	    return ENOMEM;
	  *psize = 1;
	}
      **pbuf = 0;
      return 0;
    }

  if (!chr || (n = strlen (chr)) == 0)
    {
      n = strlen (str) + 1;
      if (!*pbuf || *psize < n)
	{
	  if ((buf = strdup (str)) == NULL)
	    return errno;
	  *pbuf = buf;
	  *psize = n;
	}
      else
	strcpy (*pbuf, str);
      return 0;
    }

  if (xtab)
    {
      if (strlen (xtab) != n)
	return EINVAL;
    }
  else
    xtab = chr;

  buf = *pbuf;
  size = *psize;
  n = mu_c_str_escape_length (str, chr) + 1;
  if (n > size)
    {
      if ((buf = realloc (buf, n)) == NULL)
	return ENOMEM;
      *pbuf = buf;
      *psize = n;
    }

  while ((c = *str++) != 0)
    {
      char *p = strchr (chr, c);

      if (p)
	{
	  *buf++ = '\\';
	  *buf++ = xtab[p - chr];
	}
      else
	*buf++ = c;
    }
  *buf = 0;

  return 0;
}

/*
 * Same as mu_c_str_escape_mem, but always allocates a new buffer and
 * returns a pointer to it in *RET_STR.
 */
int
mu_c_str_escape (char const *str, char const *chr, char const *xtab,
		 char **ret_str)
{
  size_t size = 0;
  *ret_str = NULL;
  return mu_c_str_escape_mem (str, chr, xtab, ret_str, &size);
}

/*
 * Escape certain characters in STR.  Return allocated string in RET_STR.
 *
 * Escapable characters are defined by the array TRANS, which consists of an
 * even number of elements.  Each pair of characters in this array contains:
 *
 *   TRANS[i+1]  - character to be escaped
 *   TRANS[i]    - character to use in escape sequence for TRANS[i+1].
 *
 * Each TRANS[i+1] is replaced by backslash + TRANS[i].
 *
 * Returns 0 on success, or error code if an error occurred.
 *
 * E.g., to escape control characters, backslash and double-quote using
 * C convention:
 *
 *    mu_c_str_escape_trans (str, "\\\\\"\"a\ab\bf\fn\nr\rt\tv\v", &ret)
 *
 * See also mu_wordsplit_c_escape_tab in wordsplit.c
 */
int
mu_c_str_escape_trans (char const *str, char const *trans, char **ret_str)
{
  char *chr, *xtab;
  size_t n, i;
  int rc;

  if (trans)
    {
      n = strlen (trans);
      if (n % 2)
	return EINVAL;
      chr = malloc (n + 2);
      if (!chr)
	return errno;
      xtab = chr + n / 2 + 1;
      for (i = 0; i < n; i += 2)
	{
	  chr[i / 2] = trans[i + 1];
	  xtab[i / 2] = trans[i];
	}
      chr[i / 2] = xtab[i / 2] = 0;
    }
  else
    {
      chr = xtab = NULL;
    }

  rc = mu_c_str_escape (str, chr, xtab, ret_str);

  free (chr);

  return rc;
}
