From 64879d702df7723a731787d414f51d243b312158 Mon Sep 17 00:00:00 2001
From: Matt Whitlock <findutils@mattwhitlock.name>
Date: Wed, 30 Jan 2019 20:58:32 -0500
Subject: [PATCH] find: introduce -attr predicate to test inode flags

---
 NEWS           |   3 +
 doc/find.texi  |  42 +++++++++++++
 find/defs.h    |  18 ++++++
 find/find.1    |  13 ++++
 find/ftsfind.c |   6 +-
 find/parser.c  | 168 +++++++++++++++++++++++++++++++++++++++++++++++++
 find/pred.c    |  20 +++++-
 find/tree.c    |   7 ++-
 find/util.c    |  57 ++++++++++++++---
 9 files changed, 322 insertions(+), 12 deletions(-)

diff --git a/NEWS b/NEWS
index 47338232..c91a613d 100644
--- a/NEWS
+++ b/NEWS
@@ -45,6 +45,9 @@ an existing file.
 find now supports the debug option '-D all' to include all of the other
 debug options at once.
 
+find now implements an '-attr' test to match files based on inode flags,
+which are file attributes you can access with chattr(1) and lsattr(1).
+
 xargs now supports the -o, --open-tty option to reopen stdin as /dev/tty
 in the child process before executing the command; useful to run an
 interactive application.  Added for compatibility with BSD.
diff --git a/doc/find.texi b/doc/find.texi
index a573a228..d8c1d576 100644
--- a/doc/find.texi
+++ b/doc/find.texi
@@ -349,6 +349,7 @@ more information about the matching files.
 * Type::
 * Owner::
 * Mode Bits::
+* Attributes::
 * Contents::
 * Directories::
 * Filesystems::
@@ -1283,6 +1284,47 @@ This predicate is supported only on @code{find} versions compiled with
 SELinux support and only when SELinux is enabled.
 @end deffn
 
+@node Attributes
+@section File Attributes
+
+The @samp{-attr} test checks that a file's attributes, also known as
+its @dfn{inode flags}, match a specified pattern.
+These file attributes are the ones you can manipulate using the
+@command{chattr} and @command{lsattr} commands.
+See the manual page for @command{chattr} for descriptions of the
+available attributes.
+
+@deffn Test -attr flags
+
+True if the file's attributes match the pattern specified in @var{flags}.
+
+A @var{flags} that starts with @samp{/} matches any file that has
+@emph{any} of the attributes listed after the @samp{/}.
+
+A @var{flags} that starts with @samp{+} matches any file that has
+@emph{all} of the attributes listed after the @samp{+}.
+
+A @var{flags} that starts with @samp{-} matches any file that has
+@emph{none} of the attributes listed after the @samp{-}.
+
+@var{flags} may be a concatenation of any number of @samp{-} and
+@samp{+} sub-patterns. This allows you test for the absence and
+presence of various attributes using a single @samp{-attr} test.
+
+@table @samp
+
+@item -attr /DS
+Match files that have the @code{dirsync} and/or @code{sync}
+attribute.
+
+@item -attr +ad-S
+Match files that have the @code{append} and @code{nodump}
+attributes but not the @code{sync} attribute.
+
+@end table
+
+@end deffn
+
 @node Contents
 @section Contents
 
diff --git a/find/defs.h b/find/defs.h
index 6e79ee00..387ff8e7 100644
--- a/find/defs.h
+++ b/find/defs.h
@@ -138,6 +138,12 @@ struct perm_val
   mode_t val[2];
 };
 
+struct intmask_val
+{
+  int and_mask, xor_mask;
+  bool negate;
+};
+
 /* dir_id is used to support loop detection in oldfind.c
  */
 struct dir_id
@@ -297,6 +303,9 @@ struct predicate
   /* True if this predicate node requires knowledge of the inode number. */
   bool need_inum;
 
