/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 2008-2023 The ProFTPD Project team
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
 *
 * As a special exemption, Public Flood Software/MacGyver aka Habeeb J. Dihu
 * and other respective copyright holders give permission to link this program
 * with OpenSSL, and distribute the resulting executable, without including
 * the source code for OpenSSL in the source distribution.
 */

/* String manipulation functions. */

#include "conf.h"

/* Maximum number of matches that we will do in a given string. */
#define PR_STR_MAX_MATCHES			128

static const char *str_vreplace(pool *p, unsigned int max_replaces,
    const char *s, va_list args) {
  const char *src;
  char *m, *r, *cp;
  char *matches[PR_STR_MAX_MATCHES+1], *replaces[PR_STR_MAX_MATCHES+1];
  char buf[PR_TUNABLE_PATH_MAX] = {'\0'}, *pbuf = NULL;
  size_t nmatches = 0, rlen = 0;
  int blen = 0;

  src = s;
  cp = buf;
  *cp = '\0';

  memset(matches, 0, sizeof(matches));
  memset(replaces, 0, sizeof(replaces));

  blen = strlen(src) + 1;

  while ((m = va_arg(args, char *)) != NULL &&
         nmatches < PR_STR_MAX_MATCHES) {
    char *tmp = NULL;
    unsigned int count = 0;

    r = va_arg(args, char *);
    if (r == NULL) {
      break;
    }

    /* Increase the length of the needed buffer by the difference between
     * the given match and replacement strings, multiplied by the number
     * of times the match string occurs in the source string.
     */
    tmp = strstr(s, m);
    while (tmp) {
      pr_signals_handle();
      count++;
      if (count > max_replaces) {
        errno = E2BIG;
        return NULL;
      }

      /* Be sure to increment the pointer returned by strstr(3), to
       * advance past the beginning of the substring for which we are
       * looking.  Otherwise, we just loop endlessly, seeing the same
       * value for tmp over and over.
       */
      tmp += strlen(m);
      tmp = strstr(tmp, m);
    }

    /* We are only concerned about match/replacement strings that actually
     * occur in the given string.
     */
    if (count) {
      blen += count * (strlen(r) - strlen(m));
      if (blen < 0) {
        /* Integer overflow. In order to overflow this, somebody must be
         * doing something very strange. The possibility still exists that
         * we might not catch this overflow in extreme corner cases, but
         * massive amounts of data (gigabytes) would need to be in s to
         * trigger this, easily larger than any buffer we might use.
         */
        return s;
      }
      matches[nmatches] = m;
      replaces[nmatches++] = r;
    }
  }

  /* If there are no matches, then there is nothing to replace. */
  if (nmatches == 0) {
    return s;
  }

  /* Try to handle large buffer situations (i.e. escaping of PR_TUNABLE_PATH_MAX
   * (>2048) correctly, but do not allow very big buffer sizes, that may
   * be dangerous (BUFSIZ may be defined in stdio.h) in some library
   * functions.
   */
#ifndef BUFSIZ
# define BUFSIZ 8192
#endif

  if (blen >= BUFSIZ) {
    errno = ENOSPC;
    return NULL;
  }

  cp = pbuf = (char *) pcalloc(p, ++blen);

  while (*src) {
    char **mptr, **rptr;

    for (mptr = matches, rptr = replaces; *mptr; mptr++, rptr++) {
      size_t mlen;

      pr_signals_handle();

      mlen = strlen(*mptr);
      rlen = strlen(*rptr);

      if (strncmp(src, *mptr, mlen) == 0) {
        sstrncpy(cp, *rptr, blen - strlen(pbuf));

        if (((cp + rlen) - pbuf + 1) > blen) {
          pr_log_pri(PR_LOG_ERR,
            "WARNING: attempt to overflow internal ProFTPD buffers");
          cp = pbuf;

          cp += (blen - 1);
          goto done;

        } else {
          cp += rlen;
        }
	
        src += mlen;
        break;
      }
    }

    if (!*mptr) {
      if ((cp - pbuf + 1) >= blen) {
        pr_log_pri(PR_LOG_ERR,
          "WARNING: attempt to overflow internal ProFTPD buffers");
        cp = pbuf;

        cp += (blen - 1);
        goto done;
      }

      *cp++ = *src++;
    }
  }

 done:
  *cp = '\0';

  return pbuf;
}

