Blob Blame History Raw
/*
   expr.c - limited shell-like expression parsing functions
   This file is part of the nss-pam-ldapd library.

   Copyright (C) 2009-2016 Arthur de Jong
   Copyright (c) 2012 Thorsten Glaser <t.glaser@tarent.de>
   Copyright (c) 2016 Giovanni Mascellani <gio@debian.org>

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

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

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301 USA
*/

#include "config.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include "expr.h"
#include "compat/attrs.h"

/* the maximum length of a variable name */
#define MAXVARLENGTH 30

static inline int my_isalpha(const char c)
{
  return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
}

static inline int my_isdigit(const char c)
{
  return (c >= '0') && (c <= '9');
}

static inline int my_isalphanum(const char c)
{
  return my_isalpha(c) || ((c >= '0') && (c <= '9'));
}

/* return the part of the string that is a valid name */
MUST_USE static const char *parse_name(const char *str, int *ptr,
                                       char *buffer, size_t buflen)
{
  int i = 0;
  /* clear the buffer */
  buffer[i] = '\0';
  /* look for an alpha + alphanumeric* string */
  if (!my_isalpha(str[*ptr]))
    return NULL;
  while (my_isalphanum(str[*ptr]) || (str[*ptr] == ';'))
  {
    if ((size_t)i >= buflen)
      return NULL;
    buffer[i++] = str[(*ptr)++];
  }
  /* NULL-terminate the string */
  if ((size_t)i >= buflen)
    return NULL;
  buffer[i++] = '\0';
  return buffer;
}

/* dummy expander function to always return an empty string */
static const char *empty_expander(const char UNUSED(*name),
                                  void UNUSED(*expander_arg))
{
  return "";
}

/* definition of the parse functions (they call eachother) */
MUST_USE static const char *parse_dollar_expression(
              const char *str, int *ptr, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg);
MUST_USE static const char *parse_expression(
              const char *str, int *ptr, int endat, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg);

/* handle ${attr:-word} expressions */
MUST_USE static const char *parse_dollar_default(
              const char *str, int *ptr, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg,
              const char *varvalue)
{
  if ((varvalue != NULL) && (*varvalue != '\0'))
  {
    /* value is set, skip rest of expression and use value */
    if (parse_expression(str, ptr, '}', buffer, buflen, empty_expander, NULL) == NULL)
      return NULL;
    if (strlen(varvalue) >= buflen)
      return NULL;
    strcpy(buffer, varvalue);
  }
  else
  {
    /* value is not set, evaluate rest of expression */
    if (parse_expression(str, ptr, '}', buffer, buflen, expander, expander_arg) == NULL)
      return NULL;
  }
  return buffer;
}

/* handle ${attr:+word} expressions */
MUST_USE static const char *parse_dollar_alternative(
              const char *str, int *ptr, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg,
              const char *varvalue)
{
  if ((varvalue != NULL) && (*varvalue != '\0'))
  {
    /* value is set, evaluate rest of expression */
    if (parse_expression(str, ptr, '}', buffer, buflen, expander, expander_arg) == NULL)
      return NULL;
  }
  else
  {
    /* value is not set, skip rest of expression and blank */
    if (parse_expression(str, ptr, '}', buffer, buflen, empty_expander, NULL) == NULL)
      return NULL;
    buffer[0] = '\0';
  }
  return buffer;
}

/* handle ${attr:offset:length} expressions */
MUST_USE static const char *parse_dollar_substring(
              const char *str, int *ptr, char *buffer, size_t buflen,
              const char *varvalue)
{
  char *tmp;
  unsigned long int offset, length;
  size_t varlen;
  /* parse input */
  tmp = (char *)str + *ptr;
  if (!my_isdigit(*tmp))
    return NULL;
  errno = 0;
  offset = strtoul(tmp, &tmp, 10);
  if ((*tmp != ':') || (errno != 0))
    return NULL;
  tmp += 1;
  errno = 0;
  length = strtoul(tmp, &tmp, 10);
  if ((*tmp != '}') || (errno != 0))
    return NULL;
  /* don't skip closing '}' here, because it will be skipped later */
  *ptr += tmp - (str + *ptr);
  varlen = strlen(varvalue);
  if (offset > varlen)
    offset = varlen;
  if (offset + length > varlen)
    length = varlen - offset;
  if (length >= buflen)
    return NULL;
  /* everything's ok, copy data; we use memcpy instead of strncpy
     because we already know the exact lenght in play */
  memcpy(buffer, varvalue + offset, length);
  buffer[length] = '\0';
  return buffer;
}