+  /* True if this predicate node requires knowledge of the inode flags. */
+  bool need_iflags;
+
   enum EvaluationCost p_cost;
 
   /* est_success_rate is a number between 0.0 and 1.0 */
@@ -324,6 +333,7 @@ struct predicate
     gid_t gid;			/* group */
     struct time_val reftime;	/* newer newerXY anewer cnewer mtime atime ctime mmin amin cmin */
     struct perm_val perm;	/* perm */
+    struct intmask_val intmask;	/* attr */
     struct samefile_file_id samefileid; /* samefile */
     bool types[FTYPE_COUNT];	/* file type(s) */
     struct format_val printf_vec; /* printf fprintf fprint ls fls print0 fprint0 print */
@@ -417,6 +427,7 @@ PREDICATEFUNCTION pred_amin;
 PREDICATEFUNCTION pred_and;
 PREDICATEFUNCTION pred_anewer;
 PREDICATEFUNCTION pred_atime;
+PREDICATEFUNCTION pred_attr;
 PREDICATEFUNCTION pred_closeparen;
 PREDICATEFUNCTION pred_cmin;
 PREDICATEFUNCTION pred_cnewer;
@@ -645,6 +656,9 @@ struct state
   /* Current depth; 0 means current path is a command line arg. */
   int curdepth;
 
+  /* If true, we have fetched the inode flags for the current path. */
+  bool have_iflags;
+
   /* If true, we have called stat on the current path. */
   bool have_stat;
 
@@ -652,6 +666,10 @@ struct state
   bool have_type;
   mode_t type;			/* this is the actual type */
 
+  /* The actual inode flags for the current path.
+   * Valid if and only if have_iflags is true.  */
+  int iflags;
+
   /* The file being operated on, relative to the current directory.
      Used for stat, readlink, remove, and opendir.  */
   const char *rel_pathname;
diff --git a/find/find.1 b/find/find.1
index abf70add..c53a383f 100644
--- a/find/find.1
+++ b/find/find.1
@@ -612,6 +612,19 @@ a file has to have been accessed at least
 .I two
 days ago.
 
+.IP "\-attr /\fIflags\fR"
+Any of the inode attributes in \fIflags\fR are set for the file.
+The \fIflags\fR argument may contain any attribute characters
+described in
+.BR chattr (1).
+
+.IP "\-attr [\-+]\fIflags\fR"
+The inode attributes in \fIflags\fR have the specified state.
+For example,
+.B "\-attr +ad\-S"
+will match files that have the append and nodump flags set
+but not the sync flag.
+
 .IP "\-cmin \fIn\fR"
 File's status was last changed \fIn\fR minutes ago.
 
diff --git a/find/ftsfind.c b/find/ftsfind.c
index 0d96c4ca..511ac5b0 100644
--- a/find/ftsfind.c
+++ b/find/ftsfind.c
@@ -497,10 +497,10 @@ consider_visiting (FTS *p, FTSENT *ent)
   if (options.debug_options & DebugSearch)
     fprintf (stderr,
 	     "consider_visiting (late): %s: "
-	     "fts_info=%-6s, isdir=%d ignore=%d have_stat=%d have_type=%d \n",
+	     "fts_info=%-6s, isdir=%d ignore=%d have_stat=%d have_type=%d have_iflags=%d\n",
 	     quotearg_n_style (0, options.err_quoting_style, ent->fts_path),
 	     get_fts_info_name (ent->fts_info),
-	     isdir, ignore, state.have_stat, state.have_type);
+	     isdir, ignore, state.have_stat, state.have_type, state.have_iflags);
 
   if (!ignore)
     {
@@ -581,6 +581,8 @@ find (char *arg)
 	  state.have_stat = false;
 	  state.have_type = !!ent->fts_statp->st_mode;
 	  state.type = state.have_type ? ent->fts_statp->st_mode : 0;
+	  state.have_iflags = false;
+	  state.iflags = 0;
 	  consider_visiting (p, ent);
 	}
       /* fts_read returned NULL; distinguish between "finished" and "error". */