const char *pr_str_quote(pool *p, const char *str) {
  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  return sreplace(p, str, "\"", "\"\"", NULL);
}

const char *quote_dir(pool *p, char *path) {
  return pr_str_quote(p, path);
}

const char *pr_str_replace(pool *p, unsigned int max_replaces,
    const char *s, ...) {
  va_list args;
  const char *res = NULL;

  if (p == NULL ||
      s == NULL ||
      max_replaces == 0) {
    errno = EINVAL;
    return NULL;
  }

  va_start(args, s);
  res = str_vreplace(p, max_replaces, s, args);
  va_end(args);

  return res;
}

const char *sreplace(pool *p, const char *s, ...) {
  va_list args;
  const char *res = NULL;

  if (p == NULL ||
      s == NULL) {
    errno = EINVAL;
    return NULL;
  }

  va_start(args, s);
  res = str_vreplace(p, PR_STR_MAX_REPLACEMENTS, s, args);
  va_end(args);

  if (res == NULL &&
      errno == E2BIG) {
    /* For backward compatible behavior. */
    return s;
  }

  return res;
}

/* "safe" strcat, saves room for NUL at end of dst, and refuses to copy more
 * than "n" bytes.
 */
char *sstrcat(char *dst, const char *src, size_t n) {
  register char *d = dst;

  if (dst == NULL ||
      src == NULL ||
      n == 0) {
    errno = EINVAL;
    return NULL;
  }

  /* Edge case short ciruit; strlcat(3) doesn't do what I think it should
   * do for this particular case.
   */
  if (n > 1) {
#if defined(HAVE_STRLCAT)
    strlcat(dst, src, n);

#else
    for (; *d && n > 1; d++, n--) {
    }

    while (n-- > 1 && *src) {
      *d++ = *src++;
    }

    *d = '\0';
#endif /* HAVE_STRLCAT */

  } else {
    *d = '\0';
  }

  return dst;
}

char *pstrdup(pool *p, const char *str) {
  char *res;
  size_t len;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(str) + 1;

  res = palloc(p, len);
  if (res != NULL) {
    sstrncpy(res, str, len);
  }

  return res;
}

char *pstrndup(pool *p, const char *str, size_t n) {
  char *res;

  if (!p || !str) {
    errno = EINVAL;
    return NULL;
  }

  res = palloc(p, n + 1);
  sstrncpy(res, str, n + 1);
  return res;
}

char *pdircat(pool *p, ...) {
  char *argp, *ptr, *res;
  char last;
  int count = 0;
  size_t len = 0, res_len = 0;
  va_list ap;

  if (p == NULL) {
    errno = EINVAL;
    return NULL;
  }

  va_start(ap, p);

  last = 0;

  while ((res = va_arg(ap, char *)) != NULL) {
    /* If the first argument is "", we have to account for a leading /
     * which must be added.
     */
    if (!count++ && !*res) {
      len++;

    } else if (last && last != '/' && *res != '/') {
      len++;

    } else if (last && last == '/' && *res == '/') {
      len--;
    }

    res_len = strlen(res);
    len += res_len;
    last = (*res ? res[res_len-1] : 0);
  }

  va_end(ap);
  ptr = res = (char *) pcalloc(p, len + 1);

  va_start(ap, p);

  last = res_len = 0;

  while ((argp = va_arg(ap, char *)) != NULL) {
    size_t arglen;

    if (last && last == '/' && *argp == '/') {
      argp++;

    } else if (last && last != '/' && *argp != '/') {
      sstrcat(ptr, "/", len + 1);
      ptr += 1;
      res_len += 1;
    }

    arglen = strlen(argp);
    sstrcat(ptr, argp, len + 1);
    ptr += arglen;
    res_len += arglen;

    last = (*res ? res[res_len-1] : 0);
  }

  va_end(ap);

  return res;
}

