summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bfs.h20
-rw-r--r--eval.c31
-rw-r--r--parse.c277
-rwxr-xr-xtests.sh51
4 files changed, 366 insertions, 13 deletions
diff --git a/bfs.h b/bfs.h
index b504f0c..55aeda2 100644
--- a/bfs.h
+++ b/bfs.h
@@ -129,6 +129,18 @@ enum cmp_flag {
};
/**
+ * Possible types of mode comparison.
+ */
+enum mode_cmp {
+ /** Mode is an exact match (MODE). */
+ MODE_EXACT,
+ /** Mode has all these bits (-MODE). */
+ MODE_ALL,
+ /** Mode has any of these bits (/MODE). */
+ MODE_ANY,
+};
+
+/**
* Available struct stat time fields.
*/
enum time_field {
@@ -207,6 +219,13 @@ struct expr {
/** The optional comparison flag. */
enum cmp_flag cmp_flag;
+ /** The mode comparison flag. */
+ enum mode_cmp mode_cmp;
+ /** Mode to use for files. */
+ mode_t file_mode;
+ /** Mode to use for directories (different due to X). */
+ mode_t dir_mode;
+
/** The optional reference time. */
struct timespec reftime;
/** The optional time field. */
@@ -260,6 +279,7 @@ bool eval_true(const struct expr *expr, struct eval_state *state);
bool eval_false(const struct expr *expr, struct eval_state *state);
bool eval_access(const struct expr *expr, struct eval_state *state);
+bool eval_perm(const struct expr *expr, struct eval_state *state);
bool eval_acmtime(const struct expr *expr, struct eval_state *state);
bool eval_acnewer(const struct expr *expr, struct eval_state *state);
diff --git a/eval.c b/eval.c
index f930bac..54d9a7c 100644
--- a/eval.c
+++ b/eval.c
@@ -636,6 +636,37 @@ bool eval_path(const struct expr *expr, struct eval_state *state) {
}
/**
+ * -perm test.
+ */
+bool eval_perm(const struct expr *expr, struct eval_state *state) {
+ const struct stat *statbuf = fill_statbuf(state);
+ if (!statbuf) {
+ return false;
+ }
+
+ mode_t mode = statbuf->st_mode;
+ mode_t target;
+ if (state->ftwbuf->typeflag == BFTW_DIR) {
+ target = expr->dir_mode;
+ } else {
+ target = expr->file_mode;
+ }
+
+ switch (expr->mode_cmp) {
+ case MODE_EXACT:
+ return (mode & 07777) == target;
+
+ case MODE_ALL:
+ return (mode & target) == target;
+
+ case MODE_ANY:
+ return !(mode & target) == !target;
+ }
+
+ return false;
+}
+
+/**
* -print action.
*/
bool eval_print(const struct expr *expr, struct eval_state *state) {
diff --git a/parse.c b/parse.c
index 20f256c..ca8a00f 100644
--- a/parse.c
+++ b/parse.c
@@ -370,12 +370,14 @@ static const char *skip_paths(struct parser_state *state) {
/** Integer parsing flags. */
enum int_flags {
- IF_INT = 0,
- IF_LONG = 1,
- IF_LONG_LONG = 2,
- IF_SIZE_MASK = 0x3,
- IF_UNSIGNED = 1 << 2,
- IF_PARTIAL_OK = 1 << 3,
+ IF_BASE_MASK = 0x03F,
+ IF_INT = 0x040,
+ IF_LONG = 0x080,
+ IF_LONG_LONG = 0x0C0,
+ IF_SIZE_MASK = 0x0C0,
+ IF_UNSIGNED = 0x100,
+ IF_PARTIAL_OK = 0x200,
+ IF_QUIET = 0x400,
};
/**
@@ -384,8 +386,13 @@ enum int_flags {
static const char *parse_int(const struct parser_state *state, const char *str, void *result, enum int_flags flags) {
char *endptr;
+ int base = flags & IF_BASE_MASK;
+ if (base == 0) {
+ base = 10;
+ }
+
errno = 0;
- long long value = strtoll(str, &endptr, 10);
+ long long value = strtoll(str, &endptr, base);
if (errno != 0) {
goto bad;
}
@@ -425,8 +432,10 @@ static const char *parse_int(const struct parser_state *state, const char *str,
return endptr;
bad:
- pretty_error(state->cmdline->stderr_colors,
- "error: '%s' is not a valid integer.\n", str);
+ if (!(flags & IF_QUIET)) {
+ pretty_error(state->cmdline->stderr_colors,
+ "error: '%s' is not a valid integer.\n", str);
+ }
return NULL;
}
@@ -1209,6 +1218,255 @@ static struct expr *parse_noleaf(struct parser_state *state, int arg1, int arg2)
}
/**
+ * Parse a permission mode like chmod(1).
+ */
+static int parse_mode(const struct parser_state *state, const char *mode, struct expr *expr) {
+ if (mode[0] >= '0' && mode[0] <= '9') {
+ unsigned int parsed;
+ if (!parse_int(state, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ goto fail;
+ }
+ if (parsed > 07777) {
+ goto fail;
+ }
+
+ expr->file_mode = parsed;
+ expr->dir_mode = parsed;
+ return 0;
+ }
+
+ expr->file_mode = 0;
+ expr->dir_mode = 0;
+
+ // Parse the same grammar as chmod(1), which looks like this:
+ //
+ // MODE : CLAUSE ["," CLAUSE]*
+ //
+ // CLAUSE : WHO* ACTION+
+ //
+ // WHO : "u" | "g" | "o" | "a"
+ //
+ // ACTION : OP PERM*
+ // | OP PERMCOPY
+ //
+ // OP : "+" | "-" | "="
+ //
+ // PERM : "r" | "w" | "x" | "X" | "s" | "t"
+ //
+ // PERMCOPY : "u" | "g" | "o"
+
+ // State machine state
+ enum {
+ MODE_CLAUSE,
+ MODE_WHO,
+ MODE_ACTION,
+ MODE_ACTION_APPLY,
+ MODE_OP,
+ MODE_PERM,
+ } mstate = MODE_CLAUSE;
+
+ enum {
+ MODE_PLUS,
+ MODE_MINUS,
+ MODE_EQUALS,
+ } op;
+
+ mode_t who;
+ mode_t file_change;
+ mode_t dir_change;
+
+ const char *i = mode;
+ while (true) {
+ switch (mstate) {
+ case MODE_CLAUSE:
+ who = 0;
+ mstate = MODE_WHO;
+ // Fallthrough
+
+ case MODE_WHO:
+ switch (*i) {
+ case 'u':
+ who |= 0700;
+ break;
+ case 'g':
+ who |= 0070;
+ break;
+ case 'o':
+ who |= 0007;
+ break;
+ case 'a':
+ who |= 0777;
+ break;
+ default:
+ mstate = MODE_ACTION;
+ continue;
+ }
+ break;
+
+ case MODE_ACTION_APPLY:
+ switch (op) {
+ case MODE_EQUALS:
+ expr->file_mode &= ~who;
+ expr->dir_mode &= ~who;
+ // Fallthrough
+ case MODE_PLUS:
+ expr->file_mode |= file_change;
+ expr->dir_mode |= dir_change;
+ break;
+ case MODE_MINUS:
+ expr->file_mode &= ~file_change;
+ expr->dir_mode &= ~dir_change;
+ break;
+ }
+ // Fallthrough
+
+ case MODE_ACTION:
+ if (who == 0) {
+ who = 0777;
+ }
+
+ switch (*i) {
+ case '+':
+ op = MODE_PLUS;
+ mstate = MODE_OP;
+ break;
+ case '-':
+ op = MODE_MINUS;
+ mstate = MODE_OP;
+ break;
+ case '=':
+ op = MODE_EQUALS;
+ mstate = MODE_OP;
+ break;
+
+ case ',':
+ if (mstate == MODE_ACTION_APPLY) {
+ mstate = MODE_CLAUSE;
+ } else {
+ goto fail;
+ }
+ break;
+
+ case '\0':
+ if (mstate == MODE_ACTION_APPLY) {
+ goto done;
+ } else {
+ goto fail;
+ }
+
+ default:
+ goto fail;
+ }
+ break;
+
+ case MODE_OP:
+ file_change = 0;
+ dir_change = 0;
+
+ switch (*i) {
+ case 'u':
+ case 'g':
+ case 'o':
+ // PERMCOPY (e.g. u=g) has no effect for -perm
+ mstate = MODE_ACTION_APPLY;
+ break;
+
+ default:
+ mstate = MODE_PERM;
+ continue;
+ }
+ break;
+
+ case MODE_PERM:
+ switch (*i) {
+ case 'r':
+ file_change |= who & 0444;
+ dir_change |= who & 0444;
+ break;
+ case 'w':
+ file_change |= who & 0222;
+ dir_change |= who & 0222;
+ break;
+ case 'x':
+ file_change |= who & 0111;
+ // Fallthrough
+ case 'X':
+ dir_change |= who & 0111;
+ break;
+ case 's':
+ if (who & 0700) {
+ file_change |= S_ISUID;
+ dir_change |= S_ISUID;
+ }
+ if (who & 0070) {
+ file_change |= S_ISGID;
+ dir_change |= S_ISGID;
+ }
+ break;
+ case 't':
+ file_change |= S_ISVTX;
+ dir_change |= S_ISVTX;
+ break;
+ default:
+ mstate = MODE_ACTION_APPLY;
+ continue;
+ }
+ break;
+ }
+
+ ++i;
+ }
+
+done:
+ return 0;
+
+fail:
+ pretty_error(state->cmdline->stderr_colors,
+ "error: '%s' is an invalid mode.\n\n",
+ mode);
+ return -1;
+}
+
+/**
+ * Parse -perm MODE.
+ */
+static struct expr *parse_perm(struct parser_state *state, int field, int arg2) {
+ struct expr *expr = parse_unary_test(state, eval_perm);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *mode = expr->sdata;
+ switch (mode[0]) {
+ case '-':
+ expr->mode_cmp = MODE_ALL;
+ ++mode;
+ break;
+ case '/':
+ expr->mode_cmp = MODE_ANY;
+ ++mode;
+ break;
+ case '+':
+ pretty_error(state->cmdline->stderr_colors,
+ "error: -perm +mode is not supported.\n\n");
+ goto fail;
+ default:
+ expr->mode_cmp = MODE_EXACT;
+ break;
+ }
+
+ if (parse_mode(state, mode, expr) != 0) {
+ goto fail;
+ }
+
+ return expr;
+
+fail:
+ free_expr(expr);
+ return NULL;
+}
+
+/**
* Parse -print.
*/
static struct expr *parse_print(struct parser_state *state, int arg1, int arg2) {
@@ -1478,6 +1736,7 @@ static const struct table_entry parse_table[] = {
{"okdir", false, parse_exec, EXEC_CONFIRM | EXEC_CHDIR},
{"or"},
{"path", false, parse_path, false},
+ {"perm", false, parse_perm},
{"print", false, parse_print},
{"print0", false, parse_print0},
{"prune", false, parse_prune},
diff --git a/tests.sh b/tests.sh
index a615346..51f891c 100755
--- a/tests.sh
+++ b/tests.sh
@@ -391,7 +391,50 @@ function test_0069() {
"$BFS" "$scratch" -mindepth 1 -ignore_readdir_race -links 1 -exec ./tests/remove-sibling.sh '{}' ';'
}
-for i in {1..69}; do
+function test_0070() {
+ find_diff "$perms" -perm 222 && \
+ find_diff "$perms" -perm -222 && \
+ find_diff "$perms" -perm /222
+}
+
+function test_0071() {
+ find_diff "$perms" -perm 644 && \
+ find_diff "$perms" -perm -644 && \
+ find_diff "$perms" -perm /644
+}
+
+function test_0072() {
+ find_diff "$perms" -perm a+r,u=wX,g+wX-w && \
+ find_diff "$perms" -perm -a+r,u=wX,g+wX-w && \
+ find_diff "$perms" -perm /a+r,u=wX,g+wX-w
+}
+
+function test_0073() {
+ ! "$BFS" "$perms" -perm a+r, 2>/dev/null
+}
+
+function test_0074() {
+ ! "$BFS" "$perms" -perm a+r,,u+w 2>/dev/null
+}
+
+function test_0075() {
+ ! "$BFS" "$perms" -perm a 2>/dev/null
+}
+
+function test_0076() {
+ find_diff "$perms" -perm -+rwx && \
+ find_diff "$perms" -perm /+rwx
+}
+
+function test_0077() {
+ ! "$BFS" "$perms" -perm +rwx 2>/dev/null
+}
+
+function test_0078() {
+ ! "$BFS" "$perms" -perm +777 2>/dev/null
+}
+
+for i in {1..78}; do
test="test_$(printf '%04d' $i)"
if [ -t 1 ]; then
@@ -403,14 +446,14 @@ for i in {1..69}; do
if [ $status -ne 0 ]; then
if [ -t 1 ]; then
- echo " failed!"
+ printf '\r%s failed!\n' "$test"
else
- echo "$test failed!"
+ printf '%s failed!\n' "$test"
fi
exit $status
fi
done
if [ -t 1 ]; then
- echo
+ printf '\n'
fi