diff --git a/find/parser.c b/find/parser.c
index a3b9e762..33215757 100644
--- a/find/parser.c
+++ b/find/parser.c
@@ -24,6 +24,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <grp.h>
+#include <linux/fs.h>
 #include <math.h>
 #include <pwd.h>
 #include <regex.h>
@@ -79,6 +80,7 @@ static bool parse_accesscheck   (const struct parser_table*, char *argv[], int *
 static bool parse_amin          (const struct parser_table*, char *argv[], int *arg_ptr);
 static bool parse_and           (const struct parser_table*, char *argv[], int *arg_ptr);
 static bool parse_anewer        (const struct parser_table*, char *argv[], int *arg_ptr);
+static bool parse_attr          (const struct parser_table*, char *argv[], int *arg_ptr);
 static bool parse_cmin          (const struct parser_table*, char *argv[], int *arg_ptr);
 static bool parse_cnewer        (const struct parser_table*, char *argv[], int *arg_ptr);
 static bool parse_comma         (const struct parser_table*, char *argv[], int *arg_ptr);
@@ -229,6 +231,7 @@ static struct parser_table const parse_table[] =
   PARSE_PUNCTUATION("and",                   and),		/* GNU */
   PARSE_TEST       ("anewer",                anewer),	     /* GNU */
   {ARG_TEST,       "atime",                  parse_time, pred_atime}, /* POSIX */
+  PARSE_TEST       ("attr",                  attr),	     /* GNU, 4.7.0+ */
   PARSE_TEST       ("cmin",                  cmin),	     /* GNU */
   PARSE_TEST       ("cnewer",                cnewer),	     /* GNU */
   {ARG_TEST,       "ctime",                  parse_time, pred_ctime}, /* POSIX */
@@ -797,6 +800,171 @@ parse_anewer (const struct parser_table* entry, char **argv, int *arg_ptr)
   return false;
 }
 