char *pstrcat(pool *p, ...) {
  char *argp, *ptr, *res;
  size_t len = 0;
  va_list ap;

  if (p == NULL) {
    errno = EINVAL;
    return NULL;
  }

  va_start(ap, p);

  while ((res = va_arg(ap, char *)) != NULL) {
    len += strlen(res);
  }

  va_end(ap);

  ptr = res = pcalloc(p, len + 1);

  va_start(ap, p);

  while ((argp = va_arg(ap, char *)) != NULL) {
    size_t arglen;

    arglen = strlen(argp);
    sstrcat(ptr, argp, len + 1);
    ptr += arglen;
  }

  va_end(ap);

  return res;
}

int pr_strnrstr(const char *s, size_t slen, const char *suffix,
    size_t suffixlen, int flags) {
  int res = FALSE;

  if (s == NULL ||
      suffix == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (slen == 0) {
    slen = strlen(s);
  }

  if (suffixlen == 0) {
    suffixlen = strlen(suffix);
  }

  if (slen == 0 &&
      suffixlen == 0) {
    return TRUE;
  }

  if (slen == 0 ||
      suffixlen == 0) {
    return FALSE;
  }

  if (suffixlen > slen) {
    return FALSE;
  }

  if (flags & PR_STR_FL_IGNORE_CASE) {
    if (strncasecmp(s + (slen - suffixlen), suffix, suffixlen) == 0) {
      res = TRUE;
    }

  } else {
    if (strncmp(s + (slen - suffixlen), suffix, suffixlen) == 0) {
      res = TRUE;
    }
  }

  return res;
}

const char *pr_str_strip(pool *p, const char *str) {
  const char *dup_str, *start, *finish;
  size_t len = 0;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  /* First, find the non-whitespace start of the given string */
  for (start = str; PR_ISSPACE(*start); start++) {
  }

  /* Now, find the non-whitespace end of the given string */
  for (finish = &str[strlen(str)-1]; PR_ISSPACE(*finish); finish--) {
  }

  /* Include for the last byte, of course. */
  len = finish - start + 1;

  /* The space-stripped string is, then, everything from start to finish. */
  dup_str = pstrndup(p, start, len);

  return dup_str;
}

char *pr_str_strip_end(char *s, const char *ch) {
  size_t len;

  if (s == NULL ||
      ch == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(s);

  while (len && strchr(ch, *(s+len - 1))) {
    pr_signals_handle();

    *(s+len - 1) = '\0';
    len--;
  }

  return s;
}

#if defined(HAVE_STRTOULL) && \
  (SIZEOF_UID_T == SIZEOF_LONG_LONG && SIZEOF_GID_T == SIZEOF_LONG_LONG)
static int parse_ull(const char *val, unsigned long long *num) {
  char *endp = NULL;
  unsigned long long res;

  res = strtoull(val, &endp, 10);
  if (endp && *endp) {
    errno = EINVAL;
    return -1;
  }

  *num = res;
  return 0;
}
#endif /* HAVE_STRTOULL */

static int parse_ul(const char *val, unsigned long *num) {
  char *endp = NULL;
  unsigned long res;

  res = strtoul(val, &endp, 10);
  if (endp && *endp) {
    errno = EINVAL;
    return -1;
  }

  *num = res;
  return 0;
}

char *pr_str_bin2hex(pool *p, const unsigned char *buf, size_t len, int flags) {
  static const char *hex_lc = "0123456789abcdef", *hex_uc = "0123456789ABCDEF";
  register unsigned int i;
  const char *hex_vals;
  char *hex, *ptr;
  size_t hex_len;

  if (p == NULL ||
      buf == NULL) {
    errno = EINVAL;
    return NULL;
  }

  if (len == 0) {
    return pstrdup(p, "");
  }

  /* By default, we use lowercase hex values. */
  hex_vals = hex_lc;
  if (flags & PR_STR_FL_HEX_USE_UC) {
    hex_vals = hex_uc;
  }

  hex_len = (len * 2) + 1;
  hex = palloc(p, hex_len);

  ptr = hex;
  for (i = 0; i < len; i++) {
    *ptr++ = hex_vals[buf[i] >> 4];
    *ptr++ = hex_vals[buf[i] % 16];
  }
  *ptr = '\0';

  return hex;
}

static int c2h(char c, unsigned char *h) {
  if (c >= '0' &&
      c <= '9') {
    *h = c - '0';
    return TRUE;
  }

  if (c >= 'a' &&
      c <= 'f') {
    *h = c - 'a' + 10;
    return TRUE;
  }

  if (c >= 'A' &&
      c <= 'F') {
    *h = c - 'A' + 10;
    return TRUE;
  }

  return FALSE;
}

unsigned char *pr_str_hex2bin(pool *p, const unsigned char *hex, size_t hex_len,
    size_t *len) {
  register unsigned int i, j;
  unsigned char *data;
  size_t data_len;

  if (p == NULL ||
      hex == NULL) {
    errno = EINVAL;
    return NULL;
  }

  if (hex_len == 0) {
    hex_len = strlen((char *) hex);
  }

  if (hex_len == 0) {
    data = (unsigned char *) pstrdup(p, "");
    return data;
  }

  data_len = hex_len / 2;
  data = palloc(p, data_len);

  for (i = 0, j = 0; i < hex_len; i += 2) {
    unsigned char v1, v2;

    if (c2h(hex[i], &v1) == FALSE) {
      errno = ERANGE;
      return NULL;
    }

    if (c2h(hex[i+1], &v2) == FALSE) {
      errno = ERANGE;
      return NULL;
    }

    data[j++] = ((v1 << 4) | v2);
  }

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

  return data;
}

/* Calculate the Damerau-Levenshtein distance between strings `a' and `b'.
 * This implementation borrows from the git implementation; see
 * git/src/levenshtein.c.
 */
int pr_str_levenshtein(pool *p, const char *a, const char *b, int swap_cost,
    int subst_cost, int insert_cost, int del_cost, int flags) {
  size_t alen, blen;
  unsigned int i, j;
  int *row0, *row1, *row2, res;
  pool *tmp_pool;

  if (p == NULL ||
      a == NULL ||
      b == NULL) {
    errno = EINVAL;
    return -1;
  }

  alen = strlen(a);
  blen = strlen(b);

  tmp_pool = make_sub_pool(p);
  pr_pool_tag(tmp_pool, "Levenshtein Distance pool");

  if (flags & PR_STR_FL_IGNORE_CASE) {
    char *a2, *b2;

    a2 = pstrdup(tmp_pool, a);
    for (i = 0; i < alen; i++) {
      a2[i] = tolower((int) a[i]);
    }

    b2 = pstrdup(tmp_pool, b);
    for (i = 0; i < blen; i++) {
      b2[i] = tolower((int) b[i]);
    }

    a = a2;
    b = b2;
  }

  row0 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));
  row1 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));
  row2 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));

  for (j = 0; j <= blen; j++) {
    row1[j] = j * insert_cost;
  }

  for (i = 0; i < alen; i++) {
    int *ptr;

    row2[0] = (i + 1) * del_cost;
    for (j = 0; j < blen; j++) {
      /* Substitution */
      row2[j + 1] = row1[j] + (subst_cost * (a[i] != b[j]));

      /* Swap */
      if (i > 0 &&
          j > 0 &&
          a[i-1] == b[j] &&
          a[i] == b[j-1] &&
          row2[j+1] > (row0[j-1] + swap_cost)) {
        row2[j+1] = row0[j-1] + swap_cost;
      }

      /* Deletion */
      if (row2[j+1] > (row1[j+1] + del_cost)) {
        row2[j+1] = row1[j+1] + del_cost;
      }

      /* Insertion */
      if (row2[j+1] > (row2[j] + insert_cost)) {
        row2[j+1] = row2[j] + insert_cost;
      }
    }

    ptr = row0;
    row0 = row1;
    row1 = row2;
    row2 = ptr;
  }

  res = row2[blen];

  destroy_pool(tmp_pool);
  return res;
}

