/* $NetBSD: test.c,v 1.33 2007/06/24 18:54:58 christos Exp $ */ /* * test(1); version 7-like -- author Erik Baalbergen * modified by Eric Gisin to be used as built-in. * modified by Arnold Robbins to add SVR3 compatibility * (-x -c -b -p -u -g -k) plus Korn's -L -nt -ot -ef and new -S (socket). * modified by J.T. Conklin for NetBSD. * * This program is in the Public Domain. */ /*#include #ifndef lint __RCSID("$NetBSD: test.c,v 1.33 2007/06/24 18:54:58 christos Exp $"); #endif*/ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include "config.h" #include #include #include #include "err.h" #include #include #include #include #include #ifdef _MSC_VER # include # include # include # include "mscfakes.h" # include "quote_argv.h" #else # include #endif #include #include #include "kmkbuiltin.h" /********************************************************************************************************************************* * Defined Constants And Macros * *********************************************************************************************************************************/ #ifndef __arraycount # define __arraycount(a) ( sizeof(a) / sizeof(a[0]) ) #endif /********************************************************************************************************************************* * Structures and Typedefs * *********************************************************************************************************************************/ /* test(1) accepts the following grammar: oexpr ::= aexpr | aexpr "-o" oexpr ; aexpr ::= nexpr | nexpr "-a" aexpr ; nexpr ::= primary | "!" primary primary ::= unary-operator operand | operand binary-operator operand | operand | "(" oexpr ")" ; unary-operator ::= "-r"|"-w"|"-x"|"-f"|"-d"|"-c"|"-b"|"-p"| "-u"|"-g"|"-k"|"-s"|"-t"|"-z"|"-n"|"-o"|"-O"|"-G"|"-L"|"-S"; binary-operator ::= "="|"!="|"-eq"|"-ne"|"-ge"|"-gt"|"-le"|"-lt"| "-nt"|"-ot"|"-ef"; operand ::= */ enum token { EOI, FILRD, FILWR, FILEX, FILEXIST, FILREG, FILDIR, FILCDEV, FILBDEV, FILFIFO, FILSOCK, FILSYM, FILGZ, FILTT, FILSUID, FILSGID, FILSTCK, FILNT, FILOT, FILEQ, FILUID, FILGID, STREZ, STRNZ, STREQ, STRNE, STRLT, STRGT, INTEQ, INTNE, INTGE, INTGT, INTLE, INTLT, UNOT, BAND, BOR, LPAREN, RPAREN, OPERAND }; enum token_types { UNOP, BINOP, BUNOP, BBINOP, PAREN }; struct t_op { const char *op_text; short op_num, op_type; }; /** kmk_test instance data. */ typedef struct TESTINSTANCE { PKMKBUILTINCTX pCtx; char **t_wp; struct t_op const *t_wp_op; } TESTINSTANCE; typedef TESTINSTANCE *PTESTINSTANCE; /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ static const struct t_op cop[] = { {"!", UNOT, BUNOP}, {"(", LPAREN, PAREN}, {")", RPAREN, PAREN}, {"<", STRLT, BINOP}, {"=", STREQ, BINOP}, {">", STRGT, BINOP}, }; static const struct t_op cop2[] = { {"!=", STRNE, BINOP}, }; static const struct t_op mop3[] = { {"ef", FILEQ, BINOP}, {"eq", INTEQ, BINOP}, {"ge", INTGE, BINOP}, {"gt", INTGT, BINOP}, {"le", INTLE, BINOP}, {"lt", INTLT, BINOP}, {"ne", INTNE, BINOP}, {"nt", FILNT, BINOP}, {"ot", FILOT, BINOP}, }; static const struct t_op mop2[] = { {"G", FILGID, UNOP}, {"L", FILSYM, UNOP}, {"O", FILUID, UNOP}, {"S", FILSOCK,UNOP}, {"a", BAND, BBINOP}, {"b", FILBDEV,UNOP}, {"c", FILCDEV,UNOP}, {"d", FILDIR, UNOP}, {"e", FILEXIST,UNOP}, {"f", FILREG, UNOP}, {"g", FILSGID,UNOP}, {"h", FILSYM, UNOP}, /* for backwards compat */ {"k", FILSTCK,UNOP}, {"n", STRNZ, UNOP}, {"o", BOR, BBINOP}, {"p", FILFIFO,UNOP}, {"r", FILRD, UNOP}, {"s", FILGZ, UNOP}, {"t", FILTT, UNOP}, {"u", FILSUID,UNOP}, {"w", FILWR, UNOP}, {"x", FILEX, UNOP}, {"z", STREZ, UNOP}, }; /********************************************************************************************************************************* * Internal Functions * *********************************************************************************************************************************/ static int syntax(PTESTINSTANCE, const char *, const char *); static int oexpr(PTESTINSTANCE, enum token); static int aexpr(PTESTINSTANCE, enum token); static int nexpr(PTESTINSTANCE, enum token); static int primary(PTESTINSTANCE, enum token); static int binop(PTESTINSTANCE); static int test_access(struct stat *, mode_t); static int filstat(char *, enum token); static enum token t_lex(PTESTINSTANCE, char *); static int isoperand(PTESTINSTANCE); static int getn(PTESTINSTANCE, const char *); static int newerf(const char *, const char *); static int olderf(const char *, const char *); static int equalf(const char *, const char *); static int usage(PKMKBUILTINCTX, int); #if !defined(KMK_BUILTIN_STANDALONE) || defined(ELECTRIC_HEAP) extern void *xmalloc(unsigned int); #else extern void *xmalloc(unsigned int sz) { void *p = malloc(sz); if (!p) { fprintf(stderr, "kmk_test: malloc(%u) failed\n", sz); exit(1); } return p; } #endif int kmk_builtin_test(int argc, char **argv, char **envp, PKMKBUILTINCTX pCtx, char ***ppapszArgvSpawn) { TESTINSTANCE This; int res; char **argv_spawn; int i; This.pCtx = pCtx; This.t_wp = NULL; This.t_wp_op = NULL; /* look for the '--', '--help' and '--version'. */ argv_spawn = NULL; for (i = 1; i < argc; i++) { if ( argv[i][0] == '-' && argv[i][1] == '-') { if (argv[i][2] == '\0') { argc = i; argv[i] = NULL; /* skip blank arguments (happens inside kmk) */ while (argv[++i]) { const char *psz = argv[i]; while (isspace(*psz)) psz++; if (*psz) break; } argv_spawn = &argv[i]; break; } if (!strcmp(argv[i], "--help")) return usage(pCtx, 0); if (!strcmp(argv[i], "--version")) return kbuild_version(argv[0]); } } /* are we '['? then check for ']'. */ if (strcmp(argv[0], "[") == 0) { /** @todo should skip the path in g_progname */ if (strcmp(argv[--argc], "]")) return errx(pCtx, 1, "missing ]"); argv[argc] = NULL; } /* evaluate the expression */ if (argc < 2) res = 1; else { This.t_wp = &argv[1]; res = oexpr(&This, t_lex(&This, *This.t_wp)); if (res != -42 && *This.t_wp != NULL && *++This.t_wp != NULL) res = syntax(&This, *This.t_wp, "unexpected operator"); if (res == -42) return 1; /* don't mix syntax errors with the argv_spawn ignore */ res = !res; } /* anything to execute on success? */ if (argv_spawn) { if (res != 0 || !argv_spawn[0]) res = 0; /* ignored */ else { #ifdef KMK_BUILTIN_STANDALONE /* try exec the specified process */ # if defined(_MSC_VER) int argc_spawn = 0; while (argv_spawn[argc_spawn]) argc_spawn++; if (quote_argv(argc, argv_spawn, 0 /*fWatcomBrainDamage*/, 0/*fFreeOrLeak*/) != -1) { res = _spawnvp(_P_WAIT, argv_spawn[0], argv_spawn); if (res == -1) res = err(pCtx, 1, "_spawnvp(_P_WAIT,%s,..)", argv_spawn[0]); } else res = err(pCtx, 1, "quote_argv: out of memory"); # else execvp(argv_spawn[0], argv_spawn); res = err(pCtx, 1, "execvp(%s,..)", argv_spawn[0]); # endif #else /* in kmk */ /* let job.c spawn the process, make a job.c style argv_spawn copy. */ char *cur, **argv_new; size_t sz = 0; int argc_new = 0; while (argv_spawn[argc_new]) { size_t len = strlen(argv_spawn[argc_new]) + 1; sz += (len + sizeof(void *) - 1) & ~(sizeof(void *) - 1); argc_new++; } argv_new = xmalloc((argc_new + 1) * sizeof(char *)); cur = xmalloc(sz); for (i = 0; i < argc_new; i++) { size_t len = strlen(argv_spawn[i]) + 1; argv_new[i] = memcpy(cur, argv_spawn[i], len); cur += (len + sizeof(void *) - 1) & ~(sizeof(void *) - 1); } argv_new[i] = NULL; *ppapszArgvSpawn = argv_new; res = 0; #endif /* in kmk */ } } return res; } #ifdef KMK_BUILTIN_STANDALONE int main(int argc, char **argv, char **envp) { KMKBUILTINCTX Ctx = { "kmk_test", NULL }; return kmk_builtin_test(argc, argv, envp, &Ctx, NULL); } #endif static int syntax(PTESTINSTANCE pThis, const char *op, const char *msg) { if (op && *op) errx(pThis->pCtx, 1, "%s: %s", op, msg); else errx(pThis->pCtx, 1, "%s", msg); return -42; } static int oexpr(PTESTINSTANCE pThis, enum token n) { int res; res = aexpr(pThis, n); if (res == -42 || *pThis->t_wp == NULL) return res; if (t_lex(pThis, *++(pThis->t_wp)) == BOR) { int res2 = oexpr(pThis, t_lex(pThis, *++(pThis->t_wp))); return res2 != -42 ? res2 || res : res2; } pThis->t_wp--; return res; } static int aexpr(PTESTINSTANCE pThis, enum token n) { int res; res = nexpr(pThis, n); if (res == -42 || *pThis->t_wp == NULL) return res; if (t_lex(pThis, *++(pThis->t_wp)) == BAND) { int res2 = aexpr(pThis, t_lex(pThis, *++(pThis->t_wp))); return res2 != -42 ? res2 && res : res2; } pThis->t_wp--; return res; } static int nexpr(PTESTINSTANCE pThis, enum token n) { if (n == UNOT) { int res = nexpr(pThis, t_lex(pThis, *++(pThis->t_wp))); return res != -42 ? !res : res; } return primary(pThis, n); } static int primary(PTESTINSTANCE pThis, enum token n) { enum token nn; int res; if (n == EOI) return 0; /* missing expression */ if (n == LPAREN) { if ((nn = t_lex(pThis, *++(pThis->t_wp))) == RPAREN) return 0; /* missing expression */ res = oexpr(pThis, nn); if (res != -42 && t_lex(pThis, *++(pThis->t_wp)) != RPAREN) return syntax(pThis, NULL, "closing paren expected"); return res; } if (pThis->t_wp_op && pThis->t_wp_op->op_type == UNOP) { /* unary expression */ if (*++(pThis->t_wp) == NULL) return syntax(pThis, pThis->t_wp_op->op_text, "argument expected"); switch (n) { case STREZ: return strlen(*pThis->t_wp) == 0; case STRNZ: return strlen(*pThis->t_wp) != 0; case FILTT: return isatty(getn(pThis, *pThis->t_wp)); default: return filstat(*pThis->t_wp, n); } } if (t_lex(pThis, pThis->t_wp[1]), pThis->t_wp_op && pThis->t_wp_op->op_type == BINOP) { return binop(pThis); } return strlen(*pThis->t_wp) > 0; } static int binop(PTESTINSTANCE pThis) { const char *opnd1, *opnd2; struct t_op const *op; opnd1 = *pThis->t_wp; (void) t_lex(pThis, *++(pThis->t_wp)); op = pThis->t_wp_op; if ((opnd2 = *++(pThis->t_wp)) == NULL) return syntax(pThis, op->op_text, "argument expected"); switch (op->op_num) { case STREQ: return strcmp(opnd1, opnd2) == 0; case STRNE: return strcmp(opnd1, opnd2) != 0; case STRLT: return strcmp(opnd1, opnd2) < 0; case STRGT: return strcmp(opnd1, opnd2) > 0; case INTEQ: return getn(pThis, opnd1) == getn(pThis, opnd2); case INTNE: return getn(pThis, opnd1) != getn(pThis, opnd2); case INTGE: return getn(pThis, opnd1) >= getn(pThis, opnd2); case INTGT: return getn(pThis, opnd1) > getn(pThis, opnd2); case INTLE: return getn(pThis, opnd1) <= getn(pThis, opnd2); case INTLT: return getn(pThis, opnd1) < getn(pThis, opnd2); case FILNT: return newerf(opnd1, opnd2); case FILOT: return olderf(opnd1, opnd2); case FILEQ: return equalf(opnd1, opnd2); default: abort(); /* NOTREACHED */ #ifdef _MSC_VER return -42; #endif } } /* * The manual, and IEEE POSIX 1003.2, suggests this should check the mode bits, * not use access(): * * True shall indicate only that the write flag is on. The file is not * writable on a read-only file system even if this test indicates true. * * Unfortunately IEEE POSIX 1003.1-2001, as quoted in SuSv3, says only: * * True shall indicate that permission to read from file will be granted, * as defined in "File Read, Write, and Creation". * * and that section says: * * When a file is to be read or written, the file shall be opened with an * access mode corresponding to the operation to be performed. If file * access permissions deny access, the requested operation shall fail. * * and of course access permissions are described as one might expect: * * * If a process has the appropriate privilege: * * * If read, write, or directory search permission is requested, * access shall be granted. * * * If execute permission is requested, access shall be granted if * execute permission is granted to at least one user by the file * permission bits or by an alternate access control mechanism; * otherwise, access shall be denied. * * * Otherwise: * * * The file permission bits of a file contain read, write, and * execute/search permissions for the file owner class, file group * class, and file other class. * * * Access shall be granted if an alternate access control mechanism * is not enabled and the requested access permission bit is set for * the class (file owner class, file group class, or file other class) * to which the process belongs, or if an alternate access control * mechanism is enabled and it allows the requested access; otherwise, * access shall be denied. * * and when I first read this I thought: surely we can't go about using * open(O_WRONLY) to try this test! However the POSIX 1003.1-2001 Rationale * section for test does in fact say: * * On historical BSD systems, test -w directory always returned false * because test tried to open the directory for writing, which always * fails. * * and indeed this is in fact true for Seventh Edition UNIX, UNIX 32V, and UNIX * System III, and thus presumably also for BSD up to and including 4.3. * * Secondly I remembered why using open() and/or access() are bogus. They * don't work right for detecting read and write permissions bits when called * by root. * * Interestingly the 'test' in 4.4BSD was closer to correct (as per * 1003.2-1992) and it was implemented efficiently with stat() instead of * open(). * * This was apparently broken in NetBSD around about 1994/06/30 when the old * 4.4BSD implementation was replaced with a (arguably much better coded) * implementation derived from pdksh. * * Note that modern pdksh is yet different again, but still not correct, at * least not w.r.t. 1003.2-1992. * * As I think more about it and read more of the related IEEE docs I don't like * that wording about 'test -r' and 'test -w' in 1003.1-2001 at all. I very * much prefer the original wording in 1003.2-1992. It is much more useful, * and so that's what I've implemented. * * (Note that a strictly conforming implementation of 1003.1-2001 is in fact * totally useless for the case in question since its 'test -w' and 'test -r' * can never fail for root for any existing files, i.e. files for which 'test * -e' succeeds.) * * The rationale for 1003.1-2001 suggests that the wording was "clarified" in * 1003.1-2001 to align with the 1003.2b draft. 1003.2b Draft 12 (July 1999), * which is the latest copy I have, does carry the same suggested wording as is * in 1003.1-2001, with its rationale saying: * * This change is a clarification and is the result of interpretation * request PASC 1003.2-92 #23 submitted for IEEE Std 1003.2-1992. * * That interpretation can be found here: * * http://www.pasc.org/interps/unofficial/db/p1003.2/pasc-1003.2-23.html * * Not terribly helpful, unfortunately. I wonder who that fence sitter was. * * Worse, IMVNSHO, I think the authors of 1003.2b-D12 have mis-interpreted the * PASC interpretation and appear to be gone against at least one widely used * implementation (namely 4.4BSD). The problem is that for file access by root * this means that if test '-r' and '-w' are to behave as if open() were called * then there's no way for a shell script running as root to check if a file * has certain access bits set other than by the grotty means of interpreting * the output of 'ls -l'. This was widely considered to be a bug in V7's * "test" and is, I believe, one of the reasons why direct use of access() was * avoided in some more recent implementations! * * I have always interpreted '-r' to match '-w' and '-x' as per the original * wording in 1003.2-1992, not the other way around. I think 1003.2b goes much * too far the wrong way without any valid rationale and that it's best if we * stick with 1003.2-1992 and test the flags, and not mimic the behaviour of * open() since we already know very well how it will work -- existance of the * file is all that matters to open() for root. * * Unfortunately the SVID is no help at all (which is, I guess, partly why * we're in this mess in the first place :-). * * The SysV implementation (at least in the 'test' builtin in /bin/sh) does use * access(name, 2) even though it also goes to much greater lengths for '-x' * matching the 1003.2-1992 definition (which is no doubt where that definition * came from). * * The ksh93 implementation uses access() for '-r' and '-w' if * (euid==uid&&egid==gid), but uses st_mode for '-x' iff running as root. * i.e. it does strictly conform to 1003.1-2001 (and presumably 1003.2b). */ static int test_access(struct stat *sp, mode_t stmode) { #ifdef _MSC_VER /* just pretend to be root for now. */ stmode = (stmode << 6) | (stmode << 3) | stmode; return !!(sp->st_mode & stmode); #else gid_t *groups; register int n; uid_t euid; int maxgroups; /* * I suppose we could use access() if not running as root and if we are * running with ((euid == uid) && (egid == gid)), but we've already * done the stat() so we might as well just test the permissions * directly instead of asking the kernel to do it.... */ euid = geteuid(); if (euid == 0) /* any bit is good enough */ stmode = (stmode << 6) | (stmode << 3) | stmode; else if (sp->st_uid == euid) stmode <<= 6; else if (sp->st_gid == getegid()) stmode <<= 3; else { /* XXX stolen almost verbatim from ksh93.... */ /* on some systems you can be in several groups */ if ((maxgroups = getgroups(0, NULL)) <= 0) maxgroups = NGROUPS_MAX; /* pre-POSIX system? */ groups = xmalloc((maxgroups + 1) * sizeof(gid_t)); n = getgroups(maxgroups, groups); while (--n >= 0) { if (groups[n] == sp->st_gid) { stmode <<= 3; break; } } free(groups); } return !!(sp->st_mode & stmode); #endif } static int filstat(char *nm, enum token mode) { struct stat s; if (mode == FILSYM ? lstat(nm, &s) : stat(nm, &s)) return 0; switch (mode) { case FILRD: return test_access(&s, S_IROTH); case FILWR: return test_access(&s, S_IWOTH); case FILEX: return test_access(&s, S_IXOTH); case FILEXIST: return 1; /* the successful lstat()/stat() is good enough */ case FILREG: return S_ISREG(s.st_mode); case FILDIR: return S_ISDIR(s.st_mode); case FILCDEV: #ifdef S_ISCHR return S_ISCHR(s.st_mode); #else return 0; #endif case FILBDEV: #ifdef S_ISBLK return S_ISBLK(s.st_mode); #else return 0; #endif case FILFIFO: #ifdef S_ISFIFO return S_ISFIFO(s.st_mode); #else return 0; #endif case FILSOCK: #ifdef S_ISSOCK return S_ISSOCK(s.st_mode); #else return 0; #endif case FILSYM: #ifdef S_ISLNK return S_ISLNK(s.st_mode); #else return 0; #endif case FILSUID: return (s.st_mode & S_ISUID) != 0; case FILSGID: return (s.st_mode & S_ISGID) != 0; case FILSTCK: #ifdef S_ISVTX return (s.st_mode & S_ISVTX) != 0; #else return 0; #endif case FILGZ: return s.st_size > (off_t)0; case FILUID: return s.st_uid == geteuid(); case FILGID: return s.st_gid == getegid(); default: return 1; } } #define VTOC(x) (const unsigned char *)((const struct t_op *)x)->op_text static int compare1(const void *va, const void *vb) { const unsigned char *a = va; const unsigned char *b = VTOC(vb); return a[0] - b[0]; } static int compare2(const void *va, const void *vb) { const unsigned char *a = va; const unsigned char *b = VTOC(vb); int z = a[0] - b[0]; return z ? z : (a[1] - b[1]); } static struct t_op const * findop(const char *s) { if (s[0] == '-') { if (s[1] == '\0') return NULL; if (s[2] == '\0') return bsearch(s + 1, mop2, __arraycount(mop2), sizeof(*mop2), compare1); else if (s[3] != '\0') return NULL; else return bsearch(s + 1, mop3, __arraycount(mop3), sizeof(*mop3), compare2); } else { if (s[1] == '\0') return bsearch(s, cop, __arraycount(cop), sizeof(*cop), compare1); else if (strcmp(s, cop2[0].op_text) == 0) return cop2; else return NULL; } } static enum token t_lex(PTESTINSTANCE pThis, char *s) { struct t_op const *op; if (s == NULL) { pThis->t_wp_op = NULL; return EOI; } if ((op = findop(s)) != NULL) { if (!((op->op_type == UNOP && isoperand(pThis)) || (op->op_num == LPAREN && *(pThis->t_wp+1) == 0))) { pThis->t_wp_op = op; return op->op_num; } } pThis->t_wp_op = NULL; return OPERAND; } static int isoperand(PTESTINSTANCE pThis) { struct t_op const *op; char *s, *t; if ((s = *(pThis->t_wp+1)) == 0) return 1; if ((t = *(pThis->t_wp+2)) == 0) return 0; if ((op = findop(s)) != NULL) return op->op_type == BINOP && (t[0] != ')' || t[1] != '\0'); return 0; } /* atoi with error detection */ static int getn(PTESTINSTANCE pThis, const char *s) { char *p; long r; errno = 0; r = strtol(s, &p, 10); if (errno != 0) return errx(pThis->pCtx, -42, "%s: out of range", s); while (isspace((unsigned char)*p)) p++; if (*p) return errx(pThis->pCtx, -42, "%s: bad number", s); return (int) r; } static int newerf(const char *f1, const char *f2) { struct stat b1, b2; return (stat(f1, &b1) == 0 && stat(f2, &b2) == 0 && b1.st_mtime > b2.st_mtime); } static int olderf(const char *f1, const char *f2) { struct stat b1, b2; return (stat(f1, &b1) == 0 && stat(f2, &b2) == 0 && b1.st_mtime < b2.st_mtime); } static int equalf(const char *f1, const char *f2) { struct stat b1, b2; return (stat(f1, &b1) == 0 && stat(f2, &b2) == 0 && b1.st_dev == b2.st_dev && b1.st_ino == b2.st_ino); } static int usage(PKMKBUILTINCTX pCtx, int fIsErr) { kmk_builtin_ctx_printf(pCtx, fIsErr, "usage: %s expression [-- [args]]\n", pCtx->pszProgName); return 0; /* only used in --help. */ }