+static int _GL_ATTRIBUTE_CONST
+attr_to_iflag (char attr)
+{
+  switch (attr)
+    {
+    case 'a':
+      return FS_APPEND_FL;
+    case 'A':
+      return FS_NOATIME_FL;
+    case 'c':
+      return FS_COMPR_FL;
+    case 'C':
+      return FS_NOCOW_FL;
+    case 'd':
+      return FS_NODUMP_FL;
+    case 'D':
+      return FS_DIRSYNC_FL;
+    case 'e':
+      return FS_EXTENT_FL;
+    case 'E':
+      return FS_ENCRYPT_FL;
+    case 'i':
+      return FS_IMMUTABLE_FL;
+    case 'I':
+      return FS_INDEX_FL;
+    case 'j':
+      return FS_JOURNAL_DATA_FL;
+    case 'N':
+      return FS_INLINE_DATA_FL;
+    case 'P':
+      return FS_PROJINHERIT_FL;
+    case 's':
+      return FS_SECRM_FL;
+    case 'S':
+      return FS_SYNC_FL;
+    case 't':
+      return FS_NOTAIL_FL;
+    case 'T':
+      return FS_TOPDIR_FL;
+    case 'u':
+      return FS_UNRM_FL;
+    }
+  return 0;
+}
+
+static float _GL_ATTRIBUTE_CONST
+attr_to_rate (char attr)
+{
+  switch (attr)
+    {
+    case 'e': /* FS_EXTENT_FL */
+      return 0.5f;
+    case 'd': /* FS_NODUMP_FL */
+    case 'N': /* FS_INLINE_DATA_FL */
+    case 'P': /* FS_PROJINHERIT_FL */
+      return 0.1f;
+    case 'A': /* FS_NOATIME_FL */
+    case 'I': /* FS_INDEX_FL */
+    case 'j': /* FS_JOURNAL_DATA_FL */
+    case 'S': /* FS_SYNC_FL */
+      return 0.01f;
+    case 'a': /* FS_APPEND_FL */
+    case 'c': /* FS_COMPR_FL */
+    case 'C': /* FS_NOCOW_FL */
+    case 'D': /* FS_DIRSYNC_FL */
+    case 'E': /* FS_ENCRYPT_FL */
+    case 'T': /* FS_TOPDIR_FL */
+      return 0.001f;
+    case 'i': /* FS_IMMUTABLE_FL */
+    case 's': /* FS_SECRM_FL */
+    case 't': /* FS_NOTAIL_FL */
+    case 'u': /* FS_UNRM_FL */
+      return 0.0001f;
+    }
+  return 0;
+}
+
+static bool
+parse_attr (const struct parser_table* entry, char **argv, int *arg_ptr)
+{
+  const char *expr, *p;
+  float rate;
+  struct intmask_val mask;
+  struct predicate *our_pred;
+
+  if (!collect_arg (argv, arg_ptr, &expr))
+    return false;
+  p = expr;
+
+  if (*p == '/') /* "any" match */
+    {
+      rate = 0;
+      mask.and_mask = mask.xor_mask = 0;
+      mask.negate = false;
+      while (*++p)
+	{
+	  int iflag;
+	  float my_rate;
+	  if (!(iflag = attr_to_iflag (*p)))
+	    goto bad_flag;
+	  if ((my_rate = attr_to_rate (*p)) > rate)
+	    rate = my_rate;
+	  mask.and_mask |= iflag;
+	}
+      if (!mask.and_mask)
+        goto bad_expr;
+    }
+  else if (*p == '+' || *p == '-') /* bitmask match */
+    {
+      bool on = false;
+      rate = 1;
+      mask.and_mask = mask.xor_mask = 0;
+      mask.negate = true;
+      do
+	{
+	  int iflag;
+	  if (*p == '+')
+	    ++p, on = true;
+	  else if (*p == '-')
+	    ++p, on = false;
+	  if (!(iflag = attr_to_iflag (*p)))
+	    if (*p)
+	      goto bad_flag;
+	    else
+	      goto bad_expr;
+	  if (mask.and_mask & iflag)
+	    goto bad_expr;
+	  mask.and_mask |= iflag;
+	  if (on)
+	    {
+	      mask.xor_mask |= iflag;
+	      rate *= attr_to_rate (*p);
+	    }
+	  else
+	    {
+	      mask.xor_mask &= ~iflag;
+	      rate *= 1 - attr_to_rate (*p);
+	    }
+	}
+	while (*++p);
+    }
+  else
+    goto bad_expr;
+
+    our_pred = insert_primary (entry, expr);
+    our_pred->need_stat = our_pred->need_type = false;
+    our_pred->need_iflags = true;
+    our_pred->p_cost = NeedsStatInfo;
+    our_pred->est_success_rate = rate;
+    our_pred->args.intmask = mask;
+    return true;
+
+bad_expr:
+  die (EXIT_FAILURE, 0,
+       _("invalid -attr string %s"),
+       quotearg_n_style (0, options.err_quoting_style, expr));
+  return false; /* unreachable */
+
+bad_flag:
+  die (EXIT_FAILURE, 0,
+       _("invalid -attr flag `%c'"),
+       *p);
+  return false; /* unreachable */
+}
+
 bool
 parse_closeparen (const struct parser_table* entry, char **argv, int *arg_ptr)
 {
diff --git a/find/pred.c b/find/pred.c
index e34a41dc..91ed69fe 100644
--- a/find/pred.c
+++ b/find/pred.c
@@ -74,6 +74,7 @@ struct pred_assoc pred_table[] =
   {pred_and, "and     "},
   {pred_anewer, "anewer  "},
   {pred_atime, "atime   "},
+  {pred_attr, "attr    "},
   {pred_closeparen, ")       "},
   {pred_cmin, "cmin    "},
   {pred_cnewer, "cnewer  "},
@@ -242,6 +243,20 @@ pred_atime (const char *pathname, struct stat *stat_buf, struct predicate *pred_
   return pred_timewindow (get_stat_atime(stat_buf), pred_ptr, DAYSECS);
 }
 
+bool
+pred_attr (const char *pathname, struct stat *stat_buf, struct predicate *pred_ptr)
+{
+  bool ret;
+  (void) &pathname;
+  (void) &stat_buf;
+
+  assert (state.have_iflags);
+  ret = (state.iflags & pred_ptr->args.intmask.and_mask ^ pred_ptr->args.intmask.xor_mask) != 0;
+  if (pred_ptr->args.intmask.negate)
+    ret = !ret;
+  return ret;
+}
+
 bool
 pred_closeparen (const char *pathname, struct stat *stat_buf, struct predicate *pred_ptr)
 {
@@ -1315,10 +1330,11 @@ print_optlist (FILE *fp, const struct predicate *p)
     {
       print_parenthesised (fp, p->pred_left);
       fprintf (fp,
-               "%s%s%s",
+               "%s%s%s%s",
                p->need_stat ? "[call stat] " : "",
                p->need_type ? "[need type] " : "",
-               p->need_inum ? "[need inum] " : "");
+               p->need_inum ? "[need inum] " : "",
+               p->need_iflags ? "[get iflags] " : "");
       print_predicate (fp, p);
       fprintf (fp, " [est success rate %.4g] ", p->est_success_rate);
       if (options.debug_options & DebugSuccessRates)
diff --git a/find/tree.c b/find/tree.c
index 01870dda..c3f6d74b 100644
--- a/find/tree.c
+++ b/find/tree.c
@@ -926,6 +926,7 @@ static struct pred_cost_lookup costlookup[] =
     { pred_and       ,  NeedsNothing,        },
     { pred_anewer    ,  NeedsStatInfo,       },
     { pred_atime     ,  NeedsStatInfo,       },
+    { pred_attr      ,  NeedsStatInfo        },
     { pred_closeparen,  NeedsNothing         },
     { pred_cmin      ,  NeedsStatInfo,       },
     { pred_cnewer    ,  NeedsStatInfo,       },
@@ -1658,7 +1659,7 @@ print_tree (FILE *fp, struct predicate *node, int indent)
 	   node->est_success_rate,
 	   (node->side_effects ? "" : "no "));
 
-  if (node->need_stat || node->need_type || node->need_inum)
+  if (node->need_stat || node->need_type || node->need_inum || node->need_iflags)
     {
       int comma = 0;
 
@@ -1677,6 +1678,10 @@ print_tree (FILE *fp, struct predicate *node, int indent)
 	{
 	  fprintf (fp, "%stype", comma ? "," : "");
 	}
+      if (node->need_iflags)
+	{
+	  fprintf (fp, "%siflags", comma ? "," : "");
+	}
     }
   fprintf (fp, "\n");
 
diff --git a/find/util.c b/find/util.c
index 7d8e9677..b03e776e 100644
--- a/find/util.c
+++ b/find/util.c
@@ -24,7 +24,9 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <limits.h>
+#include <linux/fs.h>
 #include <string.h>
+#include <sys/ioctl.h>
 #include <sys/stat.h> /* for fstatat() */
 #include <sys/time.h>
 #include <sys/utsname.h>
@@ -183,12 +185,12 @@ normal options (always true, specified before other expressions):\n\
       -depth --help -maxdepth LEVELS -mindepth LEVELS -mount -noleaf\n\
       --version -xdev -ignore_readdir_race -noignore_readdir_race\n"));
   HTL (_("\
-tests (N can be +N or -N or N): -amin N -anewer FILE -atime N -cmin N\n\
-      -cnewer FILE -ctime N -empty -false -fstype TYPE -gid N -group NAME\n\
-      -ilname PATTERN -iname PATTERN -inum N -iwholename PATTERN -iregex PATTERN\n\
-      -links N -lname PATTERN -mmin N -mtime N -name PATTERN -newer FILE"));
+tests (N can be +N or -N or N): -amin N -anewer FILE -atime N -attr [-+/]FLAGS\n\
+      -cmin N -cnewer FILE -ctime N -empty -false -fstype TYPE -gid N\n\
+      -group NAME -ilname PATTERN -iname PATTERN -inum N -iwholename PATTERN\n\
+      -iregex PATTERN -links N -lname PATTERN -mmin N -mtime N -name PATTERN"));
   HTL (_("\n\
-      -nouser -nogroup -path PATTERN -perm [-/]MODE -regex PATTERN\n\
+      -newer FILE -nouser -nogroup -path PATTERN -perm [-/]MODE -regex PATTERN\n\
       -readable -writable -executable\n\
       -wholename PATTERN -size N[bcwkMG] -true -type [bcdpflsD] -uid N\n\
       -used N -user NAME -xtype [bcdpfls]"));
@@ -272,6 +274,45 @@ get_statinfo (const char *pathname, const char *name, struct stat *p)
   return 0;
 }
 