/* For tracking the Levenshtein distance for a string. */
struct candidate {
  const char *s;
  int distance;
  int flags;
};

static int distance_cmp(const void *a, const void *b) {
  const struct candidate *cand1, *cand2;
  const char *s1, *s2;
  int distance1, distance2;

  cand1 = *((const struct candidate **) a);
  s1 = cand1->s;
  distance1 = cand1->distance;

  cand2 = *((const struct candidate **) b);
  s2 = cand2->s;
  distance2 = cand2->distance;

  if (distance1 != distance2) {
    return distance1 - distance2;
  }

  if (cand1->flags & PR_STR_FL_IGNORE_CASE) {
    return strcasecmp(s1, s2);
  }

  return strcmp(s1, s2);
}

array_header *pr_str_get_similars(pool *p, const char *s,
    array_header *candidates, int max_distance, int flags) {
  register unsigned int i;
  size_t len;
  array_header *similars;
  struct candidate **distances;
  pool *tmp_pool;

  if (p == NULL ||
      s == NULL ||
      candidates == NULL) {
    errno = EINVAL;
    return NULL;
  }

  if (candidates->nelts == 0) {
    errno = ENOENT;
    return NULL;
  }

  if (max_distance <= 0) {
    max_distance = PR_STR_DEFAULT_MAX_EDIT_DISTANCE;
  }

  tmp_pool = make_sub_pool(p);
  pr_pool_tag(tmp_pool, "Similar Strings pool");

  /* In order to use qsort(3), we need a contiguous block of memory, not
   * one of our array_headers.
   */

  distances = pcalloc(tmp_pool, candidates->nelts * sizeof(struct candidate *));

  len = strlen(s);
  for (i = 0; i < candidates->nelts; i++) {
    const char *c;
    struct candidate *cand;
    int prefix_match = FALSE;

    c = ((const char **) candidates->elts)[i];
    cand = pcalloc(tmp_pool, sizeof(struct candidate));
    cand->s = c;
    cand->flags = flags;

    /* Give prefix matches a higher score */
    if (flags & PR_STR_FL_IGNORE_CASE) {
      if (strncasecmp(c, s, len) == 0) {
        prefix_match = TRUE;
      }

    } else {
      if (strncmp(c, s, len) == 0) {
        prefix_match = TRUE;
      }
    }

    if (prefix_match == TRUE) {
      cand->distance = 0;

    } else {
      /* Note: We arbitrarily add one to the edit distance, in order to
       * distinguish a distance of zero from our prefix match "distances" of
       * zero above.
       */
      cand->distance = pr_str_levenshtein(tmp_pool, s, c, 0, 2, 1, 3,
        flags) + 1;
    }

    distances[i] = cand;
  }

  qsort(distances, candidates->nelts, sizeof(struct candidate *), distance_cmp);

  similars = make_array(p, candidates->nelts, sizeof(const char *));
  for (i = 0; i < candidates->nelts; i++) {
    struct candidate *cand;

    cand = distances[i];
    if (cand->distance <= max_distance) {
      *((const char **) push_array(similars)) = cand->s;
    }
  }

  destroy_pool(tmp_pool);
  return similars;
}

