From 8f6b0c1b360f2fea3f7f6563808513cbdd51df80 Mon Sep 17 00:00:00 2001
From: Tavian Barnes <tavianator@tavianator.com>
Date: Wed, 10 Apr 2024 10:10:51 -0400
Subject: Implement -context

Closes: https://github.com/tavianator/bfs/issues/27
---
 completions/bfs.bash |   1 +
 completions/bfs.fish |   1 +
 completions/bfs.zsh  |   1 +
 docs/bfs.1           |   4 ++
 src/eval.c           |  43 ++++++++++++-------
 src/eval.h           |   1 +
 src/parse.c          | 114 +++++++++++++++++++++++++++++----------------------
 7 files changed, 103 insertions(+), 62 deletions(-)

diff --git a/completions/bfs.bash b/completions/bfs.bash
index 816f1ec..6fd82c8 100644
--- a/completions/bfs.bash
+++ b/completions/bfs.bash
@@ -32,6 +32,7 @@ _bfs() {
     # (e.g. because they are numeric, glob, regexp, time, etc.)
     local nocomp=(
         -{a,B,c,m}{min,since,time}
+        -context
         -ilname
         -iname
         -inum
diff --git a/completions/bfs.fish b/completions/bfs.fish
index 0c58ef4..24b0ad9 100644
--- a/completions/bfs.fish
+++ b/completions/bfs.fish
@@ -71,6 +71,7 @@ complete -c bfs -o Btime -d "Find files birthed specified number of days ago" -x
 complete -c bfs -o ctime -d "Find files changed specified number of days ago" -x
 complete -c bfs -o mtime -d "Find files modified specified number of days ago" -x
 complete -c bfs -o capable -d "Find files with capabilities set"
+complete -c bfs -o context -d "Find files by SELinux context" -x
 complete -c bfs -o depth -d "Find files with specified number of depth" -x
 complete -c bfs -o empty -d "Find empty files/directories"
 complete -c bfs -o executable -d "Find files the current user can execute"
diff --git a/completions/bfs.zsh b/completions/bfs.zsh
index 07db456..432ab8c 100644
--- a/completions/bfs.zsh
+++ b/completions/bfs.zsh
@@ -74,6 +74,7 @@ args=(
     '*-mtime[find files modified N days ago]:modification time (days):->times'
 
     '*-capable[find files with POSIX.1e capabilities set]'
+    '*-context[find files by SELinux context]:pattern'
     # -depth without parameters exist above. I don't know how to handle this gracefully
     '*-empty[find empty files/directories]'
     '*-executable[find files the current user can execute]'
diff --git a/docs/bfs.1 b/docs/bfs.1
index 3a4f15a..54166ab 100644
--- a/docs/bfs.1
+++ b/docs/bfs.1
@@ -452,6 +452,10 @@ Find files with POSIX.1e
 .BR capabilities (7)
 set.
 .TP
+\fB\-context \fIGLOB\fR
+Find files whose SELinux context matches the
+.IR GLOB .
+.TP
 \fB\-depth\fR [\fI\-+\fR]\fIN\fR
 Find files with depth
 .IR N .
diff --git a/src/eval.c b/src/eval.c
index 2f06858..d0112c2 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -145,6 +145,20 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) {
 	return false;
 }
 
+/** Common code for fnmatch() tests. */
+static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
+	if (expr->literal) {
+#ifdef FNM_CASEFOLD
+		if (expr->fnm_flags & FNM_CASEFOLD) {
+			return strcasecmp(expr->pattern, str) == 0;
+		}
+#endif
+		return strcmp(expr->pattern, str) == 0;
+	} else {
+		return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
+	}
+}
+
 /**
  * -true test.
  */
@@ -193,6 +207,21 @@ bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) {
 	}
 }
 
+/**
+ * -context test.
+ */
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) {
+	char *con = bfs_getfilecon(state->ftwbuf);
+	if (!con) {
+		eval_report_error(state);
+		return false;
+	}
+
+	bool ret = eval_fnmatch(expr, con);
+	bfs_freecon(con);
+	return ret;
+}
+
 /**
  * Get the given timespec field out of a stat buffer.
  */
@@ -546,20 +575,6 @@ bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state) {
 	return bfs_expr_cmp(expr, statbuf->nlink);
 }
 
-/** Common code for fnmatch() tests. */
-static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
-	if (expr->literal) {
-#ifdef FNM_CASEFOLD
-		if (expr->fnm_flags & FNM_CASEFOLD) {
-			return strcasecmp(expr->pattern, str) == 0;
-		}
-#endif
-		return strcmp(expr->pattern, str) == 0;
-	} else {
-		return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
-	}
-}
-
 /**
  * -i?lname test.
  */
