git: da2025a0e894 - main - fts: Add FTS_COMFOLLOWDIR and FTS_NOSTAT_TYPE.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Thu, 08 May 2025 14:29:42 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=da2025a0e89455fb646b542b53d5a4ddaa2acbe0

commit da2025a0e89455fb646b542b53d5a4ddaa2acbe0
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-05-08 14:28:51 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-05-08 14:29:15 +0000

    fts: Add FTS_COMFOLLOWDIR and FTS_NOSTAT_TYPE.
    
    MFC after:      never
    Relnotes:       yes
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans, imp
    Differential Revision:  https://reviews.freebsd.org/D50233
---
 include/fts.h      |  6 +++++-
 lib/libc/gen/fts.3 | 22 +++++++++++++++++++++-
 lib/libc/gen/fts.c | 45 ++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 64 insertions(+), 9 deletions(-)

diff --git a/include/fts.h b/include/fts.h
index f2c40b854ffb..479905bda463 100644
--- a/include/fts.h
+++ b/include/fts.h
@@ -65,7 +65,11 @@ typedef struct {
 #define	FTS_SEEDOT	0x000020	/* return dot and dot-dot */
 #define	FTS_XDEV	0x000040	/* don't cross devices */
 #define	FTS_WHITEOUT	0x000080	/* return whiteout information */
-#define	FTS_OPTIONMASK	0x0000ff	/* valid user option mask */
+					/* 0x0100 is FTS_NAMEONLY below */
+					/* 0x0200 was previously FTS_STOP */
+#define FTS_COMFOLLOWDIR 0x00400	/* like COMFOLLOW but directories only */
+#define FTS_NOSTAT_TYPE	0x000800	/* like NOSTAT but use d_type */
+#define	FTS_OPTIONMASK	0x000cff	/* valid user option mask */
 
 /* valid only for fts_children() */
 #define	FTS_NAMEONLY	0x000100	/* child names only */
diff --git a/lib/libc/gen/fts.3 b/lib/libc/gen/fts.3
index 3007b773ec55..0c32bb0ebdb6 100644
--- a/lib/libc/gen/fts.3
+++ b/lib/libc/gen/fts.3
@@ -25,7 +25,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd April 17, 2025
+.Dd May 7, 2025
 .Dt FTS 3
 .Os
 .Sh NAME
@@ -394,6 +394,10 @@ This option causes any symbolic link specified as a root path to be
 followed immediately whether or not
 .Dv FTS_LOGICAL
 is also specified.
+.It Dv FTS_COMFOLLOWDIR
+This option is similar to
+.Dv FTS_COMFOLLOW ,
+but only follows symbolic links to directories.
 .It Dv FTS_LOGICAL
 This option causes the
 .Nm
@@ -449,6 +453,15 @@ field to
 and leave the contents of the
 .Fa statp
 field undefined.
+.It Dv FTS_NOSTAT_TYPE
+This option is similar to
+.Dv FTS_NOSTAT ,
+but attempts to populate
+.Fa fts_info
+based on information from the
+.Fa d_type
+field of
+.Vt struct dirent .
 .It Dv FTS_PHYSICAL
 This option causes the
 .Nm
@@ -820,6 +833,13 @@ functions were introduced in
 principally to provide for alternative interfaces to the
 .Nm
 functionality using different data structures.
+Blocks support and the
+.Dv FTS_COMFOLLOWDIR
+and
+.Dv FTS_NOSTAT
+options were added in
+.Fx 15.0
+based on similar functionality in macOS.
 .Sh BUGS
 The
 .Fn fts_open
diff --git a/lib/libc/gen/fts.c b/lib/libc/gen/fts.c
index a55b4a6e2981..fa3ee9cc4c83 100644
--- a/lib/libc/gen/fts.c
+++ b/lib/libc/gen/fts.c
@@ -126,6 +126,10 @@ __fts_open(FTS *sp, char * const *argv)
 	if (ISSET(FTS_LOGICAL))
 		SET(FTS_NOCHDIR);
 
+	/* NOSTAT_TYPE implies NOSTAT */
+        if (ISSET(FTS_NOSTAT_TYPE))
+                SET(FTS_NOSTAT);
+
 	/*
 	 * Start out with 1K of path space, and enough, in any case,
 	 * to hold the user's paths.
@@ -149,7 +153,9 @@ __fts_open(FTS *sp, char * const *argv)
 		p->fts_level = FTS_ROOTLEVEL;
 		p->fts_parent = parent;
 		p->fts_accpath = p->fts_name;
-		p->fts_info = fts_stat(sp, p, ISSET(FTS_COMFOLLOW), -1);
+		p->fts_info = fts_stat(sp, p,
+		    ISSET(FTS_COMFOLLOWDIR) ? -1 : ISSET(FTS_COMFOLLOW),
+		    -1);
 
 		/* Command-line "." and ".." are real directories. */
 		if (p->fts_info == FTS_DOT)
@@ -904,6 +910,25 @@ mem1:				saved_errno = errno;
 			    p->fts_info == FTS_DC || p->fts_info == FTS_DOT))
 				--nlinks;
 		}