array_header *pr_str_text_to_array(pool *p, const char *text, char delimiter) {
  char *ptr;
  array_header *items;
  size_t text_len;

  if (p == NULL ||
      text == NULL) {
    errno = EINVAL;
    return NULL;
  }

  text_len = strlen(text);
  items = make_array(p, 1, sizeof(char *));

  if (text_len == 0) {
    return items;
  }

  ptr = memchr(text, delimiter, text_len);
  while (ptr != NULL) {
    size_t item_len;

    pr_signals_handle();

    item_len = ptr - text;
    if (item_len > 0) {
      char *item;

      item = palloc(p, item_len + 1);
      memcpy(item, text, item_len);
      item[item_len] = '\0';
      *((char **) push_array(items)) = item;
    }

    text = ++ptr;

    /* Include one byte for the delimiter character being skipped over. */
    text_len = text_len - item_len - 1;

    if (text_len == 0) {
      break;
    }

    ptr = memchr(text, delimiter, text_len);
  }

  if (text_len > 0) {
    *((char **) push_array(items)) = pstrdup(p, text);
  }

  return items;
}

char *pr_str_array_to_text(pool *p, const array_header *items,
    const char *delimiter) {
  register unsigned int i;
  char **elts, *text = "";

  if (p == NULL ||
      items == NULL ||
      delimiter == NULL) {
    errno = EINVAL;
    return NULL;
  }

  if (items->nelts == 0) {
    return pstrdup(p, "");
  }

  elts = items->elts;
  for (i = 0; i < items->nelts; i++) {
    char *elt;

    elt = elts[i];
    text = pstrcat(p, text, *text ? delimiter : "", elt, NULL);
  }

  return text;
}