diff --git a/src/eval.h b/src/eval.h
index f7f6c77..ae43628 100644
--- a/src/eval.h
+++ b/src/eval.h
@@ -49,6 +49,7 @@ bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state);
 bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state);
diff --git a/src/parse.c b/src/parse.c
index 38ebf3f..a3e32fe 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -1075,6 +1075,67 @@ static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int ar
 	return expr;
 }
 
+/**
+ * Common code for fnmatch() tests.
+ */
+static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
+	if (!expr) {
+		return NULL;
+	}
+
+	expr->pattern = expr->argv[1];
+
+	if (casefold) {
+#ifdef FNM_CASEFOLD
+		expr->fnm_flags = FNM_CASEFOLD;
+#else
+		parse_expr_error(parser, expr, "Missing platform support.\n");
+		return NULL;
+#endif
+	} else {
+		expr->fnm_flags = 0;
+	}
+
+	// POSIX says, about fnmatch():
+	//
+	//     If pattern ends with an unescaped <backslash>, fnmatch() shall
+	//     return a non-zero value (indicating either no match or an error).
+	//
+	// But not all implementations obey this, so check for it ourselves.
+	size_t i, len = strlen(expr->pattern);
+	for (i = 0; i < len; ++i) {
+		if (expr->pattern[len - i - 1] != '\\') {
+			break;
+		}
+	}
+	if (i % 2 != 0) {
+		parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
+		expr->eval_fn = eval_false;
+		return expr;
+	}
+
+	// strcmp() can be much faster than fnmatch() since it doesn't have to
+	// parse the pattern, so special-case patterns with no wildcards.
+	//
+	//     https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
+	expr->literal = strcspn(expr->pattern, "?*\\[") == len;
+
+	return expr;
+}
+
+/**
+ * Parse -context.
+ */
+static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_CONTEXT
+	struct bfs_expr *expr = parse_unary_test(parser, eval_context);
+	return parse_fnmatch(parser, expr, false);
+#else
+	parse_error(parser, "Missing platform support.\n");
+	return NULL;
+#endif
+}
+
 /**
  * Parse -{false,true}.
  */
@@ -1631,54 +1692,6 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg
 	return expr;
 }
 
-/**
- * Common code for fnmatch() tests.
- */
-static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
-	if (!expr) {
-		return NULL;
-	}
-
-	expr->pattern = expr->argv[1];
-
-	if (casefold) {
-#ifdef FNM_CASEFOLD
-		expr->fnm_flags = FNM_CASEFOLD;
-#else
-		parse_expr_error(parser, expr, "Missing platform support.\n");
-		return NULL;
-#endif
-	} else {
-		expr->fnm_flags = 0;
-	}
-
-	// POSIX says, about fnmatch():
-	//
-	//     If pattern ends with an unescaped <backslash>, fnmatch() shall
-	//     return a non-zero value (indicating either no match or an error).
-	//
-	// But not all implementations obey this, so check for it ourselves.
-	size_t i, len = strlen(expr->pattern);
-	for (i = 0; i < len; ++i) {
-		if (expr->pattern[len - i - 1] != '\\') {
-			break;
-		}
-	}
-	if (i % 2 != 0) {
-		parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
-		expr->eval_fn = eval_false;
-		return expr;
-	}
-
-	// strcmp() can be much faster than fnmatch() since it doesn't have to
-	// parse the pattern, so special-case patterns with no wildcards.
-	//
-	//     https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
-	expr->literal = strcspn(expr->pattern, "?*\\[") == len;
-
-	return expr;
-}
-
 /**
  * Parse -i?name.
  */
@@ -2767,6 +2780,10 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2
 #if BFS_CAN_CHECK_CAPABILITIES
 	cfprintf(cout, "  ${blu}-capable${rs}\n");
 	cfprintf(cout, "      Find files with POSIX.1e capabilities set\n");
+#endif
+#if BFS_CAN_CHECK_CONTEXT
+	cfprintf(cout, "  ${blu}-context${rs} ${bld}GLOB${rs}\n");
+	cfprintf(cout, "      Find files with SELinux context matching a glob pattern\n");
 #endif
 	cfprintf(cout, "  ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
 	cfprintf(cout, "      Find files with depth ${bld}N${rs}\n");
@@ -2961,6 +2978,7 @@ static const struct table_entry parse_table[] = {
 	{"-cmin", T_TEST, parse_min, BFS_STAT_CTIME},
 	{"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME},
 	{"-color", T_OPTION, parse_color, true},
+	{"-context", T_TEST, parse_context, true},
 	{"-csince", T_TEST, parse_since, BFS_STAT_CTIME},
 	{"-ctime", T_TEST, parse_time, BFS_STAT_CTIME},
 	{"-d", T_FLAG, parse_depth},
-- 
cgit v1.2.3