/* test.c - evaluate expression
 *
 * Copyright 2018 Rob Landley <rob@landley.net>
 *
 * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html
 *
 * TODO sh [[ ]] options: <   aaa<bbb  >   bbb>aaa   ~= regex

USE_TEST(NEWTOY(test, 0, TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_NOHELP|TOYFLAG_MAYFORK))
USE_TEST_GLUE(OLDTOY([, test, TOYFLAG_BIN|TOYFLAG_MAYFORK|TOYFLAG_NOHELP))

config TEST
  bool "test"
  default y
  help
    usage: test [-bcdefghLPrSsuwx PATH] [-nz STRING] [-t FD] [X ?? Y]

    Return true or false by performing tests. (With no arguments return false.)

    --- Tests with a single argument (after the option):
    PATH is/has:
      -b  block device   -f  regular file   -p  fifo           -u  setuid bit
      -c  char device    -g  setgid         -r  read bit       -w  write bit
      -d  directory      -h  symlink        -S  socket         -x  execute bit
      -e  exists         -L  symlink        -s  nonzero size   -k  sticky bit
    STRING is:
      -n  nonzero size   -z  zero size      (STRING by itself implies -n)
    FD (integer file descriptor) is:
      -t  a TTY

    --- Tests with one argument on each side of an operator:
    Two strings:
      =  are identical   !=  differ

    Two integers:
      -eq  equal         -gt  first > second    -lt  first < second
      -ne  not equal     -ge  first >= second   -le  first <= second

    --- Modify or combine tests:
      ! EXPR     not (swap true/false)   EXPR -a EXPR    and (are both true)
      ( EXPR )   evaluate this first     EXPR -o EXPR    or (is either true)

config TEST_GLUE
  bool
  default y
  depends on TEST || SH
*/

#include "toys.h"

// Consume 3, 2, or 1 argument test, returning result and *count used.
static int do_test(char **args, int *count)
{
  char c, *s;
  int i;

  if (*count>=3) {
    *count = 3;
    char *s = args[1], *ss = "eqnegtgeltle";
    if (!strcmp(s, "=") || !strcmp(s, "==")) return !strcmp(args[0], args[2]);
    if (!strcmp(s, "!=")) return strcmp(args[0], args[2]);
    if (*s=='-' && strlen(s)==3 && (s = strstr(ss, s+1)) && !((i = s-ss)&1)) {
      long long a = atolx(args[0]), b = atolx(args[2]);

      if (!i) return a == b;
      if (i==2) return a != b;
      if (i==4) return a > b;
      if (i==6) return a >= b;
      if (i==8) return a < b;
      if (i==10) return a<= b;
    }
  }
  s = *args;
  if (*count>=2 && *s == '-' && s[1] && !s[2]) {
    *count = 2;
    c = s[1];
    if (-1 != (i = stridx("hLbcdefgkpSusxwr", c))) {
      struct stat st;

      // stat or lstat, then handle rwx and s
      if (-1 == ((i<2) ? lstat : stat)(args[1], &st)) return 0;
      if (i>=13) return !!(st.st_mode&(0111<<(i-13)));
      if (c == 's') return !!st.st_size; // otherwise 1<<32 == 0

      // handle file type checking and SUID/SGID
      if ((i = ((char []){80,80,48,16,32,0,64,2,1,8,96,4}[i])<<9)>=4096)
        return (st.st_mode&S_IFMT) == i;
      else return (st.st_mode & i) == i;
    } else if (c == 'z') return !*args[1];
    else if (c == 'n') return *args[1];
    else if (c == 't') return isatty(atolx(args[1]));
  }
  return *count = 0;
}

#define NOT 1  // Most recent test had an odd number of preceding !
#define AND 2  // test before -a failed since -o or ( so force false
#define OR  4  // test before -o succeeded since ( so force true
void test_main(void)
{
  char *s;
  int pos, paren, pstack, result = 0;

  toys.exitval = 2;
  if (CFG_TOYBOX && !strcmp("[", toys.which->name))
    if (!toys.optc || strcmp("]", toys.optargs[--toys.optc]))
      error_exit("Missing ']'");

  // loop through command line arguments
  if (toys.optc) for (pos = paren = pstack = 0; ; pos++) {
    int len = toys.optc-pos;

    if (!len) perror_exit("need arg @%d", pos);

    // Evaluate next test
    result = do_test(toys.optargs+pos, &len);
    pos += len;
    // Single argument could be ! ( or nonempty
    if (!len) {
      if (toys.optargs[pos+1]) {
        if (!strcmp("!", toys.optargs[pos])) {
          pstack ^= NOT;
          continue;
        }
        if (!strcmp("(", toys.optargs[pos])) {
          if (++paren>9) perror_exit("bad (");
          pstack <<= 3;
          continue;
        }
      }
      result = *toys.optargs[pos++];
    }
    s = toys.optargs[pos];
    for (;;) {

      // Handle pending ! -a -o (the else means -o beats -a)
      if (pstack&NOT) result = !result;
      pstack &= ~NOT;
      if (pstack&OR) result = 1;
      else if (pstack&AND) result = 0;

      // Do it again for every )
      if (!paren || pos==toys.optc || strcmp(")", s)) break;
      paren--;
      pstack >>= 3;
      s = toys.optargs[++pos];
    }

    // Out of arguments?
    if (pos==toys.optc) {
      if (paren) perror_exit("need )");
      break;
    }

    // are we followed by -a or -o?

    if (!strcmp("-a", s)) {
      if (!result) pstack |= AND;
    } else if (!strcmp("-o", s)) {
      // -o flushes -a even if previous test was false
      pstack &=~AND;
      if (result) pstack |= OR;
    } else error_exit("too many arguments");
  }

  // Invert C logic to get shell logic
  toys.exitval = !result;
}
