diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2021-02-16 13:36:19 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2021-02-16 13:41:49 +0200 |
commit | b16803d6a9e7e403c4d92350fa16464d348509dc (patch) | |
tree | 53665db5171ac41b164431b219d7d373398e6161 | |
parent | 78a90ad8afc3dedb1ad8d721875e770221c61c56 (diff) | |
download | rush-b16803d6a9e7e403c4d92350fa16464d348509dc.tar.gz rush-b16803d6a9e7e403c4d92350fa16464d348509dc.tar.bz2 |
Implement file type and ownership tests
* NEWS: Document changes.
* configure.ac: Version 2.1.90. Raise autoconf and automake
requirements.
* doc/rush.texi: Document changes.
* src/cfgram.y: New rule for file system tests.
* src/cflex.l: New rule for the TEST token.
* src/rush.c (groupmember): Change type of the first argument.
(rush_access,eval_fstest): New functions.
(test_eval): Process the test_fstest node type.
* src/rush.h (test_fstest): New node type.
(fstest_op): New enum.
(test_node): New member: fstest
* tests/.gitignore: Add mksock
* tests/mksock.c: New file.
* tests/fstest.at: New test.
* tests/Makefile.am: Add new test.
* tests/testsuite.at: Likewise.
-rw-r--r-- | NEWS | 15 | ||||
-rw-r--r-- | configure.ac | 6 | ||||
-rw-r--r-- | doc/rush.texi | 87 | ||||
-rw-r--r-- | src/cfgram.y | 8 | ||||
-rw-r--r-- | src/cflex.l | 2 | ||||
-rw-r--r-- | src/rush.c | 100 | ||||
-rw-r--r-- | src/rush.h | 27 | ||||
-rw-r--r-- | tests/.gitignore | 1 | ||||
-rw-r--r-- | tests/Makefile.am | 4 | ||||
-rw-r--r-- | tests/fstest.at | 216 | ||||
-rw-r--r-- | tests/mksock.c | 29 | ||||
-rw-r--r-- | tests/testsuite.at | 3 |
12 files changed, 489 insertions, 9 deletions
@@ -1,8 +1,21 @@ -GNU Rush NEWS -- history of user-visible changes. 2019-07-12 +GNU Rush NEWS -- history of user-visible changes. 2021-02-16 See the end of file for copying conditions. Please send bug reports to <bug-rush@gnu.org.ua> +Version 2.1 .90 (git) + +* File system tests + +New predicates, designed after options to the "test" system command, +check for file types. modes and ownership. For example + + rule + match -d /var/lock/sd + +will match if /var/lock/sd exists on diks and is a directory + + Version 2.1, 2019-07-12 * Include updated wordsplit version (v1.0-7-g6ccb9ad) diff --git a/configure.ac b/configure.ac index 212c921..ee6adcf 100644 --- a/configure.ac +++ b/configure.ac @@ -14,12 +14,12 @@ # You should have received a copy of the GNU General Public License # along with GNU Rush. If not, see <http://www.gnu.org/licenses/>. -AC_PREREQ(2.63) -AC_INIT([GNU rush], [2.1], [bug-rush@gnu.org]) +AC_PREREQ(2.64) +AC_INIT([GNU rush], [2.1.90], [bug-rush@gnu.org]) AC_CONFIG_SRCDIR([src/rush.c]) AC_CONFIG_HEADER([config.h]) AC_CONFIG_AUX_DIR([build-aux]) -AM_INIT_AUTOMAKE([1.11.1 gnits tar-ustar dist-bzip2 dist-xz std-options]) +AM_INIT_AUTOMAKE([1.15 gnits tar-ustar dist-bzip2 dist-xz std-options]) # Enable silent rules by default: AM_SILENT_RULES([yes]) diff --git a/doc/rush.texi b/doc/rush.texi index 9116c8c..04c6557 100644 --- a/doc/rush.texi +++ b/doc/rush.texi @@ -142,6 +142,7 @@ Matching Conditions * Comparisons:: * Membership operators:: +* File system tests:: * Boolean expressions:: Modifying variables @@ -1098,6 +1099,7 @@ A @dfn{simple expression} is either a comparison or membership test. @menu * Comparisons:: * Membership operators:: +* File system tests:: * Boolean expressions:: @end menu @@ -1223,6 +1225,91 @@ in whitespace delimited @var{list}. Members of @var{list} are group names or GIDs. @end table +@node File system tests +@subsubsection File system tests +@cindex file type, checking +@cindex checking file type +@cindex file ownership, checking +@cindex checking file ownership + File system tests check file types and ownership. They are similar +to options to @command{test} shell command: + +@table @code +@cindex -b, file system test +@item -b @var{file} +@var{file} exists and is block special + +@cindex -c, file system test +@item -c @var{file} +@var{file} exists and is character special + +@cindex -d, file system test +@item -d @var{file} +@var{file} exists and is a directory + +@cindex -e, file system test +@item -e @var{file} +@var{file} exists + +@cindex -f, file system test +@item -f @var{file} +@var{file} exists and is a regular file + +@cindex -g, file system test +@item -g @var{file} +@var{file} exists and is set-group-ID + +@cindex -G, file system test +@item -G @var{file} +@var{file} exists and is owned by the primary group of the current user. + +@cindex -h, file system test +@cindex -L, file system test +@item -h @var{file} +@itemx -L @var{file} +@var{file} exists and is a symbolic link + +@cindex -k, file system test +@item -k @var{file} +@var{file} exists and has its sticky bit set + +@cindex -L, file system test +@item -L @var{file} +@var{file} exists and is a symbolic link (same as -h) + +@cindex -O, file system test +@item -O @var{file} +@var{file} exists and is owned by the current user + +@cindex -p, file system test +@item -p @var{file} +@var{file} exists and is a named pipe + +@cindex -r, file system test +@item -r @var{file} +@var{file} exists and read permission is granted + +@cindex -s, file system test +@item -s @var{file} +@var{file} exists and has a size greater than zero + +@cindex -S, file system test +@item -S @var{file} +@var{file} exists and is a socket + +@cindex -u, file system test +@item -u @var{file} +@var{file} exists and its set-user-ID bit is set + +@cindex -w, file system test +@item -w @var{file} +@var{file} exists and write permission is granted + +@cindex -x, file system test +@item -x @var{file} +@var{file} exists and execute (or search) permission is granted +@end table + @node Boolean expressions @subsubsection Boolean expressions Simple expressions can be combined into complex conditions using diff --git a/src/cfgram.y b/src/cfgram.y index 9cca689..fb7e757 100644 --- a/src/cfgram.y +++ b/src/cfgram.y @@ -64,6 +64,7 @@ static struct transform_node *new_set_node(enum transform_node_type type, struct argval *tail; } arglist; struct { unsigned major, minor; } version; + int fstest; } %token <str> STRING "string" @@ -108,6 +109,7 @@ static struct transform_node *new_set_node(enum transform_node_type type, %token NM "!~" %token IN "in" %token GROUP "group" +%token <fstest> TEST "test" %left OR %left AND @@ -396,6 +398,12 @@ expr : string '~' regex $$ = new_test_node(test_group); $$->v.groups = $2.argv; } + | TEST string + { + $$ = new_test_node(test_fstest); + $$->v.fstest.op = $1; + $$->v.fstest.arg = $2; + } ; literal : IDENT diff --git a/src/cflex.l b/src/cflex.l index 8648697..cafe1d4 100644 --- a/src/cflex.l +++ b/src/cflex.l @@ -368,6 +368,8 @@ static int noinc(void); "!~" return tok(NM); "in" return tok(IN); "group" return tok(GROUP); +-[bcdefgGkOprsSuwx] { yylval.fstest = yytext[1]; return tok(TEST); } +-[hL] { yylval.fstest = fs_symlink; return tok(TEST); } [+-]?[0-9]+ { yylval.num.strval = xstrdup(yytext); yylval.num.intval = atoi(yytext); return tok(NUMBER); } @@ -346,9 +346,8 @@ eval_in(struct test_node *node, } static rush_bool_t -groupmember(char const *gname, struct passwd const *pw) +groupmember(struct group *grp, struct passwd const *pw) { - struct group *grp = getgrnam(gname); if (grp) { char **p; @@ -370,12 +369,104 @@ eval_member(struct test_node *node, size_t i; for (i = 0; node->v.groups[i]; i++) { - if (groupmember(node->v.groups[i], req->pw)) + if (groupmember(getgrnam(node->v.groups[i]), req->pw)) return rush_true; } return rush_false; } +static rush_bool_t +rush_access(struct stat *st, struct rush_request *req, int bit) +{ + if (st->st_mode & bit) + return rush_true; + + bit <<= 3; + if ((st->st_mode & bit) && groupmember(getgrgid(st->st_gid), req->pw)) + return rush_true; + + bit <<= 3; + if ((st->st_mode & bit) && st->st_uid == req->pw->pw_uid) + return rush_true; + + return rush_false; +} + +static rush_bool_t +eval_fstest(struct test_node *node, + struct rush_rule *rule, struct rush_request *req) +{ + struct stat st; + + if (((node->v.fstest.op == fs_symlink) ? lstat : stat)(node->v.fstest.arg, &st)) { + if (errno == ENOENT) + return rush_false; + die(system_error, &req->i18n, + _("%s:%d: %s: can't stat '%s': %s"), + rule->file, rule->line, rule->tag, + node->v.fstest.arg, + strerror(errno)); + } + + switch (node->v.fstest.op) { + case fs_block_special: + return S_ISBLK(st.st_mode); + + case fs_char_special: + return S_ISCHR(st.st_mode); + + case fs_directory: + return S_ISDIR(st.st_mode); + + case fs_exists: + return rush_true; + + case fs_regular: + return S_ISREG(st.st_mode); + + case fs_set_gid: + return st.st_mode & S_ISGID; + + case fs_owner_egid: + return st.st_gid == req->pw->pw_gid; + + case fs_symlink: + return S_ISLNK(st.st_mode); + + case fs_sticky: + return st.st_mode & S_ISVTX; + + case fs_owner_euid: + return st.st_uid == req->pw->pw_uid; + + case fs_pipe: + return S_ISFIFO(st.st_mode); + + case fs_readable: + return rush_access(&st, req, 4); + + case fs_size: + return st.st_size > 0; + + case fs_socket: + return S_ISSOCK(st.st_mode); + + case fs_set_uid: + return st.st_mode & S_ISUID; + + case fs_writable: + return rush_access(&st, req, 2); + + case fs_executable: + return rush_access(&st, req, 1); + + default: + die(system_error, NULL, + _("INTERNAL ERROR at %s:%d: unrecognized operation %d"), + __FILE__, __LINE__, node->v.fstest.op); + } +} + rush_bool_t test_eval(struct test_node *node, struct rush_rule *rule, struct rush_request *req) @@ -406,6 +497,9 @@ test_eval(struct test_node *node, case test_not: return ! test_eval(node->v.arg[0], rule, req); + case test_fstest: + return eval_fstest(node, rule, req); + default: die(system_error, NULL, _("INTERNAL ERROR at %s:%d: unrecognized node type %d"), @@ -149,7 +149,8 @@ enum test_type { test_group, test_and, test_or, - test_not + test_not, + test_fstest }; enum cmp_op { @@ -163,6 +164,26 @@ enum cmp_op { cmp_in }; +enum fstest_op { + fs_block_special = 'b', + fs_char_special = 'c', + fs_directory = 'd', + fs_exists = 'e', + fs_regular = 'f', + fs_set_gid = 'g', + fs_owner_egid = 'G', + fs_symlink = 'h', + fs_sticky = 'k', + fs_owner_euid = 'O', + fs_pipe = 'p', + fs_readable = 'r', + fs_size = 's', + fs_socket = 'S', + fs_set_uid = 'u', + fs_writable = 'w', + fs_executable = 'x' +}; + typedef unsigned long rush_num_t; struct test_node { @@ -180,6 +201,10 @@ struct test_node { } cmp; char **groups; struct test_node *arg[2]; + struct { + int op; + char *arg; + } fstest; } v; }; diff --git a/tests/.gitignore b/tests/.gitignore index a5a7d62..1ce0ab2 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -4,4 +4,5 @@ package.m4 testsuite testsuite.dir testsuite.log +mksock myid diff --git a/tests/Makefile.am b/tests/Makefile.am index a5166a7..daff4d0 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -79,6 +79,7 @@ TESTSUITE_AT = \ insert.at\ remopt.at\ lex.at\ + fstest.at\ legacy/argc.at\ legacy/backref.at\ legacy/chdir.at\ @@ -117,4 +118,5 @@ clean-local: check-local: atconfig atlocal $(TESTSUITE) @$(SHELL) $(TESTSUITE) -noinst_PROGRAMS=myid +noinst_PROGRAMS=myid mksock + diff --git a/tests/fstest.at b/tests/fstest.at new file mode 100644 index 0000000..8fbc111 --- /dev/null +++ b/tests/fstest.at @@ -0,0 +1,216 @@ +# This file is part of GNU Rush. +# Copyright (C) 2021 Sergey Poznyakoff +# +# GNU Rush 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, or (at your option) +# any later version. +# +# GNU Rush 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 GNU Rush. If not, see <http://www.gnu.org/licenses/>. + +m4_pushdef([AT_FS_SUCCESS], +[AT_CHECK([$1]) +AT_RUSH_TEST([ +rush 2.0 +rule + match $2 + set SUCCESS = true +], +[vars], +[cmd], +[0], +[{ + "vars":{ + "SUCCESS":"true" + } +} +], +[])]) + +m4_pushdef([AT_FS_FAILURE], +[AT_CHECK([$1]) +AT_RUSH_TEST([ +rush 2.0 +rule + match $2 + set SUCCESS = true +], +[vars], +[cmd], +[1], +[], +[rush: Error: no matching rule for "cmd", user $MY_USER +])]) + +AT_SETUP([basic filesystem tests]) +AT_KEYWORDS([match fstest]) + +# ############################## +# test -e +# ############################## + +AT_FS_FAILURE([rm -f file],[-e file]) +AT_FS_SUCCESS([touch file],[-e file]) + +# ############################## +# test -f +# ############################## +AT_FS_FAILURE([rm -rf file],[-f file]) +AT_FS_SUCCESS([touch file],[-f file]) +AT_FS_FAILURE([rm -f file && mkdir file],[-f file]) + +# ############################## +# test -d +# ############################## +AT_FS_FAILURE([rm -rf file],[-d file]) +AT_FS_FAILURE([touch file],[-d file]) +AT_FS_SUCCESS([rm -f file && mkdir file],[-d file]) + +# ############################## +# test -s +# ############################## +AT_FS_FAILURE([rm -rf file],[-s file]) +AT_FS_FAILURE([touch file],[-s file]) +AT_FS_SUCCESS([echo "test" > file],[-s file]) + +# ############################## +# test -r +# ############################## + +AT_CHECK([rm -rf file && touch file]) +AT_FS_SUCCESS([chmod u=r file],[-r file]) +AT_FS_SUCCESS([chmod g=r file],[-r file]) +AT_FS_SUCCESS([chmod o=r file],[-r file]) +AT_FS_SUCCESS([chmod a=r file],[-r file]) +AT_FS_FAILURE([chmod -r file],[-r file]) + +# ############################## +# test -w +# ############################## + +AT_FS_SUCCESS([chmod u=w file],[-w file]) +AT_FS_SUCCESS([chmod g=w file],[-w file]) +AT_FS_SUCCESS([chmod o=w file],[-w file]) +AT_FS_SUCCESS([chmod a=w file],[-w file]) +AT_FS_FAILURE([chmod 444 file],[-w file]) + +# ############################## +# test -x +# ############################## + +AT_FS_SUCCESS([chmod u=x file],[-x file]) +AT_FS_SUCCESS([chmod g=x file],[-x file]) +AT_FS_SUCCESS([chmod o=x file],[-x file]) +AT_FS_SUCCESS([chmod a=x file],[-x file]) +AT_FS_FAILURE([chmod 644 file],[-x file]) + +AT_CLEANUP + +AT_SETUP([match -h]) +AT_KEYWORDS([match fstest fs_symlink]) +# ############################## +# test -h +# ############################## +AT_FS_FAILURE([rm -rf file symlink],[-h symlink]) +AT_FS_FAILURE([touch file],[-h file]) +AT_FS_SUCCESS([ln -sf file symlink || AT_SKIP_TEST],[-h symlink]) +AT_FS_SUCCESS([true],[-L symlink]) +AT_CLEANUP + +AT_SETUP([match -p]) +AT_KEYWORDS([match fstest fs_pipe]) +# ############################## +# test -p +# ############################## +AT_FS_FAILURE([rm -rf file],[-p file]) +AT_FS_SUCCESS([mkfifo file || AT_SKIP_TEST],[-p file]) +AT_CLEANUP + +AT_SETUP([match -k]) +AT_KEYWORDS([match fstest fs_sticky]) +# ############################## +# test -k +# ############################## +AT_FS_FAILURE([rm -rf file],[-k file]) +AT_FS_SUCCESS([touch file && chmod +t file || AT_SKIP_TEST],[-k file]) +AT_CLEANUP + +AT_SETUP([match -c]) +AT_KEYWORDS([match fstest fs_char_special]) +# ############################## +# test -c +# ############################## +AT_FS_FAILURE([rm -rf file],[-c file]) +AT_FS_FAILURE([touch file],[-c file]) +AT_FS_SUCCESS([test -c /dev/tty || AT_SKIP_TEST],[-c /dev/tty]) +AT_CLEANUP + +AT_SETUP([match -b]) +AT_KEYWORDS([match fstest fs_block_special]) +AT_FS_FAILURE([rm -rf file],[-b file]) +AT_FS_FAILURE([touch file],[-b file]) +AT_FS_SUCCESS([mknod file b 1 1 || AT_SKIP_TEST],[-b file]) +AT_CLEANUP + +AT_SETUP([match -S]) +AT_KEYWORDS([match fstest fs_socket]) +# ############################## +# test -c +# ############################## +AT_FS_FAILURE([rm -rf file],[-S file]) +AT_FS_FAILURE([touch file],[-S file]) +AT_FS_SUCCESS([mksock socket || AT_SKIP_TEST],[-S socket]) +AT_CLEANUP + +AT_SETUP([match -O]) +AT_KEYWORDS([match fstest fs_owner_euid]) +# ############################## +# test -O +# ############################## +AT_FS_FAILURE([rm -rf file],[-O file]) +AT_FS_SUCCESS([touch file],[-O file]) +AT_CLEANUP + +AT_SETUP([match -G]) +AT_KEYWORDS([match fstest fs_owner_egid]) +# ############################## +# test -G +# ############################## +AT_FS_FAILURE([rm -rf file],[-G file]) +AT_FS_SUCCESS([touch file],[-G file]) +AT_CLEANUP + +AT_SETUP([match -u]) +AT_KEYWORDS([match fstest fs_set_uid]) +# ############################## +# test -u +# ############################## +AT_FS_FAILURE([rm -rf file],[-u file]) +AT_FS_FAILURE([touch file],[-u file]) +AT_FS_SUCCESS([chmod u+s file || AT_SKIP_TEST],[-u file]) +AT_CLEANUP + +AT_SETUP([match -g]) +AT_KEYWORDS([match fstest fs_set_gid]) +# ############################## +# test -g +# ############################## +AT_FS_FAILURE([rm -rf file],[-g file]) +AT_FS_FAILURE([touch file],[-g file]) +AT_FS_SUCCESS([chmod g+s file || AT_SKIP_TEST],[-g file]) +AT_CLEANUP + +# ############################## +# Cleanup +# ############################## + +m4_popdef([AT_FS_FAILURE]) +m4_popdef([AT_FS_SUCCESS]) + + diff --git a/tests/mksock.c b/tests/mksock.c new file mode 100644 index 0000000..64e82b5 --- /dev/null +++ b/tests/mksock.c @@ -0,0 +1,29 @@ +#include <stdlib.h> +#include <stdio.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <assert.h> + +int +main(int argc, char **argv) +{ + struct sockaddr_un s; + int fd; + + assert(argc == 2); + s.sun_family = AF_UNIX; + assert(strlen(argv[1]) < sizeof(s.sun_path)); + strcpy(s.sun_path, argv[1]); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + perror("socket"); + return 1; + } + if (bind(fd, (struct sockaddr*)&s, sizeof(s))) { + perror("bind"); + return 1; + } + return 0; +} + diff --git a/tests/testsuite.at b/tests/testsuite.at index 3df57de..421ee29 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -16,6 +16,8 @@ m4_version_prereq([2.52g]) +m4_define([AT_SKIP_TEST],[exit 77]) + dnl m4_run_rush([KW], [CMD],[EC],[STDOUT],[STDERR],...) m4_define([m4_run_rush],[m4_ifval([$2],[dnl echo "; m4_bpatsubst($2,",\\")" @@ -75,6 +77,7 @@ m4_include([user.at]) m4_include([in.at]) m4_include([gid.at]) m4_include([group.at]) +m4_include([fstest.at]) AT_BANNER([Complex Conditions]) m4_include([and.at]) |