diff options
Diffstat (limited to 'src/test.c')
-rw-r--r-- | src/test.c | 863 |
1 files changed, 863 insertions, 0 deletions
diff --git a/src/test.c b/src/test.c new file mode 100644 index 0000000..cea7dc1 --- /dev/null +++ b/src/test.c @@ -0,0 +1,863 @@ +/* GNU test program (ksb and mjb) */ + +/* Modified to run with the GNU shell by bfox. */ + +/* Copyright (C) 1987-2022 Free Software Foundation, Inc. + + 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 3 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, see <https://www.gnu.org/licenses/>. */ + +/* Define TEST_STANDALONE to get the /bin/test version. Otherwise, you get + the shell builtin version. */ + +/* Without this pragma, gcc 4.6.2 20111027 mistakenly suggests that + the advance function might be candidate for attribute 'pure'. */ +#if (__GNUC__ == 4 && 6 <= __GNUC_MINOR__) || 4 < __GNUC__ +# pragma GCC diagnostic ignored "-Wsuggest-attribute=pure" +#endif + +#include <config.h> +#include <stdio.h> +#include <sys/types.h> + +#define TEST_STANDALONE 1 + +#ifndef LBRACKET +# define LBRACKET 0 +#endif + +/* The official name of this program (e.g., no 'g' prefix). */ +#if LBRACKET +# define PROGRAM_NAME "[" +#else +# define PROGRAM_NAME "test" +#endif + +#include "system.h" +#include "quote.h" +#include "stat-time.h" +#include "strnumcmp.h" + +#include <stdarg.h> +#include "verror.h" + +#if HAVE_SYS_PARAM_H +# include <sys/param.h> +#endif + +/* Exit status for syntax errors, etc. */ +enum { TEST_TRUE, TEST_FALSE, TEST_FAILURE }; + +#if defined TEST_STANDALONE +# define test_exit(val) exit (val) +# define test_main_return(val) return val +#else + static jmp_buf test_exit_buf; + static int test_error_return = 0; +# define test_exit(val) test_error_return = val, longjmp (test_exit_buf, 1) +# define test_main_return(val) test_exit (val) +#endif /* !TEST_STANDALONE */ + +static int pos; /* The offset of the current argument in ARGV. */ +static int argc; /* The number of arguments present in ARGV. */ +static char **argv; /* The argument list. */ + +static bool unary_operator (void); +static bool binary_operator (bool); +static bool two_arguments (void); +static bool three_arguments (void); +static bool posixtest (int); + +static bool expr (void); +static bool term (void); +static bool and (void); +static bool or (void); + +static void beyond (void); + +ATTRIBUTE_FORMAT ((printf, 1, 2)) +static _Noreturn void +test_syntax_error (char const *format, ...) +{ + va_list ap; + va_start (ap, format); + verror (0, 0, format, ap); + test_exit (TEST_FAILURE); +} + +/* Increment our position in the argument list. Check that we're not + past the end of the argument list. This check is suppressed if the + argument is false. */ + +static void +advance (bool f) +{ + ++pos; + + if (f && pos >= argc) + beyond (); +} + +static void +unary_advance (void) +{ + advance (true); + ++pos; +} + +/* + * beyond - call when we're beyond the end of the argument list (an + * error condition) + */ +static void +beyond (void) +{ + test_syntax_error (_("missing argument after %s"), quote (argv[argc - 1])); +} + +/* If the characters pointed to by STRING constitute a valid number, + return a pointer to the start of the number, skipping any blanks or + leading '+'. Otherwise, report an error and exit. */ +static char const * +find_int (char const *string) +{ + char const *p; + char const *number_start; + + for (p = string; isblank (to_uchar (*p)); p++) + continue; + + if (*p == '+') + { + p++; + number_start = p; + } + else + { + number_start = p; + p += (*p == '-'); + } + + if (ISDIGIT (*p++)) + { + while (ISDIGIT (*p)) + p++; + while (isblank (to_uchar (*p))) + p++; + if (!*p) + return number_start; + } + + test_syntax_error (_("invalid integer %s"), quote (string)); +} + +/* Find the modification time of FILE, and stuff it into *MTIME. + Return true if successful. */ +static bool +get_mtime (char const *filename, struct timespec *mtime) +{ + struct stat finfo; + bool ok = (stat (filename, &finfo) == 0); + if (ok) + *mtime = get_stat_mtime (&finfo); + return ok; +} + +/* Return true if S is one of the test command's binary operators. */ +static bool +binop (char const *s) +{ + return ((STREQ (s, "=")) || (STREQ (s, "!=")) || (STREQ (s, "==")) || + (STREQ (s, "-nt")) || + (STREQ (s, "-ot")) || (STREQ (s, "-ef")) || (STREQ (s, "-eq")) || + (STREQ (s, "-ne")) || (STREQ (s, "-lt")) || (STREQ (s, "-le")) || + (STREQ (s, "-gt")) || (STREQ (s, "-ge"))); +} + +/* + * term - parse a term and return 1 or 0 depending on whether the term + * evaluates to true or false, respectively. + * + * term ::= + * '-'('h'|'d'|'f'|'r'|'s'|'w'|'c'|'b'|'p'|'u'|'g'|'k') filename + * '-'('L'|'x') filename + * '-t' int + * '-'('z'|'n') string + * string + * string ('!='|'=') string + * <int> '-'(eq|ne|le|lt|ge|gt) <int> + * file '-'(nt|ot|ef) file + * '(' <expr> ')' + * int ::= + * '-l' string + * positive and negative integers + */ +static bool +term (void) +{ + bool value; + bool negated = false; + + /* Deal with leading 'not's. */ + while (pos < argc && argv[pos][0] == '!' && argv[pos][1] == '\0') + { + advance (true); + negated = !negated; + } + + if (pos >= argc) + beyond (); + + /* A paren-bracketed argument. */ + if (argv[pos][0] == '(' && argv[pos][1] == '\0') + { + int nargs; + + advance (true); + + for (nargs = 1; + pos + nargs < argc && ! STREQ (argv[pos + nargs], ")"); + nargs++) + if (nargs == 4) + { + nargs = argc - pos; + break; + } + + value = posixtest (nargs); + if (argv[pos] == 0) + test_syntax_error (_("%s expected"), quote (")")); + else + if (argv[pos][0] != ')' || argv[pos][1]) + test_syntax_error (_("%s expected, found %s"), + quote_n (0, ")"), quote_n (1, argv[pos])); + advance (false); + } + + /* Are there enough arguments left that this could be dyadic? */ + else if (4 <= argc - pos && STREQ (argv[pos], "-l") && binop (argv[pos + 2])) + value = binary_operator (true); + else if (3 <= argc - pos && binop (argv[pos + 1])) + value = binary_operator (false); + + /* It might be a switch type argument. */ + else if (argv[pos][0] == '-' && argv[pos][1] && argv[pos][2] == '\0') + value = unary_operator (); + else + { + value = (argv[pos][0] != '\0'); + advance (false); + } + + return negated ^ value; +} + +static bool +binary_operator (bool l_is_l) +{ + int op; + struct stat stat_buf, stat_spare; + /* Is the right integer expression of the form '-l string'? */ + bool r_is_l; + + if (l_is_l) + advance (false); + op = pos + 1; + + if ((op < argc - 2) && STREQ (argv[op + 1], "-l")) + { + r_is_l = true; + advance (false); + } + else + r_is_l = false; + + if (argv[op][0] == '-') + { + /* check for eq, nt, and stuff */ + if ((((argv[op][1] == 'l' || argv[op][1] == 'g') + && (argv[op][2] == 'e' || argv[op][2] == 't')) + || (argv[op][1] == 'e' && argv[op][2] == 'q') + || (argv[op][1] == 'n' && argv[op][2] == 'e')) + && !argv[op][3]) + { + char lbuf[INT_BUFSIZE_BOUND (uintmax_t)]; + char rbuf[INT_BUFSIZE_BOUND (uintmax_t)]; + char const *l = (l_is_l + ? umaxtostr (strlen (argv[op - 1]), lbuf) + : find_int (argv[op - 1])); + char const *r = (r_is_l + ? umaxtostr (strlen (argv[op + 2]), rbuf) + : find_int (argv[op + 1])); + int cmp = strintcmp (l, r); + bool xe_operator = (argv[op][2] == 'e'); + pos += 3; + return (argv[op][1] == 'l' ? cmp < xe_operator + : argv[op][1] == 'g' ? cmp > - xe_operator + : (cmp != 0) == xe_operator); + } + + switch (argv[op][1]) + { + default: + break; + + case 'n': + if (argv[op][2] == 't' && !argv[op][3]) + { + /* nt - newer than */ + struct timespec lt, rt; + bool le, re; + pos += 3; + if (l_is_l || r_is_l) + test_syntax_error (_("-nt does not accept -l")); + le = get_mtime (argv[op - 1], <); + re = get_mtime (argv[op + 1], &rt); + return le && (!re || timespec_cmp (lt, rt) > 0); + } + break; + + case 'e': + if (argv[op][2] == 'f' && !argv[op][3]) + { + /* ef - hard link? */ + pos += 3; + if (l_is_l || r_is_l) + test_syntax_error (_("-ef does not accept -l")); + return (stat (argv[op - 1], &stat_buf) == 0 + && stat (argv[op + 1], &stat_spare) == 0 + && stat_buf.st_dev == stat_spare.st_dev + && stat_buf.st_ino == stat_spare.st_ino); + } + break; + + case 'o': + if ('t' == argv[op][2] && '\000' == argv[op][3]) + { + /* ot - older than */ + struct timespec lt, rt; + bool le, re; + pos += 3; + if (l_is_l || r_is_l) + test_syntax_error (_("-ot does not accept -l")); + le = get_mtime (argv[op - 1], <); + re = get_mtime (argv[op + 1], &rt); + return re && (!le || timespec_cmp (lt, rt) < 0); + } + break; + } + + /* FIXME: is this dead code? */ + test_syntax_error (_("%s: unknown binary operator"), quote (argv[op])); + } + + if (argv[op][0] == '=' + && (!argv[op][1] || ((argv[op][1] == '=') && !argv[op][2]))) + { + bool value = STREQ (argv[pos], argv[pos + 2]); + pos += 3; + return value; + } + + if (STREQ (argv[op], "!=")) + { + bool value = !STREQ (argv[pos], argv[pos + 2]); + pos += 3; + return value; + } + + /* Not reached. */ + abort (); +} + +static bool +unary_operator (void) +{ + struct stat stat_buf; + + switch (argv[pos][1]) + { + default: + test_syntax_error (_("%s: unary operator expected"), quote (argv[pos])); + return false; + + /* All of the following unary operators use unary_advance (), which + checks to make sure that there is an argument, and then advances + pos right past it. This means that pos - 1 is the location of the + argument. */ + + case 'e': /* file exists in the file system? */ + unary_advance (); + return stat (argv[pos - 1], &stat_buf) == 0; + + case 'r': /* file is readable? */ + unary_advance (); + return euidaccess (argv[pos - 1], R_OK) == 0; + + case 'w': /* File is writable? */ + unary_advance (); + return euidaccess (argv[pos - 1], W_OK) == 0; + + case 'x': /* File is executable? */ + unary_advance (); + return euidaccess (argv[pos - 1], X_OK) == 0; + + case 'N': /* File exists and has been modified since it was last read? */ + { + unary_advance (); + if (stat (argv[pos - 1], &stat_buf) != 0) + return false; + struct timespec atime = get_stat_atime (&stat_buf); + struct timespec mtime = get_stat_mtime (&stat_buf); + return (timespec_cmp (mtime, atime) > 0); + } + + case 'O': /* File is owned by you? */ + { + unary_advance (); + if (stat (argv[pos - 1], &stat_buf) != 0) + return false; + errno = 0; + uid_t euid = geteuid (); + uid_t NO_UID = -1; + return ! (euid == NO_UID && errno) && euid == stat_buf.st_uid; + } + + case 'G': /* File is owned by your group? */ + { + unary_advance (); + if (stat (argv[pos - 1], &stat_buf) != 0) + return false; + errno = 0; + gid_t egid = getegid (); + gid_t NO_GID = -1; + return ! (egid == NO_GID && errno) && egid == stat_buf.st_gid; + } + + case 'f': /* File is a file? */ + unary_advance (); + /* Under POSIX, -f is true if the given file exists + and is a regular file. */ + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISREG (stat_buf.st_mode)); + + case 'd': /* File is a directory? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISDIR (stat_buf.st_mode)); + + case 's': /* File has something in it? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && 0 < stat_buf.st_size); + + case 'S': /* File is a socket? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISSOCK (stat_buf.st_mode)); + + case 'c': /* File is character special? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISCHR (stat_buf.st_mode)); + + case 'b': /* File is block special? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISBLK (stat_buf.st_mode)); + + case 'p': /* File is a named pipe? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && S_ISFIFO (stat_buf.st_mode)); + + case 'L': /* Same as -h */ + /*FALLTHROUGH*/ + + case 'h': /* File is a symbolic link? */ + unary_advance (); + return (lstat (argv[pos - 1], &stat_buf) == 0 + && S_ISLNK (stat_buf.st_mode)); + + case 'u': /* File is setuid? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && (stat_buf.st_mode & S_ISUID)); + + case 'g': /* File is setgid? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && (stat_buf.st_mode & S_ISGID)); + + case 'k': /* File has sticky bit set? */ + unary_advance (); + return (stat (argv[pos - 1], &stat_buf) == 0 + && (stat_buf.st_mode & S_ISVTX)); + + case 't': /* File (fd) is a terminal? */ + { + long int fd; + char const *arg; + unary_advance (); + arg = find_int (argv[pos - 1]); + errno = 0; + fd = strtol (arg, NULL, 10); + return (errno != ERANGE && 0 <= fd && fd <= INT_MAX && isatty (fd)); + } + + case 'n': /* True if arg has some length. */ + unary_advance (); + return argv[pos - 1][0] != 0; + + case 'z': /* True if arg has no length. */ + unary_advance (); + return argv[pos - 1][0] == '\0'; + } +} + +/* + * and: + * term + * term '-a' and + */ +static bool +and (void) +{ + bool value = true; + + while (true) + { + value &= term (); + if (! (pos < argc && STREQ (argv[pos], "-a"))) + return value; + advance (false); + } +} + +/* + * or: + * and + * and '-o' or + */ +static bool +or (void) +{ + bool value = false; + + while (true) + { + value |= and (); + if (! (pos < argc && STREQ (argv[pos], "-o"))) + return value; + advance (false); + } +} + +/* + * expr: + * or + */ +static bool +expr (void) +{ + if (pos >= argc) + beyond (); + + return or (); /* Same with this. */ +} + +static bool +one_argument (void) +{ + return argv[pos++][0] != '\0'; +} + +static bool +two_arguments (void) +{ + bool value; + + if (STREQ (argv[pos], "!")) + { + advance (false); + value = ! one_argument (); + } + else if (argv[pos][0] == '-' + && argv[pos][1] != '\0' + && argv[pos][2] == '\0') + { + value = unary_operator (); + } + else + beyond (); + return (value); +} + +static bool +three_arguments (void) +{ + bool value; + + if (binop (argv[pos + 1])) + value = binary_operator (false); + else if (STREQ (argv[pos], "!")) + { + advance (true); + value = !two_arguments (); + } + else if (STREQ (argv[pos], "(") && STREQ (argv[pos + 2], ")")) + { + advance (false); + value = one_argument (); + advance (false); + } + else if (STREQ (argv[pos + 1], "-a") || STREQ (argv[pos + 1], "-o")) + value = expr (); + else + test_syntax_error (_("%s: binary operator expected"), + quote (argv[pos + 1])); + return (value); +} + +/* This is an implementation of a Posix.2 proposal by David Korn. */ +static bool +posixtest (int nargs) +{ + bool value; + + switch (nargs) + { + case 1: + value = one_argument (); + break; + + case 2: + value = two_arguments (); + break; + + case 3: + value = three_arguments (); + break; + + case 4: + if (STREQ (argv[pos], "!")) + { + advance (true); + value = !three_arguments (); + break; + } + if (STREQ (argv[pos], "(") && STREQ (argv[pos + 3], ")")) + { + advance (false); + value = two_arguments (); + advance (false); + break; + } + FALLTHROUGH; + case 5: + default: + if (nargs <= 0) + abort (); + value = expr (); + } + + return (value); +} + +#if defined TEST_STANDALONE + +void +usage (int status) +{ + if (status != EXIT_SUCCESS) + emit_try_help (); + else + { + fputs (_("\ +Usage: test EXPRESSION\n\ + or: test\n\ + or: [ EXPRESSION ]\n\ + or: [ ]\n\ + or: [ OPTION\n\ +"), stdout); + fputs (_("\ +Exit with the status determined by EXPRESSION.\n\ +\n\ +"), stdout); + fputs (HELP_OPTION_DESCRIPTION, stdout); + fputs (VERSION_OPTION_DESCRIPTION, stdout); + fputs (_("\ +\n\ +An omitted EXPRESSION defaults to false. Otherwise,\n\ +EXPRESSION is true or false and sets exit status. It is one of:\n\ +"), stdout); + fputs (_("\ +\n\ + ( EXPRESSION ) EXPRESSION is true\n\ + ! EXPRESSION EXPRESSION is false\n\ + EXPRESSION1 -a EXPRESSION2 both EXPRESSION1 and EXPRESSION2 are true\n\ + EXPRESSION1 -o EXPRESSION2 either EXPRESSION1 or EXPRESSION2 is true\n\ +"), stdout); + fputs (_("\ +\n\ + -n STRING the length of STRING is nonzero\n\ + STRING equivalent to -n STRING\n\ + -z STRING the length of STRING is zero\n\ + STRING1 = STRING2 the strings are equal\n\ + STRING1 != STRING2 the strings are not equal\n\ +"), stdout); + fputs (_("\ +\n\ + INTEGER1 -eq INTEGER2 INTEGER1 is equal to INTEGER2\n\ + INTEGER1 -ge INTEGER2 INTEGER1 is greater than or equal to INTEGER2\n\ + INTEGER1 -gt INTEGER2 INTEGER1 is greater than INTEGER2\n\ + INTEGER1 -le INTEGER2 INTEGER1 is less than or equal to INTEGER2\n\ + INTEGER1 -lt INTEGER2 INTEGER1 is less than INTEGER2\n\ + INTEGER1 -ne INTEGER2 INTEGER1 is not equal to INTEGER2\n\ +"), stdout); + fputs (_("\ +\n\ + FILE1 -ef FILE2 FILE1 and FILE2 have the same device and inode numbers\n\ + FILE1 -nt FILE2 FILE1 is newer (modification date) than FILE2\n\ + FILE1 -ot FILE2 FILE1 is older than FILE2\n\ +"), stdout); + fputs (_("\ +\n\ + -b FILE FILE exists and is block special\n\ + -c FILE FILE exists and is character special\n\ + -d FILE FILE exists and is a directory\n\ + -e FILE FILE exists\n\ +"), stdout); + fputs (_("\ + -f FILE FILE exists and is a regular file\n\ + -g FILE FILE exists and is set-group-ID\n\ + -G FILE FILE exists and is owned by the effective group ID\n\ + -h FILE FILE exists and is a symbolic link (same as -L)\n\ + -k FILE FILE exists and has its sticky bit set\n\ +"), stdout); + fputs (_("\ + -L FILE FILE exists and is a symbolic link (same as -h)\n\ + -N FILE FILE exists and has been modified since it was last read\n\ + -O FILE FILE exists and is owned by the effective user ID\n\ + -p FILE FILE exists and is a named pipe\n\ + -r FILE FILE exists and the user has read access\n\ + -s FILE FILE exists and has a size greater than zero\n\ +"), stdout); + fputs (_("\ + -S FILE FILE exists and is a socket\n\ + -t FD file descriptor FD is opened on a terminal\n\ + -u FILE FILE exists and its set-user-ID bit is set\n\ + -w FILE FILE exists and the user has write access\n\ + -x FILE FILE exists and the user has execute (or search) access\n\ +"), stdout); + fputs (_("\ +\n\ +Except for -h and -L, all FILE-related tests dereference symbolic links.\n\ +Beware that parentheses need to be escaped (e.g., by backslashes) for shells.\n\ +INTEGER may also be -l STRING, which evaluates to the length of STRING.\n\ +"), stdout); + fputs (_("\ +\n\ +NOTE: Binary -a and -o are inherently ambiguous. Use 'test EXPR1 && test\n\ +EXPR2' or 'test EXPR1 || test EXPR2' instead.\n\ +"), stdout); + fputs (_("\ +\n\ +NOTE: [ honors the --help and --version options, but test does not.\n\ +test treats each of those as it treats any other nonempty STRING.\n\ +"), stdout); + printf (USAGE_BUILTIN_WARNING, _("test and/or [")); + emit_ancillary_info (PROGRAM_NAME); + } + exit (status); +} +#endif /* TEST_STANDALONE */ + +#if !defined TEST_STANDALONE +# define main test_command +#endif + +#define AUTHORS \ + proper_name ("Kevin Braunsdorf"), \ + proper_name ("Matthew Bradburn") + +/* + * [: + * '[' expr ']' + * test: + * test expr + */ +int +main (int margc, char **margv) +{ + bool value; + +#if !defined TEST_STANDALONE + int code; + + code = setjmp (test_exit_buf); + + if (code) + return (test_error_return); +#else /* TEST_STANDALONE */ + initialize_main (&margc, &margv); + set_program_name (margv[0]); + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEDIR); + textdomain (PACKAGE); + + initialize_exit_failure (TEST_FAILURE); + atexit (close_stdout); +#endif /* TEST_STANDALONE */ + + argv = margv; + + if (LBRACKET) + { + /* Recognize --help or --version, but only when invoked in the + "[" form, when the last argument is not "]". Use direct + parsing, rather than parse_long_options, to avoid accepting + abbreviations. POSIX allows "[ --help" and "[ --version" to + have the usual GNU behavior, but it requires "test --help" + and "test --version" to exit silently with status 0. */ + if (margc == 2) + { + if (STREQ (margv[1], "--help")) + usage (EXIT_SUCCESS); + + if (STREQ (margv[1], "--version")) + { + version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS, + (char *) NULL); + test_main_return (EXIT_SUCCESS); + } + } + if (margc < 2 || !STREQ (margv[margc - 1], "]")) + test_syntax_error (_("missing %s"), quote ("]")); + + --margc; + } + + argc = margc; + pos = 1; + + if (pos >= argc) + test_main_return (TEST_FALSE); + + value = posixtest (argc - 1); + + if (pos != argc) + test_syntax_error (_("extra argument %s"), quote (argv[pos])); + + test_main_return (value ? TEST_TRUE : TEST_FALSE); +} |