int pr_str2uid(const char *val, uid_t *uid) {
#ifdef HAVE_STRTOULL
  unsigned long long ull = 0ULL;
#endif /* HAVE_STRTOULL */
  unsigned long ul = 0UL;

  if (val == NULL ||
      uid == NULL) {
    errno = EINVAL;
    return -1;
  }

#if SIZEOF_UID_T == SIZEOF_LONG_LONG
# ifdef HAVE_STRTOULL
  if (parse_ull(val, &ull) < 0) {
    return -1;
  }
  *uid = ull;

# else
  if (parse_ul(val, &ul) < 0) {
    return -1;
  }
  *uid = ul;
# endif /* HAVE_STRTOULL */
#else
  (void) ull;
  if (parse_ul(val, &ul) < 0) {
    return -1;
  }
  *uid = ul;
#endif /* sizeof(uid_t) != sizeof(long long) */

  return 0;
}

int pr_str2gid(const char *val, gid_t *gid) {
#ifdef HAVE_STRTOULL
  unsigned long long ull = 0ULL;
#endif /* HAVE_STRTOULL */
  unsigned long ul = 0UL;

  if (val == NULL ||
      gid == NULL) {
    errno = EINVAL;
    return -1;
  }

#if SIZEOF_GID_T == SIZEOF_LONG_LONG
# ifdef HAVE_STRTOULL
  if (parse_ull(val, &ull) < 0) {
    return -1;
  }
  *gid = ull;

# else
  if (parse_ul(val, &ul) < 0) {
    return -1;
  }
  *gid = ul;
# endif /* HAVE_STRTOULL */
#else
  (void) ull;
  if (parse_ul(val, &ul) < 0) {
    return -1;
  }
  *gid = ul;
#endif /* sizeof(gid_t) != sizeof(long long) */

  return 0;
}

const char *pr_uid2str(pool *p, uid_t uid) {
  static char buf[64];

  memset(&buf, 0, sizeof(buf));
  if (uid != (uid_t) -1) {
#if SIZEOF_UID_T == SIZEOF_LONG_LONG
    pr_snprintf(buf, sizeof(buf)-1, "%llu", (unsigned long long) uid);
#else
    pr_snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) uid);
#endif /* sizeof(uid_t) != sizeof(long long) */
  } else {
    pr_snprintf(buf, sizeof(buf)-1, "%d", -1);
  }

  if (p != NULL) {
    return pstrdup(p, buf);
  }

  return buf;
}

const char *pr_gid2str(pool *p, gid_t gid) {
  static char buf[64];

  memset(&buf, 0, sizeof(buf));
  if (gid != (gid_t) -1) {
#if SIZEOF_GID_T == SIZEOF_LONG_LONG
    pr_snprintf(buf, sizeof(buf)-1, "%llu", (unsigned long long) gid);
#else
    pr_snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) gid);
#endif /* sizeof(gid_t) != sizeof(long long) */
  } else {
    pr_snprintf(buf, sizeof(buf)-1, "%d", -1);
  }

  if (p != NULL) {
    return pstrdup(p, buf);
  }

  return buf;
}