+/* Get the inode flags for the current file, if they are not already known.
+ * Returns 0 on success (or if we did nothing).
+ */
+static int
+get_iflags ()
+{
+  int ret = 0;
+
+  if (!state.have_iflags)
+    {
+      int fd = -1, ret;
+      assert (state.have_type);
+      if (!S_ISREG(state.type) && !S_ISDIR(state.type))
+	return -1;
+      if (AT_FDCWD != state.cwd_dir_fd)
+	assert (state.cwd_dir_fd >= 0);
+      switch (options.symlink_handling)
+	{
+	case SYMLINK_DEREF_ARGSONLY:
+	  if (state.curdepth == 0)
+	case SYMLINK_ALWAYS_DEREF:
+	    if ((fd = openat (state.cwd_dir_fd, state.rel_pathname,
+	                      O_RDONLY | O_CLOEXEC | O_NOATIME | O_NOCTTY)) >= 0)
+	      break;
+	  /* fall through */
+	case SYMLINK_NEVER_DEREF:
+	  fd = openat (state.cwd_dir_fd, state.rel_pathname,
+	               O_RDONLY | O_CLOEXEC | O_NOATIME | O_NOCTTY | O_NOFOLLOW);
+	}
+      if (fd < 0)
+	return -1;
+      ret = ioctl (fd, FS_IOC_GETFLAGS, &state.iflags);
+      close (fd);
+      state.have_iflags = ret == 0;
+    }
+
+  return ret;
+}
+
 /* Get the stat/type/inode information for a file, if it is not
  * already known.   Returns 0 on success (or if we did nothing).
  */
@@ -289,7 +330,7 @@ get_info (const char *pathname,
     {
       todo = true;		/* need full stat info */
     }
-  else if (pred_ptr->need_type && !state.have_type)
+  else if ((pred_ptr->need_type || pred_ptr->need_iflags) && !state.have_type)
     {
       todo = true;		/* need to stat to get the type */
     }
@@ -1096,12 +1137,14 @@ apply_predicate(const char *pathname, struct stat *stat_buf, struct predicate *p
 {
   ++p->perf.visits;
 
-  if (p->need_stat || p->need_type || p->need_inum)
+  if (p->need_stat || p->need_type || p->need_inum || p->need_iflags)
     {
       /* We may need a stat here. */
       if (get_info(pathname, stat_buf, p) != 0)
 	    return false;
     }
+  if (p->need_iflags && get_iflags () != 0)
+    return false;
   if ((p->pred_func)(pathname, stat_buf, p))
     {
       ++(p->perf.successes);
-- 
2.20.1

