summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--corebinutils/chflags/.gitignore45
-rw-r--r--corebinutils/chflags/GNUmakefile41
-rw-r--r--corebinutils/chflags/LICENSE32
-rw-r--r--corebinutils/chflags/LICENSES/BSD-3-Clause.txt11
-rw-r--r--corebinutils/chflags/README.md24
-rw-r--r--corebinutils/chflags/chflags.1265
-rw-r--r--corebinutils/chflags/chflags.c252
-rw-r--r--corebinutils/chflags/flags.c255
-rw-r--r--corebinutils/chflags/flags.h38
-rw-r--r--corebinutils/chflags/fts.c458
-rw-r--r--corebinutils/chflags/fts.h90
-rw-r--r--corebinutils/chflags/tests/test.sh96
12 files changed, 1607 insertions, 0 deletions
diff --git a/corebinutils/chflags/.gitignore b/corebinutils/chflags/.gitignore
new file mode 100644
index 0000000000..4df974f9b5
--- /dev/null
+++ b/corebinutils/chflags/.gitignore
@@ -0,0 +1,45 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+# Don't ignore translation files under `contrib/...`.
+!contrib/**/po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+_.tinderbox.*
+_.universe-toolchain
+_.amd64.*
+_.arm.*
+_.arm64.*
+_.i386.*
+_.powerpc.*
+_.riscv.*
+.*DS_Store
+.depend*
+GPATH
+GRTAGS
+GTAGS
+ID
+cscope.files
+cscope.in.out
+cscope.out
+cscope.po.out
+compile_commands.json
+compile_commands.events.json
+tags
+.cache
+.clangd
+.ccls-cache
+sys/*/compile
+/src.conf
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/chflags/GNUmakefile b/corebinutils/chflags/GNUmakefile
new file mode 100644
index 0000000000..e0240633ff
--- /dev/null
+++ b/corebinutils/chflags/GNUmakefile
@@ -0,0 +1,41 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_GNU_SOURCE -I$(CURDIR)
+CFLAGS ?= -O2
+CFLAGS += -g -Wall -Wextra -Wno-unused-parameter
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/chflags
+OBJS := $(OBJDIR)/chflags.o $(OBJDIR)/flags.o $(OBJDIR)/fts.o
+
+.PHONY: all clean dirs test status
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/chflags.o: $(CURDIR)/chflags.c $(CURDIR)/flags.h $(CURDIR)/fts.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/chflags.c" -o "$@"
+
+$(OBJDIR)/flags.o: $(CURDIR)/flags.c $(CURDIR)/flags.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/flags.c" -o "$@"
+
+$(OBJDIR)/fts.o: $(CURDIR)/fts.c $(CURDIR)/fts.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/fts.c" -o "$@"
+
+test: $(TARGET)
+ CHFLAGS_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(CURDIR)/build" "$(CURDIR)/out"
diff --git a/corebinutils/chflags/LICENSE b/corebinutils/chflags/LICENSE
new file mode 100644
index 0000000000..7dafee1456
--- /dev/null
+++ b/corebinutils/chflags/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1992, 1993, 1994
+ The Regents of the University of California. All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+Kevin Fall.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE. \ No newline at end of file
diff --git a/corebinutils/chflags/LICENSES/BSD-3-Clause.txt b/corebinutils/chflags/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..ea890afbc7
--- /dev/null
+++ b/corebinutils/chflags/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/corebinutils/chflags/README.md b/corebinutils/chflags/README.md
new file mode 100644
index 0000000000..3b25c556ff
--- /dev/null
+++ b/corebinutils/chflags/README.md
@@ -0,0 +1,24 @@
+# chflags
+
+Standalone musl-libc-based Linux port of FreeBSD `chflags` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Notes
+
+- Port strategy is minimum-diff: the original traversal and CLI flow stay in `chflags.c`.
+- `fts(3)` is provided locally in this project because musl does not ship `<fts.h>`.
+- Linux flag handling is implemented with `FS_IOC_GETFLAGS` and `FS_IOC_SETFLAGS`.
+- Only the Linux-mappable subset of FreeBSD flag names is supported in this phase.
diff --git a/corebinutils/chflags/chflags.1 b/corebinutils/chflags/chflags.1
new file mode 100644
index 0000000000..ee93f10506
--- /dev/null
+++ b/corebinutils/chflags/chflags.1
@@ -0,0 +1,265 @@
+.\"-
+.\" Copyright (c) 1989, 1990, 1993, 1994
+.\" The Regents of the University of California. All rights reserved.
+.\"
+.\" Copyright (c) 2026
+.\" Project Tick. All rights reserved.
+.\"
+.\" This code is derived from software contributed to Berkeley by
+.\" the Institute of Electrical and Electronics Engineers, Inc.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd June 12, 2018
+.Dt CHFLAGS 1
+.Os
+.Sh NAME
+.Nm chflags
+.Nd change file flags
+.Sh SYNOPSIS
+.Nm
+.Op Fl fhvx
+.Oo
+.Fl R
+.Op Fl H | Fl L | Fl P
+.Oc
+.Ar flags
+.Ar
+.Sh DESCRIPTION
+The
+.Nm
+utility modifies the file flags of the listed files
+as specified by the
+.Ar flags
+operand.
+.Pp
+The options are as follows:
+.Bl -tag -width indent
+.It Fl f
+Do not display a diagnostic message if
+.Nm
+could not modify the flags for
+.Va file ,
+nor modify the exit status to reflect such failures.
+.It Fl H
+If the
+.Fl R
+option is specified, symbolic links on the command line are followed
+and hence unaffected by the command.
+(Symbolic links encountered during traversal are not followed.)
+.It Fl h
+If the
+.Ar file
+is a symbolic link,
+change the file flags of the link itself rather than the file to which it points.
+.It Fl L
+If the
+.Fl R
+option is specified, all symbolic links are followed.
+.It Fl P
+If the
+.Fl R
+option is specified, no symbolic links are followed.
+This is the default.
+.It Fl R
+Change the file flags of the file hierarchies rooted in the files,
+instead of just the files themselves.
+Beware of unintentionally matching the
+.Dq Pa ".."
+hard link to the parent directory when using wildcards like
+.Dq Li ".*" .
+.It Fl v
+Cause
+.Nm
+to be verbose, showing filenames as the flags are modified.
+If the
+.Fl v
+option is specified more than once, the old and new flags of the file
+will also be printed, in octal notation.
+.It Fl x
+Do not cross mount points.
+.El
+.Pp
+The flags are specified as an octal number or a comma separated list
+of keywords.
+The following keywords are currently defined:
+.Bl -tag -offset indent -width ".Cm opaque"
+.It Cm arch , archived
+set the archived flag (super-user only)
+.It Cm nodump
+set the nodump flag (owner or super-user only)
+.It Cm opaque
+set the opaque flag (owner or super-user only)
+.It Cm sappnd , sappend
+set the system append-only flag (super-user only)
+.It Cm schg , schange , simmutable
+set the system immutable flag (super-user only)
+.It Cm snapshot
+set the snapshot flag (filesystems do not allow changing this flag)
+.It Cm sunlnk , sunlink
+set the system undeletable flag (super-user only)
+.It Cm uappnd , uappend
+set the user append-only flag (owner or super-user only)
+.It Cm uarch , uarchive
+set the archive flag (owner or super-user only)
+.It Cm uchg , uchange , uimmutable
+set the user immutable flag (owner or super-user only)
+.It Cm uhidden , hidden
+set the hidden file attribute (owner or super-user only)
+.It Cm uoffline , offline
+set the offline file attribute (owner or super-user only)
+.It Cm urdonly , rdonly , readonly
+set the DOS, Windows and CIFS readonly flag (owner or super-user only)
+.It Cm usparse , sparse
+set the sparse file attribute (owner or super-user only)
+.It Cm usystem , system
+set the DOS, Windows and CIFS system flag (owner or super-user only)
+.It Cm ureparse , reparse
+set the Windows reparse point file attribute (owner or super-user only)
+.It Cm uunlnk , uunlink
+set the user undeletable flag (owner or super-user only)
+.El
+.Pp
+Putting the letters
+.Dq Ar no
+before or removing the letters
+.Dq Ar no
+from a keyword causes the flag to be cleared.
+For example:
+.Pp
+.Bl -tag -offset indent -width "nouchg" -compact
+.It Cm nouchg
+clear the user immutable flag (owner or super-user only)
+.It Cm dump
+clear the nodump flag (owner or super-user only)
+.El
+.Pp
+A few of the octal values include:
+.Bl -tag -offset indent -width ".Li 10"
+.It Li 0
+Clear all file flags.
+.It Li 1
+Translates to the
+.Cm nodump
+keyword.
+.It Li 2
+Translates to the
+.Cm uchg
+keyword.
+.It Li 3
+Translates to the
+.Cm uchg , nodump
+keywords.
+.It Li 4
+Translates to the
+.Cm uappnd
+keyword.
+.It Li 10
+Translates to the
+.Cm opaque
+keyword.
+.It Li 20
+translates to the
+.Cm uunlnk
+keyword.
+.El
+.Pp
+Other combinations of keywords may be placed by using
+the octets assigned; however, these are the most notable.
+.Pp
+Unless the
+.Fl H ,
+.Fl L ,
+or
+.Fl h
+options are given,
+.Nm
+on a symbolic link always succeeds and has no effect.
+The
+.Fl H ,
+.Fl L
+and
+.Fl P
+options are ignored unless the
+.Fl R
+option is specified.
+In addition, these options override each other and the
+command's actions are determined by the last one specified.
+.Pp
+You can use "ls -lo" to see the flags of existing files.
+.Pp
+Note that the ability to change certain flags is dependent
+on the current kernel
+.Va securelevel
+setting.
+See
+.Xr security 7
+for more information on this setting.
+.Pp
+If
+.Nm
+receives a
+.Dv SIGINFO
+signal (see the
+.Cm status
+argument for
+.Xr stty 1 ) ,
+then the current filename as well as the old and new flags are displayed.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Recursively clear all flags on files and directories contained within the
+.Fa foobar
+directory hierarchy:
+.Dl Nm Fl R Ar 0 Ar foobar
+.Sh SEE ALSO
+.Xr ls 1 ,
+.Xr chflags 2 ,
+.Xr stat 2 ,
+.Xr fts 3 ,
+.Xr security 7 ,
+.Xr symlink 7
+.Sh HISTORY
+The
+.Nm
+command first appeared in
+.Bx 4.4 .
+.Sh BUGS
+Only a limited number of utilities are
+.Nm
+aware.
+Some of these tools include
+.Xr ls 1 ,
+.Xr cp 1 ,
+.Xr find 1 ,
+.Xr install 1 ,
+.Xr dump 8 ,
+and
+.Xr restore 8 .
+In particular a tool which is not currently
+.Nm
+aware is the
+.Xr pax 1
+utility.
diff --git a/corebinutils/chflags/chflags.c b/corebinutils/chflags/chflags.c
new file mode 100644
index 0000000000..07e7a94f06
--- /dev/null
+++ b/corebinutils/chflags/chflags.c
@@ -0,0 +1,252 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1992, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "flags.h"
+#include "fts.h"
+
+#ifndef SIGINFO
+#define SIGINFO SIGUSR1
+#endif
+
+#ifndef __dead2
+#define __dead2 __attribute__((noreturn))
+#endif
+
+#ifndef __unused
+#define __unused __attribute__((unused))
+#endif
+
+#ifndef warnc
+#define warnc(code, fmt, ...) do { errno = (code); warn((fmt), ##__VA_ARGS__); } while (0)
+#endif
+
+static volatile sig_atomic_t siginfo;
+
+static void usage(void) __dead2;
+
+static void
+siginfo_handler(int sig __unused)
+{
+ (void)sig;
+ siginfo = 1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ FTS *ftsp;
+ FTSENT *p;
+ u_long clear, newflags, set;
+ long val;
+ int Hflag, Lflag, Rflag, fflag, hflag, vflag, xflag;
+ int ch, e, fts_options, oct, rval;
+ char *flags, *ep;
+
+ Hflag = Lflag = Rflag = fflag = hflag = vflag = xflag = 0;
+ while ((ch = getopt(argc, argv, "HLPRfhvx")) != -1)
+ switch (ch) {
+ case 'H':
+ Hflag = 1;
+ Lflag = 0;
+ break;
+ case 'L':
+ Lflag = 1;
+ Hflag = 0;
+ break;
+ case 'P':
+ Hflag = Lflag = 0;
+ break;
+ case 'R':
+ Rflag = 1;
+ break;
+ case 'f':
+ fflag = 1;
+ break;
+ case 'h':
+ hflag = 1;
+ break;
+ case 'v':
+ vflag++;
+ break;
+ case 'x':
+ xflag = 1;
+ break;
+ case '?':
+ default:
+ usage();
+ }
+ argv += optind;
+ argc -= optind;
+
+ if (argc < 2)
+ usage();
+
+ (void)signal(SIGINFO, siginfo_handler);
+
+ if (Rflag) {
+ if (hflag)
+ errx(1, "the -R and -h options may not be "
+ "specified together.");
+ if (Lflag) {
+ fts_options = FTS_LOGICAL;
+ } else {
+ fts_options = FTS_PHYSICAL;
+
+ if (Hflag) {
+ fts_options |= FTS_COMFOLLOW;
+ }
+ }
+ } else if (hflag) {
+ fts_options = FTS_PHYSICAL;
+ } else {
+ fts_options = FTS_LOGICAL;
+ }
+ if (xflag)
+ fts_options |= FTS_XDEV;
+
+ flags = *argv;
+ if (*flags >= '0' && *flags <= '7') {
+ errno = 0;
+ val = strtol(flags, &ep, 8);
+ if (val < 0)
+ errno = ERANGE;
+ if (errno)
+ err(1, "invalid flags: %s", flags);
+ if (*ep)
+ errx(1, "invalid flags: %s", flags);
+ set = val;
+ oct = 1;
+ } else {
+ if (strtofflags(&flags, &set, &clear))
+ errx(1, "invalid flag: %s", flags);
+ clear = ~clear;
+ oct = 0;
+ }
+
+ if ((ftsp = fts_open(++argv, fts_options , 0)) == NULL)
+ err(1, NULL);
+
+ for (rval = 0; errno = 0, (p = fts_read(ftsp)) != NULL;) {
+ int atflag;
+ u_long oldflags;
+
+ if ((fts_options & FTS_LOGICAL) ||
+ ((fts_options & FTS_COMFOLLOW) &&
+ p->fts_level == FTS_ROOTLEVEL))
+ atflag = 0;
+ else
+ atflag = AT_SYMLINK_NOFOLLOW;
+
+ switch (p->fts_info) {
+ case FTS_D: /* Change it at FTS_DP if we're recursive. */
+ if (!Rflag)
+ fts_set(ftsp, p, FTS_SKIP);
+ continue;
+ case FTS_DNR: /* Warn, chflags. */
+ warnx("%s: %s", p->fts_path, strerror(p->fts_errno));
+ rval = 1;
+ break;
+ case FTS_ERR: /* Warn, continue. */
+ case FTS_NS:
+ warnx("%s: %s", p->fts_path, strerror(p->fts_errno));
+ rval = 1;
+ continue;
+ default:
+ break;
+ }
+ if (getflagsat(AT_FDCWD, p->fts_accpath, &oldflags, atflag) == -1) {
+ e = errno;
+ if (!fflag) {
+ warnc(e, "%s", p->fts_path);
+ rval = 1;
+ }
+ if (siginfo) {
+ (void)printf("%s: %s\n", p->fts_path,
+ strerror(e));
+ siginfo = 0;
+ }
+ continue;
+ }
+ if (oct)
+ newflags = set;
+ else
+ newflags = (oldflags | set) & clear;
+ if (newflags == oldflags)
+ continue;
+ if (chflagsat(AT_FDCWD, p->fts_accpath, newflags,
+ atflag) == -1) {
+ e = errno;
+ if (!fflag) {
+ warnc(e, "%s", p->fts_path);
+ rval = 1;
+ }
+ if (siginfo) {
+ (void)printf("%s: %s\n", p->fts_path,
+ strerror(e));
+ siginfo = 0;
+ }
+ } else if (vflag || siginfo) {
+ (void)printf("%s", p->fts_path);
+ if (vflag > 1 || siginfo)
+ (void)printf(": 0%lo -> 0%lo",
+ oldflags,
+ newflags);
+ (void)printf("\n");
+ siginfo = 0;
+ }
+ }
+ if (errno)
+ err(1, "fts_read");
+ exit(rval);
+}
+
+static void
+usage(void)
+{
+ (void)fprintf(stderr,
+ "usage: chflags [-fhvx] [-R [-H | -L | -P]] flags file ...\n");
+ exit(1);
+}
diff --git a/corebinutils/chflags/flags.c b/corebinutils/chflags/flags.c
new file mode 100644
index 0000000000..31bb2a3cbd
--- /dev/null
+++ b/corebinutils/chflags/flags.c
@@ -0,0 +1,255 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "flags.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifndef FS_IOC_GETFLAGS
+#define FS_IOC_GETFLAGS _IOR('f', 1, long)
+#endif
+
+#ifndef FS_IOC_SETFLAGS
+#define FS_IOC_SETFLAGS _IOW('f', 2, long)
+#endif
+
+#ifndef FS_SECRM_FL
+#define FS_SECRM_FL 0x00000001
+#endif
+
+#ifndef FS_UNRM_FL
+#define FS_UNRM_FL 0x00000002
+#endif
+
+#ifndef FS_COMPR_FL
+#define FS_COMPR_FL 0x00000004
+#endif
+
+#ifndef FS_SYNC_FL
+#define FS_SYNC_FL 0x00000008
+#endif
+
+#ifndef FS_IMMUTABLE_FL
+#define FS_IMMUTABLE_FL 0x00000010
+#endif
+
+#ifndef FS_APPEND_FL
+#define FS_APPEND_FL 0x00000020
+#endif
+
+#ifndef FS_NODUMP_FL
+#define FS_NODUMP_FL 0x00000040
+#endif
+
+#ifndef FS_NOATIME_FL
+#define FS_NOATIME_FL 0x00000080
+#endif
+
+#ifndef FS_DIRSYNC_FL
+#define FS_DIRSYNC_FL 0x00010000
+#endif
+
+#ifndef FS_TOPDIR_FL
+#define FS_TOPDIR_FL 0x00020000
+#endif
+
+struct flag_name {
+ const char *name;
+ unsigned long set;
+ unsigned long clear;
+};
+
+static const struct flag_name flag_names[] = {
+ { "nodump", FS_NODUMP_FL, 0 },
+ { "dump", 0, FS_NODUMP_FL },
+ { "uappnd", FS_APPEND_FL, 0 },
+ { "uappend", FS_APPEND_FL, 0 },
+ { "appnd", FS_APPEND_FL, 0 },
+ { "append", FS_APPEND_FL, 0 },
+ { "sappnd", FS_APPEND_FL, 0 },
+ { "sappend", FS_APPEND_FL, 0 },
+ { "nouappnd", 0, FS_APPEND_FL },
+ { "nouappend", 0, FS_APPEND_FL },
+ { "noappnd", 0, FS_APPEND_FL },
+ { "noappend", 0, FS_APPEND_FL },
+ { "nosappnd", 0, FS_APPEND_FL },
+ { "nosappend", 0, FS_APPEND_FL },
+ { "uchg", FS_IMMUTABLE_FL, 0 },
+ { "uimmutable", FS_IMMUTABLE_FL, 0 },
+ { "chg", FS_IMMUTABLE_FL, 0 },
+ { "immutable", FS_IMMUTABLE_FL, 0 },
+ { "schg", FS_IMMUTABLE_FL, 0 },
+ { "simmutable", FS_IMMUTABLE_FL, 0 },
+ { "nouchg", 0, FS_IMMUTABLE_FL },
+ { "nouimmutable", 0, FS_IMMUTABLE_FL },
+ { "nochg", 0, FS_IMMUTABLE_FL },
+ { "noimmutable", 0, FS_IMMUTABLE_FL },
+ { "noschg", 0, FS_IMMUTABLE_FL },
+ { "nosimmutable", 0, FS_IMMUTABLE_FL },
+ { "noatime", FS_NOATIME_FL, 0 },
+ { "atime", 0, FS_NOATIME_FL },
+ { "sync", FS_SYNC_FL, 0 },
+ { "nosync", 0, FS_SYNC_FL },
+ { "dirsync", FS_DIRSYNC_FL, 0 },
+ { "nodirsync", 0, FS_DIRSYNC_FL },
+ { "topdir", FS_TOPDIR_FL, 0 },
+ { "notopdir", 0, FS_TOPDIR_FL },
+ { "compr", FS_COMPR_FL, 0 },
+ { "compress", FS_COMPR_FL, 0 },
+ { "nocompr", 0, FS_COMPR_FL },
+ { "nocompress", 0, FS_COMPR_FL },
+};
+
+static int
+open_flag_target(int dirfd, const char *path, int atflags)
+{
+ struct stat st;
+ int fd;
+ int open_flags;
+
+ if ((atflags & AT_SYMLINK_NOFOLLOW) != 0) {
+ if (fstatat(dirfd, path, &st, AT_SYMLINK_NOFOLLOW) != 0)
+ return (-1);
+ if (S_ISLNK(st.st_mode)) {
+ errno = EOPNOTSUPP;
+ return (-1);
+ }
+ }
+
+ open_flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC;
+ if ((atflags & AT_SYMLINK_NOFOLLOW) != 0)
+ open_flags |= O_NOFOLLOW;
+ fd = openat(dirfd, path, open_flags);
+ return (fd);
+}
+
+int
+getflagsat(int dirfd, const char *path, unsigned long *flagsp, int atflags)
+{
+ int fd;
+ int iflags;
+
+ fd = open_flag_target(dirfd, path, atflags);
+ if (fd < 0)
+ return (-1);
+ if (ioctl(fd, FS_IOC_GETFLAGS, &iflags) != 0) {
+ int saved_errno;
+
+ saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+ return (-1);
+ }
+ close(fd);
+ *flagsp = (unsigned long)(unsigned int)iflags;
+ return (0);
+}
+
+int
+chflagsat(int dirfd, const char *path, unsigned long flags, int atflags)
+{
+ int fd;
+ int iflags;
+
+ fd = open_flag_target(dirfd, path, atflags);
+ if (fd < 0)
+ return (-1);
+ iflags = (int)flags;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &iflags) != 0) {
+ int saved_errno;
+
+ saved_errno = errno;
+ close(fd);
+ errno = saved_errno;
+ return (-1);
+ }
+ close(fd);
+ return (0);
+}
+
+int
+strtofflags(char **stringp, unsigned long *setp, unsigned long *clrp)
+{
+ char *flags;
+ char *copy;
+ char *saveptr;
+ char *token;
+ size_t i;
+
+ *setp = 0;
+ *clrp = 0;
+ flags = *stringp;
+ copy = strdup(flags);
+ if (copy == NULL)
+ return (-1);
+
+ for (token = strtok_r(copy, ",", &saveptr);
+ token != NULL;
+ token = strtok_r(NULL, ",", &saveptr)) {
+ bool found;
+
+ while (isspace((unsigned char)*token))
+ token++;
+ if (*token == '\0') {
+ free(copy);
+ *stringp = flags;
+ return (-1);
+ }
+
+ found = false;
+ for (i = 0; i < sizeof(flag_names) / sizeof(flag_names[0]); i++) {
+ if (strcmp(token, flag_names[i].name) == 0) {
+ *setp |= flag_names[i].set;
+ *clrp |= flag_names[i].clear;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ free(copy);
+ *stringp = flags;
+ return (-1);
+ }
+ }
+
+ free(copy);
+ return (0);
+}
diff --git a/corebinutils/chflags/flags.h b/corebinutils/chflags/flags.h
new file mode 100644
index 0000000000..5936832d7a
--- /dev/null
+++ b/corebinutils/chflags/flags.h
@@ -0,0 +1,38 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <sys/types.h>
+
+int chflagsat(int dirfd, const char *path, unsigned long flags, int atflags);
+int getflagsat(int dirfd, const char *path, unsigned long *flagsp, int atflags);
+int strtofflags(char **stringp, unsigned long *setp, unsigned long *clrp);
diff --git a/corebinutils/chflags/fts.c b/corebinutils/chflags/fts.c
new file mode 100644
index 0000000000..2396ff2a14
--- /dev/null
+++ b/corebinutils/chflags/fts.c
@@ -0,0 +1,458 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "fts.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum {
+ FTS_STATE_PRE = 0,
+ FTS_STATE_CHILDREN,
+ FTS_STATE_DNR,
+ FTS_STATE_POST,
+ FTS_STATE_DONE,
+};
+
+static FTSENT *fts_make_entry(FTS *ftsp, FTSENT *parent, const char *path,
+ int level, int is_root);
+static int fts_prepare_children(FTS *ftsp, FTSENT *parent);
+static FTSENT *fts_advance(FTS *ftsp, FTSENT *ent);
+static void fts_free_entry(FTSENT *ent);
+static const char *fts_basename(const char *path);
+static char *fts_join_path(const char *parent, const char *name);
+static int fts_should_follow(const FTS *ftsp, int is_root);
+static int fts_is_cycle(const FTSENT *parent, const struct stat *st);
+static const FTSENT *fts_root(const FTSENT *ent);
+
+static const char *
+fts_basename(const char *path)
+{
+ const char *end;
+ const char *base;
+
+ end = path + strlen(path);
+ while (end > path + 1 && end[-1] == '/')
+ end--;
+ base = end;
+ while (base > path && base[-1] != '/')
+ base--;
+ if (base == end)
+ return ("/");
+ return (base);
+}
+
+static char *
+fts_join_path(const char *parent, const char *name)
+{
+ size_t parent_len, name_len;
+ int need_sep;
+ char *path;
+
+ parent_len = strlen(parent);
+ name_len = strlen(name);
+ need_sep = parent_len > 0 && parent[parent_len - 1] != '/';
+ path = malloc(parent_len + need_sep + name_len + 1);
+ if (path == NULL)
+ return (NULL);
+ memcpy(path, parent, parent_len);
+ if (need_sep)
+ path[parent_len++] = '/';
+ memcpy(path + parent_len, name, name_len + 1);
+ return (path);
+}
+
+static int
+fts_should_follow(const FTS *ftsp, int is_root)
+{
+ if (ftsp->fts_options & FTS_LOGICAL)
+ return (1);
+ if (is_root && (ftsp->fts_options & FTS_COMFOLLOW))
+ return (1);
+ return (0);
+}
+
+static const FTSENT *
+fts_root(const FTSENT *ent)
+{
+ while (ent != NULL && ent->fts_parent != NULL)
+ ent = ent->fts_parent;
+ return (ent);
+}
+
+static int
+fts_is_cycle(const FTSENT *parent, const struct stat *st)
+{
+ const FTSENT *cur;
+
+ for (cur = parent; cur != NULL; cur = cur->fts_parent) {
+ if (cur->fts_statp != NULL &&
+ cur->fts_statp->st_dev == st->st_dev &&
+ cur->fts_statp->st_ino == st->st_ino)
+ return (1);
+ }
+ return (0);
+}
+
+static FTSENT *
+fts_make_entry(FTS *ftsp, FTSENT *parent, const char *path, int level, int is_root)
+{
+ FTSENT *ent;
+ struct stat st;
+ int follow;
+
+ ent = calloc(1, sizeof(*ent));
+ if (ent == NULL)
+ return (NULL);
+ ent->fts_path = strdup(path);
+ ent->fts_accpath = ent->fts_path;
+ ent->fts_name = strdup(fts_basename(path));
+ ent->fts_parent = parent;
+ ent->fts_level = level;
+ ent->fts_statp = malloc(sizeof(*ent->fts_statp));
+ if (ent->fts_path == NULL || ent->fts_name == NULL || ent->fts_statp == NULL) {
+ fts_free_entry(ent);
+ return (NULL);
+ }
+
+ follow = fts_should_follow(ftsp, is_root);
+ if ((follow ? stat(path, &st) : lstat(path, &st)) != 0) {
+ if (lstat(path, &st) == 0 && S_ISLNK(st.st_mode)) {
+ ent->fts_info = FTS_SL;
+ *ent->fts_statp = st;
+ } else {
+ ent->fts_info = FTS_NS;
+ ent->fts_errno = errno;
+ memset(ent->fts_statp, 0, sizeof(*ent->fts_statp));
+ }
+ return (ent);
+ }
+
+ *ent->fts_statp = st;
+ if (S_ISDIR(st.st_mode)) {
+ if (fts_is_cycle(parent, &st)) {
+ ent->fts_info = FTS_DC;
+ } else {
+ const FTSENT *root;
+
+ ent->fts_info = FTS_D;
+ root = fts_root(ent);
+ if ((ftsp->fts_options & FTS_XDEV) != 0 && root != NULL &&
+ root->fts_statp != NULL && root != ent &&
+ root->fts_statp->st_dev != st.st_dev)
+ ent->fts_instr = FTS_SKIP;
+ }
+ } else if (S_ISLNK(st.st_mode) && !follow) {
+ ent->fts_info = FTS_SL;
+ } else {
+ ent->fts_info = FTS_F;
+ }
+
+ return (ent);
+}
+
+static int
+fts_prepare_children(FTS *ftsp, FTSENT *parent)
+{
+ struct dirent *dp;
+ DIR *dirp;
+ char **names;
+ char *child_path;
+ FTSENT *child;
+ size_t cap, count, i;
+
+ dirp = opendir(parent->fts_accpath);
+ if (dirp == NULL) {
+ parent->fts_errno = errno;
+ parent->fts_info = FTS_DNR;
+ parent->_state = FTS_STATE_DNR;
+ return (-1);
+ }
+
+ names = NULL;
+ cap = count = 0;
+ while ((dp = readdir(dirp)) != NULL) {
+ char *name;
+
+ if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0)
+ continue;
+ if (count == cap) {
+ size_t new_cap;
+ char **new_names;
+
+ new_cap = cap == 0 ? 16 : cap * 2;
+ new_names = realloc(names, new_cap * sizeof(*names));
+ if (new_names == NULL)
+ goto fail;
+ names = new_names;
+ cap = new_cap;
+ }
+ name = strdup(dp->d_name);
+ if (name == NULL)
+ goto fail;
+ names[count++] = name;
+ }
+ closedir(dirp);
+
+ if (count == 0) {
+ free(names);
+ parent->_children = NULL;
+ parent->_child_count = 0;
+ parent->_child_index = 0;
+ return (0);
+ }
+
+ if (ftsp->fts_compar != NULL) {
+ FTSENT **cmp_entries;
+
+ cmp_entries = calloc(count, sizeof(*cmp_entries));
+ if (cmp_entries == NULL)
+ goto fail_names;
+ for (i = 0; i < count; i++) {
+ child_path = fts_join_path(parent->fts_path, names[i]);
+ if (child_path == NULL) {
+ while (i > 0)
+ fts_free_entry(cmp_entries[--i]);
+ free(cmp_entries);
+ goto fail_names;
+ }
+ cmp_entries[i] = fts_make_entry(ftsp, parent, child_path,
+ parent->fts_level + 1, 0);
+ free(child_path);
+ if (cmp_entries[i] == NULL) {
+ while (i > 0)
+ fts_free_entry(cmp_entries[--i]);
+ free(cmp_entries);
+ goto fail_names;
+ }
+ }
+ qsort(cmp_entries, count, sizeof(*cmp_entries),
+ (int (*)(const void *, const void *))ftsp->fts_compar);
+ parent->_children = cmp_entries;
+ parent->_child_count = count;
+ parent->_child_index = 0;
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ return (0);
+ }
+
+ parent->_children = calloc(count, sizeof(*parent->_children));
+ if (parent->_children == NULL)
+ goto fail_names;
+ for (i = 0; i < count; i++) {
+ child_path = fts_join_path(parent->fts_path, names[i]);
+ if (child_path == NULL)
+ goto fail_children;
+ child = fts_make_entry(ftsp, parent, child_path, parent->fts_level + 1, 0);
+ free(child_path);
+ if (child == NULL)
+ goto fail_children;
+ parent->_children[i] = child;
+ }
+ parent->_child_count = count;
+ parent->_child_index = 0;
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ return (0);
+
+fail_children:
+ while (i > 0)
+ fts_free_entry(parent->_children[--i]);
+ free(parent->_children);
+ parent->_children = NULL;
+fail_names:
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ parent->fts_errno = ENOMEM;
+ parent->fts_info = FTS_ERR;
+ parent->_state = FTS_STATE_DONE;
+ return (-1);
+fail:
+ closedir(dirp);
+ free(names);
+ parent->fts_errno = ENOMEM;
+ parent->fts_info = FTS_ERR;
+ parent->_state = FTS_STATE_DONE;
+ return (-1);
+}
+
+static FTSENT *
+fts_advance(FTS *ftsp, FTSENT *ent)
+{
+ FTSENT *parent;
+
+ for (;;) {
+ if (ent == NULL)
+ break;
+
+ if (ent->fts_info == FTS_D && ent->_state == FTS_STATE_PRE) {
+ if (ent->fts_instr == FTS_SKIP || ent->fts_info == FTS_DC) {
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+ if (fts_prepare_children(ftsp, ent) != 0) {
+ if (ent->fts_info == FTS_DNR || ent->fts_info == FTS_ERR)
+ return (ent);
+ }
+ ent->_state = FTS_STATE_CHILDREN;
+ if (ent->_child_count > 0)
+ return (ent->_children[ent->_child_index++]);
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ if (ent->_state == FTS_STATE_DNR) {
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ if (ent->_state == FTS_STATE_CHILDREN) {
+ if (ent->_child_index < ent->_child_count)
+ return (ent->_children[ent->_child_index++]);
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ ent->_state = FTS_STATE_DONE;
+ parent = ent->fts_parent;
+ if (parent != NULL) {
+ ent = parent;
+ continue;
+ }
+ if (++ftsp->_root_index < ftsp->_root_count)
+ return (ftsp->_roots[ftsp->_root_index]);
+ break;
+ }
+
+ return (NULL);
+}
+
+FTS *
+fts_open(char * const *paths, int options,
+ int (*compar)(const FTSENT * const *, const FTSENT * const *))
+{
+ FTS *ftsp;
+ size_t count, i;
+
+ count = 0;
+ while (paths[count] != NULL)
+ count++;
+
+ ftsp = calloc(1, sizeof(*ftsp));
+ if (ftsp == NULL)
+ return (NULL);
+ ftsp->_roots = calloc(count, sizeof(*ftsp->_roots));
+ if (ftsp->_roots == NULL) {
+ free(ftsp);
+ return (NULL);
+ }
+ ftsp->_root_count = count;
+ ftsp->fts_options = options;
+ ftsp->fts_compar = compar;
+
+ for (i = 0; i < count; i++) {
+ ftsp->_roots[i] = fts_make_entry(ftsp, NULL, paths[i], FTS_ROOTLEVEL, 1);
+ if (ftsp->_roots[i] == NULL) {
+ while (i > 0)
+ fts_free_entry(ftsp->_roots[--i]);
+ free(ftsp->_roots);
+ free(ftsp);
+ return (NULL);
+ }
+ }
+
+ return (ftsp);
+}
+
+FTSENT *
+fts_read(FTS *ftsp)
+{
+ if (ftsp == NULL || ftsp->_root_count == 0)
+ return (NULL);
+ if (ftsp->_current == NULL) {
+ ftsp->_root_index = 0;
+ ftsp->_current = ftsp->_roots[0];
+ return (ftsp->_current);
+ }
+ ftsp->_current = fts_advance(ftsp, ftsp->_current);
+ if (ftsp->_current == NULL)
+ errno = 0;
+ return (ftsp->_current);
+}
+
+int
+fts_set(FTS *ftsp, FTSENT *f, int instr)
+{
+ (void)ftsp;
+ if (f == NULL)
+ return (-1);
+ f->fts_instr = instr;
+ return (0);
+}
+
+static void
+fts_free_entry(FTSENT *ent)
+{
+ size_t i;
+
+ if (ent == NULL)
+ return;
+ for (i = 0; i < ent->_child_count; i++)
+ fts_free_entry(ent->_children[i]);
+ free(ent->_children);
+ free(ent->fts_statp);
+ free(ent->fts_name);
+ free(ent->fts_path);
+ free(ent);
+}
+
+int
+fts_close(FTS *ftsp)
+{
+ size_t i;
+
+ if (ftsp == NULL)
+ return (0);
+ for (i = 0; i < ftsp->_root_count; i++)
+ fts_free_entry(ftsp->_roots[i]);
+ free(ftsp->_roots);
+ free(ftsp);
+ return (0);
+}
diff --git a/corebinutils/chflags/fts.h b/corebinutils/chflags/fts.h
new file mode 100644
index 0000000000..74b15c1ad1
--- /dev/null
+++ b/corebinutils/chflags/fts.h
@@ -0,0 +1,90 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <sys/stat.h>
+
+#include <stddef.h>
+
+typedef struct _ftsent FTSENT;
+typedef struct _fts FTS;
+
+struct _ftsent {
+ FTSENT *fts_parent;
+ char *fts_path;
+ char *fts_accpath;
+ char *fts_name;
+ int fts_errno;
+ int fts_info;
+ int fts_instr;
+ int fts_level;
+ long fts_number;
+ struct stat *fts_statp;
+ FTSENT **_children;
+ size_t _child_count;
+ size_t _child_index;
+ int _state;
+};
+
+struct _fts {
+ FTSENT **_roots;
+ size_t _root_count;
+ size_t _root_index;
+ int fts_options;
+ int (*fts_compar)(const FTSENT * const *, const FTSENT * const *);
+ FTSENT *_current;
+};
+
+#define FTS_COMFOLLOW 0x0001
+#define FTS_LOGICAL 0x0002
+#define FTS_NOCHDIR 0x0004
+#define FTS_PHYSICAL 0x0008
+#define FTS_XDEV 0x0010
+
+#define FTS_ROOTLEVEL 0
+
+#define FTS_D 1
+#define FTS_DC 2
+#define FTS_DNR 3
+#define FTS_DP 4
+#define FTS_ERR 5
+#define FTS_F 6
+#define FTS_NS 7
+#define FTS_SL 8
+
+#define FTS_SKIP 1
+
+FTS *fts_open(char * const *paths, int options,
+ int (*compar)(const FTSENT * const *, const FTSENT * const *));
+FTSENT *fts_read(FTS *ftsp);
+int fts_set(FTS *ftsp, FTSENT *f, int instr);
+int fts_close(FTS *ftsp);
diff --git a/corebinutils/chflags/tests/test.sh b/corebinutils/chflags/tests/test.sh
new file mode 100644
index 0000000000..ea7db1d8ed
--- /dev/null
+++ b/corebinutils/chflags/tests/test.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+CHFLAGS_BIN=${CHFLAGS_BIN:-"$ROOT/out/chflags"}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/chflags-test.XXXXXX")
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+expect_usage() {
+ output=$("$CHFLAGS_BIN" "$@" 2>&1 || true)
+ case $output in
+ *"usage: chflags "*) ;;
+ *) fail "usage output missing for args: $*" ;;
+ esac
+}
+
+[ -x "$CHFLAGS_BIN" ] || fail "missing binary: $CHFLAGS_BIN"
+
+expect_usage
+expect_usage -f
+expect_usage -H
+expect_usage -h
+expect_usage -L
+expect_usage -P
+expect_usage -R
+expect_usage -v
+expect_usage -x
+
+invalid_output=$("$CHFLAGS_BIN" badflag "$WORKDIR/missing" 2>&1 || true)
+case $invalid_output in
+ *"invalid flag: badflag"*) ;;
+ *) fail "invalid flag output missing" ;;
+esac
+
+invalid_octal=$("$CHFLAGS_BIN" 88 "$WORKDIR/missing" 2>&1 || true)
+case $invalid_octal in
+ *"invalid flag: 88"*) ;;
+ *) fail "invalid octal output missing" ;;
+esac
+
+conflict_output=$("$CHFLAGS_BIN" -R -h nodump "$WORKDIR/missing" 2>&1 || true)
+case $conflict_output in
+ *"the -R and -h options may not be specified together."*) ;;
+ *) fail "missing -R/-h conflict output" ;;
+esac
+
+touch "$WORKDIR/file"
+if "$CHFLAGS_BIN" -v nodump "$WORKDIR/file" >/tmp/chflags.out.$$ 2>/tmp/chflags.err.$$; then
+ verbose_text=$(cat /tmp/chflags.out.$$)
+ case $verbose_text in
+ *"$WORKDIR/file"*) ;;
+ *) rm -f /tmp/chflags.out.$$ /tmp/chflags.err.$$ /tmp/chflags.out2.$$ /tmp/chflags.err2.$$; fail "verbose output missing file path" ;;
+ esac
+ "$CHFLAGS_BIN" -v dump "$WORKDIR/file" >/tmp/chflags.out2.$$ 2>/tmp/chflags.err2.$$ || true
+else
+ err_text=$(cat /tmp/chflags.err.$$)
+ case $err_text in
+ *"Operation not supported"*|*"Inappropriate ioctl for device"*|*"Operation not permitted"*) ;;
+ *) rm -f /tmp/chflags.out.$$ /tmp/chflags.err.$$ /tmp/chflags.out2.$$ /tmp/chflags.err2.$$; fail "unexpected flag application failure: $err_text" ;;
+ esac
+fi
+
+mkdir -p "$WORKDIR/tree/sub"
+touch "$WORKDIR/tree/sub/file"
+recursive_verbose=$("$CHFLAGS_BIN" -R -vv nodump "$WORKDIR/tree" 2>/tmp/chflags-rec.err.$$ || true)
+recursive_err=$(cat /tmp/chflags-rec.err.$$)
+if [ -n "$recursive_err" ]; then
+ case $recursive_err in
+ *"Operation not supported"*|*"Inappropriate ioctl for device"*|*"Operation not permitted"*) ;;
+ *) rm -f /tmp/chflags.out.$$ /tmp/chflags.err.$$ /tmp/chflags.out2.$$ /tmp/chflags.err2.$$ /tmp/chflags-rec.err.$$; fail "unexpected recursive failure: $recursive_err" ;;
+ esac
+else
+ case $recursive_verbose in
+ *"$WORKDIR/tree/sub/file"*"$WORKDIR/tree/sub"*"$WORKDIR/tree"*) ;;
+ *) rm -f /tmp/chflags.out.$$ /tmp/chflags.err.$$ /tmp/chflags.out2.$$ /tmp/chflags.err2.$$ /tmp/chflags-rec.err.$$; fail "recursive verbose output missing expected paths" ;;
+ esac
+
+ recursive_noop=$("$CHFLAGS_BIN" -R -vv nodump "$WORKDIR/tree" 2>/tmp/chflags-noop.err.$$ || true)
+ [ -z "$recursive_noop" ] || fail "expected recursive no-op to produce no stdout"
+ [ ! -s /tmp/chflags-noop.err.$$ ] || fail "expected recursive no-op to produce no stderr"
+
+ "$CHFLAGS_BIN" -R dump "$WORKDIR/tree" >/tmp/chflags-rec-clear.out.$$ 2>/tmp/chflags-rec-clear.err.$$ || true
+fi
+
+ln -s "$WORKDIR/file" "$WORKDIR/link"
+"$CHFLAGS_BIN" -fh nodump "$WORKDIR/link" >/tmp/chflags-link.out.$$ 2>/tmp/chflags-link.err.$$ || fail "-fh on symlink should not fail"
+[ ! -s /tmp/chflags-link.err.$$ ] || fail "-fh on symlink should suppress stderr"
+
+rm -f /tmp/chflags.out.$$ /tmp/chflags.err.$$ /tmp/chflags.out2.$$ /tmp/chflags.err2.$$ /tmp/chflags-rec.err.$$ /tmp/chflags-noop.err.$$ /tmp/chflags-rec-clear.out.$$ /tmp/chflags-rec-clear.err.$$ /tmp/chflags-link.out.$$ /tmp/chflags-link.err.$$
+printf '%s\n' "PASS"