/* NOTE: Update mod_ban's ban_parse_timestr() to use this function. */
int pr_str_get_duration(const char *str, int *duration) {
  int hours, mins, secs;
  int flags = PR_STR_FL_IGNORE_CASE, has_suffix = FALSE;
  size_t len;
  char *ptr = NULL;

  if (str == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (sscanf(str, "%2d:%2d:%2d", &hours, &mins, &secs) == 3) {
    if (hours < 0 ||
        mins < 0 ||
        secs < 0) {
      errno = ERANGE;
      return -1;
    }

    if (duration != NULL) {
      *duration = (hours * 60 * 60) + (mins * 60) + secs;
    }

    return 0;
  }

  len = strlen(str);
  if (len == 0) {
    errno = EINVAL;
    return -1;
  }

  /* Handle the "single component" formats:
   *
   * If ends with "S", "s", or "sec": parse secs
   * If ends with "M", "m", or "min": parse minutes
   * If ends with "H", "h", or "hr": parse hours
   *
   * Otherwise, try to parse as just a number of seconds.
   */

  has_suffix = pr_strnrstr(str, len, "s", 1, flags);
  if (has_suffix == FALSE) {
    has_suffix = pr_strnrstr(str, len, "sec", 3, flags);
  }
  if (has_suffix == TRUE) {
    /* Parse seconds */

    if (sscanf(str, "%d", &secs) == 1) {
      if (secs < 0) {
        errno = ERANGE;
        return -1;
      }

      if (duration != NULL) {
        *duration = secs;
      }

      return 0;
    }

    errno = EINVAL;
    return -1;
  }

  has_suffix = pr_strnrstr(str, len, "m", 1, flags);
  if (has_suffix == FALSE) {
    has_suffix = pr_strnrstr(str, len, "min", 3, flags);
  }
  if (has_suffix == TRUE) {
    /* Parse minutes */

    if (sscanf(str, "%d", &mins) == 1) {
      if (mins < 0) {
        errno = ERANGE;
        return -1;
      }

      if (duration != NULL) {
        *duration = (mins * 60);
      }

      return 0;
    }

    errno = EINVAL;
    return -1;
  }

  has_suffix = pr_strnrstr(str, len, "h", 1, flags);
  if (has_suffix == FALSE) {
    has_suffix = pr_strnrstr(str, len, "hr", 2, flags);
  }
  if (has_suffix == TRUE) {
    /* Parse hours */

    if (sscanf(str, "%d", &hours) == 1) {
      if (hours < 0) {
        errno = ERANGE;
        return -1;
      }

      if (duration != NULL) {
        *duration = (hours * 60 * 60);
      }

      return 0;
    }

    errno = EINVAL;
    return -1;
  }

  /* Use strtol(3) here, check for trailing garbage, etc. */
  secs = (int) strtol(str, &ptr, 10);
  if (ptr && *ptr) {
    /* Not a bare number, but a string with non-numeric characters. */
    errno = EINVAL;
    return -1;
  }

  if (secs < 0) {
    errno = ERANGE;
    return -1;
  }

  if (duration != NULL) {
    *duration = secs;
  }

  return 0;
}

int pr_str_get_nbytes(const char *str, const char *units, off_t *nbytes) {
  off_t sz;
  char *ptr = NULL;
  float factor = 0.0;

  if (str == NULL) {
    errno = EINVAL;
    return -1;
  }

  /* No negative numbers. */
  if (*str == '-') {
    errno = EINVAL;
    return -1;
  }

  if (units == NULL ||
      *units == '\0') {
    factor = 1.0;

  } else if (strncasecmp(units, "KB", 3) == 0) {
    factor = 1024.0;

  } else if (strncasecmp(units, "MB", 3) == 0) {
    factor = 1024.0 * 1024.0;

  } else if (strncasecmp(units, "GB", 3) == 0) {
    factor = 1024.0 * 1024.0 * 1024.0;

  } else if (strncasecmp(units, "TB", 3) == 0) {
    factor = 1024.0 * 1024.0 * 1024.0 * 1024.0;

  } else if (strncasecmp(units, "B", 2) == 0) {
    factor = 1.0;

  } else {
    errno = EINVAL;
    return -1;
  }

  errno = 0;

#ifdef HAVE_STRTOULL
  sz = strtoull(str, &ptr, 10);
#else
  sz = strtoul(str, &ptr, 10);
#endif /* !HAVE_STRTOULL */

  if (errno == ERANGE) {
    return -1;
  }

  if (ptr != NULL && *ptr) {
    /* Error parsing the given string */
    errno = EINVAL;
    return -1;
  }

  /* Don't bother applying the factor if the result will overflow the result. */
#ifdef ULLONG_MAX
  if (sz > (ULLONG_MAX / factor)) {
#else
  if (sz > (ULONG_MAX / factor)) {
#endif /* !ULLONG_MAX */
    errno = ERANGE;
    return -1;
  }

  if (nbytes != NULL) {
    *nbytes = (off_t) (sz * factor);
  }

  return 0;
}

char *pr_str_get_word(char **cp, int flags) {
  char *res, *dst;
  int quote_mode = FALSE;

  if (cp == NULL ||
     !*cp ||
     !**cp) {
    errno = EINVAL;
    return NULL;
  }

  if (!(flags & PR_STR_FL_PRESERVE_WHITESPACE)) {
    while (**cp && PR_ISSPACE(**cp)) {
      pr_signals_handle();
      (*cp)++;
    }
  }

  if (!**cp) {
    return NULL;
  }

  res = dst = *cp;

  if (!(flags & PR_STR_FL_PRESERVE_COMMENTS)) {
    /* Stop processing at start of an inline comment. */
    if (**cp == '#') {
      return NULL;
    }
  }

  if (!(flags & PR_STR_FL_IGNORE_QUOTES)) {
    if (**cp == '\"') {
      quote_mode = TRUE;
      (*cp)++;
    }
  }

  while (**cp && (quote_mode ? (**cp != '\"') : !PR_ISSPACE(**cp))) {
    pr_signals_handle();

    if (**cp == '\\' &&
        quote_mode == TRUE) {
      /* Escaped char */
      if (*((*cp)+1)) {
        *dst++ = *(++(*cp));
        (*cp)++;
        continue;
      }
    }

    *dst++ = **cp;
    (*cp)++;
  }

  if (**cp) {
    (*cp)++;
  }

  *dst = '\0';
  return res;
}

/* get_token tokenizes a string, increments the src pointer to the next
 * non-separator in the string.  If the src string is empty or NULL, the next
 * token returned is NULL.
 */
char *pr_str_get_token2(char **src, char *sep, size_t *token_len) {
  char *token;
  size_t len = 0;

  if (src == NULL ||
      *src == NULL ||
      **src == '\0' ||
      sep == NULL) {

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

    errno = EINVAL;
    return NULL;
  }

  token = *src;

  while (**src && !strchr(sep, **src)) {
    (*src)++;
    len++;
  }

  if (**src) {
    *(*src)++ = '\0';
  }

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

  return token;
}

char *pr_str_get_token(char **src, char *sep) {
  return pr_str_get_token2(src, sep, NULL);
}

int pr_str_is_boolean(const char *str) {
  if (str == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (strncasecmp(str, "on", 3) == 0) {
    return TRUE;
  }

  if (strncasecmp(str, "off", 4) == 0) {
    return FALSE;
  }

  if (strncasecmp(str, "yes", 4) == 0) {
    return TRUE;
  }

  if (strncasecmp(str, "no", 3) == 0) {
    return FALSE;
  }

  if (strncasecmp(str, "true", 5) == 0) {
    return TRUE;
  }

  if (strncasecmp(str, "false", 6) == 0) {
    return FALSE;
  }

  if (strncasecmp(str, "1", 2) == 0) {
    return TRUE;
  }

  if (strncasecmp(str, "0", 2) == 0) {
    return FALSE;
  }

  errno = EINVAL;
  return -1;
}

/* Return true if str contains any of the glob(7) characters. */
int pr_str_is_fnmatch(const char *str) {
  int have_bracket = 0;

  if (str == NULL) {
    return FALSE;
  }

  while (*str) {
    switch (*str) {
      case '?':
      case '*':
        return TRUE;

      case '\\':
        /* If the next character is NUL, we've reached the end of the string. */
        if (*(str+1) == '\0') {
          return FALSE;
        }

        /* Skip past the escaped character, i.e. the next character. */
        str++;
        break;

      case '[':
        have_bracket++;
        break;

      case ']':
        if (have_bracket) {
          return TRUE;
        }
        break;

      default:
        break;
    }

    str++;
  }

  return FALSE;
}
