summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/codeql.yml9
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/ci.yml108
-rw-r--r--.github/workflows/codecov.yml6
-rw-r--r--.github/workflows/codeql.yml6
-rw-r--r--.gitignore2
-rw-r--r--GNUmakefile379
-rw-r--r--Makefile231
-rw-r--r--README.md10
-rw-r--r--bench/bench.sh10
-rwxr-xr-xbuild/cc.sh16
-rw-r--r--build/config.mk62
-rwxr-xr-xbuild/define-if.sh20
-rw-r--r--build/deps.mk18
-rw-r--r--build/empty.c6
-rw-r--r--build/exports.mk20
-rw-r--r--build/flags.mk136
-rw-r--r--build/has/acl-get-entry.c8
-rw-r--r--build/has/acl-get-file.c8
-rw-r--r--build/has/acl-get-tag-type.c10
-rw-r--r--build/has/acl-is-trivial-np.c12
-rw-r--r--build/has/acl-trivial.c8
-rw-r--r--build/has/aligned-alloc.c8
-rw-r--r--build/has/confstr.c9
-rw-r--r--build/has/extattr-get-file.c10
-rw-r--r--build/has/extattr-get-link.c10
-rw-r--r--build/has/extattr-list-file.c10
-rw-r--r--build/has/extattr-list-link.c10
-rw-r--r--build/has/fdclosedir.c8
-rw-r--r--build/has/getdents.c9
-rw-r--r--build/has/getdents64-syscall.c11
-rw-r--r--build/has/getdents64.c9
-rw-r--r--build/has/getmntent-1.c9
-rw-r--r--build/has/getmntent-2.c10
-rw-r--r--build/has/getmntinfo.c10
-rw-r--r--build/has/getprogname-gnu.c9
-rw-r--r--build/has/getprogname.c9
-rw-r--r--build/has/max-align-t.c8
-rw-r--r--build/has/pipe2.c10
-rw-r--r--build/has/posix-spawn-addfchdir-np.c11
-rw-r--r--build/has/posix-spawn-addfchdir.c11
-rw-r--r--build/has/st-acmtim.c12
-rw-r--r--build/has/st-acmtimespec.c12
-rw-r--r--build/has/st-birthtim.c9
-rw-r--r--build/has/st-birthtimespec.c9
-rw-r--r--build/has/st-flags.c9
-rw-r--r--build/has/statx-syscall.c13
-rw-r--r--build/has/statx.c11
-rw-r--r--build/has/strerror-l.c11
-rw-r--r--build/has/strerror-r-gnu.c11
-rw-r--r--build/has/strerror-r-posix.c11
-rw-r--r--build/has/string-to-flags.c9
-rw-r--r--build/has/strtofflags.c9
-rw-r--r--build/has/timegm.c9
-rw-r--r--build/has/tm-gmtoff.c9
-rw-r--r--build/has/uselocale.c9
-rw-r--r--build/header.mk72
-rwxr-xr-xbuild/msg-if.sh21
-rwxr-xr-xbuild/msg.sh62
-rwxr-xr-xbuild/pkgconf.sh100
-rw-r--r--build/pkgs.mk33
-rw-r--r--build/prelude.mk122
-rw-r--r--build/use/libacl.c9
-rw-r--r--build/use/libcap.c9
-rw-r--r--build/use/libselinux.c9
-rw-r--r--build/use/liburing.c9
-rw-r--r--build/use/oniguruma.c9
-rw-r--r--completions/bfs.bash1
-rw-r--r--completions/bfs.fish1
-rw-r--r--completions/bfs.zsh1
-rwxr-xr-xconfigure137
-rw-r--r--docs/BUILDING.md212
-rw-r--r--docs/CHANGELOG.md30
-rw-r--r--docs/USAGE.md2
-rw-r--r--docs/bfs.14
-rw-r--r--src/alloc.c8
-rw-r--r--src/alloc.h2
-rw-r--r--src/bar.c2
-rw-r--r--src/bfstd.c146
-rw-r--r--src/bfstd.h22
-rw-r--r--src/bftw.c2
-rw-r--r--src/bftw.h4
-rw-r--r--src/bit.h2
-rw-r--r--src/color.c2
-rw-r--r--src/color.h2
-rw-r--r--src/ctx.c2
-rw-r--r--src/ctx.h2
-rw-r--r--src/diag.c2
-rw-r--r--src/diag.h2
-rw-r--r--src/dir.c18
-rw-r--r--src/dir.h7
-rw-r--r--src/dstring.c3
-rw-r--r--src/dstring.h2
-rw-r--r--src/eval.c85
-rw-r--r--src/eval.h3
-rw-r--r--src/exec.c2
-rw-r--r--src/expr.h2
-rw-r--r--src/fsade.c164
-rw-r--r--src/fsade.h28
-rw-r--r--src/ioq.c7
-rw-r--r--src/ioq.h2
-rw-r--r--src/main.c4
-rw-r--r--src/mtab.c54
-rw-r--r--src/mtab.h2
-rw-r--r--src/opt.c8
-rw-r--r--src/parse.c120
-rw-r--r--src/prelude.h (renamed from src/config.h)76
-rw-r--r--src/printf.c141
-rw-r--r--src/pwcache.c2
-rw-r--r--src/sanity.h2
-rw-r--r--src/stat.c53
-rw-r--r--src/stat.h10
-rw-r--r--src/thread.c2
-rw-r--r--src/thread.h2
-rw-r--r--src/trie.c2
-rw-r--r--src/xregex.c2
-rw-r--r--src/xspawn.c40
-rw-r--r--src/xspawn.h2
-rw-r--r--src/xtime.c40
-rw-r--r--tests/alloc.c6
-rw-r--r--tests/bfstd.c6
-rw-r--r--tests/bit.c6
-rw-r--r--tests/bsd/sparse.out1
-rw-r--r--tests/bsd/sparse.sh12
-rw-r--r--tests/common/delete_error.out8
-rw-r--r--tests/common/delete_error.sh9
-rw-r--r--tests/common/empty.out (renamed from tests/gnu/empty.out)0
-rw-r--r--tests/common/empty.sh (renamed from tests/gnu/empty.sh)0
-rw-r--r--tests/common/empty_error.out3
-rw-r--r--tests/common/empty_error.sh7
-rw-r--r--tests/common/empty_special.out (renamed from tests/gnu/empty_special.out)0
-rw-r--r--tests/common/empty_special.sh (renamed from tests/gnu/empty_special.sh)0
-rw-r--r--tests/gnu/newer_link.out (renamed from tests/posix/newer_link.out)0
-rw-r--r--tests/gnu/newer_link.sh (renamed from tests/posix/newer_link.sh)0
-rw-r--r--tests/gnu/used.out4
-rw-r--r--tests/gnu/used.sh40
-rw-r--r--tests/ioq.c10
-rw-r--r--tests/main.c6
-rw-r--r--tests/mksock.c2
-rw-r--r--tests/posix/newer_broken.out1
-rw-r--r--tests/posix/newer_broken.sh4
-rw-r--r--tests/run.sh6
-rw-r--r--tests/tests.h4
-rw-r--r--tests/tests.mk10
-rw-r--r--tests/trie.c6
-rw-r--r--tests/util.sh9
-rw-r--r--tests/xspawn.c48
-rw-r--r--tests/xtime.c20
-rw-r--r--tests/xtouch.c12
149 files changed, 2556 insertions, 1037 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml
new file mode 100644
index 0000000..6ff8337
--- /dev/null
+++ b/.github/codeql.yml
@@ -0,0 +1,9 @@
+query-filters:
+ - exclude:
+ id: cpp/commented-out-code
+ - exclude:
+ id: cpp/long-switch
+ - exclude:
+ id: cpp/loop-variable-changed
+ - exclude:
+ id: cpp/poorly-documented-function
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4c1c89c..1d4196e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,8 +23,6 @@ jobs:
libacl1-dev \
libacl1:i386 \
attr \
- libattr1-dev \
- libattr1:i386 \
libcap2-bin \
libcap-dev \
libcap2:i386 \
@@ -34,7 +32,6 @@ jobs:
# Ubuntu doesn't let you install the -dev packages for both amd64 and
# i386 at once, so we make our own symlinks to fix -m32 -lacl -l...
sudo ln -s libacl.so.1 /lib/i386-linux-gnu/libacl.so
- sudo ln -s libattr.so.1 /lib/i386-linux-gnu/libattr.so
sudo ln -s libcap.so.2 /lib/i386-linux-gnu/libcap.so
sudo ln -s libonig.so.5 /lib/i386-linux-gnu/libonig.so
# Work around https://github.com/actions/runner-images/issues/9491
@@ -44,6 +41,11 @@ jobs:
run: |
make -j$(nproc) distcheck
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-config.log
+ path: distcheck-*/gen/config.log
+
macos:
name: macOS
@@ -72,27 +74,25 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/freebsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "14.0"
- usesh: true
- copyback: false
+ operating_system: freebsd
+ version: "14.0"
- prepare: |
- pkg install -y \
+ run: |
+ sudo pkg install -y \
bash \
expect \
- gmake \
oniguruma \
- sudo \
+ pkgconf \
tcl-wrapper
- pw useradd -n action -m -G wheel -s /usr/local/bin/bash
- echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers
- mount -t fdescfs none /dev/fd
+ sudo mount -t fdescfs none /dev/fd
+ make -j$(nproc) distcheck
- run: |
- chown -R action:action .
- sudo -u action gmake -j$(nproc) distcheck
+ - uses: actions/upload-artifact@v4
+ with:
+ name: freebsd-config.log
+ path: distcheck-*/gen/config.log
openbsd:
name: OpenBSD
@@ -103,26 +103,25 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/openbsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "7.4"
- usesh: true
- copyback: false
+ operating_system: openbsd
+ version: "7.5"
- prepare: |
- pkg_add \
+ run: |
+ sudo pkg_add \
bash \
expect \
gmake \
oniguruma
- adduser -group USER -batch action wheel </dev/null
- cp /etc/examples/doas.conf /etc/doas.conf
- echo "permit nopass keepenv :wheel" >>/etc/doas.conf
-
- run: |
- chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- doas -u action gmake -j$jobs check TEST_FLAGS="--sudo=doas --verbose=skipped"
+ ./configure MAKE=gmake
+ gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: openbsd-config.log
+ path: gen/config.log
netbsd:
name: NetBSD
@@ -133,29 +132,26 @@ jobs:
- uses: actions/checkout@v4
- name: Run tests
- uses: vmactions/netbsd-vm@v1
+ uses: cross-platform-actions/action@v0.24.0
with:
- release: "9.3"
- usesh: true
- copyback: false
+ operating_system: netbsd
+ version: "10.0"
- prepare: |
+ run: |
PATH="/sbin:/usr/sbin:$PATH"
- pkg_add \
+ sudo pkgin -y install \
bash \
- clang \
- gmake \
oniguruma \
- sudo \
+ pkgconf \
tcl-expect
- useradd -m -G wheel -g =uid action
- echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/pkg/etc/sudoers
-
- run: |
- PATH="/sbin:/usr/sbin:$PATH"
- chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- sudo -u action gmake -j$jobs check CC=clang LDFLAGS="-rpath /usr/pkg/lib" TEST_FLAGS="--sudo --verbose=skipped"
+ ./configure
+ make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: netbsd-config.log
+ path: gen/config.log
dragonflybsd:
name: DragonFly BSD
@@ -170,14 +166,13 @@ jobs:
with:
release: "6.4.0"
usesh: true
- copyback: false
prepare: |
pkg install -y \
bash \
expect \
- gmake \
oniguruma \
+ pkgconf \
sudo \
tcl-wrapper
pw useradd -n action -m -G wheel -s /usr/local/bin/bash
@@ -186,7 +181,13 @@ jobs:
run: |
chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- sudo -u action gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+ sudo -u action ./configure
+ sudo -u action make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dragonfly-config.log
+ path: gen/config.log
omnios:
name: OmniOS
@@ -201,7 +202,6 @@ jobs:
with:
release: "r151048"
usesh: true
- copyback: false
prepare: |
pkg install \
@@ -218,4 +218,10 @@ jobs:
PATH="/usr/xpg4/bin:$PATH"
chown -R action:staff .
jobs=$(getconf NPROCESSORS_ONLN)
- sudo -u action gmake -j$jobs check LDFLAGS="-Wl,-rpath,/opt/ooce/lib/amd64" TEST_FLAGS="--sudo --verbose=skipped"
+ sudo -u action ./configure MAKE=gmake
+ sudo -u action gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: omnios-config.log
+ path: gen/config.log
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 9c2119b..6aaace6 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -18,7 +18,6 @@ jobs:
acl \
libacl1-dev \
attr \
- libattr1-dev \
libcap2-bin \
libcap-dev \
libonig-dev \
@@ -26,10 +25,11 @@ jobs:
- name: Generate coverage
run: |
- make -j$(nproc) gcov check TEST_FLAGS="--sudo"
+ ./configure --enable-gcov
+ make -j$(nproc) check TEST_FLAGS="--sudo"
gcov -abcfpu obj/*/*.o
- - uses: codecov/codecov-action@v3
+ - uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 71073c6..a0b8fe3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -34,17 +34,21 @@ jobs:
acl \
libacl1-dev \
attr \
- libattr1-dev \
libcap2-bin \
libcap-dev \
libonig-dev \
liburing-dev
+ - name: Configure
+ run: |
+ ./configure
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: cpp
queries: +security-and-quality
+ config-file: .github/codeql.yml
- name: Build
run: |
diff --git a/.gitignore b/.gitignore
index 4ded7c4..84e47fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/bin/
+/gen/
/obj/
+/distcheck-*/
diff --git a/GNUmakefile b/GNUmakefile
deleted file mode 100644
index 080fb17..0000000
--- a/GNUmakefile
+++ /dev/null
@@ -1,379 +0,0 @@
-# Copyright © Tavian Barnes <tavianator@tavianator.com>
-# SPDX-License-Identifier: 0BSD
-
-ifneq ($(wildcard .git),)
-VERSION := $(shell git describe --always 2>/dev/null)
-endif
-
-ifndef VERSION
-VERSION := 3.1.3
-endif
-
-ifndef OS
-OS := $(shell uname)
-endif
-
-ifndef ARCH
-ARCH := $(shell uname -m)
-endif
-
-CC ?= gcc
-INSTALL ?= install
-MKDIR ?= mkdir -p
-RM ?= rm -f
-
-export BUILDDIR ?= .
-DESTDIR ?=
-PREFIX ?= /usr
-MANDIR ?= $(PREFIX)/share/man
-
-BIN := $(BUILDDIR)/bin
-OBJ := $(BUILDDIR)/obj
-
-DEFAULT_CFLAGS := \
- -g \
- -Wall \
- -Wformat=2 \
- -Werror=implicit \
- -Wimplicit-fallthrough \
- -Wmissing-declarations \
- -Wshadow \
- -Wsign-compare \
- -Wstrict-prototypes
-
-CFLAGS ?= $(DEFAULT_CFLAGS)
-LDFLAGS ?=
-DEPFLAGS ?= -MD -MP -MF $(@:.o=.d)
-
-LOCAL_CPPFLAGS := \
- -D__EXTENSIONS__ \
- -D_ATFILE_SOURCE \
- -D_BSD_SOURCE \
- -D_DARWIN_C_SOURCE \
- -D_DEFAULT_SOURCE \
- -D_GNU_SOURCE \
- -D_LARGEFILE64_SOURCE \
- -D_POSIX_PTHREAD_SEMANTICS \
- -D_FILE_OFFSET_BITS=64 \
- -D_TIME_BITS=64 \
- -DBFS_VERSION=\"$(VERSION)\"
-
-LOCAL_CFLAGS := -std=c17 -pthread
-LOCAL_LDFLAGS :=
-LOCAL_LDLIBS :=
-
-ASAN := $(filter asan,$(MAKECMDGOALS))
-LSAN := $(filter lsan,$(MAKECMDGOALS))
-MSAN := $(filter msan,$(MAKECMDGOALS))
-TSAN := $(filter tsan,$(MAKECMDGOALS))
-UBSAN := $(filter ubsan,$(MAKECMDGOALS))
-
-ifdef ASAN
-LOCAL_CFLAGS += -fsanitize=address
-SANITIZE := y
-endif
-
-ifdef LSAN
-LOCAL_CFLAGS += -fsanitize=leak
-SANITIZE := y
-endif
-
-ifdef MSAN
-# msan needs all code instrumented
-NOLIBS := y
-LOCAL_CFLAGS += -fsanitize=memory -fsanitize-memory-track-origins
-SANITIZE := y
-endif
-
-ifdef TSAN
-# tsan needs all code instrumented
-NOLIBS := y
-# https://github.com/google/sanitizers/issues/342
-LOCAL_CPPFLAGS += -DBFS_USE_TARGET_CLONES=0
-LOCAL_CFLAGS += -fsanitize=thread
-SANITIZE := y
-endif
-
-ifdef UBSAN
-LOCAL_CFLAGS += -fsanitize=undefined
-SANITIZE := y
-endif
-
-ifdef SANITIZE
-LOCAL_CFLAGS += -fno-sanitize-recover=all
-endif
-
-ifndef NOLIBS
-USE_ONIGURUMA := y
-endif
-
-ifdef USE_ONIGURUMA
-LOCAL_CPPFLAGS += -DBFS_USE_ONIGURUMA=1
-
-ONIG_CONFIG := $(shell command -v onig-config 2>/dev/null)
-ifdef ONIG_CONFIG
-ONIG_CFLAGS := $(shell $(ONIG_CONFIG) --cflags)
-ONIG_LDLIBS := $(shell $(ONIG_CONFIG) --libs)
-else
-ONIG_LDLIBS := -lonig
-endif
-
-LOCAL_CFLAGS += $(ONIG_CFLAGS)
-LOCAL_LDLIBS += $(ONIG_LDLIBS)
-endif # USE_ONIGURUMA
-
-ifeq ($(OS),Linux)
-ifndef NOLIBS
-USE_ACL := y
-USE_ATTR := y
-USE_LIBCAP := y
-USE_LIBURING := y
-endif
-
-ifdef USE_ACL
-LOCAL_LDLIBS += -lacl
-else
-LOCAL_CPPFLAGS += -DBFS_USE_SYS_ACL_H=0
-endif
-
-ifdef USE_ATTR
-LOCAL_LDLIBS += -lattr
-else
-LOCAL_CPPFLAGS += -DBFS_USE_SYS_XATTR_H=0
-endif
-
-ifdef USE_LIBCAP
-LOCAL_LDLIBS += -lcap
-else
-LOCAL_CPPFLAGS += -DBFS_USE_SYS_CAPABILITY_H=0
-endif
-
-ifdef USE_LIBURING
-LOCAL_CPPFLAGS += -DBFS_USE_LIBURING=1
-LOCAL_LDLIBS += -luring
-endif
-
-LOCAL_LDFLAGS += -Wl,--as-needed
-LOCAL_LDLIBS += -lrt
-endif # Linux
-
-ifeq ($(OS),NetBSD)
-LOCAL_LDLIBS += -lutil
-endif
-
-ifeq ($(OS),DragonFly)
-LOCAL_LDLIBS += -lposix1e
-endif
-
-ifeq ($(OS),SunOS)
-LOCAL_LDLIBS += -lsocket -lnsl
-endif
-
-ifneq ($(filter gcov,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += --coverage
-# gcov only intercepts fork()/exec() with -std=gnu*
-LOCAL_CFLAGS := $(patsubst -std=c%,-std=gnu%,$(LOCAL_CFLAGS))
-endif
-
-ifneq ($(filter lint,$(MAKECMDGOALS)),)
-LOCAL_CPPFLAGS += \
- -D_FORTIFY_SOURCE=3 \
- -DBFS_LINT
-LOCAL_CFLAGS += -Werror -O2
-endif
-
-ifneq ($(filter release,$(MAKECMDGOALS)),)
-LOCAL_CPPFLAGS += -DNDEBUG
-CFLAGS := $(DEFAULT_CFLAGS) -O3 -flto=auto
-endif
-
-ALL_CPPFLAGS = $(LOCAL_CPPFLAGS) $(CPPFLAGS) $(EXTRA_CPPFLAGS)
-ALL_CFLAGS = $(ALL_CPPFLAGS) $(LOCAL_CFLAGS) $(CFLAGS) $(EXTRA_CFLAGS) $(DEPFLAGS)
-ALL_LDFLAGS = $(ALL_CFLAGS) $(LOCAL_LDFLAGS) $(LDFLAGS) $(EXTRA_LDFLAGS)
-ALL_LDLIBS = $(LOCAL_LDLIBS) $(LDLIBS) $(EXTRA_LDLIBS)
-
-# Default make target
-bfs: $(BIN)/bfs
-.PHONY: bfs
-
-# Goals that are treated like flags by this makefile
-FLAG_GOALS := asan lsan msan tsan ubsan gcov lint release
-
-# These are the remaining non-flag goals
-GOALS := $(filter-out $(FLAG_GOALS),$(MAKECMDGOALS))
-
-# Build the default goal if only flag goals are specified
-FLAG_PREREQS :=
-ifndef GOALS
-FLAG_PREREQS += bfs
-endif
-
-# Make sure that "make release" builds everything, but "make release obj/src/main.o" doesn't
-$(FLAG_GOALS): $(FLAG_PREREQS)
- @:
-.PHONY: $(FLAG_GOALS)
-
-all: bfs tests
-.PHONY: all
-
-$(BIN)/%:
- @$(MKDIR) $(@D)
- +$(CC) $(ALL_LDFLAGS) $^ $(ALL_LDLIBS) -o $@
-ifeq ($(OS) $(SANITIZE),FreeBSD y)
- elfctl -e +noaslr $@
-endif
-
-$(OBJ)/%.o: %.c $(OBJ)/FLAGS
- @$(MKDIR) $(@D)
- $(CC) $(ALL_CFLAGS) -c $< -o $@
-
-# Save the full set of flags to rebuild everything when they change
-$(OBJ)/FLAGS.new:
- @$(MKDIR) $(@D)
- @echo $(CC) : $(ALL_CFLAGS) : $(ALL_LDFLAGS) : $(ALL_LDLIBS) >$@
-.PHONY: $(OBJ)/FLAGS.new
-
-# Only update obj/FLAGS if obj/FLAGS.new is different
-$(OBJ)/FLAGS: $(OBJ)/FLAGS.new
- @test -e $@ && cmp -s $@ $< && rm $< || mv $< $@
-
-# All object files except the entry point
-LIBBFS := \
- $(OBJ)/src/alloc.o \
- $(OBJ)/src/bar.o \
- $(OBJ)/src/bfstd.o \
- $(OBJ)/src/bftw.o \
- $(OBJ)/src/color.o \
- $(OBJ)/src/ctx.o \
- $(OBJ)/src/diag.o \
- $(OBJ)/src/dir.o \
- $(OBJ)/src/dstring.o \
- $(OBJ)/src/eval.o \
- $(OBJ)/src/exec.o \
- $(OBJ)/src/expr.o \
- $(OBJ)/src/fsade.o \
- $(OBJ)/src/ioq.o \
- $(OBJ)/src/mtab.o \
- $(OBJ)/src/opt.o \
- $(OBJ)/src/parse.o \
- $(OBJ)/src/printf.o \
- $(OBJ)/src/pwcache.o \
- $(OBJ)/src/stat.o \
- $(OBJ)/src/thread.o \
- $(OBJ)/src/trie.o \
- $(OBJ)/src/typo.o \
- $(OBJ)/src/xregex.o \
- $(OBJ)/src/xspawn.o \
- $(OBJ)/src/xtime.o
-
-# The main executable
-$(BIN)/bfs: $(OBJ)/src/main.o $(LIBBFS)
-
-# Testing utilities
-TEST_UTILS := \
- $(BIN)/tests/mksock \
- $(BIN)/tests/xspawnee \
- $(BIN)/tests/xtouch
-
-$(BIN)/tests/mksock: $(OBJ)/tests/mksock.o $(LIBBFS)
-
-$(BIN)/tests/xspawnee: $(OBJ)/tests/xspawnee.o
-
-$(BIN)/tests/xtouch: $(OBJ)/tests/xtouch.o $(LIBBFS)
-
-# All test binaries
-TESTS := $(BIN)/tests/units $(TEST_UTILS)
-
-$(BIN)/tests/units: \
- $(OBJ)/tests/alloc.o \
- $(OBJ)/tests/bfstd.o \
- $(OBJ)/tests/bit.o \
- $(OBJ)/tests/ioq.o \
- $(OBJ)/tests/main.o \
- $(OBJ)/tests/trie.o \
- $(OBJ)/tests/xspawn.o \
- $(OBJ)/tests/xtime.o \
- $(LIBBFS)
-
-# Build all the test binaries
-tests: $(TESTS)
-.PHONY: tests
-
-# Run the unit tests
-unit-tests: $(BIN)/tests/units $(BIN)/tests/xspawnee
- $<
-.PHONY: unit-tests
-
-# The different flag combinations we check
-INTEGRATIONS := default dfs ids eds j1 j2 j3 s
-INTEGRATION_TESTS := $(INTEGRATIONS:%=check-%)
-
-check-default: $(BIN)/bfs $(TEST_UTILS)
- +./tests/tests.sh --make="$(MAKE)" --bfs="$<" $(TEST_FLAGS)
-
-check-dfs check-ids check-eds: check-%: $(BIN)/bfs $(TEST_UTILS)
- +./tests/tests.sh --make="$(MAKE)" --bfs="$< -S $*" $(TEST_FLAGS)
-
-check-j1 check-j2 check-j3 check-s: check-%: $(BIN)/bfs $(TEST_UTILS)
- +./tests/tests.sh --make="$(MAKE)" --bfs="$< -$*" $(TEST_FLAGS)
-
-# Run the integration tests
-integration-tests: $(INTEGRATION_TESTS)
-.PHONY: integration-tests
-
-# Run all the tests
-check: unit-tests integration-tests
-.PHONY: check
-
-# Custom test flags for distcheck
-DISTCHECK_FLAGS := -s TEST_FLAGS="--sudo --verbose=skipped"
-
-distcheck:
- +$(MAKE) -B asan ubsan check $(DISTCHECK_FLAGS)
-ifneq ($(OS),Darwin)
- +$(MAKE) -B msan ubsan check CC=clang $(DISTCHECK_FLAGS)
-endif
- +$(MAKE) -B tsan ubsan check CC=clang $(DISTCHECK_FLAGS)
-ifeq ($(OS) $(ARCH),Linux x86_64)
- +$(MAKE) -B check EXTRA_CFLAGS="-m32" ONIG_CONFIG= USE_LIBURING= $(DISTCHECK_FLAGS)
-endif
- +$(MAKE) -B release check $(DISTCHECK_FLAGS)
- +$(MAKE) -B check $(DISTCHECK_FLAGS)
- +$(MAKE) check-install $(DISTCHECK_FLAGS)
-.PHONY: distcheck
-
-clean:
- $(RM) -r $(BIN) $(OBJ)
-.PHONY: clean
-
-install:
- $(MKDIR) $(DESTDIR)$(PREFIX)/bin
- $(INSTALL) -m755 $(BIN)/bfs $(DESTDIR)$(PREFIX)/bin/bfs
- $(MKDIR) $(DESTDIR)$(MANDIR)/man1
- $(INSTALL) -m644 docs/bfs.1 $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/bash-completion/completions
- $(INSTALL) -m644 completions/bfs.bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/zsh/site-functions
- $(INSTALL) -m644 completions/bfs.zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_bfs
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d
- $(INSTALL) -m644 completions/bfs.fish $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/bfs.fish
-.PHONY: install
-
-uninstall:
- $(RM) $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
- $(RM) $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_bfs
- $(RM) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/bfs.fish
- $(RM) $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(RM) $(DESTDIR)$(PREFIX)/bin/bfs
-.PHONY: uninstall
-
-check-install:
- +$(MAKE) install DESTDIR=$(BUILDDIR)/pkg
- +$(MAKE) uninstall DESTDIR=$(BUILDDIR)/pkg
- $(BIN)/bfs $(BUILDDIR)/pkg -not -type d -print -exit 1
- $(RM) -r $(BUILDDIR)/pkg
-.PHONY: check-install
-
-.SUFFIXES:
-
--include $(wildcard $(OBJ)/*/*.d)
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..69048cf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,231 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# To build bfs, run
+#
+# $ ./configure
+# $ make
+
+# Utilities and GNU/BSD portability
+include build/prelude.mk
+
+# The default build target
+default: bfs
+.PHONY: default
+
+# Include the generated build config, if it exists
+-include gen/config.mk
+
+## Configuration phase (`./configure`)
+
+# bfs used to have flag-like targets (`make release`, `make asan ubsan`, etc.).
+# Direct users to the new configuration system.
+asan lsan msan tsan ubsan gcov lint release::
+ @printf 'error: `%s %s` is no longer supported. Use `./configure --enable-%s` instead.\n' \
+ "${MAKE}" $@ $@ >&2
+ @false
+
+# Print an error if `make` is run before `./configure`
+gen/config.mk::
+ @if ! [ -e $@ ]; then \
+ printf 'error: You must run `./configure` before `%s`.\n' "${MAKE}" >&2; \
+ false; \
+ fi
+
+## Build phase (`make`)
+
+# The main binary
+bfs: bin/bfs
+.PHONY: bfs
+
+# All binaries
+BINS := \
+ bin/bfs \
+ bin/tests/mksock \
+ bin/tests/units \
+ bin/tests/xspawnee \
+ bin/tests/xtouch
+
+all: ${BINS}
+.PHONY: all
+
+# The main binary
+bin/bfs: ${LIBBFS} obj/src/main.o
+
+${BINS}:
+ @${MKDIR} ${@D}
+ +${MSG} "[ LD ] $@" ${CC} ${CFLAGS} ${LDFLAGS} ${.ALLSRC} ${LDLIBS} -o $@
+ ${POSTLINK}
+
+# Get the .c file for a .o file
+CSRC = ${@:obj/%.o=%.c}
+
+# Rebuild when the configuration changes
+${OBJS}: gen/config.mk
+ @${MKDIR} ${@D}
+ ${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::
+ @${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' >>$@
+
+gen/version.c: gen/version.c.new
+ @test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@
+
+obj/gen/version.o: gen/version.c
+
+## Test phase (`make check`)
+
+# Unit test binaries
+UTEST_BINS := \
+ bin/tests/units \
+ bin/tests/xspawnee
+
+# Integration test binaries
+ITEST_BINS := \
+ bin/tests/mksock \
+ bin/tests/xtouch
+
+# Build (but don't run) test binaries
+tests: ${UTEST_BINS} ${ITEST_BINS}
+.PHONY: tests
+
+# Run all the tests
+check: unit-tests integration-tests
+.PHONY: check
+
+# Run the unit tests
+unit-tests: ${UTEST_BINS}
+ ${MSG} "[TEST] tests/units" bin/tests/units
+.PHONY: unit-tests
+
+bin/tests/units: \
+ ${UNIT_OBJS} \
+ ${LIBBFS}
+
+bin/tests/xspawnee: \
+ obj/tests/xspawnee.o
+
+# The different flag combinations we check
+INTEGRATIONS := default dfs ids eds j1 j2 j3 s
+INTEGRATION_TESTS := ${INTEGRATIONS:%=check-%}
+
+# Check just `bfs`
+check-default: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs" ${TEST_FLAGS}
+
+# Check the different search strategies
+check-dfs check-ids check-eds: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs -S ${@:check-%=%}" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -S ${@:check-%=%}" ${TEST_FLAGS}
+
+# Check various flags
+check-j1 check-j2 check-j3 check-s: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs -${@:check-%=%}" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -${@:check-%=%}" ${TEST_FLAGS}
+
+# Run the integration tests
+integration-tests: ${INTEGRATION_TESTS}
+.PHONY: integration-tests
+
+bin/tests/mksock: \
+ obj/tests/mksock.o \
+ ${LIBBFS}
+
+bin/tests/xtouch: \
+ obj/tests/xtouch.o \
+ ${LIBBFS}
+
+# `make distcheck` configurations
+DISTCHECKS := \
+ distcheck-asan \
+ distcheck-msan \
+ distcheck-tsan \
+ distcheck-m32 \
+ distcheck-release
+
+# Test multiple configurations
+distcheck:
+ @+${MAKE} distcheck-asan
+ @+test "$$(uname)" = Darwin || ${MAKE} distcheck-msan
+ @+${MAKE} distcheck-tsan
+ @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32
+ @+${MAKE} distcheck-release
+.PHONY: distcheck
+
+# Per-distcheck configuration
+DISTCHECK_CONFIG_asan := --enable-asan --enable-ubsan
+DISTCHECK_CONFIG_msan := --enable-msan --enable-ubsan CC=clang
+DISTCHECK_CONFIG_tsan := --enable-tsan --enable-ubsan CC=clang
+DISTCHECK_CONFIG_m32 := EXTRA_CFLAGS="-m32" PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig
+DISTCHECK_CONFIG_release := --enable-release
+
+${DISTCHECKS}::
+ @${MKDIR} $@
+ @+cd $@ \
+ && ../configure ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \
+ && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped"
+
+## Packaging (`make install`)
+
+DEST_PREFIX := ${DESTDIR}${PREFIX}
+DEST_MANDIR := ${DESTDIR}${MANDIR}
+
+install::
+ ${Q}${MKDIR} ${DEST_PREFIX}/bin
+ ${MSG} "[INST] bin/bfs" \
+ ${INSTALL} -m755 bin/bfs ${DEST_PREFIX}/bin/bfs
+ ${Q}${MKDIR} ${DEST_MANDIR}/man1
+ ${MSG} "[INST] man/man1/bfs.1" \
+ ${INSTALL} -m644 docs/bfs.1 ${DEST_MANDIR}/man1/bfs.1
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/bash-completion/completions
+ ${MSG} "[INST] completions/bfs.bash" \
+ ${INSTALL} -m644 completions/bfs.bash ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/zsh/site-functions
+ ${MSG} "[INST] completions/bfs.zsh" \
+ ${INSTALL} -m644 completions/bfs.zsh ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/fish/vendor_completions.d
+ ${MSG} "[INST] completions/bfs.fish" \
+ ${INSTALL} -m644 completions/bfs.fish ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+
+uninstall::
+ ${MSG} "[ RM ] completions/bfs.bash" \
+ ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${MSG} "[ RM ] completions/bfs.zsh" \
+ ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${MSG} "[ RM ] completions/bfs.fish" \
+ ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+ ${MSG} "[ RM ] man/man1/bfs.1" \
+ ${RM} ${DEST_MANDIR}/man1/bfs.1
+ ${MSG} "[ RM ] bin/bfs" \
+ ${RM} ${DEST_PREFIX}/bin/bfs
+
+# Check that `make install` works and `make uninstall` removes everything
+check-install::
+ +${MAKE} install DESTDIR=pkg
+ +${MAKE} uninstall DESTDIR=pkg
+ bin/bfs pkg -not -type d -print -exit 1
+ ${RM} -r pkg
+
+## Cleanup (`make clean`)
+
+# Clean all build products
+clean::
+ ${MSG} "[ RM ] bin obj" \
+ ${RM} -r bin obj
+
+# Clean everything, including generated files
+distclean: clean
+ ${MSG} "[ RM ] gen" \
+ ${RM} -r gen ${DISTCHECKS}
+.PHONY: distclean
diff --git a/README.md b/README.md
index 9992938..b9aad82 100644
--- a/README.md
+++ b/README.md
@@ -293,7 +293,7 @@ Here's how to install them on some common platforms:
<pre>
<strong>Alpine Linux</strong>
-# apk add acl{,-dev} attr{,-dev} libcap{,-dev} liburing-dev oniguruma-dev
+# apk add acl{,-dev} attr libcap{,-dev} liburing-dev oniguruma-dev
<strong>Arch Linux</strong>
# pacman -S acl attr libcap liburing oniguruma
@@ -302,13 +302,13 @@ Here's how to install them on some common platforms:
# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev liburing-dev libonig-dev
<strong>Fedora</strong>
-# dnf install acl libacl-devel libattr-devel libcap-devel liburing-devel oniguruma-devel
+# dnf install acl libacl-devel attr libcap-devel liburing-devel oniguruma-devel
<strong>NixOS</strong>
# nix-env -i acl attr libcap liburing oniguruma
<strong>Void Linux</strong>
-# xbps-install -S acl-{devel,progs} attr-{devel,progs} libcap-{devel,progs} liburing-devel oniguruma-devel
+# xbps-install -S acl-{devel,progs} attr-progs libcap-{devel,progs} liburing-devel oniguruma-devel
<strong>Homebrew</strong>
$ brew install oniguruma
@@ -333,6 +333,7 @@ Once you have the dependencies, you can build <code>bfs</code>.
Download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs).
Then run
+ $ ./configure
$ make
This will build the `./bin/bfs` binary.
@@ -342,7 +343,8 @@ Run the test suite to make sure it works correctly:
If you're interested in speed, you may want to build the release version instead:
- $ make release
+ $ ./configure --enable-release
+ $ make
Finally, if you want to install it globally, run
diff --git a/bench/bench.sh b/bench/bench.sh
index e4b5511..cf1ae49 100644
--- a/bench/bench.sh
+++ b/bench/bench.sh
@@ -221,7 +221,8 @@ setup() {
fi
echo "Building bfs ..."
- as-user make -s -j"$nproc" release all
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc" all
as-user mkdir -p bench/corpus
@@ -253,7 +254,12 @@ setup() {
echo "Building bfs $commit ..."
cd "$worktree"
as-user git checkout -qd "$commit" --
- as-user make -s -j"$nproc" release
+ if [ -e configure ]; then
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc"
+ else
+ as-user make -s -j"$nproc" release
+ fi
if [ -e ./bin/bfs ]; then
as-user cp ./bin/bfs "$bin/bfs-$commit"
else
diff --git a/build/cc.sh b/build/cc.sh
new file mode 100755
index 0000000..45d51ca
--- /dev/null
+++ b/build/cc.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# 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"
+)
diff --git a/build/config.mk b/build/config.mk
new file mode 100644
index 0000000..24873ec
--- /dev/null
+++ b/build/config.mk
@@ -0,0 +1,62 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that implements `./configure`
+
+include build/prelude.mk
+include build/exports.mk
+
+# All configuration steps
+config: gen/config.mk gen/config.h
+.PHONY: config
+
+# Makefile fragments generated by `./configure`
+MKS := \
+ gen/vars.mk \
+ gen/flags.mk \
+ gen/deps.mk \
+ gen/pkgs.mk
+
+# The main configuration file, which includes the others
+gen/config.mk: ${MKS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'include %s\n' ${.ALLSRC} >>$@
+ ${VCAT} gen/config.mk
+.PHONY: gen/config.mk
+
+# Saves the configurable variables
+gen/vars.mk::
+ @${MKDIR} ${@D}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'PREFIX := %s\n' "$$XPREFIX" >>$@
+ @printf 'MANDIR := %s\n' "$$XMANDIR" >>$@
+ @printf 'OS := %s\n' "$${OS:-$$(uname)}" >>$@
+ @printf 'CC := %s\n' "$$XCC" >>$@
+ @printf 'INSTALL := %s\n' "$$XINSTALL" >>$@
+ @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@
+ @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@
+ @printf 'RM := %s\n' "$$XRM" >>$@
+ ${VCAT} $@
+
+# Sets the build flags. This depends on vars.mk and uses a recursive make so
+# that the default flags can depend on variables like ${OS}.
+gen/flags.mk: gen/vars.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/flags.mk $@
+.PHONY: gen/flags.mk
+
+# Check for dependency generation support
+gen/deps.mk: gen/flags.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/deps.mk $@
+.PHONY: gen/deps.mk
+
+# Auto-detect dependencies and their build flags
+gen/pkgs.mk: gen/flags.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/pkgs.mk $@
+.PHONY: gen/pkgs.mk
+
+# Compile-time feature detection
+gen/config.h: gen/config.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@
+.PHONY: gen/config.h
diff --git a/build/define-if.sh b/build/define-if.sh
new file mode 100755
index 0000000..295ead8
--- /dev/null
+++ b/build/define-if.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Output a C preprocessor definition based on whether a command succeeds
+
+set -eu
+
+SLUG="${1#build/}"
+SLUG="${SLUG%.c}"
+MACRO="BFS_$(printf '%s' "$SLUG" | tr '/a-z-' '_A-Z_')"
+shift
+
+if "$@"; then
+ printf '#define %s true\n' "$MACRO"
+else
+ printf '#define %s false\n' "$MACRO"
+ exit 1
+fi
diff --git a/build/deps.mk b/build/deps.mk
new file mode 100644
index 0000000..3db62b6
--- /dev/null
+++ b/build/deps.mk
@@ -0,0 +1,18 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/deps.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+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 \
+ printf 'CPPFLAGS += -MD -MP\n'; \
+ fi >>$@ 2>$@.log
+ ${VCAT} $@
+ @printf -- '-include %s\n' ${OBJS:.o=.d} >>$@
diff --git a/build/empty.c b/build/empty.c
new file mode 100644
index 0000000..4fa9a5b
--- /dev/null
+++ b/build/empty.c
@@ -0,0 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+int main(void) {
+ return 0;
+}
diff --git a/build/exports.mk b/build/exports.mk
new file mode 100644
index 0000000..ed19134
--- /dev/null
+++ b/build/exports.mk
@@ -0,0 +1,20 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile fragment that exports variables used by configuration scripts
+
+export XPREFIX=${PREFIX}
+export XMANDIR=${MANDIR}
+
+export XCC=${CC}
+export XINSTALL=${INSTALL}
+export XMKDIR=${MKDIR}
+export XPKG_CONFIG=${PKG_CONFIG}
+export XRM=${RM}
+
+export XCPPFLAGS=${CPPFLAGS}
+export XCFLAGS=${CFLAGS}
+export XLDFLAGS=${LDFLAGS}
+export XLDLIBS=${LDLIBS}
+
+export XNOLIBS=${NOLIBS}
diff --git a/build/flags.mk b/build/flags.mk
new file mode 100644
index 0000000..c911b22
--- /dev/null
+++ b/build/flags.mk
@@ -0,0 +1,136 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/flags.mk
+
+include build/prelude.mk
+include gen/vars.mk
+
+# Configurable flags
+CPPFLAGS ?=
+CFLAGS ?= \
+ -g \
+ -Wall \
+ -Wformat=2 \
+ -Werror=implicit \
+ -Wimplicit-fallthrough \
+ -Wmissing-declarations \
+ -Wshadow \
+ -Wsign-compare \
+ -Wstrict-prototypes
+LDFLAGS ?=
+LDLIBS ?=
+
+export XCPPFLAGS=${CPPFLAGS}
+export XCFLAGS=${CFLAGS}
+export XLDFLAGS=${LDFLAGS}
+export XLDLIBS=${LDLIBS}
+
+# Immutable flags
+export BFS_CPPFLAGS= \
+ -Isrc \
+ -Igen \
+ -D__EXTENSIONS__ \
+ -D_ATFILE_SOURCE \
+ -D_BSD_SOURCE \
+ -D_DARWIN_C_SOURCE \
+ -D_DEFAULT_SOURCE \
+ -D_GNU_SOURCE \
+ -D_POSIX_PTHREAD_SEMANTICS \
+ -D_FILE_OFFSET_BITS=64 \
+ -D_TIME_BITS=64
+export BFS_CFLAGS= -std=c17 -pthread
+
+# Platform-specific system libraries
+LDLIBS,DragonFly := -lposix1e
+LDLIBS,Linux := -lrt
+LDLIBS,NetBSD := -lutil
+LDLIBS,SunOS := -lsec -lsocket -lnsl
+export BFS_LDLIBS=${LDLIBS,${OS}}
+
+# Build profiles
+_ASAN := ${TRUTHY,${ASAN}}
+_LSAN := ${TRUTHY,${LSAN}}
+_MSAN := ${TRUTHY,${MSAN}}
+_TSAN := ${TRUTHY,${TSAN}}
+_UBSAN := ${TRUTHY,${UBSAN}}
+_GCOV := ${TRUTHY,${GCOV}}
+_LINT := ${TRUTHY,${LINT}}
+_RELEASE := ${TRUTHY,${RELEASE}}
+
+# https://github.com/google/sanitizers/issues/342
+TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0
+export TSAN_CPPFLAGS=${TSAN_CPPFLAGS,${_TSAN}}
+
+ASAN_CFLAGS,y := -fsanitize=address
+LSAN_CFLAGS,y := -fsanitize=leak
+MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
+TSAN_CFLAGS,y := -fsanitize=thread
+UBSAN_CFLAGS.y := -fsanitize=undefined
+
+export ASAN_CFLAGS=${ASAN_CFLAGS,${_ASAN}}
+export LSAN_CFLAGS=${LSAN_CFLAGS,${_LSAN}}
+export MSAN_CFLAGS=${MSAN_CFLAGS,${_MSAN}}
+export TSAN_CFLAGS=${TSAN_CFLAGS,${_TSAN}}
+export UBSAN_CFLAGS=${UBSAN_CFLAGS,${_UBSAN}}
+
+SAN_CFLAGS,y := -fno-sanitize-recover=all
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+SAN := ${NOT,${INSANE}}
+export SAN_CFLAGS=${SAN_CFLAGS,${SAN}}
+
+# MSAN and TSAN both need all code to be instrumented
+YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+NOLIBS ?= ${NOT,${YESLIBS}}
+export XNOLIBS=${NOLIBS}
+
+# gcov only intercepts fork()/exec() with -std=gnu*
+GCOV_CFLAGS,y := -std=gnu17 --coverage
+export GCOV_CFLAGS=${GCOV_CFLAGS,${_GCOV}}
+
+LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
+LINT_CFLAGS,y := -Werror -O2
+
+export LINT_CPPFLAGS=${LINT_CPPFLAGS,${_LINT}}
+export LINT_CFLAGS=${LINT_CFLAGS,${_LINT}}
+
+RELEASE_CPPFLAGS,y := -DNDEBUG
+RELEASE_CFLAGS,y := -O3 -flto=auto
+
+export RELEASE_CPPFLAGS=${RELEASE_CPPFLAGS,${_RELEASE}}
+export RELEASE_CFLAGS=${RELEASE_CFLAGS,${_RELEASE}}
+
+# Set a variable
+SETVAR = @printf '%s := %s\n' >>$@
+
+# Append to a variable, if non-empty
+APPEND = @append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append
+
+gen/flags.mk::
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ ${SETVAR} CPPFLAGS "$$BFS_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$TSAN_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$LINT_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$RELEASE_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$XCPPFLAGS"
+ ${APPEND} CPPFLAGS "$$EXTRA_CPPFLAGS"
+ ${SETVAR} CFLAGS "$$BFS_CFLAGS"
+ ${APPEND} CFLAGS "$$ASAN_CFLAGS"
+ ${APPEND} CFLAGS "$$LSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$MSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$TSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$UBSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$SAN_CFLAGS"
+ ${APPEND} CFLAGS "$$GCOV_CFLAGS"
+ ${APPEND} CFLAGS "$$LINT_CFLAGS"
+ ${APPEND} CFLAGS "$$RELEASE_CFLAGS"
+ ${APPEND} CFLAGS "$$XCFLAGS"
+ ${APPEND} CFLAGS "$$EXTRA_CFLAGS"
+ ${SETVAR} LDFLAGS "$$XLDFLAGS"
+ ${SETVAR} LDLIBS "$$XLDLIBS"
+ ${APPEND} LDLIBS "$$EXTRA_LDLIBS"
+ ${APPEND} LDLIBS "$$BFS_LDLIBS"
+ ${SETVAR} NOLIBS "$$XNOLIBS"
+ @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
+ ${VCAT} $@
diff --git a/build/has/acl-get-entry.c b/build/has/acl-get-entry.c
new file mode 100644
index 0000000..3cce771
--- /dev/null
+++ b/build/has/acl-get-entry.c
@@ -0,0 +1,8 @@
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ acl_entry_t entry;
+ return acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+}
diff --git a/build/has/acl-get-file.c b/build/has/acl-get-file.c
new file mode 100644
index 0000000..89fbf23
--- /dev/null
+++ b/build/has/acl-get-file.c
@@ -0,0 +1,8 @@
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ return acl == (acl_t)NULL;
+}
diff --git a/build/has/acl-get-tag-type.c b/build/has/acl-get-tag-type.c
new file mode 100644
index 0000000..2901956
--- /dev/null
+++ b/build/has/acl-get-tag-type.c
@@ -0,0 +1,10 @@
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_entry_t entry;
+ memset(&entry, 0, sizeof(entry));
+ acl_tag_t tag;
+ return acl_get_tag_type(entry, &tag);
+}
diff --git a/build/has/acl-is-trivial-np.c b/build/has/acl-is-trivial-np.c
new file mode 100644
index 0000000..9ca9fc7
--- /dev/null
+++ b/build/has/acl-is-trivial-np.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_fd(3);
+ int trivial;
+ acl_is_trivial_np(acl, &trivial);
+ return 0;
+}
diff --git a/build/has/acl-trivial.c b/build/has/acl-trivial.c
new file mode 100644
index 0000000..7efc838
--- /dev/null
+++ b/build/has/acl-trivial.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ return acl_trivial(".");
+}
diff --git a/build/has/aligned-alloc.c b/build/has/aligned-alloc.c
new file mode 100644
index 0000000..4460038
--- /dev/null
+++ b/build/has/aligned-alloc.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+
+int main(void) {
+ return !aligned_alloc(_Alignof(void *), sizeof(void *));
+}
diff --git a/build/has/confstr.c b/build/has/confstr.c
new file mode 100644
index 0000000..58280b4
--- /dev/null
+++ b/build/has/confstr.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <unistd.h>
+
+int main(void) {
+ confstr(_CS_PATH, NULL, 0);
+ return 0;
+}
diff --git a/build/has/extattr-get-file.c b/build/has/extattr-get-file.c
new file mode 100644
index 0000000..ac9cf96
--- /dev/null
+++ b/build/has/extattr-get-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_file("file", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-get-link.c b/build/has/extattr-get-link.c
new file mode 100644
index 0000000..c35be5b
--- /dev/null
+++ b/build/has/extattr-get-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_link("link", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-list-file.c b/build/has/extattr-list-file.c
new file mode 100644
index 0000000..e68a8bb
--- /dev/null
+++ b/build/has/extattr-list-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_file("file", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/build/has/extattr-list-link.c b/build/has/extattr-list-link.c
new file mode 100644
index 0000000..49f0ec2
--- /dev/null
+++ b/build/has/extattr-list-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_link("link", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/build/has/fdclosedir.c b/build/has/fdclosedir.c
new file mode 100644
index 0000000..f4ad1f5
--- /dev/null
+++ b/build/has/fdclosedir.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ return fdclosedir(opendir("."));
+}
diff --git a/build/has/getdents.c b/build/has/getdents.c
new file mode 100644
index 0000000..579898f
--- /dev/null
+++ b/build/has/getdents.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return getdents(3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getdents64-syscall.c b/build/has/getdents64-syscall.c
new file mode 100644
index 0000000..7642d93
--- /dev/null
+++ b/build/has/getdents64-syscall.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+int main(void) {
+ char buf[1024];
+ return syscall(SYS_getdents64, 3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getdents64.c b/build/has/getdents64.c
new file mode 100644
index 0000000..d8e8062
--- /dev/null
+++ b/build/has/getdents64.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return getdents64(3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getmntent-1.c b/build/has/getmntent-1.c
new file mode 100644
index 0000000..9854dcd
--- /dev/null
+++ b/build/has/getmntent-1.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <mntent.h>
+#include <stdio.h>
+
+int main(void) {
+ return !getmntent(stdin);
+}
diff --git a/build/has/getmntent-2.c b/build/has/getmntent-2.c
new file mode 100644
index 0000000..71f0220
--- /dev/null
+++ b/build/has/getmntent-2.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdio.h>
+#include <sys/mnttab.h>
+
+int main(void) {
+ struct mnttab mnt;
+ return getmntent(stdin, &mnt);
+}
diff --git a/build/has/getmntinfo.c b/build/has/getmntinfo.c
new file mode 100644
index 0000000..90ef5fb
--- /dev/null
+++ b/build/has/getmntinfo.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/mount.h>
+
+int main(void) {
+ return getmntinfo(NULL, MNT_WAIT);
+}
diff --git a/build/has/getprogname-gnu.c b/build/has/getprogname-gnu.c
new file mode 100644
index 0000000..6b97c5e
--- /dev/null
+++ b/build/has/getprogname-gnu.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+
+int main(void) {
+ const char *str = program_invocation_short_name;
+ return str[0];
+}
diff --git a/build/has/getprogname.c b/build/has/getprogname.c
new file mode 100644
index 0000000..83dc8e8
--- /dev/null
+++ b/build/has/getprogname.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+
+int main(void) {
+ const char *str = getprogname();
+ return str[0];
+}
diff --git a/build/has/max-align-t.c b/build/has/max-align-t.c
new file mode 100644
index 0000000..96165ce
--- /dev/null
+++ b/build/has/max-align-t.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+
+int main(void) {
+ return _Alignof(max_align_t);
+}
diff --git a/build/has/pipe2.c b/build/has/pipe2.c
new file mode 100644
index 0000000..4cb43b5
--- /dev/null
+++ b/build/has/pipe2.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <unistd.h>
+
+int main(void) {
+ int fds[2];
+ return pipe2(fds, O_CLOEXEC);
+}
diff --git a/build/has/posix-spawn-addfchdir-np.c b/build/has/posix-spawn-addfchdir-np.c
new file mode 100644
index 0000000..b870a53
--- /dev/null
+++ b/build/has/posix-spawn-addfchdir-np.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <spawn.h>
+
+int main(void) {
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_addfchdir_np(&actions, 3);
+ return 0;
+}
diff --git a/build/has/posix-spawn-addfchdir.c b/build/has/posix-spawn-addfchdir.c
new file mode 100644
index 0000000..c52ff81
--- /dev/null
+++ b/build/has/posix-spawn-addfchdir.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <spawn.h>
+
+int main(void) {
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_addfchdir(&actions, 3);
+ return 0;
+}
diff --git a/build/has/st-acmtim.c b/build/has/st-acmtim.c
new file mode 100644
index 0000000..d687ab0
--- /dev/null
+++ b/build/has/st-acmtim.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ unsigned int a = sb.st_atim.tv_sec;
+ unsigned int c = sb.st_ctim.tv_sec;
+ unsigned int m = sb.st_mtim.tv_sec;
+ return a + c + m;
+}
diff --git a/build/has/st-acmtimespec.c b/build/has/st-acmtimespec.c
new file mode 100644
index 0000000..f747bc0
--- /dev/null
+++ b/build/has/st-acmtimespec.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ unsigned int a = sb.st_atimespec.tv_sec;
+ unsigned int c = sb.st_ctimespec.tv_sec;
+ unsigned int m = sb.st_mtimespec.tv_sec;
+ return a + c + m;
+}
diff --git a/build/has/st-birthtim.c b/build/has/st-birthtim.c
new file mode 100644
index 0000000..4964571
--- /dev/null
+++ b/build/has/st-birthtim.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_birthtim.tv_sec;
+}
diff --git a/build/has/st-birthtimespec.c b/build/has/st-birthtimespec.c
new file mode 100644
index 0000000..91a613f
--- /dev/null
+++ b/build/has/st-birthtimespec.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_birthtimespec.tv_sec;
+}
diff --git a/build/has/st-flags.c b/build/has/st-flags.c
new file mode 100644
index 0000000..b1d0c32
--- /dev/null
+++ b/build/has/st-flags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_flags;
+}
diff --git a/build/has/statx-syscall.c b/build/has/statx-syscall.c
new file mode 100644
index 0000000..87ec869
--- /dev/null
+++ b/build/has/statx-syscall.c
@@ -0,0 +1,13 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <linux/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+int main(void) {
+ struct statx sb;
+ syscall(SYS_statx, AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb);
+ return 0;
+}
diff --git a/build/has/statx.c b/build/has/statx.c
new file mode 100644
index 0000000..65f1674
--- /dev/null
+++ b/build/has/statx.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+int main(void) {
+ struct statx sb;
+ statx(AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb);
+ return 0;
+}
diff --git a/build/has/strerror-l.c b/build/has/strerror-l.c
new file mode 100644
index 0000000..3dcc4d7
--- /dev/null
+++ b/build/has/strerror-l.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <locale.h>
+#include <string.h>
+
+int main(void) {
+ locale_t locale = duplocale(LC_GLOBAL_LOCALE);
+ return !strerror_l(ENOMEM, locale);
+}
diff --git a/build/has/strerror-r-gnu.c b/build/has/strerror-r-gnu.c
new file mode 100644
index 0000000..26ca0ee
--- /dev/null
+++ b/build/has/strerror-r-gnu.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <string.h>
+
+int main(void) {
+ char buf[256];
+ // Check that strerror_r() returns a pointer
+ return *strerror_r(ENOMEM, buf, sizeof(buf));
+}
diff --git a/build/has/strerror-r-posix.c b/build/has/strerror-r-posix.c
new file mode 100644
index 0000000..41b2d30
--- /dev/null
+++ b/build/has/strerror-r-posix.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <string.h>
+
+int main(void) {
+ char buf[256];
+ // Check that strerror_r() returns an integer
+ return 2 * strerror_r(ENOMEM, buf, sizeof(buf));
+}
diff --git a/build/has/string-to-flags.c b/build/has/string-to-flags.c
new file mode 100644
index 0000000..027d72c
--- /dev/null
+++ b/build/has/string-to-flags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <util.h>
+
+int main(void) {
+ return string_to_flags(NULL, NULL, NULL);
+}
diff --git a/build/has/strtofflags.c b/build/has/strtofflags.c
new file mode 100644
index 0000000..73ecbcb
--- /dev/null
+++ b/build/has/strtofflags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <unistd.h>
+
+int main(void) {
+ return strtofflags(NULL, NULL, NULL);
+}
diff --git a/build/has/timegm.c b/build/has/timegm.c
new file mode 100644
index 0000000..6e2d155
--- /dev/null
+++ b/build/has/timegm.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ struct tm tm = {0};
+ return (int)timegm(&tm);
+}
diff --git a/build/has/tm-gmtoff.c b/build/has/tm-gmtoff.c
new file mode 100644
index 0000000..543df48
--- /dev/null
+++ b/build/has/tm-gmtoff.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ struct tm tm = {0};
+ return tm.tm_gmtoff;
+}
diff --git a/build/has/uselocale.c b/build/has/uselocale.c
new file mode 100644
index 0000000..a712ff8
--- /dev/null
+++ b/build/has/uselocale.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <locale.h>
+
+int main(void) {
+ locale_t locale = uselocale((locale_t)0);
+ return locale == LC_GLOBAL_LOCALE;
+}
diff --git a/build/header.mk b/build/header.mk
new file mode 100644
index 0000000..da6de68
--- /dev/null
+++ b/build/header.mk
@@ -0,0 +1,72 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/config.h
+
+include build/prelude.mk
+include gen/config.mk
+include build/exports.mk
+
+# All header fragments we generate
+HEADERS := \
+ gen/has/acl-get-entry.h \
+ gen/has/acl-get-file.h \
+ gen/has/acl-get-tag-type.h \
+ gen/has/acl-is-trivial-np.h \
+ gen/has/acl-trivial.h \
+ gen/has/aligned-alloc.h \
+ gen/has/confstr.h \
+ gen/has/extattr-get-file.h \
+ gen/has/extattr-get-link.h \
+ gen/has/extattr-list-file.h \
+ gen/has/extattr-list-link.h \
+ gen/has/fdclosedir.h \
+ gen/has/getdents.h \
+ gen/has/getdents64.h \
+ gen/has/getdents64-syscall.h \
+ gen/has/getmntent-1.h \
+ gen/has/getmntent-2.h \
+ gen/has/getmntinfo.h \
+ gen/has/getprogname.h \
+ gen/has/getprogname-gnu.h \
+ gen/has/max-align-t.h \
+ gen/has/pipe2.h \
+ gen/has/posix-spawn-addfchdir.h \
+ gen/has/posix-spawn-addfchdir-np.h \
+ gen/has/st-acmtim.h \
+ gen/has/st-acmtimespec.h \
+ gen/has/st-birthtim.h \
+ gen/has/st-birthtimespec.h \
+ gen/has/st-flags.h \
+ gen/has/statx.h \
+ gen/has/statx-syscall.h \
+ gen/has/strerror-l.h \
+ gen/has/strerror-r-gnu.h \
+ gen/has/strerror-r-posix.h \
+ gen/has/string-to-flags.h \
+ gen/has/strtofflags.h \
+ gen/has/timegm.h \
+ gen/has/tm-gmtoff.h \
+ gen/has/uselocale.h
+
+# Previously generated by pkgs.mk
+PKG_HEADERS := ${ALL_PKGS:%=gen/use/%.h}
+
+gen/config.h: ${PKG_HEADERS} ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '// %s\n' "$@" >$@
+ @printf '#ifndef BFS_CONFIG_H\n' >>$@
+ @printf '#define BFS_CONFIG_H\n' >>$@
+ @cat ${.ALLSRC} >>$@
+ @printf '#endif // BFS_CONFIG_H\n' >>$@
+ @cat ${.ALLSRC:%=%.log} >gen/config.log
+ ${VCAT} $@
+.PHONY: gen/config.h
+
+# The short name of the config test
+SLUG = ${@:gen/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
diff --git a/build/msg-if.sh b/build/msg-if.sh
new file mode 100755
index 0000000..8112aea
--- /dev/null
+++ b/build/msg-if.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a success/failure indicator from a makefile:
+#
+# $ ./configure
+# [ CC ] use/liburing.c ✘
+# [ CC ] use/oniguruma.c ✔
+
+set -eu
+
+MSG="$1"
+shift
+
+if "$@"; then
+ build/msg.sh "$(printf '%-37s ✔' "$MSG")"
+else
+ build/msg.sh "$(printf '%-37s ✘' "$MSG")"
+fi
diff --git a/build/msg.sh b/build/msg.sh
new file mode 100755
index 0000000..a7da31b
--- /dev/null
+++ b/build/msg.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a message from a makefile:
+#
+# $ make -s
+# $ make
+# [ CC ] src/main.c
+# $ make V=1
+# cc -Isrc -Igen -D...
+
+set -eu
+
+# Get the $MAKEFLAGS from the top-level make invocation
+MFLAGS="${XMAKEFLAGS-${MAKEFLAGS-}}"
+
+# Check if make should be quiet (make -s)
+is_quiet() {
+ # GNU make puts single-letter flags in the first word of $MAKEFLAGS,
+ # without a leading dash
+ case "${MFLAGS%% *}" in
+ -*) : ;;
+ *s*) return 0 ;;
+ esac
+
+ # BSD make puts each flag separately like -r -s -j 48
+ for flag in $MFLAGS; do
+ case "$flag" in
+ # Ignore things like --jobserver-auth
+ --*) continue ;;
+ # Skip variable assignments
+ *=*) break ;;
+ -*s*) return 0 ;;
+ esac
+ done
+
+ return 1
+}
+
+# Check if make should be loud (make V=1)
+is_loud() {
+ test "$XV"
+}
+
+MSG="$1"
+shift
+
+if ! is_quiet && ! is_loud; then
+ printf '%s\n' "$MSG"
+fi
+
+if [ $# -eq 0 ]; then
+ exit
+fi
+
+if is_loud; then
+ printf '%s\n' "$*"
+fi
+
+"$@"
diff --git a/build/pkgconf.sh b/build/pkgconf.sh
new file mode 100755
index 0000000..96e4bf1
--- /dev/null
+++ b/build/pkgconf.sh
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# pkg-config wrapper with hardcoded fallbacks
+
+set -eu
+
+MODE=
+case "${1:-}" in
+ --*)
+ MODE="$1"
+ shift
+esac
+
+if [ $# -lt 1 ]; then
+ exit
+fi
+
+case "$XNOLIBS" in
+ y|1)
+ exit 1
+esac
+
+if [ -z "$MODE" ]; then
+ # Check whether the libraries exist at all
+ for LIB; do
+ # Check ${USE_$LIB}
+ USE_LIB="USE_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')"
+ eval "USE=\"\${$USE_LIB:-}\""
+ case "$USE" in
+ y|1)
+ continue
+ ;;
+ n|0)
+ exit 1
+ ;;
+ esac
+
+ CFLAGS=$("$0" --cflags "$LIB") || exit 1
+ LDFLAGS=$("$0" --ldflags "$LIB") || exit 1
+ LDLIBS=$("$0" --ldlibs "$LIB") || exit 1
+ build/cc.sh $CFLAGS $LDFLAGS build/use/$LIB.c $LDLIBS || exit 1
+ done
+fi
+
+# Defer to pkg-config if possible
+if command -v "${XPKG_CONFIG:-}" >/dev/null 2>&1; then
+ case "$MODE" in
+ --cflags)
+ "$XPKG_CONFIG" --cflags "$@"
+ ;;
+ --ldflags)
+ "$XPKG_CONFIG" --libs-only-L --libs-only-other "$@"
+ ;;
+ --ldlibs)
+ "$XPKG_CONFIG" --libs-only-l "$@"
+ ;;
+ esac
+
+ exit
+fi
+
+# pkg-config unavailable, emulate it ourselves
+CFLAGS=""
+LDFLAGS=""
+LDLIBS=""
+
+for LIB; do
+ case "$LIB" in
+ libacl)
+ LDLIB=-lacl
+ ;;
+ libcap)
+ LDLIB=-lcap
+ ;;
+ libselinux)
+ LDLIB=-lselinux
+ ;;
+ liburing)
+ LDLIB=-luring
+ ;;
+ oniguruma)
+ LDLIB=-lonig
+ ;;
+ *)
+ printf 'error: Unknown package %s\n' "$LIB" >&2
+ exit 1
+ ;;
+ esac
+
+ LDLIBS="$LDLIBS$LDLIB "
+done
+
+case "$MODE" in
+ --ldlibs)
+ printf '%s\n' "$LDLIBS"
+ ;;
+esac
diff --git a/build/pkgs.mk b/build/pkgs.mk
new file mode 100644
index 0000000..39b550d
--- /dev/null
+++ b/build/pkgs.mk
@@ -0,0 +1,33 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/pkgs.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+include build/exports.mk
+
+HEADERS := ${ALL_PKGS:%=gen/use/%.h}
+
+gen/pkgs.mk: ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @gen() { \
+ printf 'PKGS := %s\n' "$$*"; \
+ printf 'CFLAGS += %s\n' "$$(build/pkgconf.sh --cflags "$$@")"; \
+ printf 'LDFLAGS += %s\n' "$$(build/pkgconf.sh --ldflags "$$@")"; \
+ printf 'LDLIBS := %s $${LDLIBS}\n' "$$(build/pkgconf.sh --ldlibs "$$@")"; \
+ }; \
+ gen $$(grep -l ' true$$' ${.ALLSRC} | sed 's|.*/\(.*\)\.h|\1|') >>$@
+ ${VCAT} $@
+
+.PHONY: gen/pkgs.mk
+
+# Convert gen/use/foo.h to foo
+PKG = ${@:gen/use/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh use/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] use/${PKG}.c" test $$? -eq 0;
diff --git a/build/prelude.mk b/build/prelude.mk
new file mode 100644
index 0000000..5be26cb
--- /dev/null
+++ b/build/prelude.mk
@@ -0,0 +1,122 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Common makefile utilities. Compatible with both GNU make and most BSD makes.
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# We don't use any suffix rules
+.SUFFIXES:
+
+# GNU make has $^ for the full list of targets, while BSD make has $> and the
+# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would
+# break if one of them implemented support for the other. So instead, bring
+# BSD's ${.ALLSRC} to GNU.
+.ALLSRC ?= $^
+
+# Installation paths
+DESTDIR ?=
+PREFIX ?= /usr
+MANDIR ?= ${PREFIX}/share/man
+
+# Configurable executables
+CC ?= cc
+INSTALL ?= install
+MKDIR ?= mkdir -p
+PKG_CONFIG ?= pkg-config
+RM ?= rm -f
+
+# GNU and BSD make have incompatible syntax for conditionals, but we can do a
+# lot with just nested variable expansion. We use "y" as the canonical
+# truthy value, and "" (the empty string) as the canonical falsey value.
+#
+# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this:
+#
+# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y
+# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y
+# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty]
+# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty]
+# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [emtpy]
+#
+# Inspired by https://github.com/wahern/autoguess
+TRUTHY,y := y
+TRUTHY,1 := y
+
+# Boolean operators are also implemented with nested expansion
+NOT, := y
+
+# Normalize ${V} to either "y" or ""
+export XV=${TRUTHY,${V}}
+
+# Suppress output unless V=1
+Q, := @
+Q := ${Q,${XV}}
+
+# Show full commands with `make V=1`, otherwise short summaries
+MSG = @build/msg.sh
+
+# cat a file if V=1
+VCAT,y := @cat
+VCAT, := @:
+VCAT := ${VCAT,${XV}}
+
+# All external dependencies
+ALL_PKGS := \
+ libacl \
+ libcap \
+ libselinux \
+ liburing \
+ oniguruma
+
+# List all object files here, as they're needed by both `./configure` and `make`
+
+# All object files except the entry point
+LIBBFS := \
+ obj/src/alloc.o \
+ obj/src/bar.o \
+ obj/src/bfstd.o \
+ obj/src/bftw.o \
+ obj/src/color.o \
+ obj/src/ctx.o \
+ obj/src/diag.o \
+ obj/src/dir.o \
+ obj/src/dstring.o \
+ obj/src/eval.o \
+ obj/src/exec.o \
+ obj/src/expr.o \
+ obj/src/fsade.o \
+ obj/src/ioq.o \
+ obj/src/mtab.o \
+ obj/src/opt.o \
+ obj/src/parse.o \
+ obj/src/printf.o \
+ obj/src/pwcache.o \
+ obj/src/stat.o \
+ obj/src/thread.o \
+ obj/src/trie.o \
+ obj/src/typo.o \
+ obj/src/xregex.o \
+ obj/src/xspawn.o \
+ obj/src/xtime.o \
+ obj/gen/version.o
+
+# Unit test objects
+UNIT_OBJS := \
+ obj/tests/alloc.o \
+ obj/tests/bfstd.o \
+ obj/tests/bit.o \
+ obj/tests/ioq.o \
+ obj/tests/main.o \
+ obj/tests/trie.o \
+ obj/tests/xspawn.o \
+ obj/tests/xtime.o
+
+# All object files
+OBJS := \
+ obj/src/main.o \
+ obj/tests/mksock.o \
+ obj/tests/xspawnee.o \
+ obj/tests/xtouch.o \
+ ${LIBBFS} \
+ ${UNIT_OBJS}
diff --git a/build/use/libacl.c b/build/use/libacl.c
new file mode 100644
index 0000000..de1fe50
--- /dev/null
+++ b/build/use/libacl.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ acl_free(0);
+ return 0;
+}
diff --git a/build/use/libcap.c b/build/use/libcap.c
new file mode 100644
index 0000000..58e832c
--- /dev/null
+++ b/build/use/libcap.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/capability.h>
+
+int main(void) {
+ cap_free(0);
+ return 0;
+}
diff --git a/build/use/libselinux.c b/build/use/libselinux.c
new file mode 100644
index 0000000..bca409d
--- /dev/null
+++ b/build/use/libselinux.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <selinux/selinux.h>
+
+int main(void) {
+ freecon(0);
+ return 0;
+}
diff --git a/build/use/liburing.c b/build/use/liburing.c
new file mode 100644
index 0000000..bea499a
--- /dev/null
+++ b/build/use/liburing.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <liburing.h>
+
+int main(void) {
+ io_uring_free_probe(0);
+ return 0;
+}
diff --git a/build/use/oniguruma.c b/build/use/oniguruma.c
new file mode 100644
index 0000000..cb17596
--- /dev/null
+++ b/build/use/oniguruma.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <oniguruma.h>
+
+int main(void) {
+ onig_free(0);
+ return 0;
+}
diff --git a/completions/bfs.bash b/completions/bfs.bash
index 816f1ec..6fd82c8 100644
--- a/completions/bfs.bash
+++ b/completions/bfs.bash
@@ -32,6 +32,7 @@ _bfs() {
# (e.g. because they are numeric, glob, regexp, time, etc.)
local nocomp=(
-{a,B,c,m}{min,since,time}
+ -context
-ilname
-iname
-inum
diff --git a/completions/bfs.fish b/completions/bfs.fish
index 0c58ef4..24b0ad9 100644
--- a/completions/bfs.fish
+++ b/completions/bfs.fish
@@ -71,6 +71,7 @@ complete -c bfs -o Btime -d "Find files birthed specified number of days ago" -x
complete -c bfs -o ctime -d "Find files changed specified number of days ago" -x
complete -c bfs -o mtime -d "Find files modified specified number of days ago" -x
complete -c bfs -o capable -d "Find files with capabilities set"
+complete -c bfs -o context -d "Find files by SELinux context" -x
complete -c bfs -o depth -d "Find files with specified number of depth" -x
complete -c bfs -o empty -d "Find empty files/directories"
complete -c bfs -o executable -d "Find files the current user can execute"
diff --git a/completions/bfs.zsh b/completions/bfs.zsh
index 07db456..432ab8c 100644
--- a/completions/bfs.zsh
+++ b/completions/bfs.zsh
@@ -74,6 +74,7 @@ args=(
'*-mtime[find files modified N days ago]:modification time (days):->times'
'*-capable[find files with POSIX.1e capabilities set]'
+ '*-context[find files by SELinux context]:pattern'
# -depth without parameters exist above. I don't know how to handle this gracefully
'*-empty[find empty files/directories]'
'*-executable[find files the current user can execute]'
diff --git a/configure b/configure
new file mode 100755
index 0000000..d42dec3
--- /dev/null
+++ b/configure
@@ -0,0 +1,137 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# bfs build configuration script
+
+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
+
+for arg; do
+ case "$arg" in
+ -h|--help)
+ cat <<EOF
+Usage:
+
+ \$ $0 [--enable-*|--disable-*] [CC=...] [CFLAGS=...] [...]
+ \$ $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 --enable or
+--disable them manually:
+
+ --enable-libacl --disable-libacl
+ --enable-libcap --disable-libcap
+ --enable-libselinux --disable-libselinux
+ --enable-liburing --disable-liburing
+ --enable-oniguruma --disable-oniguruma
+
+Packaging:
+
+ --prefix=/path
+ Set the installation prefix (default: /usr)
+
+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
+ exit 0
+ ;;
+
+ --enable-*|--disable-*)
+ case "$arg" in
+ --enable-*) yn=y ;;
+ --disable-*) yn=n ;;
+ esac
+
+ name="${arg#--*able-}"
+ NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_')
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ shift
+ set -- "$@" "USE_$NAME=$yn"
+ ;;
+ release|asan|lsan|msan|tsan|ubsan|lint|gcov)
+ shift
+ set -- "$@" "$NAME=$yn"
+ ;;
+ *)
+ printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+
+ --prefix=*)
+ shift
+ set -- "$@" "PREFIX=${arg#*=}"
+ ;;
+
+ MAKE=*)
+ MAKE="${arg#*=}"
+ shift
+ ;;
+
+ # make flag (-j2) or variable (CC=clang)
+ -*|*=*)
+ continue
+ ;;
+
+ *)
+ printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+ ;;
+ esac
+done
+
+# Get the relative path to the source tree based on how the script was run
+DIR=$(dirname -- "$0")
+
+# Set up symbolic links for out-of-tree builds
+for f in Makefile build completions docs src tests; do
+ test -e "$f" || ln -s "$DIR/$f" "$f"
+done
+
+# Set MAKEFLAGS to -j$(nproc) if it's unset
+export MAKEFLAGS="${MAKEFLAGS-$j}"
+
+$MAKE -rf build/config.mk "$@"
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
index 02f9756..cb33c51 100644
--- a/docs/BUILDING.md
+++ b/docs/BUILDING.md
@@ -1,105 +1,157 @@
Building `bfs`
==============
-Compiling
----------
-
-`bfs` uses [GNU Make](https://www.gnu.org/software/make/) as its build system.
A simple invocation of
+ $ ./configure
$ make
-should build `bfs` successfully, with no additional steps necessary.
-As usual with `make`, you can run a [parallel build](https://www.gnu.org/software/make/manual/html_node/Parallel.html) with `-j`.
-For example, to use all your cores, run `make -j$(nproc)`.
+should build `bfs` successfully.
-### Targets
-| Command | Description |
-|------------------|---------------------------------------------------------------|
-| `make` | Builds just the `bfs` binary |
-| `make all` | Builds everything, including the tests (but doesn't run them) |
-| `make check` | Builds everything, and runs the tests |
-| `make install` | Installs `bfs` (with man page, shell completions, etc.) |
-| `make uninstall` | Uninstalls `bfs` |
-
-### Flag-like targets
-
-The build system provides a few shorthand targets for handy configurations:
-
-| Command | Description |
-|----------------|-------------------------------------------------------------|
-| `make release` | Build `bfs` with optimizations, LTO, and without assertions |
-| `make asan` | Enable [AddressSanitizer] |
-| `make lsan` | Enable [LeakSanitizer] |
-| `make msan` | Enable [MemorySanitizer] |
-| `make tsan` | Enable [ThreadSanitizer] |
-| `make ubsan` | Enable [UndefinedBehaviorSanitizer] |
-| `make gcov` | Enable [code coverage] |
-
-[AddressSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizer
-[LeakSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode
-[MemorySanitizer]: https://github.com/google/sanitizers/wiki/MemorySanitizer
-[ThreadSanitizer]: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
-[UndefinedBehaviorSanitizer]: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
-[code coverage]: https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
-
-You can combine multiple flags and other targets (e.g. `make asan ubsan check`), but not all of them will work together.
-
-### Flags
-
-Other flags are controlled with `make` variables and/or environment variables.
-Here are some of the common ones; check the [`GNUmakefile`](/GNUmakefile) for more.
-
-| Flag | Description |
-|----------------------------------|---------------------------------------------|
-| `CC` | The C compiler to use, e.g. `make CC=clang` |
-| `CFLAGS`<br>`EXTRA_CFLAGS` | Override/add to the default compiler flags |
-| `LDFLAGS`<br>`EXTRA_LDFLAGS` | Override/add to the linker flags |
-| `USE_ACL`<br>`USE_ATTR`<br>... | Enable/disable [optional dependencies] |
-| `TEST_FLAGS` | `tests.sh` flags for `make check` |
-| `BUILDDIR` | The build output directory (default: `.`) |
-| `DESTDIR` | The root directory for `make install` |
-| `PREFIX` | The installation prefix (default: `/usr`) |
-| `MANDIR` | The man page installation directory |
-
-[optional dependencies]: #dependencies
+Configuration
+-------------
+
+```console
+$ ./configure --help
+Usage:
+
+ $ ./configure [--enable-*|--disable-*] [CC=...] [CFLAGS=...] [...]
+ $ make
+
+...
+```
+
+### Variables
+
+Variables set in the environment or on the command line will be picked up:
+These variables specify binaries to run during the configuration and build process:
+
+<pre>
+<b>MAKE</b>=<i>make</i>
+ <a href="https://en.wikipedia.org/wiki/Make_(software)">make</a> implementation
+<b>CC</b>=<i>cc</i>
+ C compiler
+<b>INSTALL</b>=<i>install</i>
+ Copy files during <i>make install</i>
+<b>MKDIR</b>="<i>mkdir -p</i>"
+ Create directories
+<b>PKG_CONFIG</b>=<i>pkg-config</i>
+ Detect external libraries and required build flags
+<b>RM</b>="<i>rm -f</i>"
+ Delete files
+</pre>
+
+These flags will be used by the build process:
+
+<pre>
+<b>CPPFLAGS</b>="<i>-I... -D...</i>"
+<b>CFLAGS</b>="<i>-W... -f...</i>"
+<b>LDFLAGS</b>="<i>-L... -Wl,...</i>"
+ Preprocessor/compiler/linker flags
+
+<b>LDLIBS</b>="<i>-l... -l...</i>"
+ Dynamic libraries to link
+
+<b>EXTRA_</b>{<b>CPPFLAGS</b>,<b>CFLAGS</b>,<b>LDFLAGS</b>,<b>LDLIBS</b>}="<i>...</i>"
+ Adds to the default flags, instead of replacing them
+</pre>
+
+### Build profiles
+
+The default flags result in a plain debug build.
+Other build profiles can be enabled:
+
+<pre>
+--enable-release
+ Enable optimizations, disable assertions
+
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizer">asan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode">lsan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/MemorySanitizer">msan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual">tsan</a>
+--enable-<a href="https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html">ubsan</a>
+ Enable sanitizers
+
+--enable-<a href="https://gcc.gnu.org/onlinedocs/gcc/gcov/introduction-to-gcov.html">gcov</a>
+ Enable code coverage instrumentation
+</pre>
+
+You can combine multiple profiles (e.g. `./configure --enable-asan --enable-ubsan`), but not all of them will work together.
### Dependencies
`bfs` depends on some system libraries for some of its features.
-These dependencies are optional, and can be turned off at build time if necessary by setting the appropriate variable to the empty string (e.g. `make USE_ONIGURUMA=`).
+External dependencies are auto-detected by default, but you can `--enable` or `--disable` them manually:
-| Dependency | Platforms | `make` flag |
-|-------------|------------|-----------------|
-| [acl] | Linux only | `USE_ACL` |
-| [attr] | Linux only | `USE_ATTR` |
-| [libcap] | Linux only | `USE_LIBCAP` |
-| [liburing] | Linux only | `USE_LIBURING` |
-| [Oniguruma] | All | `USE_ONIGURUMA` |
+<pre>
+--enable-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --disable-libacl
+--enable-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --disable-libcap
+--enable-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --disable-libselinux
+--enable-<a href="https://github.com/axboe/liburing">liburing</a> --disable-liburing
+--enable-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --disable-oniguruma
+</pre>
-[acl]: https://savannah.nongnu.org/projects/acl
-[attr]: https://savannah.nongnu.org/projects/attr
-[libcap]: https://sites.google.com/site/fullycapable/
-[liburing]: https://github.com/axboe/liburing
-[Oniguruma]: https://github.com/kkos/oniguruma
+[`pkg-config`] is used, if available, to detect these libraries and any additional build flags they may require.
+If this is undesireable, disable it by setting `PKG_CONFIG` to the empty string (`./configure PKG_CONFIG=""`).
-### Dependency tracking
+[`pkg-config`]: https://www.freedesktop.org/wiki/Software/pkg-config/
-The build system automatically tracks header dependencies with the `-M` family of compiler options (see `DEPFLAGS` in the [`GNUmakefile`](/GNUmakefile)).
-So if you edit a header file, `make` will rebuild the necessary object files ensuring they don't go out of sync.
+### Out-of-tree builds
-We go one step further than most build systems by tracking the flags that were used for the previous compilation.
-That means you can change configurations without having to `make clean`.
-For example,
+You can set up an out-of-tree build by running the `configure` script from another directory, for example:
+ $ mkdir out
+ $ cd out
+ $ ../configure
$ make
- $ make release
-will build the project in debug mode and then rebuild it in release mode.
-A side effect of this may be surprising: `make check` by itself will rebuild the project in the default configuration.
-To test a different configuration, you'll have to repeat it (e.g. `make release check`).
+Building
+--------
+
+### Targets
+
+The [`Makefile`](/Makefile) supports several different build targets:
+
+<pre>
+make
+ The default target; builds just the <i>bfs</i> binary
+make <b>all</b>
+ Builds everything, including the tests (but doesn't run them)
+
+make <b>check</b>
+ Builds everything, and runs all tests
+make <b>unit-tests</b>
+ Builds and runs the unit tests
+make <b>integration-tests</b>
+ Builds and runs the integration tests
+make <b>distcheck</b>
+ Builds and runs the tests in multiple different configurations
+
+make <b>install</b>
+ Installs bfs globally
+make <b>uninstall</b>
+ Uninstalls bfs
+
+make <b>clean</b>
+ Deletes all built files
+make <b>distclean</b>
+ Also deletes files generated by ./configure
+</pre>
+
+
+Troubleshooting
+---------------
+
+If the build fails or behaves unexpectedly, start by enabling verbose mode:
+
+ $ ./configure V=1
+ $ make V=1
+
+This will print the generated configuration and the exact commands that are executed.
+
+You can also check the file `gen/config.log`, which contains any errors from commands run during the configuration phase.
Testing
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 672c2b4..62b6480 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,36 @@
3.*
===
+3.2
+---
+
+**May 2, 2024**
+
+### New features
+
+- New `-limit N` action that quits immediately after `N` results
+
+- Implemented `-context` (from GNU find) for matching SELinux contexts ([#27](https://github.com/tavianator/bfs/issues/27))
+
+- Implemented `-printf %Z` for printing SELinux contexts
+
+### Changes
+
+- The build system has been rewritten, and there is now a configure step:
+
+ $ ./configure
+ $ make
+
+ See `./configure --help` or [docs/BUILDING.md](/docs/BUILDING.md) for more details.
+
+- Improved platform support
+ - Implemented `-acl` on Solaris/Illumos
+ - Implemented `-xattr` on DragonFly BSD
+
+### Bug fixes
+
+- Fixed some rarely-used code paths that clean up after allocation failures
+
3.1.3
-----
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 071c95b..70f8475 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -7,8 +7,8 @@ When invoked with no arguments, `bfs` will list everything under the current dir
```console
$ bfs
.
-./GNUmakefile
./LICENSE
+./Makefile
./README.md
./completions
./docs
diff --git a/docs/bfs.1 b/docs/bfs.1
index 3a4f15a..54166ab 100644
--- a/docs/bfs.1
+++ b/docs/bfs.1
@@ -452,6 +452,10 @@ Find files with POSIX.1e
.BR capabilities (7)
set.
.TP
+\fB\-context \fIGLOB\fR
+Find files whose SELinux context matches the
+.IR GLOB .
+.TP
\fB\-depth\fR [\fI\-+\fR]\fIN\fR
Find files with depth
.IR N .
diff --git a/src/alloc.c b/src/alloc.c
index b65d0c5..ebaff38 100644
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -1,9 +1,9 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "alloc.h"
#include "bit.h"
-#include "config.h"
#include "diag.h"
#include "sanity.h"
#include <errno.h>
@@ -24,12 +24,12 @@ static void *xmemalign(size_t align, size_t size) {
bfs_assert(align >= sizeof(void *));
bfs_assert(is_aligned(align, size));
-#if __APPLE__
+#if BFS_HAS_ALIGNED_ALLOC
+ return aligned_alloc(align, size);
+#else
void *ptr = NULL;
errno = posix_memalign(&ptr, align, size);
return ptr;
-#else
- return aligned_alloc(align, size);
#endif
}
diff --git a/src/alloc.h b/src/alloc.h
index ae055bc..095134a 100644
--- a/src/alloc.h
+++ b/src/alloc.h
@@ -8,7 +8,7 @@
#ifndef BFS_ALLOC_H
#define BFS_ALLOC_H
-#include "config.h"
+#include "prelude.h"
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
diff --git a/src/bar.c b/src/bar.c
index 8ab4112..184d9a0 100644
--- a/src/bar.c
+++ b/src/bar.c
@@ -1,11 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "bar.h"
#include "atomic.h"
#include "bfstd.h"
#include "bit.h"
-#include "config.h"
#include "dstring.h"
#include <errno.h>
#include <fcntl.h>
diff --git a/src/bfstd.c b/src/bfstd.c
index 2499f00..0ac3a72 100644
--- a/src/bfstd.c
+++ b/src/bfstd.c
@@ -1,9 +1,9 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "bfstd.h"
#include "bit.h"
-#include "config.h"
#include "diag.h"
#include "sanity.h"
#include "thread.h"
@@ -186,10 +186,10 @@ char *xgetdelim(FILE *file, char delim) {
const char *xgetprogname(void) {
const char *cmd = NULL;
-#if __GLIBC__
- cmd = program_invocation_short_name;
-#elif BSD
+#if BFS_HAS_GETPROGNAME
cmd = getprogname();
+#elif BFS_HAS_GETPROGNAME_GNU
+ cmd = program_invocation_short_name;
#endif
if (!cmd) {
@@ -252,40 +252,6 @@ int ynprompt(void) {
return ret;
}
-/** Get the single character describing the given file type. */
-static char type_char(mode_t mode) {
- switch (mode & S_IFMT) {
- case S_IFREG:
- return '-';
- case S_IFBLK:
- return 'b';
- case S_IFCHR:
- return 'c';
- case S_IFDIR:
- return 'd';
- case S_IFLNK:
- return 'l';
- case S_IFIFO:
- return 'p';
- case S_IFSOCK:
- return 's';
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return 'D';
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return 'P';
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return 'w';
-#endif
- }
-
- return '?';
-}
-
void *xmemdup(const void *src, size_t size) {
void *ret = malloc(size);
if (ret) {
@@ -317,35 +283,33 @@ const char *xstrerror(int errnum) {
const char *ret = NULL;
static thread_local char buf[256];
- // - __APPLE__
- // - __COSMOPOLITAN__
- // - No strerror_l()
- // - __FreeBSD__ && SANITIZE_MEMORY
- // - duplocale() triggers https://github.com/llvm/llvm-project/issues/65532
-#if __APPLE__ || __COSMOPOLITAN__ || (__FreeBSD__ && SANITIZE_MEMORY)
- if (strerror_r(errnum, buf, sizeof(buf)) == 0) {
- ret = buf;
- }
-#else
-# if __NetBSD__
- // NetBSD has no thread-specific locales
- locale_t loc = LC_GLOBAL_LOCALE;
-# else
+ // On FreeBSD with MemorySanitizer, duplocale() triggers
+ // https://github.com/llvm/llvm-project/issues/65532
+#if BFS_HAS_STRERROR_L && !(__FreeBSD__ && SANITIZE_MEMORY)
+# if BFS_HAS_USELOCALE
locale_t loc = uselocale((locale_t)0);
+# else
+ locale_t loc = LC_GLOBAL_LOCALE;
# endif
- locale_t copy = loc;
- if (copy == LC_GLOBAL_LOCALE) {
- copy = duplocale(copy);
+ bool free_loc = false;
+ if (loc == LC_GLOBAL_LOCALE) {
+ loc = duplocale(loc);
+ free_loc = true;
}
- if (copy != (locale_t)0) {
- ret = strerror_l(errnum, copy);
-
- if (loc == LC_GLOBAL_LOCALE) {
- freelocale(copy);
+ if (loc != (locale_t)0) {
+ ret = strerror_l(errnum, loc);
+ if (free_loc) {
+ freelocale(loc);
}
}
+#elif BFS_HAS_STRERROR_R_POSIX
+ if (strerror_r(errnum, buf, sizeof(buf)) == 0) {
+ ret = buf;
+ }
+#elif BFS_HAS_STRERROR_R_GNU
+ ret = strerror_r(errnum, buf, sizeof(buf));
#endif
if (!ret) {
@@ -358,6 +322,40 @@ const char *xstrerror(int errnum) {
return ret;
}
+/** Get the single character describing the given file type. */
+static char type_char(mode_t mode) {
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ return '-';
+ case S_IFBLK:
+ return 'b';
+ case S_IFCHR:
+ return 'c';
+ case S_IFDIR:
+ return 'd';
+ case S_IFLNK:
+ return 'l';
+ case S_IFIFO:
+ return 'p';
+ case S_IFSOCK:
+ return 's';
+#ifdef S_IFDOOR
+ case S_IFDOOR:
+ return 'D';
+#endif
+#ifdef S_IFPORT
+ case S_IFPORT:
+ return 'P';
+#endif
+#ifdef S_IFWHT
+ case S_IFWHT:
+ return 'w';
+#endif
+ }
+
+ return '?';
+}
+
void xstrmode(mode_t mode, char str[11]) {
strcpy(str, "----------");
@@ -489,7 +487,7 @@ int dup_cloexec(int fd) {
}
int pipe_cloexec(int pipefd[2]) {
-#if __linux__ || (BSD && !__APPLE__)
+#if BFS_HAS_PIPE2
return pipe2(pipefd, O_CLOEXEC);
#else
if (pipe(pipefd) != 0) {
@@ -581,10 +579,7 @@ int xfaccessat(int fd, const char *path, int amode) {
}
char *xconfstr(int name) {
-#if __ANDROID__
- errno = ENOTSUP;
- return NULL;
-#else
+#if BFS_HAS_CONFSTR
size_t len = confstr(name, NULL, 0);
if (len == 0) {
return NULL;
@@ -601,7 +596,10 @@ char *xconfstr(int name) {
}
return str;
-#endif // !__ANDROID__
+#else
+ errno = ENOTSUP;
+ return NULL;
+#endif
}
char *xreadlinkat(int fd, const char *path, size_t size) {
@@ -639,8 +637,14 @@ error:
return NULL;
}
+#if BFS_HAS_STRTOFFLAGS
+# define BFS_STRTOFFLAGS strtofflags
+#elif BFS_HAS_STRING_TO_FLAGS
+# define BFS_STRTOFFLAGS string_to_flags
+#endif
+
int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
-#if BSD && !__GNU__
+#ifdef BFS_STRTOFFLAGS
char *str_arg = (char *)*str;
#if __OpenBSD__
@@ -651,11 +655,7 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *
bfs_fflags_t set_arg = 0;
bfs_fflags_t clear_arg = 0;
-#if __NetBSD__
- int ret = string_to_flags(&str_arg, &set_arg, &clear_arg);
-#else
- int ret = strtofflags(&str_arg, &set_arg, &clear_arg);
-#endif
+ int ret = BFS_STRTOFFLAGS(&str_arg, &set_arg, &clear_arg);
*str = str_arg;
*set = set_arg;
@@ -665,7 +665,7 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *
errno = EINVAL;
}
return ret;
-#else // !BSD
+#else // !BFS_STRTOFFLAGS
errno = ENOTSUP;
return -1;
#endif
diff --git a/src/bfstd.h b/src/bfstd.h
index fc22971..f91e380 100644
--- a/src/bfstd.h
+++ b/src/bfstd.h
@@ -8,7 +8,7 @@
#ifndef BFS_BFSTD_H
#define BFS_BFSTD_H
-#include "config.h"
+#include "prelude.h"
#include "sanity.h"
#include <stddef.h>
@@ -283,11 +283,21 @@ int xminor(dev_t dev);
// #include <sys/stat.h>
-#if __APPLE__
-# define st_atim st_atimespec
-# define st_ctim st_ctimespec
-# define st_mtim st_mtimespec
-# define st_birthtim st_birthtimespec
+/**
+ * Get the access/change/modification time from a struct stat.
+ */
+#if BFS_HAS_ST_ACMTIM
+# define ST_ATIM(sb) (sb).st_atim
+# define ST_CTIM(sb) (sb).st_ctim
+# define ST_MTIM(sb) (sb).st_mtim
+#elif BFS_HAS_ST_ACMTIMESPEC
+# define ST_ATIM(sb) (sb).st_atimespec
+# define ST_CTIM(sb) (sb).st_ctimespec
+# define ST_MTIM(sb) (sb).st_mtimespec
+#else
+# define ST_ATIM(sb) ((struct timespec) { .tv_sec = (sb).st_atime })
+# define ST_CTIM(sb) ((struct timespec) { .tv_sec = (sb).st_ctime })
+# define ST_MTIM(sb) ((struct timespec) { .tv_sec = (sb).st_mtime })
#endif
// #include <sys/wait.h>
diff --git a/src/bftw.c b/src/bftw.c
index 6130c44..c4d3c17 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -18,10 +18,10 @@
* various helper functions to take fewer parameters.
*/
+#include "prelude.h"
#include "bftw.h"
#include "alloc.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
diff --git a/src/bftw.h b/src/bftw.h
index 2805361..8656ca7 100644
--- a/src/bftw.h
+++ b/src/bftw.h
@@ -54,7 +54,7 @@ struct BFTW {
/** The file type. */
enum bfs_type type;
- /** The errno that occurred, if type == BFTW_ERROR. */
+ /** The errno that occurred, if type == BFS_ERROR. */
int error;
/** A parent file descriptor for the *at() family of calls. */
@@ -104,7 +104,7 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat
* @param flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
- * The type of the file, or BFTW_ERROR if an error occurred.
+ * The type of the file, or BFS_ERROR if an error occurred.
*/
enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags);
diff --git a/src/bit.h b/src/bit.h
index 69df21e..17cfbcf 100644
--- a/src/bit.h
+++ b/src/bit.h
@@ -8,7 +8,7 @@
#ifndef BFS_BIT_H
#define BFS_BIT_H
-#include "config.h"
+#include "prelude.h"
#include <limits.h>
#include <stdint.h>
diff --git a/src/color.c b/src/color.c
index 8c32a68..f004bf2 100644
--- a/src/color.c
+++ b/src/color.c
@@ -1,11 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "color.h"
#include "alloc.h"
#include "bfstd.h"
#include "bftw.h"
-#include "config.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
diff --git a/src/color.h b/src/color.h
index e3e7973..3278cd6 100644
--- a/src/color.h
+++ b/src/color.h
@@ -8,7 +8,7 @@
#ifndef BFS_COLOR_H
#define BFS_COLOR_H
-#include "config.h"
+#include "prelude.h"
#include "dstring.h"
#include <stdio.h>
diff --git a/src/ctx.c b/src/ctx.c
index f5b28c7..aa73b35 100644
--- a/src/ctx.c
+++ b/src/ctx.c
@@ -237,7 +237,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) {
if (bfs_ctx_fclose(ctx, ctx_file) != 0) {
if (cerr) {
- bfs_error(ctx, "'%s': %m.\n", ctx_file->path);
+ bfs_error(ctx, "%pq: %m.\n", ctx_file->path);
}
ret = -1;
}
diff --git a/src/ctx.h b/src/ctx.h
index e14db21..fc3020c 100644
--- a/src/ctx.h
+++ b/src/ctx.h
@@ -8,9 +8,9 @@
#ifndef BFS_CTX_H
#define BFS_CTX_H
+#include "prelude.h"
#include "alloc.h"
#include "bftw.h"
-#include "config.h"
#include "diag.h"
#include "expr.h"
#include "trie.h"
diff --git a/src/diag.c b/src/diag.c
index cb27b92..deb6f26 100644
--- a/src/diag.c
+++ b/src/diag.c
@@ -1,11 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "diag.h"
#include "alloc.h"
#include "bfstd.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "dstring.h"
#include "expr.h"
diff --git a/src/diag.h b/src/diag.h
index 4054c48..2b13609 100644
--- a/src/diag.h
+++ b/src/diag.h
@@ -8,7 +8,7 @@
#ifndef BFS_DIAG_H
#define BFS_DIAG_H
-#include "config.h"
+#include "prelude.h"
#include <stdarg.h>
/**
diff --git a/src/dir.c b/src/dir.c
index 98518f2..cfbbca4 100644
--- a/src/dir.c
+++ b/src/dir.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "dir.h"
#include "alloc.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
#include "sanity.h"
#include "trie.h"
@@ -17,7 +17,7 @@
#include <unistd.h>
#if BFS_USE_GETDENTS
-# if __linux__
+# if BFS_HAS_GETDENTS64_SYSCALL
# include <sys/syscall.h>
# endif
@@ -25,12 +25,14 @@
static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
sanitize_uninit(buf, size);
-#if (__linux__ && __GLIBC__ && !__GLIBC_PREREQ(2, 30)) || __ANDROID__
- ssize_t ret = syscall(SYS_getdents64, fd, buf, size);
-#elif __linux__
+#if BFS_HAS_GETDENTS
+ ssize_t ret = getdents(fd, buf, size);
+#elif BFS_HAS_GETDENTS64
ssize_t ret = getdents64(fd, buf, size);
+#elif BFS_HAS_GETDENTS64_SYSCALL
+ ssize_t ret = syscall(SYS_getdents64, fd, buf, size);
#else
- ssize_t ret = getdents(fd, buf, size);
+# error "No getdents() implementation"
#endif
if (ret > 0) {
@@ -42,7 +44,7 @@ static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
#endif // BFS_USE_GETDENTS
-#if BFS_USE_GETDENTS && __linux__
+#if BFS_USE_GETDENTS && !BFS_HAS_GETDENTS
/** Directory entry type for bfs_getdents() */
typedef struct dirent64 sys_dirent;
#else
@@ -351,7 +353,7 @@ int bfs_closedir(struct bfs_dir *dir) {
int bfs_unwrapdir(struct bfs_dir *dir) {
#if BFS_USE_GETDENTS
int ret = dir->fd;
-#elif __FreeBSD__
+#elif BFS_HAS_FDCLOSEDIR
int ret = fdclosedir(dir->dir);
#endif
diff --git a/src/dir.h b/src/dir.h
index 18d907e..6d5c9c5 100644
--- a/src/dir.h
+++ b/src/dir.h
@@ -8,14 +8,15 @@
#ifndef BFS_DIR_H
#define BFS_DIR_H
+#include "prelude.h"
#include <sys/types.h>
/**
* Whether the implementation uses the getdents() syscall directly, rather than
* libc's readdir().
*/
-#ifndef BFS_USE_GETDENTS
-# define BFS_USE_GETDENTS (__linux__ || __FreeBSD__)
+#if !defined(BFS_USE_GETDENTS) && (__linux__ || __FreeBSD__)
+# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL)
#endif
/**
@@ -152,7 +153,7 @@ int bfs_closedir(struct bfs_dir *dir);
* Whether the bfs_unwrapdir() function is supported.
*/
#ifndef BFS_USE_UNWRAPDIR
-# define BFS_USE_UNWRAPDIR (BFS_USE_GETDENTS || __FreeBSD__)
+# define BFS_USE_UNWRAPDIR (BFS_USE_GETDENTS || BFS_HAS_FDCLOSEDIR)
#endif
#if BFS_USE_UNWRAPDIR
diff --git a/src/dstring.c b/src/dstring.c
index 10b0fad..b5bf3d3 100644
--- a/src/dstring.c
+++ b/src/dstring.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "dstring.h"
#include "alloc.h"
#include "bit.h"
-#include "config.h"
#include "diag.h"
#include <stdarg.h>
#include <stddef.h>
@@ -245,6 +245,7 @@ int dstrvcatf(dchar **str, const char *format, va_list args) {
return 0;
fail:
+ va_end(copy);
*tail = '\0';
return -1;
}
diff --git a/src/dstring.h b/src/dstring.h
index 6006199..9ea7eb9 100644
--- a/src/dstring.h
+++ b/src/dstring.h
@@ -8,8 +8,8 @@
#ifndef BFS_DSTRING_H
#define BFS_DSTRING_H
+#include "prelude.h"
#include "bfstd.h"
-#include "config.h"
#include <stdarg.h>
#include <stddef.h>
diff --git a/src/eval.c b/src/eval.c
index 2f06858..49028b7 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -5,12 +5,12 @@
* Implementation of all the primary expressions.
*/
+#include "prelude.h"
#include "eval.h"
#include "bar.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
@@ -145,6 +145,20 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) {
return false;
}
+/** Common code for fnmatch() tests. */
+static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
+ if (expr->literal) {
+#ifdef FNM_CASEFOLD
+ if (expr->fnm_flags & FNM_CASEFOLD) {
+ return strcasecmp(expr->pattern, str) == 0;
+ }
+#endif
+ return strcmp(expr->pattern, str) == 0;
+ } else {
+ return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
+ }
+}
+
/**
* -true test.
*/
@@ -194,6 +208,21 @@ bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) {
}
/**
+ * -context test.
+ */
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) {
+ char *con = bfs_getfilecon(state->ftwbuf);
+ if (!con) {
+ eval_report_error(state);
+ return false;
+ }
+
+ bool ret = eval_fnmatch(expr, con);
+ bfs_freecon(con);
+ return ret;
+}
+
+/**
* Get the given timespec field out of a stat buffer.
*/
static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) {
@@ -416,38 +445,42 @@ bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) {
* -empty test.
*/
bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) {
- bool ret = false;
const struct BFTW *ftwbuf = state->ftwbuf;
+ const struct bfs_stat *statbuf;
+ struct bfs_dir *dir;
- if (ftwbuf->type == BFS_DIR) {
- struct bfs_dir *dir = bfs_allocdir();
+ switch (ftwbuf->type) {
+ case BFS_REG:
+ statbuf = eval_stat(state);
+ return statbuf && statbuf->size == 0;
+
+ case BFS_DIR:
+ dir = bfs_allocdir();
if (!dir) {
- eval_report_error(state);
- return ret;
+ goto error;
}
if (bfs_opendir(dir, ftwbuf->at_fd, ftwbuf->at_path, 0) != 0) {
- eval_report_error(state);
- return ret;
+ goto error;
}
int did_read = bfs_readdir(dir, NULL);
+ bfs_closedir(dir);
+
if (did_read < 0) {
- eval_report_error(state);
- } else {
- ret = !did_read;
+ goto error;
}
- bfs_closedir(dir);
free(dir);
- } else if (ftwbuf->type == BFS_REG) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (statbuf) {
- ret = statbuf->size == 0;
- }
- }
+ return did_read == 0;
+ error:
+ eval_report_error(state);
+ free(dir);
+ return false;
- return ret;
+ default:
+ return false;
+ }
}
/**
@@ -546,20 +579,6 @@ bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state) {
return bfs_expr_cmp(expr, statbuf->nlink);
}
-/** Common code for fnmatch() tests. */
-static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
- if (expr->literal) {
-#ifdef FNM_CASEFOLD
- if (expr->fnm_flags & FNM_CASEFOLD) {
- return strcasecmp(expr->pattern, str) == 0;
- }
-#endif
- return strcmp(expr->pattern, str) == 0;
- } else {
- return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
- }
-}
-
/**
* -i?lname test.
*/
diff --git a/src/eval.h b/src/eval.h
index f7f6c77..4dd7996 100644
--- a/src/eval.h
+++ b/src/eval.h
@@ -9,7 +9,7 @@
#ifndef BFS_EVAL_H
#define BFS_EVAL_H
-#include "config.h"
+#include "prelude.h"
struct bfs_ctx;
struct bfs_expr;
@@ -49,6 +49,7 @@ bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state);
diff --git a/src/exec.c b/src/exec.c
index 60bfd28..e782d49 100644
--- a/src/exec.c
+++ b/src/exec.c
@@ -1,12 +1,12 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "exec.h"
#include "alloc.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "dstring.h"
diff --git a/src/expr.h b/src/expr.h
index 75cb5fd..7bcace7 100644
--- a/src/expr.h
+++ b/src/expr.h
@@ -8,8 +8,8 @@
#ifndef BFS_EXPR_H
#define BFS_EXPR_H
+#include "prelude.h"
#include "color.h"
-#include "config.h"
#include "eval.h"
#include "stat.h"
#include <sys/types.h>
diff --git a/src/fsade.c b/src/fsade.c
index ee17416..d56fb07 100644
--- a/src/fsade.c
+++ b/src/fsade.c
@@ -1,11 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "fsade.h"
#include "atomic.h"
#include "bfstd.h"
#include "bftw.h"
-#include "config.h"
#include "dir.h"
#include "dstring.h"
#include "sanity.h"
@@ -22,6 +22,10 @@
# include <sys/capability.h>
#endif
+#if BFS_CAN_CHECK_CONTEXT
+# include <selinux/selinux.h>
+#endif
+
#if BFS_USE_SYS_EXTATTR_H
# include <sys/extattr.h>
#elif BFS_USE_SYS_XATTR_H
@@ -117,11 +121,23 @@ static bool is_absence_error(int error) {
#if BFS_CAN_CHECK_ACL
+#if BFS_HAS_ACL_GET_FILE
+
/** Unified interface for incompatible acl_get_entry() implementations. */
static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
-#if __DragonFly__ && !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
-# define ACL_FIRST_ENTRY 0
-# define ACL_NEXT_ENTRY 1
+#if BFS_HAS_ACL_GET_ENTRY
+ int ret = acl_get_entry(acl, which, entry);
+# if __APPLE__
+ // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
+ return !ret;
+# else
+ return ret;
+# endif
+#elif __DragonFly__
+# if !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
+# define ACL_FIRST_ENTRY 0
+# define ACL_NEXT_ENTRY 1
+# endif
switch (which) {
case ACL_FIRST_ENTRY:
@@ -138,24 +154,22 @@ static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
acl_entry_t last = &acl->acl_entry[acl->acl_cnt];
return *entry == last;
#else
- int ret = acl_get_entry(acl, which, entry);
-# if __APPLE__
- // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
- return !ret;
-# else
- return ret;
-# endif
+ errno = ENOTSUP;
+ return -1;
#endif
}
/** Unified interface for acl_get_tag_type(). */
attr(maybe_unused)
static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) {
-#if __DragonFly__
+#if BFS_HAS_ACL_GET_TAG_TYPE
+ return acl_get_tag_type(entry, tag);
+#elif __DragonFly__
*tag = entry->ae_tag;
return 0;
#else
- return acl_get_tag_type(entry, tag);
+ errno = ENOTSUP;
+ return -1;
#endif
}
@@ -195,7 +209,7 @@ static int bfs_check_acl_type(acl_t acl, acl_type_t type) {
return bfs_check_posix1e_acl(acl, false);
}
-#if __FreeBSD__
+#if BFS_HAS_ACL_IS_TRIVIAL_NP
int trivial;
int ret = acl_is_trivial_np(acl, &trivial);
@@ -209,34 +223,40 @@ static int bfs_check_acl_type(acl_t acl, acl_type_t type) {
} else {
return 1;
}
-#else // !__FreeBSD__
+#else
return bfs_check_posix1e_acl(acl, true);
#endif
}
+#endif // BFS_HAS_ACL_GET_FILE
+
int bfs_check_acl(const struct BFTW *ftwbuf) {
+ if (ftwbuf->type == BFS_LNK) {
+ return 0;
+ }
+
+ const char *path = fake_at(ftwbuf);
+
+#if BFS_HAS_ACL_TRIVIAL
+ int ret = acl_trivial(path);
+ int error = errno;
+#elif BFS_HAS_ACL_GET_FILE
static const acl_type_t acl_types[] = {
-#if __APPLE__
+# if __APPLE__
// macOS gives EINVAL for either of the two standard ACL types,
// supporting only ACL_TYPE_EXTENDED
ACL_TYPE_EXTENDED,
-#else
+# else
// The two standard POSIX.1e ACL types
ACL_TYPE_ACCESS,
ACL_TYPE_DEFAULT,
-#endif
+# endif
-#ifdef ACL_TYPE_NFS4
+# ifdef ACL_TYPE_NFS4
ACL_TYPE_NFS4,
-#endif
+# endif
};
- if (ftwbuf->type == BFS_LNK) {
- return 0;
- }
-
- const char *path = fake_at(ftwbuf);
-
int ret = -1, error = 0;
for (size_t i = 0; i < countof(acl_types) && ret <= 0; ++i) {
acl_type_t type = acl_types[i];
@@ -260,6 +280,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) {
error = errno;
acl_free(acl);
}
+#endif
free_fake_at(ftwbuf, path);
errno = error;
@@ -323,17 +344,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) {
#if BFS_CAN_CHECK_XATTRS
+#if BFS_USE_SYS_EXTATTR_H
+
+/** Wrapper for extattr_list_{file,link}. */
+static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_LIST_LINK
+ return extattr_list_link(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_LIST_FILE
+ return extattr_list_file(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_FILE
+ // From man extattr(2):
+ //
+ // In earlier versions of this API, passing an empty string for the
+ // attribute name to extattr_get_file() would return the list of attributes
+ // defined for the target object. This interface has been deprecated in
+ // preference to using the explicit list API, and should not be used.
+ return extattr_get_file(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+/** Wrapper for extattr_get_{file,link}. */
+static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_GET_FILE
+ return extattr_get_file(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+#endif // BFS_USE_SYS_EXTATTR_H
+
int bfs_check_xattrs(const struct BFTW *ftwbuf) {
const char *path = fake_at(ftwbuf);
ssize_t len;
#if BFS_USE_SYS_EXTATTR_H
- ssize_t (*extattr_list)(const char *, int, void *, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_list_link : extattr_list_file;
-
- len = extattr_list(path, EXTATTR_NAMESPACE_SYSTEM, NULL, 0);
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM);
if (len <= 0) {
- len = extattr_list(path, EXTATTR_NAMESPACE_USER, NULL, 0);
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
@@ -367,12 +433,9 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
ssize_t len;
#if BFS_USE_SYS_EXTATTR_H
- ssize_t (*extattr_get)(const char *, int, const char *, void *, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_get_link : extattr_get_file;
-
- len = extattr_get(path, EXTATTR_NAMESPACE_SYSTEM, name, NULL, 0);
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name);
if (len < 0) {
- len = extattr_get(path, EXTATTR_NAMESPACE_USER, name, NULL, 0);
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
@@ -414,3 +477,32 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
}
#endif
+
+char *bfs_getfilecon(const struct BFTW *ftwbuf) {
+#if BFS_CAN_CHECK_CONTEXT
+ const char *path = fake_at(ftwbuf);
+
+ char *con;
+ int ret;
+ if (ftwbuf->type == BFS_LNK) {
+ ret = lgetfilecon(path, &con);
+ } else {
+ ret = getfilecon(path, &con);
+ }
+
+ if (ret >= 0) {
+ return con;
+ } else {
+ return NULL;
+ }
+#else
+ errno = ENOTSUP;
+ return NULL;
+#endif
+}
+
+void bfs_freecon(char *con) {
+#if BFS_CAN_CHECK_CONTEXT
+ freecon(con);
+#endif
+}
diff --git a/src/fsade.h b/src/fsade.h
index 413938d..eefef9f 100644
--- a/src/fsade.h
+++ b/src/fsade.h
@@ -9,16 +9,13 @@
#ifndef BFS_FSADE_H
#define BFS_FSADE_H
-#include "config.h"
+#include "prelude.h"
-#define BFS_CAN_CHECK_ACL BFS_USE_SYS_ACL_H
+#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL)
-#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_USE_SYS_CAPABILITY_H
-# include <sys/capability.h>
-# ifdef CAP_CHOWN
-# define BFS_CAN_CHECK_CAPABILITIES true
-# endif
-#endif
+#define BFS_CAN_CHECK_CAPABILITIES BFS_USE_LIBCAP
+
+#define BFS_CAN_CHECK_CONTEXT BFS_USE_LIBSELINUX
#define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H)
@@ -66,4 +63,19 @@ int bfs_check_xattrs(const struct BFTW *ftwbuf);
*/
int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name);
+/**
+ * Get a file's SELinux context
+ *
+ * @param ftwbuf
+ * The file to check.
+ * @return
+ * The file's SELinux context, or NULL on failure.
+ */
+char *bfs_getfilecon(const struct BFTW *ftwbuf);
+
+/**
+ * Free a bfs_getfilecon() result.
+ */
+void bfs_freecon(char *con);
+
#endif // BFS_FSADE_H
diff --git a/src/ioq.c b/src/ioq.c
index b936681..43a1b35 100644
--- a/src/ioq.c
+++ b/src/ioq.c
@@ -118,12 +118,12 @@
* [1]: https://arxiv.org/abs/2201.02179
*/
+#include "prelude.h"
#include "ioq.h"
#include "alloc.h"
#include "atomic.h"
#include "bfstd.h"
#include "bit.h"
-#include "config.h"
#include "diag.h"
#include "dir.h"
#include "stat.h"
@@ -918,17 +918,14 @@ static void ioq_thread_join(struct ioq_thread *thread) {
}
struct ioq *ioq_create(size_t depth, size_t nthreads) {
- struct ioq *ioq = ALLOC_FLEX(struct ioq, threads, nthreads);
+ struct ioq *ioq = ZALLOC_FLEX(struct ioq, threads, nthreads);
if (!ioq) {
goto fail;
}
ioq->depth = depth;
- ioq->size = 0;
- ioq->cancel = false;
ARENA_INIT(&ioq->ents, struct ioq_ent);
-
#if BFS_USE_LIBURING && BFS_USE_STATX
ARENA_INIT(&ioq->xbufs, struct statx);
#endif
diff --git a/src/ioq.h b/src/ioq.h
index 818eea6..d8e1573 100644
--- a/src/ioq.h
+++ b/src/ioq.h
@@ -8,7 +8,7 @@
#ifndef BFS_IOQ_H
#define BFS_IOQ_H
-#include "config.h"
+#include "prelude.h"
#include "dir.h"
#include "stat.h"
#include <stddef.h>
diff --git a/src/main.c b/src/main.c
index e120f03..9d8b206 100644
--- a/src/main.c
+++ b/src/main.c
@@ -26,7 +26,7 @@
* - bit.h (bit manipulation)
* - bfstd.[ch] (standard library wrappers/polyfills)
* - color.[ch] (for pretty terminal colors)
- * - config.h (configuration and feature/platform detection)
+ * - prelude.h (configuration and feature/platform detection)
* - diag.[ch] (formats diagnostic messages)
* - dir.[ch] (a directory API facade)
* - dstring.[ch] (a dynamic string library)
@@ -45,8 +45,8 @@
* - xtime.[ch] (date/time handling utilities)
*/
+#include "prelude.h"
#include "bfstd.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "eval.h"
diff --git a/src/mtab.c b/src/mtab.c
index c76f403..0377fea 100644
--- a/src/mtab.c
+++ b/src/mtab.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "mtab.h"
#include "alloc.h"
#include "bfstd.h"
-#include "config.h"
#include "stat.h"
#include "trie.h"
#include <errno.h>
@@ -13,11 +13,11 @@
#include <string.h>
#include <sys/types.h>
-#if !defined(BFS_USE_MNTENT) && BFS_USE_MNTENT_H
+#if !defined(BFS_USE_MNTENT) && BFS_HAS_GETMNTENT_1
# define BFS_USE_MNTENT true
-#elif !defined(BFS_USE_MNTINFO) && BSD
+#elif !defined(BFS_USE_MNTINFO) && BFS_HAS_GETMNTINFO
# define BFS_USE_MNTINFO true
-#elif !defined(BFS_USE_MNTTAB) && __SVR4
+#elif !defined(BFS_USE_MNTTAB) && BFS_HAS_GETMNTENT_2
# define BFS_USE_MNTTAB true
#endif
@@ -27,7 +27,6 @@
# include <stdio.h>
#elif BFS_USE_MNTINFO
# include <sys/mount.h>
-# include <sys/ucred.h>
#elif BFS_USE_MNTTAB
# include <stdio.h>
# include <sys/mnttab.h>
@@ -41,11 +40,16 @@ struct bfs_mount {
char *path;
/** The filesystem type. */
char *type;
+ /** Buffer for the strings. */
+ char buf[];
};
struct bfs_mtab {
+ /** Mount point arena. */
+ struct varena varena;
+
/** The array of mount points. */
- struct bfs_mount *mounts;
+ struct bfs_mount **mounts;
/** The number of mount points. */
size_t nmounts;
@@ -63,28 +67,37 @@ struct bfs_mtab {
*/
attr(maybe_unused)
static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) {
- struct bfs_mount *mount = RESERVE(struct bfs_mount, &mtab->mounts, &mtab->nmounts);
+ size_t path_size = strlen(path) + 1;
+ size_t type_size = strlen(type) + 1;
+ size_t size = path_size + type_size;
+ struct bfs_mount *mount = varena_alloc(&mtab->varena, size);
if (!mount) {
return -1;
}
- mount->path = strdup(path);
- mount->type = strdup(type);
- if (!mount->path || !mount->type) {
- goto fail;
+ struct bfs_mount **ptr = RESERVE(struct bfs_mount *, &mtab->mounts, &mtab->nmounts);
+ if (!ptr) {
+ goto free;
}
+ *ptr = mount;
+
+ mount->path = mount->buf;
+ memcpy(mount->path, path, path_size);
+
+ mount->type = mount->buf + path_size;
+ memcpy(mount->type, type, type_size);
const char *name = path + xbaseoff(path);
if (!trie_insert_str(&mtab->names, name)) {
- goto fail;
+ goto shrink;
}
return 0;
-fail:
- free(mount->type);
- free(mount->path);
+shrink:
--mtab->nmounts;
+free:
+ varena_free(&mtab->varena, mount, size);
return -1;
}
@@ -94,6 +107,8 @@ struct bfs_mtab *bfs_mtab_parse(void) {
return NULL;
}
+ VARENA_INIT(&mtab->varena, struct bfs_mount, buf);
+
trie_init(&mtab->names);
trie_init(&mtab->types);
@@ -132,7 +147,7 @@ struct bfs_mtab *bfs_mtab_parse(void) {
bfs_statfs *mntbuf;
int size = getmntinfo(&mntbuf, MNT_WAIT);
- if (size < 0) {
+ if (size <= 0) {
error = errno;
goto fail;
}
@@ -195,7 +210,7 @@ static int bfs_mtab_fill_types(struct bfs_mtab *mtab) {
struct bfs_stat parent_stat;
for (size_t i = 0; i < mtab->nmounts; ++i) {
- struct bfs_mount *mount = &mtab->mounts[i];
+ struct bfs_mount *mount = mtab->mounts[i];
const char *path = mount->path;
int fd = AT_FDCWD;
@@ -280,11 +295,8 @@ void bfs_mtab_free(struct bfs_mtab *mtab) {
trie_destroy(&mtab->types);
trie_destroy(&mtab->names);
- for (size_t i = 0; i < mtab->nmounts; ++i) {
- free(mtab->mounts[i].type);
- free(mtab->mounts[i].path);
- }
free(mtab->mounts);
+ varena_destroy(&mtab->varena);
free(mtab);
}
diff --git a/src/mtab.h b/src/mtab.h
index d99d78f..67290c2 100644
--- a/src/mtab.h
+++ b/src/mtab.h
@@ -8,7 +8,7 @@
#ifndef BFS_MTAB_H
#define BFS_MTAB_H
-#include "config.h"
+#include "prelude.h"
struct bfs_stat;
diff --git a/src/opt.c b/src/opt.c
index b74b4e1..883d598 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -25,11 +25,11 @@
* effects are reachable at all, skipping the traversal if not.
*/
+#include "prelude.h"
#include "opt.h"
#include "bftw.h"
#include "bit.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
@@ -947,8 +947,12 @@ static struct bfs_expr *visit_shallow(struct bfs_opt *opt, struct bfs_expr *expr
expr = general(opt, expr, visitor);
}
+ if (!expr) {
+ return NULL;
+ }
+
visit_fn *specific = look_up_visitor(expr, visitor->table);
- if (expr && specific) {
+ if (specific) {
expr = specific(opt, expr, visitor);
}
diff --git a/src/parse.c b/src/parse.c
index 2dfcab2..a1155c0 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -8,12 +8,12 @@
* flags like always-true options, and skipping over paths wherever they appear.
*/
+#include "prelude.h"
#include "parse.h"
#include "alloc.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
@@ -1076,6 +1076,67 @@ static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int ar
}
/**
+ * Common code for fnmatch() tests.
+ */
+static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->pattern = expr->argv[1];
+
+ if (casefold) {
+#ifdef FNM_CASEFOLD
+ expr->fnm_flags = FNM_CASEFOLD;
+#else
+ parse_expr_error(parser, expr, "Missing platform support.\n");
+ return NULL;
+#endif
+ } else {
+ expr->fnm_flags = 0;
+ }
+
+ // POSIX says, about fnmatch():
+ //
+ // If pattern ends with an unescaped <backslash>, fnmatch() shall
+ // return a non-zero value (indicating either no match or an error).
+ //
+ // But not all implementations obey this, so check for it ourselves.
+ size_t i, len = strlen(expr->pattern);
+ for (i = 0; i < len; ++i) {
+ if (expr->pattern[len - i - 1] != '\\') {
+ break;
+ }
+ }
+ if (i % 2 != 0) {
+ parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
+ expr->eval_fn = eval_false;
+ return expr;
+ }
+
+ // strcmp() can be much faster than fnmatch() since it doesn't have to
+ // parse the pattern, so special-case patterns with no wildcards.
+ //
+ // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
+ expr->literal = strcspn(expr->pattern, "?*\\[") == len;
+
+ return expr;
+}
+
+/**
+ * Parse -context.
+ */
+static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_CONTEXT
+ struct bfs_expr *expr = parse_unary_test(parser, eval_context);
+ return parse_fnmatch(parser, expr, false);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
* Parse -{false,true}.
*/
static struct bfs_expr *parse_const(struct bfs_parser *parser, int value, int arg2) {
@@ -1632,54 +1693,6 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg
}
/**
- * Common code for fnmatch() tests.
- */
-static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
- if (!expr) {
- return NULL;
- }
-
- expr->pattern = expr->argv[1];
-
- if (casefold) {
-#ifdef FNM_CASEFOLD
- expr->fnm_flags = FNM_CASEFOLD;
-#else
- parse_expr_error(parser, expr, "Missing platform support.\n");
- return NULL;
-#endif
- } else {
- expr->fnm_flags = 0;
- }
-
- // POSIX says, about fnmatch():
- //
- // If pattern ends with an unescaped <backslash>, fnmatch() shall
- // return a non-zero value (indicating either no match or an error).
- //
- // But not all implementations obey this, so check for it ourselves.
- size_t i, len = strlen(expr->pattern);
- for (i = 0; i < len; ++i) {
- if (expr->pattern[len - i - 1] != '\\') {
- break;
- }
- }
- if (i % 2 != 0) {
- parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
- expr->eval_fn = eval_false;
- return expr;
- }
-
- // strcmp() can be much faster than fnmatch() since it doesn't have to
- // parse the pattern, so special-case patterns with no wildcards.
- //
- // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
- expr->literal = strcspn(expr->pattern, "?*\\[") == len;
-
- return expr;
-}
-
-/**
* Parse -i?name.
*/
static struct bfs_expr *parse_name(struct bfs_parser *parser, int casefold, int arg2) {
@@ -1742,7 +1755,7 @@ static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr)
fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday);
fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
-#if __FreeBSD__
+#if BFS_HAS_TM_GMTOFF
int gmtoff = tm.tm_gmtoff;
#else
int gmtoff = -timezone;
@@ -2768,6 +2781,10 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2
cfprintf(cout, " ${blu}-capable${rs}\n");
cfprintf(cout, " Find files with POSIX.1e capabilities set\n");
#endif
+#if BFS_CAN_CHECK_CONTEXT
+ cfprintf(cout, " ${blu}-context${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find files with SELinux context matching a glob pattern\n");
+#endif
cfprintf(cout, " ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
cfprintf(cout, " Find files with depth ${bld}N${rs}\n");
cfprintf(cout, " ${blu}-empty${rs}\n");
@@ -2909,7 +2926,7 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2
* "Parse" -version.
*/
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);
+ cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version);
printf("%s\n", BFS_HOMEPAGE);
@@ -2961,6 +2978,7 @@ static const struct table_entry parse_table[] = {
{"-cmin", T_TEST, parse_min, BFS_STAT_CTIME},
{"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME},
{"-color", T_OPTION, parse_color, true},
+ {"-context", T_TEST, parse_context, true},
{"-csince", T_TEST, parse_since, BFS_STAT_CTIME},
{"-ctime", T_TEST, parse_time, BFS_STAT_CTIME},
{"-d", T_FLAG, parse_depth},
diff --git a/src/config.h b/src/prelude.h
index 892f24c..72f88b0 100644
--- a/src/config.h
+++ b/src/prelude.h
@@ -5,8 +5,8 @@
* Configuration and feature/platform detection.
*/
-#ifndef BFS_CONFIG_H
-#define BFS_CONFIG_H
+#ifndef BFS_PRELUDE_H
+#define BFS_PRELUDE_H
// Possible __STDC_VERSION__ values
@@ -26,16 +26,19 @@
// bfs packaging configuration
+#include "config.h"
+
#ifndef BFS_COMMAND
# define BFS_COMMAND "bfs"
#endif
-#ifndef BFS_VERSION
-# define BFS_VERSION "3.1.3"
-#endif
#ifndef BFS_HOMEPAGE
# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html"
#endif
+// This is a symbol instead of a literal so we don't have to rebuild everything
+// when the version number changes
+extern const char bfs_version[];
+
// Check for system headers
#ifdef __has_include
@@ -46,12 +49,6 @@
#if __has_include(<paths.h>)
# define BFS_HAS_PATHS_H true
#endif
-#if __has_include(<sys/acl.h>)
-# define BFS_HAS_SYS_ACL_H true
-#endif
-#if __has_include(<sys/capability.h>)
-# define BFS_HAS_SYS_CAPABILITY_H true
-#endif
#if __has_include(<sys/extattr.h>)
# define BFS_HAS_SYS_EXTATTR_H true
#endif
@@ -78,8 +75,6 @@
#define BFS_HAS_MNTENT_H __GLIBC__
#define BFS_HAS_PATHS_H true
-#define BFS_HAS_SYS_ACL_H true
-#define BFS_HAS_SYS_CAPABILITY_H __linux__
#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__
#define BFS_HAS_SYS_MKDEV_H false
#define BFS_HAS_SYS_PARAM_H true
@@ -96,14 +91,8 @@
#ifndef BFS_USE_PATHS_H
# define BFS_USE_PATHS_H BFS_HAS_PATHS_H
#endif
-#ifndef BFS_USE_SYS_ACL_H
-# define BFS_USE_SYS_ACL_H (BFS_HAS_SYS_ACL_H && !__illumos__)
-#endif
-#ifndef BFS_USE_SYS_CAPABILITY_H
-# define BFS_USE_SYS_CAPABILITY_H (BFS_HAS_SYS_CAPABILITY_H && !__FreeBSD__)
-#endif
#ifndef BFS_USE_SYS_EXTATTR_H
-# define BFS_USE_SYS_EXTATTR_H (BFS_HAS_SYS_EXTATTR_H && !__DragonFly__)
+# define BFS_USE_SYS_EXTATTR_H BFS_HAS_SYS_EXTATTR_H
#endif
#ifndef BFS_USE_SYS_MKDEV_H
# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H
@@ -138,21 +127,6 @@
# define __has_attribute(attr) false
#endif
-// Platform detection
-
-// Get the definition of BSD if available
-#if BFS_USE_SYS_PARAM_H
-# include <sys/param.h>
-#endif
-
-#ifndef __GLIBC_PREREQ
-# define __GLIBC_PREREQ(maj, min) false
-#endif
-
-#ifndef __NetBSD_Prereq__
-# define __NetBSD_Prereq__(maj, min, patch) false
-#endif
-
// Fundamental utilities
/**
@@ -179,14 +153,25 @@
#endif
/**
+ * Polyfill max_align_t if we don't already have it.
+ */
+#if !BFS_HAS_MAX_ALIGN_T
+typedef union {
+# ifdef __BIGGEST_ALIGNMENT__
+ alignas(__BIGGEST_ALIGNMENT__) char c;
+# else
+ long double ld;
+ long long ll;
+ void *ptr;
+# endif
+} max_align_t;
+#endif
+
+/**
* Alignment specifier that avoids false sharing.
*/
#define cache_align alignas(FALSE_SHARING_SIZE)
-#if __COSMOPOLITAN__
-typedef long double max_align_t;
-#endif
-
// Wrappers for attributes
/**
@@ -244,10 +229,19 @@ typedef long double max_align_t;
#endif
/**
+ * Annotates functions that potentially modify and return format strings.
+ */
+#if __has_attribute(format_arg)
+# define attr_format_arg(arg) __attribute__((format_arg(arg)))
+#else
+# define attr_format_arg(args)
+#endif
+
+/**
* Annotates allocator-like functions.
*/
#if __has_attribute(malloc)
-# if __GNUC__ >= 11
+# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC
# define attr_malloc(...) attr_nodiscard __attribute__((malloc(__VA_ARGS__)))
# else
# define attr_malloc(...) attr_nodiscard __attribute__((malloc))
@@ -373,4 +367,4 @@ typedef long double max_align_t;
// Only trigger an error on more than 9 arguments
#define attr_too_many_none(...)
-#endif // BFS_CONFIG_H
+#endif // BFS_PRELUDE_H
diff --git a/src/printf.c b/src/printf.c
index 487f039..f8428f7 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -1,17 +1,18 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "printf.h"
#include "alloc.h"
#include "bfstd.h"
#include "bftw.h"
#include "color.h"
-#include "config.h"
#include "ctx.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
#include "expr.h"
+#include "fsade.h"
#include "mtab.h"
#include "pwcache.h"
#include "stat.h"
@@ -87,22 +88,20 @@ static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
(void)ret
-/**
- * Common entry point for fprintf() with a dynamic format string.
- */
-static int dyn_fprintf(FILE *file, const struct bfs_fmt *fmt, ...) {
- va_list args;
- va_start(args, fmt);
-
-#if __GNUC__
-# pragma GCC diagnostic push
-# pragma GCC diagnostic ignored "-Wformat-nonliteral"
-#endif
- int ret = vfprintf(file, fmt->str, args);
-#if __GNUC__
-# pragma GCC diagnostic pop
-#endif
+/** Return a dynamic format string. */
+attr(format_arg(2))
+static const char *dyn_fmt(const char *str, const char *fake) {
+ bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
+ "Mismatched format specifiers: '%s' vs. '%s'", str, fake);
+ return str;
+}
+/** Wrapper for fprintf(). */
+attr(printf(3, 4))
+static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
+ va_list args;
+ va_start(args, fake);
+ int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args);
va_end(args);
return ret;
}
@@ -138,7 +137,7 @@ static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struc
(long)ts->tv_nsec,
1900 + tm.tm_year);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %A, %B/%W, %C, %T: strftime() */
@@ -213,7 +212,7 @@ static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const st
bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf));
(void)ret;
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %b: blocks */
@@ -225,12 +224,12 @@ static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %d: depth */
static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
- return dyn_fprintf(cfile->file, fmt, (intmax_t)ftwbuf->depth);
+ return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth);
}
/** %D: device */
@@ -241,7 +240,7 @@ static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %f: file name */
@@ -249,7 +248,7 @@ static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pF", ftwbuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path + ftwbuf->nameoff);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff);
}
}
@@ -265,7 +264,7 @@ static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return -1;
}
- return dyn_fprintf(cfile->file, fmt, type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %G: gid */
@@ -276,7 +275,7 @@ static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %g: group name */
@@ -292,7 +291,7 @@ static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return bfs_printf_G(cfile, fmt, ftwbuf);
}
- return dyn_fprintf(cfile->file, fmt, grp->gr_name);
+ return bfs_fprintf(cfile, fmt, "%s", grp->gr_name);
}
/** %h: leading directories */
@@ -321,7 +320,7 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
ret = cfprintf(cfile, "${di}%pQ${rs}", buf);
} else {
- ret = dyn_fprintf(cfile->file, fmt, buf);
+ ret = bfs_fprintf(cfile, fmt, "%s", buf);
}
free(copy);
@@ -337,7 +336,7 @@ static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root);
}
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->root);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root);
}
}
@@ -349,7 +348,7 @@ static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %k: 1K blocks */
@@ -361,7 +360,7 @@ static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %l: link target */
@@ -383,7 +382,7 @@ static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
}
- int ret = dyn_fprintf(cfile->file, fmt, target);
+ int ret = bfs_fprintf(cfile, fmt, "%s", target);
free(buf);
return ret;
}
@@ -395,7 +394,7 @@ static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return -1;
}
- return dyn_fprintf(cfile->file, fmt, (unsigned int)(statbuf->mode & 07777));
+ return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777));
}
/** %M: symbolic mode */
@@ -407,7 +406,7 @@ static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
char buf[11];
xstrmode(statbuf->mode, buf);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %n: link count */
@@ -418,7 +417,7 @@ static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %p: full path */
@@ -426,7 +425,7 @@ static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pP", ftwbuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path);
}
}
@@ -447,7 +446,7 @@ static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
copybuf.nameoff -= offset;
return cfprintf(cfile, "%pP", &copybuf);
} else {
- return dyn_fprintf(cfile->file, fmt, ftwbuf->path + offset);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset);
}
}
@@ -459,7 +458,7 @@ static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %S: sparseness */
@@ -475,7 +474,7 @@ static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
} else {
sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size;
}
- return dyn_fprintf(cfile->file, fmt, sparsity);
+ return bfs_fprintf(cfile, fmt, "%g", sparsity);
}
/** %U: uid */
@@ -486,7 +485,7 @@ static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
- return dyn_fprintf(cfile->file, fmt, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %u: user name */
@@ -502,7 +501,7 @@ static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return bfs_printf_U(cfile, fmt, ftwbuf);
}
- return dyn_fprintf(cfile->file, fmt, pwd->pw_name);
+ return bfs_fprintf(cfile, fmt, "%s", pwd->pw_name);
}
static const char *bfs_printf_type(enum bfs_type type) {
@@ -519,10 +518,14 @@ static const char *bfs_printf_type(enum bfs_type type) {
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";
}
@@ -531,39 +534,29 @@ static const char *bfs_printf_type(enum bfs_type type) {
/** %y: type */
static int bfs_printf_y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const char *type = bfs_printf_type(ftwbuf->type);
- return dyn_fprintf(cfile->file, fmt, type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %Y: target type */
static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
- int error = 0;
-
- if (ftwbuf->type != BFS_LNK) {
- return bfs_printf_y(cfile, fmt, ftwbuf);
- }
+ enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW);
+ const char *str;
- const char *type = "U";
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW);
- if (statbuf) {
- type = bfs_printf_type(bfs_mode_to_type(statbuf->mode));
- } else {
- switch (errno) {
- case ELOOP:
- type = "L";
- break;
- case ENOENT:
- case ENOTDIR:
- type = "N";
- break;
- default:
- type = "?";
+ int error = 0;
+ if (type == BFS_ERROR) {
+ if (errno_is_like(ELOOP)) {
+ str = "L";
+ } else if (errno_is_like(ENOENT)) {
+ str = "N";
+ } else {
+ str = "?";
error = errno;
- break;
}
+ } else {
+ str = bfs_printf_type(type);
}
- int ret = dyn_fprintf(cfile->file, fmt, type);
+ int ret = bfs_fprintf(cfile, fmt, "%s", str);
if (error != 0) {
ret = -1;
errno = error;
@@ -571,6 +564,19 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
return ret;
}
+/** %Z: SELinux context */
+attr(maybe_unused)
+static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ char *con = bfs_getfilecon(ftwbuf);
+ if (!con) {
+ return -1;
+ }
+
+ int ret = bfs_fprintf(cfile, fmt, "%s", con);
+ bfs_freecon(con);
+ return ret;
+}
+
/**
* Append a literal string to the chain.
*/
@@ -840,6 +846,15 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
case 'Y':
fmt.fn = bfs_printf_Y;
break;
+ case 'Z':
+#if BFS_CAN_CHECK_CONTEXT
+ fmt.fn = bfs_printf_Z;
+ break;
+#else
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Missing platform support for '%%%c'.\n", c);
+ goto fmt_error;
+#endif
case 'A':
fmt.stat_field = BFS_STAT_ATIME;
diff --git a/src/pwcache.c b/src/pwcache.c
index 79437d8..af8c237 100644
--- a/src/pwcache.c
+++ b/src/pwcache.c
@@ -1,9 +1,9 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "pwcache.h"
#include "alloc.h"
-#include "config.h"
#include "trie.h"
#include <errno.h>
#include <grp.h>
diff --git a/src/sanity.h b/src/sanity.h
index 423e6ff..e168b8f 100644
--- a/src/sanity.h
+++ b/src/sanity.h
@@ -8,7 +8,7 @@
#ifndef BFS_SANITY_H
#define BFS_SANITY_H
-#include "config.h"
+#include "prelude.h"
#include <stddef.h>
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
diff --git a/src/stat.c b/src/stat.c
index 2f2743b..5a10a99 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "stat.h"
#include "atomic.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
#include "sanity.h"
#include <errno.h>
@@ -13,7 +13,7 @@
#include <sys/stat.h>
#include <sys/types.h>
-#if BFS_USE_STATX && !BFS_HAS_LIBC_STATX
+#if BFS_USE_STATX && !BFS_HAS_STATX
# include <linux/stat.h>
# include <sys/syscall.h>
# include <unistd.h>
@@ -62,7 +62,7 @@ int bfs_fstatat_flags(enum bfs_stat_flags flags) {
ret |= AT_SYMLINK_NOFOLLOW;
}
-#if defined(AT_NO_AUTOMOUNT) && (!__GNU__ || __GLIBC_PREREQ(2, 35))
+#ifdef AT_NO_AUTOMOUNT
ret |= AT_NO_AUTOMOUNT;
#endif
@@ -99,23 +99,26 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) {
dest->rdev = src->st_rdev;
dest->mask |= BFS_STAT_RDEV;
-#if BSD
+#if BFS_HAS_ST_FLAGS
dest->attrs = src->st_flags;
dest->mask |= BFS_STAT_ATTRS;
#endif
- dest->atime = src->st_atim;
+ dest->atime = ST_ATIM(*src);
dest->mask |= BFS_STAT_ATIME;
- dest->ctime = src->st_ctim;
+ dest->ctime = ST_CTIM(*src);
dest->mask |= BFS_STAT_CTIME;
- dest->mtime = src->st_mtim;
+ dest->mtime = ST_MTIM(*src);
dest->mask |= BFS_STAT_MTIME;
-#if __APPLE__ || __FreeBSD__ || __NetBSD__
+#if BFS_HAS_ST_BIRTHTIM
dest->btime = src->st_birthtim;
dest->mask |= BFS_STAT_BTIME;
+#elif BFS_HAS_ST_BIRTHTIMESPEC
+ dest->btime = src->st_birthtimespec;
+ dest->mask |= BFS_STAT_BTIME;
#endif
}
@@ -137,7 +140,7 @@ static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, struct bf
* Wrapper for the statx() system call, which had no glibc wrapper prior to 2.28.
*/
static int bfs_statx(int at_fd, const char *at_path, int at_flags, unsigned int mask, struct statx *buf) {
-#if BFS_HAS_LIBC_STATX
+#if BFS_HAS_STATX
int ret = statx(at_fd, at_path, at_flags, mask, buf);
#else
int ret = syscall(SYS_statx, at_fd, at_path, at_flags, mask, buf);
@@ -294,27 +297,21 @@ int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct b
return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf);
}
- // Check __GNU__ to work around https://lists.gnu.org/archive/html/bug-hurd/2021-12/msg00001.html
-#if defined(AT_EMPTY_PATH) && !__GNU__
- static atomic bool has_at_ep = true;
- if (load(&has_at_ep, relaxed)) {
- at_flags |= AT_EMPTY_PATH;
- int ret = bfs_stat_explicit(at_fd, "", at_flags, buf);
- if (ret != 0 && errno == EINVAL) {
- store(&has_at_ep, false, relaxed);
- } else {
- return ret;
- }
- }
-#endif
-
- struct stat statbuf;
- if (fstat(at_fd, &statbuf) == 0) {
- bfs_stat_convert(buf, &statbuf);
- return 0;
- } else {
+#if BFS_USE_STATX
+ // If we have statx(), use it with AT_EMPTY_PATH for its extra features
+ at_flags |= AT_EMPTY_PATH;
+ return bfs_stat_explicit(at_fd, "", at_flags, buf);
+#else
+ // Otherwise, just use fstat() rather than fstatat(at_fd, ""), to save
+ // the kernel the trouble of copying in the empty string
+ struct stat sb;
+ if (fstat(at_fd, &sb) != 0) {
return -1;
}
+
+ bfs_stat_convert(buf, &sb);
+ return 0;
+#endif
}
const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field) {
diff --git a/src/stat.h b/src/stat.h
index 856a2ca..8d7144d 100644
--- a/src/stat.h
+++ b/src/stat.h
@@ -12,21 +12,17 @@
#ifndef BFS_STAT_H
#define BFS_STAT_H
-#include "config.h"
+#include "prelude.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
-#if defined(STATX_BASIC_STATS) && (!__ANDROID__ || __ANDROID_API__ >= 30)
-# define BFS_HAS_LIBC_STATX true
-#elif __linux__
+#if !BFS_HAS_STATX && BFS_HAS_STATX_SYSCALL
# include <linux/stat.h>
#endif
#ifndef BFS_USE_STATX
-# ifdef STATX_BASIC_STATS
-# define BFS_USE_STATX true
-# endif
+# define BFS_USE_STATX (BFS_HAS_STATX || BFS_HAS_STATX_SYSCALL)
#endif
#if BFS_USE_SYS_PARAM_H
diff --git a/src/thread.c b/src/thread.c
index 200d8c3..3793896 100644
--- a/src/thread.c
+++ b/src/thread.c
@@ -1,9 +1,9 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "thread.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
#include <errno.h>
#include <pthread.h>
diff --git a/src/thread.h b/src/thread.h
index 8174fe4..db11bd8 100644
--- a/src/thread.h
+++ b/src/thread.h
@@ -8,7 +8,7 @@
#ifndef BFS_THREAD_H
#define BFS_THREAD_H
-#include "config.h"
+#include "prelude.h"
#include <pthread.h>
#if __STDC_VERSION__ < C23 && !defined(thread_local)
diff --git a/src/trie.c b/src/trie.c
index f275064..808953e 100644
--- a/src/trie.c
+++ b/src/trie.c
@@ -81,10 +81,10 @@
* and insert intermediate singleton "jump" nodes when necessary.
*/
+#include "prelude.h"
#include "trie.h"
#include "alloc.h"
#include "bit.h"
-#include "config.h"
#include "diag.h"
#include "list.h"
#include <stdint.h>
diff --git a/src/xregex.c b/src/xregex.c
index 3df27f0..c2711bc 100644
--- a/src/xregex.c
+++ b/src/xregex.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xregex.h"
#include "alloc.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
#include "sanity.h"
#include "thread.h"
diff --git a/src/xspawn.c b/src/xspawn.c
index f67adec..0b0cea4 100644
--- a/src/xspawn.c
+++ b/src/xspawn.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xspawn.h"
#include "alloc.h"
#include "bfstd.h"
-#include "config.h"
#include "list.h"
#include <errno.h>
#include <fcntl.h>
@@ -209,10 +209,10 @@ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
* PATH ..., using the working directory of the child process after all
* file_actions have been performed.
*
- * but macOS resolves the PATH *before* file_actions (because there
+ * but macOS and NetBSD resolve the PATH *before* file_actions (because there
* posix_spawn() is its own syscall).
*/
-#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !__APPLE__
+#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__)
int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR);
@@ -220,29 +220,15 @@ int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
return -1;
}
-#ifndef BFS_HAS_POSIX_SPAWN_FCHDIR
-# define BFS_HAS_POSIX_SPAWN_FCHDIR __NetBSD_Prereq__(10, 0, 0)
-#endif
-
-#ifndef BFS_HAS_POSIX_SPAWN_FCHDIR_NP
-# if __GLIBC__
-# define BFS_HAS_POSIX_SPAWN_FCHDIR_NP __GLIBC_PREREQ(2, 29)
-# elif __ANDROID__
-# define BFS_HAS_POSIX_SPAWN_FCHDIR_NP (__ANDROID_API__ >= 34)
-# else
-# define BFS_HAS_POSIX_SPAWN_FCHDIR_NP (__linux__ || __FreeBSD__ || __APPLE__)
-# endif
-#endif
-
-#if BFS_HAS_POSIX_SPAWN_FCHDIR
-# define BFS_POSIX_SPAWN_FCHDIR posix_spawn_file_actions_addfchdir
-#elif BFS_HAS_POSIX_SPAWN_FCHDIR_NP
-# define BFS_POSIX_SPAWN_FCHDIR posix_spawn_file_actions_addfchdir_np
+#if BFS_HAS_POSIX_SPAWN_ADDFCHDIR
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir
+#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np
#endif
#if _POSIX_SPAWN > 0 && defined(BFS_POSIX_SPAWN_FCHDIR)
if (ctx->flags & BFS_SPAWN_USE_POSIX) {
- errno = BFS_POSIX_SPAWN_FCHDIR(&ctx->actions, fd);
+ errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd);
if (errno != 0) {
free(action);
return -1;
@@ -364,16 +350,10 @@ static int bfs_resolve_late(struct bfs_resolver *res) {
res->done = true;
return 0;
}
-
- if (end) {
- path = end + 1;
- } else {
- errno = ENOENT;
- return -1;
- }
}
- return 0;
+ errno = ENOENT;
+ return -1;
}
/** Check if we can skip path resolution entirely. */
diff --git a/src/xspawn.h b/src/xspawn.h
index a20cbd0..6a8f54a 100644
--- a/src/xspawn.h
+++ b/src/xspawn.h
@@ -8,7 +8,7 @@
#ifndef BFS_XSPAWN_H
#define BFS_XSPAWN_H
-#include "config.h"
+#include "prelude.h"
#include <sys/resource.h>
#include <sys/types.h>
#include <unistd.h>
diff --git a/src/xtime.c b/src/xtime.c
index bcf6dd3..eb11afa 100644
--- a/src/xtime.c
+++ b/src/xtime.c
@@ -1,10 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xtime.h"
#include "bfstd.h"
-#include "config.h"
#include "diag.h"
+#include "sanity.h"
#include <errno.h>
#include <limits.h>
#include <sys/time.h>
@@ -12,13 +13,13 @@
#include <unistd.h>
int xmktime(struct tm *tm, time_t *timep) {
- *timep = mktime(tm);
+ time_t time = mktime(tm);
- if (*timep == -1) {
+ if (time == -1) {
int error = errno;
struct tm tmp;
- if (!localtime_r(timep, &tmp)) {
+ if (!localtime_r(&time, &tmp)) {
bfs_bug("localtime_r(-1): %s", xstrerror(errno));
return -1;
}
@@ -30,9 +31,38 @@ int xmktime(struct tm *tm, time_t *timep) {
}
}
+ *timep = time;
+ return 0;
+}
+
+// FreeBSD is missing an interceptor
+#if BFS_HAS_TIMEGM && !(__FreeBSD__ && SANITIZE_MEMORY)
+
+int xtimegm(struct tm *tm, time_t *timep) {
+ time_t time = timegm(tm);
+
+ if (time == -1) {
+ int error = errno;
+
+ struct tm tmp;
+ if (!gmtime_r(&time, &tmp)) {
+ bfs_bug("gmtime_r(-1): %s", xstrerror(errno));
+ return -1;
+ }
+
+ if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday
+ || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) {
+ errno = error;
+ return -1;
+ }
+ }
+
+ *timep = time;
return 0;
}
+#else
+
static int safe_add(int *value, int delta) {
if (*value >= 0) {
if (delta > INT_MAX - *value) {
@@ -147,6 +177,8 @@ overflow:
return -1;
}
+#endif // !BFS_HAS_TIMEGM
+
/** Parse a decimal digit. */
static int xgetdigit(char c) {
int ret = c - '0';
diff --git a/tests/alloc.c b/tests/alloc.c
index 9f08111..6c0defd 100644
--- a/tests/alloc.c
+++ b/tests/alloc.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/alloc.h"
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "alloc.h"
+#include "diag.h"
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
diff --git a/tests/bfstd.c b/tests/bfstd.c
index dc5ceaa..07b68b0 100644
--- a/tests/bfstd.c
+++ b/tests/bfstd.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/bfstd.h"
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "bfstd.h"
+#include "diag.h"
#include <errno.h>
#include <langinfo.h>
#include <stdlib.h>
diff --git a/tests/bit.c b/tests/bit.c
index 6548c30..674d1b2 100644
--- a/tests/bit.c
+++ b/tests/bit.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/bit.h"
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "bit.h"
+#include "diag.h"
#include <limits.h>
#include <stdint.h>
#include <string.h>
diff --git a/tests/bsd/sparse.out b/tests/bsd/sparse.out
new file mode 100644
index 0000000..52dcf49
--- /dev/null
+++ b/tests/bsd/sparse.out
@@ -0,0 +1 @@
+mnt/sparse
diff --git a/tests/bsd/sparse.sh b/tests/bsd/sparse.sh
new file mode 100644
index 0000000..7fcdeed
--- /dev/null
+++ b/tests/bsd/sparse.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+truncate -s 1M mnt/sparse
+dd if=/dev/zero of=mnt/dense bs=1M count=1
+
+bfs_diff mnt -type f -sparse
diff --git a/tests/common/delete_error.out b/tests/common/delete_error.out
new file mode 100644
index 0000000..b6b6505
--- /dev/null
+++ b/tests/common/delete_error.out
@@ -0,0 +1,8 @@
+.
+.
+./baz
+./baz
+./baz/qux
+./baz/qux
+./foo
+./foo/bar
diff --git a/tests/common/delete_error.sh b/tests/common/delete_error.sh
new file mode 100644
index 0000000..e6327f3
--- /dev/null
+++ b/tests/common/delete_error.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/bar baz/qux
+chmod -w foo
+defer chmod +w foo
+
+! invoke_bfs . -print -delete -print >"$OUT" || fail
+sort_output
+diff_output
diff --git a/tests/gnu/empty.out b/tests/common/empty.out
index a0f4b76..a0f4b76 100644
--- a/tests/gnu/empty.out
+++ b/tests/common/empty.out
diff --git a/tests/gnu/empty.sh b/tests/common/empty.sh
index 95ee988..95ee988 100644
--- a/tests/gnu/empty.sh
+++ b/tests/common/empty.sh
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
new file mode 100644
index 0000000..da45e23
--- /dev/null
+++ b/tests/common/empty_error.out
@@ -0,0 +1,3 @@
+./bar
+./baz
+./qux
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
new file mode 100644
index 0000000..7c8049c
--- /dev/null
+++ b/tests/common/empty_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/ bar/ baz qux
+chmod -r foo baz
+defer chmod +r foo baz
+
+! bfs_diff . -empty
diff --git a/tests/gnu/empty_special.out b/tests/common/empty_special.out
index fa35478..fa35478 100644
--- a/tests/gnu/empty_special.out
+++ b/tests/common/empty_special.out
diff --git a/tests/gnu/empty_special.sh b/tests/common/empty_special.sh
index 31e9d2e..31e9d2e 100644
--- a/tests/gnu/empty_special.sh
+++ b/tests/common/empty_special.sh
diff --git a/tests/posix/newer_link.out b/tests/gnu/newer_link.out
index d2dcdd1..d2dcdd1 100644
--- a/tests/posix/newer_link.out
+++ b/tests/gnu/newer_link.out
diff --git a/tests/posix/newer_link.sh b/tests/gnu/newer_link.sh
index 685ac78..685ac78 100644
--- a/tests/posix/newer_link.sh
+++ b/tests/gnu/newer_link.sh
diff --git a/tests/gnu/used.out b/tests/gnu/used.out
new file mode 100644
index 0000000..647621b
--- /dev/null
+++ b/tests/gnu/used.out
@@ -0,0 +1,4 @@
+-used +7: ./nextyear
+-used 1: ./tomorrow
+-used 2: ./dayafter
+-used 7: ./nextweek
diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh
new file mode 100644
index 0000000..5e5d4e9
--- /dev/null
+++ b/tests/gnu/used.sh
@@ -0,0 +1,40 @@
+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]}))
+
+# -used is always false if atime < ctime
+yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss)
+"$XTOUCH" -at "$yesterday" yesterday
+
+# -used rounds up
+tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$tomorrow" tomorrow
+
+dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$dayafter" dayafter
+
+nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$nextweek" nextweek
+
+nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss)
+"$XTOUCH" -at "$nextyear" nextyear
+
+bfs_diff -mindepth 1 \
+ -a -used 1 -printf '-used 1: %p\n' \
+ -o -used 2 -printf '-used 2: %p\n' \
+ -o -used 7 -printf '-used 7: %p\n' \
+ -o -used +7 -printf '-used +7: %p\n'
diff --git a/tests/ioq.c b/tests/ioq.c
index 1ce8f75..ef5ee3b 100644
--- a/tests/ioq.c
+++ b/tests/ioq.c
@@ -1,12 +1,12 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/ioq.h"
-#include "../src/bfstd.h"
-#include "../src/config.h"
-#include "../src/diag.h"
-#include "../src/dir.h"
+#include "ioq.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "dir.h"
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
diff --git a/tests/main.c b/tests/main.c
index 69903d4..429772b 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -5,10 +5,10 @@
* Entry point for unit tests.
*/
+#include "prelude.h"
#include "tests.h"
-#include "../src/bfstd.h"
-#include "../src/color.h"
-#include "../src/config.h"
+#include "bfstd.h"
+#include "color.h"
#include <errno.h>
#include <locale.h>
#include <stdio.h>
diff --git a/tests/mksock.c b/tests/mksock.c
index f3b61da..5786cb6 100644
--- a/tests/mksock.c
+++ b/tests/mksock.c
@@ -6,7 +6,7 @@
* program does the job.
*/
-#include "../src/bfstd.h"
+#include "bfstd.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
diff --git a/tests/posix/newer_broken.out b/tests/posix/newer_broken.out
new file mode 100644
index 0000000..d2dcdd1
--- /dev/null
+++ b/tests/posix/newer_broken.out
@@ -0,0 +1 @@
+times
diff --git a/tests/posix/newer_broken.sh b/tests/posix/newer_broken.sh
new file mode 100644
index 0000000..dccaa73
--- /dev/null
+++ b/tests/posix/newer_broken.sh
@@ -0,0 +1,4 @@
+ln -s nowhere "$TEST/broken"
+"$XTOUCH" -h -t "1991-12-14 00:03" "$TEST/broken"
+
+bfs_diff times -newer "$TEST/broken"
diff --git a/tests/run.sh b/tests/run.sh
index 720515d..ad9c0be 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -156,7 +156,7 @@ comake() {
-f "$TESTS/tests.mk" \
DONE=$DONE_PIPE \
READY=$READY_PIPE \
- "${TEST_CASES[@]/#/tests/}" \
+ "${!TEST_CASES[@]}" \
</dev/null >/dev/null
}
@@ -430,7 +430,7 @@ make_xattrs() {
EX_DIFF=20
# Detect colored diff support
-if diff --color /dev/null /dev/null 2>/dev/null; then
+if diff --color /dev/null /dev/null &>/dev/null; then
DIFF="diff --color"
else
DIFF="diff"
@@ -447,7 +447,7 @@ diff_output() {
if ((UPDATE)); then
cp "$OUT" "$GOLD"
- else
+ elif ! cmp -s "$GOLD" "$OUT"; then
$DIFF -u "$GOLD" "$OUT" >&$DUPERR
fi
}
diff --git a/tests/tests.h b/tests/tests.h
index 351badb..9078938 100644
--- a/tests/tests.h
+++ b/tests/tests.h
@@ -8,8 +8,8 @@
#ifndef BFS_TESTS_H
#define BFS_TESTS_H
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "prelude.h"
+#include "diag.h"
/** Unit test function type. */
typedef bool test_fn(void);
diff --git a/tests/tests.mk b/tests/tests.mk
index 5bf4f6c..035ca79 100644
--- a/tests/tests.mk
+++ b/tests/tests.mk
@@ -1,7 +1,13 @@
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
-# GNU makefile that exposes make's job control to tests.sh
+# Makefile that exposes make's job control to tests.sh
-tests/%:
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# Turn off implicit rules
+.SUFFIXES:
+
+.DEFAULT::
bash -c 'printf . >&$(READY) && read -r -N1 -u$(DONE)'
diff --git a/tests/trie.c b/tests/trie.c
index fec0de2..4667322 100644
--- a/tests/trie.c
+++ b/tests/trie.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/trie.h"
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "trie.h"
+#include "diag.h"
#include <stdlib.h>
#include <string.h>
diff --git a/tests/util.sh b/tests/util.sh
index 7dba9fb..3969db5 100644
--- a/tests/util.sh
+++ b/tests/util.sh
@@ -12,12 +12,9 @@ _realpath() (
)
# Globals
-TESTS=$(_realpath "$TESTS")
-if [ "${BUILDDIR-}" ]; then
- BIN=$(_realpath "$BUILDDIR/bin")
-else
- BIN=$(_realpath "$TESTS/../bin")
-fi
+ROOT=$(_realpath "$(dirname -- "$TESTS")")
+TESTS="$ROOT/tests"
+BIN="$ROOT/bin"
MKSOCK="$BIN/tests/mksock"
XTOUCH="$BIN/tests/xtouch"
UNAME=$(uname)
diff --git a/tests/xspawn.c b/tests/xspawn.c
index 5f80e76..785ea48 100644
--- a/tests/xspawn.c
+++ b/tests/xspawn.c
@@ -1,12 +1,12 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/alloc.h"
-#include "../src/bfstd.h"
-#include "../src/config.h"
-#include "../src/dstring.h"
-#include "../src/xspawn.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "dstring.h"
+#include "xspawn.h"
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
@@ -64,27 +64,20 @@ static bool check_use_path(bool use_posix) {
spawn.flags &= ~BFS_SPAWN_USE_POSIX;
}
- const char *builddir = getenv("BUILDDIR");
- dchar *bin = dstrprintf("%s/bin", builddir ? builddir : ".");
- ret &= bfs_pcheck(bin, "dstrprintf()");
- if (!ret) {
- goto destroy;
- }
-
- ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, bin, O_RDONLY | O_DIRECTORY, 0) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0);
ret &= bfs_pcheck(bfs_spawn_adddup2(&spawn, 10, 11) == 0);
ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 10) == 0);
ret &= bfs_pcheck(bfs_spawn_addfchdir(&spawn, 11) == 0);
ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 11) == 0);
if (!ret) {
- goto bin;
+ goto destroy;
}
// Check that $PATH is resolved in the parent's environment
char **envp;
ret &= bfs_pcheck(envp = envdup());
if (!ret) {
- goto bin;
+ goto destroy;
}
// Check that $PATH is resolved after the file actions
@@ -138,8 +131,6 @@ env:
free(*var);
}
free(envp);
-bin:
- dstrfree(bin);
destroy:
ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
out:
@@ -170,6 +161,27 @@ out:
return ret;
}
+static bool check_resolve(void) {
+ bool ret = true;
+ char *exe;
+
+ exe = bfs_spawn_resolve("sh");
+ ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')");
+ free(exe);
+
+ exe = bfs_spawn_resolve("/bin/sh");
+ ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0);
+ free(exe);
+
+ exe = bfs_spawn_resolve("bin/tests/xspawnee");
+ ret &= bfs_pcheck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ free(exe);
+
+ ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+
+ return ret;
+}
+
bool check_xspawn(void) {
bool ret = true;
@@ -179,5 +191,7 @@ bool check_xspawn(void) {
ret &= check_enoent(true);
ret &= check_enoent(false);
+ ret &= check_resolve();
+
return ret;
}
diff --git a/tests/xtime.c b/tests/xtime.c
index f85402e..d9d6c5c 100644
--- a/tests/xtime.c
+++ b/tests/xtime.c
@@ -1,11 +1,11 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "tests.h"
-#include "../src/xtime.h"
-#include "../src/bfstd.h"
-#include "../src/config.h"
-#include "../src/diag.h"
+#include "xtime.h"
+#include "bfstd.h"
+#include "diag.h"
#include <errno.h>
#include <limits.h>
#include <stdint.h>
@@ -137,6 +137,7 @@ static bool check_one_xtimegm(const struct tm *tm) {
return ret;
}
+#if !BFS_HAS_TIMEGM
/** Check an overflowing xtimegm() call. */
static bool check_xtimegm_overflow(const struct tm *tm) {
struct tm copy = *tm;
@@ -154,6 +155,7 @@ static bool check_xtimegm_overflow(const struct tm *tm) {
return ret;
}
+#endif
/** xtimegm() tests. */
static bool check_xtimegm(void) {
@@ -173,11 +175,13 @@ static bool check_xtimegm(void) {
ret &= check_one_xtimegm(&tm);
}
+#if !BFS_HAS_TIMEGM
// Check integer overflow cases
- check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
- check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
+ ret &= check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+#endif
return ret;
}
diff --git a/tests/xtouch.c b/tests/xtouch.c
index fad272f..cd41842 100644
--- a/tests/xtouch.c
+++ b/tests/xtouch.c
@@ -1,10 +1,10 @@
// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD
-#include "../src/bfstd.h"
-#include "../src/config.h"
-#include "../src/sanity.h"
-#include "../src/xtime.h"
+#include "prelude.h"
+#include "bfstd.h"
+#include "sanity.h"
+#include "xtime.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
@@ -237,8 +237,8 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno));
return EXIT_FAILURE;
}
- times[0] = buf.st_atim;
- times[1] = buf.st_mtim;
+ times[0] = ST_ATIM(buf);
+ times[1] = ST_MTIM(buf);
} else if (darg) {
if (xgetdate(darg, &times[0]) != 0) {
fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno));