summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/codeql.yml2
-rwxr-xr-x.github/diag.sh2
-rw-r--r--LICENSE2
-rw-r--r--Makefile19
-rwxr-xr-xbuild/cc.sh11
-rw-r--r--build/config.mk8
-rw-r--r--build/deps.mk2
-rwxr-xr-xbuild/embed.sh12
-rw-r--r--build/has/builtin-riscv-pause.c7
-rw-r--r--build/header.mk28
-rwxr-xr-xbuild/msg.sh2
-rwxr-xr-xbuild/pkgconf.sh2
-rw-r--r--build/prelude.mk3
-rwxr-xr-xconfigure152
-rw-r--r--docs/CHANGELOG.md44
-rw-r--r--docs/RELATED.md5
-rw-r--r--docs/SECURITY.md126
-rw-r--r--docs/bfs.114
-rw-r--r--src/atomic.h2
-rw-r--r--src/bar.c9
-rw-r--r--src/bfstd.c39
-rw-r--r--src/bfstd.h29
-rw-r--r--src/bftw.c43
-rw-r--r--src/bftw.h2
-rw-r--r--src/bit.h100
-rw-r--r--src/color.c12
-rw-r--r--src/ctx.c4
-rw-r--r--src/eval.c59
-rw-r--r--src/exec.c2
-rw-r--r--src/ioq.c22
-rw-r--r--src/list.h24
-rw-r--r--src/main.c1
-rw-r--r--src/opt.c117
-rw-r--r--src/parse.c255
-rw-r--r--src/prelude.h13
-rw-r--r--src/printf.c43
-rw-r--r--src/sighook.c55
-rw-r--r--src/stat.c2
-rw-r--r--src/trie.c2
-rw-r--r--src/typo.c2
-rw-r--r--src/version.c32
-rw-r--r--src/xregex.c49
-rw-r--r--src/xregex.h4
-rw-r--r--src/xspawn.c13
-rw-r--r--src/xtime.c18
-rw-r--r--tests/bfs/Dmulti.out19
-rw-r--r--tests/bfs/Dmulti.sh1
-rw-r--r--tests/bfs/LD_stat.out17
-rw-r--r--tests/bfs/LD_stat.sh1
-rw-r--r--tests/bfs/LDstat.out17
-rw-r--r--tests/bfs/LDstat.sh1
-rw-r--r--tests/bfs/O_3.sh1
-rw-r--r--tests/bfs/Sbfs.out19
-rw-r--r--tests/bfs/Sbfs.sh2
-rw-r--r--tests/bfs/color_ext_case.sh10
-rw-r--r--tests/bfs/color_ext_override.sh2
-rw-r--r--tests/bfs/nohidden.out8
-rw-r--r--tests/bfs/nohidden_depth.out8
-rw-r--r--tests/bfs/xtype_depth.sh2
-rw-r--r--tests/bfs/xtype_reorder.sh3
-rw-r--r--tests/bit.c36
-rw-r--r--tests/bsd/Hf.out1
-rw-r--r--tests/bsd/Hf.sh1
-rw-r--r--tests/bsd/X.out4
-rw-r--r--tests/bsd/perm_000_plus.out16
-rw-r--r--tests/bsd/perm_222_plus.out9
-rw-r--r--tests/bsd/perm_644_plus.out14
-rw-r--r--tests/bsd/printx.out8
-rw-r--r--tests/bsd/s.out4
-rw-r--r--tests/common/HLP.out1
-rw-r--r--tests/common/HLP.sh1
-rw-r--r--tests/common/amin.out6
-rw-r--r--tests/common/amin.sh15
-rw-r--r--tests/common/atime.out6
-rw-r--r--tests/common/atime.sh15
-rw-r--r--tests/common/empty_error.out4
-rw-r--r--tests/common/empty_error.sh8
-rw-r--r--tests/common/mmin.out6
-rw-r--r--tests/common/mmin.sh15
-rw-r--r--tests/common/mtime.out6
-rw-r--r--tests/common/mtime.sh15
-rw-r--r--tests/gnu/executable.out8
-rw-r--r--tests/gnu/files0_from_file.out12
-rw-r--r--tests/gnu/files0_from_stdin.out12
-rw-r--r--tests/gnu/follow_comma.out8
-rw-r--r--tests/gnu/ignore_readdir_race_loop.out11
-rw-r--r--tests/gnu/ignore_readdir_race_loop.sh2
-rw-r--r--tests/gnu/perm_000_slash.out16
-rw-r--r--tests/gnu/perm_222_slash.out9
-rw-r--r--tests/gnu/perm_644_slash.out14
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.out14
-rw-r--r--tests/gnu/perm_symbolic_slash.out14
-rw-r--r--tests/gnu/readable.out10
-rw-r--r--tests/gnu/regextype_awk.out2
-rw-r--r--tests/gnu/regextype_awk.sh3
-rw-r--r--tests/gnu/regextype_egrep.out0
-rw-r--r--tests/gnu/regextype_egrep.sh3
-rw-r--r--tests/gnu/regextype_emacs.sh2
-rw-r--r--tests/gnu/regextype_findutils_default.out3
-rw-r--r--tests/gnu/regextype_findutils_default.sh3
-rw-r--r--tests/gnu/regextype_gnu_awk.out2
-rw-r--r--tests/gnu/regextype_gnu_awk.sh3
-rw-r--r--tests/gnu/regextype_posix_awk.out2
-rw-r--r--tests/gnu/regextype_posix_awk.sh3
-rw-r--r--tests/gnu/regextype_posix_minimal_basic.out1
-rw-r--r--tests/gnu/regextype_posix_minimal_basic.sh2
-rw-r--r--tests/gnu/used.sh31
-rw-r--r--tests/gnu/writable.out9
-rw-r--r--tests/gnu/xtype_l_loops.out3
-rw-r--r--tests/gnu/xtype_l_loops.sh1
-rw-r--r--tests/list.c97
-rw-r--r--tests/main.c1
-rw-r--r--tests/posix/HL.out17
-rw-r--r--tests/posix/HL.sh1
-rw-r--r--tests/posix/LH.out1
-rw-r--r--tests/posix/LH.sh1
-rw-r--r--tests/posix/depth_error.out6
-rw-r--r--tests/posix/depth_error.sh8
-rw-r--r--tests/posix/perm_000.out2
-rw-r--r--tests/posix/perm_000_minus.out16
-rw-r--r--tests/posix/perm_222.out2
-rw-r--r--tests/posix/perm_222_minus.out2
-rw-r--r--tests/posix/perm_644.out2
-rw-r--r--tests/posix/perm_644_minus.out5
-rw-r--r--tests/posix/perm_symbolic_minus.out5
-rw-r--r--tests/posix/permcopy.out2
-rw-r--r--tests/posix/prune_error.out1
-rw-r--r--tests/posix/prune_error.sh1
-rw-r--r--tests/run.sh10
-rw-r--r--tests/stddirs.sh25
-rw-r--r--tests/tests.h3
-rw-r--r--tests/trie.c4
-rw-r--r--tests/xspawn.c2
133 files changed, 1545 insertions, 568 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml
index 6ff8337..b61c30e 100644
--- a/.github/codeql.yml
+++ b/.github/codeql.yml
@@ -2,6 +2,8 @@ query-filters:
- exclude:
id: cpp/commented-out-code
- exclude:
+ id: cpp/include-non-header
+ - exclude:
id: cpp/long-switch
- exclude:
id: cpp/loop-variable-changed
diff --git a/.github/diag.sh b/.github/diag.sh
index 942487a..fe78be8 100755
--- a/.github/diag.sh
+++ b/.github/diag.sh
@@ -13,4 +13,4 @@ filter() {
\1/'
}
-"$@" 2> >(filter >&2) | filter
+exec "$@" > >(filter) 2> >(filter >&2)
diff --git a/LICENSE b/LICENSE
index 240d2ac..9fd4f17 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright © 2015-2023 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
+Copyright © 2015-2024 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
Permission to use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted.
diff --git a/Makefile b/Makefile
index f33a604..d21cbb3 100644
--- a/Makefile
+++ b/Makefile
@@ -66,22 +66,20 @@ ${OBJS}: gen/config.mk
${MSG} "[ CC ] ${CSRC}" ${CC} ${_CPPFLAGS} ${_CFLAGS} -c ${CSRC} -o $@
# Save the version number to this file, but only update version.c if it changes
-gen/version.c.new::
+gen/version.i.new::
@${MKDIR} ${@D}
- @printf 'const char bfs_version[] = "' >$@
@if [ "$$VERSION" ]; then \
printf '%s' "$$VERSION"; \
elif test -e src/../.git && command -v git >/dev/null 2>&1; then \
git -C src/.. describe --always --dirty; \
else \
- echo "3.2"; \
- fi | tr -d '\n' >>$@
- @printf '";\n' >>$@
+ echo "3.3.1"; \
+ fi | tr -d '\n' | build/embed.sh >$@
-gen/version.c: gen/version.c.new
- @test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@
+gen/version.i: gen/version.i.new
+ @test -e $@ && cmp -s $@ ${.ALLSRC} && ${RM} ${.ALLSRC} || mv ${.ALLSRC} $@
-obj/gen/version.o: gen/version.c
+obj/src/version.o: gen/version.i
## Test phase (`make check`)
@@ -217,6 +215,11 @@ check-install::
bin/bfs pkg -not -type d -print -exit 1
${RM} -r pkg
+# Check man page markup
+check-man::
+ ${MSG} "[LINT] docs/bfs.1" \
+ groff -man -rCHECKSTYLE=3 -ww -b -z docs/bfs.1
+
## Cleanup (`make clean`)
# Clean all build products
diff --git a/build/cc.sh b/build/cc.sh
index 45d51ca..23a4c01 100755
--- a/build/cc.sh
+++ b/build/cc.sh
@@ -5,12 +5,5 @@
# Run the compiler and check if it succeeded
-set -eu
-
-TMP=$(mktemp)
-trap 'rm -f "$TMP"' EXIT
-
-(
- set -x
- $XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS -o "$TMP"
-)
+set -eux
+$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS
diff --git a/build/config.mk b/build/config.mk
index 24873ec..80206c7 100644
--- a/build/config.mk
+++ b/build/config.mk
@@ -7,7 +7,7 @@ include build/prelude.mk
include build/exports.mk
# All configuration steps
-config: gen/config.mk gen/config.h
+config: gen/config.mk
.PHONY: config
# Makefile fragments generated by `./configure`
@@ -18,10 +18,10 @@ MKS := \
gen/pkgs.mk
# The main configuration file, which includes the others
-gen/config.mk: ${MKS}
+gen/config.mk: ${MKS} gen/config.h
${MSG} "[ GEN] $@"
@printf '# %s\n' "$@" >$@
- @printf 'include %s\n' ${.ALLSRC} >>$@
+ @printf 'include %s\n' ${MKS} >>$@
${VCAT} gen/config.mk
.PHONY: gen/config.mk
@@ -57,6 +57,6 @@ gen/pkgs.mk: gen/flags.mk
.PHONY: gen/pkgs.mk
# Compile-time feature detection
-gen/config.h: gen/config.mk
+gen/config.h: gen/pkgs.mk
@+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@
.PHONY: gen/config.h
diff --git a/build/deps.mk b/build/deps.mk
index d382f5d..a6ea673 100644
--- a/build/deps.mk
+++ b/build/deps.mk
@@ -11,7 +11,7 @@ include build/exports.mk
gen/deps.mk::
${MSG} "[ GEN] $@"
@printf '# %s\n' "$@" >$@
- @if build/cc.sh -MD -MP -MF /dev/null build/empty.c; then \
+ @if build/cc.sh -MD -MP build/empty.c -o gen/.deps.out; then \
printf '_CPPFLAGS += -MD -MP\n'; \
fi >>$@ 2>$@.log
${VCAT} $@
diff --git a/build/embed.sh b/build/embed.sh
new file mode 100755
index 0000000..8d7d0f1
--- /dev/null
+++ b/build/embed.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Convert data into a C array like #embed
+
+set -eu
+
+{ cat; printf '\0'; } \
+ | od -An -tx1 \
+ | sed 's/\([^ ][^ ]*\)/0x\1,/g'
diff --git a/build/has/builtin-riscv-pause.c b/build/has/builtin-riscv-pause.c
new file mode 100644
index 0000000..24b0675
--- /dev/null
+++ b/build/has/builtin-riscv-pause.c
@@ -0,0 +1,7 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+int main(void) {
+ __builtin_riscv_pause();
+ return 0;
+}
diff --git a/build/header.mk b/build/header.mk
index 09454c5..afd04d0 100644
--- a/build/header.mk
+++ b/build/header.mk
@@ -4,7 +4,10 @@
# Makefile that generates gen/config.h
include build/prelude.mk
-include gen/config.mk
+include gen/vars.mk
+include gen/flags.mk
+include gen/deps.mk
+include gen/pkgs.mk
include build/exports.mk
# All header fragments we generate
@@ -16,6 +19,7 @@ HEADERS := \
gen/has/acl-is-trivial-np.h \
gen/has/acl-trivial.h \
gen/has/aligned-alloc.h \
+ gen/has/builtin-riscv-pause.h \
gen/has/confstr.h \
gen/has/extattr-get-file.h \
gen/has/extattr-get-link.h \
@@ -60,14 +64,30 @@ gen/config.h: ${PKG_HEADERS} ${HEADERS}
@printf '#define BFS_CONFIG_H\n' >>$@
@cat ${.ALLSRC} >>$@
@printf '#endif // BFS_CONFIG_H\n' >>$@
- @cat ${.ALLSRC:%=%.log} >gen/config.log
+ @cat gen/cc.log ${.ALLSRC:%=%.log} >gen/config.log
${VCAT} $@
+ @printf '%s' "$$CONFFLAGS" | build/embed.sh >gen/confflags.i
+ @printf '%s' "$$XCC" | build/embed.sh >gen/cc.i
+ @printf '%s' "$$XCPPFLAGS" | build/embed.sh >gen/cppflags.i
+ @printf '%s' "$$XCFLAGS" | build/embed.sh >gen/cflags.i
+ @printf '%s' "$$XLDFLAGS" | build/embed.sh >gen/ldflags.i
+ @printf '%s' "$$XLDLIBS" | build/embed.sh >gen/ldlibs.i
.PHONY: gen/config.h
# The short name of the config test
SLUG = ${@:gen/%.h=%}
+# The hidden output file name
+OUT = ${SLUG:has/%=gen/has/.%.out}
-${HEADERS}::
+${HEADERS}: cc
@${MKDIR} ${@D}
- @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c >$@ 2>$@.log; \
+ @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \
build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
+.PHONY: ${HEADERS}
+
+# Check that the C compiler works at all
+cc::
+ @build/cc.sh build/empty.c -o gen/.cc.out 2>gen/cc.log; \
+ ret=$$?; \
+ build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \
+ exit $$ret
diff --git a/build/msg.sh b/build/msg.sh
index a7da31b..2249125 100755
--- a/build/msg.sh
+++ b/build/msg.sh
@@ -59,4 +59,4 @@ if is_loud; then
printf '%s\n' "$*"
fi
-"$@"
+exec "$@"
diff --git a/build/pkgconf.sh b/build/pkgconf.sh
index 244c95d..a8a3341 100755
--- a/build/pkgconf.sh
+++ b/build/pkgconf.sh
@@ -37,7 +37,7 @@ if [ -z "$MODE" ]; then
CFLAGS=$("$0" --cflags "$LIB") || exit 1
LDFLAGS=$("$0" --ldflags "$LIB") || exit 1
LDLIBS=$("$0" --ldlibs "$LIB") || exit 1
- build/cc.sh $CFLAGS $LDFLAGS build/with/$LIB.c $LDLIBS || exit 1
+ build/cc.sh $CFLAGS $LDFLAGS "build/with/$LIB.c" $LDLIBS -o "gen/with/.$LIB.out" || exit 1
done
fi
diff --git a/build/prelude.mk b/build/prelude.mk
index d0968ea..76fbce8 100644
--- a/build/prelude.mk
+++ b/build/prelude.mk
@@ -97,10 +97,10 @@ LIBBFS := \
obj/src/thread.o \
obj/src/trie.o \
obj/src/typo.o \
+ obj/src/version.o \
obj/src/xregex.o \
obj/src/xspawn.o \
obj/src/xtime.o \
- obj/gen/version.o
# Unit test objects
UNIT_OBJS := \
@@ -108,6 +108,7 @@ UNIT_OBJS := \
obj/tests/bfstd.o \
obj/tests/bit.o \
obj/tests/ioq.o \
+ obj/tests/list.o \
obj/tests/main.o \
obj/tests/sighook.o \
obj/tests/trie.o \
diff --git a/configure b/configure
index 60d7a92..8fb0cc2 100755
--- a/configure
+++ b/configure
@@ -7,19 +7,62 @@
set -eu
-# Default to `make`
-MAKE="${MAKE:-make}"
-
-# Pass -j$(nproc) unless MAKEFLAGS is set
-if [ "${MAKEFLAGS+y}" ]; then
- j=""
-else
- j="-j$({ nproc || sysctl -n hw.ncpu || getconf _NPROCESSORS_ONLN || echo 1; } 2>/dev/null)"
-fi
-
-# Convert kebab-case to UPPER_CASE
-toupper() {
- printf '%s' "$1" | tr 'a-z-' 'A-Z_'
+# Print the help message
+help() {
+ cat <<EOF
+Usage:
+
+ \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
+ \$ $MAKE $j
+
+Variables set in the environment or on the command line will be picked up:
+
+ MAKE
+ The make implementation to use
+ CC
+ The C compiler to use
+
+ CPPFLAGS="-I... -D..."
+ CFLAGS="-W... -f..."
+ LDFLAGS="-L... -Wl,..."
+ Preprocessor/compiler/linker flags
+
+ LDLIBS="-l... -l..."
+ Dynamic libraries to link
+
+ EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS}
+ Adds to the default flags, instead of replacing them
+
+The default flags result in a plain debug build. Other build profiles include:
+
+ --enable-release
+ Enable optimizations, disable assertions
+ --enable-{asan,lsan,msan,tsan,ubsan}
+ Enable sanitizers
+ --enable-gcov
+ Enable code coverage instrumentation
+
+External dependencies are auto-detected by default, but you can build --with or
+--without them explicitly:
+
+ --with-libacl --without-libacl
+ --with-libcap --without-libcap
+ --with-libselinux --without-libselinux
+ --with-liburing --without-liburing
+ --with-oniguruma --without-oniguruma
+
+Packaging:
+
+ --prefix=/path
+ Set the installation prefix (default: /usr)
+ --mandir=/path
+ Set the man page directory (default: \$PREFIX/share/man)
+
+This script is a thin wrapper around a makefile-based configuration system.
+Any other arguments will be passed directly to the $MAKE invocation, e.g.
+
+ \$ $0 $j V=1
+EOF
}
# Report an argument parsing error
@@ -29,7 +72,26 @@ invalid() {
exit 1
}
+# Get the number of cores to use
+nproc() {
+ {
+ command nproc \
+ || sysctl -n hw.ncpu \
+ || getconf _NPROCESSORS_ONLN \
+ || echo 1
+ } 2>/dev/null
+}
+
+# Save the ./configure command line for bfs --version
+export CONFFLAGS="$*"
+
+# Default to `make`
+MAKE="${MAKE-make}"
+
+# Parse the command-line arguments
for arg; do
+ shift
+
# --[(enable|disable|with|without)-]$name[=$value]
value="${arg#*=}"
name="${arg%%=*}"
@@ -77,67 +139,13 @@ for arg; do
case "$arg" in
-h|--help)
- cat <<EOF
-Usage:
-
- \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
- \$ $MAKE $j
-
-Variables set in the environment or on the command line will be picked up:
-
- MAKE
- The make implementation to use
- CC
- The C compiler to use
-
- CPPFLAGS="-I... -D..."
- CFLAGS="-W... -f..."
- LDFLAGS="-L... -Wl,..."
- Preprocessor/compiler/linker flags
-
- LDLIBS="-l... -l..."
- Dynamic libraries to link
-
- EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS}
- Adds to the default flags, instead of replacing them
-
-The default flags result in a plain debug build. Other build profiles include:
-
- --enable-release
- Enable optimizations, disable assertions
- --enable-{asan,lsan,msan,tsan,ubsan}
- Enable sanitizers
- --enable-gcov
- Enable code coverage instrumentation
-
-External dependencies are auto-detected by default, but you can build --with or
---without them explicitly:
-
- --with-libacl --without-libacl
- --with-libcap --without-libcap
- --with-libselinux --without-libselinux
- --with-liburing --without-liburing
- --with-oniguruma --without-oniguruma
-
-Packaging:
-
- --prefix=/path
- Set the installation prefix (default: /usr)
- --mandir=/path
- Set the man page directory (default: \$PREFIX/share/man)
-
-This script is a thin wrapper around a makefile-based configuration system.
-Any other arguments will be passed directly to the $MAKE invocation, e.g.
-
- \$ $0 $j V=1
-EOF
+ help
exit 0
;;
--enable-*|--disable-*)
case "$name" in
release|asan|lsan|msan|tsan|ubsan|lint|gcov)
- shift
set -- "$@" "$NAME=$yn"
;;
*)
@@ -149,7 +157,6 @@ EOF
--with-*|--without-*)
case "$name" in
libacl|libcap|libselinux|liburing|oniguruma)
- shift
set -- "$@" "WITH_$NAME=$yn"
;;
*)
@@ -159,23 +166,20 @@ EOF
;;
--prefix=*|--mandir=*)
- shift
set -- "$@" "$NAME=$value"
;;
--infodir=*|--build=*|--host=*|--target=*)
- shift
printf 'warning: Ignoring option "%s"\n' "$arg" >&2
;;
MAKE=*)
- shift
MAKE="$value"
;;
# make flag (-j2) or variable (CC=clang)
-*|*=*)
- continue
+ set -- "$@" "$arg"
;;
*)
@@ -193,6 +197,6 @@ for f in Makefile build completions docs src tests; do
done
# Set MAKEFLAGS to -j$(nproc) if it's unset
-export MAKEFLAGS="${MAKEFLAGS-$j}"
+export MAKEFLAGS="${MAKEFLAGS--j$(nproc)}"
$MAKE -rf build/config.mk "$@"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 62b6480..db978b8 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,50 @@
3.*
===
+3.3.1
+-----
+
+**June 3, 2024**
+
+### Bug fixes
+
+- Reduced the scope of the symbolic link loop change in version 3.3.
+ `-xtype l` remains true for symbolic link loops, matching a change in GNU findutils 4.10.0.
+ However, `-L` will report an error, just like `bfs` prior to 3.3 and other `find` implementations, as required by POSIX.
+
+
+3.3
+---
+
+**May 28, 2024**
+
+### New features
+
+- The `-status` bar can now be toggled by `SIGINFO` (<kbd>Ctrl</kbd>+<kbd>T</kbd>) on systems that support it, and `SIGUSR1` on other systems
+
+- `-regextype` now supports all regex types from GNU find ([#21](https://github.com/tavianator/bfs/issues/21))
+
+- File birth times are now supported on OpenBSD
+
+### Changes
+
+- Symbolic link loops are now treated like other broken links, rather than an error
+
+- `./configure` now expects `--with-libacl`, `--without-libcap`, etc. rather than `--enable-`/`--disable-`
+
+- The ` ` (space) flag is now restricted to numeric `-printf` specifiers
+
+### Bug fixes
+
+- `-regextype emacs` now supports [shy](https://www.gnu.org/software/emacs/manual/html_node/elisp/Regexp-Backslash.html#index-shy-groups) (non-capturing) groups
+
+- Fixed `-status` bar visual corruption when the terminal is resized
+
+- `bfs` now prints a reset escape sequence when terminated by a signal in the middle of colored output ([#138](https://github.com/tavianator/bfs/issues/138))
+
+- `./configure CFLAGS=...` no longer overrides flags from `pkg-config` during configuration
+
+
3.2
---
diff --git a/docs/RELATED.md b/docs/RELATED.md
index cf52b70..6e7bd38 100644
--- a/docs/RELATED.md
+++ b/docs/RELATED.md
@@ -25,8 +25,9 @@ These are not usually installed as the system `find`, but are designed to be `fi
- [`bfs`](https://tavianator.com/projects/bfs.html) ([manual](https://man.archlinux.org/man/bfs.1), [source](https://github.com/tavianator/bfs))
- [schilytools](https://codeberg.org/schilytools/schilytools) `sfind` ([source](https://codeberg.org/schilytools/schilytools/src/branch/master/sfind))
- [BusyBox](https://busybox.net/) `find` ([manual](https://busybox.net/downloads/BusyBox.html#find), [source](https://git.busybox.net/busybox/tree/findutils/find.c))
-- [ToyBox](http://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c))
-- uutils `find` ([source](https://github.com/uutils/findutils))
+- [ToyBox](https://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c))
+- [Heirloom Project](https://heirloom.sourceforge.net/) `find` ([manual](https://heirloom.sourceforge.net/man/find.1.html), [source](https://github.com/eunuchs/heirloom-project/blob/master/heirloom/heirloom/find/find.c))
+- [uutils](https://uutils.github.io/) `find` ([source](https://github.com/uutils/findutils))
## `find` alternatives
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..f26efc5
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,126 @@
+Security
+========
+
+Threat model
+------------
+
+`bfs` is a command line program running on multi-user operating systems.
+Those other users may be malicious, but `bfs` should not allow them to do anything they couldn't already do.
+That includes situations where one user (especially `root`) is running `bfs` on files owned or controlled by another user.
+
+On the other hand, `bfs` implicitly trusts the user running it.
+Anyone with enough control over the command line of `bfs` or any `find`-compatible tool can wreak havoc with dangerous actions like `-exec`, `-delete`, etc.
+
+> [!CAUTION]
+> The only untrusted input that should *ever* be passed on the `bfs` command line are **file paths**.
+> It is *always* unsafe to allow *any* other part of the command line to be affected by untrusted input.
+> Use the `-f` flag, or `-files0-from`, to ensure that the input is interpreted as a path.
+
+This still has security implications, incuding:
+
+- **Information disclosure:** an attacker may learn whether particular files exist by observing `bfs`'s output, exit status, or even side channels like execution time.
+- **Denial of service:** large directory trees or slow/network storage may cause `bfs` to consume excessive system resources.
+
+> [!TIP]
+> When in doubt, do not pass any untrusted input to `bfs`.
+
+
+Executing commands
+------------------
+
+The `-exec` family of actions execute commands, passing the matched paths as arguments.
+File names that begin with a dash may be misinterpreted as options, so `bfs` adds a leading `./` in some instances:
+
+```console
+user@host$ bfs -execdir echo {} \;
+./-rf
+```
+
+This might save you from accidentally running `rm -rf` (for example) when you didn't mean to.
+This mitigation applies to `-execdir`, but not `-exec`, because the full path typically does not begin with a dash.
+But it is possible, so be careful:
+
+```console
+user@host$ bfs -f -rf -exec echo {} \;
+-rf
+```
+
+
+Race conditions
+---------------
+
+Like many programs that interface with the file system, `bfs` can be affected by race conditions&mdash;in particular, "[time-of-check to time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)" (TOCTTOU) issues.
+For example,
+
+```console
+user@host$ bfs / -user user -exec dangerous_command {} \;
+```
+
+is not guaranteed to only run `dangerous_command` on files you own, because another user may run
+
+```console
+evil@host$ mv /path/to/file /path/to/exile
+evil@host$ mv ~/malicious /path/to/file
+```
+
+in between checking `-user user` and executing the command.
+
+> [!WARNING]
+> Be careful when running `bfs` on directories that other users have write access to, because they can modify the directory tree while `bfs` is running, leading to unpredictable results and possible TOCTTOU issues.
+
+
+Output sanitization
+-------------------
+
+In general, printing arbitrary data to a terminal may have [security](https://hdm.io/writing/termulation.txt) [implications](https://dgl.cx/2023/09/ansi-terminal-security#vulnerabilities-using-known-replies).
+On many platforms, file paths may be completely arbitrary data (except for NUL (`\0`) bytes).
+Therefore, when `bfs` is writing output to a terminal, it will escape non-printable characters:
+
+<pre>
+user@host$ touch $'\e[1mBOLD\e[0m'
+user@host$ bfs
+.
+./$'\e[1mBOLD\e[0m'
+</pre>
+
+However, this is fragile as it only applies when outputting directly to a terminal:
+
+<pre>
+user@host$ bfs | grep BOLD
+<strong>BOLD</strong>
+</pre>
+
+
+Code quality
+------------
+
+Every correctness issue in `bfs` is a potential security issue, because acting on the wrong path may do arbitrarily bad things.
+For example:
+
+```console
+root@host# bfs /etc -name passwd -exec cat {} \;
+```
+
+should print `/etc/passwd` but not `/etc/shadow`.
+`bfs` tries to ensure correct behavior through careful programming practice, an extensive testsuite, and static analysis.
+
+`bfs` is written in C, which is a memory unsafe language.
+Bugs that lead to memory corruption are likely to be exploitable due to the nature of C.
+We use [sanitizers](https://github.com/google/sanitizers) to try to detect these bugs.
+Fuzzing has also been applied in the past, and deploying continuous fuzzing is a work in progress.
+
+
+Supported versions
+------------------
+
+`bfs` comes with [no warranty](/LICENSE), and is maintained by [me](https://tavianator.com/) and [other volunteers](https://github.com/tavianator/bfs/graphs/contributors) in our spare time.
+In that sense, there are no *supported* versions.
+However, as long as I maintain `bfs` I will attempt to address any security issues swiftly.
+In general, security fixes will we part of the latest release, though for significant issues I may backport fixes to older release series.
+
+
+Reporting a vulnerability
+-------------------------
+
+If you think you have found a sensitive security issue in `bfs`, you can [report it privately](https://github.com/tavianator/bfs/security/advisories/new).
+Or you can [report it publicly](https://github.com/tavianator/bfs/issues/new); I won't judge you.
diff --git a/docs/bfs.1 b/docs/bfs.1
index 54166ab..cc9504e 100644
--- a/docs/bfs.1
+++ b/docs/bfs.1
@@ -1,4 +1,4 @@
-.TH BFS 1
+.TH BFS 1 2024-06-17 "bfs 3.3.1"
.SH NAME
bfs \- breadth-first search for your files
.SH SYNOPSIS
@@ -45,7 +45,7 @@ For example,
.RE
.fi
.PP
-will print the all the paths that are either .txt files or symbolic links to .txt files.
+will print all the paths that are either .txt files or symbolic links to .txt files.
.B \-and
is implied between two consecutive expressions, so this is equivalent:
.PP
@@ -71,7 +71,7 @@ will also accept
.I \-N
or
.IR +N .
-.IR \-N
+.I \-N
means "less than
.IR N ,"
and
@@ -479,6 +479,10 @@ Find files the current user can execute/read/write.
Always false/true.
.RE
.TP
+\fB\-flags\fR [\fI\-+\fR]\fIFLAGS\fR
+Find files with matching inode
+.BR FLAGS .
+.TP
\fB\-fstype \fITYPE\fR
Find files on file systems with the given
.IR TYPE .
@@ -582,7 +586,7 @@ Find files whose entire path matches the
.IR GLOB .
.RE
.TP
-\fB\-perm\fR [\fI\-\fR]\fIMODE\fR
+\fB\-perm\fR [\fI\-+/\fR]\fIMODE\fR
Find files with a matching mode.
.TP
\fB\-regex \fIREGEX\fR
@@ -913,7 +917,7 @@ Finds broken symbolic links.
.TP
.B bfs \-name config \-exclude \-name .git
Finds all files named
-.BR config,
+.BR config ,
skipping every
.B .git
directory.
diff --git a/src/atomic.h b/src/atomic.h
index 360de20..ad5303b 100644
--- a/src/atomic.h
+++ b/src/atomic.h
@@ -109,7 +109,7 @@
# define spin_loop() __builtin_ia32_pause()
#elif __has_builtin(__builtin_arm_yield)
# define spin_loop() __builtin_arm_yield()
-#elif __has_builtin(__builtin_riscv_pause)
+#elif BFS_HAS_BUILTIN_RISCV_PAUSE
# define spin_loop() __builtin_riscv_pause()
#else
# define spin_loop() ((void)0)
diff --git a/src/bar.c b/src/bar.c
index b928373..be77694 100644
--- a/src/bar.c
+++ b/src/bar.c
@@ -144,14 +144,7 @@ struct bfs_bar *bfs_bar_show(void) {
return NULL;
}
- char term[L_ctermid];
- ctermid(term);
- if (strlen(term) == 0) {
- errno = ENOTTY;
- goto fail;
- }
-
- bar->fd = open(term, O_RDWR | O_CLOEXEC);
+ bar->fd = open_cterm(O_RDWR | O_CLOEXEC);
if (bar->fd < 0) {
goto fail;
}
diff --git a/src/bfstd.c b/src/bfstd.c
index ce2218c..6d244ca 100644
--- a/src/bfstd.c
+++ b/src/bfstd.c
@@ -184,6 +184,16 @@ char *xgetdelim(FILE *file, char delim) {
}
}
+int open_cterm(int flags) {
+ char path[L_ctermid];
+ if (ctermid(path) == NULL || strlen(path) == 0) {
+ errno = ENOTTY;
+ return -1;
+ }
+
+ return open(path, flags);
+}
+
const char *xgetprogname(void) {
const char *cmd = NULL;
#if BFS_HAS_GETPROGNAME
@@ -199,6 +209,35 @@ const char *xgetprogname(void) {
return cmd;
}
+int xstrtoll(const char *str, char **end, int base, long long *value) {
+ // strtoll() skips leading spaces, but we want to reject them
+ if (xisspace(str[0])) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ // If end is NULL, make sure the entire string is valid
+ bool entire = !end;
+ char *endp;
+ if (!end) {
+ end = &endp;
+ }
+
+ errno = 0;
+ long long result = strtoll(str, end, base);
+ if (errno != 0) {
+ return -1;
+ }
+
+ if (*end == str || (entire && **end != '\0')) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ *value = result;
+ return 0;
+}
+
/** Compile and execute a regular expression for xrpmatch(). */
static int xrpregex(nl_item item, const char *response) {
const char *pattern = nl_langinfo(item);
diff --git a/src/bfstd.h b/src/bfstd.h
index 8055c55..afb5282 100644
--- a/src/bfstd.h
+++ b/src/bfstd.h
@@ -158,6 +158,16 @@ FILE *xfopen(const char *path, int flags);
*/
char *xgetdelim(FILE *file, char delim);
+/**
+ * Open the controlling terminal.
+ *
+ * @param flags
+ * The open() flags.
+ * @return
+ * An open file descriptor, or -1 on failure.
+ */
+int open_cterm(int flags);
+
// #include <stdlib.h>
/**
@@ -169,6 +179,23 @@ char *xgetdelim(FILE *file, char delim);
const char *xgetprogname(void);
/**
+ * Wrapper for strtoll() that forbids leading spaces.
+ *
+ * @param str
+ * The string to parse.
+ * @param end
+ * If non-NULL, will hold a pointer to the first invalid character.
+ * If NULL, the entire string must be valid.
+ * @param base
+ * The base for the conversion, or 0 to auto-detect.
+ * @param value
+ * Will hold the parsed integer value, on success.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xstrtoll(const char *str, char **end, int base, long long *value);
+
+/**
* Process a yes/no prompt.
*
* @return 1 for yes, 0 for no, and -1 for unknown.
@@ -349,7 +376,7 @@ size_t xread(int fd, void *buf, size_t nbytes);
* writes.
*
* @return
- The number of bytes written. A value != nbytes indicates an error.
+ * The number of bytes written. A value != nbytes indicates an error.
*/
size_t xwrite(int fd, const void *buf, size_t nbytes);
diff --git a/src/bftw.c b/src/bftw.c
index c80ac74..f9ef2a1 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -1529,11 +1529,28 @@ static bool bftw_pop_file(struct bftw_state *state) {
return bftw_pop(state, &state->fileq);
}
+/** Add a path component to the path. */
+static void bftw_prepend_path(char *path, size_t nameoff, size_t namelen, const char *name) {
+ if (nameoff > 0) {
+ path[nameoff - 1] = '/';
+ }
+ memcpy(path + nameoff, name, namelen);
+}
+
/** Build the path to the current file. */
static int bftw_build_path(struct bftw_state *state, const char *name) {
const struct bftw_file *file = state->file;
- size_t pathlen = file ? file->nameoff + file->namelen : 0;
+ size_t nameoff, namelen;
+ if (name) {
+ nameoff = file ? bftw_child_nameoff(file) : 0;
+ namelen = strlen(name);
+ } else {
+ nameoff = file->nameoff;
+ namelen = file->namelen;
+ }
+
+ size_t pathlen = nameoff + namelen;
if (dstresize(&state->path, pathlen) != 0) {
state->error = errno;
return -1;
@@ -1546,11 +1563,11 @@ static int bftw_build_path(struct bftw_state *state, const char *name) {
}
// Build the path backwards
+ if (name) {
+ bftw_prepend_path(state->path, nameoff, namelen, name);
+ }
while (file && file != ancestor) {
- if (file->nameoff > 0) {
- state->path[file->nameoff - 1] = '/';
- }
- memcpy(state->path + file->nameoff, file->name, file->namelen);
+ bftw_prepend_path(state->path, file->nameoff, file->namelen, file->name);
if (ancestor && ancestor->depth == file->depth) {
ancestor = ancestor->parent;
@@ -1559,20 +1576,6 @@ static int bftw_build_path(struct bftw_state *state, const char *name) {
}
state->previous = state->file;
-
- if (name) {
- if (pathlen > 0 && state->path[pathlen - 1] != '/') {
- if (dstrapp(&state->path, '/') != 0) {
- state->error = errno;
- return -1;
- }
- }
- if (dstrcat(&state->path, name) != 0) {
- state->error = errno;
- return -1;
- }
- }
-
return 0;
}
@@ -1676,6 +1679,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
ftwbuf->visit = visit;
ftwbuf->type = BFS_UNKNOWN;
ftwbuf->error = state->direrror;
+ ftwbuf->loopoff = 0;
ftwbuf->at_fd = AT_FDCWD;
ftwbuf->at_path = ftwbuf->path;
bftw_stat_init(&ftwbuf->stat_bufs, &state->stat_buf, &state->lstat_buf);
@@ -1733,6 +1737,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) {
ftwbuf->type = BFS_ERROR;
ftwbuf->error = ELOOP;
+ ftwbuf->loopoff = ancestor->nameoff + ancestor->namelen;
return;
}
}
diff --git a/src/bftw.h b/src/bftw.h
index 8656ca7..a2a201c 100644
--- a/src/bftw.h
+++ b/src/bftw.h
@@ -56,6 +56,8 @@ struct BFTW {
enum bfs_type type;
/** The errno that occurred, if type == BFS_ERROR. */
int error;
+ /** For filesystem loops, the length of the loop prefix. */
+ size_t loopoff;
/** A parent file descriptor for the *at() family of calls. */
int at_fd;
diff --git a/src/bit.h b/src/bit.h
index 17cfbcf..c494130 100644
--- a/src/bit.h
+++ b/src/bit.h
@@ -12,7 +12,7 @@
#include <limits.h>
#include <stdint.h>
-#if __STDC_VERSION__ >= C23
+#if BFS_HAS_STDBIT_H
# include <stdbit.h>
#endif
@@ -173,11 +173,7 @@
# define ENDIAN_NATIVE 0
#endif
-#if __STDC_VERSION__ >= C23
-# define bswap_u16 stdc_memreverse8u16
-# define bswap_u32 stdc_memreverse8u32
-# define bswap_u64 stdc_memreverse8u64
-#elif __GNUC__
+#if __GNUC__
# define bswap_u16 __builtin_bswap16
# define bswap_u32 __builtin_bswap32
# define bswap_u64 __builtin_bswap64
@@ -222,25 +218,17 @@ static inline uint8_t bswap_u8(uint8_t n) {
// Select an overload based on an unsigned integer type
#define UINT_SELECT(n, name) \
_Generic((n), \
- char: name##_uc, \
- signed char: name##_uc, \
unsigned char: name##_uc, \
- signed short: name##_us, \
unsigned short: name##_us, \
- signed int: name##_ui, \
unsigned int: name##_ui, \
- signed long: name##_ul, \
unsigned long: name##_ul, \
- signed long long: name##_ull, \
unsigned long long: name##_ull)
// C23 polyfill: bit utilities
-#if __STDC_VERSION__ >= C23
+#if __STDC_VERSION_STDBIT_H__ >= C23
# define count_ones stdc_count_ones
# define count_zeros stdc_count_zeros
-# define rotate_left stdc_rotate_left
-# define rotate_right stdc_rotate_right
# define leading_zeros stdc_leading_zeros
# define leading_ones stdc_leading_ones
# define trailing_zeros stdc_trailing_zeros
@@ -273,31 +261,31 @@ static inline uint8_t bswap_u8(uint8_t n) {
#define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix
#define COUNT_ONES(type, suffix, width) \
- static inline int count_ones##suffix(type n) { \
+ static inline unsigned int count_ones##suffix(type n) { \
return UINT_BUILTIN(popcount, suffix)(n); \
}
#define LEADING_ZEROS(type, suffix, width) \
- static inline int leading_zeros##suffix(type n) { \
+ static inline unsigned int leading_zeros##suffix(type n) { \
return n \
? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \
: width; \
}
#define TRAILING_ZEROS(type, suffix, width) \
- static inline int trailing_zeros##suffix(type n) { \
+ static inline unsigned int trailing_zeros##suffix(type n) { \
return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \
}
#define FIRST_TRAILING_ONE(type, suffix, width) \
- static inline int first_trailing_one##suffix(type n) { \
+ static inline unsigned int first_trailing_one##suffix(type n) { \
return UINT_BUILTIN(ffs, suffix)(n); \
}
#else // !__GNUC__
#define COUNT_ONES(type, suffix, width) \
- static inline int count_ones##suffix(type n) { \
+ static inline unsigned int count_ones##suffix(type n) { \
int ret; \
for (ret = 0; n; ++ret) { \
n &= n - 1; \
@@ -306,7 +294,7 @@ static inline uint8_t bswap_u8(uint8_t n) {
}
#define LEADING_ZEROS(type, suffix, width) \
- static inline int leading_zeros##suffix(type n) { \
+ static inline unsigned int leading_zeros##suffix(type n) { \
type bit = (type)1 << (width - 1); \
int ret; \
for (ret = 0; bit && !(n & bit); ++ret, bit >>= 1); \
@@ -314,7 +302,7 @@ static inline uint8_t bswap_u8(uint8_t n) {
}
#define TRAILING_ZEROS(type, suffix, width) \
- static inline int trailing_zeros##suffix(type n) { \
+ static inline unsigned int trailing_zeros##suffix(type n) { \
type bit = 1; \
int ret; \
for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \
@@ -322,7 +310,7 @@ static inline uint8_t bswap_u8(uint8_t n) {
}
#define FIRST_TRAILING_ONE(type, suffix, width) \
- static inline int first_trailing_one##suffix(type n) { \
+ static inline unsigned int first_trailing_one##suffix(type n) { \
return n ? trailing_zeros##suffix(n) + 1 : 0; \
}
@@ -333,19 +321,9 @@ UINT_OVERLOADS(LEADING_ZEROS)
UINT_OVERLOADS(TRAILING_ZEROS)
UINT_OVERLOADS(FIRST_TRAILING_ONE)
-#define ROTATE_LEFT(type, suffix, width) \
- static inline type rotate_left##suffix(type n, int c) { \
- return (n << c) | (n >> ((width - c) % width)); \
- }
-
-#define ROTATE_RIGHT(type, suffix, width) \
- static inline type rotate_right##suffix(type n, int c) { \
- return (n >> c) | (n << ((width - c) % width)); \
- }
-
#define FIRST_LEADING_ONE(type, suffix, width) \
- static inline int first_leading_one##suffix(type n) { \
- return width - leading_zeros##suffix(n); \
+ static inline unsigned int first_leading_one##suffix(type n) { \
+ return n ? leading_zeros##suffix(n) + 1 : 0; \
}
#define HAS_SINGLE_BIT(type, suffix, width) \
@@ -354,17 +332,30 @@ UINT_OVERLOADS(FIRST_TRAILING_ONE)
return n - 1 < (n ^ (n - 1)); \
}
-UINT_OVERLOADS(ROTATE_LEFT)
-UINT_OVERLOADS(ROTATE_RIGHT)
+#define BIT_WIDTH(type, suffix, width) \
+ static inline unsigned int bit_width##suffix(type n) { \
+ return width - leading_zeros##suffix(n); \
+ }
+
+#define BIT_FLOOR(type, suffix, width) \
+ static inline type bit_floor##suffix(type n) { \
+ return n ? (type)1 << (bit_width##suffix(n) - 1) : 0; \
+ }
+
+#define BIT_CEIL(type, suffix, width) \
+ static inline type bit_ceil##suffix(type n) { \
+ return (type)1 << bit_width##suffix(n - !!n); \
+ }
+
UINT_OVERLOADS(FIRST_LEADING_ONE)
UINT_OVERLOADS(HAS_SINGLE_BIT)
+UINT_OVERLOADS(BIT_WIDTH)
+UINT_OVERLOADS(BIT_FLOOR)
+UINT_OVERLOADS(BIT_CEIL)
#define count_ones(n) UINT_SELECT(n, count_ones)(n)
#define count_zeros(n) UINT_SELECT(n, count_ones)(~(n))
-#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c)
-#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c)
-
#define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n)
#define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n))
@@ -379,23 +370,26 @@ UINT_OVERLOADS(HAS_SINGLE_BIT)
#define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n)
-#define BIT_FLOOR(type, suffix, width) \
- static inline type bit_floor##suffix(type n) { \
- return n ? (type)1 << (first_leading_one##suffix(n) - 1) : 0; \
- }
+#define bit_width(n) UINT_SELECT(n, bit_width)(n)
+#define bit_floor(n) UINT_SELECT(n, bit_floor)(n)
+#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n)
-#define BIT_CEIL(type, suffix, width) \
- static inline type bit_ceil##suffix(type n) { \
- return (type)1 << first_leading_one##suffix(n - !!n); \
+#endif // __STDC_VERSION_STDBIT_H__ < C23
+
+#define ROTATE_LEFT(type, suffix, width) \
+ static inline type rotate_left##suffix(type n, int c) { \
+ return (n << c) | (n >> ((width - c) % width)); \
}
-UINT_OVERLOADS(BIT_FLOOR)
-UINT_OVERLOADS(BIT_CEIL)
+#define ROTATE_RIGHT(type, suffix, width) \
+ static inline type rotate_right##suffix(type n, int c) { \
+ return (n >> c) | (n << ((width - c) % width)); \
+ }
-#define bit_width(n) first_leading_one(n)
-#define bit_floor(n) UINT_SELECT(n, bit_floor)(n)
-#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n)
+UINT_OVERLOADS(ROTATE_LEFT)
+UINT_OVERLOADS(ROTATE_RIGHT)
-#endif // __STDC_VERSION__ < C23
+#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c)
+#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c)
#endif // BFS_BIT_H
diff --git a/src/color.c b/src/color.c
index 81f28bb..701a89c 100644
--- a/src/color.c
+++ b/src/color.c
@@ -583,7 +583,7 @@ static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) {
break;
}
- if (dstrncpy(&key, chunk, equals - chunk) != 0) {
+ if (dstrxcpy(&key, chunk, equals - chunk) != 0) {
goto fail;
}
if (unescape(&value, equals + 1, ':', &next) != 0) {
@@ -968,7 +968,7 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf,
if (path == ftwbuf->path) {
if (ftwbuf->depth == 0) {
at_fd = AT_FDCWD;
- at_path = dstrndup(path, max);
+ at_path = dstrxdup(path, max);
} else {
// The parent must have existed to get here
goto out;
@@ -977,13 +977,13 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf,
// We're in print_link_target(), so resolve relative to the link's parent directory
at_fd = ftwbuf->at_fd;
if (at_fd == (int)AT_FDCWD && path[0] != '/') {
- at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff);
- if (at_path && dstrncat(&at_path, path, max) != 0) {
+ at_path = dstrxdup(ftwbuf->path, ftwbuf->nameoff);
+ if (at_path && dstrxcat(&at_path, path, max) != 0) {
ret = -1;
goto out_path;
}
} else {
- at_path = dstrndup(path, max);
+ at_path = dstrxdup(path, max);
}
}
@@ -1206,7 +1206,7 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
for (const char *i = format; *i; ++i) {
size_t verbatim = strcspn(i, "%$");
- if (dstrncat(&cfile->buffer, i, verbatim) != 0) {
+ if (dstrxcat(&cfile->buffer, i, verbatim) != 0) {
return -1;
}
i += verbatim;
diff --git a/src/ctx.c b/src/ctx.c
index fac501a..0f619c2 100644
--- a/src/ctx.c
+++ b/src/ctx.c
@@ -229,9 +229,7 @@ static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) {
error = errno;
}
- if (ctx_file->hook) {
- sigunhook(ctx_file->hook);
- }
+ sigunhook(ctx_file->hook);
// Close the CFILE, except for stdio streams, which are closed later
if (cfile != ctx->cout && cfile != ctx->cerr) {
diff --git a/src/eval.c b/src/eval.c
index 8863e34..0b5992e 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -7,6 +7,7 @@
#include "prelude.h"
#include "eval.h"
+#include "atomic.h"
#include "bar.h"
#include "bfstd.h"
#include "bftw.h"
@@ -22,6 +23,7 @@
#include "printf.h"
#include "pwcache.h"
#include "sanity.h"
+#include "sighook.h"
#include "stat.h"
#include "trie.h"
#include "xregex.h"
@@ -997,6 +999,13 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
enum bfs_stat_flags flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW);
enum bfs_type type = bftw_type(ftwbuf, flags);
+
+ // GNU find treats ELOOP as a broken symbolic link for -xtype l
+ // (but not -L -type l)
+ if ((flags & BFS_STAT_TRYFOLLOW) && type == BFS_ERROR && errno == ELOOP) {
+ type = BFS_LNK;
+ }
+
if (type == BFS_ERROR) {
eval_report_error(state);
return false;
@@ -1284,7 +1293,7 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enu
DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW);
DEBUG_FLAG(flags, BFS_STAT_NOSYNC);
- fprintf(stderr, ") == %d", err ? 0 : -1);
+ fprintf(stderr, ") == %d", err == 0 ? 0 : -1);
if (err) {
fprintf(stderr, " [%d]", err);
@@ -1373,6 +1382,11 @@ struct callback_args {
struct bfs_bar *bar;
/** The time of the last status update. */
struct timespec last_status;
+ /** SIGINFO hook. */
+ struct sighook *info_hook;
+ /** Flag set by SIGINFO hook. */
+ atomic bool info_flag;
+
/** The number of files visited so far. */
size_t count;
@@ -1399,15 +1413,38 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) {
state.ret = &args->ret;
state.quit = false;
+ // Check whether SIGINFO was delivered and show/hide the bar
+ if (exchange(&args->info_flag, false, relaxed)) {
+ if (args->bar) {
+ bfs_bar_hide(args->bar);
+ args->bar = NULL;
+ } else {
+ args->bar = bfs_bar_show();
+ if (!args->bar) {
+ bfs_warning(ctx, "Couldn't show status bar: %s.\n", errstr());
+ }
+ }
+ }
+
if (args->bar) {
eval_status(&state, args->bar, &args->last_status, args->count);
}
if (ftwbuf->type == BFS_ERROR) {
- if (!eval_should_ignore(&state, ftwbuf->error)) {
- eval_error(&state, "%s.\n", xstrerror(ftwbuf->error));
- }
state.action = BFTW_PRUNE;
+
+ if (ftwbuf->error == ELOOP && ftwbuf->loopoff > 0) {
+ char *loop = strndup(ftwbuf->path, ftwbuf->loopoff);
+ if (loop) {
+ eval_error(&state, "Filesystem loop back to ${di}%pq${rs}\n", loop);
+ free(loop);
+ goto done;
+ }
+ } else if (eval_should_ignore(&state, ftwbuf->error)) {
+ goto done;
+ }
+
+ eval_error(&state, "%s.\n", xstrerror(ftwbuf->error));
goto done;
}
@@ -1462,6 +1499,12 @@ done:
return state.action;
}
+/** Show/hide the bar in response to SIGINFO. */
+static void eval_siginfo(int sig, siginfo_t *info, void *ptr) {
+ struct callback_args *args = ptr;
+ store(&args->info_flag, true, relaxed);
+}
+
/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
static int raise_fdlimit(struct bfs_ctx *ctx) {
rlim_t cur = ctx->orig_nofile.rlim_cur;
@@ -1624,6 +1667,13 @@ int bfs_eval(struct bfs_ctx *ctx) {
}
}
+#ifdef SIGINFO
+ int siginfo = SIGINFO;
+#else
+ int siginfo = SIGUSR1;
+#endif
+ args.info_hook = sighook(siginfo, eval_siginfo, &args, SH_CONTINUE);
+
struct trie seen;
if (ctx->unique) {
trie_init(&seen);
@@ -1691,6 +1741,7 @@ int bfs_eval(struct bfs_ctx *ctx) {
trie_destroy(&seen);
}
+ sigunhook(args.info_hook);
bfs_bar_hide(args.bar);
return args.ret;
diff --git a/src/exec.c b/src/exec.c
index cd73d6c..2faa731 100644
--- a/src/exec.c
+++ b/src/exec.c
@@ -234,7 +234,7 @@ static char *bfs_exec_format_arg(char *arg, const char *path) {
char *last = arg;
do {
- if (dstrncat(&ret, last, match - last) != 0) {
+ if (dstrxcat(&ret, last, match - last) != 0) {
goto err;
}
if (dstrcat(&ret, path) != 0) {
diff --git a/src/ioq.c b/src/ioq.c
index 4057a5e..6bb1ceb 100644
--- a/src/ioq.c
+++ b/src/ioq.c
@@ -313,9 +313,11 @@ static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) {
cond_broadcast(&monitor->cond);
}
-/** Branch-free (slot & IOQ_SKIP) ? ~IOQ_BLOCKED : 0 */
-static uintptr_t ioq_skip_mask(uintptr_t slot) {
- return -(slot >> IOQ_SKIP_BIT) << 1;
+/** Branch-free ((slot & IOQ_SKIP) ? skip : full) & ~IOQ_BLOCKED */
+static uintptr_t ioq_slot_blend(uintptr_t slot, uintptr_t skip, uintptr_t full) {
+ uintptr_t mask = -(slot >> IOQ_SKIP_BIT);
+ uintptr_t ret = (skip & mask) | (full & ~mask);
+ return ret & ~IOQ_BLOCKED;
}
/** Push an entry into a slot. */
@@ -323,19 +325,18 @@ static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent
uintptr_t prev = load(slot, relaxed);
while (true) {
- size_t skip_mask = ioq_skip_mask(prev);
- size_t full_mask = ~skip_mask & ~IOQ_BLOCKED;
- if (prev & full_mask) {
+ uintptr_t full = ioq_slot_blend(prev, 0, prev);
+ if (full) {
// full(ptr) → wait
prev = ioq_slot_wait(ioqq, slot, prev);
continue;
}
// empty → full(ptr)
- uintptr_t next = ((uintptr_t)ent >> 1) & full_mask;
+ uintptr_t next = (uintptr_t)ent >> 1;
// skip(1) → empty
// skip(n) → skip(n - 1)
- next |= (prev - IOQ_SKIP_ONE) & skip_mask;
+ next = ioq_slot_blend(prev, prev - IOQ_SKIP_ONE, next);
if (compare_exchange_weak(slot, &prev, next, release, relaxed)) {
break;
@@ -357,9 +358,8 @@ static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool bloc
// skip(n) → skip(n + 1)
// full(ptr) → full(ptr - 1)
uintptr_t next = prev + IOQ_SKIP_ONE;
- // skip(n) → ~IOQ_BLOCKED
// full(ptr) → 0
- next &= ioq_skip_mask(next);
+ next = ioq_slot_blend(next, next, 0);
if (block && next) {
prev = ioq_slot_wait(ioqq, slot, prev);
@@ -378,7 +378,7 @@ static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool bloc
// empty → 0
// skip(n) → 0
// full(ptr) → ptr
- prev &= ioq_skip_mask(~prev);
+ prev = ioq_slot_blend(prev, 0, prev);
return (struct ioq_ent *)(prev << 1);
}
diff --git a/src/list.h b/src/list.h
index 61d0e5b..d95483f 100644
--- a/src/list.h
+++ b/src/list.h
@@ -324,6 +324,25 @@
LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__))
/**
+ * Splice a singly-linked list into another.
+ *
+ * @param dest
+ * The destination list.
+ * @param cursor
+ * A pointer to the item to splice after, e.g. &list->head or list->tail.
+ * @param src
+ * The source list.
+ */
+#define SLIST_SPLICE(dest, cursor, src) \
+ LIST_VOID_(SLIST_SPLICE_((dest), (cursor), (src)))
+
+#define SLIST_SPLICE_(dest, cursor, src) \
+ *src->tail = *cursor, \
+ *cursor = src->head, \
+ dest->tail = *dest->tail ? src->tail : dest->tail, \
+ SLIST_INIT(src)
+
+/**
* Add an entire singly-linked list to the tail of another.
*
* @param dest
@@ -332,10 +351,7 @@
* The source list.
*/
#define SLIST_EXTEND(dest, src) \
- SLIST_EXTEND_((dest), (src))
-
-#define SLIST_EXTEND_(dest, src) \
- (src->head ? (*dest->tail = src->head, dest->tail = src->tail, SLIST_INIT(src)) : (void)0)
+ SLIST_SPLICE(dest, (dest)->tail, src)
/**
* Remove an item from a singly-linked list.
diff --git a/src/main.c b/src/main.c
index 5dd88e4..6f23034 100644
--- a/src/main.c
+++ b/src/main.c
@@ -41,6 +41,7 @@
* - thread.h (multi-threading)
* - trie.[ch] (a trie set/map implementation)
* - typo.[ch] (fuzzy matching for typos)
+ * - version.c (embeds version information)
* - xregex.[ch] (regular expression support)
* - xspawn.[ch] (spawns processes)
* - xtime.[ch] (date/time handling utilities)
diff --git a/src/opt.c b/src/opt.c
index 6704a46..9b091bd 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -102,39 +102,20 @@ enum pred_type {
PRED_TYPES,
};
-/** Get the name of a predicate type. */
-static const char *pred_type_name(enum pred_type type) {
- switch (type) {
- case READABLE_PRED:
- return "-readable";
- case WRITABLE_PRED:
- return "-writable";
- case EXECUTABLE_PRED:
- return "-executable";
- case ACL_PRED:
- return "-acl";
- case CAPABLE_PRED:
- return "-capable";
- case EMPTY_PRED:
- return "-empty";
- case HIDDEN_PRED:
- return "-hidden";
- case NOGROUP_PRED:
- return "-nogroup";
- case NOUSER_PRED:
- return "-nouser";
- case SPARSE_PRED:
- return "-sparse";
- case XATTR_PRED:
- return "-xattr";
-
- case PRED_TYPES:
- break;
- }
-
- bfs_bug("Unknown predicate %d", (int)type);
- return "???";
-}
+/** Predicate type names. */
+static const char *const pred_names[] = {
+ [READABLE_PRED] = "-readable",
+ [WRITABLE_PRED] = "-writable",
+ [EXECUTABLE_PRED] = "-executable",
+ [ACL_PRED] = "-acl",
+ [CAPABLE_PRED] = "-capable",
+ [EMPTY_PRED] = "-empty",
+ [HIDDEN_PRED] = "-hidden",
+ [NOGROUP_PRED] = "-nogroup",
+ [NOUSER_PRED] = "-nouser",
+ [SPARSE_PRED] = "-sparse",
+ [XATTR_PRED] = "-xattr",
+};
/**
* A contrained integer range.
@@ -242,29 +223,15 @@ enum range_type {
RANGE_TYPES,
};
-/** Get the name of a range type. */
-static const char *range_type_name(enum range_type type) {
- switch (type) {
- case DEPTH_RANGE:
- return "-depth";
- case GID_RANGE:
- return "-gid";
- case INUM_RANGE:
- return "-inum";
- case LINKS_RANGE:
- return "-links";
- case SIZE_RANGE:
- return "-size";
- case UID_RANGE:
- return "-uid";
-
- case RANGE_TYPES:
- break;
- }
-
- bfs_bug("Unknown range %d", (int)type);
- return "???";
-}
+/** Range type names. */
+static const char *const range_names[] = {
+ [DEPTH_RANGE] = "-depth",
+ [GID_RANGE] = "-gid",
+ [INUM_RANGE] = "-inum",
+ [LINKS_RANGE] = "-links",
+ [SIZE_RANGE] = "-size",
+ [UID_RANGE] = "-uid",
+};
/**
* The data flow analysis domain.
@@ -333,27 +300,27 @@ static void df_init_top(struct df_domain *value) {
/** Check for the top element. */
static bool df_is_top(const struct df_domain *value) {
- for (int i = 0; i < PRED_TYPES; ++i) {
- if (value->preds[i] != PRED_TOP) {
- return false;
- }
- }
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] != PRED_TOP) {
+ return false;
+ }
+ }
- for (int i = 0; i < RANGE_TYPES; ++i) {
- if (!range_is_top(&value->ranges[i])) {
- return false;
- }
- }
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (!range_is_top(&value->ranges[i])) {
+ return false;
+ }
+ }
- if (value->types != ~0U) {
- return false;
- }
+ if (value->types != ~0U) {
+ return false;
+ }
- if (value->xtypes != ~0U) {
- return false;
- }
+ if (value->xtypes != ~0U) {
+ return false;
+ }
- return true;
+ return true;
}
/** Compute the union of two fact sets. */
@@ -503,7 +470,7 @@ typedef bool dump_fn(struct bfs_opt *opt, const char *format, ...);
/** Print a df_pred. */
static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum pred_type type) {
- dump(opt, "${blu}%s${rs}: ", pred_type_name(type));
+ dump(opt, "${blu}%s${rs}: ", pred_names[type]);
FILE *file = opt->ctx->cerr->file;
switch (value->preds[type]) {
@@ -524,7 +491,7 @@ static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain
/** Print a df_range. */
static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) {
- dump(opt, "${blu}%s${rs}: ", range_type_name(type));
+ dump(opt, "${blu}%s${rs}: ", range_names[type]);
FILE *file = opt->ctx->cerr->file;
const struct df_range *range = &value->ranges[type];
diff --git a/src/parse.c b/src/parse.c
index 539aa05..86ce72c 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -115,21 +115,28 @@ struct bfs_parser {
};
/**
- * Possible token types.
+ * Token types and flags.
*/
-enum token_type {
+enum token_info {
/** A flag. */
- T_FLAG,
+ T_FLAG = 1,
/** A root path. */
- T_PATH,
+ T_PATH = 2,
/** An option. */
- T_OPTION,
+ T_OPTION = 3,
/** A test. */
- T_TEST,
+ T_TEST = 4,
/** An action. */
- T_ACTION,
+ T_ACTION = 5,
/** An operator. */
- T_OPERATOR,
+ T_OPERATOR = 6,
+ /** Mask for token types. */
+ T_TYPE = (1 << 3) - 1,
+
+ /** A token can match a prefix of an argument, like -On, -newerXY, etc. */
+ T_PREFIX = 1 << 3,
+ /** A flag that takes an argument. */
+ T_NEEDS_ARG = 1 << 4,
};
/**
@@ -160,7 +167,6 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc,
*/
attr(printf(2, 3))
static void parse_error(const struct bfs_parser *parser, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
@@ -170,7 +176,6 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ...
va_list args;
va_start(args, format);
- errno = error;
bfs_verror(parser->ctx, format, args);
va_end(args);
}
@@ -180,7 +185,6 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ...
*/
attr(printf(4, 5))
static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
@@ -190,7 +194,6 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_
va_list args;
va_start(args, format);
- errno = error;
bfs_verror(ctx, format, args);
va_end(args);
}
@@ -200,7 +203,6 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_
*/
attr(printf(6, 7))
static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
@@ -211,7 +213,6 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1,
va_list args;
va_start(args, format);
- errno = error;
bfs_verror(ctx, format, args);
va_end(args);
}
@@ -221,14 +222,12 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1,
*/
attr(printf(3, 4))
static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bfs_expr_error(ctx, expr);
va_list args;
va_start(args, format);
- errno = error;
bfs_verror(ctx, format, args);
va_end(args);
}
@@ -238,7 +237,6 @@ static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_e
*/
attr(printf(2, 3))
static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
@@ -250,7 +248,6 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, .
va_list args;
va_start(args, format);
- errno = error;
bool ret = bfs_vwarning(parser->ctx, format, args);
va_end(args);
return ret;
@@ -261,7 +258,6 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, .
*/
attr(printf(6, 7))
static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
@@ -274,7 +270,6 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1
va_list args;
va_start(args, format);
- errno = error;
bool ret = bfs_vwarning(ctx, format, args);
va_end(args);
return ret;
@@ -285,7 +280,6 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1
*/
attr(printf(3, 4))
static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
- int error = errno;
const struct bfs_ctx *ctx = parser->ctx;
if (!bfs_expr_warning(ctx, expr)) {
@@ -294,7 +288,6 @@ static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs
va_list args;
va_start(args, format);
- errno = error;
bool ret = bfs_vwarning(ctx, format, args);
va_end(args);
return ret;
@@ -414,7 +407,9 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser);
/**
* Advance by a single token.
*/
-static char **parser_advance(struct bfs_parser *parser, enum token_type type, size_t argc) {
+static char **parser_advance(struct bfs_parser *parser, enum token_info type, size_t argc) {
+ bfs_assert(type == (type & T_TYPE));
+
if (type != T_FLAG && type != T_PATH) {
parser->expr_started = true;
}
@@ -517,20 +512,14 @@ enum int_flags {
* Parse an integer.
*/
static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) {
- // strtoll() skips leading spaces, but we want to reject them
- if (xisspace(str[0])) {
- goto bad;
- }
-
int base = flags & IF_BASE_MASK;
if (base == 0) {
base = 10;
}
char *endptr;
- errno = 0;
- long long value = strtoll(str, &endptr, base);
- if (errno != 0) {
+ long long value;
+ if (xstrtoll(str, &endptr, base, &value) != 0) {
if (errno == ERANGE) {
goto range;
} else {
@@ -538,13 +527,6 @@ static const char *parse_int(const struct bfs_parser *parser, char **arg, const
}
}
- // https://github.com/llvm/llvm-project/issues/64946
- sanitize_init(&endptr);
-
- if (endptr == str) {
- goto bad;
- }
-
if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') {
goto bad;
}
@@ -657,9 +639,11 @@ static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) {
*/
static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) {
const char *arg = parser->argv[0];
+ char flag = arg[strlen(arg) - 1];
+
const char *value = parser->argv[1];
if (!value) {
- parse_error(parser, "${cyn}%s${rs} needs a value.\n", arg);
+ parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
return NULL;
}
@@ -667,6 +651,29 @@ static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) {
}
/**
+ * Parse a prefix flag like -O3, -j8, etc.
+ */
+static struct bfs_expr *parse_prefix_flag(struct bfs_parser *parser, char flag, bool allow_separate, const char **value) {
+ const char *arg = parser->argv[0];
+
+ const char *suffix = strchr(arg, flag) + 1;
+ if (*suffix) {
+ *value = suffix;
+ return parse_nullary_flag(parser);
+ }
+
+ suffix = parser->argv[1];
+ if (allow_separate && suffix) {
+ *value = suffix;
+ } else {
+ parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
+ return NULL;
+ }
+
+ return parse_unary_flag(parser);
+}
+
+/**
* Parse a single option.
*/
static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) {
@@ -811,7 +818,8 @@ static bool parse_debug_flag(const char *flag, size_t len, const char *expected)
static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) {
struct bfs_ctx *ctx = parser->ctx;
- struct bfs_expr *expr = parse_unary_flag(parser);
+ const char *flags;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags);
if (!expr) {
cfprintf(ctx->cerr, "\n");
debug_help(ctx->cerr);
@@ -820,7 +828,7 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg
bool unrecognized = false;
- for (const char *flag = expr->argv[1], *next; flag; flag = next) {
+ for (const char *flag = flags, *next; flag; flag = next) {
size_t len = strcspn(flag, ",");
if (flag[len]) {
next = flag + len + 1;
@@ -868,21 +876,22 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg
* Parse -On.
*/
static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_flag(parser);
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg);
if (!expr) {
return NULL;
}
int *optlevel = &parser->ctx->optlevel;
- if (strcmp(expr->argv[0], "-Ofast") == 0) {
+ if (strcmp(arg, "fast") == 0) {
*optlevel = 4;
- } else if (!parse_int(parser, expr->argv, expr->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) {
+ } else if (!parse_int(parser, expr->argv, arg, optlevel, IF_INT | IF_UNSIGNED)) {
return NULL;
}
if (*optlevel > 4) {
- parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", expr->argv[0] + 2);
+ parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", arg);
}
return expr;
@@ -1613,13 +1622,14 @@ static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2
* Parse -j<n>.
*/
static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_flag(parser);
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'j', false, &arg);
if (!expr) {
return NULL;
}
unsigned int n;
- if (!parse_int(parser, expr->argv, expr->argv[0] + 2, &n, IF_INT | IF_UNSIGNED)) {
+ if (!parse_int(parser, expr->argv, arg, &n, IF_INT | IF_UNSIGNED)) {
return NULL;
}
@@ -2253,16 +2263,27 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int
// See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html
const char *type = expr->argv[1];
if (strcmp(type, "posix-basic") == 0
+ || strcmp(type, "posix-minimal-basic") == 0
|| strcmp(type, "ed") == 0
|| strcmp(type, "sed") == 0) {
parser->regex_type = BFS_REGEX_POSIX_BASIC;
} else if (strcmp(type, "posix-extended") == 0) {
parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
#if BFS_WITH_ONIGURUMA
+ } else if (strcmp(type, "awk") == 0
+ || strcmp(type, "posix-awk") == 0) {
+ parser->regex_type = BFS_REGEX_AWK;
+ } else if (strcmp(type, "gnu-awk") == 0) {
+ parser->regex_type = BFS_REGEX_GNU_AWK;
} else if (strcmp(type, "emacs") == 0) {
parser->regex_type = BFS_REGEX_EMACS;
} else if (strcmp(type, "grep") == 0) {
parser->regex_type = BFS_REGEX_GREP;
+ } else if (strcmp(type, "egrep") == 0
+ || strcmp(type, "posix-egrep") == 0) {
+ parser->regex_type = BFS_REGEX_EGREP;
+ } else if (strcmp(type, "findutils-default") == 0) {
+ parser->regex_type = BFS_REGEX_GNU_FIND;
#endif
} else if (strcmp(type, "help") == 0) {
parser->just_info = true;
@@ -2277,14 +2298,23 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int
list_types:
cfprintf(cfile, "Supported types are:\n\n");
- cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
- cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n");
- cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
+ cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
+ cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
+ cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n");
+
+ cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n");
+
#if BFS_WITH_ONIGURUMA
- cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n");
- cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n");
+ cfprintf(cfile, " [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n");
+ cfprintf(cfile, " ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n");
+ cfprintf(cfile, " [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n");
#endif
- cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n");
return NULL;
}
@@ -2322,13 +2352,13 @@ static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg
struct bfs_ctx *ctx = parser->ctx;
CFILE *cfile = ctx->cerr;
- struct bfs_expr *expr = parse_unary_flag(parser);
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg);
if (!expr) {
cfprintf(cfile, "\n");
goto list_strategies;
}
- const char *arg = expr->argv[1];
if (strcmp(arg, "bfs") == 0) {
ctx->strategy = BFTW_BFS;
} else if (strcmp(arg, "dfs") == 0) {
@@ -2928,12 +2958,23 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2
static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) {
cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version);
- printf("%s\n", BFS_HOMEPAGE);
+ printf("Copyright © Tavian Barnes and the bfs contributors\n");
+ printf("No rights reserved (https://opensource.org/license/0BSD)\n\n");
+
+ printf("CONFFLAGS := %s\n", bfs_confflags);
+ printf("CC := %s\n", bfs_cc);
+ printf("CPPFLAGS := %s\n", bfs_cppflags);
+ printf("CFLAGS := %s\n", bfs_cflags);
+ printf("LDFLAGS := %s\n", bfs_ldflags);
+ printf("LDLIBS := %s\n", bfs_ldlibs);
+
+ printf("\n%s\n", BFS_HOMEPAGE);
parser->just_info = true;
return NULL;
}
+/** Parser callback function type. */
typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2);
/**
@@ -2941,11 +2982,10 @@ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2)
*/
struct table_entry {
char *arg;
- enum token_type type;
+ enum token_info info;
parse_fn *parse;
int arg1;
int arg2;
- bool prefix;
};
/**
@@ -2959,13 +2999,13 @@ static const struct table_entry parse_table[] = {
{"-Bnewer", T_TEST, parse_newer, BFS_STAT_BTIME},
{"-Bsince", T_TEST, parse_since, BFS_STAT_BTIME},
{"-Btime", T_TEST, parse_time, BFS_STAT_BTIME},
- {"-D", T_FLAG, parse_debug},
+ {"-D", T_FLAG | T_PREFIX, parse_debug},
{"-E", T_FLAG, parse_regex_extended},
{"-H", T_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false},
{"-L", T_FLAG, parse_follow, BFTW_FOLLOW_ALL, false},
- {"-O", T_FLAG, parse_optlevel, 0, 0, true},
+ {"-O", T_FLAG | T_PREFIX, parse_optlevel},
{"-P", T_FLAG, parse_follow, 0, false},
- {"-S", T_FLAG, parse_search_strategy},
+ {"-S", T_FLAG | T_PREFIX, parse_search_strategy},
{"-X", T_FLAG, parse_xargs_safe},
{"-a", T_OPERATOR},
{"-acl", T_TEST, parse_acl},
@@ -2991,7 +3031,7 @@ static const struct table_entry parse_table[] = {
{"-execdir", T_ACTION, parse_exec, BFS_EXEC_CHDIR},
{"-executable", T_TEST, parse_access, X_OK},
{"-exit", T_ACTION, parse_exit},
- {"-f", T_FLAG, parse_f},
+ {"-f", T_FLAG | T_NEEDS_ARG, parse_f},
{"-false", T_TEST, parse_const, false},
{"-files0-from", T_OPTION, parse_files0_from},
{"-flags", T_TEST, parse_flags},
@@ -3012,7 +3052,7 @@ static const struct table_entry parse_table[] = {
{"-ipath", T_TEST, parse_path, true},
{"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE},
{"-iwholename", T_TEST, parse_path, true},
- {"-j", T_FLAG, parse_jobs, 0, 0, true},
+ {"-j", T_FLAG | T_PREFIX, parse_jobs},
{"-limit", T_ACTION, parse_limit},
{"-links", T_TEST, parse_links},
{"-lname", T_TEST, parse_lname, false},
@@ -3026,7 +3066,7 @@ static const struct table_entry parse_table[] = {
{"-mtime", T_TEST, parse_time, BFS_STAT_MTIME},
{"-name", T_TEST, parse_name, false},
{"-newer", T_TEST, parse_newer, BFS_STAT_MTIME},
- {"-newer", T_TEST, parse_newerxy, 0, 0, true},
+ {"-newer", T_TEST | T_PREFIX, parse_newerxy},
{"-nocolor", T_OPTION, parse_color, false},
{"-nogroup", T_TEST, parse_nogroup},
{"-nohidden", T_TEST, parse_nohidden},
@@ -3079,7 +3119,7 @@ static const struct table_entry parse_table[] = {
static const struct table_entry *table_lookup(const char *arg) {
for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
bool match;
- if (entry->prefix) {
+ if (entry->info & T_PREFIX) {
match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0;
} else {
match = strcmp(arg, entry->arg) == 0;
@@ -3092,6 +3132,85 @@ static const struct table_entry *table_lookup(const char *arg) {
return NULL;
}
+/** Look up a single-character flag in the parse table. */
+static const struct table_entry *flag_lookup(char flag) {
+ for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
+ enum token_info type = entry->info & T_TYPE;
+ if (type == T_FLAG && entry->arg[1] == flag && !entry->arg[2]) {
+ return entry;
+ }
+ }
+
+ return NULL;
+}
+
+/** Check for a multi-flag argument like -LEXO2. */
+static bool is_flag_group(const char *arg) {
+ // We enforce that at least one flag in a flag group must be a capital
+ // letter, to avoid ambiguity with primary expressions
+ bool has_upper = false;
+
+ // Flags that take an argument must appear last
+ bool needs_arg = false;
+
+ for (size_t i = 1; arg[i]; ++i) {
+ char c = arg[i];
+ if (c >= 'A' && c <= 'Z') {
+ has_upper = true;
+ }
+
+ if (needs_arg) {
+ return false;
+ }
+
+ const struct table_entry *entry = flag_lookup(c);
+ if (!entry || !entry->parse) {
+ return false;
+ }
+
+ if (entry->info & T_PREFIX) {
+ // The rest is the flag's argument
+ break;
+ }
+
+ if (entry->info & T_NEEDS_ARG) {
+ needs_arg = true;
+ }
+ }
+
+ return has_upper;
+}
+
+/** Parse a multi-flag argument. */
+static struct bfs_expr *parse_flag_group(struct bfs_parser *parser) {
+ struct bfs_expr *expr = NULL;
+
+ char **start = parser->argv;
+ char **end = start;
+ const char *arg = start[0];
+
+ for (size_t i = 1; arg[i]; ++i) {
+ parser->argv = start;
+
+ const struct table_entry *entry = flag_lookup(arg[i]);
+ expr = entry->parse(parser, entry->arg1, entry->arg2);
+
+ if (parser->argv > end) {
+ end = parser->argv;
+ }
+
+ if (!expr || entry->info & T_PREFIX) {
+ break;
+ }
+ }
+
+ if (expr) {
+ bfs_assert(parser->argv == end, "Didn't eat enough tokens");
+ }
+
+ return expr;
+}
+
/** Search for a fuzzy match in the parse table. */
static const struct table_entry *table_lookup_fuzzy(const char *arg) {
const struct table_entry *best = NULL;
@@ -3130,11 +3249,15 @@ static struct bfs_expr *parse_primary(struct bfs_parser *parser) {
}
}
+ if (is_flag_group(arg)) {
+ return parse_flag_group(parser);
+ }
+
match = table_lookup_fuzzy(arg);
CFILE *cerr = parser->ctx->cerr;
parse_error(parser, "Unknown argument; did you mean ");
- switch (match->type) {
+ switch (match->info & T_TYPE) {
case T_FLAG:
cfprintf(cerr, "${cyn}%s${rs}?", match->arg);
break;
diff --git a/src/prelude.h b/src/prelude.h
index 3521fe8..34ce803 100644
--- a/src/prelude.h
+++ b/src/prelude.h
@@ -40,6 +40,13 @@
// when the version number changes
extern const char bfs_version[];
+extern const char bfs_confflags[];
+extern const char bfs_cc[];
+extern const char bfs_cppflags[];
+extern const char bfs_cflags[];
+extern const char bfs_ldflags[];
+extern const char bfs_ldlibs[];
+
// Check for system headers
#ifdef __has_include
@@ -50,6 +57,9 @@ extern const char bfs_version[];
#if __has_include(<paths.h>)
# define BFS_HAS_PATHS_H true
#endif
+#if __has_include(<stdbit.h>)
+# define BFS_HAS_STDBIT_H true
+#endif
#if __has_include(<sys/extattr.h>)
# define BFS_HAS_SYS_EXTATTR_H true
#endif
@@ -76,6 +86,7 @@ extern const char bfs_version[];
#define BFS_HAS_MNTENT_H __GLIBC__
#define BFS_HAS_PATHS_H true
+#define BFS_HAS_STDBIT_H (__STDC_VERSION__ >= C23)
#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__
#define BFS_HAS_SYS_MKDEV_H false
#define BFS_HAS_SYS_PARAM_H true
@@ -133,7 +144,7 @@ extern const char bfs_version[];
/**
* Get the length of an array.
*/
-#define countof(array) (sizeof(array) / sizeof(0[array]))
+#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__]))
/**
* False sharing/destructive interference/largest cache line size.
diff --git a/src/printf.c b/src/printf.c
index be09ebd..6b07c54 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -505,30 +505,25 @@ static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
static const char *bfs_printf_type(enum bfs_type type) {
- switch (type) {
- case BFS_BLK:
- return "b";
- case BFS_CHR:
- return "c";
- case BFS_DIR:
- return "d";
- case BFS_DOOR:
- return "D";
- case BFS_FIFO:
- return "p";
- case BFS_LNK:
- return "l";
- case BFS_PORT:
- return "P";
- case BFS_REG:
- return "f";
- case BFS_SOCK:
- return "s";
- case BFS_WHT:
- return "w";
- default:
- return "U";
+ const char *const names[] = {
+ [BFS_BLK] = "b",
+ [BFS_CHR] = "c",
+ [BFS_DIR] = "d",
+ [BFS_DOOR] = "D",
+ [BFS_FIFO] = "p",
+ [BFS_LNK] = "l",
+ [BFS_PORT] = "P",
+ [BFS_REG] = "f",
+ [BFS_SOCK] = "s",
+ [BFS_WHT] = "w",
+ };
+
+ const char *name = NULL;
+ if ((size_t)type < countof(names)) {
+ name = names[type];
}
+
+ return name ? name : "U";
}
/** %y: type */
@@ -544,7 +539,7 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
int error = 0;
if (type == BFS_ERROR) {
- if (errno_is_like(ELOOP)) {
+ if (errno == ELOOP) {
str = "L";
} else if (errno_is_like(ENOENT)) {
str = "N";
diff --git a/src/sighook.c b/src/sighook.c
index ff5b96f..6d6ff01 100644
--- a/src/sighook.c
+++ b/src/sighook.c
@@ -52,6 +52,8 @@ struct arc {
/** Initialize an arc. */
static void arc_init(struct arc *arc) {
+ bfs_verify(atomic_is_lock_free(&arc->refs));
+
atomic_init(&arc->refs, 0);
arc->ptr = NULL;
@@ -166,6 +168,8 @@ static void *RCU_NULL = &RCU_NULL;
/** Initialize an RCU block. */
static void rcu_init(struct rcu *rcu) {
+ bfs_verify(atomic_is_lock_free(&rcu->active));
+
atomic_init(&rcu->active, 0);
arc_init(&rcu->slots[0]);
arc_init(&rcu->slots[1]);
@@ -323,11 +327,16 @@ static int rcu_sigtable_add(struct rcu *rcu, struct sighook *hook) {
return 0;
}
-/** The global table of signal hooks. */
-static struct rcu rcu_sighooks;
-/** The global table of atsigexit() hooks. */
+/** The sharded table of signal hooks. */
+static struct rcu rcu_sighooks[64];
+/** The table of atsigexit() hooks. */
static struct rcu rcu_exithooks;
+/** Get the table for a particular signal. */
+static struct rcu *sigshard(int sig) {
+ return &rcu_sighooks[sig % countof(rcu_sighooks)];
+}
+
/** Mutex for initialization and RCU writer exclusion. */
static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER;
@@ -475,7 +484,8 @@ static void sigdispatch(int sig, siginfo_t *info, void *context) {
int error = errno;
// Run the normal hooks
- enum sigflags flags = run_hooks(&rcu_sighooks, sig, info);
+ struct rcu *shard = sigshard(sig);
+ enum sigflags flags = run_hooks(shard, sig, info);
// Run the atsigexit() hooks, if we're exiting
if (!(flags & SH_CONTINUE) && is_fatal(sig)) {
@@ -501,8 +511,12 @@ static int siginit(int sig) {
|| sigemptyset(&action.sa_mask) != 0) {
return -1;
}
- rcu_init(&rcu_sighooks);
+
+ for (size_t i = 0; i < countof(rcu_sighooks); ++i) {
+ rcu_init(&rcu_sighooks[i]);
+ }
rcu_init(&rcu_exithooks);
+
initialized = true;
}
@@ -552,7 +566,8 @@ struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags)
goto done;
}
- ret = sighook_impl(&rcu_sighooks, sig, fn, arg, flags);
+ struct rcu *shard = sigshard(sig);
+ ret = sighook_impl(shard, sig, fn, arg, flags);
done:
mutex_unlock(&sigmutex);
return ret;
@@ -561,32 +576,38 @@ done:
struct sighook *atsigexit(sighook_fn *fn, void *arg) {
mutex_lock(&sigmutex);
- struct sighook *ret = NULL;
-
for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) {
- if (siginit(FATAL_SIGNALS[i]) != 0) {
- goto done;
- }
+ // Ignore errors; atsigexit() is best-effort anyway and things
+ // like sanitizer runtimes or valgrind may reserve signals for
+ // their own use
+ siginit(FATAL_SIGNALS[i]);
}
#ifdef SIGRTMIN
for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) {
- if (siginit(i) != 0) {
- goto done;
- }
+ siginit(i);
}
#endif
- ret = sighook_impl(&rcu_exithooks, 0, fn, arg, 0);
-done:
+ struct sighook *ret = sighook_impl(&rcu_exithooks, 0, fn, arg, 0);
mutex_unlock(&sigmutex);
return ret;
}
void sigunhook(struct sighook *hook) {
+ if (!hook) {
+ return;
+ }
+
mutex_lock(&sigmutex);
- struct rcu *rcu = hook->sig ? &rcu_sighooks : &rcu_exithooks;
+ struct rcu *rcu;
+ if (hook->sig) {
+ rcu = sigshard(hook->sig);
+ } else {
+ rcu = &rcu_exithooks;
+ }
+
struct sigtable *table = rcu_peek(rcu);
bfs_verify(sigtable_del(table, hook) == 0);
diff --git a/src/stat.c b/src/stat.c
index 2044564..1e340d0 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -51,7 +51,7 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) {
return "modification time";
}
- bfs_bug("Unrecognized stat field");
+ bfs_bug("Unrecognized stat field %d", (int)field);
return "???";
}
diff --git a/src/trie.c b/src/trie.c
index 7504d53..cc5db09 100644
--- a/src/trie.c
+++ b/src/trie.c
@@ -621,7 +621,7 @@ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) {
struct trie_node *node = trie_decode_node(ptr);
// Make sure the bitmap is a power of two, i.e. it has just one child
- bfs_assert(has_single_bit(node->bitmap));
+ bfs_assert(has_single_bit((size_t)node->bitmap));
ptr = node->children[0];
trie_node_free(trie, node, 1);
diff --git a/src/typo.c b/src/typo.c
index b1c5c44..cb826fc 100644
--- a/src/typo.c
+++ b/src/typo.c
@@ -125,7 +125,7 @@ int typo_distance(const char *actual, const char *expected) {
// This is the Wagner-Fischer algorithm for Levenshtein distance, using
// Manhattan distance on the keyboard for individual characters.
- const int insert_cost = 12;
+ const int insert_cost = (40 + 12 + 1) / 2;
size_t rows = strlen(actual) + 1;
size_t cols = strlen(expected) + 1;
diff --git a/src/version.c b/src/version.c
new file mode 100644
index 0000000..8383350
--- /dev/null
+++ b/src/version.c
@@ -0,0 +1,32 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+
+const char bfs_version[] = {
+#include "version.i"
+};
+
+const char bfs_confflags[] = {
+#include "confflags.i"
+};
+
+const char bfs_cc[] = {
+#include "cc.i"
+};
+
+const char bfs_cppflags[] = {
+#include "cppflags.i"
+};
+
+const char bfs_cflags[] = {
+#include "cflags.i"
+};
+
+const char bfs_ldflags[] = {
+#include "ldflags.i"
+};
+
+const char bfs_ldlibs[] = {
+#include "ldlibs.i"
+};
diff --git a/src/xregex.c b/src/xregex.c
index 484f099..2d089b2 100644
--- a/src/xregex.c
+++ b/src/xregex.c
@@ -37,6 +37,12 @@ struct bfs_regex {
static int bfs_onig_status;
static OnigEncoding bfs_onig_enc;
+static OnigSyntaxType bfs_onig_syntax_awk;
+static OnigSyntaxType bfs_onig_syntax_gnu_awk;
+static OnigSyntaxType bfs_onig_syntax_emacs;
+static OnigSyntaxType bfs_onig_syntax_egrep;
+static OnigSyntaxType bfs_onig_syntax_gnu_find;
+
/** pthread_once() callback. */
static void bfs_onig_once(void) {
// Fall back to ASCII by default
@@ -103,6 +109,35 @@ static void bfs_onig_once(void) {
if (bfs_onig_status != ONIG_NORMAL) {
bfs_onig_enc = NULL;
}
+
+ // Compute the GNU extensions
+ OnigSyntaxType *ere = ONIG_SYNTAX_POSIX_EXTENDED;
+ OnigSyntaxType *gnu = ONIG_SYNTAX_GNU_REGEX;
+ unsigned int gnu_op = gnu->op & ~ere->op;
+ unsigned int gnu_op2 = gnu->op2 & ~ere->op2;
+ unsigned int gnu_behavior = gnu->behavior & ~ere->behavior;
+
+ onig_copy_syntax(&bfs_onig_syntax_awk, ONIG_SYNTAX_POSIX_EXTENDED);
+ bfs_onig_syntax_awk.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL;
+ bfs_onig_syntax_awk.behavior |= ONIG_SYN_BACKSLASH_ESCAPE_IN_CC;
+
+ onig_copy_syntax(&bfs_onig_syntax_gnu_awk, &bfs_onig_syntax_awk);
+ bfs_onig_syntax_gnu_awk.op |= gnu_op;
+ bfs_onig_syntax_gnu_awk.op2 |= gnu_op2;
+ bfs_onig_syntax_gnu_awk.behavior |= gnu_behavior;
+ bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INDEP_REPEAT_OPS;
+ bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS;
+
+ // https://github.com/kkos/oniguruma/issues/296
+ onig_copy_syntax(&bfs_onig_syntax_emacs, ONIG_SYNTAX_EMACS);
+ bfs_onig_syntax_emacs.op2 |= ONIG_SYN_OP2_QMARK_GROUP_EFFECT;
+
+ onig_copy_syntax(&bfs_onig_syntax_egrep, ONIG_SYNTAX_POSIX_EXTENDED);
+ bfs_onig_syntax_egrep.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL;
+ bfs_onig_syntax_egrep.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS;
+
+ onig_copy_syntax(&bfs_onig_syntax_gnu_find, &bfs_onig_syntax_emacs);
+ bfs_onig_syntax_gnu_find.options |= ONIG_OPTION_MULTILINE;
}
/** Initialize Oniguruma. */
@@ -143,12 +178,24 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ
case BFS_REGEX_POSIX_EXTENDED:
syntax = ONIG_SYNTAX_POSIX_EXTENDED;
break;
+ case BFS_REGEX_AWK:
+ syntax = &bfs_onig_syntax_awk;
+ break;
+ case BFS_REGEX_GNU_AWK:
+ syntax = &bfs_onig_syntax_gnu_awk;
+ break;
case BFS_REGEX_EMACS:
- syntax = ONIG_SYNTAX_EMACS;
+ syntax = &bfs_onig_syntax_emacs;
break;
case BFS_REGEX_GREP:
syntax = ONIG_SYNTAX_GREP;
break;
+ case BFS_REGEX_EGREP:
+ syntax = &bfs_onig_syntax_egrep;
+ break;
+ case BFS_REGEX_GNU_FIND:
+ syntax = &bfs_onig_syntax_gnu_find;
+ break;
}
bfs_assert(syntax, "Invalid regex type");
diff --git a/src/xregex.h b/src/xregex.h
index 998a2b0..750db24 100644
--- a/src/xregex.h
+++ b/src/xregex.h
@@ -15,8 +15,12 @@ struct bfs_regex;
enum bfs_regex_type {
BFS_REGEX_POSIX_BASIC,
BFS_REGEX_POSIX_EXTENDED,
+ BFS_REGEX_AWK,
+ BFS_REGEX_GNU_AWK,
BFS_REGEX_EMACS,
BFS_REGEX_GREP,
+ BFS_REGEX_EGREP,
+ BFS_REGEX_GNU_FIND,
};
/**
diff --git a/src/xspawn.c b/src/xspawn.c
index 33e5a4a..2c64011 100644
--- a/src/xspawn.c
+++ b/src/xspawn.c
@@ -426,8 +426,17 @@ static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const st
};
if (bfs_can_skip_resolve(res, ctx)) {
- res->done = true;
- return 0;
+ // Do this check eagerly, even though posix_spawn()/execv() also
+ // would, because:
+ //
+ // - faccessat() is faster than fork()/clone() + execv()
+ // - posix_spawn() is not guaranteed to report ENOENT
+ if (xfaccessat(AT_FDCWD, exe, X_OK) == 0) {
+ res->done = true;
+ return 0;
+ } else {
+ return -1;
+ }
}
res->path = getenv("PATH");
diff --git a/src/xtime.c b/src/xtime.c
index 2808455..186651b 100644
--- a/src/xtime.c
+++ b/src/xtime.c
@@ -206,6 +206,23 @@ static int xgetpart(const char **str, size_t n, int *result) {
}
int xgetdate(const char *str, struct timespec *result) {
+ // Handle @epochseconds
+ if (str[0] == '@') {
+ long long value;
+ if (xstrtoll(str + 1, NULL, 10, &value) != 0) {
+ goto error;
+ }
+
+ time_t time = (time_t)value;
+ if ((long long)time != value) {
+ errno = ERANGE;
+ goto error;
+ }
+
+ result->tv_sec = time;
+ goto done;
+ }
+
struct tm tm = {
.tm_isdst = -1,
};
@@ -324,6 +341,7 @@ end:
}
}
+done:
result->tv_nsec = 0;
return 0;
diff --git a/tests/bfs/Dmulti.out b/tests/bfs/Dmulti.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/Dmulti.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/Dmulti.sh b/tests/bfs/Dmulti.sh
new file mode 100644
index 0000000..35d64b1
--- /dev/null
+++ b/tests/bfs/Dmulti.sh
@@ -0,0 +1 @@
+bfs_diff -Dopt,tree,unknown basic
diff --git a/tests/bfs/LD_stat.out b/tests/bfs/LD_stat.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/bfs/LD_stat.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/bfs/LD_stat.sh b/tests/bfs/LD_stat.sh
new file mode 100644
index 0000000..d407de3
--- /dev/null
+++ b/tests/bfs/LD_stat.sh
@@ -0,0 +1 @@
+bfs_diff -LD stat links
diff --git a/tests/bfs/LDstat.out b/tests/bfs/LDstat.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/bfs/LDstat.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/bfs/LDstat.sh b/tests/bfs/LDstat.sh
new file mode 100644
index 0000000..ec6df0b
--- /dev/null
+++ b/tests/bfs/LDstat.sh
@@ -0,0 +1 @@
+bfs_diff -LDstat links
diff --git a/tests/bfs/O_3.sh b/tests/bfs/O_3.sh
new file mode 100644
index 0000000..f159852
--- /dev/null
+++ b/tests/bfs/O_3.sh
@@ -0,0 +1 @@
+! invoke_bfs -O 3 basic
diff --git a/tests/bfs/Sbfs.out b/tests/bfs/Sbfs.out
new file mode 100644
index 0000000..bb3cd8d
--- /dev/null
+++ b/tests/bfs/Sbfs.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/e
+basic/g
+basic/i
+basic/j
+basic/k
+basic/l
+basic/c/d
+basic/e/f
+basic/g/h
+basic/j/foo
+basic/k/foo
+basic/l/foo
+basic/k/foo/bar
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/Sbfs.sh b/tests/bfs/Sbfs.sh
new file mode 100644
index 0000000..72d92c8
--- /dev/null
+++ b/tests/bfs/Sbfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -Sbfs -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh
index 4adba69..4c14610 100644
--- a/tests/bfs/color_ext_case.sh
+++ b/tests/bfs/color_ext_case.sh
@@ -1,6 +1,6 @@
-# *.gz=01;31:*.GZ=01;32 -- case sensitive
-# *.tAr=01;33:*.TaR=01;33 -- case-insensitive
-# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive
-# *.txt=35:*TXT=36 -- case-insensitive
-export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36"
+# *.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32 -- case sensitive
+# *.tAr=01;33:*.TaR=01;33 -- case-insensitive
+# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive
+# *.txt=35:*TXT=36 -- case-insensitive
+export LS_COLORS="*.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36"
bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh
index ac4c7fb..9f818c9 100644
--- a/tests/bfs/color_ext_override.sh
+++ b/tests/bfs/color_ext_override.sh
@@ -1 +1 @@
-LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color
+LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;30:*.gz=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/nohidden.out b/tests/bfs/nohidden.out
index d3ec901..84e6bd2 100644
--- a/tests/bfs/nohidden.out
+++ b/tests/bfs/nohidden.out
@@ -1,4 +1,8 @@
+
+/n
weirdnames
+weirdnames/
+weirdnames/
weirdnames/
weirdnames/ /j
weirdnames/!
@@ -11,6 +15,8 @@ weirdnames/(-/c
weirdnames/(/b
weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
weirdnames/,
weirdnames/,/f
weirdnames/-
@@ -19,3 +25,5 @@ weirdnames/[
weirdnames/[/k
weirdnames/\
weirdnames/\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bfs/nohidden_depth.out b/tests/bfs/nohidden_depth.out
index d3ec901..84e6bd2 100644
--- a/tests/bfs/nohidden_depth.out
+++ b/tests/bfs/nohidden_depth.out
@@ -1,4 +1,8 @@
+
+/n
weirdnames
+weirdnames/
+weirdnames/
weirdnames/
weirdnames/ /j
weirdnames/!
@@ -11,6 +15,8 @@ weirdnames/(-/c
weirdnames/(/b
weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
weirdnames/,
weirdnames/,/f
weirdnames/-
@@ -19,3 +25,5 @@ weirdnames/[
weirdnames/[/k
weirdnames/\
weirdnames/\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh
index 02c8173..4683764 100644
--- a/tests/bfs/xtype_depth.sh
+++ b/tests/bfs/xtype_depth.sh
@@ -1,2 +1,2 @@
# Make sure -xtype is considered side-effecting for facts_when_impure
-! invoke_bfs loops -xtype l -depth 100
+! invoke_bfs inaccessible/link -xtype l -depth 100
diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh
index 8d75d7d..c1d94f3 100644
--- a/tests/bfs/xtype_reorder.sh
+++ b/tests/bfs/xtype_reorder.sh
@@ -1,4 +1,3 @@
# Make sure -xtype is not reordered in front of anything -- if -xtype runs
# before -links 100, it will report an ELOOP error
-bfs_diff loops -links 100 -xtype l
-invoke_bfs loops -links 100 -xtype l
+bfs_diff inaccessible/link -links 100 -xtype l
diff --git a/tests/bit.c b/tests/bit.c
index 674d1b2..49e167d 100644
--- a/tests/bit.c
+++ b/tests/bit.c
@@ -76,15 +76,15 @@ bool check_bit(void) {
ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412);
ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412);
- ret &= check_eq(count_ones(0x0), 0);
- ret &= check_eq(count_ones(0x1), 1);
- ret &= check_eq(count_ones(0x2), 1);
- ret &= check_eq(count_ones(0x3), 2);
- ret &= check_eq(count_ones(0x137F), 10);
-
- ret &= check_eq(count_zeros(0), INT_WIDTH);
- ret &= check_eq(count_zeros(0L), LONG_WIDTH);
- ret &= check_eq(count_zeros(0LL), LLONG_WIDTH);
+ ret &= check_eq(count_ones(0x0U), 0);
+ ret &= check_eq(count_ones(0x1U), 1);
+ ret &= check_eq(count_ones(0x2U), 1);
+ ret &= check_eq(count_ones(0x3U), 2);
+ ret &= check_eq(count_ones(0x137FU), 10);
+
+ ret &= check_eq(count_zeros(0U), UINT_WIDTH);
+ ret &= check_eq(count_zeros(0UL), ULONG_WIDTH);
+ ret &= check_eq(count_zeros(0ULL), ULLONG_WIDTH);
ret &= check_eq(count_zeros((uint8_t)0), 8);
ret &= check_eq(count_zeros((uint16_t)0), 16);
ret &= check_eq(count_zeros((uint32_t)0), 32);
@@ -100,16 +100,16 @@ bool check_bit(void) {
ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123);
ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678);
- for (int i = 0; i < 16; ++i) {
+ for (unsigned int i = 0; i < 16; ++i) {
uint16_t n = (uint16_t)1 << i;
- for (int j = i; j < 16; ++j) {
+ for (unsigned int j = i; j < 16; ++j) {
uint16_t m = (uint16_t)1 << j;
uint16_t nm = n | m;
ret &= check_eq(count_ones(nm), 1 + (n != m));
ret &= check_eq(count_zeros(nm), 15 - (n != m));
ret &= check_eq(leading_zeros(nm), 15 - j);
ret &= check_eq(trailing_zeros(nm), i);
- ret &= check_eq(first_leading_one(nm), j + 1);
+ ret &= check_eq(first_leading_one(nm), 16 - j);
ret &= check_eq(first_trailing_one(nm), i + 1);
ret &= check_eq(bit_width(nm), j + 1);
ret &= check_eq(bit_floor(nm), m);
@@ -127,13 +127,13 @@ bool check_bit(void) {
ret &= check_eq(leading_zeros((uint16_t)0), 16);
ret &= check_eq(trailing_zeros((uint16_t)0), 16);
- ret &= check_eq(first_leading_one(0), 0);
- ret &= check_eq(first_trailing_one(0), 0);
- ret &= check_eq(bit_width(0), 0);
- ret &= check_eq(bit_floor(0), 0);
- ret &= check_eq(bit_ceil(0), 1);
+ ret &= check_eq(first_leading_one(0U), 0);
+ ret &= check_eq(first_trailing_one(0U), 0);
+ ret &= check_eq(bit_width(0U), 0);
+ ret &= check_eq(bit_floor(0U), 0);
+ ret &= check_eq(bit_ceil(0U), 1);
- ret &= bfs_check(!has_single_bit(0));
+ ret &= bfs_check(!has_single_bit(0U));
ret &= bfs_check(!has_single_bit(UINT32_MAX));
ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1)));
diff --git a/tests/bsd/Hf.out b/tests/bsd/Hf.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/bsd/Hf.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/bsd/Hf.sh b/tests/bsd/Hf.sh
new file mode 100644
index 0000000..333280c
--- /dev/null
+++ b/tests/bsd/Hf.sh
@@ -0,0 +1 @@
+bfs_diff -Hf links/deeply/nested/dir
diff --git a/tests/bsd/X.out b/tests/bsd/X.out
index afa84f7..dbe2408 100644
--- a/tests/bsd/X.out
+++ b/tests/bsd/X.out
@@ -9,6 +9,8 @@ weirdnames/(-/c
weirdnames/(/b
weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
weirdnames/,
weirdnames/,/f
weirdnames/-
@@ -17,3 +19,5 @@ weirdnames/...
weirdnames/.../h
weirdnames/[
weirdnames/[/k
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bsd/perm_000_plus.out b/tests/bsd/perm_000_plus.out
index d7494b8..42f2fed 100644
--- a/tests/bsd/perm_000_plus.out
+++ b/tests/bsd/perm_000_plus.out
@@ -1,8 +1,10 @@
perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f---
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out
index 9a5b95a..5c78ecc 100644
--- a/tests/bsd/perm_222_plus.out
+++ b/tests/bsd/perm_222_plus.out
@@ -1,5 +1,6 @@
perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/frw-
+perms/frwx
diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out
index 7e5ae98..774c0ea 100644
--- a/tests/bsd/perm_644_plus.out
+++ b/tests/bsd/perm_644_plus.out
@@ -1,7 +1,9 @@
perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/bsd/printx.out b/tests/bsd/printx.out
index 04bf9a9..034b2da 100644
--- a/tests/bsd/printx.out
+++ b/tests/bsd/printx.out
@@ -1,3 +1,5 @@
+
+/n
weirdnames
weirdnames/!
weirdnames/!-
@@ -9,6 +11,8 @@ weirdnames/(-/c
weirdnames/(/b
weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
weirdnames/,
weirdnames/,/f
weirdnames/-
@@ -17,7 +21,11 @@ weirdnames/...
weirdnames/.../h
weirdnames/[
weirdnames/[/k
+weirdnames/\
+weirdnames/\
weirdnames/\
weirdnames/\ /j
weirdnames/\\
weirdnames/\\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bsd/s.out b/tests/bsd/s.out
index 6b790c2..5c85ac8 100644
--- a/tests/bsd/s.out
+++ b/tests/bsd/s.out
@@ -1,12 +1,16 @@
weirdnames
+weirdnames/
+
weirdnames/
weirdnames/!
weirdnames/!-
weirdnames/(
weirdnames/(-
weirdnames/)
+weirdnames/*
weirdnames/,
weirdnames/-
weirdnames/...
weirdnames/[
weirdnames/\
+weirdnames/{
diff --git a/tests/common/HLP.out b/tests/common/HLP.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/common/HLP.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/common/HLP.sh b/tests/common/HLP.sh
new file mode 100644
index 0000000..4b6d631
--- /dev/null
+++ b/tests/common/HLP.sh
@@ -0,0 +1 @@
+bfs_diff -HLP links/deeply/nested/dir
diff --git a/tests/common/amin.out b/tests/common/amin.out
new file mode 100644
index 0000000..af57325
--- /dev/null
+++ b/tests/common/amin.out
@@ -0,0 +1,6 @@
+-amin 1: ./one_minute_ago
+-amin +1: ./one_hour_ago
+-amin +1: ./two_minutes_ago
+-amin -1: ./in_one_hour
+-amin -1: ./in_one_minute
+-amin -1: ./thirty_seconds_ago
diff --git a/tests/common/amin.sh b/tests/common/amin.sh
new file mode 100644
index 0000000..c4d53fb
--- /dev/null
+++ b/tests/common/amin.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -at "@$((now - 121))" two_minutes_ago
+"$XTOUCH" -at "@$((now - 61))" one_minute_ago
+"$XTOUCH" -at "@$((now - 30))" thirty_seconds_ago
+"$XTOUCH" -at "@$((now + 60))" in_one_minute
+"$XTOUCH" -at "@$((now + 60 * 60))" in_one_hour
+
+bfs_diff -mindepth 1 \
+ \( -amin -1 -exec printf -- '-amin -1: %s\n' {} \; -o -true \) \
+ \( -amin 1 -exec printf -- '-amin 1: %s\n' {} \; -o -true \) \
+ \( -amin +1 -exec printf -- '-amin +1: %s\n' {} \; -o -true \)
diff --git a/tests/common/atime.out b/tests/common/atime.out
new file mode 100644
index 0000000..5ed206b
--- /dev/null
+++ b/tests/common/atime.out
@@ -0,0 +1,6 @@
+-atime 1: ./yesterday
+-atime +1: ./last_week
+-atime +1: ./two_days_ago
+-atime -1: ./now
+-atime -1: ./one_hour_ago
+-atime -1: ./tomorrow
diff --git a/tests/common/atime.sh b/tests/common/atime.sh
new file mode 100644
index 0000000..c2e4031
--- /dev/null
+++ b/tests/common/atime.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -at "@$((now - 60 * 60 * 24 * 7))" last_week
+"$XTOUCH" -at "@$((now - 60 * 60 * 49))" two_days_ago
+"$XTOUCH" -at "@$((now - 60 * 60 * 25))" yesterday
+"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -at "@$((now))" now
+"$XTOUCH" -at "@$((now + 60 * 60 * 24))" tomorrow
+
+bfs_diff -mindepth 1 \
+ \( -atime -1 -exec printf -- '-atime -1: %s\n' {} \; -o -true \) \
+ \( -atime 1 -exec printf -- '-atime 1: %s\n' {} \; -o -true \) \
+ \( -atime +1 -exec printf -- '-atime +1: %s\n' {} \; -o -true \)
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
index da45e23..49f773d 100644
--- a/tests/common/empty_error.out
+++ b/tests/common/empty_error.out
@@ -1,3 +1 @@
-./bar
-./baz
-./qux
+inaccessible/file
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
index 7c8049c..3438cca 100644
--- a/tests/common/empty_error.sh
+++ b/tests/common/empty_error.sh
@@ -1,7 +1 @@
-cd "$TEST"
-
-"$XTOUCH" -p foo/ bar/ baz qux
-chmod -r foo baz
-defer chmod +r foo baz
-
-! bfs_diff . -empty
+! bfs_diff inaccessible -empty
diff --git a/tests/common/mmin.out b/tests/common/mmin.out
new file mode 100644
index 0000000..4c79a16
--- /dev/null
+++ b/tests/common/mmin.out
@@ -0,0 +1,6 @@
+-mmin 1: ./one_minute_ago
+-mmin +1: ./one_hour_ago
+-mmin +1: ./two_minutes_ago
+-mmin -1: ./in_one_hour
+-mmin -1: ./in_one_minute
+-mmin -1: ./thirty_seconds_ago
diff --git a/tests/common/mmin.sh b/tests/common/mmin.sh
new file mode 100644
index 0000000..38e3337
--- /dev/null
+++ b/tests/common/mmin.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -mt "@$((now - 121))" two_minutes_ago
+"$XTOUCH" -mt "@$((now - 61))" one_minute_ago
+"$XTOUCH" -mt "@$((now - 30))" thirty_seconds_ago
+"$XTOUCH" -mt "@$((now + 60))" in_one_minute
+"$XTOUCH" -mt "@$((now + 60 * 60))" in_one_hour
+
+bfs_diff -mindepth 1 \
+ \( -mmin -1 -exec printf -- '-mmin -1: %s\n' {} \; -o -true \) \
+ \( -mmin 1 -exec printf -- '-mmin 1: %s\n' {} \; -o -true \) \
+ \( -mmin +1 -exec printf -- '-mmin +1: %s\n' {} \; -o -true \)
diff --git a/tests/common/mtime.out b/tests/common/mtime.out
new file mode 100644
index 0000000..91f0114
--- /dev/null
+++ b/tests/common/mtime.out
@@ -0,0 +1,6 @@
+-mtime 1: ./yesterday
+-mtime +1: ./last_week
+-mtime +1: ./two_days_ago
+-mtime -1: ./now
+-mtime -1: ./one_hour_ago
+-mtime -1: ./tomorrow
diff --git a/tests/common/mtime.sh b/tests/common/mtime.sh
new file mode 100644
index 0000000..7ba127e
--- /dev/null
+++ b/tests/common/mtime.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -mt "@$((now - 60 * 60 * 24 * 7))" last_week
+"$XTOUCH" -mt "@$((now - 60 * 60 * 49))" two_days_ago
+"$XTOUCH" -mt "@$((now - 60 * 60 * 25))" yesterday
+"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -mt "@$((now))" now
+"$XTOUCH" -mt "@$((now + 60 * 60 * 24))" tomorrow
+
+bfs_diff -mindepth 1 \
+ \( -mtime -1 -exec printf -- '-mtime -1: %s\n' {} \; -o -true \) \
+ \( -mtime 1 -exec printf -- '-mtime 1: %s\n' {} \; -o -true \) \
+ \( -mtime +1 -exec printf -- '-mtime +1: %s\n' {} \; -o -true \)
diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out
index 49c1b21..08965bf 100644
--- a/tests/gnu/executable.out
+++ b/tests/gnu/executable.out
@@ -1,4 +1,6 @@
perms
-perms/rwx
-perms/rx
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f-wx
+perms/fr-x
+perms/frwx
diff --git a/tests/gnu/files0_from_file.out b/tests/gnu/files0_from_file.out
index 1d87e6b..0f6b00d 100644
--- a/tests/gnu/files0_from_file.out
+++ b/tests/gnu/files0_from_file.out
@@ -1,3 +1,7 @@
+
+
+
+
/j
/j
@@ -16,6 +20,9 @@
)
)/g
)/g
+*
+*/m
+*/m
,
,/f
,/f
@@ -25,9 +32,14 @@
...
.../h
.../h
+/n
+/n
[
[/k
[/k
\
\/i
\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin.out b/tests/gnu/files0_from_stdin.out
index 1d87e6b..0f6b00d 100644
--- a/tests/gnu/files0_from_stdin.out
+++ b/tests/gnu/files0_from_stdin.out
@@ -1,3 +1,7 @@
+
+
+
+
/j
/j
@@ -16,6 +20,9 @@
)
)/g
)/g
+*
+*/m
+*/m
,
,/f
,/f
@@ -25,9 +32,14 @@
...
.../h
.../h
+/n
+/n
[
[/k
[/k
\
\/i
\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/follow_comma.out b/tests/gnu/follow_comma.out
index 920b3d3..5e4b806 100644
--- a/tests/gnu/follow_comma.out
+++ b/tests/gnu/follow_comma.out
@@ -1,4 +1,7 @@
+
.
+./
+./
./
./ /j
./!
@@ -11,6 +14,8 @@
./(/b
./)
./)/g
+./*
+./*/m
./,
./,/f
./-
@@ -21,3 +26,6 @@
./[/k
./\
./\/i
+./{
+./{/l
+/n
diff --git a/tests/gnu/ignore_readdir_race_loop.out b/tests/gnu/ignore_readdir_race_loop.out
new file mode 100644
index 0000000..a514555
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_loop.out
@@ -0,0 +1,11 @@
+loops
+loops/broken
+loops/deeply
+loops/deeply/nested
+loops/deeply/nested/dir
+loops/file
+loops/notdir
+loops/skip
+loops/skip/dir
+loops/skip/loop
+loops/symlink
diff --git a/tests/gnu/ignore_readdir_race_loop.sh b/tests/gnu/ignore_readdir_race_loop.sh
new file mode 100644
index 0000000..3329169
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_loop.sh
@@ -0,0 +1,2 @@
+# Make sure -ignore_readdir_race doesn't suppress ELOOP from an actual filesystem loop
+! bfs_diff -L loops -ignore_readdir_race
diff --git a/tests/gnu/perm_000_slash.out b/tests/gnu/perm_000_slash.out
index d7494b8..42f2fed 100644
--- a/tests/gnu/perm_000_slash.out
+++ b/tests/gnu/perm_000_slash.out
@@ -1,8 +1,10 @@
perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f---
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out
index 9a5b95a..5c78ecc 100644
--- a/tests/gnu/perm_222_slash.out
+++ b/tests/gnu/perm_222_slash.out
@@ -1,5 +1,6 @@
perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out
index 7e5ae98..774c0ea 100644
--- a/tests/gnu/perm_644_slash.out
+++ b/tests/gnu/perm_644_slash.out
@@ -1,7 +1,9 @@
perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out
index 7e5ae98..774c0ea 100644
--- a/tests/gnu/perm_leading_plus_symbolic_slash.out
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.out
@@ -1,7 +1,9 @@
perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out
index 7e5ae98..774c0ea 100644
--- a/tests/gnu/perm_symbolic_slash.out
+++ b/tests/gnu/perm_symbolic_slash.out
@@ -1,7 +1,9 @@
perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out
index 386feba..285aa43 100644
--- a/tests/gnu/readable.out
+++ b/tests/gnu/readable.out
@@ -1,5 +1,7 @@
perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
+perms/dr-x
+perms/drwx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/regextype_awk.out b/tests/gnu/regextype_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_awk.sh b/tests/gnu/regextype_awk.sh
new file mode 100644
index 0000000..3718473
--- /dev/null
+++ b/tests/gnu/regextype_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype awk -quit || skip
+
+bfs_diff weirdnames -regextype awk -regex '.*/[\[\*]/.*'
diff --git a/tests/gnu/regextype_egrep.out b/tests/gnu/regextype_egrep.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/gnu/regextype_egrep.out
diff --git a/tests/gnu/regextype_egrep.sh b/tests/gnu/regextype_egrep.sh
new file mode 100644
index 0000000..281d9c0
--- /dev/null
+++ b/tests/gnu/regextype_egrep.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype egrep -quit || skip
+
+bfs_diff weirdnames -regextype egrep -regex '*.*/{l'
diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh
index 3cc388c..164d17a 100644
--- a/tests/gnu/regextype_emacs.sh
+++ b/tests/gnu/regextype_emacs.sh
@@ -1,3 +1,3 @@
invoke_bfs -regextype emacs -quit || skip
-bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)'
+bfs_diff basic -regextype emacs -regex '.*/\(?:f+o?o?\|bar\)'
diff --git a/tests/gnu/regextype_findutils_default.out b/tests/gnu/regextype_findutils_default.out
new file mode 100644
index 0000000..709a7ba
--- /dev/null
+++ b/tests/gnu/regextype_findutils_default.out
@@ -0,0 +1,3 @@
+/n
+weirdnames/
+weirdnames/*/m
diff --git a/tests/gnu/regextype_findutils_default.sh b/tests/gnu/regextype_findutils_default.sh
new file mode 100644
index 0000000..c870312
--- /dev/null
+++ b/tests/gnu/regextype_findutils_default.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype findutils-default -quit || skip
+
+bfs_diff weirdnames -regextype findutils-default -regex '.*/./\(m\|n\)'
diff --git a/tests/gnu/regextype_gnu_awk.out b/tests/gnu/regextype_gnu_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_gnu_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_gnu_awk.sh b/tests/gnu/regextype_gnu_awk.sh
new file mode 100644
index 0000000..6b66496
--- /dev/null
+++ b/tests/gnu/regextype_gnu_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype gnu-awk -quit || skip
+
+bfs_diff weirdnames -regextype gnu-awk -regex '.*/[\[\*]/(\<.\>)'
diff --git a/tests/gnu/regextype_posix_awk.out b/tests/gnu/regextype_posix_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_posix_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_posix_awk.sh b/tests/gnu/regextype_posix_awk.sh
new file mode 100644
index 0000000..86377d7
--- /dev/null
+++ b/tests/gnu/regextype_posix_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype posix-awk -quit || skip
+
+bfs_diff weirdnames -regextype posix-awk -regex '.*/[\[\*]/.*'
diff --git a/tests/gnu/regextype_posix_minimal_basic.out b/tests/gnu/regextype_posix_minimal_basic.out
new file mode 100644
index 0000000..0f0971e
--- /dev/null
+++ b/tests/gnu/regextype_posix_minimal_basic.out
@@ -0,0 +1 @@
+./(
diff --git a/tests/gnu/regextype_posix_minimal_basic.sh b/tests/gnu/regextype_posix_minimal_basic.sh
new file mode 100644
index 0000000..ee324f3
--- /dev/null
+++ b/tests/gnu/regextype_posix_minimal_basic.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-minimal-basic -regex '\./\((\)'
diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh
index 5e5d4e9..fe0a778 100644
--- a/tests/gnu/used.sh
+++ b/tests/gnu/used.sh
@@ -1,37 +1,18 @@
-iso8601() {
- printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@"
-}
-
cd "$TEST"
-now=$(date '+%Y-%m-%dT%H:%M:%S')
-
-# Parse the current date
-[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail
-# Treat leading zeros as decimal, not octal
-YYYY=$((10#${BASH_REMATCH[1]}))
-MM=$((10#${BASH_REMATCH[2]}))
-DD=$((10#${BASH_REMATCH[3]}))
-hh=$((10#${BASH_REMATCH[4]}))
-mm=$((10#${BASH_REMATCH[5]}))
-ss=$((10#${BASH_REMATCH[6]}))
+now=$(epoch_time)
# -used is always false if atime < ctime
-yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss)
-"$XTOUCH" -at "$yesterday" yesterday
+"$XTOUCH" -at "@$((now - 60 * 60 * 24))" yesterday
# -used rounds up
-tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss)
-"$XTOUCH" -at "$tomorrow" tomorrow
+"$XTOUCH" -at "@$((now + 60 * 60))" tomorrow
-dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss)
-"$XTOUCH" -at "$dayafter" dayafter
+"$XTOUCH" -at "@$((now + 60 * 60 * 25))" dayafter
-nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss)
-"$XTOUCH" -at "$nextweek" nextweek
+"$XTOUCH" -at "@$((now + 60 * 60 * (24 * 6 + 1)))" nextweek
-nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss)
-"$XTOUCH" -at "$nextyear" nextyear
+"$XTOUCH" -at "@$((now + 60 * 60 * 24 * 365))" nextyear
bfs_diff -mindepth 1 \
-a -used 1 -printf '-used 1: %p\n' \
diff --git a/tests/gnu/writable.out b/tests/gnu/writable.out
index 9a5b95a..5c78ecc 100644
--- a/tests/gnu/writable.out
+++ b/tests/gnu/writable.out
@@ -1,5 +1,6 @@
perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
+perms/drwx
+perms/f-w-
+perms/f-wx
+perms/frw-
+perms/frwx
diff --git a/tests/gnu/xtype_l_loops.out b/tests/gnu/xtype_l_loops.out
new file mode 100644
index 0000000..fdaccab
--- /dev/null
+++ b/tests/gnu/xtype_l_loops.out
@@ -0,0 +1,3 @@
+loops/broken
+loops/loop
+loops/notdir
diff --git a/tests/gnu/xtype_l_loops.sh b/tests/gnu/xtype_l_loops.sh
new file mode 100644
index 0000000..6893134
--- /dev/null
+++ b/tests/gnu/xtype_l_loops.sh
@@ -0,0 +1 @@
+bfs_diff loops -xtype l
diff --git a/tests/list.c b/tests/list.c
new file mode 100644
index 0000000..e14570f
--- /dev/null
+++ b/tests/list.c
@@ -0,0 +1,97 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "list.h"
+
+struct item {
+ int n;
+ struct item *next;
+};
+
+struct list {
+ struct item *head;
+ struct item **tail;
+};
+
+static bool check_list_items(struct list *list, int *array, size_t size) {
+ struct item **cur = &list->head;
+ for (size_t i = 0; i < size; ++i) {
+ if (!bfs_check(*cur != NULL)) {
+ return false;
+ }
+ int n = (*cur)->n;
+ if (!bfs_check(n == array[i], "%d != %d", n, array[i])) {
+ return false;
+ }
+ cur = &(*cur)->next;
+ }
+
+ if (!bfs_check(*cur == NULL)) {
+ return false;
+ }
+ if (!bfs_check(list->tail == cur)) {
+ return false;
+ }
+
+ return true;
+}
+
+#define ARRAY(...) (int[]){ __VA_ARGS__ }, countof((int[]){ __VA_ARGS__ })
+#define EMPTY() NULL, 0
+
+bool check_list(void) {
+ struct list l1;
+ SLIST_INIT(&l1);
+ bfs_verify(check_list_items(&l1, EMPTY()));
+
+ struct list l2;
+ SLIST_INIT(&l2);
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, EMPTY()));
+
+ struct item i10 = { .n = 10 };
+ SLIST_APPEND(&l1, &i10);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ SLIST_SPLICE(&l1, &l1.head, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ struct item i20 = { .n = 20 };
+ SLIST_PREPEND(&l2, &i20);
+ bfs_verify(check_list_items(&l2, ARRAY(20)));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 20)));
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ struct item i15 = { .n = 15 };
+ SLIST_APPEND(&l2, &i15);
+ SLIST_SPLICE(&l1, &i10.next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ SLIST_SPLICE(&l1, &i10.next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ SLIST_SPLICE(&l1, &l1.head, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ struct item i11 = { .n = 11 };
+ struct item i12 = { .n = 12 };
+ SLIST_APPEND(&l2, &i11);
+ SLIST_APPEND(&l2, &i12);
+ SLIST_SPLICE(&l1, &l1.head->next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 11, 12, 15, 20)));
+
+ return true;
+}
diff --git a/tests/main.c b/tests/main.c
index aef0583..bef2e37 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -111,6 +111,7 @@ int main(int argc, char *argv[]) {
run_test(&ctx, "bfstd", check_bfstd);
run_test(&ctx, "bit", check_bit);
run_test(&ctx, "ioq", check_ioq);
+ run_test(&ctx, "list", check_list);
run_test(&ctx, "sighook", check_sighook);
run_test(&ctx, "trie", check_trie);
run_test(&ctx, "xspawn", check_xspawn);
diff --git a/tests/posix/HL.out b/tests/posix/HL.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/posix/HL.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/posix/HL.sh b/tests/posix/HL.sh
new file mode 100644
index 0000000..1858982
--- /dev/null
+++ b/tests/posix/HL.sh
@@ -0,0 +1 @@
+bfs_diff -HL links
diff --git a/tests/posix/LH.out b/tests/posix/LH.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/posix/LH.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/posix/LH.sh b/tests/posix/LH.sh
new file mode 100644
index 0000000..ef1d980
--- /dev/null
+++ b/tests/posix/LH.sh
@@ -0,0 +1 @@
+bfs_diff -LH links/deeply/nested/dir
diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out
index 7ed5f0d..c4f8ce4 100644
--- a/tests/posix/depth_error.out
+++ b/tests/posix/depth_error.out
@@ -1,2 +1,4 @@
-.
-./foo
+inaccessible
+inaccessible/dir
+inaccessible/file
+inaccessible/link
diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh
index db414ba..9b29385 100644
--- a/tests/posix/depth_error.sh
+++ b/tests/posix/depth_error.sh
@@ -1,7 +1 @@
-cd "$TEST"
-"$XTOUCH" -p foo/bar
-
-chmod a-r foo
-defer chmod +r foo
-
-! bfs_diff . -depth
+! bfs_diff inaccessible -depth
diff --git a/tests/posix/perm_000.out b/tests/posix/perm_000.out
index 5fd30bc..b46af62 100644
--- a/tests/posix/perm_000.out
+++ b/tests/posix/perm_000.out
@@ -1 +1 @@
-perms/0
+perms/f---
diff --git a/tests/posix/perm_000_minus.out b/tests/posix/perm_000_minus.out
index d7494b8..42f2fed 100644
--- a/tests/posix/perm_000_minus.out
+++ b/tests/posix/perm_000_minus.out
@@ -1,8 +1,10 @@
perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
+perms/dr-x
+perms/drwx
+perms/f---
+perms/f-w-
+perms/f-wx
+perms/fr--
+perms/fr-x
+perms/frw-
+perms/frwx
diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out
index 1690e43..4876193 100644
--- a/tests/posix/perm_222.out
+++ b/tests/posix/perm_222.out
@@ -1 +1 @@
-perms/w
+perms/f-w-
diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out
index 1690e43..4876193 100644
--- a/tests/posix/perm_222_minus.out
+++ b/tests/posix/perm_222_minus.out
@@ -1 +1 @@
-perms/w
+perms/f-w-
diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out
index 4e64e49..4598cc1 100644
--- a/tests/posix/perm_644.out
+++ b/tests/posix/perm_644.out
@@ -1 +1 @@
-perms/rw
+perms/frw-
diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out
index 2e2576b..9e041c3 100644
--- a/tests/posix/perm_644_minus.out
+++ b/tests/posix/perm_644_minus.out
@@ -1,3 +1,4 @@
perms
-perms/rw
-perms/rwx
+perms/drwx
+perms/frw-
+perms/frwx
diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out
index 2e2576b..9e041c3 100644
--- a/tests/posix/perm_symbolic_minus.out
+++ b/tests/posix/perm_symbolic_minus.out
@@ -1,3 +1,4 @@
perms
-perms/rw
-perms/rwx
+perms/drwx
+perms/frw-
+perms/frwx
diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out
index 4e64e49..4598cc1 100644
--- a/tests/posix/permcopy.out
+++ b/tests/posix/permcopy.out
@@ -1 +1 @@
-perms/rw
+perms/frw-
diff --git a/tests/posix/prune_error.out b/tests/posix/prune_error.out
new file mode 100644
index 0000000..436c48e
--- /dev/null
+++ b/tests/posix/prune_error.out
@@ -0,0 +1 @@
+inaccessible
diff --git a/tests/posix/prune_error.sh b/tests/posix/prune_error.sh
new file mode 100644
index 0000000..07a2523
--- /dev/null
+++ b/tests/posix/prune_error.sh
@@ -0,0 +1 @@
+! bfs_diff -L inaccessible -path '*/*' -prune -o -print
diff --git a/tests/run.sh b/tests/run.sh
index 381b03e..115a036 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -410,14 +410,20 @@ make_xattrs() {
esac
}
+# Get the Unix epoch time in seconds
+epoch_time() {
+ # https://stackoverflow.com/a/12746260/502399
+ awk 'BEGIN { srand(); print srand(); }'
+}
+
## Snapshot testing
# Return value when a difference is detected
EX_DIFF=20
# Detect colored diff support
-if diff --color /dev/null /dev/null &>/dev/null; then
- DIFF="diff --color"
+if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then
+ DIFF="diff --color=always"
else
DIFF="diff"
fi
diff --git a/tests/stddirs.sh b/tests/stddirs.sh
index e08e6bf..b3cd521 100644
--- a/tests/stddirs.sh
+++ b/tests/stddirs.sh
@@ -14,13 +14,13 @@ make_basic() {
# Creates a file+directory structure with various permissions for tests
make_perms() {
- "$XTOUCH" -p -M000 "$1/0"
- "$XTOUCH" -p -M444 "$1/r"
- "$XTOUCH" -p -M222 "$1/w"
- "$XTOUCH" -p -M644 "$1/rw"
- "$XTOUCH" -p -M555 "$1/rx"
- "$XTOUCH" -p -M311 "$1/wx"
- "$XTOUCH" -p -M755 "$1/rwx"
+ "$XTOUCH" -p -M000 "$1/f---"
+ "$XTOUCH" -p -M444 "$1/fr--"
+ "$XTOUCH" -p -M222 "$1/f-w-"
+ "$XTOUCH" -p -M644 "$1/frw-"
+ "$XTOUCH" -p -M311 "$1/f-wx"
+ "$XTOUCH" -p -M555 "$1/fr-x" "$1/dr-x/"
+ "$XTOUCH" -p -M755 "$1/frwx" "$1/drwx/"
}
# Creates a file+directory structure with various symbolic and hard links
@@ -48,6 +48,12 @@ make_loops() {
ln -s deeply/nested/loop/nested "$1/skip"
}
+# Creates a file+directory structure with inaccessible files
+make_inaccessible() {
+ "$XTOUCH" -p -M000 "$1/file" "$1/dir/"
+ ln -s dir/file "$1/link"
+}
+
# Creates a file+directory structure with varying timestamps
make_times() {
"$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
@@ -71,6 +77,9 @@ make_weirdnames() {
"$XTOUCH" -p "$1/\\/i"
"$XTOUCH" -p "$1/ /j"
"$XTOUCH" -p "$1/[/k"
+ "$XTOUCH" -p "$1/{/l"
+ "$XTOUCH" -p "$1/*/m"
+ "$XTOUCH" -p "$1/"$'\n/n'
}
# Creates a very deep directory structure for testing PATH_MAX handling
@@ -133,6 +142,7 @@ make_stddirs() {
make_perms "$TMP/perms"
make_links "$TMP/links"
make_loops "$TMP/loops"
+ make_inaccessible "$TMP/inaccessible"
make_times "$TMP/times"
make_weirdnames "$TMP/weirdnames"
make_deep "$TMP/deep"
@@ -148,5 +158,6 @@ clean_stddirs() {
fi
done
+ chmod -R +rwX "$TMP"
rm -rf "$TMP"
}
diff --git a/tests/tests.h b/tests/tests.h
index de95a49..2958fe1 100644
--- a/tests/tests.h
+++ b/tests/tests.h
@@ -27,6 +27,9 @@ bool check_bit(void);
/** I/O queue tests. */
bool check_ioq(void);
+/** Linked list tests. */
+bool check_list(void);
+
/** Signal hook tests. */
bool check_sighook(void);
diff --git a/tests/trie.c b/tests/trie.c
index 4667322..ebaae5d 100644
--- a/tests/trie.c
+++ b/tests/trie.c
@@ -8,7 +8,7 @@
#include <stdlib.h>
#include <string.h>
-const char *keys[] = {
+static const char *keys[] = {
"foo",
"bar",
"baz",
@@ -37,7 +37,7 @@ const char *keys[] = {
">>>",
};
-const size_t nkeys = countof(keys);
+static const size_t nkeys = countof(keys);
bool check_trie(void) {
bool ret = true;
diff --git a/tests/xspawn.c b/tests/xspawn.c
index b1d6dc1..f48e220 100644
--- a/tests/xspawn.c
+++ b/tests/xspawn.c
@@ -179,6 +179,8 @@ static bool check_resolve(void) {
ret &= bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+ ret &= bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT);
+
return ret;
}