+		if (p->fts_info == FTS_NSOK && ISSET(FTS_NOSTAT_TYPE)) {
+			switch (dp->d_type) {
+			case DT_FIFO:
+			case DT_CHR:
+			case DT_BLK:
+			case DT_SOCK:
+				p->fts_info = FTS_DEFAULT;
+				break;
+			case DT_REG:
+				p->fts_info = FTS_F;
+				break;
+			case DT_LNK:
+				p->fts_info = FTS_SL;
+				break;
+			case DT_WHT:
+				p->fts_info = FTS_W;
+				break;
+			}
+		}
 
 		/* We walk in directory order so "ls -f" doesn't get upset. */
 		p->fts_link = NULL;
@@ -980,7 +1005,7 @@ fts_stat(FTS *sp, FTSENT *p, int follow, int dfd)
 	dev_t dev;
 	ino_t ino;
 	struct stat *sbp, sb;
-	int saved_errno;
+	int ret, saved_errno;
 	const char *path;
 
 	if (dfd == -1) {
@@ -1003,19 +1028,25 @@ fts_stat(FTS *sp, FTSENT *p, int follow, int dfd)
 	}
 
 	/*
-	 * If doing a logical walk, or application requested FTS_FOLLOW, do
-	 * a stat(2).  If that fails, check for a non-existent symlink.  If
-	 * fail, set the errno from the stat call.
+	 * If doing a logical walk, or caller requested FTS_COMFOLLOW, do
+	 * a full stat(2).  If that fails, do an lstat(2) to check for a
+	 * non-existent symlink.  If that fails, set the errno from the
+	 * stat(2) call.
+	 *
+	 * As a special case, if stat(2) succeeded but the target is not a
+	 * directory and follow is negative (indicating FTS_COMFOLLOWDIR
+	 * rather than FTS_COMFOLLOW), we also revert to lstat(2).
 	 */
 	if (ISSET(FTS_LOGICAL) || follow) {
-		if (fstatat(dfd, path, sbp, 0)) {
+		if ((ret = fstatat(dfd, path, sbp, 0)) != 0 ||
+		    (follow < 0 && !S_ISDIR(sbp->st_mode))) {
 			saved_errno = errno;
 			if (fstatat(dfd, path, sbp, AT_SYMLINK_NOFOLLOW)) {
 				p->fts_errno = saved_errno;
 				goto err;
 			}
 			errno = 0;
-			if (S_ISLNK(sbp->st_mode))
+			if (ret != 0 && S_ISLNK(sbp->st_mode))
 				return (FTS_SLNONE);
 		}
 	} else if (fstatat(dfd, path, sbp, AT_SYMLINK_NOFOLLOW)) {