/* handle ${attr#word} expressions */
MUST_USE static const char *parse_dollar_match(
              const char *str, int *ptr, char *buffer, size_t buflen,
              const char *varvalue)
{
  char c;
  const char *cp, *vp;
  int ismatch;
  size_t vallen;
  cp = str + *ptr;
  vp = varvalue;
  ismatch = 1;
  while (((c = *cp++) != '\0') && (c != '}'))
  {
    if (ismatch && (*vp =='\0'))
      ismatch = 0; /* varvalue shorter than trim string */
    if (c == '?')
    {
      /* match any one character */
      vp++;
      continue;
    }
    if (c == '\\')
    {
      if ((c = *cp++) == '\0')
        return NULL; /* end of input: syntax error */
      /* escape the next character c */
    }
    if (ismatch && (*vp != c))
      ismatch = 0; /* they differ */
    vp++;
  }
  if (c == '\0')
    return NULL; /* end of input: syntax error */
  /* at this point, cp points to after the closing } */
  (*ptr) = cp - str - 1;
  /* if ismatch, vp points to the beginning of the
     data after trimming, otherwise vp is invalid */
  if (!ismatch)
    vp = varvalue;
  /* now copy the (trimmed or not) value to the buffer */
  if ((vallen = strlen(vp) + 1) > buflen)
    return NULL;
  memcpy(buffer, vp, vallen);
  return buffer;
}

MUST_USE static const char *parse_dollar_expression(
              const char *str, int *ptr, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg)
{
  char varname[MAXVARLENGTH];
  const char *varvalue;
  if ((buflen <= 0) || (buffer == NULL) || (str == NULL) || (ptr == NULL))
    return NULL;
  if (str[*ptr] == '{')
  {
    (*ptr)++;
    /* the first part is always a variable name */
    if (parse_name(str, ptr, varname, sizeof(varname)) == NULL)
      return NULL;
    varvalue = expander(varname, expander_arg);
    if (varvalue == NULL)
      varvalue = "";
    if (str[*ptr] == '}')
    {
      /* simple substitute */
      if (strlen(varvalue) >= buflen)
        return NULL;
      strcpy(buffer, varvalue);
    }
    else if (strncmp(str + *ptr, ":-", 2) == 0)
    {
      /* if variable is not set or empty, substitute remainder */
      (*ptr) += 2;
      if (parse_dollar_default(str, ptr, buffer, buflen, expander, expander_arg, varvalue) == NULL)
        return NULL;
    }
    else if (strncmp(str + *ptr, ":+", 2) == 0)
    {
      /* if variable is set, substitute remainder */
      (*ptr) += 2;
      if (parse_dollar_alternative(str, ptr, buffer, buflen, expander, expander_arg, varvalue) == NULL)
        return NULL;
    }
    else if (str[*ptr] == ':')
    {
      /* substitute substring of variable */
      (*ptr) += 1;
      if (parse_dollar_substring(str, ptr, buffer, buflen, varvalue) == NULL)
        return NULL;
    }
    else if (str[*ptr] == '#')
    {
      /* try to strip the remainder value from variable beginning */
      (*ptr) += 1;
      if (parse_dollar_match(str, ptr, buffer, buflen, varvalue) == NULL)
        return NULL;
    }
    else
      return NULL;
    (*ptr)++; /* skip closing } */
  }
  else
  {
    /* it is a simple reference to a variable, like $uidNumber */
    if (parse_name(str, ptr, varname, sizeof(varname)) == NULL)
      return NULL;
    varvalue = expander(varname, expander_arg);
    if (varvalue == NULL)
      varvalue = "";
    if (strlen(varvalue) >= buflen)
      return NULL;
    strcpy(buffer, varvalue);
  }
  return buffer;
}

MUST_USE static const char *parse_expression(
              const char *str, int *ptr, int endat, char *buffer, size_t buflen,
              expr_expander_func expander, void *expander_arg)
{
  int j = 0;
  /* go over string */
  while ((str[*ptr] != endat) && (str[*ptr] != '\0'))
  {
    switch (str[*ptr])
    {
      case '$': /* beginning of an expression */
        (*ptr)++;
        if ((size_t)j >= buflen)
          return NULL;
        if (parse_dollar_expression(str, ptr, buffer + j, buflen - j,
                                    expander, expander_arg) == NULL)
          return NULL;
        j = strlen(buffer);
        break;
      case '\\': /* escaped character, unescape */
        (*ptr)++;
        FALLTHROUGH; /* no break needed here */
      default: /* just copy the text */
        if ((size_t)j >= buflen)
          return NULL;
        buffer[j++] = str[*ptr];
        (*ptr)++;
    }
  }
  /* NULL-terminate buffer */
  if ((size_t)j >= buflen)
    return NULL;
  buffer[j++] = '\0';
  return buffer;
}

MUST_USE const char *expr_parse(const char *str, char *buffer, size_t buflen,
                                expr_expander_func expander, void *expander_arg)
{
  int i = 0;
  return parse_expression(str, &i, '\0', buffer, buflen,
                          expander, expander_arg);
}

SET *expr_vars(const char *str, SET *set)
{
  char varname[MAXVARLENGTH];
  int i = 0;
  /* allocate set if needed */
  if (set == NULL)
    set = set_new();
  if (set == NULL)
    return NULL;
  /* go over string */
  while (str[i] != '\0')
  {
    switch (str[i])
    {
      case '$': /* beginning of a $-expression */
        i++;
        if (str[i] == '{')
          i++;
        /* the rest should start with a variable name */
        if (parse_name(str, &i, varname, sizeof(varname)) != NULL)
          set_add(set, varname);
        break;
      case '\\': /* escaped character, unescape */
        i++;
        FALLTHROUGH; /* no break needed here */
      default: /* just skip */
        i++;
    }
  }
  return set;
}