diff options
386 files changed, 12220 insertions, 4332 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml new file mode 100644 index 0000000..a4271ec --- /dev/null +++ b/.github/codeql.yml @@ -0,0 +1,13 @@ +query-filters: + - exclude: + id: cpp/commented-out-code + - exclude: + id: cpp/include-non-header + - exclude: + id: cpp/long-switch + - exclude: + id: cpp/loop-variable-changed + - exclude: + id: cpp/poorly-documented-function + - exclude: + id: cpp/constant-comparison 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/diag.sh b/.github/diag.sh new file mode 100755 index 0000000..d89e7a4 --- /dev/null +++ b/.github/diag.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Convert compiler diagnostics to GitHub Actions messages +# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message + +set -eu + +SEDFLAGS="-En" +if sed -u 's/s/s/' </dev/null &>/dev/null; then + SEDFLAGS="${SEDFLAGS}u" +fi + +filter() { + sed $SEDFLAGS 'p; s/^([^:]*):([^:]*):([^:]*): (warning|error): (.*)$/::\4 file=\1,line=\2,col=\3,title=Compiler \4::\5/p' +} + +exec "$@" > >(filter) 2> >(filter >&2) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c1c89c..4075eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,12 @@ name: CI on: [push, pull_request] jobs: - linux: - name: Linux + linux-x86: + name: Linux (x86) + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + # Don't run on both pushes and pull requests + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -16,15 +18,13 @@ jobs: sudo dpkg --add-architecture i386 sudo apt-get update -y sudo apt-get install -y \ - expect \ + mandoc \ gcc-multilib \ libgcc-s1:i386 \ acl \ libacl1-dev \ libacl1:i386 \ attr \ - libattr1-dev \ - libattr1:i386 \ libcap2-bin \ libcap-dev \ libcap2:i386 \ @@ -34,133 +34,159 @@ 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 - sudo sysctl vm.mmap_rnd_bits=28 - name: Run tests run: | - make -j$(nproc) distcheck + .github/diag.sh make -j$(nproc) distcheck + + - uses: actions/upload-artifact@v4 + with: + name: linux-x86-config.log + path: distcheck-*/gen/config.log + + linux-arm: + name: Linux (Arm64) + runs-on: ubuntu-24.04-arm + + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y \ + mandoc \ + acl \ + libacl1-dev \ + attr \ + libcap2-bin \ + libcap-dev \ + libonig-dev \ + liburing-dev + + - name: Run tests + run: | + .github/diag.sh make -j$(nproc) distcheck + + - uses: actions/upload-artifact@v4 + with: + name: linux-arm-config.log + path: distcheck-*/gen/config.log macos: name: macOS + runs-on: macos-15 - runs-on: macos-14 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install \ - bash \ - expect + brew install bash - name: Run tests run: | jobs=$(sysctl -n hw.ncpu) - make -j$jobs distcheck + .github/diag.sh make -j$jobs distcheck freebsd: name: FreeBSD + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/freebsd-vm@v1 + uses: cross-platform-actions/action@v0.28.0 with: - release: "14.0" - usesh: true - copyback: false + operating_system: freebsd + version: "14.2" - prepare: | - pkg install -y \ + run: | + sudo pkg install -y \ bash \ - expect \ - gmake \ oniguruma \ - sudo \ - 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 + pkgconf + sudo mount -t fdescfs none /dev/fd + .github/diag.sh 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 + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/openbsd-vm@v1 + uses: cross-platform-actions/action@v0.28.0 with: - release: "7.4" - usesh: true - copyback: false + operating_system: openbsd + version: "7.7" - 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 + .github/diag.sh 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 + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: vmactions/netbsd-vm@v1 + uses: cross-platform-actions/action@v0.28.0 with: - release: "9.3" - usesh: true - copyback: false + operating_system: netbsd + version: "10.1" - prepare: | + run: | PATH="/sbin:/usr/sbin:$PATH" - pkg_add \ + sudo pkgin -y install \ bash \ - clang \ - gmake \ oniguruma \ - sudo \ - 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 . + pkgconf 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 + .github/diag.sh 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 + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -170,28 +196,32 @@ jobs: with: release: "6.4.0" usesh: true - copyback: false prepare: | pkg install -y \ bash \ - expect \ - gmake \ oniguruma \ - sudo \ - tcl-wrapper + pkgconf \ + sudo pw useradd -n action -m -G wheel -s /usr/local/bin/bash echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers 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 .github/diag.sh 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 + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -199,15 +229,13 @@ jobs: - name: Run tests uses: vmactions/omnios-vm@v1 with: - release: "r151048" + release: "r151052" usesh: true - copyback: false prepare: | pkg install \ bash \ build-essential \ - expect \ gnu-make \ onig \ sudo @@ -218,4 +246,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 .github/diag.sh 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..e4e8f71 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -13,12 +13,10 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y \ - expect \ gcc \ acl \ libacl1-dev \ attr \ - libattr1-dev \ libcap2-bin \ libcap-dev \ libonig-dev \ @@ -26,10 +24,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@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 71073c6..1f2041c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,7 +13,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: actions: read contents: read @@ -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: | @@ -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) @@ -1,4 +1,4 @@ -Copyright © 2015-2023 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors +Copyright © 2015-2025 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5e6d25c --- /dev/null +++ b/Makefile @@ -0,0 +1,310 @@ +# 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 +.SILENT: gen/config.mk + +## Build phase (`make`) + +# The main binary +bfs: bin/bfs +.PHONY: bfs + +# All binaries +BINS := \ + bin/bfs \ + bin/tests/mksock \ + bin/tests/ptyx \ + bin/tests/units \ + bin/tests/xspawnee \ + bin/tests/xtouch \ + bin/bench/ioq + +all: ${BINS} +.PHONY: all + +# 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/sighook.o \ + obj/src/stat.o \ + obj/src/thread.o \ + obj/src/trie.o \ + obj/src/typo.o \ + obj/src/version.o \ + obj/src/xregex.o \ + obj/src/xspawn.o \ + obj/src/xtime.o + +# All object files +OBJS := ${LIBBFS} + +# The main binary +bin/bfs: obj/src/main.o ${LIBBFS} +OBJS += obj/src/main.o + +${BINS}: + @${MKDIR} ${@D} + +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} $^ ${_LDLIBS} -o $@ + ${POSTLINK} + +# Get the .c file for a .o file +CSRC = ${@:obj/%.o=%.c} + +# Save the version number to this file, but only update version.c if it changes +gen/version.i.new:: + ${MKDIR} ${@D} + build/version.sh | tr -d '\n' | build/embed.sh >$@ +.SILENT: gen/version.i.new + +gen/version.i: gen/version.i.new + test -e $@ && cmp -s $@ $^ && ${RM} $^ || mv $^ $@ +.SILENT: gen/version.i + +obj/src/version.o: gen/version.i + +## 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/ptyx \ + 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 + +# Unit test objects +UNIT_OBJS := \ + obj/tests/alloc.o \ + obj/tests/bfstd.o \ + obj/tests/bit.o \ + obj/tests/ioq.o \ + obj/tests/list.o \ + obj/tests/main.o \ + obj/tests/sighook.o \ + obj/tests/trie.o \ + obj/tests/xspawn.o \ + obj/tests/xtime.o + +bin/tests/units: ${UNIT_OBJS} ${LIBBFS} +OBJS += ${UNIT_OBJS} + +bin/tests/xspawnee: obj/tests/xspawnee.o +OBJS += 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} +OBJS += obj/tests/mksock.o + +bin/tests/ptyx: obj/tests/ptyx.o ${LIBBFS} +OBJS += obj/tests/ptyx.o + +bin/tests/xtouch: obj/tests/xtouch.o ${LIBBFS} +OBJS += obj/tests/xtouch.o + +# `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 + @+test "$$(uname)" = FreeBSD || ${MAKE} distcheck-tsan + @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32 + @+${MAKE} distcheck-release + @+${MAKE} -C distcheck-release check-install + @+test "$$(uname)" != Linux || ${MAKE} check-man +.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} $@ + @test "$${GITHUB_ACTIONS-}" != true || printf '::group::%s\n' $@ + @+cd $@ \ + && ../configure MAKE="${MAKE}" ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \ + && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped" + @test "$${GITHUB_ACTIONS-}" != true || printf '::endgroup::\n' + +## Benchmarks (`make bench`) + +bench: bin/bench/ioq +.PHONY: bench + +bin/bench/ioq: obj/bench/ioq.o ${LIBBFS} +OBJS += obj/bench/ioq.o + +## Automatic dependency tracking + +# Rebuild when the configuration changes +${OBJS}: gen/config.mk + @${MKDIR} ${@D} + ${MSG} "[ CC ] ${CSRC}" ${CC} ${_CPPFLAGS} ${_CFLAGS} -c ${CSRC} -o $@ + +# Include any generated dependency files +-include ${OBJS:.o=.d} + +## Packaging (`make dist`, `make install`) + +TARBALL = bfs-$$(build/version.sh).tar.gz + +dist: + ${MSG} "[DIST] ${TARBALL}" git archive HEAD -o ${TARBALL} + +distsign: dist + ${MSG} "[SIGN] ${TARBALL}" ssh-keygen -Y sign -q -f $$(git config user.signingkey) -n file ${TARBALL} + +.PHONY: dist distsign + +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 + +# Check man page markup +check-man:: + ${MSG} "[LINT] docs/bfs.1" + ${Q}groff -man -rCHECKSTYLE=3 -ww -b -z docs/bfs.1 + ${Q}mandoc -Tlint -Wwarning docs/bfs.1 + +## 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 distcheck-*" \ + ${RM} -r gen ${DISTCHECKS} +.PHONY: distclean @@ -34,7 +34,7 @@ It is otherwise compatible with many versions of `find`, including **[POSIX] • [GNU] • [FreeBSD] • [OpenBSD] • [NetBSD] • [macOS]** -[POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html +[POSIX]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html [GNU]: https://www.gnu.org/software/findutils/ [FreeBSD]: https://www.freebsd.org/cgi/man.cgi?find(1) [OpenBSD]: https://man.openbsd.org/find.1 @@ -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..c9ed978 100644 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -22,6 +22,7 @@ PRINT_DEFAULT=(linux) STRATEGIES_DEFAULT=(rust) JOBS_DEFAULT=(rust) EXEC_DEFAULT=(linux) +SORTED_DEFAULT=(chromium) usage() { printf 'Usage: tailfin run %s\n' "${BASH_SOURCE[0]}" @@ -60,6 +61,10 @@ usage() { printf ' Process spawning benchmark.\n' printf ' Default corpus is --exec=%s\n\n' "${EXEC_DEFAULT[*]}" + printf ' --sorted[=CORPUS]\n' + printf ' Sorted traversal benchmark.\n' + printf ' Default corpus is --sorted=%s\n\n' "${SORTED_DEFAULT[*]}" + printf ' --build=COMMIT\n' printf ' Build this bfs commit and benchmark it. Specify multiple times to\n' printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n' @@ -121,6 +126,7 @@ setup() { STRATEGIES=() JOBS=() EXEC=() + SORTED=() for arg; do case "$arg" in @@ -195,6 +201,12 @@ setup() { --exec=*) read -ra EXEC <<<"${arg#*=}" ;; + --sorted) + SORTED=("${SORTED_DEFAULT[@]}") + ;; + --sorted=*) + read -ra SORTED <<<"${arg#*=}" + ;; --default) COMPLETE=("${COMPLETE_DEFAULT[@]}") EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}") @@ -203,6 +215,7 @@ setup() { STRATEGIES=("${STRATEGIES_DEFAULT[@]}") JOBS=("${JOBS_DEFAULT[@]}") EXEC=("${EXEC_DEFAULT[@]}") + SORTED=("${SORTED_DEFAULT[@]}") ;; --help) usage @@ -221,12 +234,13 @@ 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 declare -A cloned=() - for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}"; do + for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}" "${SORTED[@]}"; do if ((cloned["$corpus"])); then continue fi @@ -253,7 +267,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 @@ -263,12 +282,7 @@ setup() { ) done - # $SETUP_DIR contains `:` so it won't work in $PATH - # Work around this with a symlink - tmp=$(as-user mktemp) - as-user ln -sf "$bin" "$tmp" - defer rm "$tmp" - export PATH="$tmp:$PATH" + export PATH="$bin:$PATH" fi export_array BFS @@ -282,6 +296,7 @@ setup() { export_array STRATEGIES export_array JOBS export_array EXEC + export_array SORTED if ((UID == 0)); then turbo-off @@ -359,7 +374,7 @@ bench-complete() { fi } -# Benchmark quiting as soon as a file is seen +# Benchmark quitting as soon as a file is seen bench-early-quit-corpus() { dir="$2" max_depth=$(./bin/bfs "$dir" -printf '%d\n' | sort -rn | head -n1) @@ -649,6 +664,29 @@ bench-exec() { fi } +# Benchmark sorted traversal +bench-sorted-corpus() { + subgroup '%s' "$1" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs -s $2 -false") + done + + do-hyperfine "${cmds[@]}" +} + +# All sorted traversal benchmarks +bench-sorted() { + if (($#)); then + group "Sorted traversal" + + for corpus; do + bench-sorted-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + # Print benchmarked versions bench-versions() { subgroup "Versions" @@ -697,6 +735,7 @@ bench() { import_array STRATEGIES import_array JOBS import_array EXEC + import_array SORTED bench-complete "${COMPLETE[@]}" bench-early-quit "${EARLY_QUIT[@]}" @@ -705,5 +744,6 @@ bench() { bench-strategies "${STRATEGIES[@]}" bench-jobs "${JOBS[@]}" bench-exec "${EXEC[@]}" + bench-sorted "${SORTED[@]}" bench-details } diff --git a/bench/ioq.c b/bench/ioq.c new file mode 100644 index 0000000..fb9edbc --- /dev/null +++ b/bench/ioq.c @@ -0,0 +1,455 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "ioq.h" +#include "sighook.h" +#include "xtime.h" + +#include <errno.h> +#include <locale.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> + +/** A latency sample. */ +struct lat { + /** The sampled latency. */ + struct timespec time; + /** A random integer, for reservoir sampling. */ + long key; +}; + +/** Number of latency samples to keep. */ +#define SAMPLES 1000 +/** Latency sampling period. */ +#define PERIOD 128 + +/** Latency measurements. */ +struct lats { + /** Lowest observed latency. */ + struct timespec min; + /** Highest observed latency. */ + struct timespec max; + /** Total latency. */ + struct timespec sum; + /** Number of measured requests. */ + size_t count; + + /** Priority queue for reservoir sampling. */ + struct lat heap[SAMPLES]; + /** Current size of the heap. */ + size_t heap_size; +}; + +/** Initialize a latency reservoir. */ +static void lats_init(struct lats *lats) { + lats->min = (struct timespec) { .tv_sec = 1000 }; + lats->max = (struct timespec) { 0 }; + lats->sum = (struct timespec) { 0 }; + lats->count = 0; + lats->heap_size = 0; +} + +/** Binary heap parent. */ +static size_t heap_parent(size_t i) { + return (i - 1) / 2; +} + +/** Binary heap left child. */ +static size_t heap_child(size_t i) { + return 2 * i + 1; +} + +/** Binary heap smallest child. */ +static size_t heap_min_child(const struct lats *lats, size_t i) { + size_t j = heap_child(i); + size_t k = j + 1; + if (k < lats->heap_size && lats->heap[k].key < lats->heap[j].key) { + return k; + } else { + return j; + } +} + +/** Check if the heap property is met. */ +static bool heap_check(const struct lat *parent, const struct lat *child) { + return parent->key <= child->key; +} + +/** Reservoir sampling. */ +static void heap_push(struct lats *lats, const struct lat *lat) { + size_t i; + + if (lats->heap_size < SAMPLES) { + // Heapify up + i = lats->heap_size++; + while (i > 0) { + size_t j = heap_parent(i); + if (heap_check(&lats->heap[j], lat)) { + break; + } + lats->heap[i] = lats->heap[j]; + i = j; + } + } else if (lat->key > lats->heap[0].key) { + // Heapify down + i = 0; + while (true) { + size_t j = heap_min_child(lats, i); + if (j >= SAMPLES || heap_check(lat, &lats->heap[j])) { + break; + } + lats->heap[i] = lats->heap[j]; + i = j; + } + } else { + // Reject + return; + } + + lats->heap[i] = *lat; +} + +/** Add a latency sample. */ +static void lats_push(struct lats *lats, const struct timespec *ts) { + timespec_min(&lats->min, ts); + timespec_max(&lats->max, ts); + timespec_add(&lats->sum, ts); + ++lats->count; + + struct lat lat = { + .time = *ts, + .key = lrand48(), + }; + heap_push(lats, &lat); +} + +/** Merge two latency reservoirs. */ +static void lats_merge(struct lats *into, const struct lats *from) { + timespec_min(&into->min, &from->min); + timespec_max(&into->max, &from->max); + timespec_add(&into->sum, &from->sum); + into->count += from->count; + + for (size_t i = 0; i < from->heap_size; ++i) { + heap_push(into, &from->heap[i]); + } +} + +/** Latency qsort() comparator. */ +static int lat_cmp(const void *a, const void *b) { + const struct lat *la = a; + const struct lat *lb = b; + return timespec_cmp(&la->time, &lb->time); +} + +/** Sort the latency reservoir. */ +static void lats_sort(struct lats *lats) { + qsort(lats->heap, lats->heap_size, sizeof(lats->heap[0]), lat_cmp); +} + +/** Get the nth percentile. */ +static const struct timespec *lats_percentile(const struct lats *lats, int percent) { + size_t i = lats->heap_size * percent / 100; + return &lats->heap[i].time; +} + +/** Which clock to use for benchmarking. */ +static clockid_t clockid = CLOCK_REALTIME; + +/** Get a current time measurement. */ +static void gettime(struct timespec *tp) { + int ret = clock_gettime(clockid, tp); + bfs_everify(ret == 0, "clock_gettime(%d)", (int)clockid); +} + +/** + * Time measurements. + */ +struct times { + /** The start time. */ + struct timespec start; + + /** Total requests started. */ + size_t pushed; + /** Total requests finished. */ + size_t popped; + + /** The start time for the currently tracked request. */ + struct timespec req_start; + /** Whether a timed request is currently in flight. */ + bool timing; + + /** Latency measurements. */ + struct lats lats; +}; + +/** Initialize a timer. */ +static void times_init(struct times *times) { + gettime(×->start); + times->pushed = 0; + times->popped = 0; + bfs_assert(!times->timing); + lats_init(×->lats); +} + +/** Finish timing a request. */ +static void track_latency(struct times *times) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, ×->req_start); + lats_push(×->lats, &elapsed); + + bfs_assert(times->timing); + times->timing = false; +} + +/** Add times to the totals, and reset the lap times. */ +static void times_lap(struct times *total, struct times *lap) { + total->pushed += lap->pushed; + total->popped += lap->popped; + lats_merge(&total->lats, &lap->lats); + + times_init(lap); +} + +/** Print some times. */ +static void times_print(struct times *times, long seconds) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, ×->start); + + double fsec = timespec_ns(&elapsed) / 1.0e9; + + if (seconds > 0) { + printf("%5ld", seconds); + } else if (elapsed.tv_nsec >= 10 * 1000 * 1000) { + printf("%5.2f", fsec); + } else { + printf("%5.0f", fsec); + } + + double iops = times->popped / fsec; + double mean = timespec_ns(×->lats.sum) / times->lats.count; + double min = timespec_ns(×->lats.min); + double max = timespec_ns(×->lats.max); + + lats_sort(×->lats); + double n50 = timespec_ns(lats_percentile(×->lats, 50)); + double n90 = timespec_ns(lats_percentile(×->lats, 90)); + double n99 = timespec_ns(lats_percentile(×->lats, 99)); + + printf(" │ %'12.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f\n", iops, mean, min, n50, n90, n99, max); + fflush(stdout); +} + +/** Push an ioq request. */ +static bool push(struct ioq *ioq, enum ioq_nop_type type, struct times *lap) { + void *ptr = NULL; + + // Track latency for a small fraction of requests + if (!lap->timing && (lap->pushed + 1) % PERIOD == 0) { + ptr = lap; + gettime(&lap->req_start); + } + + int ret = ioq_nop(ioq, type, ptr); + if (ret != 0) { + bfs_everify(errno == EAGAIN, "ioq_nop(%d)", (int)type); + return false; + } + + ++lap->pushed; + if (ptr) { + lap->timing = true; + } + return true; +} + +/** Pop an ioq request. */ +static bool pop(struct ioq *ioq, struct times *lap, bool block) { + struct ioq_ent *ent = ioq_pop(ioq, block); + if (!ent) { + return false; + } + + if (ent->ptr) { + track_latency(lap); + } + + ioq_free(ioq, ent); + ++lap->popped; + return true; +} + +/** ^C flag. */ +static atomic bool quit = false; + +/** ^C hook. */ +static void ctrlc(int sig, siginfo_t *info, void *arg) { + store(&quit, true, relaxed); +} + +int main(int argc, char *argv[]) { + // Use CLOCK_MONOTONIC if available +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clockid = CLOCK_MONOTONIC; + } +#endif + + // Enable thousands separators + setlocale(LC_ALL, ""); + + // -d: queue depth + unsigned int depth = 4096; + // -j: threads + unsigned int threads = 0; + // -t: timeout + double timeout = 5.0; + // -L|-H: ioq_nop() type + enum ioq_nop_type type = IOQ_NOP_LIGHT; + + const char *cmd = argc > 0 ? argv[0] : "ioq"; + int c; + while (c = getopt(argc, argv, ":d:j:t:LH"), c != -1) { + switch (c) { + case 'd': + if (xstrtoui(optarg, NULL, 10, &depth) != 0) { + fprintf(stderr, "%s: Bad depth '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 'j': + if (xstrtoui(optarg, NULL, 10, &threads) != 0) { + fprintf(stderr, "%s: Bad thread count '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 't': + if (xstrtod(optarg, NULL, &timeout) != 0) { + fprintf(stderr, "%s: Bad timeout '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 'L': + type = IOQ_NOP_LIGHT; + break; + case 'H': + type = IOQ_NOP_HEAVY; + break; + case ':': + fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt); + return EXIT_FAILURE; + case '?': + fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt); + return EXIT_FAILURE; + } + } + + if (!threads) { + threads = nproc(); + if (threads > 8) { + threads = 8; + } + } + if (threads < 2) { + threads = 2; + } + --threads; + + // Listen for ^C to print the summary + struct sighook *hook = sighook(SIGINT, ctrlc, NULL, SH_CONTINUE | SH_ONESHOT); + + printf("I/O queue benchmark (%s)\n\n", bfs_version); + + printf("[-d] depth: %u\n", depth); + printf("[-j] threads: %u (including main)\n", threads + 1); + if (type == IOQ_NOP_HEAVY) { + printf("[-H] type: heavy (with syscalls)\n"); + } else { + printf("[-L] type: light (no syscalls)\n"); + } + printf("\n"); + + printf(" Time │ Throughput │ Latency │ min │ 50%% │ 90%% │ 99%% │ max\n"); + printf(" (s) │ (IO/s) │ (ns/IO) │ │ │ │ │\n"); + printf("══════╪══════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════\n"); + fflush(stdout); + + struct ioq *ioq = ioq_create(depth, threads); + bfs_everify(ioq, "ioq_create(%u, %u)", depth, threads); + + // Pre-allocate all the requests + while (ioq_capacity(ioq) > 0) { + int ret = ioq_nop(ioq, type, NULL); + bfs_everify(ret == 0, "ioq_nop(%d)", (int)type); + } + while (true) { + struct ioq_ent *ent = ioq_pop(ioq, true); + if (!ent) { + break; + } + ioq_free(ioq, ent); + } + + struct times total, lap; + times_init(&total); + lap = total; + + long seconds = 0; + while (!load(&quit, relaxed)) { + bool was_timing = lap.timing; + + for (int i = 0; i < 16; ++i) { + bool block = ioq_capacity(ioq) == 0; + if (!pop(ioq, &lap, block)) { + break; + } + } + + if (was_timing && !lap.timing) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, &total.start); + + if (elapsed.tv_sec > seconds) { + seconds = elapsed.tv_sec; + times_print(&lap, seconds); + times_lap(&total, &lap); + } + + double ns = timespec_ns(&elapsed); + if (timeout > 0 && ns >= timeout * 1.0e9) { + break; + } + } + + for (int i = 0; i < 8; ++i) { + if (!push(ioq, type, &lap)) { + break; + } + } + ioq_submit(ioq); + } + + while (pop(ioq, &lap, true)); + times_lap(&total, &lap); + + if (load(&quit, relaxed)) { + printf("\r──^C──┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n"); + } else { + printf("──────┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n"); + } + times_print(&total, 0); + + ioq_destroy(ioq); + sigunhook(hook); + return 0; +} diff --git a/build/cc.sh b/build/cc.sh new file mode 100755 index 0000000..e1d2b0b --- /dev/null +++ b/build/cc.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Run the compiler and check if it succeeded. Usage: +# +# $ build/cc.sh [-q] path/to/file.c [-flags -Warnings ...] + +set -eu + +QUIET= +if [ "$1" = "-q" ]; then + QUIET=y + shift +fi + +# Source files can specify their own flags with lines like +# +# /// _CFLAGS += -Wmissing-variable-declarations +# +# which will be added to the makefile on success, or lines like +# +# /// -Werror +# +# which are just used for the current file. +EXTRA_FLAGS=$(sed -n '\|^///|{s|^/// ||; s|[^=]*= ||; p;}' "$1") + +# Without -q, print the executed command for config.log +if [ -z "$QUIET" ]; then + set -x +fi + +$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $EXTRA_FLAGS $XLDLIBS diff --git a/build/config.mk b/build/config.mk new file mode 100644 index 0000000..663926c --- /dev/null +++ b/build/config.mk @@ -0,0 +1,51 @@ +# 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 + +# The main configuration file, which includes the others +gen/config.mk: gen/vars.mk gen/flags.mk gen/pkgs.mk + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @printf 'include %s\n' $^ >>$@ + ${VCAT} $@ +.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" >>$@ + @test -z "$$VERSION" || printf 'export VERSION=%s\n' "$$VERSION" >>$@ + ${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 + +# 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/pkgs.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..204cfa4 --- /dev/null +++ b/build/define-if.sh @@ -0,0 +1,18 @@ +#!/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 + +MACRO=$(printf 'BFS_%s' "$1" | 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/embed.sh b/build/embed.sh new file mode 100755 index 0000000..c0744f6 --- /dev/null +++ b/build/embed.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Convert data into a C array like #embed + +set -eu + +{ cat; printf '\0'; } \ + | od -An -tx1 \ + | sed 's/[^ ][^ ]*/0x&,/g' 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..913a1aa --- /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-if.sh b/build/flags-if.sh new file mode 100755 index 0000000..81eb345 --- /dev/null +++ b/build/flags-if.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Add flags to a makefile if a build succeeds + +set -eu + +build/cc.sh "$@" || exit 1 + +# If the build succeeded, print any lines like +# +# /// _CFLAGS += -foo +# +# (unless they're already set) +OLD_FLAGS="$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS $XLDLIBS" + +while IFS="" read -r line; do + case "$line" in + ///*=*) + flag="${line#*= }" + if [ "${OLD_FLAGS#*"$flag"}" = "$OLD_FLAGS" ]; then + printf '%s\n' "${line#/// }" + fi + ;; + esac +done <"$1" diff --git a/build/flags.mk b/build/flags.mk new file mode 100644 index 0000000..3748a8a --- /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 + +# Internal flags +_CPPFLAGS := -Isrc -Igen -include src/prelude.h +_CFLAGS := -std=c17 +_LDFLAGS := +_LDLIBS := + +# Platform-specific system libraries +LDLIBS,DragonFly := -lposix1e +LDLIBS,FreeBSD := -lrt +LDLIBS,Linux := -lrt +LDLIBS,NetBSD := -lutil +LDLIBS,QNX := -lregex -lsocket +LDLIBS,SunOS := -lsec -lsocket -lnsl +_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}} + +LTO ?= ${RELEASE} +_LTO := ${TRUTHY,${LTO}} + +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 + +_CFLAGS += ${ASAN_CFLAGS,${_ASAN}} +_CFLAGS += ${LSAN_CFLAGS,${_LSAN}} +_CFLAGS += ${MSAN_CFLAGS,${_MSAN}} +_CFLAGS += ${TSAN_CFLAGS,${_TSAN}} +_CFLAGS += ${UBSAN_CFLAGS,${_UBSAN}} + +SAN_CFLAGS,y := -fno-sanitize-recover=all +INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}} +SAN := ${NOT,${INSANE}} +_CFLAGS += ${SAN_CFLAGS,${SAN}} + +# MSAN and TSAN both need all code to be instrumented +YESLIBS := ${NOT,${_MSAN}${_TSAN}} +NOLIBS ?= ${NOT,${YESLIBS}} + +# gcov only intercepts fork()/exec() with -std=gnu* +GCOV_CFLAGS,y := -std=gnu17 --coverage +_CFLAGS += ${GCOV_CFLAGS,${_GCOV}} + +LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT +LINT_CFLAGS,y := -Werror -O2 + +_CPPFLAGS += ${LINT_CPPFLAGS,${_LINT}} +_CFLAGS += ${LINT_CFLAGS,${_LINT}} + +RELEASE_CPPFLAGS,y := -DNDEBUG +RELEASE_CFLAGS,y := -O3 + +_CPPFLAGS += ${RELEASE_CPPFLAGS,${_RELEASE}} +_CFLAGS += ${RELEASE_CFLAGS,${_RELEASE}} + +LTO_CFLAGS,y := -flto=auto +_CFLAGS += ${LTO_CFLAGS,${_LTO}} + +# Configurable flags +CFLAGS ?= -g -Wall + +# Add the configurable flags last so they can override ours +_CPPFLAGS += ${CPPFLAGS} ${EXTRA_CPPFLAGS} +_CFLAGS += ${CFLAGS} ${EXTRA_CFLAGS} +_LDFLAGS += ${LDFLAGS} ${EXTRA_LDFLAGS} +# (except LDLIBS, as earlier libs override later ones) +_LDLIBS := ${LDLIBS} ${EXTRA_LDLIBS} ${_LDLIBS} + +include build/exports.mk + +# Conditionally-supported flags +AUTO_FLAGS := \ + gen/flags/Wformat.mk \ + gen/flags/Wimplicit-fallthrough.mk \ + gen/flags/Wimplicit.mk \ + gen/flags/Wmissing-decls.mk \ + gen/flags/Wmissing-var-decls.mk \ + gen/flags/Wshadow.mk \ + gen/flags/Wsign-compare.mk \ + gen/flags/Wstrict-prototypes.mk \ + gen/flags/Wundef-prefix.mk \ + gen/flags/bind-now.mk \ + gen/flags/deps.mk \ + gen/flags/pthread.mk + +gen/flags.mk: ${AUTO_FLAGS} + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @printf '_CPPFLAGS := %s\n' "$$XCPPFLAGS" >>$@ + @printf '_CFLAGS := %s\n' "$$XCFLAGS" >>$@ + @printf '_LDFLAGS := %s\n' "$$XLDFLAGS" >>$@ + @printf '_LDLIBS := %s\n' "$$XLDLIBS" >>$@ + @printf 'NOLIBS := %s\n' "$$XNOLIBS" >>$@ + @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@ + @cat $^ >>$@ + @cat ${^:%=%.log} >gen/flags.log + ${VCAT} $@ +.PHONY: gen/flags.mk + +# Check that the C compiler works at all +cc:: + @build/cc.sh -q build/empty.c -o gen/.cc.out; \ + ret=$$?; \ + build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \ + exit $$ret + +# The short name of the config test +SLUG = ${@:gen/%.mk=%} +# The source file to build +CSRC = build/${SLUG}.c +# The hidden output file name +OUT = ${SLUG:flags/%=gen/flags/.%.out} + +${AUTO_FLAGS}: cc + @${MKDIR} ${@D} + @build/flags-if.sh ${CSRC} -o ${OUT} >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0 +.PHONY: ${AUTO_FLAGS} diff --git a/build/flags/Wformat.c b/build/flags/Wformat.c new file mode 100644 index 0000000..287b209 --- /dev/null +++ b/build/flags/Wformat.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wformat=2 +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wimplicit-fallthrough.c b/build/flags/Wimplicit-fallthrough.c new file mode 100644 index 0000000..c32058d --- /dev/null +++ b/build/flags/Wimplicit-fallthrough.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wimplicit-fallthrough +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wimplicit.c b/build/flags/Wimplicit.c new file mode 100644 index 0000000..3ea2b90 --- /dev/null +++ b/build/flags/Wimplicit.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Werror=implicit +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wmissing-decls.c b/build/flags/Wmissing-decls.c new file mode 100644 index 0000000..5ef3e96 --- /dev/null +++ b/build/flags/Wmissing-decls.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wmissing-declarations +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wmissing-var-decls.c b/build/flags/Wmissing-var-decls.c new file mode 100644 index 0000000..5c20cc6 --- /dev/null +++ b/build/flags/Wmissing-var-decls.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wmissing-variable-declarations +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wshadow.c b/build/flags/Wshadow.c new file mode 100644 index 0000000..28f6ef3 --- /dev/null +++ b/build/flags/Wshadow.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wshadow +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wsign-compare.c b/build/flags/Wsign-compare.c new file mode 100644 index 0000000..f083083 --- /dev/null +++ b/build/flags/Wsign-compare.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wsign-compare +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wstrict-prototypes.c b/build/flags/Wstrict-prototypes.c new file mode 100644 index 0000000..9614bee --- /dev/null +++ b/build/flags/Wstrict-prototypes.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wstrict-prototypes +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wundef-prefix.c b/build/flags/Wundef-prefix.c new file mode 100644 index 0000000..3eaf82b --- /dev/null +++ b/build/flags/Wundef-prefix.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CPPFLAGS += -Wundef-prefix=BFS_ +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/bind-now.c b/build/flags/bind-now.c new file mode 100644 index 0000000..08bb4f2 --- /dev/null +++ b/build/flags/bind-now.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _LDFLAGS += -Wl,-z,now + +int main(void) { + return 0; +} diff --git a/build/flags/deps.c b/build/flags/deps.c new file mode 100644 index 0000000..1c8c309 --- /dev/null +++ b/build/flags/deps.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CPPFLAGS += -MD -MP + +int main(void) { + return 0; +} diff --git a/build/flags/pthread.c b/build/flags/pthread.c new file mode 100644 index 0000000..db09aa4 --- /dev/null +++ b/build/flags/pthread.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -pthread + +int main(void) { + return 0; +} diff --git a/build/has/--st-birthtim.c b/build/has/--st-birthtim.c new file mode 100644 index 0000000..4da621f --- /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/_Fork.c b/build/has/_Fork.c new file mode 100644 index 0000000..4d7fbd3 --- /dev/null +++ b/build/has/_Fork.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <unistd.h> + +int main(void) { + return _Fork(); +} diff --git a/build/has/acl-get-entry.c b/build/has/acl-get-entry.c new file mode 100644 index 0000000..1e7f473 --- /dev/null +++ b/build/has/acl-get-entry.c @@ -0,0 +1,11 @@ +// 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_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..0b76ee2 --- /dev/null +++ b/build/has/acl-get-file.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#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..67b7d37 --- /dev/null +++ b/build/has/acl-get-tag-type.c @@ -0,0 +1,13 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#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/builtin-riscv-pause.c b/build/has/builtin-riscv-pause.c new file mode 100644 index 0000000..24b0675 --- /dev/null +++ b/build/has/builtin-riscv-pause.c @@ -0,0 +1,7 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +int main(void) { + __builtin_riscv_pause(); + return 0; +} diff --git a/build/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/dprintf.c b/build/has/dprintf.c new file mode 100644 index 0000000..c206fa3 --- /dev/null +++ b/build/has/dprintf.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdio.h> + +int main(void) { + return dprintf(1, "%s\n", "Hello world!"); +} 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/io-uring-max-workers.c b/build/has/io-uring-max-workers.c new file mode 100644 index 0000000..34ab5b7 --- /dev/null +++ b/build/has/io-uring-max-workers.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <liburing.h> + +int main(void) { + struct io_uring ring; + io_uring_queue_init(1, &ring, 0); + unsigned int values[] = {0, 0}; + return io_uring_register_iowq_max_workers(&ring, values); +} 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-getdents.c b/build/has/posix-getdents.c new file mode 100644 index 0000000..f74bbe5 --- /dev/null +++ b/build/has/posix-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 posix_getdents(3, (void *)buf, sizeof(buf), 0); +} 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/pragma-nounroll.c b/build/has/pragma-nounroll.c new file mode 100644 index 0000000..2bdae14 --- /dev/null +++ b/build/has/pragma-nounroll.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// -Werror + +int main(void) { +#pragma nounroll + for (int i = 0; i < 100; ++i); + return 0; +} diff --git a/build/has/pthread-set-name-np.c b/build/has/pthread-set-name-np.c new file mode 100644 index 0000000..324aab9 --- /dev/null +++ b/build/has/pthread-set-name-np.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <pthread.h> +#include <pthread_np.h> + +int main(void) { + pthread_set_name_np(pthread_self(), "name"); + return 0; +} diff --git a/build/has/pthread-setname-np.c b/build/has/pthread-setname-np.c new file mode 100644 index 0000000..a3b94c1 --- /dev/null +++ b/build/has/pthread-setname-np.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <pthread.h> + +int main(void) { + return pthread_setname_np(pthread_self(), "name"); +} diff --git a/build/has/sched-getaffinity.c b/build/has/sched-getaffinity.c new file mode 100644 index 0000000..6f8fd98 --- /dev/null +++ b/build/has/sched-getaffinity.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sched.h> + +int main(void) { + cpu_set_t set; + return sched_getaffinity(0, sizeof(set), &set); +} 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/tcgetwinsize.c b/build/has/tcgetwinsize.c new file mode 100644 index 0000000..d25d12b --- /dev/null +++ b/build/has/tcgetwinsize.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <termios.h> + +int main(void) { + struct winsize ws; + return tcgetwinsize(0, &ws); +} diff --git a/build/has/tcsetwinsize.c b/build/has/tcsetwinsize.c new file mode 100644 index 0000000..6717415 --- /dev/null +++ b/build/has/tcsetwinsize.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <termios.h> + +int main(void) { + const struct winsize ws = {0}; + return tcsetwinsize(0, &ws); +} 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/timer-create.c b/build/has/timer-create.c new file mode 100644 index 0000000..d5354c3 --- /dev/null +++ b/build/has/timer-create.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <time.h> + +int main(void) { + timer_t timer; + return timer_create(CLOCK_REALTIME, NULL, &timer); +} 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..f15829a --- /dev/null +++ b/build/header.mk @@ -0,0 +1,93 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that generates gen/config.h + +include build/prelude.mk +include gen/vars.mk +include gen/flags.mk +include gen/pkgs.mk +include build/exports.mk + +# All header fragments we generate +HEADERS := \ + gen/has/--st-birthtim.h \ + gen/has/_Fork.h \ + 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/builtin-riscv-pause.h \ + gen/has/confstr.h \ + gen/has/dprintf.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-syscall.h \ + gen/has/getdents64.h \ + gen/has/getmntent-1.h \ + gen/has/getmntent-2.h \ + gen/has/getmntinfo.h \ + gen/has/getprogname-gnu.h \ + gen/has/getprogname.h \ + gen/has/io-uring-max-workers.h \ + gen/has/pipe2.h \ + gen/has/pragma-nounroll.h \ + gen/has/posix-getdents.h \ + gen/has/posix-spawn-addfchdir-np.h \ + gen/has/posix-spawn-addfchdir.h \ + gen/has/pthread-set-name-np.h \ + gen/has/pthread-setname-np.h \ + gen/has/sched-getaffinity.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-syscall.h \ + gen/has/statx.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/tcgetwinsize.h \ + gen/has/tcsetwinsize.h \ + gen/has/timegm.h \ + gen/has/timer-create.h \ + gen/has/tm-gmtoff.h \ + gen/has/uselocale.h + +# Previously generated by pkgs.mk +PKG_HEADERS := ${ALL_PKGS:%=gen/with/%.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 $^ >>$@ + @printf '#endif // BFS_CONFIG_H\n' >>$@ + @cat gen/flags.log ${^:%=%.log} >gen/config.log + ${VCAT} $@ + @printf '%s' "$$CONFFLAGS" | build/embed.sh >gen/confflags.i + @printf '%s' "$$XCC" | build/embed.sh >gen/cc.i + @printf '%s' "$$XCPPFLAGS" | build/embed.sh >gen/cppflags.i + @printf '%s' "$$XCFLAGS" | build/embed.sh >gen/cflags.i + @printf '%s' "$$XLDFLAGS" | build/embed.sh >gen/ldflags.i + @printf '%s' "$$XLDLIBS" | build/embed.sh >gen/ldlibs.i +.PHONY: gen/config.h + +# The short name of the config test +SLUG = ${@:gen/%.h=%} +# The hidden output file name +OUT = ${SLUG:has/%=gen/has/.%.out} + +${HEADERS}:: + @${MKDIR} ${@D} + @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0 diff --git a/build/msg-if.sh b/build/msg-if.sh new file mode 100755 index 0000000..afb478c --- /dev/null +++ b/build/msg-if.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Print a success/failure indicator from a makefile: +# +# $ ./configure +# [ CC ] with/liburing.c ✘ +# [ CC ] with/oniguruma.c ✔ + +set -eu + +MSG="$1" +shift + +if [ -z "${NO_COLOR:-}" ] && [ -t 1 ]; then + Y='\033[1;32m✔\033[0m' + N='\033[1;31m✘\033[0m' +else + Y='✔' + N='✘' +fi + +if "$@"; then + YN="$Y" +else + YN="$N" +fi + +build/msg.sh "$(printf "%-37s $YN" "$MSG")" diff --git a/build/msg.sh b/build/msg.sh new file mode 100755 index 0000000..2249125 --- /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 + +exec "$@" diff --git a/build/pkgconf.sh b/build/pkgconf.sh new file mode 100755 index 0000000..decf706 --- /dev/null +++ b/build/pkgconf.sh @@ -0,0 +1,96 @@ +#!/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 ${WITH_$LIB} + WITH_LIB="WITH_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')" + eval "WITH=\"\${$WITH_LIB:-}\"" + case "$WITH" in + y|1) continue ;; + n|0) exit 1 ;; + esac + + XCFLAGS="$XCFLAGS $("$0" --cflags "$LIB")" || exit 1 + XLDFLAGS="$XLDFLAGS $("$0" --ldflags "$LIB")" || exit 1 + XLDLIBS="$("$0" --ldlibs "$LIB") $XLDLIBS" || exit 1 + build/cc.sh "build/with/$LIB.c" -o "gen/with/.$LIB.out" || 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..f692739 --- /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/with/%.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$$' $^ | sed 's|.*/\(.*\)\.h|\1|') >>$@ + ${VCAT} $@ + +.PHONY: gen/pkgs.mk + +# Convert gen/with/foo.h to foo +PKG = ${@:gen/with/%.h=%} + +${HEADERS}:: + @${MKDIR} ${@D} + @build/define-if.sh with/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] with/${PKG}.c" test $$? -eq 0; diff --git a/build/prelude.mk b/build/prelude.mk new file mode 100644 index 0000000..6250d73 --- /dev/null +++ b/build/prelude.mk @@ -0,0 +1,68 @@ +# 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 use the GNU version, bringing it to BSD like this: +^ ?= $> + +# 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,} => [empty] +# +# 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 diff --git a/build/version.sh b/build/version.sh new file mode 100755 index 0000000..ec0663a --- /dev/null +++ b/build/version.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Print the version number + +set -eu + +DIR="$(dirname -- "$0")/.." + +if [ "${VERSION-}" ]; then + printf '%s' "$VERSION" +elif [ -e "$DIR/.git" ] && command -v git >/dev/null 2>&1; then + git -C "$DIR" describe --always --dirty +else + echo "4.0.8" +fi diff --git a/build/with/libacl.c b/build/with/libacl.c new file mode 100644 index 0000000..de1fe50 --- /dev/null +++ b/build/with/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/with/libcap.c b/build/with/libcap.c new file mode 100644 index 0000000..58e832c --- /dev/null +++ b/build/with/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/with/libselinux.c b/build/with/libselinux.c new file mode 100644 index 0000000..bca409d --- /dev/null +++ b/build/with/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/with/liburing.c b/build/with/liburing.c new file mode 100644 index 0000000..bea499a --- /dev/null +++ b/build/with/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/with/oniguruma.c b/build/with/oniguruma.c new file mode 100644 index 0000000..cb17596 --- /dev/null +++ b/build/with/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..0dd39f4 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 @@ -98,6 +99,7 @@ _bfs() { -ignore_readdir_race -mount -nocolor + -noerror -noignore_readdir_race -noleaf -nowarn diff --git a/completions/bfs.fish b/completions/bfs.fish index 0c58ef4..7182bee 100644 --- a/completions/bfs.fish +++ b/completions/bfs.fish @@ -42,7 +42,8 @@ complete -c bfs -o ignore_readdir_race -d "Don't report an error if the file tre complete -c bfs -o noignore_readdir_race -d "Report an error if the file tree is modified during the search" complete -c bfs -o maxdepth -d "Ignore files deeper than specified number" -x complete -c bfs -o mindepth -d "Ignore files shallower than specified number" -x -complete -c bfs -o mount -d "Don't descend into other mount points" +complete -c bfs -o mount -d "Exclude mount points" +complete -c bfs -o noerror -d "Ignore any errors that occur during traversal" complete -c bfs -o nohidden -d "Exclude hidden files and directories" complete -c bfs -o noleaf -d "Ignored; for compatibility with GNU find" complete -c bfs -o regextype -d "Use specified flavored regex" -a $regex_type_comp -x @@ -71,6 +72,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..6b46f83 100644 --- a/completions/bfs.zsh +++ b/completions/bfs.zsh @@ -41,7 +41,8 @@ args=( '*-noignore_readdir_race[do not report an error if bfs detects file tree is modified during search]' '*-maxdepth[ignore files deeper than N]:maximum search depth' '*-mindepth[ignore files shallower than N]:minimum search depth' - "*-mount[don't descend into other mount points]" + "*-mount[exclude mount points]" + '*-noerror[ignore any errors that occur during traversal]' '*-nohidden[exclude hidden files]' '*-noleaf[ignored, for compatibility with GNU find]' '-regextype[type of regex to use, default posix-basic]:regexp syntax:(help posix-basic posix-extended ed emacs grep sed)' @@ -74,6 +75,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]' @@ -89,11 +91,11 @@ args=( '*-user[find files owned by user NAME]:user:_users' '*-hidden[find hidden files (those beginning with .)]' - '*-ilname[find symbolic links whose target matches GLOB (case insensitve)]:link pattern to search (case insensitive):' + '*-ilname[find symbolic links whose target matches GLOB (case insensitive)]:link pattern to search (case insensitive):' '*-iname[find files whose name matches GLOB (case insensitive)]:name pattern to match (case insensitive):' '*-inum[find files with inode number N]:inode number:' - '*-ipath[find files whose entire path matches GLOB (case insenstive)]:path pattern to search (case insensitive):' - '*-iregex[find files whose entire path matches REGEX (case insenstive)]:regular expression to search (case insensitive):' + '*-ipath[find files whose entire path matches GLOB (case insensitive)]:path pattern to search (case insensitive):' + '*-iregex[find files whose entire path matches REGEX (case insensitive)]:regular expression to search (case insensitive):' '*-iwholename[find files whose entire path matches GLOB (case insensitive)]:full path pattern to search (case insensitive):' '*-links[find files with N hard links]:number of links:' diff --git a/configure b/configure new file mode 100755 index 0000000..7f0bd04 --- /dev/null +++ b/configure @@ -0,0 +1,237 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# bfs build configuration script + +set -eu + +# Get the relative path to the source tree based on how the script was run +DIR=$(dirname -- "$0") + +# Print the help message +help() { + cat <<EOF +Usage: + + \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...] + \$ $MAKE -j$(_nproc) + +Variables set in the environment or on the command line will be picked up: + + MAKE + The make implementation to use + CC + The C compiler to use + + CPPFLAGS="-I... -D..." + CFLAGS="-W... -f..." + LDFLAGS="-L... -Wl,..." + Preprocessor/compiler/linker flags + + LDLIBS="-l... -l..." + Dynamic libraries to link + + EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS} + Adds to the default flags, instead of replacing them + +The default flags result in a plain debug build. Other build profiles include: + + --enable-release + Enable optimizations, disable assertions + --enable-{asan,lsan,msan,tsan,ubsan} + Enable sanitizers + --enable-gcov + Enable code coverage instrumentation + +External dependencies are auto-detected by default, but you can build --with or +--without them explicitly: + + --with-libacl --without-libacl + --with-libcap --without-libcap + --with-libselinux --without-libselinux + --with-liburing --without-liburing + --with-oniguruma --without-oniguruma + +Packaging: + + --prefix=/path + Set the installation prefix (default: /usr) + --mandir=/path + Set the man page directory (default: \$PREFIX/share/man) + --version=X.Y.Z + Set the version string (default: $("$DIR/build/version.sh")) + +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$(_nproc) V=1 +EOF +} + +# Report a warning +warn() { + fmt="$1" + shift + printf "%s: warning: $fmt\\n" "$0" "$@" >&2 +} + +# Report an argument parsing error +invalid() { + printf '%s: error: Unrecognized option "%s"\n\n' "$0" "$1" >&2 + printf 'Run %s --help for more information.\n' "$0" >&2 + exit 1 +} + +# Get the number of cores to use +_nproc() { + { + nproc \ + || sysctl -n hw.ncpu \ + || getconf _NPROCESSORS_ONLN \ + || echo 1 + } 2>/dev/null +} + +# Save the ./configure command line for bfs --version +export CONFFLAGS="" + +# Default to `make` +MAKE="${MAKE-make}" + +# Parse the command-line arguments +for arg; do + shift + + # Only add --options to CONFFLAGS, so we don't print FLAG=values twice in bfs --version + case "$arg" in + -*) + CONFFLAGS="${CONFFLAGS}${CONFFLAGS:+ }${arg}" + ;; + esac + + # --[(enable|disable|with|without)-]$name[=$value] + value="${arg#*=}" + name="${arg%%=*}" + name="${name#--}" + case "$arg" in + --enable-*|--disable-*|--with-*|--without-*) + name="${name#*-}" + ;; + esac + NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_') + + # y/n modality + case "$arg" in + --enable-*|--with-*) + case "$arg" in + *=y|*=yes) yn=y ;; + *=n|*=no) yn=n ;; + *=*) invalid "$arg" ;; + *) yn=y ;; + esac + ;; + --disable-*|--without-*) + case "$arg" in + *=*) invalid "arg" ;; + *) yn=n ;; + esac + ;; + esac + + # Fix up --enable-lib* to --with-lib* + case "$arg" in + --enable-*|--disable-*) + case "$name" in + libacl|libcap|libselinux|liburing|oniguruma) + old="$arg" + case "$arg" in + --enable-*) arg="--with-${arg#--*-}" ;; + --disable-*) arg="--without-${arg#--*-}" ;; + esac + warn 'Treating "%s" like "%s"' "$old" "$arg" + ;; + esac + ;; + esac + + case "$arg" in + -h|--help) + help + exit 0 + ;; + + --enable-*|--disable-*) + case "$name" in + release|lto|asan|lsan|msan|tsan|ubsan|lint|gcov) + set -- "$@" "$NAME=$yn" + ;; + *) + invalid "$arg" + ;; + esac + ;; + + --with-*|--without-*) + case "$name" in + libacl|libcap|libselinux|liburing|oniguruma) + set -- "$@" "WITH_$NAME=$yn" + ;; + *) + invalid "$arg" + ;; + esac + ;; + + --prefix=*|--mandir=*|--version=*) + set -- "$@" "$NAME=$value" + ;; + + --infodir=*|--build=*|--host=*|--target=*) + warn 'Ignoring option "%s"' "$arg" + ;; + + MAKE=*) + MAKE="$value" + ;; + + # Warn about MAKE variables that have documented configure flags + RELEASE=*|LTO=*|ASAN=*|LSAN=*|MSAN=*|TSAN=*|UBSAN=*|LINT=*|GCOV=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --enable-%s' "$arg" "$name" + set -- "$@" "$arg" + ;; + + PREFIX=*|MANDIR=*|VERSION=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --%s=%s' "$arg" "$name" "$value" + set -- "$@" "$arg" + ;; + + WITH_*=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --%s' "$arg" "$name" + set -- "$@" "$arg" + ;; + + # make flag (-j2) or variable (CC=clang) + -*|*=*) + set -- "$@" "$arg" + ;; + + *) + invalid "$arg" + ;; + esac +done + +# Set up symbolic links for out-of-tree builds +for f in Makefile bench 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$(_nproc)}" + +$MAKE -rf build/config.mk "$@" diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 02f9756..69a997c 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-*] [--with-*|--without-*] [CC=...] [...] + $ 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 build `--with` or `--without` them explicitly: -| 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> +--with-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --without-libacl +--with-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --without-libcap +--with-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --without-libselinux +--with-<a href="https://github.com/axboe/liburing">liburing</a> --without-liburing +--with-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --without-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 undesirable, 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 @@ -126,11 +178,11 @@ It can be handy to generate the snapshot with a different `find` implementation But keep in mind, other `find` implementations may not be correct. To my knowledge, no other implementation passes even the POSIX-compatible subset of the tests: - $ ./tests/tests.sh --bfs=find --posix + $ ./tests/tests.sh --bfs=find --sudo --posix ... - tests passed: 90 - tests skipped: 3 - tests failed: 6 + [PASS] 104 / 119 + [SKIP] 1 / 119 + [FAIL] 14 / 119 Run diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 672c2b4..56f53b4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,295 @@ +4.* +=== + +4.0.8 +----- + +**June 20, 2025** + +### Bug fixes + +- Fixed an invalid optimization that transformed + + $ bfs -user you -or -user me + + into just + + $ bfs -user you + + The bug was originally introduced in bfs 2.0 (October 14, 2020). + ([#155](https://github.com/tavianator/bfs/issues/155)) + + +4.0.7 +----- + +**June 15, 2025** + +### Changes + +- `bfs` now takes CPU affinity into account when picking how many threads to use + ([`a36774b`](https://github.com/tavianator/bfs/commit/a36774be636c3429c6e73de33bf65a1bdbdcfb4b)) + +- `-execdir /bin/...` is now allowed even with a relative path in `$PATH` + ([`cb40f51`](https://github.com/tavianator/bfs/commit/cb40f51e4e6375a10265484b6959c6b1b0591378)) + +- *Expect* is no longer a test suite dependency + ([`7102fec`](https://github.com/tavianator/bfs/commit/7102fec257835302cb4978160bba4cbebd0b63e1)) + +### Bug fixes + +- Only the last `-files0-from` argument now has any effect, to match GNU find + ([`a662fda`](https://github.com/tavianator/bfs/commit/a662fda2642e17478bc8e78adb4c6642a8505cdb)) + +- Fixed `-execdir {}`, which was inadvertently broken in bfs 4.0 + ([`def4a83`](https://github.com/tavianator/bfs/commit/def4a832425bfe94b96b8cb1146a83552b754fb4)) + + +4.0.6 +----- + +**February 26, 2025** + +### Bug fixes + +- Fixed `-fstype` with btrfs subvolumes (requires Linux 5.8+) + ([`0dccdae`](https://github.com/tavianator/bfs/commit/0dccdae4510ff5603247be871e64a6119647ea2a)) + +- Fixed `-ls` with timestamps very far in the future + ([`dd5df1f`](https://github.com/tavianator/bfs/commit/dd5df1f8997550c5bf49205578027715b957bd01)) + +- Fixed the `posix/exec_sigmask` test on mips64el Linux + ([`532dec0`](https://github.com/tavianator/bfs/commit/532dec0849dcdc3e15e530ac40a8168f146a41cd)) + +- Fixed time-related tests with `mawk 1.3.4 20250131` + ([#152](https://github.com/tavianator/bfs/issues/152)) + + +4.0.5 +----- + +**January 18, 2025** + +### Bug fixes + +- Fixed a bug that could cause child processes (e.g. from `-exec`) to run with all signals blocked. + The bug was introduced in version 3.3. + ([`af207e7`](https://github.com/tavianator/bfs/commit/af207e702148e5c9ae08047d7a2dce6394653b62)) + +### Changes + +- Fixed the build against old liburing versions + ([#147](https://github.com/tavianator/bfs/issues/147)) + +- Async I/O performance optimizations + + +4.0.4 +----- + +**October 31, 2024** + +## Bug fixes + +- Fixed a man page typo + ([#144](https://github.com/tavianator/bfs/pull/144)) + +- Fixed the build on PowerPC macOS + ([#145](https://github.com/tavianator/bfs/issues/145)) + +- Fixed a bug introduced in bfs 4.0.3 that colorized every file as if it had capabilities on non-Linux systems + ([#146](https://github.com/tavianator/bfs/pull/146)) + + +4.0.3 +----- + +**October 22, 2024** + +### Bug fixes + +- Fixed an assertion failure when `$LS_COLORS` contained escaped NUL bytes like `*\0.gz=` + ([`f5eaadb9`](https://github.com/tavianator/bfs/commit/f5eaadb96fb94b2d3666e53a99495840a3099aec)) + +- Fixed a use-after-free bug introduced in bfs 4.0 when unregistering and re-registering signal hooks. + This could be reproduced with `bfs -nocolor` by repeatedly sending `SIGINFO`/`SIGUSR1` to toggle the status bar. + ([`39ff273`](https://github.com/tavianator/bfs/commit/39ff273df97e51b1285358b9e6808b117ea8adb1)) + +- Fixed a hang present since bfs 3.0 colorizing paths like `notdir/file`, where `notdir` is a symlink pointing to a non-directory file. + ([`b89f22cb`](https://github.com/tavianator/bfs/commit/b89f22cbf250958a802915eb7b6bf0e5f38376ca)) + + +4.0.2 +----- + +**September 17, 2024** + +### New features + +- Implemented `./configure --version=X.Y.Z`, mainly for packagers to override the version number + ([`4a278d3`](https://github.com/tavianator/bfs/commit/4a278d3e39a685379711727eac7bfaa83679e0e4)) + +### Changes + +- Minor refactoring of the build system + +### Bug fixes + +- Fixed `./configure --help`, which was broken since `bfs` 4.0 + ([`07ae989`](https://github.com/tavianator/bfs/commit/07ae98906dbb0caaac2f758d72e88dd0975b2a81)) + +- Fixed compiler flag auto-detection on systems with non-GNU `sed`. + This fixes a potential race condition on FreeBSD since `bfs` 4.0 due to the [switch to `_Fork()`](https://github.com/tavianator/bfs/commit/085bb402c7b2c2f96624fb0523ff3f9686fe26d9) without passing `-z now` to the linker. + ([`34e6081`](https://github.com/tavianator/bfs/commit/34e60816adb0ea8ddb155a454676a99ab225dc8a)) + +- Fixed `$MAKE distcheck` when `$MAKE` is not `make`, e.g. `gmake distcheck` on BSD + ([`2135b00`](https://github.com/tavianator/bfs/commit/2135b00d215efc5c2c38e1abd3254baf31229ad4)) + +- Fixed some roff syntax issues in the `bfs` manpage + ([`812ecd1`](https://github.com/tavianator/bfs/commit/812ecd1feeb002252dd4d732b395d31c4179afaf)) + +- Fixed an assertion failure optimizing expressions like `bfs -not \( -prune , -type f \)` since `bfs` 3.1. + Release builds were not affected, since their assertions are disabled and the behaviour was otherwise correct. + ([`b1a9998`](https://github.com/tavianator/bfs/commit/b1a999892b9e13181ddd9a7d895f3d1c65fbb449)) + + +4.0.1 +----- + +**August 19, 2024** + +### Bug fixes + +- `bfs` no longer prints a "suppressed errors" warning unless `-noerror` is actually suppressing errors + ([`5d03c9d`](https://github.com/tavianator/bfs/commit/5d03c9d460d1c1afcdf062d494537986ce96a690)) + + +4.0 +--- + +**August 16, 2024** + +### New features + +- To match BSD `find` (and the POSIX Utility Syntax Guidelines), multiple flags can now be given in a single argument like `-LEXO2`. + Previously, you would have had to write `-L -E -X -O2`. + ([`c0fd33a`](https://github.com/tavianator/bfs/commit/c0fd33aaef5f345566a41c7c2558f27adf05558b)) + +- Explicit timestamps can now be written as `@SECONDS_SINCE_EPOCH`. + For example, `bfs -newermt @946684800` will print files modified since January 1, 2000 (UTC). + ([`c6bb003`](https://github.com/tavianator/bfs/commit/c6bb003b8882e9a16941f5803d072ec1cb728318)) + +- The new `-noerror` option suppresses all error messages during traversal. + ([#142](https://github.com/tavianator/bfs/issues/142)) + +### Changes + +- `-mount` now excludes mount points entirely, to comply with the recently published POSIX 2024 standard. + Use `-xdev` to include the mount point itself, but not its contents. + `bfs` has been warning about this change since version 1.5.1 (September 2019). + ([`33b85e1`](https://github.com/tavianator/bfs/commit/33b85e1f8769e7f75721887638ae454d109a034f)) + +- `-perm` now takes the current file creation mask into account when parsing a symbolic mode like `+rw`, as clarified by [POSIX defect 1392](https://www.austingroupbugs.net/view.php?id=1392). + This matches the behaviour of BSD `find`, contrary to the behaviour of GNU `find`. + ([`6290ce4`](https://github.com/tavianator/bfs/commit/6290ce41f3ec1f889abb881cf90ca91da869b5b2)) + +### Bug fixes + +- Fixed commands like `./configure CC=clang --enable-release` that set variables before other options + ([`49a5d48`](https://github.com/tavianator/bfs/commit/49a5d48d0a43bac313c8b8d1b167e60da9eaadf6)) + +- Fixed the build on RISC-V with GCC versions older than 14 + ([`e93a1dc`](https://github.com/tavianator/bfs/commit/e93a1dccd82f831a2f0d2cc382d8af5e1fda55ed)) + +- Fixed running `bfs` under Valgrind + ([`a01cfac`](https://github.com/tavianator/bfs/commit/a01cfacd423af28af6b7c13ba51e2395f3a52ee7)) + +- Fixed the exit code when failing to execute a non-existent command with `-exec`/`-ok` on some platforms including OpenBSD and HPPA + ([`8c130ca`](https://github.com/tavianator/bfs/commit/8c130ca0117fd225c24569be2ec16c7dc2150a13)) + +- Fixed `$LS_COLORS` case-sensitivity to match GNU ls more closely when the same extension is specified multiple times + ([`08030ae`](https://github.com/tavianator/bfs/commit/08030aea919039165c02805e8c637a9ec1ad0d70)) + +- Fixed the `-status` bar on Solaris/Illumos + + 3.* === +3.3.1 +----- + +**June 3, 2024** + +### Bug fixes + +- Reduced the scope of the symbolic link loop change in version 3.3. + `-xtype l` remains true for symbolic link loops, matching a change in GNU findutils 4.10.0. + However, `-L` will report an error, just like `bfs` prior to 3.3 and other `find` implementations, as required by POSIX. + + +3.3 +--- + +**May 28, 2024** + +### New features + +- The `-status` bar can now be toggled by `SIGINFO` (<kbd>Ctrl</kbd>+<kbd>T</kbd>) on systems that support it, and `SIGUSR1` on other systems + +- `-regextype` now supports all regex types from GNU find ([#21](https://github.com/tavianator/bfs/issues/21)) + +- File birth times are now supported on OpenBSD + +### Changes + +- Symbolic link loops are now treated like other broken links, rather than an error + +- `./configure` now expects `--with-libacl`, `--without-libcap`, etc. rather than `--enable-`/`--disable-` + +- The ` ` (space) flag is now restricted to numeric `-printf` specifiers + +### Bug fixes + +- `-regextype emacs` now supports [shy](https://www.gnu.org/software/emacs/manual/html_node/elisp/Regexp-Backslash.html#index-shy-groups) (non-capturing) groups + +- Fixed `-status` bar visual corruption when the terminal is resized + +- `bfs` now prints a reset escape sequence when terminated by a signal in the middle of colored output ([#138](https://github.com/tavianator/bfs/issues/138)) + +- `./configure CFLAGS=...` no longer overrides flags from `pkg-config` during configuration + + +3.2 +--- + +**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 ----- @@ -153,7 +442,7 @@ - Breadth-first search could become highly unbalanced, negating many of the benefits of `bfs` - - On non-{Linux,FreeBSD} plaforms, directories could stay open longer than necessary, consuming extra memory + - On non-{Linux,FreeBSD} platforms, directories could stay open longer than necessary, consuming extra memory [#107]: https://github.com/tavianator/bfs/pull/107 diff --git a/docs/RELATED.md b/docs/RELATED.md index cf52b70..6e7bd38 100644 --- a/docs/RELATED.md +++ b/docs/RELATED.md @@ -25,8 +25,9 @@ These are not usually installed as the system `find`, but are designed to be `fi - [`bfs`](https://tavianator.com/projects/bfs.html) ([manual](https://man.archlinux.org/man/bfs.1), [source](https://github.com/tavianator/bfs)) - [schilytools](https://codeberg.org/schilytools/schilytools) `sfind` ([source](https://codeberg.org/schilytools/schilytools/src/branch/master/sfind)) - [BusyBox](https://busybox.net/) `find` ([manual](https://busybox.net/downloads/BusyBox.html#find), [source](https://git.busybox.net/busybox/tree/findutils/find.c)) -- [ToyBox](http://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c)) -- uutils `find` ([source](https://github.com/uutils/findutils)) +- [ToyBox](https://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c)) +- [Heirloom Project](https://heirloom.sourceforge.net/) `find` ([manual](https://heirloom.sourceforge.net/man/find.1.html), [source](https://github.com/eunuchs/heirloom-project/blob/master/heirloom/heirloom/find/find.c)) +- [uutils](https://uutils.github.io/) `find` ([source](https://github.com/uutils/findutils)) ## `find` alternatives diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..dd3277a --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,126 @@ +Security +======== + +Threat model +------------ + +`bfs` is a command line program running on multi-user operating systems. +Those other users may be malicious, but `bfs` should not allow them to do anything they couldn't already do. +That includes situations where one user (especially `root`) is running `bfs` on files owned or controlled by another user. + +On the other hand, `bfs` implicitly trusts the user running it. +Anyone with enough control over the command line of `bfs` or any `find`-compatible tool can wreak havoc with dangerous actions like `-exec`, `-delete`, etc. + +> [!CAUTION] +> The only untrusted input that should *ever* be passed on the `bfs` command line are **file paths**. +> It is *always* unsafe to allow *any* other part of the command line to be affected by untrusted input. +> Use the `-f` flag, or `-files0-from`, to ensure that the input is interpreted as a path. + +This still has security implications, including: + +- **Information disclosure:** an attacker may learn whether particular files exist by observing `bfs`'s output, exit status, or even side channels like execution time. +- **Denial of service:** large directory trees or slow/network storage may cause `bfs` to consume excessive system resources. + +> [!TIP] +> When in doubt, do not pass any untrusted input to `bfs`. + + +Executing commands +------------------ + +The `-exec` family of actions execute commands, passing the matched paths as arguments. +File names that begin with a dash may be misinterpreted as options, so `bfs` adds a leading `./` in some instances: + +```console +user@host$ bfs -execdir echo {} \; +./-rf +``` + +This might save you from accidentally running `rm -rf` (for example) when you didn't mean to. +This mitigation applies to `-execdir`, but not `-exec`, because the full path typically does not begin with a dash. +But it is possible, so be careful: + +```console +user@host$ bfs -f -rf -exec echo {} \; +-rf +``` + + +Race conditions +--------------- + +Like many programs that interface with the file system, `bfs` can be affected by race conditions—in particular, "[time-of-check to time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)" (TOCTTOU) issues. +For example, + +```console +user@host$ bfs / -user user -exec dangerous_command {} \; +``` + +is not guaranteed to only run `dangerous_command` on files you own, because another user may run + +```console +evil@host$ mv /path/to/file /path/to/exile +evil@host$ mv ~/malicious /path/to/file +``` + +in between checking `-user user` and executing the command. + +> [!WARNING] +> Be careful when running `bfs` on directories that other users have write access to, because they can modify the directory tree while `bfs` is running, leading to unpredictable results and possible TOCTTOU issues. + + +Output sanitization +------------------- + +In general, printing arbitrary data to a terminal may have [security](https://hdm.io/writing/termulation.txt) [implications](https://dgl.cx/2023/09/ansi-terminal-security#vulnerabilities-using-known-replies). +On many platforms, file paths may be completely arbitrary data (except for NUL (`\0`) bytes). +Therefore, when `bfs` is writing output to a terminal, it will escape non-printable characters: + +<pre> +user@host$ touch $'\e[1mBOLD\e[0m' +user@host$ bfs +. +./$'\e[1mBOLD\e[0m' +</pre> + +However, this is fragile as it only applies when outputting directly to a terminal: + +<pre> +user@host$ bfs | grep BOLD +<strong>BOLD</strong> +</pre> + + +Code quality +------------ + +Every correctness issue in `bfs` is a potential security issue, because acting on the wrong path may do arbitrarily bad things. +For example: + +```console +root@host# bfs /etc -name passwd -exec cat {} \; +``` + +should print `/etc/passwd` but not `/etc/shadow`. +`bfs` tries to ensure correct behavior through careful programming practice, an extensive testsuite, and static analysis. + +`bfs` is written in C, which is a memory unsafe language. +Bugs that lead to memory corruption are likely to be exploitable due to the nature of C. +We use [sanitizers](https://github.com/google/sanitizers) to try to detect these bugs. +Fuzzing has also been applied in the past, and deploying continuous fuzzing is a work in progress. + + +Supported versions +------------------ + +`bfs` comes with [no warranty](/LICENSE), and is maintained by [me](https://tavianator.com/) and [other volunteers](https://github.com/tavianator/bfs/graphs/contributors) in our spare time. +In that sense, there are no *supported* versions. +However, as long as I maintain `bfs` I will attempt to address any security issues swiftly. +In general, security fixes will be part of the latest release, though for significant issues I may backport fixes to older release series. + + +Reporting a vulnerability +------------------------- + +If you think you have found a sensitive security issue in `bfs`, you can [report it privately](https://github.com/tavianator/bfs/security/advisories/new). +Or you can [report it publicly](https://github.com/tavianator/bfs/issues/new); I won't judge you. diff --git a/docs/USAGE.md b/docs/USAGE.md index 071c95b..16aeaf6 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 @@ -105,7 +105,7 @@ For expressions like `-name`, that's all they do. But some expressions, called *actions*, have other side effects. If no actions are included in the expression, `bfs` adds the `-print` action automatically, which is why the above examples actually print any output. -The default `-print` is supressed if any actions are given explicitly. +The default `-print` is suppressed if any actions are given explicitly. Available actions include printing with alternate formats (`-ls`, `-printf`, etc.), executing commands (`-exec`, `-execdir`, etc.), deleting files (`-delete`), and stopping the search (`-quit`, `-exit`). @@ -1,4 +1,6 @@ -.TH BFS 1 +.\" Copyright © Tavian Barnes <tavianator@tavianator.com> +.\" SPDX-License-Identifier: 0BSD +.TH BFS 1 2025-06-15 "bfs 4.0.8" .SH NAME bfs \- breadth-first search for your files .SH SYNOPSIS @@ -41,17 +43,17 @@ For example, .PP .nf .RS -.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-and \-print +.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-and \-print .RE .fi .PP -will print the all the paths that are either .txt files or symbolic links to .txt files. +will print all the paths that are either .txt files or symbolic links to .txt files. .B \-and is implied between two consecutive expressions, so this is equivalent: .PP .nf .RS -.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-print +.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-print .RE .fi .PP @@ -71,7 +73,7 @@ will also accept .I \-N or .IR +N . -.IR \-N +.I \-N means "less than .IR N ," and @@ -90,7 +92,9 @@ Follow all symbolic links. Never follow symbolic links (the default). .TP .B \-E -Use extended regular expressions (same as \fB\-regextype \fIposix-extended\fR). +Use extended regular expressions (same as +.B \-regextype +.IR posix-extended ). .TP .B \-X Filter out files with @@ -109,20 +113,20 @@ The sorting takes place within each directory separately, which makes it differe but still provides a deterministic ordering. .TP .B \-x -Don't descend into other mount points (same as \fB\-xdev\fR). +Don't descend into other mount points (same as +.BR \-xdev ). .TP -\fB\-f \fIPATH\fR +.BI "\-f " PATH Treat .I PATH as a path to search (useful if it begins with a dash). -.PP .TP -\fB\-D \fIFLAG\fR +.BI "\-D " FLAG Turn on a debugging flag (see .B \-D .IR help ). .PP -\fB\-O\fIN\fR +.BI \-O N .RS Enable optimization level .I N @@ -178,35 +182,42 @@ Typically far faster than .IR ids . .RE .TP -\fB\-j\fIN\fR +.BI \-j N Search with .I N threads in parallel (default: number of CPUs, up to .IR 8 ). .SH OPERATORS .TP -\fB( \fIexpression \fB)\fR +.BI "( " expression " )" Parentheses are used for grouping expressions together. You'll probably have to write -.B \\\\( +.B \e( .I expression -.B \\\\) +.B \e) to avoid the parentheses being interpreted by the shell. .PP \fB! \fIexpression\fR .br -\fB\-not \fIexpression\fR +.B \-not +.I expression .RS The "not" operator: returns the negation of the truth value of the .IR expression . -You may have to write \fB\\! \fIexpression\fR to avoid \fB!\fR being interpreted by the shell. +You may have to write \fB\e! \fIexpression\fR to avoid +.B ! +being interpreted by the shell. .RE .PP -\fIexpression\fR \fIexpression\fR +.I expression expression .br -\fIexpression \fB\-a \fIexpression\fR +.I expression +.B \-a +.I expression .br -\fIexpression \fB\-and \fIexpression\fR +.I expression +.B \-and +.I expression .RS Short-circuiting "and" operator: if the left-hand .I expression @@ -218,9 +229,13 @@ otherwise, returns .BR false . .RE .PP -\fIexpression \fB\-o \fIexpression\fR +.I expression +.B \-o +.I expression .br -\fIexpression \fB\-or \fIexpression\fR +.I expression +.B \-or +.I expression .RS Short-circuiting "or" operator: if the left-hand .I expression @@ -232,14 +247,14 @@ otherwise, returns .BR true . .RE .TP -\fIexpression \fB, \fIexpression\fR +.IB "expression " , " expression" The "comma" operator: evaluates the left-hand .I expression but discards the result, returning the right-hand .IR expression . .SH SPECIAL FORMS .TP -\fB\-exclude \fIexpression\fR +.BI "\-exclude " expression Exclude all paths matching the .I expression from the search. @@ -266,7 +281,6 @@ Print usage information, and exit immediately (without parsing the rest of the c Print version information, and exit immediately. .RE .SH OPTIONS -.PP .B \-color .br .B \-nocolor @@ -288,8 +302,8 @@ Search in post-order (descendents first). Follow all symbolic links (same as .BR \-L ). .TP -\fB\-files0\-from \fIFILE\fR -Treat the NUL ('\\0')-separated paths in +.BI "\-files0\-from " FILE +Treat the NUL ('\e0')-separated paths in .I FILE as starting points for the search. Pass @@ -297,9 +311,9 @@ Pass .I \- to read the paths from standard input. .PP -\fB\-ignore_readdir_race\fR +.B \-ignore_readdir_race .br -\fB\-noignore_readdir_race\fR +.B \-noignore_readdir_race .RS Whether to report an error if .B bfs @@ -307,18 +321,21 @@ detects that the file tree is modified during the search (default: .BR \-noignore_readdir_race ). .RE .PP -\fB\-maxdepth \fIN\fR +.B \-maxdepth +.I N .br -\fB\-mindepth \fIN\fR +.B \-mindepth +.I N .RS Ignore files deeper/shallower than .IR N . .RE .TP .B \-mount -Don't descend into other mount points (same as -.B \-xdev -for now, but will skip mount points entirely in the future). +Exclude mount points entirely from the results. +.TP +.B \-noerror +Ignore any errors that occur during traversal. .TP .B \-nohidden Exclude hidden files and directories. @@ -326,7 +343,7 @@ Exclude hidden files and directories. .B \-noleaf Ignored; for compatibility with GNU find. .TP -\fB\-regextype \fITYPE\fR +.BI "\-regextype " TYPE Use .IR TYPE -flavored regular expressions. @@ -337,7 +354,7 @@ The possible types are POSIX basic regular expressions (the default). .TP .I posix-extended -POSIX extended resular expressions. +POSIX extended regular expressions. .TP .I ed Like @@ -381,6 +398,9 @@ Turn on or off warnings about the command line. .TP .B \-xdev Don't descend into other mount points. +Unlike +.BR \-mount , +the mount point itself is still included. .SH TESTS .TP .B \-acl @@ -401,13 +421,17 @@ Find files minutes ago. .RE .PP -\fB\-anewer \fIFILE\fR +.B \-anewer +.I FILE .br -\fB\-Bnewer \fIFILE\fR +.B \-Bnewer +.I FILE .br -\fB\-cnewer \fIFILE\fR +.B \-cnewer +.I FILE .br -\fB\-mnewer \fIFILE\fR +.B \-mnewer +.I FILE .RS Find files .BR a ccessed/ B irthed/ c hanged/ m odified @@ -416,13 +440,17 @@ more recently than was modified. .RE .PP -\fB\-asince \fITIME\fR +.B \-asince +.I TIME .br -\fB\-Bsince \fITIME\fR +.B \-Bsince +.I TIME .br -\fB\-csince \fITIME\fR +.B \-csince +.I TIME .br -\fB\-msince \fITIME\fR +.B \-msince +.I TIME .RS Find files .BR a ccessed/ B irthed/ c hanged/ m odified @@ -452,6 +480,10 @@ Find files with POSIX.1e .BR capabilities (7) set. .TP +.BI "\-context " GLOB +Find files whose SELinux context matches the +.IR GLOB . +.TP \fB\-depth\fR [\fI\-+\fR]\fIN\fR Find files with depth .IR N . @@ -475,7 +507,11 @@ Find files the current user can execute/read/write. Always false/true. .RE .TP -\fB\-fstype \fITYPE\fR +\fB\-flags\fR [\fI\-+\fR]\fIFLAGS\fR +Find files with matching inode +.BR FLAGS . +.TP +.BI "\-fstype " TYPE Find files on file systems with the given .IR TYPE . .PP @@ -487,9 +523,11 @@ Find files owned by group/user ID .IR N . .RE .PP -\fB\-group \fINAME\fR +.B \-group +.I NAME .br -\fB\-user \fINAME\fR +.B \-user +.I NAME .RS Find files owned by the group/user .IR NAME . @@ -499,15 +537,20 @@ Find files owned by the group/user Find hidden files (those beginning with .IR . ). .PP -\fB\-ilname \fIGLOB\fR +.B \-ilname +.I GLOB .br -\fB\-iname \fIGLOB\fR +.B \-iname +.I GLOB .br -\fB\-ipath \fIGLOB\fR +.B \-ipath +.I GLOB .br -\fB\-iregex \fIREGEX\fR +.B \-iregex +.I REGEX .br -\fB\-iwholename \fIGLOB\fR +.B \-iwholename +.I GLOB .RS Case-insensitive versions of .BR \-lname / \-name / \-path / \-regex / \-wholename . @@ -522,19 +565,19 @@ Find files with .I N hard links. .TP -\fB\-lname \fIGLOB\fR +.BI "\-lname " GLOB Find symbolic links whose target matches the .IR GLOB . .TP -\fB\-name \fIGLOB\fR +.BI "\-name " GLOB Find files whose name matches the .IR GLOB . .TP -\fB\-newer \fIFILE\fR +.BI "\-newer " FILE Find files newer than .IR FILE . .TP -\fB\-newer\fIXY \fIREFERENCE\fR +.BI \-newer "XY REFERENCE" Find files whose .I X time is newer than the @@ -570,26 +613,28 @@ as an ISO 8601-style timestamp. For example: Find files owned by nonexistent groups/users. .RE .PP -\fB\-path \fIGLOB\fR +.B \-path +.I GLOB .br -\fB\-wholename \fIGLOB\fR +.B \-wholename +.I GLOB .RS Find files whose entire path matches the .IR GLOB . .RE .TP -\fB\-perm\fR [\fI\-\fR]\fIMODE\fR +\fB\-perm\fR [\fI\-+/\fR]\fIMODE\fR Find files with a matching mode. .TP -\fB\-regex \fIREGEX\fR +.BI "\-regex " REGEX Find files whose entire path matches the regular expression .IR REGEX . .TP -\fB\-samefile \fIFILE\fR +.BI "\-samefile " FILE Find hard links to .IR FILE . .TP -\fB\-since \fITIME\fR +.BI "\-since " TIME Find files modified since the ISO 8601-style timestamp .IR TIME . See @@ -668,7 +713,7 @@ days after they were changed. Find files with extended attributes .RB ( xattr (7)). .TP -\fB\-xattrname\fR \fINAME\fR +.BI "\-xattrname " NAME Find files with the extended attribute .IR NAME . .TP @@ -677,28 +722,31 @@ Find files of the given type, following links when .B \-type would not, and vice versa. .SH ACTIONS -.PP .B \-delete .br .B \-rm .RS -Delete any found files (implies \fB-depth\fR). +Delete any found files (implies +.BR \-depth ). .RE .TP -\fB\-exec \fIcommand ... {} ;\fR +.BI "\-exec " "command ... {} ;" Execute a command. .TP -\fB\-exec \fIcommand ... {} +\fR +.BI "\-exec " "command ... {} +" Execute a command with multiple files at once. .TP -\fB\-ok \fIcommand ... {} ;\fR +.BI "\-ok " "command ... {} ;" Prompt the user whether to execute a command. .PP -\fB\-execdir \fIcommand ... {} ;\fR +.B \-execdir +.I command ... {} ; .br -\fB\-execdir \fIcommand ... {} +\fR +.B \-execdir +.I command ... {} + .br -\fB\-okdir \fIcommand ... {} ;\fR +.B \-okdir +.I command ... {} ; .RS Like .BR \-exec / \-ok , @@ -710,13 +758,17 @@ Exit immediately with the given status .RI ( 0 if unspecified). .PP -\fB\-fls \fIFILE\fR +.B \-fls +.I FILE .br -\fB\-fprint \fIFILE\fR +.B \-fprint +.I FILE .br -\fB\-fprint0 \fIFILE\fR +.B \-fprint0 +.I FILE .br -\fB\-fprintf \fIFILE FORMAT\fR +.B \-fprintf +.I FILE FORMAT .RS Like .BR \-ls / \-print / \-print0 / \-printf , @@ -725,7 +777,7 @@ but write to instead of standard output. .RE .TP -\fB\-limit \fIN\fR +.BI "\-limit " N Quit once this action is evaluated .I N times. @@ -741,12 +793,12 @@ Print the path to the found file. .B \-print0 Like .BR \-print , -but use the null character ('\\0') as a separator rather than newlines. +but use the null character ('\e0') as a separator rather than newlines. Useful in conjunction with .B xargs .IR \-0 . .TP -\fB\-printf \fIFORMAT\fR +.BI "\-printf " FORMAT Print according to a format string (see .BR find (1)). These additional format directives are supported: @@ -892,7 +944,7 @@ is quoted to ensure the glob is processed by .B bfs rather than the shell. .TP -\fBbfs \-name access_log \-L \fI/var\fR +.BI "bfs \-name access_log \-L " /var Finds all files named .B access_log under @@ -901,7 +953,7 @@ following symbolic links. .B bfs allows flags and paths to appear anywhere on the command line. .TP -\fBbfs \fI~ \fB\-not \-user $USER\fR +.BI "bfs " ~ " \-not \-user $USER" Prints all files in your home directory not owned by you. .TP .B bfs \-xtype l @@ -909,7 +961,7 @@ Finds broken symbolic links. .TP .B bfs \-name config \-exclude \-name .git Finds all files named -.BR config, +.BR config , skipping every .B .git directory. diff --git a/src/alloc.c b/src/alloc.c index b65d0c5..f505eda 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -2,10 +2,12 @@ // SPDX-License-Identifier: 0BSD #include "alloc.h" + +#include "bfs.h" #include "bit.h" -#include "config.h" #include "diag.h" #include "sanity.h" + #include <errno.h> #include <stdlib.h> #include <stdint.h> @@ -18,24 +20,22 @@ # define ALLOC_MAX (SIZE_MAX / 2) #endif -/** Portable aligned_alloc()/posix_memalign(). */ +/** posix_memalign() wrapper. */ static void *xmemalign(size_t align, size_t size) { bfs_assert(has_single_bit(align)); bfs_assert(align >= sizeof(void *)); - bfs_assert(is_aligned(align, size)); -#if __APPLE__ + // Since https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2072.htm, + // aligned_alloc() doesn't require the size to be a multiple of align. + // But the sanitizers don't know about that yet, so always use + // posix_memalign(). void *ptr = NULL; errno = posix_memalign(&ptr, align, size); return ptr; -#else - return aligned_alloc(align, size); -#endif } void *alloc(size_t align, size_t size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, size)); if (size > ALLOC_MAX) { errno = EOVERFLOW; @@ -51,7 +51,6 @@ void *alloc(size_t align, size_t size) { void *zalloc(size_t align, size_t size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, size)); if (size > ALLOC_MAX) { errno = EOVERFLOW; @@ -71,8 +70,6 @@ void *zalloc(size_t align, size_t size) { void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, old_size)); - bfs_assert(is_aligned(align, new_size)); if (new_size == 0) { free(ptr); @@ -106,10 +103,10 @@ void *reserve(void *ptr, size_t align, size_t size, size_t count) { size_t old_size = size * count; // Capacity is doubled every power of two, from 0→1, 1→2, 2→4, etc. - // If we stayed within the same size class, re-use ptr. + // If we stayed within the same size class, reuse ptr. if (count & (count - 1)) { // Tell sanitizers about the new array element - sanitize_alloc((char *)ptr + old_size, size); + sanitize_resize(ptr, old_size, old_size + size, bit_ceil(count) * size); errno = 0; return ptr; } @@ -124,7 +121,7 @@ void *reserve(void *ptr, size_t align, size_t size, size_t count) { } // Pretend we only allocated one more element - sanitize_free((char *)ret + old_size + size, new_size - old_size - size); + sanitize_resize(ret, new_size, old_size + size, new_size); errno = 0; return ret; } @@ -176,7 +173,7 @@ void arena_init(struct arena *arena, size_t align, size_t size) { } /** Allocate a new slab. */ -attr(cold) +_cold static int slab_alloc(struct arena *arena) { // Make the initial allocation size ~4K size_t size = 4096; @@ -231,6 +228,7 @@ void arena_free(struct arena *arena, void *ptr) { union chunk *chunk = ptr; chunk_set_next(arena, chunk, arena->chunks); arena->chunks = chunk; + sanitize_uninit(chunk, arena->size); sanitize_free(chunk, arena->size); } @@ -250,7 +248,7 @@ void arena_destroy(struct arena *arena) { sanitize_uninit(arena); } -void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size) { +void varena_init(struct varena *varena, size_t align, size_t offset, size_t size) { varena->align = align; varena->offset = offset; varena->size = size; @@ -259,7 +257,7 @@ void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, // The smallest size class is at least as many as fit in the smallest // aligned allocation size - size_t min_count = (flex_size(align, min, offset, size, 1) - offset + size - 1) / size; + size_t min_count = (flex_size(align, offset, size, 1) - offset + size - 1) / size; varena->shift = bit_width(min_count - 1); } @@ -272,7 +270,7 @@ static size_t varena_size_class(struct varena *varena, size_t count) { /** Get the exact size of a flexible struct. */ static size_t varena_exact_size(const struct varena *varena, size_t count) { - return flex_size(varena->align, 0, varena->offset, varena->size, count); + return flex_size(varena->align, varena->offset, varena->size, count); } /** Get the arena for the given array length. */ @@ -306,8 +304,7 @@ void *varena_alloc(struct varena *varena, size_t count) { } // Tell the sanitizers the exact size of the allocated struct - sanitize_free(ret, arena->size); - sanitize_alloc(ret, varena_exact_size(varena, count)); + sanitize_resize(ret, arena->size, varena_exact_size(varena, count), arena->size); return ret; } @@ -319,15 +316,14 @@ void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t return NULL; } - size_t new_exact_size = varena_exact_size(varena, new_count); - size_t old_exact_size = varena_exact_size(varena, old_count); + size_t old_size = old_arena->size; + size_t new_size = new_arena->size; if (new_arena == old_arena) { - if (new_count < old_count) { - sanitize_free((char *)ptr + new_exact_size, old_exact_size - new_exact_size); - } else if (new_count > old_count) { - sanitize_alloc((char *)ptr + old_exact_size, new_exact_size - old_exact_size); - } + sanitize_resize(ptr, + varena_exact_size(varena, old_count), + varena_exact_size(varena, new_count), + new_size); return ptr; } @@ -336,16 +332,18 @@ void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t return NULL; } - size_t old_size = old_arena->size; - sanitize_alloc((char *)ptr + old_exact_size, old_size - old_exact_size); + // Non-sanitized builds don't bother computing exact sizes, and just use + // the potentially-larger arena size for each size class instead. To + // allow the below memcpy() to work with the less-precise sizes, expand + // the old allocation to its full capacity. + sanitize_resize(ptr, varena_exact_size(varena, old_count), old_size, old_size); - size_t new_size = new_arena->size; size_t min_size = new_size < old_size ? new_size : old_size; memcpy(ret, ptr, min_size); arena_free(old_arena, ptr); - sanitize_free((char *)ret + new_exact_size, new_size - new_exact_size); + sanitize_resize(ret, new_size, varena_exact_size(varena, new_count), new_size); return ret; } diff --git a/src/alloc.h b/src/alloc.h index ae055bc..1fafbab 100644 --- a/src/alloc.h +++ b/src/alloc.h @@ -8,127 +8,145 @@ #ifndef BFS_ALLOC_H #define BFS_ALLOC_H -#include "config.h" +#include "bfs.h" + #include <errno.h> #include <stddef.h> #include <stdlib.h> +#define IS_ALIGNED(align, size) \ + (((size) & ((align) - 1)) == 0) + /** Check if a size is properly aligned. */ static inline bool is_aligned(size_t align, size_t size) { - return (size & (align - 1)) == 0; + return IS_ALIGNED(align, size); } +#define ALIGN_FLOOR(align, size) \ + ((size) & ~((align) - 1)) + /** Round down to a multiple of an alignment. */ static inline size_t align_floor(size_t align, size_t size) { - return size & ~(align - 1); + return ALIGN_FLOOR(align, size); } +#define ALIGN_CEIL(align, size) \ + ((((size) - 1) | ((align) - 1)) + 1) + /** Round up to a multiple of an alignment. */ static inline size_t align_ceil(size_t align, size_t size) { - return align_floor(align, size + align - 1); + return ALIGN_CEIL(align, size); } /** - * Saturating array size. - * - * @param align - * Array element alignment. - * @param size - * Array element size. - * @param count - * Array element count. - * @return - * size * count, saturating to the maximum aligned value on overflow. + * Saturating size addition. + */ +static inline size_t size_add(size_t lhs, size_t rhs) { + size_t ret = lhs + rhs; + return ret >= lhs ? ret : (size_t)-1; +} + +/** + * Saturating size multiplication. */ -static inline size_t array_size(size_t align, size_t size, size_t count) { +static inline size_t size_mul(size_t size, size_t count) { size_t ret = size * count; - return ret / size == count ? ret : ~(align - 1); + return ret / size == count ? ret : (size_t)-1; } /** Saturating array sizeof. */ #define sizeof_array(type, count) \ - array_size(alignof(type), sizeof(type), count) + size_mul(sizeof(type), count) /** Size of a struct/union field. */ #define sizeof_member(type, member) \ sizeof(((type *)NULL)->member) /** + * @internal + * Our flexible struct size calculations assume that structs have the minimum + * trailing padding to align the type properly. A pathological ABI that adds + * extra padding would result in us under-allocating space for those structs, + * so we static_assert() that no such padding exists. + */ +#define ASSERT_FLEX_ABI(type, member) \ + ASSERT_FLEX_ABI_( \ + ALIGN_CEIL(alignof(type), offsetof(type, member)) >= sizeof(type), \ + "Unexpected tail padding in " #type) + +/** + * @internal + * The contortions here allow static_assert() to be used in expressions, rather + * than just declarations. + */ +#define ASSERT_FLEX_ABI_(...) \ + ((void)sizeof(struct { char _; static_assert(__VA_ARGS__); })) + +/** * Saturating flexible struct size. * - * @param align + * @align * Struct alignment. - * @param min - * Minimum struct size. - * @param offset + * @offset * Flexible array member offset. - * @param size + * @size * Flexible array element size. - * @param count + * @count * Flexible array element count. * @return * The size of the struct with count flexible array elements. Saturates * to the maximum aligned value on overflow. */ -static inline size_t flex_size(size_t align, size_t min, size_t offset, size_t size, size_t count) { - size_t ret = size * count; - size_t overflow = ret / size != count; - - size_t extra = offset + align - 1; - ret += extra; - overflow |= ret < extra; - ret |= -overflow; +static inline size_t flex_size(size_t align, size_t offset, size_t size, size_t count) { + size_t ret = size_mul(size, count); + ret = size_add(ret, offset + align - 1); ret = align_floor(align, ret); - - // Make sure flex_sizeof(type, member, 0) >= sizeof(type), even if the - // type has more padding than necessary for alignment - if (min > align_ceil(align, offset)) { - ret = ret < min ? min : ret; - } - return ret; } /** * Computes the size of a flexible struct. * - * @param type + * @type * The type of the struct containing the flexible array. - * @param member + * @member * The name of the flexible array member. - * @param count + * @count * The length of the flexible array. * @return * The size of the struct with count flexible array elements. Saturates * to the maximum aligned value on overflow. */ #define sizeof_flex(type, member, count) \ - flex_size(alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0]), count) + (ASSERT_FLEX_ABI(type, member), flex_size( \ + alignof(type), offsetof(type, member), sizeof_member(type, member[0]), count)) /** * General memory allocator. * - * @param align + * @align * The required alignment. - * @param size + * @size * The size of the allocation. * @return * The allocated memory, or NULL on failure. */ -attr(malloc(free, 1), aligned_alloc(1, 2)) +_malloc(free, 1) +_aligned_alloc(1, 2) void *alloc(size_t align, size_t size); /** * Zero-initialized memory allocator. * - * @param align + * @align * The required alignment. - * @param size + * @size * The size of the allocation. * @return * The allocated memory, or NULL on failure. */ -attr(malloc(free, 1), aligned_alloc(1, 2)) +_malloc(free, 1) +_aligned_alloc(1, 2) void *zalloc(size_t align, size_t size); /** Allocate memory for the given type. */ @@ -158,18 +176,19 @@ void *zalloc(size_t align, size_t size); /** * Alignment-aware realloc(). * - * @param ptr + * @ptr * The pointer to reallocate. - * @param align + * @align * The required alignment. - * @param old_size + * @old_size * The previous allocation size. - * @param new_size + * @new_size * The new allocation size. * @return * The reallocated memory, or NULL on failure. */ -attr(nodiscard, aligned_alloc(2, 4)) +_aligned_alloc(2, 4) +_nodiscard void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); /** Reallocate memory for an array. */ @@ -183,11 +202,11 @@ void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); /** * Reserve space for one more element in a dynamic array. * - * @param ptr + * @ptr * The pointer to reallocate. - * @param align + * @align * The required alignment. - * @param count + * @count * The current size of the array. * @return * The reallocated memory, on both success *and* failure. On success, @@ -195,17 +214,17 @@ void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); * for (count + 1) elements. On failure, errno will be non-zero, and * ptr will returned unchanged. */ -attr(nodiscard) +_nodiscard void *reserve(void *ptr, size_t align, size_t size, size_t count); /** * Convenience macro to grow a dynamic array. * - * @param type + * @type * The array element type. - * @param type **ptr + * @type **ptr * A pointer to the array. - * @param size_t *count + * @size_t *count * A pointer to the array's size. * @return * On success, a pointer to the newly reserved array element, i.e. @@ -253,7 +272,7 @@ void arena_free(struct arena *arena, void *ptr); /** * Allocate an object out of the arena. */ -attr(malloc(arena_free, 2)) +_malloc(arena_free, 2) void *arena_alloc(struct arena *arena); /** @@ -287,40 +306,39 @@ struct varena { /** * Initialize a varena for a struct with the given layout. * - * @param varena + * @varena * The varena to initialize. - * @param align + * @align * alignof(type) - * @param min - * sizeof(type) - * @param offset + * @offset * offsetof(type, flexible_array) - * @param size + * @size * sizeof(flexible_array[i]) */ -void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size); +void varena_init(struct varena *varena, size_t align, size_t offset, size_t size); /** * Initialize a varena for the given type and flexible array. * - * @param varena + * @varena * The varena to initialize. - * @param type + * @type * A struct type containing a flexible array. - * @param member + * @member * The name of the flexible array member. */ #define VARENA_INIT(varena, type, member) \ - varena_init(varena, alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0])) + (ASSERT_FLEX_ABI(type, member), varena_init( \ + varena, alignof(type), offsetof(type, member), sizeof_member(type, member[0]))) /** * Free an arena-allocated flexible struct. * - * @param varena + * @varena * The that allocated the object. - * @param ptr + * @ptr * The object to free. - * @param count + * @count * The length of the flexible array. */ void varena_free(struct varena *varena, void *ptr, size_t count); @@ -328,46 +346,46 @@ void varena_free(struct varena *varena, void *ptr, size_t count); /** * Arena-allocate a flexible struct. * - * @param varena + * @varena * The varena to allocate from. - * @param count + * @count * The length of the flexible array. * @return * The allocated struct, or NULL on failure. */ -attr(malloc(varena_free, 2)) +_malloc(varena_free, 2) void *varena_alloc(struct varena *varena, size_t count); /** * Resize a flexible struct. * - * @param varena + * @varena * The varena to allocate from. - * @param ptr + * @ptr * The object to resize. - * @param old_count - * The old array lenth. - * @param new_count + * @old_count + * The old array length. + * @new_count * The new array length. * @return * The resized struct, or NULL on failure. */ -attr(nodiscard) +_nodiscard void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count); /** * Grow a flexible struct by an arbitrary amount. * - * @param varena + * @varena * The varena to allocate from. - * @param ptr + * @ptr * The object to resize. - * @param count + * @count * Pointer to the flexible array length. * @return * The resized struct, or NULL on failure. */ -attr(nodiscard) +_nodiscard void *varena_grow(struct varena *varena, void *ptr, size_t *count); /** diff --git a/src/atomic.h b/src/atomic.h index f1a6bea..5c2826f 100644 --- a/src/atomic.h +++ b/src/atomic.h @@ -8,6 +8,8 @@ #ifndef BFS_ATOMIC_H #define BFS_ATOMIC_H +#include "bfs.h" + #include <stdatomic.h> /** @@ -18,9 +20,9 @@ /** * Shorthand for atomic_load_explicit(). * - * @param obj + * @obj * A pointer to the atomic object. - * @param order + * @order * The memory ordering to use, without the memory_order_ prefix. * @return * The loaded value. @@ -82,4 +84,35 @@ #define fetch_and(obj, arg, order) \ atomic_fetch_and_explicit(obj, arg, memory_order_##order) +/** + * Shorthand for atomic_thread_fence(). + */ +#if __SANITIZE_THREAD__ +// TSan doesn't support fences: https://github.com/google/sanitizers/issues/1415 +# define thread_fence(obj, order) \ + fetch_add(obj, 0, order) +#else +# define thread_fence(obj, order) \ + atomic_thread_fence(memory_order_##order) +#endif + +/** + * Shorthand for atomic_signal_fence(). + */ +#define signal_fence(order) \ + atomic_signal_fence(memory_order_##order) + +/** + * A hint to the CPU to relax while it spins. + */ +#if __has_builtin(__builtin_ia32_pause) +# define spin_loop() __builtin_ia32_pause() +#elif __has_builtin(__builtin_arm_yield) +# define spin_loop() __builtin_arm_yield() +#elif BFS_HAS_BUILTIN_RISCV_PAUSE +# define spin_loop() __builtin_riscv_pause() +#else +# define spin_loop() ((void)0) +#endif + #endif // BFS_ATOMIC_H @@ -2,51 +2,54 @@ // SPDX-License-Identifier: 0BSD #include "bar.h" + +#include "alloc.h" #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "bit.h" -#include "config.h" #include "dstring.h" +#include "sighook.h" + #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdarg.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> -#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> struct bfs_bar { int fd; atomic unsigned int width; atomic unsigned int height; -}; -/** The global status bar instance. */ -static struct bfs_bar the_bar = { - .fd = -1, + struct sighook *exit_hook; + struct sighook *winch_hook; }; /** Get the terminal size, if possible. */ static int bfs_bar_getsize(struct bfs_bar *bar) { -#ifdef TIOCGWINSZ struct winsize ws; - if (ioctl(bar->fd, TIOCGWINSZ, &ws) != 0) { + if (xtcgetwinsize(bar->fd, &ws) != 0) { return -1; } store(&bar->width, ws.ws_col, relaxed); store(&bar->height, ws.ws_row, relaxed); return 0; -#else - errno = ENOTSUP; - return -1; -#endif } -/** Async Signal Safe puts(). */ -static int ass_puts(int fd, const char *str) { - size_t len = strlen(str); - return xwrite(fd, str, len) == len ? 0 : -1; +/** Write a string to the status bar (async-signal-safe). */ +static int bfs_bar_write(struct bfs_bar *bar, const char *str, size_t len) { + return xwrite(bar->fd, str, len) == len ? 0 : -1; +} + +/** Write a string to the status bar (async-signal-safe). */ +static int bfs_bar_puts(struct bfs_bar *bar, const char *str) { + return bfs_bar_write(bar, str, strlen(str)); } /** Number of decimal digits needed for terminal sizes. */ @@ -68,66 +71,63 @@ static char *ass_itoa(char *str, unsigned int n) { return str + len; } +/** Reset the scrollable region and hide the bar. */ +static int bfs_bar_reset(struct bfs_bar *bar) { + return bfs_bar_puts(bar, + "\0337" // DECSC: Save cursor + "\033[r" // DECSTBM: Reset scrollable region + "\0338" // DECRC: Restore cursor + "\033[J" // ED: Erase display from cursor to end + ); +} + +/** Hide the bar if the terminal is shorter than this. */ +#define BFS_BAR_MIN_HEIGHT 3 + /** Update the size of the scrollable region. */ static int bfs_bar_resize(struct bfs_bar *bar) { - char esc_seq[12 + ITOA_DIGITS] = + unsigned int height = load(&bar->height, relaxed); + if (height < BFS_BAR_MIN_HEIGHT) { + return bfs_bar_reset(bar); + } + + static const char PREFIX[] = + "\033D" // IND: Line feed, possibly scrolling + "\033[1A" // CUU: Move cursor up 1 row "\0337" // DECSC: Save cursor "\033[;"; // DECSTBM: Set scrollable region + static const char SUFFIX[] = + "r" // (end of DECSTBM) + "\0338" // DECRC: Restore the cursor + "\033[J"; // ED: Erase display from cursor to end - // DECSTBM takes the height as the second argument - unsigned int height = load(&bar->height, relaxed); - char *ptr = esc_seq + strlen(esc_seq); - ptr = ass_itoa(ptr, height - 1); + char esc_seq[sizeof(PREFIX) + ITOA_DIGITS + sizeof(SUFFIX)]; - strcpy(ptr, - "r" // DECSTBM - "\0338" // DECRC: Restore the cursor - "\033[J" // ED: Erase display from cursor to end - ); + // DECSTBM takes the height as the second argument + char *cur = stpcpy(esc_seq, PREFIX); + cur = ass_itoa(cur, height - 1); + cur = stpcpy(cur, SUFFIX); - return ass_puts(bar->fd, esc_seq); + return bfs_bar_write(bar, esc_seq, cur - esc_seq); } #ifdef SIGWINCH /** SIGWINCH handler. */ -static void sighand_winch(int sig) { - int error = errno; - - bfs_bar_getsize(&the_bar); - bfs_bar_resize(&the_bar); - - errno = error; +static void bfs_bar_sigwinch(int sig, siginfo_t *info, void *arg) { + struct bfs_bar *bar = arg; + bfs_bar_getsize(bar); + bfs_bar_resize(bar); } #endif -/** Reset the scrollable region and hide the bar. */ -static int bfs_bar_reset(struct bfs_bar *bar) { - return ass_puts(bar->fd, - "\0337" // DECSC: Save cursor - "\033[r" // DECSTBM: Reset scrollable region - "\0338" // DECRC: Restore cursor - "\033[J" // ED: Erase display from cursor to end - ); -} - /** Signal handler for process-terminating signals. */ -static void sighand_reset(int sig) { - bfs_bar_reset(&the_bar); - raise(sig); -} - -/** Register sighand_reset() for a signal. */ -static void reset_before_death_by(int sig) { - struct sigaction sa = { - .sa_handler = sighand_reset, - .sa_flags = SA_RESETHAND, - }; - sigemptyset(&sa.sa_mask); - sigaction(sig, &sa, NULL); +static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) { + struct bfs_bar *bar = arg; + bfs_bar_reset(bar); } /** printf() to the status bar with a single write(). */ -attr(printf(2, 3)) +_printf(2, 3) static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) { va_list args; va_start(args, format); @@ -138,64 +138,47 @@ static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) { return -1; } - int ret = ass_puts(bar->fd, str); + int ret = bfs_bar_write(bar, str, dstrlen(str)); dstrfree(str); return ret; } struct bfs_bar *bfs_bar_show(void) { - if (the_bar.fd >= 0) { - errno = EBUSY; - goto fail; + struct bfs_bar *bar = ALLOC(struct bfs_bar); + if (!bar) { + return NULL; } - char term[L_ctermid]; - ctermid(term); - if (strlen(term) == 0) { - errno = ENOTTY; + bar->fd = open_cterm(O_RDWR | O_CLOEXEC); + if (bar->fd < 0) { goto fail; } - the_bar.fd = open(term, O_RDWR | O_CLOEXEC); - if (the_bar.fd < 0) { - goto fail; + if (bfs_bar_getsize(bar) != 0) { + goto fail_close; } - if (bfs_bar_getsize(&the_bar) != 0) { + bar->exit_hook = atsigexit(bfs_bar_sigexit, bar); + if (!bar->exit_hook) { goto fail_close; } - reset_before_death_by(SIGABRT); - reset_before_death_by(SIGINT); - reset_before_death_by(SIGPIPE); - reset_before_death_by(SIGQUIT); - reset_before_death_by(SIGTERM); - #ifdef SIGWINCH - struct sigaction sa = { - .sa_handler = sighand_winch, - .sa_flags = SA_RESTART, - }; - sigemptyset(&sa.sa_mask); - sigaction(SIGWINCH, &sa, NULL); + bar->winch_hook = sighook(SIGWINCH, bfs_bar_sigwinch, bar, 0); + if (!bar->winch_hook) { + goto fail_hook; + } #endif - unsigned int height = load(&the_bar.height, relaxed); - bfs_bar_printf(&the_bar, - "\n" // Make space for the bar - "\0337" // DECSC: Save cursor - "\033[;%ur" // DECSTBM: Set scrollable region - "\0338" // DECRC: Restore cursor - "\033[1A", // CUU: Move cursor up 1 row - height - 1 - ); - - return &the_bar; + bfs_bar_resize(bar); + return bar; +fail_hook: + sigunhook(bar->exit_hook); fail_close: - close_quietly(the_bar.fd); - the_bar.fd = -1; + close_quietly(bar->fd); fail: + free(bar); return NULL; } @@ -205,6 +188,10 @@ unsigned int bfs_bar_width(const struct bfs_bar *bar) { int bfs_bar_update(struct bfs_bar *bar, const char *str) { unsigned int height = load(&bar->height, relaxed); + if (height < BFS_BAR_MIN_HEIGHT) { + return 0; + } + return bfs_bar_printf(bar, "\0337" // DECSC: Save cursor "\033[%u;0f" // HVP: Move cursor to row, column @@ -223,17 +210,11 @@ void bfs_bar_hide(struct bfs_bar *bar) { return; } - signal(SIGABRT, SIG_DFL); - signal(SIGINT, SIG_DFL); - signal(SIGPIPE, SIG_DFL); - signal(SIGQUIT, SIG_DFL); - signal(SIGTERM, SIG_DFL); -#ifdef SIGWINCH - signal(SIGWINCH, SIG_DFL); -#endif + sigunhook(bar->winch_hook); + sigunhook(bar->exit_hook); bfs_bar_reset(bar); xclose(bar->fd); - bar->fd = -1; + free(bar); } @@ -27,9 +27,9 @@ unsigned int bfs_bar_width(const struct bfs_bar *bar); /** * Update the status bar message. * - * @param bar + * @bar * The status bar to update. - * @param str + * @str * The string to display. * @return * 0 on success, -1 on failure. diff --git a/src/bfs.h b/src/bfs.h new file mode 100644 index 0000000..3cee727 --- /dev/null +++ b/src/bfs.h @@ -0,0 +1,241 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Configuration and fundamental utilities. + */ + +#ifndef BFS_H +#define BFS_H + +// Standard versions + +/** Possible __STDC_VERSION__ values. */ +#define C95 199409L +#define C99 199901L +#define C11 201112L +#define C17 201710L +#define C23 202311L + +/** Possible _POSIX_C_SOURCE and _POSIX_<OPTION> values. */ +#define POSIX_1990 1 +#define POSIX_1992 2 +#define POSIX_1993 199309L +#define POSIX_1995 199506L +#define POSIX_2001 200112L +#define POSIX_2008 200809L +#define POSIX_2024 202405L + +// Build configuration + +#include "config.h" + +#ifndef BFS_COMMAND +# define BFS_COMMAND "bfs" +#endif + +#ifndef BFS_HOMEPAGE +# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html" +#endif + +#ifndef BFS_LINT +# define BFS_LINT false +#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[]; + +extern const char bfs_confflags[]; +extern const char bfs_cc[]; +extern const char bfs_cppflags[]; +extern const char bfs_cflags[]; +extern const char bfs_ldflags[]; +extern const char bfs_ldlibs[]; + +// Get __GLIBC__ +#include <assert.h> + +// Fundamental utilities + +/** + * Get the length of an array. + */ +#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__])) + +/** + * False sharing/destructive interference/largest cache line size. + */ +#ifdef __GCC_DESTRUCTIVE_SIZE +# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE +#else +# define FALSE_SHARING_SIZE 64 +#endif + +/** + * True sharing/constructive interference/smallest cache line size. + */ +#ifdef __GCC_CONSTRUCTIVE_SIZE +# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE +#else +# define TRUE_SHARING_SIZE 64 +#endif + +/** + * Alignment specifier that avoids false sharing. + */ +#define cache_align alignas(FALSE_SHARING_SIZE) + +// Wrappers for attributes + +/** + * Silence warnings about switch/case fall-throughs. + */ +#if __has_attribute(fallthrough) +# define _fallthrough __attribute__((fallthrough)) +#else +# define _fallthrough ((void)0) +#endif + +/** + * Silence warnings about unused declarations. + */ +#if __has_attribute(unused) +# define _maybe_unused __attribute__((unused)) +#else +# define _maybe_unused +#endif + +/** + * Warn if a value is unused. + */ +#if __has_attribute(warn_unused_result) +# define _nodiscard __attribute__((warn_unused_result)) +#else +# define _nodiscard +#endif + +/** + * Hint to avoid inlining a function. + */ +#if __has_attribute(noinline) +# define _noinline __attribute__((noinline)) +#else +# define _noinline +#endif + +/** + * Marks a non-returning function. + */ +#if __STDC_VERSION__ >= C23 +# define _noreturn [[noreturn]] +#else +# define _noreturn _Noreturn +#endif + +/** + * Hint that a function is unlikely to be called. + */ +#if __has_attribute(cold) +# define _cold _noinline __attribute__((cold)) +#else +# define _cold _noinline +#endif + +/** + * Adds compiler warnings for bad printf()-style function calls, if supported. + */ +#if __has_attribute(format) +# define _printf(fmt, args) __attribute__((format(printf, fmt, args))) +#else +# define _printf(fmt, args) +#endif + +/** + * Annotates functions that potentially modify and return format strings. + */ +#if __has_attribute(format_arg) +# define _format_arg(arg) __attribute__((format_arg(arg))) +#else +# define _format_arg(arg) +#endif + +/** + * Annotates allocator-like functions. + */ +#if __has_attribute(malloc) +# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC +# define _malloc(...) _nodiscard __attribute__((malloc(__VA_ARGS__))) +# else +# define _malloc(...) _nodiscard __attribute__((malloc)) +# endif +#else +# define _malloc(...) _nodiscard +#endif + +/** + * Specifies that a function returns allocations with a given alignment. + */ +#if __has_attribute(alloc_align) +# define _alloc_align(param) __attribute__((alloc_align(param))) +#else +# define _alloc_align(param) +#endif + +/** + * Specifies that a function returns allocations with a given size. + */ +#if __has_attribute(alloc_size) +# define _alloc_size(...) __attribute__((alloc_size(__VA_ARGS__))) +#else +# define _alloc_size(...) +#endif + +/** + * Shorthand for _alloc_align() and _alloc_size(). + */ +#define _aligned_alloc(align, ...) _alloc_align(align) _alloc_size(__VA_ARGS__) + +/** + * Check if function multiversioning via GNU indirect functions (ifunc) is supported. + * + * Disabled on TSan due to https://github.com/google/sanitizers/issues/342. + */ +#ifndef BFS_USE_TARGET_CLONES +# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) && !__SANITIZE_THREAD__ +# define BFS_USE_TARGET_CLONES true +# else +# define BFS_USE_TARGET_CLONES false +# endif +#endif + +/** + * Apply the target_clones attribute, if available. + */ +#if BFS_USE_TARGET_CLONES +# define _target_clones(...) __attribute__((target_clones(__VA_ARGS__))) +#else +# define _target_clones(...) +#endif + +/** + * Mark the size of a flexible array member. + */ +#if __has_attribute(counted_by) +# define _counted_by(...) __attribute__((counted_by(__VA_ARGS__))) +#else +# define _counted_by(...) +#endif + +/** + * Optimization hint to not unroll a loop. + */ +#if BFS_HAS_PRAGMA_NOUNROLL +# define _nounroll _Pragma("nounroll") +#elif __GNUC__ && !__clang__ +# define _nounroll _Pragma("GCC unroll 0") +#else +# define _nounroll +#endif + +#endif // BFS_H diff --git a/src/bfstd.c b/src/bfstd.c index 2499f00..b78af7a 100644 --- a/src/bfstd.c +++ b/src/bfstd.c @@ -2,12 +2,14 @@ // SPDX-License-Identifier: 0BSD #include "bfstd.h" + +#include "bfs.h" #include "bit.h" -#include "config.h" #include "diag.h" #include "sanity.h" #include "thread.h" #include "xregex.h" + #include <errno.h> #include <fcntl.h> #include <langinfo.h> @@ -15,25 +17,28 @@ #include <locale.h> #include <nl_types.h> #include <pthread.h> +#include <sched.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/ioctl.h> #include <sys/resource.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> +#include <termios.h> #include <unistd.h> #include <wchar.h> -#if BFS_USE_SYS_SYSMACROS_H +#if __has_include(<sys/sysmacros.h>) # include <sys/sysmacros.h> -#elif BFS_USE_SYS_MKDEV_H +#elif __has_include(<sys/mkdev.h>) # include <sys/mkdev.h> #endif -#if BFS_USE_UTIL_H +#if __has_include(<util.h>) # include <util.h> #endif @@ -186,10 +191,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) { @@ -199,6 +204,171 @@ const char *xgetprogname(void) { return cmd; } +/** Common prologue for xstrto*() wrappers. */ +static int xstrtox_prologue(const char *str) { + // strto*() skips leading spaces, but we want to reject them + if (xisspace(str[0])) { + errno = EINVAL; + return -1; + } + + errno = 0; + return 0; +} + +/** Common epilogue for xstrto*() wrappers. */ +static int xstrtox_epilogue(const char *str, char **end, char *endp) { + if (errno != 0) { + return -1; + } + + if (end) { + *end = endp; + } + + // If end is NULL, make sure the entire string is valid + if (endp == str || (!end && *endp != '\0')) { + errno = EINVAL; + return -1; + } + + return 0; +} + +int xstrtos(const char *str, char **end, int base, short *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < SHRT_MIN || n > SHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoi(const char *str, char **end, int base, int *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < INT_MIN || n > INT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtol(const char *str, char **end, int base, long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtol(str, &endp, base); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtoll(const char *str, char **end, int base, long long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoll(str, &endp, base); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtof(const char *str, char **end, float *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtof(str, &endp); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtod(const char *str, char **end, double *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtod(str, &endp); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtous(const char *str, char **end, int base, unsigned short *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > USHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoui(const char *str, char **end, int base, unsigned int *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > UINT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +/** Common epilogue for xstrtou*() wrappers. */ +static int xstrtoux_epilogue(const char *str, char **end, char *endp) { + if (xstrtox_epilogue(str, end, endp) != 0) { + return -1; + } + + if (str[0] == '-') { + errno = ERANGE; + return -1; + } + + return 0; +} + +int xstrtoul(const char *str, char **end, int base, unsigned long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoul(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + +int xstrtoull(const char *str, char **end, int base, unsigned long long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoull(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + /** Compile and execute a regular expression for xrpmatch(). */ static int xrpregex(nl_item item, const char *response) { const char *pattern = nl_langinfo(item); @@ -252,40 +422,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 +453,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 +492,44 @@ const char *xstrerror(int errnum) { return ret; } +const char *errstr(void) { + return xstrerror(errno); +} + +/** 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, "----------"); @@ -439,7 +611,9 @@ int rlim_cmp(rlim_t a, rlim_t b) { } dev_t xmakedev(int ma, int mi) { -#ifdef makedev +#if __QNX__ + return makedev(0, ma, mi); +#elif defined(makedev) return makedev(ma, mi); #else return (ma << 8) | mi; @@ -470,6 +644,32 @@ pid_t xwaitpid(pid_t pid, int *status, int flags) { return ret; } +int open_cterm(int flags) { + char path[L_ctermid]; + if (ctermid(path) == NULL || strlen(path) == 0) { + errno = ENOTTY; + return -1; + } + + return open(path, flags); +} + +int xtcgetwinsize(int fd, struct winsize *ws) { +#if BFS_HAS_TCGETWINSIZE + return tcgetwinsize(fd, ws); +#else + return ioctl(fd, TIOCGWINSZ, ws); +#endif +} + +int xtcsetwinsize(int fd, const struct winsize *ws) { +#if BFS_HAS_TCSETWINSIZE + return tcsetwinsize(fd, ws); +#else + return ioctl(fd, TIOCSWINSZ, ws); +#endif +} + int dup_cloexec(int fd) { #ifdef F_DUPFD_CLOEXEC return fcntl(fd, F_DUPFD_CLOEXEC, 0); @@ -489,7 +689,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 +781,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 +798,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 +839,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 +857,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,47 +867,124 @@ 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 } -size_t asciilen(const char *str) { - return asciinlen(str, strlen(str)); -} +long xsysconf(int name) { +#if __FreeBSD__ && __SANITIZE_MEMORY__ + // Work around https://github.com/llvm/llvm-project/issues/88163 + __msan_scoped_disable_interceptor_checks(); +#endif -size_t asciinlen(const char *str, size_t n) { - size_t i = 0; + long ret = sysconf(name); -#if SIZE_WIDTH % 8 == 0 - // Word-at-a-time isascii() - for (size_t word; i + sizeof(word) <= n; i += sizeof(word)) { - memcpy(&word, str + i, sizeof(word)); +#if __FreeBSD__ && __SANITIZE_MEMORY__ + __msan_scoped_enable_interceptor_checks(); +#endif - const size_t mask = (SIZE_MAX / 0xFF) << 7; // 0x808080... - word &= mask; - if (!word) { - continue; + return ret; +} + +#if BFS_HAS_SCHED_GETAFFINITY +/** Get the CPU count in an affinity mask of the given size. */ +static long bfs_sched_getaffinity(size_t size) { + cpu_set_t set, *pset = &set; + + if (size > sizeof(set)) { + pset = malloc(size); + if (!pset) { + return -1; } + } + + long ret = -1; + if (sched_getaffinity(0, size, pset) == 0) { +# ifdef CPU_COUNT_S + ret = CPU_COUNT_S(size, pset); +# else + bfs_assert(size <= sizeof(set)); + ret = CPU_COUNT(pset); +# endif + } + + if (pset != &set) { + free(pset); + } + return ret; +} +#endif + +long nproc(void) { + long ret = 0; -#if ENDIAN_NATIVE == ENDIAN_BIG - word = bswap(word); -#elif ENDIAN_NATIVE != ENDIAN_LITTLE +#if BFS_HAS_SCHED_GETAFFINITY + size_t size = sizeof(cpu_set_t); + do { + ret = bfs_sched_getaffinity(size); + +# ifdef CPU_COUNT_S + // On Linux, sched_getaffinity(2) says: + // + // When working on systems with large kernel CPU affinity masks, one must + // dynamically allocate the mask argument (see CPU_ALLOC(3)). Currently, + // the only way to do this is by probing for the size of the required mask + // using sched_getaffinity() calls with increasing mask sizes (until the + // call does not fail with the error EINVAL). + size *= 2; +# else + // No support for dynamically-sized CPU masks break; +# endif + } while (ret < 0 && errno == EINVAL); #endif - size_t first = trailing_zeros(word) / 8; - return i + first; + if (ret < 1) { + ret = xsysconf(_SC_NPROCESSORS_ONLN); } -#endif - for (; i < n; ++i) { - if (!xisascii(str[i])) { - break; - } + if (ret < 1) { + ret = 1; } + return ret; +} + +size_t asciilen(const char *str) { + return asciinlen(str, strlen(str)); +} + +size_t asciinlen(const char *str, size_t n) { + const unsigned char *ustr = (const unsigned char *)str; + size_t i = 0; + + // Word-at-a-time isascii() +#define CHUNK(n) CHUNK_(uint##n##_t, load8_leu##n) +#define CHUNK_(type, load8) \ + (n - i >= sizeof(type)) { \ + type word = load8(ustr + i); \ + type mask = (((type)-1) / 0xFF) << 7; /* 0x808080.. */ \ + word &= mask; \ + i += trailing_zeros(word) / 8; \ + if (word) { \ + return i; \ + } \ + } + +#if SIZE_WIDTH >= 64 + while CHUNK(64); + if CHUNK(32); +#else + while CHUNK(32); +#endif + if CHUNK(16); + if CHUNK(8); + +#undef CHUNK_ +#undef CHUNK + return i; } @@ -907,14 +1186,14 @@ static char *dollar_quote(char *dest, char *end, const char *str, size_t len, en /** How much of this string is safe as a bare word? */ static size_t bare_len(const char *str, size_t len) { - // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02 + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02 size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}"); return ret < len ? ret : len; } /** How much of this string is safe to double-quote? */ static size_t quotable_len(const char *str, size_t len) { - // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03 + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_03 size_t ret = strcspn(str, "`$\\\"!"); return ret < len ? ret : len; } diff --git a/src/bfstd.h b/src/bfstd.h index fc22971..15dd949 100644 --- a/src/bfstd.h +++ b/src/bfstd.h @@ -8,8 +8,8 @@ #ifndef BFS_BFSTD_H #define BFS_BFSTD_H -#include "config.h" -#include "sanity.h" +#include "bfs.h" + #include <stddef.h> #include <ctype.h> @@ -18,7 +18,7 @@ * Work around https://github.com/llvm/llvm-project/issues/65532 by forcing a * function, not a macro, to be called. */ -#if __FreeBSD__ && SANITIZE_MEMORY +#if __FreeBSD__ && __SANITIZE_MEMORY__ # define BFS_INTERCEPT(fn) (fn) #else # define BFS_INTERCEPT(fn) fn @@ -48,9 +48,9 @@ * Check if an error code is "like" another one. For example, ENOTDIR is * like ENOENT because they can both be triggered by non-existent paths. * - * @param error + * @error * The error code to check. - * @param category + * @category * The category to test for. Known categories include ENOENT and * ENAMETOOLONG. * @return @@ -66,7 +66,7 @@ bool errno_is_like(int category); /** * Apply the "negative errno" convention. * - * @param ret + * @ret * The return value of the attempted operation. * @return * ret, if non-negative, otherwise -errno. @@ -106,7 +106,7 @@ int try(int ret); /** * Re-entrant dirname() variant that always allocates a copy. * - * @param path + * @path * The path in question. * @return * The parent directory of the path. @@ -116,7 +116,7 @@ char *xdirname(const char *path); /** * Re-entrant basename() variant that always allocates a copy. * - * @param path + * @path * The path in question. * @return * The final component of the path. @@ -126,7 +126,7 @@ char *xbasename(const char *path); /** * Find the offset of the final component of a path. * - * @param path + * @path * The path in question. * @return * The offset of the basename. @@ -138,9 +138,9 @@ size_t xbaseoff(const char *path); /** * fopen() variant that takes open() style flags. * - * @param path + * @path * The path to open. - * @param flags + * @flags * Flags to pass to open(). */ FILE *xfopen(const char *path, int flags); @@ -148,9 +148,9 @@ FILE *xfopen(const char *path, int flags); /** * Convenience wrapper for getdelim(). * - * @param file + * @file * The file to read. - * @param delim + * @delim * The delimiter character to split on. * @return * The read chunk (without the delimiter), allocated with malloc(). @@ -169,6 +169,56 @@ char *xgetdelim(FILE *file, char delim); const char *xgetprogname(void); /** + * Like xstrtol(), but for short. + */ +int xstrtos(const char *str, char **end, int base, short *value); + +/** + * Like xstrtol(), but for int. + */ +int xstrtoi(const char *str, char **end, int base, int *value); + +/** + * Wrapper for strtol() that forbids leading spaces. + */ +int xstrtol(const char *str, char **end, int base, long *value); + +/** + * Wrapper for strtoll() that forbids leading spaces. + */ +int xstrtoll(const char *str, char **end, int base, long long *value); + +/** + * Like xstrtoul(), but for unsigned short. + */ +int xstrtous(const char *str, char **end, int base, unsigned short *value); + +/** + * Like xstrtoul(), but for unsigned int. + */ +int xstrtoui(const char *str, char **end, int base, unsigned int *value); + +/** + * Wrapper for strtoul() that forbids leading spaces, negatives. + */ +int xstrtoul(const char *str, char **end, int base, unsigned long *value); + +/** + * Wrapper for strtoull() that forbids leading spaces, negatives. + */ +int xstrtoull(const char *str, char **end, int base, unsigned long long *value); + +/** + * Wrapper for strtof() that forbids leading spaces. + */ +int xstrtof(const char *str, char **end, float *value); + +/** + * Wrapper for strtod() that forbids leading spaces. + */ +int xstrtod(const char *str, char **end, double *value); + +/** * Process a yes/no prompt. * * @return 1 for yes, 0 for no, and -1 for unknown. @@ -185,9 +235,9 @@ size_t asciilen(const char *str); /** * Get the length of the pure-ASCII prefix of a string. * - * @param str + * @str * The string to check. - * @param n + * @n * The maximum prefix length. */ size_t asciinlen(const char *str, size_t n); @@ -195,9 +245,9 @@ size_t asciinlen(const char *str, size_t n); /** * Allocate a copy of a region of memory. * - * @param src + * @src * The memory region to copy. - * @param size + * @size * The size of the memory region. * @return * A copy of the region, allocated with malloc(), or NULL on failure. @@ -207,12 +257,12 @@ void *xmemdup(const void *src, size_t size); /** * A nice string copying function. * - * @param dest + * @dest * The NUL terminator of the destination string, or `end` if it is * already truncated. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to copy from. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -222,14 +272,14 @@ char *xstpecpy(char *dest, char *end, const char *src); /** * A nice string copying function. * - * @param dest + * @dest * The NUL terminator of the destination string, or `end` if it is * already truncated. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to copy from. - * @param n + * @n * The maximum number of characters to copy. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -239,7 +289,7 @@ char *xstpencpy(char *dest, char *end, const char *src, size_t n); /** * Thread-safe strerror(). * - * @param errnum + * @errnum * An error number. * @return * A string describing that error, which remains valid until the next @@ -248,11 +298,16 @@ char *xstpencpy(char *dest, char *end, const char *src, size_t n); const char *xstrerror(int errnum); /** + * Shorthand for xstrerror(errno). + */ +const char *errstr(void); + +/** * Format a mode like ls -l (e.g. -rw-r--r--). * - * @param mode + * @mode * The mode to format. - * @param str + * @str * The string to hold the formatted mode. */ void xstrmode(mode_t mode, char str[11]); @@ -283,11 +338,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> @@ -297,12 +362,35 @@ int xminor(dev_t dev); */ pid_t xwaitpid(pid_t pid, int *status, int flags); +#include <sys/ioctl.h> // May be necessary for struct winsize +#include <termios.h> + +/** + * Open the controlling terminal. + * + * @flags + * The open() flags. + * @return + * An open file descriptor, or -1 on failure. + */ +int open_cterm(int flags); + +/** + * tcgetwinsize()/ioctl(TIOCGWINSZ) wrapper. + */ +int xtcgetwinsize(int fd, struct winsize *ws); + +/** + * tcsetwinsize()/ioctl(TIOCSWINSZ) wrapper. + */ +int xtcsetwinsize(int fd, const struct winsize *ws); + // #include <unistd.h> /** * Like dup(), but set the FD_CLOEXEC flag. * - * @param fd + * @fd * The file descriptor to duplicate. * @return * A duplicated file descriptor, or -1 on failure. @@ -312,7 +400,7 @@ int dup_cloexec(int fd); /** * Like pipe(), but set the FD_CLOEXEC flag. * - * @param pipefd + * @pipefd * The array to hold the two file descriptors. * @return * 0 on success, -1 on failure. @@ -334,14 +422,14 @@ size_t xread(int fd, void *buf, size_t nbytes); * writes. * * @return - The number of bytes written. A value != nbytes indicates an error. + * The number of bytes written. A value != nbytes indicates an error. */ size_t xwrite(int fd, const void *buf, size_t nbytes); /** * close() variant that preserves errno. * - * @param fd + * @fd * The file descriptor to close. */ void close_quietly(int fd); @@ -349,7 +437,7 @@ void close_quietly(int fd); /** * close() wrapper that asserts the file descriptor is valid. * - * @param fd + * @fd * The file descriptor to close. * @return * 0 on success, or -1 on error. @@ -364,11 +452,11 @@ int xfaccessat(int fd, const char *path, int amode); /** * readlinkat() wrapper that dynamically allocates the result. * - * @param fd + * @fd * The base directory descriptor. - * @param path + * @path * The path to the link, relative to fd. - * @param size + * @size * An estimate for the size of the link name (pass 0 if unknown). * @return * The target of the link, allocated with malloc(), or NULL on failure. @@ -378,7 +466,7 @@ char *xreadlinkat(int fd, const char *path, size_t size); /** * Wrapper for confstr() that allocates with malloc(). * - * @param name + * @name * The ID of the confstr to look up. * @return * The value of the confstr, or NULL on failure. @@ -388,30 +476,53 @@ char *xconfstr(int name); /** * Portability wrapper for strtofflags(). * - * @param str + * @str * The string to parse. The pointee will be advanced to the first * invalid position on error. - * @param set + * @set * The flags that are set in the string. - * @param clear + * @clear * The flags that are cleared in the string. * @return * 0 on success, -1 on failure. */ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear); +/** + * Wrapper for sysconf() that works around an MSan bug. + */ +long xsysconf(int name); + +/** + * Check for a POSIX option[1] at runtime. + * + * [1]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap02.html#tag_02_01_06 + * + * @name + * The symbolic name of the POSIX option (e.g. SPAWN). + * @return + * The value of the option, either -1 or a date like 202405. + */ +#define sysoption(name) \ + (_POSIX_##name == 0 ? xsysconf(_SC_##name) : _POSIX_##name) + +/** + * Get the number of CPU threads available to the current process. + */ +long nproc(void); + #include <wchar.h> /** * Error-recovering mbrtowc() wrapper. * - * @param str + * @str * The string to convert. - * @param i + * @i * The current index. - * @param len + * @len * The length of the string. - * @param mb + * @mb * The multi-byte decoding state. * @return * The wide character at index *i, or WEOF if decoding fails. In either @@ -422,7 +533,7 @@ wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb); /** * wcswidth() variant that works on narrow strings. * - * @param str + * @str * The string to measure. * @return * The likely width of that string in a terminal. @@ -474,13 +585,13 @@ enum wesc_flags { /** * Escape a string as a single shell word. * - * @param dest + * @dest * The destination string to fill. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to escape. - * @param flags + * @flags * Controls which characters to escape. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -490,15 +601,15 @@ char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags); /** * Escape a string as a single shell word. * - * @param dest + * @dest * The destination string to fill. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to escape. - * @param n + * @n * The maximum length of the string. - * @param flags + * @flags * Controls which characters to escape. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -19,9 +19,10 @@ */ #include "bftw.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "diag.h" #include "dir.h" #include "dstring.h" @@ -30,6 +31,7 @@ #include "mtab.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdlib.h> @@ -251,7 +253,7 @@ struct bftw_file { /** The length of the file's name. */ size_t namelen; /** The file's name. */ - char name[]; + char name[]; // _counted_by(namelen + 1) }; /** @@ -446,7 +448,7 @@ static void bftw_queue_rebalance(struct bftw_queue *queue, bool async) { } } -/** Detatch the next waiting file. */ +/** Detach the next waiting file. */ static void bftw_queue_detach(struct bftw_queue *queue, struct bftw_file *file, bool async) { bfs_assert(!file->ioqueued); @@ -913,7 +915,7 @@ static int bftw_state_init(struct bftw_state *state, const struct bftw_args *arg size_t qdepth = 4096; size_t nthreads = args->nthreads; -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING // io_uring uses one fd per ring, ioq uses one ring per thread if (nthreads >= nopenfd - 1) { nthreads = nopenfd - 2; @@ -1006,6 +1008,7 @@ static int bftw_ioq_pop(struct bftw_state *state, bool block) { return -1; } + ioq_submit(ioq); struct ioq_ent *ent = ioq_pop(ioq, block); if (!ent) { return -1; @@ -1049,6 +1052,10 @@ static int bftw_ioq_pop(struct bftw_state *state, bool block) { bftw_queue_attach(&state->fileq, file, true); break; + + default: + bfs_bug("Unexpected ioq op %d", (int)op); + break; } ioq_free(ioq, ent); @@ -1160,12 +1167,13 @@ static int bftw_file_open(struct bftw_state *state, struct bftw_file *file, cons struct bftw_list parents; SLIST_INIT(&parents); - struct bftw_file *cur; - for (cur = file; cur != base; cur = cur->parent) { + // Reverse the chain of parents + for (struct bftw_file *cur = file; cur != base; cur = cur->parent) { SLIST_PREPEND(&parents, cur); } - while ((cur = SLIST_POP(&parents))) { + // Open each component relative to its parent + drain_slist (struct bftw_file, cur, &parents) { if (!cur->parent || cur->parent->fd >= 0) { bftw_file_openat(state, cur, cur->parent, cur->name); } @@ -1281,8 +1289,8 @@ static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) { int fd = parent->fd; if (fd < 0) { - bfs_static_assert(AT_FDCWD != -1); - return -1; + // Don't confuse failures with AT_FDCWD + return (int)AT_FDCWD == -1 ? -2 : -1; } bftw_cache_pin(&state->cache, parent); @@ -1298,7 +1306,7 @@ static int bftw_ioq_opendir(struct bftw_state *state, struct bftw_file *file) { } int dfd = bftw_pin_parent(state, file); - if (dfd < 0 && dfd != AT_FDCWD) { + if (dfd < 0 && dfd != (int)AT_FDCWD) { goto fail; } @@ -1431,7 +1439,7 @@ static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bf if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) { return true; } - fallthru; + _fallthrough; default: #if __linux__ @@ -1450,7 +1458,7 @@ static int bftw_ioq_stat(struct bftw_state *state, struct bftw_file *file) { } int dfd = bftw_pin_parent(state, file); - if (dfd < 0 && dfd != AT_FDCWD) { + if (dfd < 0 && dfd != (int)AT_FDCWD) { goto fail; } @@ -1477,7 +1485,8 @@ fail: /** Check if we should stat() a file asynchronously. */ static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) { - // To avoid surprising users too much, process the roots in order + // POSIX wants the root paths to be processed in order + // See https://www.austingroupbugs.net/view.php?id=1859 if (file->depth == 0) { return false; } @@ -1529,11 +1538,28 @@ static bool bftw_pop_file(struct bftw_state *state) { return bftw_pop(state, &state->fileq); } +/** Add a path component to the path. */ +static void bftw_prepend_path(char *path, size_t nameoff, size_t namelen, const char *name) { + if (nameoff > 0) { + path[nameoff - 1] = '/'; + } + memcpy(path + nameoff, name, namelen); +} + /** Build the path to the current file. */ static int bftw_build_path(struct bftw_state *state, const char *name) { const struct bftw_file *file = state->file; - size_t pathlen = file ? file->nameoff + file->namelen : 0; + size_t nameoff, namelen; + if (name) { + nameoff = file ? bftw_child_nameoff(file) : 0; + namelen = strlen(name); + } else { + nameoff = file->nameoff; + namelen = file->namelen; + } + + size_t pathlen = nameoff + namelen; if (dstresize(&state->path, pathlen) != 0) { state->error = errno; return -1; @@ -1546,11 +1572,11 @@ static int bftw_build_path(struct bftw_state *state, const char *name) { } // Build the path backwards + if (name) { + bftw_prepend_path(state->path, nameoff, namelen, name); + } while (file && file != ancestor) { - if (file->nameoff > 0) { - state->path[file->nameoff - 1] = '/'; - } - memcpy(state->path + file->nameoff, file->name, file->namelen); + bftw_prepend_path(state->path, file->nameoff, file->namelen, file->name); if (ancestor && ancestor->depth == file->depth) { ancestor = ancestor->parent; @@ -1559,20 +1585,6 @@ static int bftw_build_path(struct bftw_state *state, const char *name) { } state->previous = state->file; - - if (name) { - if (pathlen > 0 && state->path[pathlen - 1] != '/') { - if (dstrapp(&state->path, '/') != 0) { - state->error = errno; - return -1; - } - } - if (dstrcat(&state->path, name) != 0) { - state->error = errno; - return -1; - } - } - return 0; } @@ -1676,6 +1688,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { ftwbuf->visit = visit; ftwbuf->type = BFS_UNKNOWN; ftwbuf->error = state->direrror; + ftwbuf->loopoff = 0; ftwbuf->at_fd = AT_FDCWD; ftwbuf->at_path = ftwbuf->path; bftw_stat_init(&ftwbuf->stat_bufs, &state->stat_buf, &state->lstat_buf); @@ -1733,6 +1746,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) { ftwbuf->type = BFS_ERROR; ftwbuf->error = ELOOP; + ftwbuf->loopoff = ancestor->nameoff + ancestor->namelen; return; } } @@ -1863,8 +1877,8 @@ static int bftw_gc(struct bftw_state *state, enum bftw_gc_flags flags) { } state->direrror = 0; - while ((file = SLIST_POP(&state->to_close, ready))) { - bftw_unwrapdir(state, file); + drain_slist (struct bftw_file, dead, &state->to_close, ready) { + bftw_unwrapdir(state, dead); } enum bftw_gc_flags visit = BFTW_VISIT_FILE; @@ -1945,6 +1959,10 @@ static void bftw_flush(struct bftw_state *state) { bftw_queue_flush(&state->dirq); bftw_ioq_opendirs(state); + + if (state->ioq) { + ioq_submit(state->ioq); + } } /** Close the current directory. */ @@ -10,6 +10,7 @@ #include "dir.h" #include "stat.h" + #include <stddef.h> /** @@ -54,8 +55,10 @@ 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; + /** For filesystem loops, the length of the loop prefix. */ + size_t loopoff; /** A parent file descriptor for the *at() family of calls. */ int at_fd; @@ -72,9 +75,9 @@ struct BFTW { * Get bfs_stat() info for a file encountered during bftw(), caching the result * whenever possible. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to stat. - * @param flags + * @flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return * A pointer to a bfs_stat() buffer, or NULL if the call failed. @@ -85,9 +88,9 @@ const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags * Get bfs_stat() info for a file encountered during bftw(), if it has already * been cached. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to stat. - * @param flags + * @flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return * A pointer to a bfs_stat() buffer, or NULL if no stat info is cached. @@ -99,12 +102,12 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat * whether to follow links. This function will avoid calling bfs_stat() if * possible. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to check. - * @param flags + * @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); @@ -123,9 +126,9 @@ enum bftw_action { /** * Callback function type for bftw(). * - * @param ftwbuf + * @ftwbuf * Data about the current file. - * @param ptr + * @ptr * The pointer passed to bftw(). * @return * An action value. @@ -208,7 +211,7 @@ struct bftw_args { * Like ftw(3) and nftw(3), this function walks a directory tree recursively, * and invokes a callback for each path it encounters. * - * @param args + * @args * The arguments that control the walk. * @return * 0 on success, or -1 on failure. @@ -8,11 +8,12 @@ #ifndef BFS_BIT_H #define BFS_BIT_H -#include "config.h" +#include "bfs.h" + #include <limits.h> #include <stdint.h> -#if __STDC_VERSION__ >= C23 +#if __has_include(<stdbit.h>) # include <stdbit.h> #endif @@ -147,7 +148,7 @@ # define INTMAX_WIDTH UINTMAX_WIDTH #endif -// C23 polyfill: byte order +// N3022 polyfill: byte order #ifdef __STDC_ENDIAN_LITTLE__ # define ENDIAN_LITTLE __STDC_ENDIAN_LITTLE__ @@ -173,11 +174,7 @@ # define ENDIAN_NATIVE 0 #endif -#if __STDC_VERSION__ >= C23 -# define bswap_u16 stdc_memreverse8u16 -# define bswap_u32 stdc_memreverse8u32 -# define bswap_u64 stdc_memreverse8u64 -#elif __GNUC__ +#if __GNUC__ # define bswap_u16 __builtin_bswap16 # define bswap_u32 __builtin_bswap32 # define bswap_u64 __builtin_bswap64 @@ -201,15 +198,35 @@ static inline uint8_t bswap_u8(uint8_t n) { return n; } -/** - * Reverse the byte order of an integer. - */ -#define bswap(n) \ - _Generic((n), \ - uint8_t: bswap_u8, \ - uint16_t: bswap_u16, \ - uint32_t: bswap_u32, \ - uint64_t: bswap_u64)(n) +#if UCHAR_WIDTH == 8 +# define bswap_uc bswap_u8 +#endif + +#if USHRT_WIDTH == 16 +# define bswap_us bswap_u16 +#elif USHRT_WIDTH == 32 +# define bswap_us bswap_u32 +#elif USHRT_WIDTH == 64 +# define bswap_us bswap_u64 +#endif + +#if UINT_WIDTH == 16 +# define bswap_ui bswap_u16 +#elif UINT_WIDTH == 32 +# define bswap_ui bswap_u32 +#elif UINT_WIDTH == 64 +# define bswap_ui bswap_u64 +#endif + +#if ULONG_WIDTH == 32 +# define bswap_ul bswap_u32 +#elif ULONG_WIDTH == 64 +# define bswap_ul bswap_u64 +#endif + +#if ULLONG_WIDTH == 64 +# define bswap_ull bswap_u64 +#endif // Define an overload for each unsigned type #define UINT_OVERLOADS(macro) \ @@ -222,25 +239,74 @@ static inline uint8_t bswap_u8(uint8_t n) { // Select an overload based on an unsigned integer type #define UINT_SELECT(n, name) \ _Generic((n), \ - char: name##_uc, \ - signed char: name##_uc, \ unsigned char: name##_uc, \ - signed short: name##_us, \ unsigned short: name##_us, \ - signed int: name##_ui, \ unsigned int: name##_ui, \ - signed long: name##_ul, \ unsigned long: name##_ul, \ - signed long long: name##_ull, \ unsigned long long: name##_ull) +/** + * Reverse the byte order of an integer. + */ +#define bswap(n) UINT_SELECT(n, bswap)(n) + +#define LOAD8_LEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (i)) +#define LOAD8_BEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (n - (i) - 8)) + +/** Load a little-endian 8-bit word. */ +static inline uint8_t load8_leu8(const void *ptr) { + return LOAD8_LEU8(ptr, 0, 8); +} + +/** Load a big-endian 8-bit word. */ +static inline uint8_t load8_beu8(const void *ptr) { + return LOAD8_BEU8(ptr, 0, 8); +} + +#define LOAD8_LEU16(ptr, i, n) (LOAD8_LEU8(ptr, i, n) | LOAD8_LEU8(ptr, i + 8, n)) +#define LOAD8_BEU16(ptr, i, n) (LOAD8_BEU8(ptr, i, n) | LOAD8_BEU8(ptr, i + 8, n)) + +/** Load a little-endian 16-bit word. */ +static inline uint16_t load8_leu16(const void *ptr) { + return LOAD8_LEU16(ptr, 0, 16); +} + +/** Load a big-endian 16-bit word. */ +static inline uint16_t load8_beu16(const void *ptr) { + return LOAD8_BEU16(ptr, 0, 16); +} + +#define LOAD8_LEU32(ptr, i, n) (LOAD8_LEU16(ptr, i, n) | LOAD8_LEU16(ptr, i + 16, n)) +#define LOAD8_BEU32(ptr, i, n) (LOAD8_BEU16(ptr, i, n) | LOAD8_BEU16(ptr, i + 16, n)) + +/** Load a little-endian 32-bit word. */ +static inline uint32_t load8_leu32(const void *ptr) { + return LOAD8_LEU32(ptr, 0, 32); +} + +/** Load a big-endian 32-bit word. */ +static inline uint32_t load8_beu32(const void *ptr) { + return LOAD8_BEU32(ptr, 0, 32); +} + +#define LOAD8_LEU64(ptr, i, n) (LOAD8_LEU32(ptr, i, n) | LOAD8_LEU32(ptr, i + 32, n)) +#define LOAD8_BEU64(ptr, i, n) (LOAD8_BEU32(ptr, i, n) | LOAD8_BEU32(ptr, i + 32, n)) + +/** Load a little-endian 64-bit word. */ +static inline uint64_t load8_leu64(const void *ptr) { + return LOAD8_LEU64(ptr, 0, 64); +} + +/** Load a big-endian 64-bit word. */ +static inline uint64_t load8_beu64(const void *ptr) { + return LOAD8_BEU64(ptr, 0, 64); +} + // C23 polyfill: bit utilities -#if __STDC_VERSION__ >= C23 +#if __STDC_VERSION_STDBIT_H__ >= C23 # define count_ones stdc_count_ones # define count_zeros stdc_count_zeros -# define rotate_left stdc_rotate_left -# define rotate_right stdc_rotate_right # define leading_zeros stdc_leading_zeros # define leading_ones stdc_leading_ones # define trailing_zeros stdc_trailing_zeros @@ -273,31 +339,31 @@ static inline uint8_t bswap_u8(uint8_t n) { #define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix #define COUNT_ONES(type, suffix, width) \ - static inline int count_ones##suffix(type n) { \ + static inline unsigned int count_ones##suffix(type n) { \ return UINT_BUILTIN(popcount, suffix)(n); \ } #define LEADING_ZEROS(type, suffix, width) \ - static inline int leading_zeros##suffix(type n) { \ + static inline unsigned int leading_zeros##suffix(type n) { \ return n \ ? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \ : width; \ } #define TRAILING_ZEROS(type, suffix, width) \ - static inline int trailing_zeros##suffix(type n) { \ + static inline unsigned int trailing_zeros##suffix(type n) { \ return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \ } #define FIRST_TRAILING_ONE(type, suffix, width) \ - static inline int first_trailing_one##suffix(type n) { \ + static inline unsigned int first_trailing_one##suffix(type n) { \ return UINT_BUILTIN(ffs, suffix)(n); \ } #else // !__GNUC__ #define COUNT_ONES(type, suffix, width) \ - static inline int count_ones##suffix(type n) { \ + static inline unsigned int count_ones##suffix(type n) { \ int ret; \ for (ret = 0; n; ++ret) { \ n &= n - 1; \ @@ -306,7 +372,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define LEADING_ZEROS(type, suffix, width) \ - static inline int leading_zeros##suffix(type n) { \ + static inline unsigned int leading_zeros##suffix(type n) { \ type bit = (type)1 << (width - 1); \ int ret; \ for (ret = 0; bit && !(n & bit); ++ret, bit >>= 1); \ @@ -314,7 +380,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define TRAILING_ZEROS(type, suffix, width) \ - static inline int trailing_zeros##suffix(type n) { \ + static inline unsigned int trailing_zeros##suffix(type n) { \ type bit = 1; \ int ret; \ for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \ @@ -322,7 +388,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define FIRST_TRAILING_ONE(type, suffix, width) \ - static inline int first_trailing_one##suffix(type n) { \ + static inline unsigned int first_trailing_one##suffix(type n) { \ return n ? trailing_zeros##suffix(n) + 1 : 0; \ } @@ -333,19 +399,9 @@ UINT_OVERLOADS(LEADING_ZEROS) UINT_OVERLOADS(TRAILING_ZEROS) UINT_OVERLOADS(FIRST_TRAILING_ONE) -#define ROTATE_LEFT(type, suffix, width) \ - static inline type rotate_left##suffix(type n, int c) { \ - return (n << c) | (n >> ((width - c) % width)); \ - } - -#define ROTATE_RIGHT(type, suffix, width) \ - static inline type rotate_right##suffix(type n, int c) { \ - return (n >> c) | (n << ((width - c) % width)); \ - } - #define FIRST_LEADING_ONE(type, suffix, width) \ - static inline int first_leading_one##suffix(type n) { \ - return width - leading_zeros##suffix(n); \ + static inline unsigned int first_leading_one##suffix(type n) { \ + return n ? leading_zeros##suffix(n) + 1 : 0; \ } #define HAS_SINGLE_BIT(type, suffix, width) \ @@ -354,17 +410,30 @@ UINT_OVERLOADS(FIRST_TRAILING_ONE) return n - 1 < (n ^ (n - 1)); \ } -UINT_OVERLOADS(ROTATE_LEFT) -UINT_OVERLOADS(ROTATE_RIGHT) +#define BIT_WIDTH(type, suffix, width) \ + static inline unsigned int bit_width##suffix(type n) { \ + return width - leading_zeros##suffix(n); \ + } + +#define BIT_FLOOR(type, suffix, width) \ + static inline type bit_floor##suffix(type n) { \ + return n ? (type)1 << (bit_width##suffix(n) - 1) : 0; \ + } + +#define BIT_CEIL(type, suffix, width) \ + static inline type bit_ceil##suffix(type n) { \ + return (type)1 << bit_width##suffix(n - !!n); \ + } + UINT_OVERLOADS(FIRST_LEADING_ONE) UINT_OVERLOADS(HAS_SINGLE_BIT) +UINT_OVERLOADS(BIT_WIDTH) +UINT_OVERLOADS(BIT_FLOOR) +UINT_OVERLOADS(BIT_CEIL) #define count_ones(n) UINT_SELECT(n, count_ones)(n) #define count_zeros(n) UINT_SELECT(n, count_ones)(~(n)) -#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c) -#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c) - #define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n) #define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n)) @@ -379,23 +448,26 @@ UINT_OVERLOADS(HAS_SINGLE_BIT) #define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n) -#define BIT_FLOOR(type, suffix, width) \ - static inline type bit_floor##suffix(type n) { \ - return n ? (type)1 << (first_leading_one##suffix(n) - 1) : 0; \ - } +#define bit_width(n) UINT_SELECT(n, bit_width)(n) +#define bit_floor(n) UINT_SELECT(n, bit_floor)(n) +#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n) -#define BIT_CEIL(type, suffix, width) \ - static inline type bit_ceil##suffix(type n) { \ - return (type)1 << first_leading_one##suffix(n - !!n); \ +#endif // __STDC_VERSION_STDBIT_H__ < C23 + +#define ROTATE_LEFT(type, suffix, width) \ + static inline type rotate_left##suffix(type n, int c) { \ + return (n << c) | (n >> ((width - c) % width)); \ } -UINT_OVERLOADS(BIT_FLOOR) -UINT_OVERLOADS(BIT_CEIL) +#define ROTATE_RIGHT(type, suffix, width) \ + static inline type rotate_right##suffix(type n, int c) { \ + return (n >> c) | (n << ((width - c) % width)); \ + } -#define bit_width(n) first_leading_one(n) -#define bit_floor(n) UINT_SELECT(n, bit_floor)(n) -#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n) +UINT_OVERLOADS(ROTATE_LEFT) +UINT_OVERLOADS(ROTATE_RIGHT) -#endif // __STDC_VERSION__ < C23 +#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c) +#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c) #endif // BFS_BIT_H diff --git a/src/color.c b/src/color.c index 8c32a68..a026831 100644 --- a/src/color.c +++ b/src/color.c @@ -2,10 +2,11 @@ // SPDX-License-Identifier: 0BSD #include "color.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" -#include "config.h" #include "diag.h" #include "dir.h" #include "dstring.h" @@ -13,6 +14,7 @@ #include "fsade.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdarg.h> @@ -29,8 +31,8 @@ struct esc_seq { /** The length of the escape sequence. */ size_t len; - /** The escape sequence iteself, without a terminating NUL. */ - char seq[]; + /** The escape sequence itself, without a terminating NUL. */ + char seq[] _counted_by(len); }; /** @@ -46,7 +48,7 @@ struct ext_color { /** Whether the comparison should be case-sensitive. */ bool case_sensitive; /** The extension to match (NUL-terminated). */ - char ext[]; + char ext[]; // _counted_by(len + 1); }; struct colors { @@ -141,13 +143,7 @@ static int init_esc(struct colors *colors, const char *name, const char *value, *field = esc; - struct trie_leaf *leaf = trie_insert_str(&colors->names, name); - if (!leaf) { - return -1; - } - - leaf->value = field; - return 0; + return trie_set_str(&colors->names, name, field); } /** Check if an escape sequence is equal to a string. */ @@ -157,12 +153,16 @@ static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) { /** Get an escape sequence from the table. */ static struct esc_seq **get_esc(const struct colors *colors, const char *name) { - const struct trie_leaf *leaf = trie_find_str(&colors->names, name); - return leaf ? leaf->value : NULL; + return trie_get_str(&colors->names, name); +} + +/** Append an escape sequence to a string. */ +static int cat_esc(dchar **dstr, const struct esc_seq *seq) { + return dstrxcat(dstr, seq->seq, seq->len); } /** Set a named escape sequence. */ -static int set_esc(struct colors *colors, const char *name, char *value) { +static int set_esc(struct colors *colors, const char *name, dchar *value) { struct esc_seq **field = get_esc(colors, name); if (!field) { return 0; @@ -209,58 +209,31 @@ static void ext_tolower(char *ext, size_t len) { } } -/** - * The "smart case" algorithm. - * - * @param ext - * The current extension being added. - * @param prev - * The previous case-sensitive match, if any, for the same extension. - * @param iprev - * The previous case-insensitive match, if any, for the same extension. - * @return - * Whether this extension should become case-sensitive. - */ -static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *prev, struct ext_color *iprev) { - // This is the first case-insensitive occurrence of this extension, e.g. - // - // *.gz=01;31:*.tar.gz=01;33 - if (!iprev) { - bfs_assert(!prev); - return false; - } - - // If the last version of this extension is already case-sensitive, - // this one should be too, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 - if (iprev->case_sensitive) { - return true; - } - - // The case matches the last occurrence exactly, e.g. - // - // *.tar.gz=01;31:*.tar.gz=01;33 - if (iprev == prev) { - return false; - } - - // Different case, but same value, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;31 - if (esc_eq(iprev->esc, ext->esc->seq, ext->esc->len)) { - return false; +/** Insert an extension into a trie. */ +static int insert_ext(struct trie *trie, struct ext_color *ext) { + // A later *.x should override any earlier *.x, *.y.x, etc. + struct trie_leaf *leaf; + while ((leaf = trie_find_postfix(trie, ext->ext))) { + trie_remove(trie, leaf); } - // Different case, different value, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;33 - return true; + size_t len = ext->len + 1; + return trie_set_mem(trie, ext->ext, len, ext); } /** Set the color for an extension. */ -static int set_ext(struct colors *colors, char *key, char *value) { +static int set_ext(struct colors *colors, dchar *key, dchar *value) { size_t len = dstrlen(key); + + // Embedded NUL bytes in extensions can lead to a non-prefix-free + // set of strings, e.g. {".gz", "\0.gz"} would be transformed to + // {"zg.\0", "zg.\0\0"} (showing the implicit terminating NUL). + // Our trie implementation only supports prefix-free key sets, but + // luckily '\0' cannot appear in filenames so we can ignore them. + if (memchr(key, '\0', len)) { + return 0; + } + struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1); if (!ext) { return -1; @@ -274,45 +247,19 @@ static int set_ext(struct colors *colors, char *key, char *value) { goto fail; } - key = memcpy(ext->ext, key, len + 1); + memcpy(ext->ext, key, len + 1); // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix() - ext_reverse(key, len); - - // Find any pre-existing exact match - struct ext_color *prev = NULL; - struct trie_leaf *leaf = trie_find_str(&colors->ext_trie, key); - if (leaf) { - prev = leaf->value; - trie_remove(&colors->ext_trie, leaf); - } - - // A later *.x should override any earlier *.x, *.y.x, etc. - while ((leaf = trie_find_postfix(&colors->ext_trie, key))) { - trie_remove(&colors->ext_trie, leaf); - } + ext_reverse(ext->ext, len); // Insert the extension into the case-sensitive trie - leaf = trie_insert_str(&colors->ext_trie, key); - if (!leaf) { + if (insert_ext(&colors->ext_trie, ext) != 0) { goto fail; } - leaf->value = ext; - // "Smart case": if the same extension is given with two different - // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive - ext_tolower(key, len); - leaf = trie_insert_str(&colors->iext_trie, key); - if (!leaf) { - goto fail; - } - - struct ext_color *iprev = leaf->value; - if (ext_case_sensitive(ext, prev, iprev)) { - iprev->case_sensitive = true; - ext->case_sensitive = true; + if (colors->ext_len < len) { + colors->ext_len = len; } - leaf->value = ext; return 0; @@ -324,32 +271,83 @@ fail: return -1; } -/** Rebuild the case-insensitive trie after all extensions have been parsed. */ -static int build_iext_trie(struct colors *colors) { - trie_clear(&colors->iext_trie); +/** + * The "smart case" algorithm. + * + * @ext + * The current extension being added. + * @iext + * The previous case-insensitive match, if any, for the same extension. + * @return + * Whether this extension should become case-sensitive. + */ +static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *iext) { + // This is the first case-insensitive occurrence of this extension, e.g. + // + // *.gz=01;31:*.tar.gz=01;33 + if (!iext) { + return false; + } + + // If the last version of this extension is already case-sensitive, + // this one should be too, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 + if (iext->case_sensitive) { + return true; + } + + // Different case, but same value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;31 + if (esc_eq(iext->esc, ext->esc->seq, ext->esc->len)) { + return false; + } + // Different case, different value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;33 + return true; +} + +/** Build the case-insensitive trie, after all extensions have been parsed. */ +static int build_iext_trie(struct colors *colors) { + // Find which extensions should be case-sensitive for_trie (leaf, &colors->ext_trie) { - size_t len = leaf->length - 1; - if (colors->ext_len < len) { - colors->ext_len = len; + struct ext_color *ext = leaf->value; + + // "Smart case": if the same extension is given with two different + // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive + ext_tolower(ext->ext, ext->len); + + size_t len = ext->len + 1; + struct trie_leaf *ileaf = trie_insert_mem(&colors->iext_trie, ext->ext, len); + if (!ileaf) { + return -1; } + struct ext_color *iext = ileaf->value; + if (ext_case_sensitive(ext, iext)) { + ext->case_sensitive = true; + iext->case_sensitive = true; + } + + ileaf->value = ext; + } + + // Rebuild the trie with only the case-insensitive ones + trie_clear(&colors->iext_trie); + + for_trie (leaf, &colors->ext_trie) { struct ext_color *ext = leaf->value; if (ext->case_sensitive) { continue; } - // set_ext() already reversed and lowercased the extension - struct trie_leaf *ileaf; - while ((ileaf = trie_find_postfix(&colors->iext_trie, ext->ext))) { - trie_remove(&colors->iext_trie, ileaf); - } - - ileaf = trie_insert_str(&colors->iext_trie, ext->ext); - if (!ileaf) { + // We already lowercased the extension above + if (insert_ext(&colors->iext_trie, ext) != 0) { return -1; } - ileaf->value = ext; } return 0; @@ -358,9 +356,8 @@ static int build_iext_trie(struct colors *colors) { /** * Find a color by an extension. */ -static const struct esc_seq *get_ext(const struct colors *colors, const char *filename) { +static const struct esc_seq *get_ext(const struct colors *colors, const char *filename, size_t name_len) { size_t ext_len = colors->ext_len; - size_t name_len = strlen(filename); if (name_len < ext_len) { ext_len = name_len; } @@ -369,7 +366,8 @@ static const struct esc_seq *get_ext(const struct colors *colors, const char *fi char buf[256]; char *copy; if (ext_len < sizeof(buf)) { - copy = memcpy(buf, suffix, ext_len + 1); + copy = memcpy(buf, suffix, ext_len); + copy[ext_len] = '\0'; } else { copy = strndup(suffix, ext_len); if (!copy) { @@ -417,13 +415,13 @@ static const struct esc_seq *get_ext(const struct colors *colors, const char *fi * * See man dir_colors. * - * @param str + * @str * A dstring to fill with the unescaped chunk. - * @param value + * @value * The value to parse. - * @param end + * @end * The character that marks the end of the chunk. - * @param[out] next + * @next[out] * Will be set to the next chunk. * @return * 0 on success, -1 on failure. @@ -578,7 +576,7 @@ static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { break; } - if (dstrncpy(&key, chunk, equals - chunk) != 0) { + if (dstrxcpy(&key, chunk, equals - chunk) != 0) { goto fail; } if (unescape(&value, equals + 1, ':', &next) != 0) { @@ -587,8 +585,8 @@ static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { // All-zero values should be treated like NULL, to fall // back on any other relevant coloring for that file - char *esc = value; - if (strspn(value, "0") == strlen(value) + dchar *esc = value; + if (strspn(value, "0") == dstrlen(value) && strcmp(key, "rs") != 0 && strcmp(key, "lc") != 0 && strcmp(key, "rc") != 0 @@ -693,6 +691,20 @@ struct colors *parse_colors(void) { colors->link->len = 0; } + // Pre-compute the reset escape sequence + if (!colors->endcode) { + dchar *ec = dstralloc(0); + if (!ec + || cat_esc(&ec, colors->leftcode) != 0 + || cat_esc(&ec, colors->reset) != 0 + || cat_esc(&ec, colors->rightcode) != 0 + || set_esc(colors, "ec", ec) != 0) { + dstrfree(ec); + goto fail; + } + dstrfree(ec); + } + return colors; fail: @@ -727,10 +739,11 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { } cfile->file = file; + cfile->fd = fileno(file); cfile->need_reset = false; cfile->close = close; - if (isatty(fileno(file))) { + if (isatty(cfile->fd)) { cfile->colors = colors; } else { cfile->colors = NULL; @@ -755,23 +768,196 @@ int cfclose(CFILE *cfile) { return ret; } +bool colors_need_stat(const struct colors *colors) { + return colors->setuid || colors->setgid || colors->executable || colors->multi_hard + || colors->sticky_other_writable || colors->other_writable || colors->sticky; +} + +/** A colorable file path. */ +struct cpath { + /** The full path to color. */ + const char *path; + /** The basename offset of the last valid component. */ + size_t nameoff; + /** The end offset of the last valid component. */ + size_t valid; + /** The total length of the path. */ + size_t len; + + /** The bftw() buffer. */ + const struct BFTW *ftwbuf; + /** bfs_stat() flags for the final component. */ + enum bfs_stat_flags flags; + /** A bfs_stat() buffer, filled in when 0 < valid < len. */ + struct bfs_stat statbuf; +}; + +/** Move the valid range of a path backwards. */ +static void cpath_retreat(struct cpath *cpath) { + const char *path = cpath->path; + size_t nameoff = cpath->nameoff; + size_t valid = cpath->valid; + + if (valid > 0 && path[valid - 1] == '/') { + // Try without trailing slashes, to distinguish "notdir/" from "notdir" + do { + --valid; + } while (valid > 0 && path[valid - 1] == '/'); + + nameoff = valid; + while (nameoff > 0 && path[nameoff - 1] != '/') { + --nameoff; + } + } else { + // Remove the last component and try again + valid = nameoff; + } + + cpath->nameoff = nameoff; + cpath->valid = valid; +} + +/** Initialize a struct cpath. */ +static int cpath_init(struct cpath *cpath, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + // Normally there are only two components to color: + // + // nameoff valid + // v v + // path/to/filename + // --------+------- + // ${di} ${fi} + // + // Error cases also usually have two components: + // + // valid, + // nameoff + // v + // path/to/nowhere + // --------+------ + // ${di} ${mi} + // + // But with ENOTDIR, there may be three: + // + // nameoff valid + // v v + // path/to/filename/nowhere + // --------+-------+------- + // ${di} ${fi} ${mi} + + cpath->path = path; + cpath->len = strlen(path); + cpath->ftwbuf = ftwbuf; + cpath->flags = flags; + + cpath->valid = cpath->len; + if (path == ftwbuf->path) { + cpath->nameoff = ftwbuf->nameoff; + } else { + cpath->nameoff = xbaseoff(path); + } + + if (bftw_type(ftwbuf, flags) != BFS_ERROR) { + return 0; + } + + cpath_retreat(cpath); + + // Find the base path. For symlinks like + // + // path/to/symlink -> nested/file + // + // this will be something like + // + // path/to/nested/file + int at_fd = AT_FDCWD; + dchar *at_path = NULL; + if (path == ftwbuf->path) { + if (ftwbuf->depth > 0) { + // The parent must have existed to get here + return 0; + } + } else { + // We're in print_link_target(), so resolve relative to the link's parent directory + at_fd = ftwbuf->at_fd; + if (at_fd == (int)AT_FDCWD && path[0] != '/') { + at_path = dstrxdup(ftwbuf->path, ftwbuf->nameoff); + if (!at_path) { + return -1; + } + } + } + + if (!at_path) { + at_path = dstralloc(cpath->valid); + if (!at_path) { + return -1; + } + } + if (dstrxcat(&at_path, path, cpath->valid) != 0) { + dstrfree(at_path); + return -1; + } + + size_t at_off = dstrlen(at_path) - cpath->valid; + + // Find the longest valid path prefix + while (cpath->valid > 0) { + if (bfs_stat(at_fd, at_path, BFS_STAT_FOLLOW, &cpath->statbuf) == 0) { + break; + } + + cpath_retreat(cpath); + dstrshrink(at_path, at_off + cpath->valid); + } + + dstrfree(at_path); + return 0; +} + +/** Get the bfs_stat() buffer for the last valid component. */ +static const struct bfs_stat *cpath_stat(const struct cpath *cpath) { + if (cpath->valid == cpath->len) { + return bftw_stat(cpath->ftwbuf, cpath->flags); + } else { + return &cpath->statbuf; + } +} + +/** Check if a path has non-trivial capabilities. */ +static bool cpath_has_capabilities(const struct cpath *cpath) { + if (cpath->valid == cpath->len) { + return bfs_check_capabilities(cpath->ftwbuf) > 0; + } else { + // TODO: implement capability checks for arbitrary paths + return false; + } +} + /** Check if a symlink is broken. */ -static bool is_link_broken(const struct BFTW *ftwbuf) { +static bool cpath_is_broken(const struct cpath *cpath) { + if (cpath->valid < cpath->len) { + // A valid parent can't be a broken link + return false; + } + + const struct BFTW *ftwbuf = cpath->ftwbuf; if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) { return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0; } else { + // A link encountered with BFS_STAT_TRYFOLLOW must be broken return true; } } -bool colors_need_stat(const struct colors *colors) { - return colors->setuid || colors->setgid || colors->executable || colors->multi_hard - || colors->sticky_other_writable || colors->other_writable || colors->sticky; -} - /** Get the color for a file. */ -static const struct esc_seq *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - enum bfs_type type = bftw_type(ftwbuf, flags); +static const struct esc_seq *file_color(const struct colors *colors, const struct cpath *cpath) { + enum bfs_type type; + if (cpath->valid == cpath->len) { + type = bftw_type(cpath->ftwbuf, cpath->flags); + } else { + type = bfs_mode_to_type(cpath->statbuf.mode); + } + if (type == BFS_ERROR) { goto error; } @@ -782,7 +968,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char switch (type) { case BFS_REG: if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) { - statbuf = bftw_stat(ftwbuf, flags); + statbuf = cpath_stat(cpath); if (!statbuf) { goto error; } @@ -792,7 +978,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char color = colors->setuid; } else if (colors->setgid && (statbuf->mode & 02000)) { color = colors->setgid; - } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) { + } else if (colors->capable && cpath_has_capabilities(cpath)) { color = colors->capable; } else if (colors->executable && (statbuf->mode & 00111)) { color = colors->executable; @@ -801,7 +987,9 @@ static const struct esc_seq *file_color(const struct colors *colors, const char } if (!color) { - color = get_ext(colors, filename); + const char *name = cpath->path + cpath->nameoff; + size_t namelen = cpath->valid - cpath->nameoff; + color = get_ext(colors, name, namelen); } if (!color) { @@ -812,7 +1000,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char case BFS_DIR: if (colors->sticky_other_writable || colors->other_writable || colors->sticky) { - statbuf = bftw_stat(ftwbuf, flags); + statbuf = cpath_stat(cpath); if (!statbuf) { goto error; } @@ -831,7 +1019,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char break; case BFS_LNK: - if (colors->orphan && is_link_broken(ftwbuf)) { + if (colors->orphan && cpath_is_broken(cpath)) { color = colors->orphan; } else { color = colors->link; @@ -874,7 +1062,7 @@ error: /** Print an escape sequence chunk. */ static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) { - return dstrxcat(&cfile->buffer, esc->seq, esc->len); + return cat_esc(&cfile->buffer, esc); } /** Print an ANSI escape sequence. */ @@ -908,12 +1096,7 @@ static int print_reset(CFILE *cfile) { } cfile->need_reset = false; - const struct colors *colors = cfile->colors; - if (colors->endcode) { - return print_esc_chunk(cfile, colors->endcode); - } else { - return print_esc(cfile, colors->reset); - } + return print_esc_chunk(cfile, cfile->colors->endcode); } /** Print a shell-escaped string. */ @@ -923,6 +1106,10 @@ static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flag /** Print a string with an optional color. */ static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) { + if (len == 0) { + return 0; + } + if (print_esc(cfile, esc) != 0) { return -1; } @@ -939,112 +1126,42 @@ static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *st return 0; } -/** Find the offset of the first broken path component. */ -static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) { - ssize_t ret = max; - bfs_assert(ret >= 0); - - if (bftw_type(ftwbuf, flags) != BFS_ERROR) { - goto out; - } - - dchar *at_path; - int at_fd; - if (path == ftwbuf->path) { - if (ftwbuf->depth == 0) { - at_fd = AT_FDCWD; - at_path = dstrndup(path, max); - } else { - // The parent must have existed to get here - goto out; - } - } else { - // We're in print_link_target(), so resolve relative to the link's parent directory - at_fd = ftwbuf->at_fd; - if (at_fd == AT_FDCWD && path[0] != '/') { - at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff); - if (at_path && dstrncat(&at_path, path, max) != 0) { - ret = -1; - goto out_path; - } - } else { - at_path = dstrndup(path, max); - } - } - - if (!at_path) { - ret = -1; - goto out; - } - - while (ret > 0) { - if (xfaccessat(at_fd, at_path, F_OK) == 0) { - break; - } - - size_t len = dstrlen(at_path); - while (ret && at_path[len - 1] == '/') { - --len, --ret; - } - if (errno != ENOTDIR) { - while (ret && at_path[len - 1] != '/') { - --len, --ret; - } - } - - dstresize(&at_path, len); - } - -out_path: - dstrfree(at_path); -out: - return ret; -} - /** Print a path with colors. */ static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - size_t nameoff; - if (path == ftwbuf->path) { - nameoff = ftwbuf->nameoff; - } else { - nameoff = xbaseoff(path); - } - - const char *name = path + nameoff; - size_t pathlen = nameoff + strlen(name); - - ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); - if (broken < 0) { + struct cpath cpath; + if (cpath_init(&cpath, path, ftwbuf, flags) != 0) { return -1; } - size_t split = broken; const struct colors *colors = cfile->colors; const struct esc_seq *dirs_color = colors->directory; - const struct esc_seq *name_color; + const struct esc_seq *name_color = NULL; + const struct esc_seq *err_color = colors->missing; + if (!err_color) { + err_color = colors->orphan; + } - if (split < nameoff) { - name_color = colors->missing; - if (!name_color) { - name_color = colors->orphan; - } - } else { - name_color = file_color(cfile->colors, path + nameoff, ftwbuf, flags); + if (cpath.nameoff < cpath.valid) { + name_color = file_color(colors, &cpath); if (name_color == dirs_color) { - split = pathlen; + cpath.nameoff = cpath.valid; } } - if (split > 0) { - if (print_colored(cfile, dirs_color, path, split) != 0) { - return -1; - } + if (print_colored(cfile, dirs_color, path, cpath.nameoff) != 0) { + return -1; } - if (split < pathlen) { - if (print_colored(cfile, name_color, path + split, pathlen - split) != 0) { - return -1; - } + const char *name = path + cpath.nameoff; + size_t name_len = cpath.valid - cpath.nameoff; + if (print_colored(cfile, name_color, name, name_len) != 0) { + return -1; + } + + const char *tail = path + cpath.valid; + size_t tail_len = cpath.len - cpath.valid; + if (print_colored(cfile, err_color, tail, tail_len) != 0) { + return -1; } return 0; @@ -1052,8 +1169,18 @@ static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW /** Print a file name with colors. */ static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - const struct esc_seq *esc = file_color(cfile->colors, name, ftwbuf, flags); - return print_colored(cfile, esc, name, strlen(name)); + size_t len = strlen(name); + const struct cpath cpath = { + .path = name, + .nameoff = 0, + .valid = len, + .len = len, + .ftwbuf = ftwbuf, + .flags = flags, + }; + + const struct esc_seq *esc = file_color(cfile->colors, &cpath); + return print_colored(cfile, esc, name, cpath.len); } /** Print the name of a file with the appropriate colors. */ @@ -1110,9 +1237,36 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { } /** Format some colored output to the buffer. */ -attr(printf(2, 3)) +_printf(2, 3) static int cbuff(CFILE *cfile, const char *format, ...); +/** Print an expression's name, for diagnostics. */ +static int print_expr_name(CFILE *cfile, const struct bfs_expr *expr) { + switch (expr->kind) { + case BFS_FLAG: + return cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]); + case BFS_OPERATOR: + return cbuff(cfile, "${red}%pq${rs}", expr->argv[0]); + default: + return cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]); + } +} + +/** Print an expression's args, for diagnostics. */ +static int print_expr_args(CFILE *cfile, const struct bfs_expr *expr) { + if (print_expr_name(cfile, expr) != 0) { + return -1; + } + + for (size_t i = 1; i < expr->argc; ++i) { + if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { + return -1; + } + } + + return 0; +} + /** Dump a parsed expression tree, for debugging. */ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) { if (depth >= 2) { @@ -1127,20 +1281,8 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i return -1; } - if (bfs_expr_is_parent(expr)) { - if (cbuff(cfile, "${red}%pq${rs}", expr->argv[0]) < 0) { - return -1; - } - } else { - if (cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]) < 0) { - return -1; - } - } - - for (size_t i = 1; i < expr->argc; ++i) { - if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { - return -1; - } + if (print_expr_args(cfile, expr) != 0) { + return -1; } if (verbose) { @@ -1156,7 +1298,7 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i } int count = 0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (dstrcat(&cfile->buffer, " ") != 0) { return -1; } @@ -1179,10 +1321,9 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i return 0; } -attr(printf(2, 0)) +_printf(2, 0) static int cvbuff(CFILE *cfile, const char *format, va_list args) { const struct colors *colors = cfile->colors; - int error = errno; // Color specifier (e.g. ${blu}) state struct esc_seq **esc; @@ -1192,7 +1333,7 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { for (const char *i = format; *i; ++i) { size_t verbatim = strcspn(i, "%$"); - if (dstrncat(&cfile->buffer, i, verbatim) != 0) { + if (dstrxcat(&cfile->buffer, i, verbatim) != 0) { return -1; } i += verbatim; @@ -1240,12 +1381,6 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { } break; - case 'm': - if (dstrcat(&cfile->buffer, xstrerror(error)) != 0) { - return -1; - } - break; - case 'p': switch (*++i) { case 'q': @@ -1287,6 +1422,16 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { return -1; } break; + case 'x': + if (print_expr_args(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; + case 'X': + if (print_expr_name(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; default: goto invalid; @@ -1379,7 +1524,7 @@ int cvfprintf(CFILE *cfile, const char *format, va_list args) { } } - dstresize(&cfile->buffer, 0); + dstrshrink(cfile->buffer, 0); return ret; } @@ -1390,3 +1535,14 @@ int cfprintf(CFILE *cfile, const char *format, ...) { va_end(args); return ret; } + +int cfreset(CFILE *cfile) { + const struct colors *colors = cfile->colors; + if (!colors) { + return 0; + } + + const struct esc_seq *esc = colors->endcode; + size_t ret = xwrite(cfile->fd, esc->seq, esc->len); + return ret == esc->len ? 0 : -1; +} diff --git a/src/color.h b/src/color.h index e3e7973..aac8b33 100644 --- a/src/color.h +++ b/src/color.h @@ -8,8 +8,9 @@ #ifndef BFS_COLOR_H #define BFS_COLOR_H -#include "config.h" +#include "bfs.h" #include "dstring.h" + #include <stdio.h> /** @@ -42,6 +43,8 @@ typedef struct CFILE { const struct colors *colors; /** A buffer for colored formatting. */ dchar *buffer; + /** Cached file descriptor number. */ + int fd; /** Whether the next ${rs} is actually necessary. */ bool need_reset; /** Whether to close the underlying stream. */ @@ -51,11 +54,11 @@ typedef struct CFILE { /** * Wrap an existing file into a colored stream. * - * @param file + * @file * The underlying file. - * @param colors + * @colors * The color table to use if file is a TTY. - * @param close + * @close * Whether to close the underlying stream when this stream is closed. * @return * A colored wrapper around file. @@ -65,7 +68,7 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close); /** * Close a colored file. * - * @param cfile + * @cfile * The colored file to close. * @return * 0 on success, -1 on failure. @@ -75,9 +78,9 @@ int cfclose(CFILE *cfile); /** * Colored, formatted output. * - * @param cfile + * @cfile * The colored stream to print to. - * @param format + * @format * A printf()-style format string, supporting these format specifiers: * * %c: A single character @@ -85,7 +88,6 @@ int cfclose(CFILE *cfile); * %g: A double * %s: A string * %zu: A size_t - * %m: strerror(errno) * %pq: A shell-escaped string, like bash's printf %q * %pQ: A TTY-escaped string. * %pF: A colored file name, from a const struct BFTW * argument @@ -93,19 +95,26 @@ int cfclose(CFILE *cfile); * %pL: A colored link target, from a const struct BFTW * argument * %pe: Dump a const struct bfs_expr *, for debugging. * %pE: Dump a const struct bfs_expr * in verbose form, for debugging. + * %px: Print a const struct bfs_expr * with syntax highlighting. + * %pX: Print the name of a const struct bfs_expr *, without arguments. * %%: A literal '%' * ${cc}: Change the color to 'cc' * $$: A literal '$' * @return * 0 on success, -1 on failure. */ -attr(printf(2, 3)) +_printf(2, 3) int cfprintf(CFILE *cfile, const char *format, ...); /** * cfprintf() variant that takes a va_list. */ -attr(printf(2, 0)) +_printf(2, 0) int cvfprintf(CFILE *cfile, const char *format, va_list args); +/** + * Reset the TTY state when terminating abnormally (async-signal-safe). + */ +int cfreset(CFILE *cfile); + #endif // BFS_COLOR_H diff --git a/src/config.h b/src/config.h deleted file mode 100644 index 892f24c..0000000 --- a/src/config.h +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright © Tavian Barnes <tavianator@tavianator.com> -// SPDX-License-Identifier: 0BSD - -/** - * Configuration and feature/platform detection. - */ - -#ifndef BFS_CONFIG_H -#define BFS_CONFIG_H - -// Possible __STDC_VERSION__ values - -#define C95 199409L -#define C99 199901L -#define C11 201112L -#define C17 201710L -#define C23 202311L - -#include <stddef.h> - -#if __STDC_VERSION__ < C23 -# include <stdalign.h> -# include <stdbool.h> -# include <stdnoreturn.h> -#endif - -// bfs packaging configuration - -#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 - -// Check for system headers - -#ifdef __has_include - -#if __has_include(<mntent.h>) -# define BFS_HAS_MNTENT_H true -#endif -#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 -#if __has_include(<sys/mkdev.h>) -# define BFS_HAS_SYS_MKDEV_H true -#endif -#if __has_include(<sys/param.h>) -# define BFS_HAS_SYS_PARAM_H true -#endif -#if __has_include(<sys/sysmacros.h>) -# define BFS_HAS_SYS_SYSMACROS_H true -#endif -#if __has_include(<sys/xattr.h>) -# define BFS_HAS_SYS_XATTR_H true -#endif -#if __has_include(<threads.h>) -# define BFS_HAS_THREADS_H true -#endif -#if __has_include(<util.h>) -# define BFS_HAS_UTIL_H true -#endif - -#else // !__has_include - -#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 -#define BFS_HAS_SYS_SYSMACROS_H __GLIBC__ -#define BFS_HAS_SYS_XATTR_H __linux__ -#define BFS_HAS_THREADS_H (!__STDC_NO_THREADS__) -#define BFS_HAS_UTIL_H __NetBSD__ - -#endif // !__has_include - -#ifndef BFS_USE_MNTENT_H -# define BFS_USE_MNTENT_H BFS_HAS_MNTENT_H -#endif -#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__) -#endif -#ifndef BFS_USE_SYS_MKDEV_H -# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H -#endif -#ifndef BFS_USE_SYS_PARAM_H -# define BFS_USE_SYS_PARAM_H BFS_HAS_SYS_PARAM_H -#endif -#ifndef BFS_USE_SYS_SYSMACROS_H -# define BFS_USE_SYS_SYSMACROS_H BFS_HAS_SYS_SYSMACROS_H -#endif -#ifndef BFS_USE_SYS_XATTR_H -# define BFS_USE_SYS_XATTR_H BFS_HAS_SYS_XATTR_H -#endif -#ifndef BFS_USE_THREADS_H -# define BFS_USE_THREADS_H BFS_HAS_THREADS_H -#endif -#ifndef BFS_USE_UTIL_H -# define BFS_USE_UTIL_H BFS_HAS_UTIL_H -#endif - -// Stub out feature detection on old/incompatible compilers - -#ifndef __has_feature -# define __has_feature(feat) false -#endif - -#ifndef __has_c_attribute -# define __has_c_attribute(attr) false -#endif - -#ifndef __has_attribute -# 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 - -/** - * Get the length of an array. - */ -#define countof(array) (sizeof(array) / sizeof(0[array])) - -/** - * False sharing/destructive interference/largest cache line size. - */ -#ifdef __GCC_DESTRUCTIVE_SIZE -# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE -#else -# define FALSE_SHARING_SIZE 64 -#endif - -/** - * True sharing/constructive interference/smallest cache line size. - */ -#ifdef __GCC_CONSTRUCTIVE_SIZE -# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE -#else -# define TRUE_SHARING_SIZE 64 -#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 - -/** - * Silence warnings about switch/case fall-throughs. - */ -#if __has_attribute(fallthrough) -# define fallthru __attribute__((fallthrough)) -#else -# define fallthru ((void)0) -#endif - -/** - * Silence warnings about unused declarations. - */ -#if __has_attribute(unused) -# define attr_maybe_unused __attribute__((unused)) -#else -# define attr_maybe_unused -#endif - -/** - * Warn if a value is unused. - */ -#if __has_attribute(warn_unused_result) -# define attr_nodiscard __attribute__((warn_unused_result)) -#else -# define attr_nodiscard -#endif - -/** - * Hint to avoid inlining a function. - */ -#if __has_attribute(noinline) -# define attr_noinline __attribute__((noinline)) -#else -# define attr_noinline -#endif - -/** - * Hint that a function is unlikely to be called. - */ -#if __has_attribute(cold) -# define attr_cold attr_noinline __attribute__((cold)) -#else -# define attr_cold attr_noinline -#endif - -/** - * Adds compiler warnings for bad printf()-style function calls, if supported. - */ -#if __has_attribute(format) -# define attr_printf(fmt, args) __attribute__((format(printf, fmt, args))) -#else -# define attr_printf(fmt, args) -#endif - -/** - * Annotates allocator-like functions. - */ -#if __has_attribute(malloc) -# if __GNUC__ >= 11 -# define attr_malloc(...) attr_nodiscard __attribute__((malloc(__VA_ARGS__))) -# else -# define attr_malloc(...) attr_nodiscard __attribute__((malloc)) -# endif -#else -# define attr_malloc(...) attr_nodiscard -#endif - -/** - * Specifies that a function returns allocations with a given alignment. - */ -#if __has_attribute(alloc_align) -# define attr_alloc_align(param) __attribute__((alloc_align(param))) -#else -# define attr_alloc_align(param) -#endif - -/** - * Specifies that a function returns allocations with a given size. - */ -#if __has_attribute(alloc_size) -# define attr_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__))) -#else -# define attr_alloc_size(...) -#endif - -/** - * Shorthand for attr_alloc_align() and attr_alloc_size(). - */ -#define attr_aligned_alloc(align, ...) \ - attr_alloc_align(align) \ - attr_alloc_size(__VA_ARGS__) - -/** - * Check if function multiversioning via GNU indirect functions (ifunc) is supported. - */ -#ifndef BFS_USE_TARGET_CLONES -# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) -# define BFS_USE_TARGET_CLONES true -# endif -#endif - -/** - * Apply the target_clones attribute, if available. - */ -#if BFS_USE_TARGET_CLONES -# define attr_target_clones(...) __attribute__((target_clones(__VA_ARGS__))) -#else -# define attr_target_clones(...) -#endif - -/** - * Shorthand for multiple attributes at once. attr(a, b(c), d) is equivalent to - * - * attr_a - * attr_b(c) - * attr_d - */ -#define attr(...) \ - attr__(attr_##__VA_ARGS__, none, none, none, none, none, none, none, none, none, ) - -/** - * attr() helper. For exposition, pretend we support only 2 args, instead of 9. - * There are a few cases: - * - * attr() - * => attr__(attr_, none, none) - * => attr_ => - * attr_none => - * attr_too_many_none() => - * - * attr(a) - * => attr__(attr_a, none, none) - * => attr_a => __attribute__((a)) - * attr_none => - * attr_too_many_none() => - * - * attr(a, b(c)) - * => attr__(attr_a, b(c), none, none) - * => attr_a => __attribute__((a)) - * attr_b(c) => __attribute__((b(c))) - * attr_too_many_none(none) => - * - * attr(a, b(c), d) - * => attr__(attr_a, b(c), d, none, none) - * => attr_a => __attribute__((a)) - * attr_b(c) => __attribute__((b(c))) - * attr_too_many_d(none, none) => error - * - * Some attribute names are the same as standard library functions, e.g. printf. - * Standard libraries are permitted to define these functions as macros, like - * - * #define printf(...) __builtin_printf(__VA_ARGS__) - * - * The token paste in - * - * #define attr(...) attr__(attr_##__VA_ARGS__, none, none) - * - * is necessary to prevent macro expansion before evaluating attr__(). - * Otherwise, we could get - * - * attr(printf(1, 2)) - * => attr__(__builtin_printf(1, 2), none, none) - * => attr____builtin_printf(1, 2) - * => error - */ -#define attr__(a1, a2, a3, a4, a5, a6, a7, a8, a9, none, ...) \ - a1 \ - attr_##a2 \ - attr_##a3 \ - attr_##a4 \ - attr_##a5 \ - attr_##a6 \ - attr_##a7 \ - attr_##a8 \ - attr_##a9 \ - attr_too_many_##none(__VA_ARGS__) - -// Ignore `attr_none` from expanding 1-9 argument attr(a1, a2, ...) -#define attr_none -// Ignore `attr_` from expanding 0-argument attr() -#define attr_ -// Only trigger an error on more than 9 arguments -#define attr_too_many_none(...) - -#endif // BFS_CONFIG_H @@ -2,36 +2,28 @@ // SPDX-License-Identifier: 0BSD #include "ctx.h" + #include "alloc.h" +#include "bfstd.h" #include "color.h" #include "diag.h" #include "expr.h" #include "list.h" #include "mtab.h" #include "pwcache.h" +#include "sighook.h" #include "stat.h" #include "trie.h" -#include "xtime.h" + #include <errno.h> #include <limits.h> +#include <signal.h> #include <stdio.h> #include <stdlib.h> +#include <sys/stat.h> +#include <time.h> #include <unistd.h> -/** Get the initial value for ctx->threads (-j). */ -static int bfs_nproc(void) { - long nproc = sysconf(_SC_NPROCESSORS_ONLN); - - if (nproc < 1) { - nproc = 1; - } else if (nproc > 8) { - // Not much speedup after 8 threads - nproc = 8; - } - - return nproc; -} - struct bfs_ctx *bfs_ctx_new(void) { struct bfs_ctx *ctx = ZALLOC(struct bfs_ctx); if (!ctx) { @@ -44,15 +36,24 @@ struct bfs_ctx *bfs_ctx_new(void) { ctx->maxdepth = INT_MAX; ctx->flags = BFTW_RECOVER; ctx->strategy = BFTW_BFS; - ctx->threads = bfs_nproc(); ctx->optlevel = 3; + ctx->threads = nproc(); + if (ctx->threads > 8) { + // Not much speedup after 8 threads + ctx->threads = 8; + } + trie_init(&ctx->files); + ctx->umask = umask(0); + umask(ctx->umask); + if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) { goto fail; } ctx->cur_nofile = ctx->orig_nofile; + ctx->raise_nofile = true; ctx->users = bfs_users_new(); if (!ctx->users) { @@ -64,7 +65,7 @@ struct bfs_ctx *bfs_ctx_new(void) { goto fail; } - if (xgettime(&ctx->now) != 0) { + if (clock_gettime(CLOCK_REALTIME, &ctx->now) != 0) { goto fail; } @@ -98,13 +99,20 @@ struct bfs_ctx_file { CFILE *cfile; /** The path to the file (for diagnostics). */ const char *path; + /** Signal hook to send a reset escape sequence. */ + struct sighook *hook; /** Remembers I/O errors, to propagate them to the exit status. */ int error; }; +/** Call cfreset() on a tracked file. */ +static void cfreset_hook(int sig, siginfo_t *info, void *arg) { + cfreset(arg); +} + CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) { struct bfs_stat sb; - if (bfs_stat(fileno(cfile->file), NULL, 0, &sb) != 0) { + if (bfs_stat(cfile->fd, NULL, 0, &sb) != 0) { return NULL; } @@ -124,19 +132,31 @@ CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) { leaf->value = ctx_file = ALLOC(struct bfs_ctx_file); if (!ctx_file) { - trie_remove(&ctx->files, leaf); - return NULL; + goto fail; } ctx_file->cfile = cfile; ctx_file->path = path; ctx_file->error = 0; + ctx_file->hook = NULL; + + if (cfile->colors) { + ctx_file->hook = atsigexit(cfreset_hook, cfile); + if (!ctx_file->hook) { + goto fail; + } + } if (cfile != ctx->cout && cfile != ctx->cerr) { ++ctx->nfiles; } return cfile; + +fail: + trie_remove(&ctx->files, leaf); + free(ctx_file); + return NULL; } void bfs_ctx_flush(const struct bfs_ctx *ctx) { @@ -156,9 +176,9 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx) { const char *path = ctx_file->path; if (path) { - bfs_error(ctx, "'%s': %m.\n", path); + bfs_error(ctx, "%pq: %s.\n", path, errstr()); } else if (cfile == ctx->cout) { - bfs_error(ctx, "(standard output): %m.\n"); + bfs_error(ctx, "(standard output): %s.\n", errstr()); } } @@ -188,30 +208,47 @@ static int bfs_ctx_fflush(CFILE *cfile) { static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) { CFILE *cfile = ctx_file->cfile; - if (cfile == ctx->cout) { - // Will be checked later - return 0; - } else if (cfile == ctx->cerr) { - // Writes to stderr are allowed to fail silently, unless the same file was used by - // -fprint, -fls, etc. - if (ctx_file->path) { - return bfs_ctx_fflush(cfile); - } else { - return 0; - } - } - + // Writes to stderr are allowed to fail silently, unless the same file + // was used by -fprint, -fls, etc. + bool silent = cfile == ctx->cerr && !ctx_file->path; int ret = 0, error = 0; - if (ferror(cfile->file)) { + + if (ctx_file->error) { + // An error was previously reported during bfs_ctx_flush() ret = -1; - error = EIO; + error = ctx_file->error; } - if (cfclose(cfile) != 0) { + + // Flush the file just before we remove the hook, to maximize the chance + // we leave the TTY in a good state + if (bfs_ctx_fflush(cfile) != 0) { ret = -1; error = errno; } - errno = error; + sigunhook(ctx_file->hook); + + // Close the CFILE, except for stdio streams, which are closed later + if (cfile != ctx->cout && cfile != ctx->cerr) { + if (cfclose(cfile) != 0) { + ret = -1; + error = errno; + } + } + + if (silent) { + ret = 0; + } + + if (ret != 0 && ctx->cerr) { + if (ctx_file->path) { + bfs_error(ctx, "%pq: %s.\n", ctx_file->path, xstrerror(error)); + } else if (cfile == ctx->cout) { + bfs_error(ctx, "(standard output): %s.\n", xstrerror(error)); + } + } + + free(ctx_file); return ret; } @@ -229,33 +266,14 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { for_trie (leaf, &ctx->files) { struct bfs_ctx_file *ctx_file = leaf->value; - - if (ctx_file->error) { - // An error was previously reported during bfs_ctx_flush() - ret = -1; - } - if (bfs_ctx_fclose(ctx, ctx_file) != 0) { - if (cerr) { - bfs_error(ctx, "'%s': %m.\n", ctx_file->path); - } ret = -1; } - - free(ctx_file); } trie_destroy(&ctx->files); - if (cout && bfs_ctx_fflush(cout) != 0) { - if (cerr) { - bfs_error(ctx, "(standard output): %m.\n"); - } - ret = -1; - } - cfclose(cout); cfclose(cerr); - free_colors(ctx->colors); for_slist (struct bfs_expr, expr, &ctx->expr_list, freelist) { @@ -268,6 +286,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { } free(ctx->paths); + free(ctx->kinds); free(ctx->argv); free(ctx); } @@ -10,12 +10,13 @@ #include "alloc.h" #include "bftw.h" -#include "config.h" #include "diag.h" #include "expr.h" #include "trie.h" + #include <stddef.h> #include <sys/resource.h> +#include <sys/types.h> #include <time.h> struct CFILE; @@ -28,6 +29,8 @@ struct bfs_ctx { size_t argc; /** The unparsed command line arguments. */ char **argv; + /** The argument token kinds. */ + enum bfs_kind *kinds; /** The root paths. */ const char **paths; @@ -67,11 +70,18 @@ struct bfs_ctx { bool status; /** Whether to only return unique files (-unique). */ bool unique; - /** Whether to print warnings (-warn/-nowarn). */ - bool warn; /** Whether to only handle paths with xargs-safe characters (-X). */ bool xargs_safe; + /** Whether bfs was run interactively. */ + bool interactive; + /** Whether to print warnings (-warn/-nowarn). */ + bool warn; + /** Whether to report errors (-noerror). */ + bool ignore_errors; + /** Whether any dangerous actions (-delete/-exec) are present. */ + bool dangerous; + /** Color data. */ struct colors *colors; /** The error that occurred parsing the color table, if any. */ @@ -98,10 +108,15 @@ struct bfs_ctx { /** The number of files owned by the context. */ int nfiles; + /** The current file creation mask. */ + mode_t umask; + /** The initial RLIMIT_NOFILE limits. */ struct rlimit orig_nofile; /** The current RLIMIT_NOFILE limits. */ struct rlimit cur_nofile; + /** Whether the fd limit should be raised. */ + bool raise_nofile; /** The current time. */ struct timespec now; @@ -116,7 +131,7 @@ struct bfs_ctx *bfs_ctx_new(void); /** * Get the mount table. * - * @param ctx + * @ctx * The bfs context. * @return * The cached mount table, or NULL on failure. @@ -126,11 +141,11 @@ const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx); /** * Deduplicate an opened file. * - * @param ctx + * @ctx * The bfs context. - * @param cfile + * @cfile * The opened file. - * @param path + * @path * The path to the opened file (or NULL for standard streams). * @return * If the same file was opened previously, that file is returned. If cfile is a new file, @@ -141,7 +156,7 @@ struct CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, struct CFILE *cfile, const char /** * Flush any caches for consistency with external processes. * - * @param ctx + * @ctx * The bfs context. */ void bfs_ctx_flush(const struct bfs_ctx *ctx); @@ -149,9 +164,9 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx); /** * Dump the parsed command line. * - * @param ctx + * @ctx * The bfs context. - * @param flag + * @flag * The -D flag that triggered the dump. */ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag); @@ -159,7 +174,7 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag); /** * Free a bfs context. * - * @param ctx + * @ctx * The context to free. * @return * 0 on success, -1 if any errors occurred. @@ -2,37 +2,42 @@ // SPDX-License-Identifier: 0BSD #include "diag.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "dstring.h" #include "expr.h" -#include <errno.h> + #include <stdarg.h> #include <stdio.h> #include <stdlib.h> - -/** bfs_diagf() implementation. */ -attr(printf(2, 0)) -static void bfs_vdiagf(const struct bfs_loc *loc, const char *format, va_list args) { - fprintf(stderr, "%s: %s@%s:%d: ", xgetprogname(), loc->func, loc->file, loc->line); - vfprintf(stderr, format, args); - fprintf(stderr, "\n"); -} - -void bfs_diagf(const struct bfs_loc *loc, const char *format, ...) { +#include <unistd.h> + +/** + * Print an error using dprintf() if possible, because it's more likely to be + * async-signal-safe in practice. + */ +#if BFS_HAS_DPRINTF +# define veprintf(...) vdprintf(STDERR_FILENO, __VA_ARGS__) +#else +# define veprintf(...) vfprintf(stderr, __VA_ARGS__) +#endif + +void bfs_diagf(const char *format, ...) { va_list args; va_start(args, format); - bfs_vdiagf(loc, format, args); + veprintf(format, args); va_end(args); } -noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...) { +_noreturn +void bfs_abortf(const char *format, ...) { va_list args; va_start(args, format); - bfs_vdiagf(loc, format, args); + veprintf(format, args); va_end(args); abort(); @@ -64,7 +69,7 @@ const char *debug_flag_name(enum debug_flags flag) { } void bfs_perror(const struct bfs_ctx *ctx, const char *str) { - bfs_error(ctx, "%s: %m.\n", str); + bfs_error(ctx, "%s: %s.\n", str, errstr()); } void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) { @@ -91,19 +96,12 @@ bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *for } void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args) { - int error = errno; - bfs_error_prefix(ctx); - - errno = error; cvfprintf(ctx->cerr, format, args); } bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) { - int error = errno; - if (bfs_warning_prefix(ctx)) { - errno = error; cvfprintf(ctx->cerr, format, args); return true; } else { @@ -112,10 +110,7 @@ bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) { } bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args) { - int error = errno; - if (bfs_debug_prefix(ctx, flag)) { - errno = error; cvfprintf(ctx->cerr, format, args); return true; } else { @@ -169,7 +164,7 @@ static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs } } - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { ret |= highlight_expr_recursive(ctx, child, args); } @@ -8,92 +8,134 @@ #ifndef BFS_DIAG_H #define BFS_DIAG_H -#include "config.h" +#include "bfs.h" +#include "bfstd.h" + #include <stdarg.h> /** - * static_assert() with an optional second argument. + * Wrap a diagnostic format string so it looks like + * + * bfs: func@src/file.c:0: Message */ -#if __STDC_VERSION__ >= C23 -# define bfs_static_assert static_assert -#else -# define bfs_static_assert(...) bfs_static_assert_(__VA_ARGS__, #__VA_ARGS__, ) -# define bfs_static_assert_(expr, msg, ...) _Static_assert(expr, msg) -#endif +#define BFS_DIAG_FORMAT_(format) \ + ((format) ? "%s: %s@%s:%d: " format "%s" : "") /** - * A source code location. + * Add arguments to match a BFS_DIAG_FORMAT string. */ -struct bfs_loc { - const char *file; - int line; - const char *func; -}; - -#define BFS_LOC_INIT { .file = __FILE__, .line = __LINE__, .func = __func__ } +#define BFS_DIAG_ARGS_(...) \ + xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ "\n" /** - * Get the current source code location. + * Print a low-level diagnostic message to standard error. */ -#if __STDC_VERSION__ >= C23 -# define bfs_location() (&(static const struct bfs_loc)BFS_LOC_INIT) -#else -# define bfs_location() (&(const struct bfs_loc)BFS_LOC_INIT) -#endif +_printf(1, 2) +void bfs_diagf(const char *format, ...); /** - * Print a low-level diagnostic message to standard error, formatted like - * - * bfs: func@src/file.c:0: Message + * Unconditional diagnostic message. */ -attr(printf(2, 3)) -void bfs_diagf(const struct bfs_loc *loc, const char *format, ...); +#define bfs_diag(...) \ + bfs_diag_(__VA_ARGS__, ) + +#define bfs_diag_(format, ...) \ + bfs_diagf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__)) /** - * Unconditional diagnostic message. + * Print a diagnostic message including the last error. */ -#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__) +#define bfs_ediag(...) \ + bfs_ediag_(__VA_ARGS__, ) + +#define bfs_ediag_(format, ...) \ + bfs_diag_(format "%s%s", __VA_ARGS__ (sizeof("" format) > 1 ? ": " : ""), errstr(), ) /** * Print a message to standard error and abort. */ -attr(cold, printf(2, 3)) -noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...); +_cold +_printf(1, 2) +_noreturn +void bfs_abortf(const char *format, ...); /** * Unconditional abort with a message. */ -#define bfs_abort(...) bfs_abortf(bfs_location(), __VA_ARGS__) +#define bfs_abort(...) \ + bfs_abort_(__VA_ARGS__, ) + +#define bfs_abort_(format, ...) \ + bfs_abortf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__)) + +/** + * Abort with a message including the last error. + */ +#define bfs_eabort(...) \ + bfs_eabort_(__VA_ARGS__, ) + +#define bfs_eabort_(format, ...) \ + ((format) ? bfs_abort_(format ": %s", __VA_ARGS__ errstr(), ) : (void)0) /** * Abort in debug builds; no-op in release builds. */ #ifdef NDEBUG # define bfs_bug(...) ((void)0) +# define bfs_ebug(...) ((void)0) #else # define bfs_bug bfs_abort +# define bfs_ebug bfs_eabort #endif /** + * Get the default assertion message, if no format string was specified. + */ +#define BFS_DIAG_MSG_(format, str) \ + (sizeof(format) > 1 ? "" : str) + +/** * Unconditional assert. */ #define bfs_verify(...) \ - bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", "") + bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", ) #define bfs_verify_(str, cond, format, ...) \ - ((cond) ? (void)0 : bfs_abort( \ + ((cond) ? (void)0 : bfs_verify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__)) + +#define bfs_verify__(format, ...) \ + bfs_abortf( \ + sizeof(format) > 1 \ + ? BFS_DIAG_FORMAT_("%s" format "%s") \ + : BFS_DIAG_FORMAT_("Assertion failed: `%s`"), \ + BFS_DIAG_ARGS_(__VA_ARGS__)) + +/** + * Unconditional assert, including the last error. + */ +#define bfs_everify(...) \ + bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", ) + + +#define bfs_everify_(str, cond, format, ...) \ + ((cond) ? (void)0 : bfs_everify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__)) + +#define bfs_everify__(format, ...) \ + bfs_abortf( \ sizeof(format) > 1 \ - ? "%.0s" format "%s%s" \ - : "Assertion failed: `%s`%s", \ - str, __VA_ARGS__)) + ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \ + : BFS_DIAG_FORMAT_("Assertion failed: `%s`: %s"), \ + BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), )) /** * Assert in debug builds; no-op in release builds. */ #ifdef NDEBUG # define bfs_assert(...) ((void)0) +# define bfs_eassert(...) ((void)0) #else # define bfs_assert bfs_verify +# define bfs_eassert bfs_everify #endif struct bfs_ctx; @@ -129,13 +171,14 @@ const char *debug_flag_name(enum debug_flags flag); /** * Like perror(), but decorated like bfs_error(). */ -attr(cold) +_cold void bfs_perror(const struct bfs_ctx *ctx, const char *str); /** * Shorthand for printing error messages. */ -attr(cold, printf(2, 3)) +_cold +_printf(2, 3) void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -143,7 +186,8 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a warning was printed. */ -attr(cold, printf(2, 3)) +_cold +_printf(2, 3) bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -151,67 +195,71 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a debug message was printed. */ -attr(cold, printf(3, 4)) +_cold +_printf(3, 4) bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...); /** * bfs_error() variant that takes a va_list. */ -attr(cold, printf(2, 0)) +_cold +_printf(2, 0) void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_warning() variant that takes a va_list. */ -attr(cold, printf(2, 0)) +_cold +_printf(2, 0) bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_debug() variant that takes a va_list. */ -attr(cold, printf(3, 0)) +_cold +_printf(3, 0) bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args); /** * Print the error message prefix. */ -attr(cold) +_cold void bfs_error_prefix(const struct bfs_ctx *ctx); /** * Print the warning message prefix. */ -attr(cold) +_cold bool bfs_warning_prefix(const struct bfs_ctx *ctx); /** * Print the debug message prefix. */ -attr(cold) +_cold bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag); /** * Highlight parts of the command line in an error message. */ -attr(cold) +_cold void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in an error message. */ -attr(cold) +_cold void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr); /** * Highlight parts of the command line in a warning message. */ -attr(cold) +_cold bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in a warning message. */ -attr(cold) +_cold bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr); #endif // BFS_DIAG_H @@ -2,12 +2,14 @@ // SPDX-License-Identifier: 0BSD #include "dir.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "diag.h" #include "sanity.h" #include "trie.h" + #include <dirent.h> #include <errno.h> #include <fcntl.h> @@ -17,7 +19,7 @@ #include <unistd.h> #if BFS_USE_GETDENTS -# if __linux__ +# if BFS_HAS_GETDENTS64_SYSCALL # include <sys/syscall.h> # endif @@ -25,12 +27,20 @@ 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_POSIX_GETDENTS + int flags = 0; +# ifdef DT_FORCE_TYPE + flags |= DT_FORCE_TYPE; +# endif + ssize_t ret = posix_getdents(fd, buf, size, flags); +#elif 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,11 +52,13 @@ static ssize_t bfs_getdents(int fd, void *buf, size_t size) { #endif // BFS_USE_GETDENTS -#if BFS_USE_GETDENTS && __linux__ /** Directory entry type for bfs_getdents() */ -typedef struct dirent64 sys_dirent; -#else +#if !BFS_USE_GETDENTS || BFS_HAS_GETDENTS typedef struct dirent sys_dirent; +#elif BFS_HAS_POSIX_GETDENTS +typedef struct posix_dent sys_dirent; +#else +typedef struct dirent64 sys_dirent; #endif enum bfs_type bfs_mode_to_type(mode_t mode) { @@ -351,7 +363,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 @@ -8,6 +8,8 @@ #ifndef BFS_DIR_H #define BFS_DIR_H +#include "bfs.h" + #include <sys/types.h> /** @@ -15,7 +17,13 @@ * libc's readdir(). */ #ifndef BFS_USE_GETDENTS -# define BFS_USE_GETDENTS (__linux__ || __FreeBSD__) +# if BFS_HAS_POSIX_GETDENTS +# define BFS_USE_GETDENTS true +# elif __linux__ || __FreeBSD__ +# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL) +# else +# define BFS_USE_GETDENTS false +# endif #endif /** @@ -81,7 +89,7 @@ struct arena; /** * Initialize an arena for directories. * - * @param arena + * @arena * The arena to initialize. */ void bfs_dir_arena(struct arena *arena); @@ -99,14 +107,14 @@ enum bfs_dir_flags { /** * Open a directory. * - * @param dir + * @dir * The allocated directory. - * @param at_fd + * @at_fd * The base directory for path resolution. - * @param at_path + * @at_path * The path of the directory to open, relative to at_fd. Pass NULL to * open at_fd itself. - * @param flags + * @flags * Flags that control which directory entries are listed. * @return * 0 on success, or -1 on failure. @@ -121,7 +129,7 @@ int bfs_dirfd(const struct bfs_dir *dir); /** * Performs any I/O necessary for the next bfs_readdir() call. * - * @param dir + * @dir * The directory to poll. * @return * 1 on success, 0 on EOF, or -1 on failure. @@ -131,9 +139,9 @@ int bfs_polldir(struct bfs_dir *dir); /** * Read a directory entry. * - * @param dir + * @dir * The directory to read. - * @param[out] dirent + * @dirent[out] * The directory entry to populate. * @return * 1 on success, 0 on EOF, or -1 on failure. @@ -152,14 +160,14 @@ 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 /** * Detach the file descriptor from an open directory. * - * @param dir + * @dir * The directory to detach. * @return * The file descriptor of the directory. diff --git a/src/dstring.c b/src/dstring.c index 10b0fad..678d685 100644 --- a/src/dstring.c +++ b/src/dstring.c @@ -2,10 +2,11 @@ // SPDX-License-Identifier: 0BSD #include "dstring.h" + #include "alloc.h" #include "bit.h" -#include "config.h" #include "diag.h" + #include <stdarg.h> #include <stddef.h> #include <stdint.h> @@ -22,7 +23,7 @@ struct dstring { /** Length of the string, *excluding* the terminating NUL. */ size_t len; /** The string itself. */ - alignas(dchar) char str[]; + alignas(dchar) char str[] _counted_by(cap); }; #define DSTR_OFFSET offsetof(struct dstring, str) @@ -45,6 +46,13 @@ static dchar *dstrdata(struct dstring *header) { return (char *)header + DSTR_OFFSET; } +/** Set the length of a dynamic string. */ +static void dstrsetlen(struct dstring *header, size_t len) { + bfs_assert(len < header->cap); + header->len = len; + header->str[len] = '\0'; +} + /** Allocate a dstring with the given contents. */ static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) { // Avoid reallocations for small strings @@ -58,11 +66,10 @@ static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) { } header->cap = cap; - header->len = len; + dstrsetlen(header, len); - char *ret = dstrdata(header); + dchar *ret = dstrdata(header); memcpy(ret, str, len); - ret[len] = '\0'; return ret; } @@ -120,11 +127,16 @@ int dstresize(dchar **dstr, size_t len) { } struct dstring *header = dstrheader(*dstr); - header->len = len; - header->str[len] = '\0'; + dstrsetlen(header, len); return 0; } +void dstrshrink(dchar *dstr, size_t len) { + struct dstring *header = dstrheader(dstr); + bfs_assert(len <= header->len); + dstrsetlen(header, len); +} + int dstrcat(dchar **dest, const char *src) { return dstrxcat(dest, src, strlen(src)); } @@ -174,7 +186,7 @@ int dstrxcpy(dchar **dest, const char *src, size_t len) { return 0; } -char *dstrprintf(const char *format, ...) { +dchar *dstrprintf(const char *format, ...) { va_list args; va_start(args, format); @@ -184,7 +196,7 @@ char *dstrprintf(const char *format, ...) { return str; } -char *dstrvprintf(const char *format, va_list args) { +dchar *dstrvprintf(const char *format, va_list args) { // Guess a capacity to try to avoid reallocating dchar *str = dstralloc(2 * strlen(format)); if (!str) { @@ -245,6 +257,7 @@ int dstrvcatf(dchar **str, const char *format, va_list args) { return 0; fail: + va_end(copy); *tail = '\0'; return -1; } @@ -276,3 +289,20 @@ void dstrfree(dchar *dstr) { free(dstrheader(dstr)); } } + +dchar *dstrepeat(const char *str, size_t n) { + size_t len = strlen(str); + dchar *ret = dstralloc(n * len); + if (!ret) { + return NULL; + } + + for (size_t i = 0; i < n; ++i) { + if (dstrxcat(&ret, str, len) < 0) { + dstrfree(ret); + return NULL; + } + } + + return ret; +} diff --git a/src/dstring.h b/src/dstring.h index 6006199..ce7ef86 100644 --- a/src/dstring.h +++ b/src/dstring.h @@ -8,8 +8,9 @@ #ifndef BFS_DSTRING_H #define BFS_DSTRING_H +#include "bfs.h" #include "bfstd.h" -#include "config.h" + #include <stdarg.h> #include <stddef.h> @@ -30,7 +31,7 @@ typedef char dchar; /** * Free a dynamic string. * - * @param dstr + * @dstr * The string to free. */ void dstrfree(dchar *dstr); @@ -38,56 +39,56 @@ void dstrfree(dchar *dstr); /** * Allocate a dynamic string. * - * @param cap + * @cap * The initial capacity of the string. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstralloc(size_t cap); /** * Create a dynamic copy of a string. * - * @param str + * @str * The NUL-terminated string to copy. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrdup(const char *str); /** * Create a length-limited dynamic copy of a string. * - * @param str + * @str * The string to copy. - * @param n + * @n * The maximum number of characters to copy from str. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrndup(const char *str, size_t n); /** * Create a dynamic copy of a dynamic string. * - * @param dstr + * @dstr * The dynamic string to copy. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrddup(const dchar *dstr); /** * Create an exact-sized dynamic copy of a string. * - * @param str + * @str * The string to copy. - * @param len + * @len * The length of the string, which may include internal NUL bytes. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrxdup(const char *str, size_t len); /** * Get a dynamic string's length. * - * @param dstr + * @dstr * The string to measure. * @return * The length of dstr. @@ -97,9 +98,9 @@ size_t dstrlen(const dchar *dstr); /** * Reserve some capacity in a dynamic string. * - * @param dstr + * @dstr * The dynamic string to preallocate. - * @param cap + * @cap * The new capacity for the string. * @return * 0 on success, -1 on failure. @@ -109,214 +110,246 @@ int dstreserve(dchar **dstr, size_t cap); /** * Resize a dynamic string. * - * @param dstr + * @dstr * The dynamic string to resize. - * @param len + * @len * The new length for the dynamic string. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstresize(dchar **dstr, size_t len); /** + * Shrink a dynamic string. + * + * @dstr + * The dynamic string to shrink. + * @len + * The new length. Must not be greater than the current length. + */ +void dstrshrink(dchar *dstr, size_t len); + +/** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. * @return 0 on success, -1 on failure. */ +_nodiscard int dstrcat(dchar **dest, const char *src); /** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. - * @param n + * @n * The maximum number of characters to take from src. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrncat(dchar **dest, const char *src, size_t n); /** * Append a dynamic string to another dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to append. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrdcat(dchar **dest, const dchar *src); /** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. - * @param len + * @len * The exact number of characters to take from src. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrxcat(dchar **dest, const char *src, size_t len); /** * Append a single character to a dynamic string. * - * @param str + * @str * The string to append to. - * @param c + * @c * The character to append. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrapp(dchar **str, char c); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to copy. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrcpy(dchar **dest, const char *str); /** * Copy a dynamic string into another one. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrdcpy(dchar **dest, const dchar *str); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. - * @param n + * @n * The maximum number of characters to take from src. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrncpy(dchar **dest, const char *str, size_t n); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. - * @param len + * @len * The exact number of characters to take from src. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrxcpy(dchar **dest, const char *str, size_t len); /** * Create a dynamic string from a format string. * - * @param format + * @format * The format string to fill in. - * @param ... + * @... * Any arguments for the format string. * @return * The created string, or NULL on failure. */ -attr(printf(1, 2)) -char *dstrprintf(const char *format, ...); +_nodiscard +_printf(1, 2) +dchar *dstrprintf(const char *format, ...); /** * Create a dynamic string from a format string and a va_list. * - * @param format + * @format * The format string to fill in. - * @param args + * @args * The arguments for the format string. * @return * The created string, or NULL on failure. */ -attr(printf(1, 0)) -char *dstrvprintf(const char *format, va_list args); +_nodiscard +_printf(1, 0) +dchar *dstrvprintf(const char *format, va_list args); /** * Format some text onto the end of a dynamic string. * - * @param str + * @str * The destination dynamic string. - * @param format + * @format * The format string to fill in. - * @param ... + * @... * Any arguments for the format string. * @return * 0 on success, -1 on failure. */ -attr(printf(2, 3)) +_nodiscard +_printf(2, 3) int dstrcatf(dchar **str, const char *format, ...); /** * Format some text from a va_list onto the end of a dynamic string. * - * @param str + * @str * The destination dynamic string. - * @param format + * @format * The format string to fill in. - * @param args + * @args * The arguments for the format string. * @return * 0 on success, -1 on failure. */ -attr(printf(2, 0)) +_nodiscard +_printf(2, 0) int dstrvcatf(dchar **str, const char *format, va_list args); /** * Concatenate while shell-escaping. * - * @param dest + * @dest * The destination dynamic string. - * @param str + * @str * The string to escape. - * @param flags + * @flags * Flags for wordesc(). * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrescat(dchar **dest, const char *str, enum wesc_flags flags); /** * Concatenate while shell-escaping. * - * @param dest + * @dest * The destination dynamic string. - * @param str + * @str * The string to escape. - * @param n + * @n * The maximum length of the string. - * @param flags + * @flags * Flags for wordesc(). * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags); +/** + * Repeat a string n times. + */ +_nodiscard +dchar *dstrepeat(const char *str, size_t n); + #endif // BFS_DSTRING_H @@ -6,11 +6,13 @@ */ #include "eval.h" + +#include "atomic.h" #include "bar.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "diag.h" #include "dir.h" @@ -22,14 +24,18 @@ #include "printf.h" #include "pwcache.h" #include "sanity.h" +#include "sighook.h" #include "stat.h" #include "trie.h" #include "xregex.h" +#include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <fnmatch.h> #include <grp.h> #include <pwd.h> +#include <signal.h> #include <stdarg.h> #include <stdint.h> #include <stdio.h> @@ -37,6 +43,7 @@ #include <string.h> #include <strings.h> #include <sys/resource.h> +#include <sys/time.h> #include <sys/types.h> #include <time.h> #include <unistd.h> @@ -51,6 +58,8 @@ struct bfs_eval { enum bftw_action action; /** The bfs_eval() return value. */ int *ret; + /** The number of errors that have occurred. */ + size_t *nerrors; /** Whether to quit immediately. */ bool quit; }; @@ -58,20 +67,24 @@ struct bfs_eval { /** * Print an error message. */ -attr(printf(2, 3)) +_printf(2, 3) static void eval_error(struct bfs_eval *state, const char *format, ...) { + const struct bfs_ctx *ctx = state->ctx; + + ++*state->nerrors; + if (ctx->ignore_errors) { + return; + } + // By POSIX, any errors should be accompanied by a non-zero exit status *state->ret = EXIT_FAILURE; - int error = errno; - const struct bfs_ctx *ctx = state->ctx; CFILE *cerr = ctx->cerr; bfs_error(ctx, "%pP: ", state->ftwbuf); va_list args; va_start(args, format); - errno = error; cvfprintf(cerr, format, args); va_end(args); } @@ -90,7 +103,7 @@ static bool eval_should_ignore(const struct bfs_eval *state, int error) { */ static void eval_report_error(struct bfs_eval *state) { if (!eval_should_ignore(state, errno)) { - eval_error(state, "%m.\n"); + eval_error(state, "%s.\n", errstr()); } } @@ -99,9 +112,9 @@ static void eval_report_error(struct bfs_eval *state) { */ static void eval_io_error(const struct bfs_expr *expr, struct bfs_eval *state) { if (expr->path) { - eval_error(state, "'%s': %m.\n", expr->path); + eval_error(state, "'%s': %s.\n", expr->path, errstr()); } else { - eval_error(state, "(standard output): %m.\n"); + eval_error(state, "(standard output): %s.\n", errstr()); } // Don't report the error again in bfs_ctx_free() @@ -124,11 +137,9 @@ static const struct bfs_stat *eval_stat(struct bfs_eval *state) { * Get the difference (in seconds) between two struct timespecs. */ static time_t timespec_diff(const struct timespec *lhs, const struct timespec *rhs) { - time_t ret = lhs->tv_sec - rhs->tv_sec; - if (lhs->tv_nsec < rhs->tv_nsec) { - --ret; - } - return ret; + struct timespec diff = *lhs; + timespec_sub(&diff, rhs); + return diff.tv_sec; } bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) { @@ -145,6 +156,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,12 +219,27 @@ 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) { const struct timespec *ret = bfs_stat_time(statbuf, field); if (!ret) { - eval_error(state, "Couldn't get file %s: %m.\n", bfs_stat_field_name(field)); + eval_error(state, "Couldn't get file %s: %s.\n", bfs_stat_field_name(field), errstr()); } return ret; } @@ -218,8 +258,7 @@ bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - return time->tv_sec > expr->reftime.tv_sec - || (time->tv_sec == expr->reftime.tv_sec && time->tv_nsec > expr->reftime.tv_nsec); + return timespec_cmp(time, &expr->reftime) > 0; } /** @@ -240,10 +279,10 @@ bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) { switch (expr->time_unit) { case BFS_DAYS: diff /= 60 * 24; - fallthru; + _fallthrough; case BFS_MINUTES: diff /= 60; - fallthru; + _fallthrough; case BFS_SECONDS: break; } @@ -369,13 +408,13 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c if (expr->eval_fn == eval_exec) { if (bfs_exec_finish(expr->exec) != 0) { if (errno != 0) { - bfs_error(ctx, "%s %s: %m.\n", expr->argv[0], expr->argv[1]); + bfs_error(ctx, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } ret = -1; } } - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (eval_exec_finish(child, ctx) != 0) { ret = -1; } @@ -390,7 +429,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) { bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0; if (errno != 0) { - eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]); + eval_error(state, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } return ret; } @@ -416,38 +455,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; + + switch (ftwbuf->type) { + case BFS_REG: + statbuf = eval_stat(state); + return statbuf && statbuf->size == 0; - if (ftwbuf->type == BFS_DIR) { - struct bfs_dir *dir = bfs_allocdir(); + 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 +589,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. */ @@ -671,6 +700,34 @@ static int print_owner(FILE *file, const char *name, uintmax_t id, int *width) { } } +/** Print a file's modification time. */ +static int print_time(FILE *file, time_t time, time_t now) { + struct tm tm; + if (!localtime_r(&time, &tm)) { + goto error; + } + + char time_str[256]; + size_t time_ret; + + time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60; + time_t tomorrow = now + 24 * 60 * 60; + if (time <= six_months_ago || time >= tomorrow) { + time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm); + } else { + time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm); + } + + if (time_ret == 0) { + goto error; + } + + return fprintf(file, " %s", time_str); + +error: + return fprintf(file, " %jd", (intmax_t)time); +} + /** * -f?ls action. */ @@ -727,28 +784,11 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { time_t time = statbuf->mtime.tv_sec; time_t now = ctx->now.tv_sec; - time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60; - time_t tomorrow = now + 24 * 60 * 60; - struct tm tm; - if (!localtime_r(&time, &tm)) { - goto error; - } - char time_str[256]; - size_t time_ret; - if (time <= six_months_ago || time >= tomorrow) { - time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm); - } else { - time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm); - } - if (time_ret == 0) { - errno = EOVERFLOW; - goto error; - } - if (cfprintf(cfile, " %s${rs}", time_str) < 0) { + if (print_time(file, time, now) < 0) { goto error; } - if (cfprintf(cfile, " %pP", ftwbuf) < 0) { + if (cfprintf(cfile, "${rs} %pP", ftwbuf) < 0) { goto error; } @@ -883,7 +923,7 @@ bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state) { eval_error(state, "%s.\n", str); free(str); } else { - eval_error(state, "bfs_regerror(): %m.\n"); + eval_error(state, "bfs_regerror(): %s.\n", errstr()); } } @@ -980,6 +1020,13 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { const struct BFTW *ftwbuf = state->ftwbuf; enum bfs_stat_flags flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW); enum bfs_type type = bftw_type(ftwbuf, flags); + + // GNU find treats ELOOP as a broken symbolic link for -xtype l + // (but not -L -type l) + if ((flags & BFS_STAT_TRYFOLLOW) && type == BFS_ERROR && errno == ELOOP) { + type = BFS_LNK; + } + if (type == BFS_ERROR) { eval_report_error(state); return false; @@ -988,40 +1035,23 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { } } -#if _POSIX_MONOTONIC_CLOCK > 0 -# define BFS_CLOCK CLOCK_MONOTONIC -#elif _POSIX_TIMERS > 0 -# define BFS_CLOCK CLOCK_REALTIME -#endif - /** - * Call clock_gettime(), if available. + * clock_gettime() wrapper. */ static int eval_gettime(struct bfs_eval *state, struct timespec *ts) { -#ifdef BFS_CLOCK - int ret = clock_gettime(BFS_CLOCK, ts); - if (ret != 0) { - bfs_warning(state->ctx, "%pP: clock_gettime(): %m.\n", state->ftwbuf); + clockid_t clock = CLOCK_REALTIME; + +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clock = CLOCK_MONOTONIC; } - return ret; -#else - return -1; #endif -} -/** - * Record an elapsed time. - */ -static void timespec_elapsed(struct timespec *elapsed, const struct timespec *start, const struct timespec *end) { - elapsed->tv_sec += end->tv_sec - start->tv_sec; - elapsed->tv_nsec += end->tv_nsec - start->tv_nsec; - if (elapsed->tv_nsec < 0) { - elapsed->tv_nsec += 1000000000L; - --elapsed->tv_sec; - } else if (elapsed->tv_nsec >= 1000000000L) { - elapsed->tv_nsec -= 1000000000L; - ++elapsed->tv_sec; + int ret = clock_gettime(clock, ts); + if (ret != 0) { + bfs_warning(state->ctx, "%pP: clock_gettime(): %s.\n", state->ftwbuf, errstr()); } + return ret; } /** @@ -1042,7 +1072,8 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { if (time) { if (eval_gettime(state, &end) == 0) { - timespec_elapsed(&expr->elapsed, &start, &end); + timespec_sub(&end, &start); + timespec_add(&expr->elapsed, &end); } } @@ -1072,7 +1103,7 @@ bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) { * Evaluate a conjunction. */ bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) { - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (!eval_expr(child, state) || state->quit) { return false; } @@ -1085,7 +1116,7 @@ bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) { * Evaluate a disjunction. */ bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) { - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (eval_expr(child, state) || state->quit) { return true; } @@ -1100,7 +1131,7 @@ bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) { bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) { bool ret uninit(false); - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { ret = eval_expr(child, state); if (state->quit) { break; @@ -1111,20 +1142,7 @@ bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) { } /** Update the status bar. */ -static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct timespec *last_status, size_t count) { - struct timespec now; - if (eval_gettime(state, &now) == 0) { - struct timespec elapsed = {0}; - timespec_elapsed(&elapsed, last_status, &now); - - // Update every 0.1s - if (elapsed.tv_sec > 0 || elapsed.tv_nsec >= 100000000L) { - *last_status = now; - } else { - return; - } - } - +static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, size_t count) { size_t width = bfs_bar_width(bar); if (width < 3) { return; @@ -1140,7 +1158,7 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time size_t rhslen = xstrwidth(rhs); if (3 + rhslen > width) { - dstresize(&rhs, 0); + dstrshrink(rhs, 0); rhslen = 0; } @@ -1184,7 +1202,7 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time } pathwidth += cwidth; } - dstresize(&status, lhslen); + dstrshrink(status, lhslen); if (dstrcat(&status, "...") != 0) { goto out; @@ -1251,7 +1269,7 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enu bfs_debug_prefix(ctx, DEBUG_STAT); fprintf(stderr, "bfs_stat("); - if (ftwbuf->at_fd == AT_FDCWD) { + if (ftwbuf->at_fd == (int)AT_FDCWD) { fprintf(stderr, "AT_FDCWD"); } else { size_t baselen = strlen(ftwbuf->path) - strlen(ftwbuf->at_path); @@ -1267,7 +1285,7 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enu DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW); DEBUG_FLAG(flags, BFS_STAT_NOSYNC); - fprintf(stderr, ") == %d", err ? 0 : -1); + fprintf(stderr, ") == %d", err == 0 ? 0 : -1); if (err) { fprintf(stderr, " [%d]", err); @@ -1354,18 +1372,85 @@ struct callback_args { /** The status bar. */ struct bfs_bar *bar; - /** The time of the last status update. */ - struct timespec last_status; + /** The SIGALRM hook. */ + struct sighook *alrm_hook; + /** The interval timer. */ + struct timer *timer; + /** Flag set by SIGALRM. */ + atomic bool alrm_flag; + /** Flag set by SIGINFO. */ + atomic bool info_flag; + /** The number of files visited so far. */ size_t count; /** The set of seen files. */ struct trie *seen; + /** The number of errors that have occurred. */ + size_t nerrors; /** Eventual return value from bfs_eval(). */ int ret; }; +/** Update the status bar in response to SIGALRM. */ +static void eval_sigalrm(int sig, siginfo_t *info, void *ptr) { + struct callback_args *args = ptr; + store(&args->alrm_flag, true, relaxed); +} + +/** Show/hide the bar in response to SIGINFO. */ +static void eval_siginfo(int sig, siginfo_t *info, void *ptr) { + struct callback_args *args = ptr; + store(&args->info_flag, true, relaxed); +} + +/** Show the status bar. */ +static void eval_show_bar(struct callback_args *args) { + args->alrm_hook = sighook(SIGALRM, eval_sigalrm, args, SH_CONTINUE); + if (!args->alrm_hook) { + goto fail; + } + + args->bar = bfs_bar_show(); + if (!args->bar) { + goto fail; + } + + // Update the bar every 0.1s + struct timespec ival = { .tv_nsec = 100 * 1000 * 1000 }; + args->timer = xtimer_start(&ival); + if (!args->timer) { + goto fail; + } + + // Update the bar immediately + store(&args->alrm_flag, true, relaxed); + + return; + +fail: + bfs_warning(args->ctx, "Couldn't show status bar: %s.\n\n", errstr()); + + bfs_bar_hide(args->bar); + args->bar = NULL; + + sigunhook(args->alrm_hook); + args->alrm_hook = NULL; +} + +/** Hide the status bar. */ +static void eval_hide_bar(struct callback_args *args) { + xtimer_stop(args->timer); + args->timer = NULL; + + sigunhook(args->alrm_hook); + args->alrm_hook = NULL; + + bfs_bar_hide(args->bar); + args->bar = NULL; +} + /** * bftw() callback. */ @@ -1380,17 +1465,37 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) { state.ctx = ctx; state.action = BFTW_CONTINUE; state.ret = &args->ret; + state.nerrors = &args->nerrors; state.quit = false; - if (args->bar) { - eval_status(&state, args->bar, &args->last_status, args->count); + // Check whether SIGINFO was delivered and show/hide the bar + if (exchange(&args->info_flag, false, relaxed)) { + if (args->bar) { + eval_hide_bar(args); + } else { + eval_show_bar(args); + } + } + + if (exchange(&args->alrm_flag, false, relaxed)) { + eval_status(&state, args->bar, args->count); } if (ftwbuf->type == BFS_ERROR) { - if (!eval_should_ignore(&state, ftwbuf->error)) { - eval_error(&state, "%s.\n", xstrerror(ftwbuf->error)); - } state.action = BFTW_PRUNE; + + if (ftwbuf->error == ELOOP && ftwbuf->loopoff > 0) { + char *loop = strndup(ftwbuf->path, ftwbuf->loopoff); + if (loop) { + eval_error(&state, "Filesystem loop back to ${di}%pq${rs}\n", loop); + free(loop); + goto done; + } + } else if (eval_should_ignore(&state, ftwbuf->error)) { + goto done; + } + + eval_error(&state, "%s.\n", xstrerror(ftwbuf->error)); goto done; } @@ -1449,6 +1554,9 @@ done: static int raise_fdlimit(struct bfs_ctx *ctx) { rlim_t cur = ctx->orig_nofile.rlim_cur; rlim_t max = ctx->orig_nofile.rlim_max; + if (!ctx->raise_nofile) { + max = cur; + } rlim_t target = 64 << 10; if (rlim_cmp(target, max) > 0) { @@ -1577,7 +1685,7 @@ static bool eval_must_buffer(const struct bfs_expr *expr) { return true; } - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (eval_must_buffer(child)) { return true; } @@ -1598,12 +1706,16 @@ int bfs_eval(struct bfs_ctx *ctx) { }; if (ctx->status) { - args.bar = bfs_bar_show(); - if (!args.bar) { - bfs_warning(ctx, "Couldn't show status bar: %m.\n\n"); - } + eval_show_bar(&args); } +#ifdef SIGINFO + int siginfo = SIGINFO; +#else + int siginfo = SIGUSR1; +#endif + struct sighook *info_hook = sighook(siginfo, eval_siginfo, &args, SH_CONTINUE); + struct trie seen; if (ctx->unique) { trie_init(&seen); @@ -1671,7 +1783,14 @@ int bfs_eval(struct bfs_ctx *ctx) { trie_destroy(&seen); } - bfs_bar_hide(args.bar); + sigunhook(info_hook); + if (args.bar) { + eval_hide_bar(&args); + } + + if (ctx->ignore_errors && args.nerrors > 0) { + bfs_warning(ctx, "Suppressed errors: %zu\n", args.nerrors); + } return args.ret; } @@ -9,8 +9,6 @@ #ifndef BFS_EVAL_H #define BFS_EVAL_H -#include "config.h" - struct bfs_ctx; struct bfs_expr; @@ -22,9 +20,9 @@ struct bfs_eval; /** * Expression evaluation function. * - * @param expr + * @expr * The current expression. - * @param state + * @state * The current evaluation state. * @return * The result of the test. @@ -34,7 +32,7 @@ typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state); /** * Evaluate the command line. * - * @param ctx + * @ctx * The bfs context to evaluate. * @return * EXIT_SUCCESS on success, otherwise on failure. @@ -49,6 +47,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); @@ -2,15 +2,17 @@ // SPDX-License-Identifier: 0BSD #include "exec.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "diag.h" #include "dstring.h" #include "xspawn.h" + #include <errno.h> #include <fcntl.h> #include <stdarg.h> @@ -22,7 +24,7 @@ #include <unistd.h> /** Print some debugging info. */ -attr(printf(2, 3)) +_printf(2, 3) static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) { const struct bfs_ctx *ctx = execbuf->ctx; @@ -56,7 +58,7 @@ static size_t bfs_exec_arg_size(const char *arg) { /** Determine the maximum argv size. */ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) { - long arg_max = sysconf(_SC_ARG_MAX); + long arg_max = xsysconf(_SC_ARG_MAX); bfs_exec_debug(execbuf, "ARG_MAX: %ld according to sysconf()\n", arg_max); if (arg_max < 0) { arg_max = BFS_EXEC_ARG_MAX; @@ -82,7 +84,7 @@ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) { // Assume arguments are counted with the granularity of a single page, // so allow a one page cushion to account for rounding up - long page_size = sysconf(_SC_PAGESIZE); + long page_size = xsysconf(_SC_PAGESIZE); if (page_size < 4096) { page_size = 4096; } @@ -234,7 +236,7 @@ static char *bfs_exec_format_arg(char *arg, const char *path) { char *last = arg; do { - if (dstrncat(&ret, last, match - last) != 0) { + if (dstrxcat(&ret, last, match - last) != 0) { goto err; } if (dstrcat(&ret, path) != 0) { @@ -268,7 +270,7 @@ static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) bfs_assert(execbuf->wd_fd < 0); bfs_assert(!execbuf->wd_path); - if (ftwbuf->at_fd != AT_FDCWD) { + if (ftwbuf->at_fd != (int)AT_FDCWD) { // Rely on at_fd being the immediate parent bfs_assert(xbaseoff(ftwbuf->at_path) == 0); @@ -67,11 +67,11 @@ struct bfs_exec { /** * Parse an exec action. * - * @param argv + * @argv * The (bfs) command line argument to parse. - * @param flags + * @flags * Any flags for this exec action. - * @param ctx + * @ctx * The bfs context. * @return * The parsed exec action, or NULL on failure. @@ -81,9 +81,9 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs /** * Execute the command for a file. * - * @param execbuf + * @execbuf * The parsed exec action. - * @param ftwbuf + * @ftwbuf * The bftw() data for the current file. * @return 0 if the command succeeded, -1 if it failed. If the command could * be executed, -1 is returned, and errno will be non-zero. For @@ -94,7 +94,7 @@ int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf); /** * Finish executing any commands. * - * @param execbuf + * @execbuf * The parsed exec action. * @return 0 on success, -1 if any errors were encountered. */ @@ -2,6 +2,7 @@ // SPDX-License-Identifier: 0BSD #include "expr.h" + #include "alloc.h" #include "ctx.h" #include "diag.h" @@ -10,9 +11,12 @@ #include "list.h" #include "printf.h" #include "xregex.h" + #include <string.h> -struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv) { +struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) { + bfs_assert(kind != BFS_PATH); + struct bfs_expr *expr = arena_alloc(&ctx->expr_arena); if (!expr) { return NULL; @@ -22,6 +26,7 @@ struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t expr->eval_fn = eval_fn; expr->argc = argc; expr->argv = argv; + expr->kind = kind; expr->probability = 0.5; SLIST_PREPEND(&ctx->expr_list, expr, freelist); @@ -63,8 +68,7 @@ void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child) { } void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children) { - while (!SLIST_EMPTY(children)) { - struct bfs_expr *child = SLIST_POP(children); + drain_slist (struct bfs_expr, child, children) { bfs_expr_append(expr, child); } } @@ -9,13 +9,37 @@ #define BFS_EXPR_H #include "color.h" -#include "config.h" #include "eval.h" #include "stat.h" + #include <sys/types.h> #include <time.h> /** + * Argument/token/expression kinds. + */ +enum bfs_kind { + /** A regular argument. */ + BFS_ARG, + + /** A flag (-H, -L, etc.). */ + BFS_FLAG, + + /** A root path. */ + BFS_PATH, + + /** An option (-follow, -mindepth, etc.). */ + BFS_OPTION, + /** A test (-name, -size, etc.). */ + BFS_TEST, + /** An action (-print, -exec, etc.). */ + BFS_ACTION, + + /** An operator (-and, -or, etc.). */ + BFS_OPERATOR, +}; + +/** * Integer comparison modes. */ enum bfs_int_cmp { @@ -97,6 +121,8 @@ struct bfs_expr { size_t argc; /** The command line arguments comprising this expression. */ char **argv; + /** The kind of expression this is. */ + enum bfs_kind kind; /** The number of files this expression keeps open between evaluations. */ int persistent_fds; @@ -123,7 +149,7 @@ struct bfs_expr { /** Total time spent running this predicate. */ struct timespec elapsed; - /** Auxilliary data for the evaluation function. */ + /** Auxiliary data for the evaluation function. */ union { /** Child expressions. */ struct bfs_exprs children; @@ -207,7 +233,7 @@ struct bfs_ctx; /** * Create a new expression. */ -struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv); +struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv, enum bfs_kind kind); /** * @return Whether this type of expression has children. @@ -244,4 +270,10 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n); */ void bfs_expr_clear(struct bfs_expr *expr); +/** + * Iterate over the children of an expression. + */ +#define for_expr(child, expr) \ + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) + #endif // BFS_EXPR_H diff --git a/src/fsade.c b/src/fsade.c index ee17416..dfdf125 100644 --- a/src/fsade.c +++ b/src/fsade.c @@ -2,13 +2,15 @@ // SPDX-License-Identifier: 0BSD #include "fsade.h" + #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" -#include "config.h" #include "dir.h" #include "dstring.h" #include "sanity.h" + #include <errno.h> #include <fcntl.h> #include <stddef.h> @@ -22,22 +24,35 @@ # include <sys/capability.h> #endif -#if BFS_USE_SYS_EXTATTR_H +#if BFS_CAN_CHECK_CONTEXT +# include <selinux/selinux.h> +#endif + +#if __has_include(<sys/extattr.h>) # include <sys/extattr.h> -#elif BFS_USE_SYS_XATTR_H +# define BFS_USE_EXTATTR true +#elif __has_include(<sys/xattr.h>) # include <sys/xattr.h> +# define BFS_USE_XATTR true +#endif + +#ifndef BFS_USE_EXTATTR +# define BFS_USE_EXTATTR false +#endif +#ifndef BFS_USE_XATTR +# define BFS_USE_XATTR false #endif /** * Many of the APIs used here don't have *at() variants, but we can try to * emulate something similar if /proc/self/fd is available. */ -attr(maybe_unused) +_maybe_unused static const char *fake_at(const struct BFTW *ftwbuf) { static atomic int proc_works = -1; dchar *path = NULL; - if (ftwbuf->at_fd == AT_FDCWD || load(&proc_works, relaxed) == 0) { + if (ftwbuf->at_fd == (int)AT_FDCWD || load(&proc_works, relaxed) == 0) { goto fail; } @@ -66,7 +81,7 @@ fail: return ftwbuf->path; } -attr(maybe_unused) +_maybe_unused static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { if (path != ftwbuf->path) { dstrfree((dchar *)path); @@ -76,7 +91,7 @@ static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { /** * Check if an error was caused by the absence of support or data for a feature. */ -attr(maybe_unused) +_maybe_unused static bool is_absence_error(int error) { // If the OS doesn't support the feature, it's obviously not enabled for // any files @@ -117,11 +132,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 +165,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) +_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 +220,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 +234,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 +291,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) { error = errno; acl_free(acl); } +#endif free_fake_at(ftwbuf, path); errno = error; @@ -323,17 +355,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) { #if BFS_CAN_CHECK_XATTRS +#if BFS_USE_EXTATTR + +/** 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_EXTATTR + 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); +#if BFS_USE_EXTATTR + 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; @@ -366,13 +443,10 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { const char *path = fake_at(ftwbuf); 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); +#if BFS_USE_EXTATTR + 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 +488,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..fbe02d8 100644 --- a/src/fsade.h +++ b/src/fsade.h @@ -9,25 +9,26 @@ #ifndef BFS_FSADE_H #define BFS_FSADE_H -#include "config.h" +#include "bfs.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_WITH_LIBCAP + +#define BFS_CAN_CHECK_CONTEXT BFS_WITH_LIBSELINUX -#define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H) +#if __has_include(<sys/extattr.h>) || __has_include(<sys/xattr.h>) +# define BFS_CAN_CHECK_XATTRS true +#else +# define BFS_CAN_CHECK_XATTRS false +#endif struct BFTW; /** * Check if a file has a non-trivial Access Control List. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -37,7 +38,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf); /** * Check if a file has a non-trivial capability set. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -47,7 +48,7 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf); /** * Check if a file has any extended attributes set. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -57,13 +58,28 @@ int bfs_check_xattrs(const struct BFTW *ftwbuf); /** * Check if a file has an extended attribute with the given name. * - * @param ftwbuf + * @ftwbuf * The file to check. - * @param name + * @name * The name of the xattr to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. */ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name); +/** + * Get a file's SELinux context + * + * @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 @@ -119,23 +119,26 @@ */ #include "ioq.h" + #include "alloc.h" #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "bit.h" -#include "config.h" #include "diag.h" #include "dir.h" #include "stat.h" #include "thread.h" + #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdint.h> #include <stdlib.h> #include <sys/stat.h> +#include <unistd.h> -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING # include <liburing.h> #endif @@ -180,8 +183,7 @@ typedef atomic uintptr_t ioq_slot; /** Amount to add for an additional skip. */ #define IOQ_SKIP_ONE (~IOQ_BLOCKED) -// Need room for two flag bits -bfs_static_assert(alignof(struct ioq_ent) >= (1 << 2)); +static_assert(alignof(struct ioq_ent) >= (1 << 2), "struct ioq_ent is underaligned"); /** * An MPMC queue of I/O commands. @@ -201,7 +203,7 @@ struct ioqq { cache_align atomic size_t tail; /** The circular buffer itself. */ - cache_align ioq_slot slots[]; + cache_align ioq_slot slots[]; // _counted_by(slot_mask + 1) }; /** Destroy an I/O command queue. */ @@ -258,17 +260,45 @@ static struct ioqq *ioqq_create(size_t size) { /** Get the monitor associated with a slot. */ static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) { - size_t i = slot - ioqq->slots; + uint32_t i = slot - ioqq->slots; + + // Hash the index to de-correlate waiters + // https://nullprogram.com/blog/2018/07/31/ + // https://github.com/skeeto/hash-prospector/issues/19#issuecomment-1120105785 + i ^= i >> 16; + i *= UINT32_C(0x21f0aaad); + i ^= i >> 15; + i *= UINT32_C(0x735a2d97); + i ^= i >> 15; + return &ioqq->monitors[i & ioqq->monitor_mask]; } /** Atomically wait for a slot to change. */ -attr(noinline) +_noinline static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) { + uintptr_t ret; + + // Try spinning a few times (with exponential backoff) before blocking + _nounroll + for (int i = 1; i < 1024; i *= 2) { + _nounroll + for (int j = 0; j < i; ++j) { + spin_loop(); + } + + // Check if the slot changed + ret = load(slot, relaxed); + if (ret != value) { + return ret; + } + } + + // Nothing changed, start blocking struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); mutex_lock(&monitor->mutex); - uintptr_t ret = load(slot, relaxed); + ret = load(slot, relaxed); if (ret != value) { goto done; } @@ -293,7 +323,7 @@ done: } /** Wake up any threads waiting on a slot. */ -attr(noinline) +_noinline static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) { struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); @@ -313,9 +343,11 @@ static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) { cond_broadcast(&monitor->cond); } -/** Branch-free (slot & IOQ_SKIP) ? ~IOQ_BLOCKED : 0 */ -static uintptr_t ioq_skip_mask(uintptr_t slot) { - return -(slot >> IOQ_SKIP_BIT) << 1; +/** Branch-free ((slot & IOQ_SKIP) ? skip : full) & ~IOQ_BLOCKED */ +static uintptr_t ioq_slot_blend(uintptr_t slot, uintptr_t skip, uintptr_t full) { + uintptr_t mask = -(slot >> IOQ_SKIP_BIT); + uintptr_t ret = (skip & mask) | (full & ~mask); + return ret & ~IOQ_BLOCKED; } /** Push an entry into a slot. */ @@ -323,19 +355,18 @@ static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent uintptr_t prev = load(slot, relaxed); while (true) { - size_t skip_mask = ioq_skip_mask(prev); - size_t full_mask = ~skip_mask & ~IOQ_BLOCKED; - if (prev & full_mask) { + uintptr_t full = ioq_slot_blend(prev, 0, prev); + if (full) { // full(ptr) → wait prev = ioq_slot_wait(ioqq, slot, prev); continue; } // empty → full(ptr) - uintptr_t next = ((uintptr_t)ent >> 1) & full_mask; + uintptr_t next = (uintptr_t)ent >> 1; // skip(1) → empty // skip(n) → skip(n - 1) - next |= (prev - IOQ_SKIP_ONE) & skip_mask; + next = ioq_slot_blend(prev, prev - IOQ_SKIP_ONE, next); if (compare_exchange_weak(slot, &prev, next, release, relaxed)) { break; @@ -353,13 +384,20 @@ static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool block) { uintptr_t prev = load(slot, relaxed); while (true) { +#if __has_builtin(__builtin_prefetch) + // Optimistically prefetch the pointer in this slot. If this + // slot is not full, this will prefetch an invalid address, but + // experimentally this is worth it on both Intel (Alder Lake) + // and AMD (Zen 2). + __builtin_prefetch((void *)(prev << 1), 1 /* write */); +#endif + // empty → skip(1) // skip(n) → skip(n + 1) // full(ptr) → full(ptr - 1) uintptr_t next = prev + IOQ_SKIP_ONE; - // skip(n) → ~IOQ_BLOCKED // full(ptr) → 0 - next &= ioq_skip_mask(next); + next = ioq_slot_blend(next, next, 0); if (block && next) { prev = ioq_slot_wait(ioqq, slot, prev); @@ -378,7 +416,7 @@ static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool bloc // empty → 0 // skip(n) → 0 // full(ptr) → ptr - prev &= ioq_skip_mask(~prev); + prev = ioq_slot_blend(prev, 0, prev); return (struct ioq_ent *)(prev << 1); } @@ -408,13 +446,6 @@ static void ioqq_push_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t s } while (size > 0); } -/** Pop an entry from the queue. */ -static struct ioq_ent *ioqq_pop(struct ioqq *ioqq, bool block) { - size_t i = fetch_add(&ioqq->tail, 1, relaxed); - ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask]; - return ioq_slot_pop(ioqq, slot, block); -} - /** Pop a batch of entries from the queue. */ static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size, bool block) { size_t mask = ioqq->slot_mask; @@ -430,36 +461,83 @@ static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t si #define IOQ_BATCH (FALSE_SHARING_SIZE / sizeof(ioq_slot)) /** - * A batch of entries to send all at once. + * A batch of I/O queue entries. */ struct ioq_batch { - /** The current batch size. */ - size_t size; + /** The start of the batch. */ + size_t head; + /** The end of the batch. */ + size_t tail; /** The array of entries. */ struct ioq_ent *entries[IOQ_BATCH]; }; -/** Send the batch to a queue. */ +/** Reset a batch. */ +static void ioq_batch_reset(struct ioq_batch *batch) { + batch->head = batch->tail = 0; +} + +/** Check if a batch is empty. */ +static bool ioq_batch_empty(const struct ioq_batch *batch) { + return batch->head >= batch->tail; +} + +/** Send a batch to a queue. */ static void ioq_batch_flush(struct ioqq *ioqq, struct ioq_batch *batch) { - if (batch->size > 0) { - ioqq_push_batch(ioqq, batch->entries, batch->size); - batch->size = 0; + if (batch->tail > 0) { + ioqq_push_batch(ioqq, batch->entries, batch->tail); + ioq_batch_reset(batch); } } -/** An an entry to a batch, flushing if necessary. */ +/** Push an entry to a batch, flushing if necessary. */ static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct ioq_ent *ent) { - if (batch->size >= IOQ_BATCH) { + batch->entries[batch->tail++] = ent; + + if (batch->tail >= IOQ_BATCH) { ioq_batch_flush(ioqq, batch); } +} + +/** Fill a batch from a queue. */ +static bool ioq_batch_fill(struct ioqq *ioqq, struct ioq_batch *batch, bool block) { + ioqq_pop_batch(ioqq, batch->entries, IOQ_BATCH, block); - batch->entries[batch->size++] = ent; + ioq_batch_reset(batch); + for (size_t i = 0; i < IOQ_BATCH; ++i) { + struct ioq_ent *ent = batch->entries[i]; + if (ent) { + batch->entries[batch->tail++] = ent; + } + } + + return batch->tail > 0; +} + +/** Pop an entry from a batch, filling it first if necessary. */ +static struct ioq_ent *ioq_batch_pop(struct ioqq *ioqq, struct ioq_batch *batch, bool block) { + if (ioq_batch_empty(batch)) { + // For non-blocking pops, make sure that each ioq_batch_pop() + // corresponds to a single (amortized) increment of ioqq->head. + // Otherwise, we start skipping many slots and batching ends up + // degrading performance. + if (!block && batch->head < IOQ_BATCH) { + ++batch->head; + return NULL; + } + + if (!ioq_batch_fill(ioqq, batch, block)) { + return NULL; + } + } + + return batch->entries[batch->head++]; } /** Sentinel stop command. */ static struct ioq_ent IOQ_STOP; -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING /** * Supported io_uring operations. */ @@ -477,7 +555,7 @@ struct ioq_thread { /** Pointer back to the I/O queue. */ struct ioq *parent; -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING /** io_uring instance. */ struct io_uring ring; /** Any error that occurred initializing the ring. */ @@ -497,20 +575,25 @@ struct ioq { /** ioq_ent arena. */ struct arena ents; -#if BFS_USE_LIBURING && BFS_USE_STATX +#if BFS_WITH_LIBURING && BFS_USE_STATX /** struct statx arena. */ struct arena xbufs; #endif - /** Pending I/O requests. */ + /** Pending I/O request queue. */ struct ioqq *pending; - /** Ready I/O responses. */ + /** Ready I/O response queue. */ struct ioqq *ready; + /** Pending request batch. */ + struct ioq_batch pending_batch; + /** Ready request batch. */ + struct ioq_batch ready_batch; + /** The number of background threads. */ size_t nthreads; /** The background threads themselves. */ - struct ioq_thread threads[]; + struct ioq_thread threads[] _counted_by(nthreads); }; /** Cancel a request if we need to. */ @@ -531,6 +614,14 @@ static bool ioq_check_cancel(struct ioq *ioq, struct ioq_ent *ent) { /** Dispatch a single request synchronously. */ static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) { switch (ent->op) { + case IOQ_NOP: + if (ent->nop.type == IOQ_NOP_HEAVY) { + // A fast, no-op syscall + getppid(); + } + ent->result = 0; + return; + case IOQ_CLOSE: ent->result = try(xclose(ent->close.fd)); return; @@ -559,7 +650,7 @@ static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) { ent->result = -ENOSYS; } -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING /** io_uring worker state. */ struct ioq_ring_state { @@ -579,23 +670,161 @@ struct ioq_ring_state { struct ioq_batch ready; }; +/** Reap a single CQE. */ +static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) { + struct ioq *ioq = state->ioq; + + struct ioq_ent *ent = io_uring_cqe_get_data(cqe); + ent->result = cqe->res; + + if (ent->result < 0) { + goto push; + } + + switch (ent->op) { + case IOQ_OPENDIR: { + int fd = ent->result; + if (ioq_check_cancel(ioq, ent)) { + xclose(fd); + goto push; + } + + struct ioq_opendir *args = &ent->opendir; + ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags)); + if (ent->result >= 0) { + // TODO: io_uring_prep_getdents() + bfs_polldir(args->dir); + } else { + xclose(fd); + } + + break; + } + +#if BFS_USE_STATX + case IOQ_STAT: { + struct ioq_stat *args = &ent->stat; + ent->result = try(bfs_statx_convert(args->buf, args->xbuf)); + break; + } +#endif + + default: + break; + } + +push: + ioq_batch_push(ioq->ready, &state->ready, ent); +} + +/** Wait for submitted requests to complete. */ +static void ioq_ring_drain(struct ioq_ring_state *state, size_t wait_nr) { + struct ioq *ioq = state->ioq; + struct io_uring *ring = state->ring; + + bfs_assert(wait_nr <= state->submitted); + + while (state->submitted > 0) { + struct io_uring_cqe *cqe; + if (wait_nr > 0) { + io_uring_wait_cqes(ring, &cqe, wait_nr, NULL, NULL); + } + + unsigned int head; + size_t seen = 0; + io_uring_for_each_cqe (ring, head, cqe) { + ioq_reap_cqe(state, cqe); + ++seen; + } + + io_uring_cq_advance(ring, seen); + state->submitted -= seen; + + if (seen >= wait_nr) { + break; + } + wait_nr -= seen; + } + + ioq_batch_flush(ioq->ready, &state->ready); +} + +/** Submit prepped SQEs, and wait for some to complete. */ +static void ioq_ring_submit(struct ioq_ring_state *state) { + struct io_uring *ring = state->ring; + + size_t unreaped = state->prepped + state->submitted; + size_t wait_nr = 0; + + if (state->prepped == 0 && unreaped > 0) { + // If we have no new SQEs, wait for at least one old one to + // complete, to avoid livelock + wait_nr = 1; + } + + if (unreaped > ring->sq.ring_entries) { + // Keep the completion queue below half full + wait_nr = unreaped - ring->sq.ring_entries; + } + + // Submit all prepped SQEs + while (state->prepped > 0) { + int ret = io_uring_submit_and_wait(state->ring, wait_nr); + if (ret <= 0) { + continue; + } + + state->submitted += ret; + state->prepped -= ret; + if (state->prepped > 0) { + // In the unlikely event of a short submission, any SQE + // links will be broken. Wait for all SQEs to complete + // to preserve any ordering requirements. + ioq_ring_drain(state, state->submitted); + wait_nr = 0; + } + } + + // Drain all the CQEs we waited for (and any others that are ready) + ioq_ring_drain(state, wait_nr); +} + +/** Reserve space for a number of SQEs, submitting if necessary. */ +static void ioq_reserve_sqes(struct ioq_ring_state *state, unsigned int count) { + while (io_uring_sq_space_left(state->ring) < count) { + ioq_ring_submit(state); + } +} + +/** Get an SQE, submitting if necessary. */ +static struct io_uring_sqe *ioq_get_sqe(struct ioq_ring_state *state) { + ioq_reserve_sqes(state, 1); + return io_uring_get_sqe(state->ring); +} + /** Dispatch a single request asynchronously. */ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, struct ioq_ent *ent) { - struct io_uring *ring = state->ring; enum ioq_ring_ops ops = state->ops; struct io_uring_sqe *sqe = NULL; switch (ent->op) { + case IOQ_NOP: + if (ent->nop.type == IOQ_NOP_HEAVY) { + sqe = ioq_get_sqe(state); + io_uring_prep_nop(sqe); + } + return sqe; + case IOQ_CLOSE: if (ops & IOQ_RING_CLOSE) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); io_uring_prep_close(sqe, ent->close.fd); } return sqe; case IOQ_OPENDIR: if (ops & IOQ_RING_OPENAT) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); struct ioq_opendir *args = &ent->opendir; int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY; io_uring_prep_openat(sqe, args->dfd, args->path, flags, 0); @@ -605,7 +834,7 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str case IOQ_CLOSEDIR: #if BFS_USE_UNWRAPDIR if (ops & IOQ_RING_CLOSE) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); io_uring_prep_close(sqe, bfs_unwrapdir(ent->closedir.dir)); } #endif @@ -614,10 +843,10 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str case IOQ_STAT: #if BFS_USE_STATX if (ops & IOQ_RING_STATX) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); struct ioq_stat *args = &ent->stat; int flags = bfs_statx_flags(args->flags); - unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + unsigned int mask = bfs_statx_mask(); io_uring_prep_statx(sqe, args->dfd, args->path, flags, mask, args->xbuf); } #endif @@ -630,7 +859,7 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str /** Check if ioq_ring_reap() has work to do. */ static bool ioq_ring_empty(struct ioq_ring_state *state) { - return !state->prepped && !state->submitted && !state->ready.size; + return !state->prepped && !state->submitted && ioq_batch_empty(&state->ready); } /** Prep a single SQE. */ @@ -658,163 +887,94 @@ static bool ioq_ring_prep(struct ioq_ring_state *state) { } struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; - struct ioq_ent *pending[IOQ_BATCH]; - - while (io_uring_sq_space_left(ring) >= IOQ_BATCH) { - bool block = ioq_ring_empty(state); - ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, block); - - bool any = false; - for (size_t i = 0; i < IOQ_BATCH; ++i) { - struct ioq_ent *ent = pending[i]; - if (ent == &IOQ_STOP) { - ioqq_push(ioq->pending, &IOQ_STOP); - state->stop = true; - goto done; - } else if (ent) { - ioq_prep_sqe(state, ent); - any = true; - } - } - - if (!any) { - break; - } - } - -done: - return !ioq_ring_empty(state); -} - -/** Reap a single CQE. */ -static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) { - struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; - - struct ioq_ent *ent = io_uring_cqe_get_data(cqe); - ent->result = cqe->res; - io_uring_cqe_seen(ring, cqe); - --state->submitted; - - if (ent->result < 0) { - goto push; - } - switch (ent->op) { - case IOQ_OPENDIR: { - int fd = ent->result; - if (ioq_check_cancel(ioq, ent)) { - xclose(fd); - goto push; - } - - struct ioq_opendir *args = &ent->opendir; - ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags)); - if (ent->result >= 0) { - // TODO: io_uring_prep_getdents() - bfs_polldir(args->dir); - } else { - xclose(fd); - } + struct ioq_batch pending; + ioq_batch_reset(&pending); + while (true) { + bool block = ioq_ring_empty(state); + struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, block); + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, ent); + state->stop = true; break; - } - -#if BFS_USE_STATX - case IOQ_STAT: { - struct ioq_stat *args = &ent->stat; - ent->result = try(bfs_statx_convert(args->buf, args->xbuf)); + } else if (ent) { + ioq_prep_sqe(state, ent); + } else { break; } -#endif - - default: - break; } -push: - ioq_batch_push(ioq->ready, &state->ready, ent); + bfs_assert(ioq_batch_empty(&pending)); + return !ioq_ring_empty(state); } -/** Reap a batch of CQEs. */ -static void ioq_ring_reap(struct ioq_ring_state *state) { - struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; +/** io_uring worker loop. */ +static int ioq_ring_work(struct ioq_thread *thread) { + struct io_uring *ring = &thread->ring; - while (state->prepped) { - int ret = io_uring_submit_and_wait(ring, 1); - if (ret > 0) { - state->prepped -= ret; - state->submitted += ret; +#ifdef IORING_SETUP_R_DISABLED + if (ring->flags & IORING_SETUP_R_DISABLED) { + if (io_uring_enable_rings(ring) != 0) { + return -1; } } +#endif - while (state->submitted) { - struct io_uring_cqe *cqe; - if (io_uring_wait_cqe(ring, &cqe) < 0) { - continue; - } - - ioq_reap_cqe(state, cqe); - } - - ioq_batch_flush(ioq->ready, &state->ready); -} - -/** io_uring worker loop. */ -static void ioq_ring_work(struct ioq_thread *thread) { struct ioq_ring_state state = { .ioq = thread->parent, - .ring = &thread->ring, + .ring = ring, .ops = thread->ring_ops, }; while (ioq_ring_prep(&state)) { - ioq_ring_reap(&state); + ioq_ring_submit(&state); } + + ioq_ring_drain(&state, state.submitted); + return 0; } -#endif // BFS_USE_LIBURING +#endif // BFS_WITH_LIBURING /** Synchronous syscall loop. */ static void ioq_sync_work(struct ioq_thread *thread) { struct ioq *ioq = thread->parent; - bool stop = false; - while (!stop) { - struct ioq_ent *pending[IOQ_BATCH]; - ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, true); - - struct ioq_batch ready; - ready.size = 0; - - for (size_t i = 0; i < IOQ_BATCH; ++i) { - struct ioq_ent *ent = pending[i]; - if (ent == &IOQ_STOP) { - ioqq_push(ioq->pending, &IOQ_STOP); - stop = true; - break; - } else if (ent) { - if (!ioq_check_cancel(ioq, ent)) { - ioq_dispatch_sync(ioq, ent); - } - ioq_batch_push(ioq->ready, &ready, ent); - } + struct ioq_batch pending, ready; + ioq_batch_reset(&pending); + ioq_batch_reset(&ready); + + while (true) { + if (ioq_batch_empty(&pending)) { + ioq_batch_flush(ioq->ready, &ready); + } + + struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, true); + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, ent); + break; } - ioq_batch_flush(ioq->ready, &ready); + if (!ioq_check_cancel(ioq, ent)) { + ioq_dispatch_sync(ioq, ent); + } + ioq_batch_push(ioq->ready, &ready, ent); } + + bfs_assert(ioq_batch_empty(&pending)); + ioq_batch_flush(ioq->ready, &ready); } /** Background thread entry point. */ static void *ioq_work(void *ptr) { struct ioq_thread *thread = ptr; -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING if (thread->ring_err == 0) { - ioq_ring_work(thread); - return NULL; + if (ioq_ring_work(thread) == 0) { + return NULL; + } } #endif @@ -822,9 +982,30 @@ static void *ioq_work(void *ptr) { return NULL; } +#if BFS_WITH_LIBURING +/** Test whether some io_uring setup flags are supported. */ +static bool ioq_ring_probe_flags(struct io_uring_params *params, unsigned int flags) { + unsigned int saved = params->flags; + params->flags |= flags; + + struct io_uring ring; + int ret = io_uring_queue_init_params(2, &ring, params); + if (ret == 0) { + io_uring_queue_exit(&ring); + } + + if (ret == -EINVAL) { + params->flags = saved; + return false; + } + + return true; +} +#endif + /** Initialize io_uring thread state. */ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING struct ioq_thread *prev = NULL; if (thread > ioq->threads) { prev = thread - 1; @@ -835,11 +1016,31 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { return -1; } - // Share io-wq workers between rings struct io_uring_params params = {0}; + if (prev) { - params.flags |= IORING_SETUP_ATTACH_WQ; + // Share io-wq workers between rings + params.flags = prev->ring.flags | IORING_SETUP_ATTACH_WQ; params.wq_fd = prev->ring.ring_fd; + } else { +#ifdef IORING_SETUP_SUBMIT_ALL + // Don't abort submission just because an inline request fails + ioq_ring_probe_flags(¶ms, IORING_SETUP_SUBMIT_ALL); +#endif + +#ifdef IORING_SETUP_R_DISABLED + // Don't enable the ring yet (needed for SINGLE_ISSUER) + if (ioq_ring_probe_flags(¶ms, IORING_SETUP_R_DISABLED)) { +# ifdef IORING_SETUP_SINGLE_ISSUER + // Allow optimizations assuming only one task submits SQEs + ioq_ring_probe_flags(¶ms, IORING_SETUP_SINGLE_ISSUER); +# endif +# ifdef IORING_SETUP_DEFER_TASKRUN + // Don't interrupt us aggressively with completion events + ioq_ring_probe_flags(¶ms, IORING_SETUP_DEFER_TASKRUN); +# endif + } +#endif } // Use a page for each SQE ring @@ -877,6 +1078,7 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { return -1; } +#if BFS_HAS_IO_URING_MAX_WORKERS // Limit the number of io_uring workers unsigned int values[] = { ioq->nthreads, // [IO_WQ_BOUND] @@ -885,12 +1087,14 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { io_uring_register_iowq_max_workers(&thread->ring, values); #endif +#endif // BFS_WITH_LIBURING + return 0; } /** Destroy an io_uring. */ static void ioq_ring_exit(struct ioq_thread *thread) { -#if BFS_USE_LIBURING +#if BFS_WITH_LIBURING if (thread->ring_err == 0) { io_uring_queue_exit(&thread->ring); } @@ -898,7 +1102,8 @@ static void ioq_ring_exit(struct ioq_thread *thread) { } /** Create an I/O queue thread. */ -static int ioq_thread_create(struct ioq *ioq, struct ioq_thread *thread) { +static int ioq_thread_create(struct ioq *ioq, size_t i) { + struct ioq_thread *thread = &ioq->threads[i]; thread->parent = ioq; ioq_ring_init(ioq, thread); @@ -908,6 +1113,11 @@ static int ioq_thread_create(struct ioq *ioq, struct ioq_thread *thread) { return -1; } + char name[16]; + if (snprintf(name, sizeof(name), "ioq-%zu", i) >= 0) { + thread_setname(thread->id, name); + } + return 0; } @@ -918,18 +1128,15 @@ 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 +#if BFS_WITH_LIBURING && BFS_USE_STATX ARENA_INIT(&ioq->xbufs, struct statx); #endif @@ -945,7 +1152,7 @@ struct ioq *ioq_create(size_t depth, size_t nthreads) { ioq->nthreads = nthreads; for (size_t i = 0; i < nthreads; ++i) { - if (ioq_thread_create(ioq, &ioq->threads[i]) != 0) { + if (ioq_thread_create(ioq, i) != 0) { ioq->nthreads = i; goto fail; } @@ -987,6 +1194,18 @@ static struct ioq_ent *ioq_request(struct ioq *ioq, enum ioq_op op, void *ptr) { return ent; } +int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr) { + struct ioq_ent *ent = ioq_request(ioq, IOQ_NOP, ptr); + if (!ent) { + return -1; + } + + ent->nop.type = type; + + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); + return 0; +} + int ioq_close(struct ioq *ioq, int fd, void *ptr) { struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSE, ptr); if (!ent) { @@ -995,7 +1214,7 @@ int ioq_close(struct ioq *ioq, int fd, void *ptr) { ent->close.fd = fd; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1011,7 +1230,7 @@ int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, args->path = path; args->flags = flags; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1023,7 +1242,7 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr) { ent->closedir.dir = dir; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1039,7 +1258,7 @@ int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags fla args->flags = flags; args->buf = buf; -#if BFS_USE_LIBURING && BFS_USE_STATX +#if BFS_WITH_LIBURING && BFS_USE_STATX args->xbuf = arena_alloc(&ioq->xbufs); if (!args->xbuf) { ioq_free(ioq, ent); @@ -1047,23 +1266,30 @@ int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags fla } #endif - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } +void ioq_submit(struct ioq *ioq) { + ioq_batch_flush(ioq->pending, &ioq->pending_batch); +} + struct ioq_ent *ioq_pop(struct ioq *ioq, bool block) { + // Don't forget to submit before popping + bfs_assert(ioq_batch_empty(&ioq->pending_batch)); + if (ioq->size == 0) { return NULL; } - return ioqq_pop(ioq->ready, block); + return ioq_batch_pop(ioq->ready, &ioq->ready_batch, block); } void ioq_free(struct ioq *ioq, struct ioq_ent *ent) { bfs_assert(ioq->size > 0); --ioq->size; -#if BFS_USE_LIBURING && BFS_USE_STATX +#if BFS_WITH_LIBURING && BFS_USE_STATX if (ent->op == IOQ_STAT && ent->stat.xbuf) { arena_free(&ioq->xbufs, ent->stat.xbuf); } @@ -1074,7 +1300,8 @@ void ioq_free(struct ioq *ioq, struct ioq_ent *ent) { void ioq_cancel(struct ioq *ioq) { if (!exchange(&ioq->cancel, true, relaxed)) { - ioqq_push(ioq->pending, &IOQ_STOP); + ioq_batch_push(ioq->pending, &ioq->pending_batch, &IOQ_STOP); + ioq_submit(ioq); } } @@ -1094,7 +1321,7 @@ void ioq_destroy(struct ioq *ioq) { ioqq_destroy(ioq->ready); ioqq_destroy(ioq->pending); -#if BFS_USE_LIBURING && BFS_USE_STATX +#if BFS_WITH_LIBURING && BFS_USE_STATX arena_destroy(&ioq->xbufs); #endif arena_destroy(&ioq->ents); @@ -8,9 +8,10 @@ #ifndef BFS_IOQ_H #define BFS_IOQ_H -#include "config.h" +#include "bfs.h" #include "dir.h" #include "stat.h" + #include <stddef.h> /** @@ -22,6 +23,8 @@ struct ioq; * I/O queue operations. */ enum ioq_op { + /** ioq_nop(). */ + IOQ_NOP, /** ioq_close(). */ IOQ_CLOSE, /** ioq_opendir(). */ @@ -33,18 +36,21 @@ enum ioq_op { }; /** - * The I/O queue implementation needs two tag bits in each pointer to a struct - * ioq_ent, so we need to ensure at least 4-byte alignment. The natural - * alignment is enough on most architectures, but not m68k, so over-align it. + * ioq_nop() types. */ -#define IOQ_ENT_ALIGN alignas(4) +enum ioq_nop_type { + /** A lightweight nop that avoids syscalls. */ + IOQ_NOP_LIGHT, + /** A heavyweight nop that involves a syscall. */ + IOQ_NOP_HEAVY, +}; /** * An I/O queue entry. */ struct ioq_ent { /** The I/O operation. */ - IOQ_ENT_ALIGN enum ioq_op op; + cache_align enum ioq_op op; /** The return value (on success) or negative error code (on failure). */ int result; @@ -54,6 +60,10 @@ struct ioq_ent { /** Operation-specific arguments. */ union { + /** ioq_nop() args. */ + struct ioq_nop { + enum ioq_nop_type type; + } nop; /** ioq_close() args. */ struct ioq_close { int fd; @@ -83,9 +93,9 @@ struct ioq_ent { /** * Create an I/O queue. * - * @param depth + * @depth * The maximum depth of the queue. - * @param nthreads + * @nthreads * The maximum number of background threads. * @return * The new I/O queue, or NULL on failure. @@ -98,13 +108,27 @@ struct ioq *ioq_create(size_t depth, size_t nthreads); size_t ioq_capacity(const struct ioq *ioq); /** + * A no-op, for benchmarking. + * + * @ioq + * The I/O queue. + * @type + * The type of operation to perform. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr); + +/** * Asynchronous close(). * - * @param ioq + * @ioq * The I/O queue. - * @param fd + * @fd * The fd to close. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -114,17 +138,17 @@ int ioq_close(struct ioq *ioq, int fd, void *ptr); /** * Asynchronous bfs_opendir(). * - * @param ioq + * @ioq * The I/O queue. - * @param dir + * @dir * The allocated directory. - * @param dfd + * @dfd * The base file descriptor. - * @param path + * @path * The path to open, relative to dfd. - * @param flags + * @flags * Flags that control which directory entries are listed. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -134,11 +158,11 @@ int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, /** * Asynchronous bfs_closedir(). * - * @param ioq + * @ioq * The I/O queue. - * @param dir + * @dir * The directory to close. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -148,17 +172,17 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr); /** * Asynchronous bfs_stat(). * - * @param ioq + * @ioq * The I/O queue. - * @param dfd + * @dfd * The base file descriptor. - * @param path + * @path * The path to stat, relative to dfd. - * @param flags + * @flags * Flags that affect the lookup. - * @param buf + * @buf * A place to store the stat buffer, if successful. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -166,9 +190,14 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr); int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr); /** + * Submit any buffered requests. + */ +void ioq_submit(struct ioq *ioq); + +/** * Pop a response from the queue. * - * @param ioq + * @ioq * The I/O queue. * @return * The next response, or NULL. @@ -178,9 +207,9 @@ struct ioq_ent *ioq_pop(struct ioq *ioq, bool block); /** * Free a queue entry. * - * @param ioq + * @ioq * The I/O queue. - * @param ent + * @ent * The entry to free. */ void ioq_free(struct ioq *ioq, struct ioq_ent *ent); @@ -83,13 +83,14 @@ #define BFS_LIST_H #include "diag.h" + #include <stddef.h> #include <string.h> /** * Initialize a singly-linked list. * - * @param list + * @list * The list to initialize. * * --- @@ -116,9 +117,9 @@ /** * Initialize a singly-linked list item. * - * @param item + * @item * The item to initialize. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * * --- @@ -200,7 +201,7 @@ /** * Get the head of a singly-linked list. * - * @param list + * @list * The list in question. * @return * The first item in the list. @@ -227,9 +228,9 @@ /** * Get the tail of a singly-linked list. * - * @param list + * @list * The list in question. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * The last item in the list. @@ -246,11 +247,11 @@ /** * Check if an item is attached to a singly-linked list. * - * @param list + * @list * The list to check. - * @param item + * @item * The item to check. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * Whether the item is attached to the list. @@ -267,13 +268,13 @@ /** * Insert an item into a singly-linked list. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * A pointer to the item to insert after, e.g. &list->head or list->tail. - * @param item + * @item * The item to insert. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * A cursor for the next item. @@ -294,11 +295,11 @@ /** * Add an item to the tail of a singly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to append. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define SLIST_APPEND(list, ...) \ @@ -310,11 +311,11 @@ /** * Add an item to the head of a singly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to prepend. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define SLIST_PREPEND(list, ...) \ @@ -324,27 +325,43 @@ LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__)) /** + * Splice a singly-linked list into another. + * + * @dest + * The destination list. + * @cursor + * A pointer to the item to splice after, e.g. &list->head or list->tail. + * @src + * The source list. + */ +#define SLIST_SPLICE(dest, cursor, src) \ + LIST_VOID_(SLIST_SPLICE_((dest), (cursor), (src))) + +#define SLIST_SPLICE_(dest, cursor, src) \ + *src->tail = *cursor, \ + *cursor = src->head, \ + dest->tail = *dest->tail ? src->tail : dest->tail, \ + SLIST_INIT(src) + +/** * Add an entire singly-linked list to the tail of another. * - * @param dest + * @dest * The destination list. - * @param src + * @src * The source list. */ #define SLIST_EXTEND(dest, src) \ - SLIST_EXTEND_((dest), (src)) - -#define SLIST_EXTEND_(dest, src) \ - (src->head ? (*dest->tail = src->head, dest->tail = src->tail, SLIST_INIT(src)) : (void)0) + SLIST_SPLICE(dest, (dest)->tail, src) /** * Remove an item from a singly-linked list. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * A pointer to the item to remove, either &list->head or &prev->next. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * The removed item. @@ -357,10 +374,10 @@ #define SLIST_REMOVE__(list, cursor, next) \ (list->tail = (*cursor)->next ? list->tail : cursor, \ - slist_remove_impl(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) + slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) // Helper for SLIST_REMOVE() -static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_t size) { +static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) { // ret = *cursor; // *cursor = ret->next; memcpy(cursor, next, size); @@ -372,9 +389,9 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Pop the head off a singly-linked list. * - * @param list + * @list * The list to modify. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. * @return * The popped item, or NULL if the list was empty. @@ -391,13 +408,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Loop over the items in a singly-linked list. * - * @param type + * @type * The list item type. - * @param item + * @item * The induction variable name. - * @param list + * @list * The list to iterate. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. */ #define for_slist(type, item, ...) \ @@ -412,9 +429,24 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ item = _next) /** + * Loop over a singly-linked list, popping each item. + * + * @type + * The list item type. + * @item + * The induction variable name. + * @list + * The list to drain. + * @node (optional) + * If specified, use head->node.next rather than head->next. + */ +#define drain_slist(type, item, ...) \ + for (type *item; (item = SLIST_POP(__VA_ARGS__));) + +/** * Initialize a doubly-linked list. * - * @param list + * @list * The list to initialize. */ #define LIST_INIT(list) \ @@ -433,9 +465,9 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Initialize a doubly-linked list item. * - * @param item + * @item * The item to initialize. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define LIST_ITEM_INIT(...) \ @@ -465,11 +497,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Add an item to the tail of a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to append. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_APPEND(list, ...) \ @@ -478,11 +510,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Add an item to the head of a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to prepend. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_PREPEND(list, ...) \ @@ -491,11 +523,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Check if an item is attached to a doubly-linked list. * - * @param list + * @list * The list to check. - * @param item + * @item * The item to check. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. * @return * Whether the item is attached to the list. @@ -512,13 +544,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Insert into a doubly-linked list after the given cursor. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * Insert after this element. - * @param item + * @item * The item to insert. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_INSERT(list, cursor, ...) \ @@ -537,11 +569,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Remove an item from a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to remove. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_REMOVE(list, ...) \ @@ -558,13 +590,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Loop over the items in a doubly-linked list. * - * @param type + * @type * The list item type. - * @param item + * @item * The induction variable name. - * @param list + * @list * The list to iterate. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. */ #define for_list(type, item, ...) \ @@ -20,13 +20,14 @@ * - bftw.[ch] (an extended version of nftw(3)) * * - Utilities: + * - prelude.h (feature test macros; automatically included) * - alloc.[ch] (memory allocation) * - atomic.h (atomic operations) * - bar.[ch] (a terminal status bar) * - bit.h (bit manipulation) + * - bfs.h (configuration and fundamental utilities) * - bfstd.[ch] (standard library wrappers/polyfills) * - color.[ch] (for pretty terminal colors) - * - config.h (configuration and feature/platform detection) * - diag.[ch] (formats diagnostic messages) * - dir.[ch] (a directory API facade) * - dstring.[ch] (a dynamic string library) @@ -36,21 +37,23 @@ * - mtab.[ch] (parses the system's mount table) * - pwcache.[ch] (a cache for the user/group tables) * - sanity.h (sanitizer interfaces) + * - sighook.[ch] (signal hooks) * - stat.[ch] (wraps stat(), or statx() on Linux) * - thread.h (multi-threading) * - trie.[ch] (a trie set/map implementation) * - typo.[ch] (fuzzy matching for typos) + * - version.c (embeds version information) * - xregex.[ch] (regular expression support) * - xspawn.[ch] (spawns processes) * - xtime.[ch] (date/time handling utilities) */ #include "bfstd.h" -#include "config.h" #include "ctx.h" #include "diag.h" #include "eval.h" #include "parse.h" + #include <errno.h> #include <fcntl.h> #include <locale.h> @@ -2,23 +2,27 @@ // SPDX-License-Identifier: 0BSD #include "mtab.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> -#if !defined(BFS_USE_MNTENT) && BFS_USE_MNTENT_H -# define BFS_USE_MNTENT true -#elif !defined(BFS_USE_MNTINFO) && BSD -# define BFS_USE_MNTINFO true -#elif !defined(BFS_USE_MNTTAB) && __SVR4 -# define BFS_USE_MNTTAB true +#ifndef BFS_USE_MNTENT +# define BFS_USE_MNTENT BFS_HAS_GETMNTENT_1 +#endif +#ifndef BFS_USE_MNTINFO +# define BFS_USE_MNTINFO (!BFS_USE_MNTENT && BFS_HAS_GETMNTINFO) +#endif +#ifndef BFS_USE_MNTTAB +# define BFS_USE_MNTTAB (!BFS_USE_MNTINFO && BFS_HAS_GETMNTENT_2) #endif #if BFS_USE_MNTENT @@ -27,7 +31,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 +44,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; @@ -61,30 +69,39 @@ struct bfs_mtab { /** * Add an entry to the mount table. */ -attr(maybe_unused) +_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 +111,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 +151,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 +214,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; @@ -237,10 +256,7 @@ static int bfs_mtab_fill_types(struct bfs_mtab *mtab) { continue; } - struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev)); - if (leaf) { - leaf->value = mount->type; - } else { + if (trie_set_mem(&mtab->types, &sb.mnt_id, sizeof(sb.mnt_id), mount->type) != 0) { goto fail; } } @@ -263,9 +279,9 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb } } - const struct trie_leaf *leaf = trie_find_mem(&mtab->types, &statbuf->dev, sizeof(statbuf->dev)); - if (leaf) { - return leaf->value; + const char *type = trie_get_mem(&mtab->types, &statbuf->mnt_id, sizeof(statbuf->mnt_id)); + if (type) { + return type; } else { return "unknown"; } @@ -280,11 +296,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); } @@ -8,8 +8,6 @@ #ifndef BFS_MTAB_H #define BFS_MTAB_H -#include "config.h" - struct bfs_stat; /** @@ -28,9 +26,9 @@ struct bfs_mtab *bfs_mtab_parse(void); /** * Determine the file system type that a file is on. * - * @param mtab + * @mtab * The current mount table. - * @param statbuf + * @statbuf * The bfs_stat() buffer for the file in question. * @return * The type of file system containing this file, "unknown" if not known, @@ -41,9 +39,9 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb /** * Check if a file could be a mount point. * - * @param mtab + * @mtab * The current mount table. - * @param name + * @name * The name of the file to check. * @return * Whether the named file could be a mount point. @@ -26,10 +26,12 @@ */ #include "opt.h" + +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "bit.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "diag.h" #include "dir.h" @@ -38,6 +40,8 @@ #include "expr.h" #include "list.h" #include "pwcache.h" +#include "xspawn.h" + #include <errno.h> #include <limits.h> #include <stdarg.h> @@ -102,42 +106,23 @@ enum pred_type { PRED_TYPES, }; -/** Get the name of a predicate type. */ -static const char *pred_type_name(enum pred_type type) { - switch (type) { - case READABLE_PRED: - return "-readable"; - case WRITABLE_PRED: - return "-writable"; - case EXECUTABLE_PRED: - return "-executable"; - case ACL_PRED: - return "-acl"; - case CAPABLE_PRED: - return "-capable"; - case EMPTY_PRED: - return "-empty"; - case HIDDEN_PRED: - return "-hidden"; - case NOGROUP_PRED: - return "-nogroup"; - case NOUSER_PRED: - return "-nouser"; - case SPARSE_PRED: - return "-sparse"; - case XATTR_PRED: - return "-xattr"; - - case PRED_TYPES: - break; - } - - bfs_bug("Unknown predicate %d", (int)type); - return "???"; -} +/** Predicate type names. */ +static const char *const pred_names[] = { + [READABLE_PRED] = "-readable", + [WRITABLE_PRED] = "-writable", + [EXECUTABLE_PRED] = "-executable", + [ACL_PRED] = "-acl", + [CAPABLE_PRED] = "-capable", + [EMPTY_PRED] = "-empty", + [HIDDEN_PRED] = "-hidden", + [NOGROUP_PRED] = "-nogroup", + [NOUSER_PRED] = "-nouser", + [SPARSE_PRED] = "-sparse", + [XATTR_PRED] = "-xattr", +}; /** - * A contrained integer range. + * A constrained integer range. */ struct df_range { /** The (inclusive) minimum value. */ @@ -192,11 +177,17 @@ static void constrain_min(struct df_range *range, long long value) { range->min = max_value(range->min, value); } -/** Contrain the maximum of a range. */ +/** Constrain the maximum of a range. */ static void constrain_max(struct df_range *range, long long value) { range->max = min_value(range->max, value); } +/** Constrain a range to a single value. */ +static void constrain_range(struct df_range *range, long long value) { + constrain_min(range, value); + constrain_max(range, value); +} + /** Remove a single value from a range. */ static void range_remove(struct df_range *range, long long value) { if (range->min == value) { @@ -242,29 +233,15 @@ enum range_type { RANGE_TYPES, }; -/** Get the name of a range type. */ -static const char *range_type_name(enum range_type type) { - switch (type) { - case DEPTH_RANGE: - return "-depth"; - case GID_RANGE: - return "-gid"; - case INUM_RANGE: - return "-inum"; - case LINKS_RANGE: - return "-links"; - case SIZE_RANGE: - return "-size"; - case UID_RANGE: - return "-uid"; - - case RANGE_TYPES: - break; - } - - bfs_bug("Unknown range %d", (int)type); - return "???"; -} +/** Range type names. */ +static const char *const range_names[] = { + [DEPTH_RANGE] = "-depth", + [GID_RANGE] = "-gid", + [INUM_RANGE] = "-inum", + [LINKS_RANGE] = "-links", + [SIZE_RANGE] = "-size", + [UID_RANGE] = "-uid", +}; /** * The data flow analysis domain. @@ -333,27 +310,27 @@ static void df_init_top(struct df_domain *value) { /** Check for the top element. */ static bool df_is_top(const struct df_domain *value) { - for (int i = 0; i < PRED_TYPES; ++i) { - if (value->preds[i] != PRED_TOP) { - return false; - } - } + for (int i = 0; i < PRED_TYPES; ++i) { + if (value->preds[i] != PRED_TOP) { + return false; + } + } - for (int i = 0; i < RANGE_TYPES; ++i) { - if (!range_is_top(&value->ranges[i])) { - return false; - } - } + for (int i = 0; i < RANGE_TYPES; ++i) { + if (!range_is_top(&value->ranges[i])) { + return false; + } + } - if (value->types != ~0U) { - return false; - } + if (value->types != ~0U) { + return false; + } - if (value->xtypes != ~0U) { - return false; - } + if (value->xtypes != ~0U) { + return false; + } - return true; + return true; } /** Compute the union of two fact sets. */ @@ -397,7 +374,7 @@ struct bfs_opt { }; /** Log an optimization. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_debug(struct bfs_opt *opt, const char *format, ...) { if (bfs_debug_prefix(opt->ctx, DEBUG_OPT)) { for (int i = 0; i < opt->depth; ++i) { @@ -415,7 +392,7 @@ static bool opt_debug(struct bfs_opt *opt, const char *format, ...) { } /** Log a recursive call. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_enter(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; if (depth > 0) { @@ -435,7 +412,7 @@ static bool opt_enter(struct bfs_opt *opt, const char *format, ...) { } /** Log a recursive return. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_leave(struct bfs_opt *opt, const char *format, ...) { bool debug = false; int depth = opt->depth; @@ -459,7 +436,7 @@ static bool opt_leave(struct bfs_opt *opt, const char *format, ...) { } /** Log a shallow visit. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_visit(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; if (depth > 0) { @@ -479,7 +456,7 @@ static bool opt_visit(struct bfs_opt *opt, const char *format, ...) { } /** Log the deletion of an expression. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_delete(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; @@ -503,7 +480,7 @@ typedef bool dump_fn(struct bfs_opt *opt, const char *format, ...); /** Print a df_pred. */ static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum pred_type type) { - dump(opt, "${blu}%s${rs}: ", pred_type_name(type)); + dump(opt, "${blu}%s${rs}: ", pred_names[type]); FILE *file = opt->ctx->cerr->file; switch (value->preds[type]) { @@ -524,7 +501,7 @@ static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain /** Print a df_range. */ static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) { - dump(opt, "${blu}%s${rs}: ", range_type_name(type)); + dump(opt, "${blu}%s${rs}: ", range_names[type]); FILE *file = opt->ctx->cerr->file; const struct df_range *range = &value->ranges[type]; @@ -641,22 +618,26 @@ static bool is_const(const struct bfs_expr *expr) { } /** Warn about an expression. */ -attr(printf(3, 4)) -static void opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { +_printf(3, 4) +static bool opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { if (!opt->warn) { - return; + return false; } if (bfs_expr_is_parent(expr) || is_const(expr)) { - return; + return false; } - if (bfs_expr_warning(opt->ctx, expr)) { - va_list args; - va_start(args, format); - bfs_vwarning(opt->ctx, format, args); - va_end(args); + if (!bfs_expr_warning(opt->ctx, expr)) { + return false; } + + va_list args; + va_start(args, format); + bfs_vwarning(opt->ctx, format, args); + va_end(args); + + return true; } /** Remove and return an expression's children. */ @@ -756,9 +737,7 @@ static struct bfs_expr *visit_and(struct bfs_opt *opt, struct bfs_expr *expr, co df_init_bottom(&opt->after_false); struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -790,9 +769,7 @@ static struct bfs_expr *visit_or(struct bfs_opt *opt, struct bfs_expr *expr, con df_init_bottom(&opt->after_true); struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -822,9 +799,7 @@ static struct bfs_expr *visit_comma(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -947,8 +922,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); } @@ -1084,7 +1063,7 @@ static struct bfs_expr *annotate_and(struct bfs_opt *opt, struct bfs_expr *expr, expr->cost = 0.0; expr->probability = 1.0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { expr->pure &= child->pure; expr->always_true &= child->always_true; expr->always_false |= child->always_false; @@ -1103,7 +1082,7 @@ static struct bfs_expr *annotate_or(struct bfs_opt *opt, struct bfs_expr *expr, expr->cost = 0.0; float false_prob = 1.0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { expr->pure &= child->pure; expr->always_true |= child->always_true; expr->always_false &= child->always_false; @@ -1120,7 +1099,7 @@ static struct bfs_expr *annotate_comma(struct bfs_opt *opt, struct bfs_expr *exp expr->pure = true; expr->cost = 0.0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { expr->pure &= child->pure; expr->always_true = child->always_true; expr->always_false = child->always_false; @@ -1356,7 +1335,7 @@ static struct bfs_expr *opt_const(struct bfs_opt *opt, bool value) { static bfs_eval_fn *const fns[] = {eval_false, eval_true}; static char *fake_args[] = {"-false", "-true"}; - struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value]); + struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value], BFS_TEST); return visit_shallow(opt, expr, &annotate); } @@ -1370,7 +1349,7 @@ static struct bfs_expr *negate_expr(struct bfs_opt *opt, struct bfs_expr *expr, return opt_const(opt, true); } - struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv); + struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv, BFS_OPERATOR); if (!ret) { return NULL; } @@ -1399,8 +1378,7 @@ static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { opt_enter(opt, "%pe\n", child); child = negate_expr(opt, child, argv); @@ -1418,18 +1396,16 @@ static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *exp /** Sink a negation into a comma expression. */ static struct bfs_expr *sink_not_comma(struct bfs_opt *opt, struct bfs_expr *expr) { - bfs_assert(expr->eval_fn == eval_comma); - - opt_enter(opt, "%pe\n", expr); - char **argv = expr->argv; expr = only_child(expr); + opt_enter(opt, "%pe\n", expr); + + bfs_assert(expr->eval_fn == eval_comma); struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { opt_enter(opt, "%pe\n", child); opt_debug(opt, "sink\n"); @@ -1457,7 +1433,6 @@ static struct bfs_expr *canonicalize_not(struct bfs_opt *opt, struct bfs_expr *e if (rhs->eval_fn == eval_not) { opt_debug(opt, "double negation\n"); - rhs = only_child(expr); return only_child(rhs); } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) { return sink_not_andor(opt, expr); @@ -1479,8 +1454,7 @@ static struct bfs_expr *canonicalize_assoc(struct bfs_opt *opt, struct bfs_expr struct bfs_exprs flat; SLIST_INIT(&flat); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (child->eval_fn == expr->eval_fn) { struct bfs_expr *head = SLIST_HEAD(&child->children); struct bfs_expr *tail = SLIST_TAIL(&child->children); @@ -1588,8 +1562,7 @@ static struct bfs_expr *reorder_andor(struct bfs_opt *opt, struct bfs_expr *expr struct bfs_exprs pure; SLIST_INIT(&pure); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (child->pure) { SLIST_APPEND(&pure, child); } else { @@ -1630,8 +1603,7 @@ static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enu switch (expr->int_cmp) { case BFS_INT_EQUAL: - constrain_min(true_range, value); - constrain_max(true_range, value); + constrain_range(true_range, value); range_remove(false_range, value); break; @@ -1651,14 +1623,31 @@ static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enu /** Transfer function for -{execut,read,writ}able. */ static struct bfs_expr *data_flow_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { - if (expr->num & R_OK) { + switch (expr->num) { + case R_OK: data_flow_pred(opt, READABLE_PRED, true); - } - if (expr->num & W_OK) { + break; + case W_OK: data_flow_pred(opt, WRITABLE_PRED, true); - } - if (expr->num & X_OK) { + break; + case X_OK: data_flow_pred(opt, EXECUTABLE_PRED, true); + break; + default: + bfs_bug("Unknown access() mode %lld", expr->num); + break; + } + + return expr; +} + +/** Transfer function for -empty. */ +static struct bfs_expr *data_flow_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.types &= (1 << BFS_REG) | (1 << BFS_DIR); + + if (opt->before.types == (1 << BFS_REG)) { + constrain_range(&opt->after_true.ranges[SIZE_RANGE], 0); + range_remove(&opt->after_false.ranges[SIZE_RANGE], 0); } return expr; @@ -1671,7 +1660,7 @@ static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr gid_t gid = range->min; bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid); if (errno == 0) { - data_flow_pred(opt, NOGROUP_PRED, nogroup); + constrain_pred(&opt->after_true.preds[NOGROUP_PRED], nogroup); } } @@ -1702,11 +1691,16 @@ static struct bfs_expr *data_flow_links(struct bfs_opt *opt, struct bfs_expr *ex return expr; } +/** Transfer function for -lname. */ +static struct bfs_expr *data_flow_lname(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.types &= 1 << BFS_LNK; + return expr; +} + /** Transfer function for -samefile. */ static struct bfs_expr *data_flow_samefile(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { struct df_range *true_range = &opt->after_true.ranges[INUM_RANGE]; - constrain_min(true_range, expr->ino); - constrain_max(true_range, expr->ino); + constrain_range(true_range, expr->ino); struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE]; range_remove(false_range, expr->ino); @@ -1740,7 +1734,7 @@ static struct bfs_expr *data_flow_uid(struct bfs_opt *opt, struct bfs_expr *expr uid_t uid = range->min; bool nouser = !bfs_getpwuid(opt->ctx->users, uid); if (errno == 0) { - data_flow_pred(opt, NOUSER_PRED, nouser); + constrain_pred(&opt->after_true.preds[NOUSER_PRED], nouser); } } @@ -1786,7 +1780,7 @@ static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *ex if (df_is_bottom(&opt->after_false)) { if (!expr->pure) { expr->always_true = true; - expr->probability = 0.0; + expr->probability = 1.0; } else if (expr->eval_fn != eval_true) { opt_warning(opt, expr, "This expression is always true.\n\n"); opt_debug(opt, "pure, always true\n"); @@ -1814,12 +1808,45 @@ static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *ex return visit_leave(opt, expr, visitor); } -/** Data flow visitor function. */ -static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { - if (opt->ignore_result && expr->pure) { +/** Ignore an expression (and possibly warn/prompt). */ +static struct bfs_expr *opt_ignore(struct bfs_opt *opt, struct bfs_expr *expr, bool delete) { + if (delete) { + opt_delete(opt, "%pe [ignored result]\n", expr); + } else { opt_debug(opt, "ignored result\n"); - opt_warning(opt, expr, "The result of this expression is ignored.\n\n"); + } + + if (expr->kind != BFS_TEST) { + goto done; + } + + if (!opt_warning(opt, expr, "The result of this expression is ignored.\n")) { + goto done; + } + + struct bfs_ctx *ctx = opt->ctx; + if (ctx->interactive && ctx->dangerous) { + bfs_warning(ctx, "Do you want to continue? "); + if (ynprompt() <= 0) { + errno = 0; + return NULL; + } + } + + fprintf(stderr, "\n"); + +done: + if (!delete && expr->pure) { + // If we're not deleting the expression entirely, replace it with -false expr = opt_const(opt, false); + } + return expr; +} + +/** Data flow visitor function. */ +static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (opt->ignore_result) { + expr = opt_ignore(opt, expr, false); if (!expr) { return NULL; } @@ -1889,9 +1916,11 @@ static const struct visitor data_flow = { .leave = data_flow_leave, .table = (const struct visitor_table[]) { {eval_access, data_flow_access}, + {eval_empty, data_flow_empty}, {eval_gid, data_flow_gid}, {eval_inum, data_flow_inum}, {eval_links, data_flow_links}, + {eval_lname, data_flow_lname}, {eval_samefile, data_flow_samefile}, {eval_size, data_flow_size}, {eval_type, data_flow_type}, @@ -1915,7 +1944,7 @@ static struct bfs_expr *simplify_not(struct bfs_opt *opt, struct bfs_expr *expr, static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *expr) { // Only lift negations if it would reduce the number of (-not) expressions size_t added = 0, removed = 0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (child->eval_fn == eval_not) { ++removed; } else { @@ -1940,8 +1969,7 @@ static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { opt_enter(opt, "%pe\n", child); child = negate_expr(opt, child, &fake_not_arg); @@ -1954,6 +1982,10 @@ static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *exp } expr = visit_shallow(opt, expr, &annotate); + if (!expr) { + return NULL; + } + return negate_expr(opt, expr, &fake_not_arg); } @@ -1964,7 +1996,7 @@ static struct bfs_expr *first_ignorable(struct bfs_opt *opt, struct bfs_expr *ex } struct bfs_expr *ret = NULL; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { if (!child->pure) { ret = NULL; } else if (!ret) { @@ -1983,16 +2015,15 @@ static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (child == ignorable) { ignore = true; } if (ignore) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2005,8 +2036,8 @@ static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, bfs_expr_append(expr, child); if (child->always_false) { - while ((child = SLIST_POP(&children))) { - opt_delete(opt, "%pe [short-circuit]\n", child); + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); } } } @@ -2031,16 +2062,15 @@ static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (child == ignorable) { ignore = true; } if (ignore) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2053,8 +2083,8 @@ static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, bfs_expr_append(expr, child); if (child->always_true) { - while ((child = SLIST_POP(&children))) { - opt_delete(opt, "%pe [short-circuit]\n", child); + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); } } } @@ -2076,12 +2106,11 @@ static struct bfs_expr *simplify_comma(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2132,6 +2161,8 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { }; struct df_domain impure; + df_init_top(&opt->after_true); + df_init_top(&opt->after_false); for (int i = 0; i < 3; ++i) { struct bfs_opt nested = *opt; @@ -2145,9 +2176,11 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { continue; } + const struct visitor *visitor = passes[j].visitor; + // Skip reordering the first time through the passes, to // make warnings more understandable - if (passes[j].visitor == &reorder) { + if (visitor == &reorder) { if (i == 0) { continue; } else { @@ -2155,10 +2188,15 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { } } - expr = visit(&nested, expr, passes[j].visitor); + expr = visit(&nested, expr, visitor); if (!expr) { return NULL; } + + if (visitor == &data_flow) { + opt->after_true = nested.after_true; + opt->after_false = nested.after_false; + } } opt_leave(&nested, NULL); @@ -2172,17 +2210,20 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { return expr; } -/** Estimate the odds of an expression calling stat(). */ -static float expr_stat_odds(struct bfs_expr *expr) { - if (expr->calls_stat) { +/** An expression predicate. */ +typedef bool expr_pred(const struct bfs_expr *expr); + +/** Estimate the odds that a matching expression will be evaluated. */ +static float estimate_odds(const struct bfs_expr *expr, expr_pred *pred) { + if (pred(expr)) { return 1.0; } - float nostat_odds = 1.0; + float nonmatch_odds = 1.0; float reached_odds = 1.0; - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { - float child_odds = expr_stat_odds(child); - nostat_odds *= 1.0 - reached_odds * child_odds; + for_expr (child, expr) { + float child_odds = estimate_odds(child, pred); + nonmatch_odds *= 1.0 - reached_odds * child_odds; if (expr->eval_fn == eval_and) { reached_odds *= child->probability; @@ -2191,7 +2232,12 @@ static float expr_stat_odds(struct bfs_expr *expr) { } } - return 1.0 - nostat_odds; + return 1.0 - nonmatch_odds; +} + +/** Whether an expression calls stat(). */ +static bool calls_stat(const struct bfs_expr *expr) { + return expr->calls_stat; } /** Estimate the odds of calling stat(). */ @@ -2200,15 +2246,20 @@ static float estimate_stat_odds(struct bfs_ctx *ctx) { return 1.0; } - float nostat_odds = 1.0 - expr_stat_odds(ctx->exclude); + float nostat_odds = 1.0 - estimate_odds(ctx->exclude, calls_stat); float reached_odds = 1.0 - ctx->exclude->probability; - float expr_odds = expr_stat_odds(ctx->expr); + float expr_odds = estimate_odds(ctx->expr, calls_stat); nostat_odds *= 1.0 - reached_odds * expr_odds; return 1.0 - nostat_odds; } +/** Matches -(exec|ok) ... \; */ +static bool single_exec(const struct bfs_expr *expr) { + return expr->eval_fn == eval_exec && !(expr->exec->flags & BFS_EXEC_MULTI); +} + int bfs_optimize(struct bfs_ctx *ctx) { bfs_ctx_dump(ctx, DEBUG_OPT); @@ -2287,6 +2338,17 @@ int bfs_optimize(struct bfs_ctx *ctx) { opt_leave(&opt, "eager stat cost: ${ylw}%g${rs}\n", eager_cost); } +#ifndef POSIX_SPAWN_SETRLIMIT + // If bfs_spawn_setrlimit() would force us to use fork() over + // posix_spawn(), the extra cost may outweigh the benefit of a + // higher RLIMIT_NOFILE + float single_exec_odds = estimate_odds(ctx->expr, single_exec); + if (single_exec_odds >= 0.5) { + opt_enter(&opt, "single ${blu}-exec${rs} odds: ${ylw}%g${rs}\n", single_exec_odds); + ctx->raise_nofile = false; + opt_leave(&opt, "not raising RLIMIT_NOFILE\n"); + } +#endif } opt_leave(&opt, NULL); @@ -13,7 +13,7 @@ struct bfs_ctx; /** * Apply optimizations to the command line. * - * @param ctx + * @ctx * The bfs context to optimize. * @return * 0 if successful, -1 on error. diff --git a/src/parse.c b/src/parse.c index 2dfcab2..5ec4c0e 100644 --- a/src/parse.c +++ b/src/parse.c @@ -9,11 +9,12 @@ */ #include "parse.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "diag.h" #include "dir.h" @@ -31,6 +32,7 @@ #include "xregex.h" #include "xspawn.h" #include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <fnmatch.h> @@ -78,14 +80,10 @@ struct bfs_parser { /** Whether stdout is a terminal. */ bool stdout_tty; - /** Whether this session is interactive (stdin and stderr are each a terminal). */ - bool interactive; /** Whether -color or -nocolor has been passed. */ enum use_color use_color; /** Whether a -print action is implied. */ bool implicit_print; - /** Whether the default root "." should be used. */ - bool implicit_root; /** Whether the expression has started. */ bool expr_started; /** Whether an information option like -help or -version was passed. */ @@ -95,44 +93,26 @@ struct bfs_parser { /** The last non-path argument. */ char **last_arg; - /** A "-depth"-type argument, if any. */ - char **depth_arg; - /** A "-limit" argument, if any. */ - char **limit_arg; - /** A "-prune" argument, if any. */ - char **prune_arg; - /** A "-mount" argument, if any. */ - char **mount_arg; - /** An "-xdev" argument, if any. */ - char **xdev_arg; - /** A "-files0-from -" argument, if any. */ - char **files0_stdin_arg; - /** An "-ok"-type expression, if any. */ - const struct bfs_expr *ok_expr; + /** A "-depth"-type expression, if any. */ + const struct bfs_expr *depth_expr; + /** A "-limit" expression, if any. */ + const struct bfs_expr *limit_expr; + /** A "-prune" expression, if any. */ + const struct bfs_expr *prune_expr; + /** A "-mount" expression, if any. */ + const struct bfs_expr *mount_expr; + /** An "-xdev" expression, if any. */ + const struct bfs_expr *xdev_expr; + /** A "-files0-from" expression, if any. */ + const struct bfs_expr *files0_expr; + /** An expression that consumes stdin, if any. */ + const struct bfs_expr *stdin_expr; /** The current time (maybe modified by -daystart). */ struct timespec now; }; /** - * Possible token types. - */ -enum token_type { - /** A flag. */ - T_FLAG, - /** A root path. */ - T_PATH, - /** An option. */ - T_OPTION, - /** A test. */ - T_TEST, - /** An action. */ - T_ACTION, - /** An operator. */ - T_OPERATOR, -}; - -/** * Print a low-level error message during parsing. */ static void parse_perror(const struct bfs_parser *parser, const char *str) { @@ -158,9 +138,8 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, /** * Print an error message during parsing. */ -attr(printf(2, 3)) +_printf(2, 3) static void parse_error(const struct bfs_parser *parser, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -170,7 +149,6 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ... va_list args; va_start(args, format); - errno = error; bfs_verror(parser->ctx, format, args); va_end(args); } @@ -178,9 +156,8 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ... /** * Print an error about some command line arguments. */ -attr(printf(4, 5)) +_printf(4, 5) static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -190,7 +167,6 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -198,20 +174,18 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ /** * Print an error about conflicting command line arguments. */ -attr(printf(6, 7)) -static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; +_printf(4, 5) +static void parse_conflict_error(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); bfs_argv_error(ctx, highlight); va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -219,16 +193,14 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, /** * Print an error about an expression. */ -attr(printf(3, 4)) +_printf(3, 4) static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bfs_expr_error(ctx, expr); va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -236,9 +208,8 @@ static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_e /** * Print a warning message during parsing. */ -attr(printf(2, 3)) +_printf(2, 3) static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -250,7 +221,6 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(parser->ctx, format, args); va_end(args); return ret; @@ -259,22 +229,20 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . /** * Print a warning about conflicting command line arguments. */ -attr(printf(6, 7)) -static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; +_printf(4, 5) +static bool parse_conflict_warning(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); if (!bfs_argv_warning(ctx, highlight)) { return false; } va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(ctx, format, args); va_end(args); return ret; @@ -283,9 +251,8 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1 /** * Print a warning about an expression. */ -attr(printf(3, 4)) +_printf(3, 4) static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; if (!bfs_expr_warning(ctx, expr)) { @@ -294,17 +261,31 @@ static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(ctx, format, args); va_end(args); return ret; } /** + * Report an error if stdin is already consumed, then consume it. + */ +static bool consume_stdin(struct bfs_parser *parser, const struct bfs_expr *expr) { + if (parser->stdin_expr) { + parse_conflict_error(parser, parser->stdin_expr, expr, + "%pX and %pX can't both use standard input.\n", + parser->stdin_expr, expr); + return false; + } + + parser->stdin_expr = expr; + return true; +} + +/** * Allocate a new expression. */ -static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv) { - struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv); +static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) { + struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv, kind); if (!expr) { parse_perror(parser, "bfs_expr_new()"); } @@ -315,7 +296,7 @@ static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval * Create a new unary expression. */ static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv); + struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR); if (!expr) { return NULL; } @@ -329,7 +310,7 @@ static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval * Create a new binary expression. */ static struct bfs_expr *new_binary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv); + struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR); if (!expr) { return NULL; } @@ -381,7 +362,7 @@ static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const cha return 0; fail: - parse_expr_error(parser, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); if (cfile) { cfclose(cfile); } else if (file) { @@ -401,7 +382,7 @@ static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat int ret = bfs_stat(AT_FDCWD, *arg, flags, sb); if (ret != 0) { - parse_argv_error(parser, arg, 1, "%m.\n"); + parse_argv_error(parser, arg, 1, "%s.\n", errstr()); } return ret; } @@ -414,15 +395,20 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser); /** * Advance by a single token. */ -static char **parser_advance(struct bfs_parser *parser, enum token_type type, size_t argc) { - if (type != T_FLAG && type != T_PATH) { +static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size_t argc) { + struct bfs_ctx *ctx = parser->ctx; + + if (kind != BFS_FLAG && kind != BFS_PATH) { parser->expr_started = true; } - if (type != T_PATH) { + if (kind != BFS_PATH) { parser->last_arg = parser->argv; } + size_t i = parser->argv - ctx->argv; + ctx->kinds[i] = kind; + char **argv = parser->argv; parser->argv += argc; return argv; @@ -446,7 +432,6 @@ static int parse_root(struct bfs_parser *parser, const char *path) { return -1; } - parser->implicit_root = false; return 0; } @@ -465,7 +450,7 @@ static int skip_paths(struct bfs_parser *parser) { // find uses -- to separate flags from the rest // of the command line. We allow mixing flags // and paths/predicates, so we just ignore --. - parser_advance(parser, T_FLAG, 1); + parser_advance(parser, BFS_FLAG, 1); continue; } if (strcmp(arg, "-") != 0) { @@ -497,7 +482,7 @@ static int skip_paths(struct bfs_parser *parser) { return -1; } - parser_advance(parser, T_PATH, 1); + parser_advance(parser, BFS_PATH, 1); } } @@ -517,20 +502,14 @@ enum int_flags { * Parse an integer. */ static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) { - // strtoll() skips leading spaces, but we want to reject them - if (xisspace(str[0])) { - goto bad; - } - int base = flags & IF_BASE_MASK; if (base == 0) { base = 10; } char *endptr; - errno = 0; - long long value = strtoll(str, &endptr, base); - if (errno != 0) { + long long value; + if (xstrtoll(str, &endptr, base, &value) != 0) { if (errno == ERANGE) { goto range; } else { @@ -538,13 +517,6 @@ static const char *parse_int(const struct bfs_parser *parser, char **arg, const } } - // https://github.com/llvm/llvm-project/issues/64946 - sanitize_init(&endptr); - - if (endptr == str) { - goto bad; - } - if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') { goto bad; } @@ -641,8 +613,8 @@ static bool looks_like_icmp(const char *str) { * Parse a single flag. */ static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) { - char **argv = parser_advance(parser, T_FLAG, argc); - return parse_new_expr(parser, eval_true, argc, argv); + char **argv = parser_advance(parser, BFS_FLAG, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_FLAG); } /** @@ -657,9 +629,11 @@ static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) { */ static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) { const char *arg = parser->argv[0]; + char flag = arg[strlen(arg) - 1]; + const char *value = parser->argv[1]; if (!value) { - parse_error(parser, "${cyn}%s${rs} needs a value.\n", arg); + parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag); return NULL; } @@ -667,11 +641,34 @@ static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) { } /** + * Parse a prefix flag like -O3, -j8, etc. + */ +static struct bfs_expr *parse_prefix_flag(struct bfs_parser *parser, char flag, bool allow_separate, const char **value) { + const char *arg = parser->argv[0]; + + const char *suffix = strchr(arg, flag) + 1; + if (*suffix) { + *value = suffix; + return parse_nullary_flag(parser); + } + + suffix = parser->argv[1]; + if (allow_separate && suffix) { + *value = suffix; + } else { + parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag); + return NULL; + } + + return parse_unary_flag(parser); +} + +/** * Parse a single option. */ static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) { - char **argv = parser_advance(parser, T_OPTION, argc); - return parse_new_expr(parser, eval_true, argc, argv); + char **argv = parser_advance(parser, BFS_OPTION, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_OPTION); } /** @@ -699,8 +696,8 @@ static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) { * Parse a single test. */ static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(parser, T_TEST, argc); - return parse_new_expr(parser, eval_fn, argc, argv); + char **argv = parser_advance(parser, BFS_TEST, argc); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_TEST); } /** @@ -728,7 +725,7 @@ static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn * Parse a single action. */ static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(parser, T_ACTION, argc); + char **argv = parser_advance(parser, BFS_ACTION, argc); if (parser->excluding) { parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n"); @@ -739,7 +736,7 @@ static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eva parser->implicit_print = false; } - return parse_new_expr(parser, eval_fn, argc, argv); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_ACTION); } /** @@ -811,7 +808,8 @@ static bool parse_debug_flag(const char *flag, size_t len, const char *expected) static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) { struct bfs_ctx *ctx = parser->ctx; - struct bfs_expr *expr = parse_unary_flag(parser); + const char *flags; + struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags); if (!expr) { cfprintf(ctx->cerr, "\n"); debug_help(ctx->cerr); @@ -820,7 +818,7 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg bool unrecognized = false; - for (const char *flag = expr->argv[1], *next; flag; flag = next) { + for (const char *flag = flags, *next; flag; flag = next) { size_t len = strcspn(flag, ","); if (flag[len]) { next = flag + len + 1; @@ -868,21 +866,22 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg * Parse -On. */ static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg); if (!expr) { return NULL; } int *optlevel = &parser->ctx->optlevel; - if (strcmp(expr->argv[0], "-Ofast") == 0) { + if (strcmp(arg, "fast") == 0) { *optlevel = 4; - } else if (!parse_int(parser, expr->argv, expr->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) { + } else if (!parse_int(parser, expr->argv, arg, optlevel, IF_INT | IF_UNSIGNED)) { return NULL; } if (*optlevel > 4) { - parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", expr->argv[0] + 2); + parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", arg); } return expr; @@ -996,16 +995,16 @@ static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg switch (*tail) { case 'w': time *= 7; - fallthru; + _fallthrough; case 'd': time *= 24; - fallthru; + _fallthrough; case 'h': time *= 60; - fallthru; + _fallthrough; case 'm': time *= 60; - fallthru; + _fallthrough; case 's': break; default: @@ -1076,6 +1075,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/9799919799/utilities/V3_chap02.html#tag_19_14_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) { @@ -1115,18 +1175,33 @@ static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int * Parse -delete. */ static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) { - parser->ctx->flags |= BFTW_POST_ORDER; - parser->depth_arg = parser->argv; - return parse_nullary_action(parser, eval_delete); + struct bfs_expr *expr = parse_nullary_action(parser, eval_delete); + if (!expr) { + return NULL; + } + + struct bfs_ctx *ctx = parser->ctx; + ctx->flags |= BFTW_POST_ORDER; + ctx->dangerous = true; + + parser->depth_expr = expr; + return expr; } /** * Parse -d. */ -static struct bfs_expr *parse_depth(struct bfs_parser *parser, int arg1, int arg2) { +static struct bfs_expr *parse_depth(struct bfs_parser *parser, int flag, int arg2) { + struct bfs_expr *expr = flag + ? parse_nullary_flag(parser) + : parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_POST_ORDER; - parser->depth_arg = parser->argv; - return parse_nullary_flag(parser); + parser->depth_expr = expr; + return expr; } /** @@ -1172,11 +1247,48 @@ static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg return expr; } +/** Check for unsafe relative paths in $PATH. */ +static const char *unsafe_path(const struct bfs_exec *execbuf) { + if (!(execbuf->flags & BFS_EXEC_CHDIR)) { + // Not -execdir or -okdir + return NULL; + } + + const char *exe = execbuf->tmpl_argv[0]; + if (strchr(exe, '/')) { + // No $PATH lookups for /foo or foo/bar + return NULL; + } + + if (strstr(exe, "{}")) { + // Substituted paths always contain a / + return NULL; + } + + const char *path = getenv("PATH"); + while (path) { + if (path[0] != '/') { + // Relative $PATH component! + return path; + } + + path = strchr(path, ':'); + if (path) { + ++path; + } + } + + // No relative components in $PATH + return NULL; +} + /** * Parse -exec(dir)?/-ok(dir)?. */ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) { - struct bfs_exec *execbuf = bfs_exec_parse(parser->ctx, parser->argv, flags); + struct bfs_ctx *ctx = parser->ctx; + + struct bfs_exec *execbuf = bfs_exec_parse(ctx, parser->argv, flags); if (!execbuf) { return NULL; } @@ -1192,29 +1304,21 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg // For pipe() in bfs_spawn() expr->ephemeral_fds = 2; - if (execbuf->flags & BFS_EXEC_CHDIR) { - // Check for relative paths in $PATH - const char *path = getenv("PATH"); - while (path) { - if (*path != '/') { - size_t len = strcspn(path, ":"); - char *comp = strndup(path, len); - if (comp) { - parse_expr_error(parser, expr, - "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp); - free(comp); - } else { - parse_perror(parser, "strndup()"); - } - return NULL; - } - - path = strchr(path, ':'); - if (path) { - ++path; - } + const char *unsafe = unsafe_path(execbuf); + if (unsafe) { + size_t len = strcspn(unsafe, ":"); + char *comp = strndup(unsafe, len); + if (comp) { + parse_expr_error(parser, expr, + "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp); + free(comp); + } else { + parse_perror(parser, "strndup()"); } + return NULL; + } + if (execbuf->flags & BFS_EXEC_CHDIR) { // To dup() the parent directory if (execbuf->flags & BFS_EXEC_MULTI) { ++expr->persistent_fds; @@ -1224,7 +1328,11 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg } if (execbuf->flags & BFS_EXEC_CONFIRM) { - parser->ok_expr = expr; + if (!consume_stdin(parser, expr)) { + return NULL; + } + } else { + ctx->dangerous = true; } return expr; @@ -1253,11 +1361,17 @@ static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2 * Parse -f PATH. */ static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; + struct bfs_expr *expr = parse_unary_flag(parser); if (!expr) { return NULL; } + // Mark the path as a path, not a regular argument + size_t i = expr->argv - ctx->argv; + ctx->kinds[i + 1] = BFS_PATH; + if (parse_root(parser, expr->argv[1]) != 0) { return NULL; } @@ -1274,50 +1388,14 @@ static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, i return NULL; } - const char *from = expr->argv[1]; - - FILE *file; - if (strcmp(from, "-") == 0) { - file = stdin; - } else { - file = xfopen(from, O_RDONLY | O_CLOEXEC); - } - if (!file) { - parse_expr_error(parser, expr, "%m.\n"); - return NULL; - } - - while (true) { - char *path = xgetdelim(file, '\0'); - if (!path) { - if (errno) { - goto fail; - } else { - break; - } - } - - int ret = parse_root(parser, path); - free(path); - if (ret != 0) { - goto fail; - } - } - - if (file == stdin) { - parser->files0_stdin_arg = expr->argv; - } else { - fclose(file); - } - - parser->implicit_root = false; + // For compatibility with GNU find, + // + // bfs -files0-from a -files0-from b + // + // should *only* use b, not a. So stash the expression here and only + // process the last one at the end of parsing. + parser->files0_expr = expr; return expr; - -fail: - if (file != stdin) { - fclose(file); - } - return NULL; } /** @@ -1448,7 +1526,7 @@ static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int ar } if (!bfs_ctx_mtab(parser->ctx)) { - parse_expr_error(parser, expr, "Couldn't parse the mount table: %m.\n"); + parse_expr_error(parser, expr, "Couldn't parse the mount table: %s.\n", errstr()); return NULL; } @@ -1473,7 +1551,7 @@ static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg return NULL; } } else if (errno) { - parse_expr_error(parser, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); return NULL; } else { parse_expr_error(parser, expr, "No such group.\n"); @@ -1516,7 +1594,7 @@ static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2 return NULL; } } else if (errno) { - parse_expr_error(parser, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); return NULL; } else { parse_expr_error(parser, expr, "No such user.\n"); @@ -1552,13 +1630,14 @@ static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2 * Parse -j<n>. */ static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'j', false, &arg); if (!expr) { return NULL; } unsigned int n; - if (!parse_int(parser, expr->argv, expr->argv[0] + 2, &n, IF_INT | IF_UNSIGNED)) { + if (!parse_int(parser, expr->argv, arg, &n, IF_INT | IF_UNSIGNED)) { return NULL; } @@ -1586,11 +1665,11 @@ static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg } if (expr->num <= 0) { - parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]); + parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr); return NULL; } - parser->limit_arg = expr->argv; + parser->limit_expr = expr; return expr; } @@ -1623,59 +1702,8 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg return NULL; } - parse_expr_warning(parser, expr, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", expr->argv[0]); - bfs_warning(parser->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n"); - - parser->ctx->flags |= BFTW_PRUNE_MOUNTS; - parser->mount_arg = expr->argv; - return expr; -} - -/** - * 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; - + parser->ctx->flags |= BFTW_SKIP_MOUNTS; + parser->mount_expr = expr; return expr; } @@ -1724,7 +1752,7 @@ static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr) if (xgetdate(expr->argv[1], &expr->reftime) == 0) { return 0; } else if (errno != EINVAL) { - parse_expr_error(parser, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); return -1; } @@ -1742,7 +1770,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; @@ -1818,6 +1846,14 @@ static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int a } /** + * Parse -noerror. + */ +static struct bfs_expr *parse_noerror(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->ignore_errors = true; + return parse_nullary_option(parser); +} + +/** * Parse -nogroup. */ static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) { @@ -1833,7 +1869,7 @@ static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int a * Parse -nohidden. */ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg); + struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg, BFS_TEST); if (!hidden) { return NULL; } @@ -1846,9 +1882,15 @@ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int * Parse -noleaf. */ static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) { - parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", - BFS_COMMAND, parser->argv[0]); - return parse_nullary_option(parser); + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + + parse_expr_warning(parser, expr, + "${ex}%s${rs} does not apply the optimization that %px inhibits.\n\n", + BFS_COMMAND, expr); + return expr; } /** @@ -1881,6 +1923,8 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct return 0; } + mode_t umask = parser->ctx->umask; + expr->file_mode = 0; expr->dir_mode = 0; @@ -1901,7 +1945,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct // // PERMCOPY : "u" | "g" | "o" - // Parser machine parser + // State machine state enum { MODE_CLAUSE, MODE_WHO, @@ -1909,7 +1953,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct MODE_ACTION_APPLY, MODE_OP, MODE_PERM, - } mparser = MODE_CLAUSE; + } state = MODE_CLAUSE; enum { MODE_PLUS, @@ -1918,16 +1962,18 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct } op uninit(MODE_EQUALS); mode_t who uninit(0); + mode_t mask uninit(0); mode_t file_change uninit(0); mode_t dir_change uninit(0); const char *i = mode; while (true) { - switch (mparser) { + switch (state) { case MODE_CLAUSE: who = 0; - mparser = MODE_WHO; - fallthru; + mask = 0777; + state = MODE_WHO; + _fallthrough; case MODE_WHO: switch (*i) { @@ -1944,7 +1990,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct who |= 0777; break; default: - mparser = MODE_ACTION; + state = MODE_ACTION; continue; } break; @@ -1954,7 +2000,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct case MODE_EQUALS: expr->file_mode &= ~who; expr->dir_mode &= ~who; - fallthru; + _fallthrough; case MODE_PLUS: expr->file_mode |= file_change; expr->dir_mode |= dir_change; @@ -1964,37 +2010,40 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct expr->dir_mode &= ~dir_change; break; } - fallthru; + _fallthrough; case MODE_ACTION: if (who == 0) { who = 0777; + mask = who & ~umask; + } else { + mask = who; } switch (*i) { case '+': op = MODE_PLUS; - mparser = MODE_OP; + state = MODE_OP; break; case '-': op = MODE_MINUS; - mparser = MODE_OP; + state = MODE_OP; break; case '=': op = MODE_EQUALS; - mparser = MODE_OP; + state = MODE_OP; break; case ',': - if (mparser == MODE_ACTION_APPLY) { - mparser = MODE_CLAUSE; + if (state == MODE_ACTION_APPLY) { + state = MODE_CLAUSE; } else { goto fail; } break; case '\0': - if (mparser == MODE_ACTION_APPLY) { + if (state == MODE_ACTION_APPLY) { goto done; } else { goto fail; @@ -2023,32 +2072,32 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct default: file_change = 0; dir_change = 0; - mparser = MODE_PERM; + state = MODE_PERM; continue; } file_change |= (file_change << 6) | (file_change << 3); - file_change &= who; + file_change &= mask; dir_change |= (dir_change << 6) | (dir_change << 3); - dir_change &= who; - mparser = MODE_ACTION_APPLY; + dir_change &= mask; + state = MODE_ACTION_APPLY; break; case MODE_PERM: switch (*i) { case 'r': - file_change |= who & 0444; - dir_change |= who & 0444; + file_change |= mask & 0444; + dir_change |= mask & 0444; break; case 'w': - file_change |= who & 0222; - dir_change |= who & 0222; + file_change |= mask & 0222; + dir_change |= mask & 0222; break; case 'x': - file_change |= who & 0111; - fallthru; + file_change |= mask & 0111; + _fallthrough; case 'X': - dir_change |= who & 0111; + dir_change |= mask & 0111; break; case 's': if (who & 0700) { @@ -2067,7 +2116,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct } break; default: - mparser = MODE_ACTION_APPLY; + state = MODE_ACTION_APPLY; continue; } break; @@ -2109,7 +2158,7 @@ static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg ++mode; break; } - fallthru; + _fallthrough; default: expr->mode_cmp = BFS_MODE_EQUAL; break; @@ -2177,8 +2226,13 @@ static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int ar * Parse -prune. */ static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) { - parser->prune_arg = parser->argv; - return parse_nullary_action(parser, eval_prune); + struct bfs_expr *expr = parse_nullary_action(parser, eval_prune); + if (!expr) { + return NULL; + } + + parser->prune_expr = expr; + return expr; } /** @@ -2240,16 +2294,27 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int // See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html const char *type = expr->argv[1]; if (strcmp(type, "posix-basic") == 0 + || strcmp(type, "posix-minimal-basic") == 0 || strcmp(type, "ed") == 0 || strcmp(type, "sed") == 0) { parser->regex_type = BFS_REGEX_POSIX_BASIC; } else if (strcmp(type, "posix-extended") == 0) { parser->regex_type = BFS_REGEX_POSIX_EXTENDED; -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA + } else if (strcmp(type, "awk") == 0 + || strcmp(type, "posix-awk") == 0) { + parser->regex_type = BFS_REGEX_AWK; + } else if (strcmp(type, "gnu-awk") == 0) { + parser->regex_type = BFS_REGEX_GNU_AWK; } else if (strcmp(type, "emacs") == 0) { parser->regex_type = BFS_REGEX_EMACS; } else if (strcmp(type, "grep") == 0) { parser->regex_type = BFS_REGEX_GREP; + } else if (strcmp(type, "egrep") == 0 + || strcmp(type, "posix-egrep") == 0) { + parser->regex_type = BFS_REGEX_EGREP; + } else if (strcmp(type, "findutils-default") == 0) { + parser->regex_type = BFS_REGEX_GNU_FIND; #endif } else if (strcmp(type, "help") == 0) { parser->just_info = true; @@ -2264,14 +2329,23 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int list_types: cfprintf(cfile, "Supported types are:\n\n"); - cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); - cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n"); - cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); -#if BFS_USE_ONIGURUMA - cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n"); - cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); + cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); + cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n"); + + cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n"); + +#if BFS_WITH_ONIGURUMA + cfprintf(cfile, " [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n"); + cfprintf(cfile, " ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n"); + + cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n"); + + cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n"); + + cfprintf(cfile, " ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n"); #endif - cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n"); return NULL; } @@ -2309,13 +2383,13 @@ static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg struct bfs_ctx *ctx = parser->ctx; CFILE *cfile = ctx->cerr; - struct bfs_expr *expr = parse_unary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg); if (!expr) { cfprintf(cfile, "\n"); goto list_strategies; } - const char *arg = expr->argv[1]; if (strcmp(arg, "bfs") == 0) { ctx->strategy = BFTW_BFS; } else if (strcmp(arg, "dfs") == 0) { @@ -2536,9 +2610,14 @@ static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int * Parse -xdev. */ static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_PRUNE_MOUNTS; - parser->xdev_arg = parser->argv; - return parse_nullary_option(parser); + parser->xdev_expr = expr; + return expr; } /** @@ -2731,8 +2810,9 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2 cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n"); cfprintf(cout, " Ignore files deeper/shallower than ${bld}N${rs}\n"); cfprintf(cout, " ${blu}-mount${rs}\n"); - cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs} for now, but will\n"); - cfprintf(cout, " skip mount points entirely in the future)\n"); + cfprintf(cout, " Exclude mount points entirely from the results\n"); + cfprintf(cout, " ${blu}-noerror${rs}\n"); + cfprintf(cout, " Ignore any errors that occur during traversal\n"); cfprintf(cout, " ${blu}-nohidden${rs}\n"); cfprintf(cout, " Exclude hidden files\n"); cfprintf(cout, " ${blu}-noleaf${rs}\n"); @@ -2768,6 +2848,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"); @@ -2905,18 +2989,60 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2 return NULL; } +/** Print the bfs "logo". */ +static void print_logo(CFILE *cout) { + if (!cout->colors) { + goto boring; + } + + size_t vwidth = xstrwidth(bfs_version); + dchar *spaces = dstrepeat(" ", vwidth); + dchar *lines = dstrepeat("─", vwidth); + if (!spaces || !lines) { + dstrfree(lines); + dstrfree(spaces); + goto boring; + } + + // We do ----\r<emoji> rather than <emoji>--- so we don't have to assume + // anything about the width of the emoji + cfprintf(cout, "╭─────%s╮\r📂\n", lines); + cfprintf(cout, "├${ex}b${rs} %s │\n", spaces); + cfprintf(cout, "╰├${ex}f${rs} ${bld}%s${rs} │\n", bfs_version); + cfprintf(cout, " ╰├${ex}s${rs} %s │\n", spaces); + cfprintf(cout, " ╰──%s─╯\n\n", lines); + + dstrfree(lines); + dstrfree(spaces); + return; + +boring: + printf("%s %s\n\n", BFS_COMMAND, bfs_version); +} + /** * "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); + print_logo(parser->ctx->cout); - printf("%s\n", BFS_HOMEPAGE); + printf("Copyright © Tavian Barnes and the bfs contributors\n"); + printf("No rights reserved (https://opensource.org/license/0BSD)\n\n"); + + printf("CONFFLAGS := %s\n", bfs_confflags); + printf("CC := %s\n", bfs_cc); + printf("CPPFLAGS := %s\n", bfs_cppflags); + printf("CFLAGS := %s\n", bfs_cflags); + printf("LDFLAGS := %s\n", bfs_ldflags); + printf("LDLIBS := %s\n", bfs_ldlibs); + + printf("\n%s\n", BFS_HOMEPAGE); parser->just_info = true; return NULL; } +/** Parser callback function type. */ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2); /** @@ -2924,136 +3050,139 @@ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2) */ struct table_entry { char *arg; - enum token_type type; + enum bfs_kind kind; parse_fn *parse; int arg1; int arg2; bool prefix; + bool needs_arg; }; /** * The parse table for primary expressions. */ static const struct table_entry parse_table[] = { - {"--", T_FLAG}, - {"--help", T_ACTION, parse_help}, - {"--version", T_ACTION, parse_version}, - {"-Bmin", T_TEST, parse_min, BFS_STAT_BTIME}, - {"-Bnewer", T_TEST, parse_newer, BFS_STAT_BTIME}, - {"-Bsince", T_TEST, parse_since, BFS_STAT_BTIME}, - {"-Btime", T_TEST, parse_time, BFS_STAT_BTIME}, - {"-D", T_FLAG, parse_debug}, - {"-E", T_FLAG, parse_regex_extended}, - {"-H", T_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false}, - {"-L", T_FLAG, parse_follow, BFTW_FOLLOW_ALL, false}, - {"-O", T_FLAG, parse_optlevel, 0, 0, true}, - {"-P", T_FLAG, parse_follow, 0, false}, - {"-S", T_FLAG, parse_search_strategy}, - {"-X", T_FLAG, parse_xargs_safe}, - {"-a", T_OPERATOR}, - {"-acl", T_TEST, parse_acl}, - {"-amin", T_TEST, parse_min, BFS_STAT_ATIME}, - {"-and", T_OPERATOR}, - {"-anewer", T_TEST, parse_newer, BFS_STAT_ATIME}, - {"-asince", T_TEST, parse_since, BFS_STAT_ATIME}, - {"-atime", T_TEST, parse_time, BFS_STAT_ATIME}, - {"-capable", T_TEST, parse_capable}, - {"-cmin", T_TEST, parse_min, BFS_STAT_CTIME}, - {"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME}, - {"-color", T_OPTION, parse_color, true}, - {"-csince", T_TEST, parse_since, BFS_STAT_CTIME}, - {"-ctime", T_TEST, parse_time, BFS_STAT_CTIME}, - {"-d", T_FLAG, parse_depth}, - {"-daystart", T_OPTION, parse_daystart}, - {"-delete", T_ACTION, parse_delete}, - {"-depth", T_OPTION, parse_depth_n}, - {"-empty", T_TEST, parse_empty}, - {"-exclude", T_OPERATOR}, - {"-exec", T_ACTION, parse_exec, 0}, - {"-execdir", T_ACTION, parse_exec, BFS_EXEC_CHDIR}, - {"-executable", T_TEST, parse_access, X_OK}, - {"-exit", T_ACTION, parse_exit}, - {"-f", T_FLAG, parse_f}, - {"-false", T_TEST, parse_const, false}, - {"-files0-from", T_OPTION, parse_files0_from}, - {"-flags", T_TEST, parse_flags}, - {"-fls", T_ACTION, parse_fls}, - {"-follow", T_OPTION, parse_follow, BFTW_FOLLOW_ALL, true}, - {"-fprint", T_ACTION, parse_fprint}, - {"-fprint0", T_ACTION, parse_fprint0}, - {"-fprintf", T_ACTION, parse_fprintf}, - {"-fstype", T_TEST, parse_fstype}, - {"-gid", T_TEST, parse_group}, - {"-group", T_TEST, parse_group}, - {"-help", T_ACTION, parse_help}, - {"-hidden", T_TEST, parse_hidden}, - {"-ignore_readdir_race", T_OPTION, parse_ignore_races, true}, - {"-ilname", T_TEST, parse_lname, true}, - {"-iname", T_TEST, parse_name, true}, - {"-inum", T_TEST, parse_inum}, - {"-ipath", T_TEST, parse_path, true}, - {"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE}, - {"-iwholename", T_TEST, parse_path, true}, - {"-j", T_FLAG, parse_jobs, 0, 0, true}, - {"-limit", T_ACTION, parse_limit}, - {"-links", T_TEST, parse_links}, - {"-lname", T_TEST, parse_lname, false}, - {"-ls", T_ACTION, parse_ls}, - {"-maxdepth", T_OPTION, parse_depth_limit, false}, - {"-mindepth", T_OPTION, parse_depth_limit, true}, - {"-mmin", T_TEST, parse_min, BFS_STAT_MTIME}, - {"-mnewer", T_TEST, parse_newer, BFS_STAT_MTIME}, - {"-mount", T_OPTION, parse_mount}, - {"-msince", T_TEST, parse_since, BFS_STAT_MTIME}, - {"-mtime", T_TEST, parse_time, BFS_STAT_MTIME}, - {"-name", T_TEST, parse_name, false}, - {"-newer", T_TEST, parse_newer, BFS_STAT_MTIME}, - {"-newer", T_TEST, parse_newerxy, 0, 0, true}, - {"-nocolor", T_OPTION, parse_color, false}, - {"-nogroup", T_TEST, parse_nogroup}, - {"-nohidden", T_TEST, parse_nohidden}, - {"-noignore_readdir_race", T_OPTION, parse_ignore_races, false}, - {"-noleaf", T_OPTION, parse_noleaf}, - {"-not", T_OPERATOR}, - {"-nouser", T_TEST, parse_nouser}, - {"-nowarn", T_OPTION, parse_warn, false}, - {"-o", T_OPERATOR}, - {"-ok", T_ACTION, parse_exec, BFS_EXEC_CONFIRM}, - {"-okdir", T_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR}, - {"-or", T_OPERATOR}, - {"-path", T_TEST, parse_path, false}, - {"-perm", T_TEST, parse_perm}, - {"-print", T_ACTION, parse_print}, - {"-print0", T_ACTION, parse_print0}, - {"-printf", T_ACTION, parse_printf}, - {"-printx", T_ACTION, parse_printx}, - {"-prune", T_ACTION, parse_prune}, - {"-quit", T_ACTION, parse_quit}, - {"-readable", T_TEST, parse_access, R_OK}, - {"-regex", T_TEST, parse_regex, 0}, - {"-regextype", T_OPTION, parse_regextype}, - {"-rm", T_ACTION, parse_delete}, - {"-s", T_FLAG, parse_s}, - {"-samefile", T_TEST, parse_samefile}, - {"-since", T_TEST, parse_since, BFS_STAT_MTIME}, - {"-size", T_TEST, parse_size}, - {"-sparse", T_TEST, parse_sparse}, - {"-status", T_OPTION, parse_status}, - {"-true", T_TEST, parse_const, true}, - {"-type", T_TEST, parse_type, false}, - {"-uid", T_TEST, parse_user}, - {"-unique", T_OPTION, parse_unique}, - {"-used", T_TEST, parse_used}, - {"-user", T_TEST, parse_user}, - {"-version", T_ACTION, parse_version}, - {"-warn", T_OPTION, parse_warn, true}, - {"-wholename", T_TEST, parse_path, false}, - {"-writable", T_TEST, parse_access, W_OK}, - {"-x", T_FLAG, parse_xdev}, - {"-xattr", T_TEST, parse_xattr}, - {"-xattrname", T_TEST, parse_xattrname}, - {"-xdev", T_OPTION, parse_xdev}, - {"-xtype", T_TEST, parse_type, true}, + {"--", BFS_FLAG}, + {"--help", BFS_ACTION, parse_help}, + {"--version", BFS_ACTION, parse_version}, + {"-Bmin", BFS_TEST, parse_min, BFS_STAT_BTIME}, + {"-Bnewer", BFS_TEST, parse_newer, BFS_STAT_BTIME}, + {"-Bsince", BFS_TEST, parse_since, BFS_STAT_BTIME}, + {"-Btime", BFS_TEST, parse_time, BFS_STAT_BTIME}, + {"-D", BFS_FLAG, parse_debug, .prefix = true}, + {"-E", BFS_FLAG, parse_regex_extended}, + {"-H", BFS_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false}, + {"-L", BFS_FLAG, parse_follow, BFTW_FOLLOW_ALL, false}, + {"-O", BFS_FLAG, parse_optlevel, .prefix = true}, + {"-P", BFS_FLAG, parse_follow, 0, false}, + {"-S", BFS_FLAG, parse_search_strategy, .prefix = true}, + {"-X", BFS_FLAG, parse_xargs_safe}, + {"-a", BFS_OPERATOR}, + {"-acl", BFS_TEST, parse_acl}, + {"-amin", BFS_TEST, parse_min, BFS_STAT_ATIME}, + {"-and", BFS_OPERATOR}, + {"-anewer", BFS_TEST, parse_newer, BFS_STAT_ATIME}, + {"-asince", BFS_TEST, parse_since, BFS_STAT_ATIME}, + {"-atime", BFS_TEST, parse_time, BFS_STAT_ATIME}, + {"-capable", BFS_TEST, parse_capable}, + {"-cmin", BFS_TEST, parse_min, BFS_STAT_CTIME}, + {"-cnewer", BFS_TEST, parse_newer, BFS_STAT_CTIME}, + {"-color", BFS_OPTION, parse_color, true}, + {"-context", BFS_TEST, parse_context, true}, + {"-csince", BFS_TEST, parse_since, BFS_STAT_CTIME}, + {"-ctime", BFS_TEST, parse_time, BFS_STAT_CTIME}, + {"-d", BFS_FLAG, parse_depth, true}, + {"-daystart", BFS_OPTION, parse_daystart}, + {"-delete", BFS_ACTION, parse_delete}, + {"-depth", BFS_OPTION, parse_depth_n, false}, + {"-empty", BFS_TEST, parse_empty}, + {"-exclude", BFS_OPERATOR}, + {"-exec", BFS_ACTION, parse_exec, 0}, + {"-execdir", BFS_ACTION, parse_exec, BFS_EXEC_CHDIR}, + {"-executable", BFS_TEST, parse_access, X_OK}, + {"-exit", BFS_ACTION, parse_exit}, + {"-f", BFS_FLAG, parse_f, .needs_arg = true}, + {"-false", BFS_TEST, parse_const, false}, + {"-files0-from", BFS_OPTION, parse_files0_from}, + {"-flags", BFS_TEST, parse_flags}, + {"-fls", BFS_ACTION, parse_fls}, + {"-follow", BFS_OPTION, parse_follow, BFTW_FOLLOW_ALL, true}, + {"-fprint", BFS_ACTION, parse_fprint}, + {"-fprint0", BFS_ACTION, parse_fprint0}, + {"-fprintf", BFS_ACTION, parse_fprintf}, + {"-fstype", BFS_TEST, parse_fstype}, + {"-gid", BFS_TEST, parse_group}, + {"-group", BFS_TEST, parse_group}, + {"-help", BFS_ACTION, parse_help}, + {"-hidden", BFS_TEST, parse_hidden}, + {"-ignore_readdir_race", BFS_OPTION, parse_ignore_races, true}, + {"-ilname", BFS_TEST, parse_lname, true}, + {"-iname", BFS_TEST, parse_name, true}, + {"-inum", BFS_TEST, parse_inum}, + {"-ipath", BFS_TEST, parse_path, true}, + {"-iregex", BFS_TEST, parse_regex, BFS_REGEX_ICASE}, + {"-iwholename", BFS_TEST, parse_path, true}, + {"-j", BFS_FLAG, parse_jobs, .prefix = true}, + {"-limit", BFS_ACTION, parse_limit}, + {"-links", BFS_TEST, parse_links}, + {"-lname", BFS_TEST, parse_lname, false}, + {"-ls", BFS_ACTION, parse_ls}, + {"-maxdepth", BFS_OPTION, parse_depth_limit, false}, + {"-mindepth", BFS_OPTION, parse_depth_limit, true}, + {"-mmin", BFS_TEST, parse_min, BFS_STAT_MTIME}, + {"-mnewer", BFS_TEST, parse_newer, BFS_STAT_MTIME}, + {"-mount", BFS_OPTION, parse_mount}, + {"-msince", BFS_TEST, parse_since, BFS_STAT_MTIME}, + {"-mtime", BFS_TEST, parse_time, BFS_STAT_MTIME}, + {"-name", BFS_TEST, parse_name, false}, + {"-newer", BFS_TEST, parse_newer, BFS_STAT_MTIME}, + {"-newer", BFS_TEST, parse_newerxy, .prefix = true}, + {"-nocolor", BFS_OPTION, parse_color, false}, + {"-noerror", BFS_OPTION, parse_noerror}, + {"-nogroup", BFS_TEST, parse_nogroup}, + {"-nohidden", BFS_TEST, parse_nohidden}, + {"-noignore_readdir_race", BFS_OPTION, parse_ignore_races, false}, + {"-noleaf", BFS_OPTION, parse_noleaf}, + {"-not", BFS_OPERATOR}, + {"-nouser", BFS_TEST, parse_nouser}, + {"-nowarn", BFS_OPTION, parse_warn, false}, + {"-o", BFS_OPERATOR}, + {"-ok", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM}, + {"-okdir", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR}, + {"-or", BFS_OPERATOR}, + {"-path", BFS_TEST, parse_path, false}, + {"-perm", BFS_TEST, parse_perm}, + {"-print", BFS_ACTION, parse_print}, + {"-print0", BFS_ACTION, parse_print0}, + {"-printf", BFS_ACTION, parse_printf}, + {"-printx", BFS_ACTION, parse_printx}, + {"-prune", BFS_ACTION, parse_prune}, + {"-quit", BFS_ACTION, parse_quit}, + {"-readable", BFS_TEST, parse_access, R_OK}, + {"-regex", BFS_TEST, parse_regex, 0}, + {"-regextype", BFS_OPTION, parse_regextype}, + {"-rm", BFS_ACTION, parse_delete}, + {"-s", BFS_FLAG, parse_s}, + {"-samefile", BFS_TEST, parse_samefile}, + {"-since", BFS_TEST, parse_since, BFS_STAT_MTIME}, + {"-size", BFS_TEST, parse_size}, + {"-sparse", BFS_TEST, parse_sparse}, + {"-status", BFS_OPTION, parse_status}, + {"-true", BFS_TEST, parse_const, true}, + {"-type", BFS_TEST, parse_type, false}, + {"-uid", BFS_TEST, parse_user}, + {"-unique", BFS_OPTION, parse_unique}, + {"-used", BFS_TEST, parse_used}, + {"-user", BFS_TEST, parse_user}, + {"-version", BFS_ACTION, parse_version}, + {"-warn", BFS_OPTION, parse_warn, true}, + {"-wholename", BFS_TEST, parse_path, false}, + {"-writable", BFS_TEST, parse_access, W_OK}, + {"-x", BFS_FLAG, parse_xdev}, + {"-xattr", BFS_TEST, parse_xattr}, + {"-xattrname", BFS_TEST, parse_xattrname}, + {"-xdev", BFS_OPTION, parse_xdev}, + {"-xtype", BFS_TEST, parse_type, true}, {0}, }; @@ -3074,6 +3203,83 @@ static const struct table_entry *table_lookup(const char *arg) { return NULL; } +/** Look up a single-character flag in the parse table. */ +static const struct table_entry *flag_lookup(char flag) { + for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { + enum bfs_kind kind = entry->kind; + if (kind == BFS_FLAG && entry->arg[1] == flag && !entry->arg[2]) { + return entry; + } + } + + return NULL; +} + +/** Check for a multi-flag argument like -LEXO2. */ +static bool is_flag_group(const char *arg) { + // We enforce that at least one flag in a flag group must be a capital + // letter, to avoid ambiguity with primary expressions + bool has_upper = false; + + // Flags that take an argument must appear last + bool needs_arg = false; + + for (size_t i = 1; arg[i]; ++i) { + char c = arg[i]; + if (c >= 'A' && c <= 'Z') { + has_upper = true; + } + + if (needs_arg) { + return false; + } + + const struct table_entry *entry = flag_lookup(c); + if (!entry || !entry->parse) { + return false; + } + + if (entry->prefix) { + // The rest is the flag's argument + break; + } + + needs_arg |= entry->needs_arg; + } + + return has_upper; +} + +/** Parse a multi-flag argument. */ +static struct bfs_expr *parse_flag_group(struct bfs_parser *parser) { + struct bfs_expr *expr = NULL; + + char **start = parser->argv; + char **end = start; + const char *arg = start[0]; + + for (size_t i = 1; arg[i]; ++i) { + parser->argv = start; + + const struct table_entry *entry = flag_lookup(arg[i]); + expr = entry->parse(parser, entry->arg1, entry->arg2); + + if (parser->argv > end) { + end = parser->argv; + } + + if (!expr || entry->prefix) { + break; + } + } + + if (expr) { + bfs_assert(parser->argv == end, "Didn't eat enough tokens"); + } + + return expr; +} + /** Search for a fuzzy match in the parse table. */ static const struct table_entry *table_lookup_fuzzy(const char *arg) { const struct table_entry *best = NULL; @@ -3096,6 +3302,8 @@ static const struct table_entry *table_lookup_fuzzy(const char *arg) { * | ACTION */ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { + struct bfs_ctx *ctx = parser->ctx; + // Paths are already skipped at this point const char *arg = parser->argv[0]; @@ -3112,15 +3320,19 @@ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { } } + if (is_flag_group(arg)) { + return parse_flag_group(parser); + } + match = table_lookup_fuzzy(arg); - CFILE *cerr = parser->ctx->cerr; + CFILE *cerr = ctx->cerr; parse_error(parser, "Unknown argument; did you mean "); - switch (match->type) { - case T_FLAG: + switch (match->kind) { + case BFS_FLAG: cfprintf(cerr, "${cyn}%s${rs}?", match->arg); break; - case T_OPERATOR: + case BFS_OPERATOR: cfprintf(cerr, "${red}%s${rs}?", match->arg); break; default: @@ -3128,7 +3340,7 @@ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { break; } - if (!parser->interactive || !match->parse) { + if (!ctx->interactive || !match->parse) { fprintf(stderr, "\n"); goto unmatched; } @@ -3170,7 +3382,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { } if (strcmp(arg, "(") == 0) { - parser_advance(parser, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *expr = parse_expr(parser); if (!expr) { @@ -3187,7 +3399,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { return NULL; } - parser_advance(parser, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); return expr; } else if (strcmp(arg, "-exclude") == 0) { if (parser->excluding) { @@ -3195,7 +3407,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { return NULL; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); parser->excluding = true; struct bfs_expr *factor = parse_factor(parser); @@ -3206,9 +3418,9 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { parser->excluding = false; bfs_expr_append(parser->ctx->exclude, factor); - return parse_new_expr(parser, eval_true, parser->argv - argv, argv); + return parse_new_expr(parser, eval_true, parser->argv - argv, argv, BFS_OPERATOR); } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) { - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *factor = parse_factor(parser); if (!factor) { @@ -3248,7 +3460,7 @@ static struct bfs_expr *parse_term(struct bfs_parser *parser) { char **argv = &fake_and_arg; if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) { - argv = parser_advance(parser, T_OPERATOR, 1); + argv = parser_advance(parser, BFS_OPERATOR, 1); } struct bfs_expr *lhs = term; @@ -3285,7 +3497,7 @@ static struct bfs_expr *parse_clause(struct bfs_parser *parser) { break; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = clause; struct bfs_expr *rhs = parse_term(parser); @@ -3320,7 +3532,7 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) { break; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = expr; struct bfs_expr *rhs = parse_clause(parser); @@ -3334,10 +3546,79 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) { return expr; } +/** Handle -files0-from after parsing. */ +static int parse_files0_roots(struct bfs_parser *parser) { + const struct bfs_ctx *ctx = parser->ctx; + const struct bfs_expr *expr = parser->files0_expr; + + if (ctx->npaths > 0) { + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, expr->argv, expr->argc, highlight); + + for (size_t i = 0; i < ctx->argc; ++i) { + if (ctx->kinds[i] == BFS_PATH) { + highlight[i] = true; + } + } + + bfs_argv_error(ctx, highlight); + bfs_error(ctx, "Cannot combine %pX with explicit root paths.\n", expr); + return -1; + } + + const char *from = expr->argv[1]; + + FILE *file; + if (strcmp(from, "-") == 0) { + if (!consume_stdin(parser, expr)) { + return -1; + } + file = stdin; + } else { + file = xfopen(from, O_RDONLY | O_CLOEXEC); + } + if (!file) { + parse_expr_error(parser, expr, "%s.\n", errstr()); + return -1; + } + + while (true) { + char *path = xgetdelim(file, '\0'); + if (!path) { + if (errno) { + goto fail; + } else { + break; + } + } + + int ret = parse_root(parser, path); + free(path); + if (ret != 0) { + goto fail; + } + } + + if (file != stdin) { + fclose(file); + } + + return 0; + +fail: + if (file != stdin) { + fclose(file); + } + return -1; +} + /** * Parse the top-level expression. */ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { + struct bfs_ctx *ctx = parser->ctx; + if (skip_paths(parser) != 0) { return NULL; } @@ -3346,7 +3627,7 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { if (parser->argv[0]) { expr = parse_expr(parser); } else { - expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg); + expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg, BFS_TEST); } if (!expr) { return NULL; @@ -3357,16 +3638,26 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { return NULL; } + if (parser->files0_expr) { + if (parse_files0_roots(parser) != 0) { + return NULL; + } + } else if (ctx->npaths == 0) { + if (parse_root(parser, ".") != 0) { + return NULL; + } + } + if (parser->implicit_print) { - char **limit = parser->limit_arg; + const struct bfs_expr *limit = parser->limit_expr; if (limit) { - parse_argv_error(parser, parser->limit_arg, 2, - "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n", - limit[0], limit[0], limit[1]); + parse_expr_error(parser, limit, + "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n", + limit, limit); return NULL; } - struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg); + struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg, BFS_ACTION); if (!print) { return NULL; } @@ -3378,20 +3669,20 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { } } - if (parser->mount_arg && parser->xdev_arg) { - parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1, - "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", - parser->xdev_arg[0], parser->mount_arg[0]); + if (parser->mount_expr && parser->xdev_expr) { + parse_conflict_warning(parser, parser->mount_expr, parser->xdev_expr, + "%px is redundant in the presence of %px.\n\n", + parser->xdev_expr, parser->mount_expr); } - if (parser->ctx->warn && parser->depth_arg && parser->prune_arg) { - parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1, - "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", - parser->prune_arg[0], parser->depth_arg[0]); + if (ctx->warn && parser->depth_expr && parser->prune_expr) { + parse_conflict_warning(parser, parser->depth_expr, parser->prune_expr, + "%px does not work in the presence of %px.\n", + parser->prune_expr, parser->depth_expr); - if (parser->interactive) { - bfs_warning(parser->ctx, "Do you want to continue? "); - if (ynprompt() == 0) { + if (ctx->interactive) { + bfs_warning(ctx, "Do you want to continue? "); + if (ynprompt() <= 0) { return NULL; } } @@ -3399,13 +3690,6 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { fprintf(stderr, "\n"); } - if (parser->ok_expr && parser->files0_stdin_arg) { - parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2, - "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", - parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]); - return NULL; - } - return expr; } @@ -3440,7 +3724,7 @@ static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag ++rparens; } else { cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]); - for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + for_expr (child, expr) { int parens = child->next ? 0 : rparens + 1; dump_expr_multiline(ctx, flag, child, indent + 1, parens); } @@ -3587,6 +3871,12 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { goto fail; } + ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc); + if (!ctx->kinds) { + perror("zalloc()"); + goto fail; + } + enum use_color use_color = COLOR_AUTO; const char *no_color = getenv("NO_COLOR"); if (no_color && *no_color) { @@ -3625,6 +3915,7 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { } else { ctx->warn = stdin_tty; } + ctx->interactive = stdin_tty && stderr_tty; struct bfs_parser parser = { .ctx = ctx, @@ -3632,23 +3923,20 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { .command = ctx->argv[0], .regex_type = BFS_REGEX_POSIX_BASIC, .stdout_tty = stdout_tty, - .interactive = stdin_tty && stderr_tty, .use_color = use_color, .implicit_print = true, - .implicit_root = true, .just_info = false, .excluding = false, .last_arg = NULL, - .depth_arg = NULL, - .prune_arg = NULL, - .mount_arg = NULL, - .xdev_arg = NULL, - .files0_stdin_arg = NULL, - .ok_expr = NULL, + .depth_expr = NULL, + .prune_expr = NULL, + .mount_expr = NULL, + .xdev_expr = NULL, + .stdin_expr = NULL, .now = ctx->now, }; - ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg); + ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg, BFS_OPERATOR); if (!ctx->exclude) { goto fail; } @@ -3667,14 +3955,10 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { } if (bfs_optimize(ctx) != 0) { - bfs_perror(ctx, "bfs_optimize()"); - goto fail; - } - - if (ctx->npaths == 0 && parser.implicit_root) { - if (parse_root(&parser, ".") != 0) { - goto fail; + if (errno != 0) { + bfs_perror(ctx, "bfs_optimize()"); } + goto fail; } if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) { diff --git a/src/parse.h b/src/parse.h index 6895c9f..fcc8234 100644 --- a/src/parse.h +++ b/src/parse.h @@ -11,9 +11,9 @@ /** * Parse the command line. * - * @param argc + * @argc * The number of arguments. - * @param argv + * @argv * The arguments to parse. * @return * A new bfs context, or NULL on failure. diff --git a/src/prelude.h b/src/prelude.h new file mode 100644 index 0000000..de89a6c --- /dev/null +++ b/src/prelude.h @@ -0,0 +1,130 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Praeludium. + * + * This header is automatically included in every translation unit, before any + * other headers, so it can set feature test macros[1][2]. This sets up our own + * mini-dialect of C, which includes + * + * - Standard C17 and POSIX.1 2024 features + * - Portable and platform-specific extensions + * - Convenience macros like `bool`, `alignof`, etc. + * - Common compiler extensions like __has_include() + * + * Further bfs-specific utilities are defined in "bfs.h". + * + * [1]: https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html + * [2]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html + */ + +#ifndef BFS_PRELUDE_H +#define BFS_PRELUDE_H + +// Feature test macros + +/** + * Linux and BSD handle _POSIX_C_SOURCE differently: on Linux, it enables POSIX + * interfaces that are not visible by default. On BSD, it also *disables* most + * extensions, giving a strict POSIX environment. Since we want the extensions, + * we don't set _POSIX_C_SOURCE. + */ +// #define _POSIX_C_SOURCE 202405L + +/** openat() etc. */ +#define _ATFILE_SOURCE 1 + +/** BSD-derived extensions. */ +#define _BSD_SOURCE 1 + +/** glibc successor to _BSD_SOURCE. */ +#define _DEFAULT_SOURCE 1 + +/** GNU extensions. */ +#define _GNU_SOURCE 1 + +/** Use 64-bit off_t. */ +#define _FILE_OFFSET_BITS 64 + +/** Use 64-bit time_t. */ +#define _TIME_BITS 64 + +/** macOS extensions. */ +#if __APPLE__ +# define _DARWIN_C_SOURCE 1 +#endif + +/** Solaris extensions. */ +#if __sun +# define __EXTENSIONS__ 1 +// https://illumos.org/man/3C/getpwnam#standard-conforming +# define _POSIX_PTHREAD_SEMANTICS 1 +#endif + +/** QNX extensions. */ +#if __QNX__ +# define _QNX_SOURCE 1 +#endif + +// Get the convenience macros that became standard spellings in C23 +#if __STDC_VERSION__ < 202311L + +/** _Static_assert() => static_assert() */ +#include <assert.h> +/** _Alignas(), _Alignof() => alignas(), alignof() */ +#include <stdalign.h> +/** _Bool => bool, true, false */ +#include <stdbool.h> + +/** + * C23 deprecates `noreturn void` in favour of `[[noreturn]] void`, so we expose + * _noreturn instead with the other attributes in "bfs.h". + */ +// #include <stdnoreturn.h> + +/** Part of <threads.h>, but we don't use anything else from it. */ +#define thread_local _Thread_local + +#endif // !C23 + +// Feature detection + +// https://clang.llvm.org/docs/LanguageExtensions.html#has-attribute +#ifndef __has_attribute +# define __has_attribute(attr) false +#endif + +// https://clang.llvm.org/docs/LanguageExtensions.html#has-builtin +#ifndef __has_builtin +# define __has_builtin(builtin) false +#endif + +// https://en.cppreference.com/w/c/language/attributes#Attribute_testing +#ifndef __has_c_attribute +# define __has_c_attribute(attr) false +#endif + +// https://clang.llvm.org/docs/LanguageExtensions.html#has-feature-and-has-extension +#ifndef __has_feature +# define __has_feature(feat) false +#endif + +// https://en.cppreference.com/w/c/preprocessor/include +#ifndef __has_include +# define __has_include(header) false +#endif + +// Sanitizer macros (GCC defines these but Clang does not) + +#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__) +# define __SANITIZE_ADDRESS__ true +#endif +#if __has_feature(memory_sanitizer) && !defined(__SANITIZE_MEMORY__) +# define __SANITIZE_MEMORY__ true +#endif +#if __has_feature(thread_sanitizer) && !defined(__SANITIZE_THREAD__) +# define __SANITIZE_THREAD__ true +#endif + +#endif // BFS_PRELUDE_H diff --git a/src/printf.c b/src/printf.c index 487f039..30ec201 100644 --- a/src/printf.c +++ b/src/printf.c @@ -2,19 +2,22 @@ // SPDX-License-Identifier: 0BSD #include "printf.h" + #include "alloc.h" +#include "bfs.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" + #include <errno.h> #include <grp.h> #include <pwd.h> @@ -87,22 +90,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. */ +_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(). */ +_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 +139,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 +214,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 +226,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 +242,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 +250,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 +266,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 +277,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 +293,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 +322,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 +338,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 +350,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 +362,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 +384,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 +396,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 +408,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 +419,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 +427,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 +448,7 @@ static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF copybuf.nameoff -= offset; return cfprintf(cfile, "%pP", ©buf); } else { - return dyn_fprintf(cfile->file, fmt, ftwbuf->path + offset); + return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset); } } @@ -459,7 +460,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 +476,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 +487,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,68 +503,57 @@ 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) { - switch (type) { - case BFS_BLK: - return "b"; - case BFS_CHR: - return "c"; - case BFS_DIR: - return "d"; - case BFS_DOOR: - return "D"; - case BFS_FIFO: - return "p"; - case BFS_LNK: - return "l"; - case BFS_REG: - return "f"; - case BFS_SOCK: - return "s"; - default: - return "U"; + const char *const names[] = { + [BFS_BLK] = "b", + [BFS_CHR] = "c", + [BFS_DIR] = "d", + [BFS_DOOR] = "D", + [BFS_FIFO] = "p", + [BFS_LNK] = "l", + [BFS_PORT] = "P", + [BFS_REG] = "f", + [BFS_SOCK] = "s", + [BFS_WHT] = "w", + }; + + const char *name = NULL; + if ((size_t)type < countof(names)) { + name = names[type]; } + + return name ? name : "U"; } /** %y: type */ 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; + enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW); + const char *str; - if (ftwbuf->type != BFS_LNK) { - return bfs_printf_y(cfile, fmt, ftwbuf); - } - - 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 == 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 +561,19 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF return ret; } +/** %Z: SELinux context */ +_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. */ @@ -703,9 +706,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha case '#': case '0': case '+': - must_be_numeric = true; - fallthru; case ' ': + must_be_numeric = true; + _fallthrough; case '-': if (strchr(fmt.str, c)) { bfs_expr_error(ctx, expr); @@ -840,6 +843,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/printf.h b/src/printf.h index 2bff087..e8d862e 100644 --- a/src/printf.h +++ b/src/printf.h @@ -22,11 +22,11 @@ struct bfs_printf; /** * Parse a -printf format string. * - * @param ctx + * @ctx * The bfs context. - * @param expr + * @expr * The expression to fill in. - * @param format + * @format * The format string to parse. * @return * 0 on success, -1 on failure. @@ -36,11 +36,11 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha /** * Evaluate a parsed format string. * - * @param cfile + * @cfile * The CFILE to print to. - * @param format + * @format * The parsed printf format. - * @param ftwbuf + * @ftwbuf * The bftw() data for the current file. * @return * 0 on success, -1 on failure. diff --git a/src/pwcache.c b/src/pwcache.c index 79437d8..fa19dad 100644 --- a/src/pwcache.c +++ b/src/pwcache.c @@ -2,15 +2,14 @@ // SPDX-License-Identifier: 0BSD #include "pwcache.h" + #include "alloc.h" -#include "config.h" #include "trie.h" + #include <errno.h> #include <grp.h> #include <pwd.h> #include <stdlib.h> -#include <string.h> -#include <unistd.h> /** Represents cache hits for negative results. */ static void *MISSING = &MISSING; diff --git a/src/pwcache.h b/src/pwcache.h index b6c0b67..d7c602d 100644 --- a/src/pwcache.h +++ b/src/pwcache.h @@ -27,9 +27,9 @@ struct bfs_users *bfs_users_new(void); /** * Get a user entry by name. * - * @param users + * @users * The user cache. - * @param name + * @name * The username to look up. * @return * The matching user, or NULL if not found (errno == 0) or an error @@ -40,9 +40,9 @@ const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name); /** * Get a user entry by ID. * - * @param users + * @users * The user cache. - * @param uid + * @uid * The ID to look up. * @return * The matching user, or NULL if not found (errno == 0) or an error @@ -53,7 +53,7 @@ const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid); /** * Flush a user cache. * - * @param users + * @users * The cache to flush. */ void bfs_users_flush(struct bfs_users *users); @@ -61,7 +61,7 @@ void bfs_users_flush(struct bfs_users *users); /** * Free a user cache. * - * @param users + * @users * The user cache to free. */ void bfs_users_free(struct bfs_users *users); @@ -82,9 +82,9 @@ struct bfs_groups *bfs_groups_new(void); /** * Get a group entry by name. * - * @param groups + * @groups * The group cache. - * @param name + * @name * The group name to look up. * @return * The matching group, or NULL if not found (errno == 0) or an error @@ -95,9 +95,9 @@ const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name); /** * Get a group entry by ID. * - * @param groups + * @groups * The group cache. - * @param uid + * @uid * The ID to look up. * @return * The matching group, or NULL if not found (errno == 0) or an error @@ -108,7 +108,7 @@ const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid); /** * Flush a group cache. * - * @param groups + * @groups * The cache to flush. */ void bfs_groups_flush(struct bfs_groups *groups); @@ -116,7 +116,7 @@ void bfs_groups_flush(struct bfs_groups *groups); /** * Free a group cache. * - * @param groups + * @groups * The group cache to free. */ void bfs_groups_free(struct bfs_groups *groups); diff --git a/src/sanity.h b/src/sanity.h index 423e6ff..be77eef 100644 --- a/src/sanity.h +++ b/src/sanity.h @@ -8,21 +8,8 @@ #ifndef BFS_SANITY_H #define BFS_SANITY_H -#include "config.h" #include <stddef.h> -#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) -# define SANITIZE_ADDRESS true -#endif - -#if __has_feature(memory_sanitizer) || defined(__SANITIZE_MEMORY__) -# define SANITIZE_MEMORY true -#endif - -#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__) -# define SANITIZE_THREAD true -#endif - // Call macro(ptr, size) or macro(ptr, sizeof(*ptr)) #define SANITIZE_CALL(...) \ SANITIZE_CALL_(__VA_ARGS__, ) @@ -33,7 +20,7 @@ #define SANITIZE_CALL__(macro, ptr, size, ...) \ macro(ptr, size) -#if SANITIZE_ADDRESS +#if __SANITIZE_ADDRESS__ # include <sanitizer/asan_interface.h> /** @@ -50,12 +37,30 @@ */ #define sanitize_free(...) SANITIZE_CALL(__asan_poison_memory_region, __VA_ARGS__) +/** + * Adjust the size of an allocated region, for things like dynamic arrays. + * + * @ptr + * The memory region. + * @old + * The previous usable size of the region. + * @new + * The new usable size of the region. + * @cap + * The total allocated capacity of the region. + */ +static inline void sanitize_resize(const void *ptr, size_t old, size_t new, size_t cap) { + const char *beg = ptr; + __sanitizer_annotate_contiguous_container(beg, beg + cap, beg + old, beg + new); +} + #else -# define sanitize_alloc sanitize_uninit -# define sanitize_free sanitize_uninit +# define sanitize_alloc(...) ((void)0) +# define sanitize_free(...) ((void)0) +# define sanitize_resize(ptr, old, new, cap) ((void)0) #endif -#if SANITIZE_MEMORY +#if __SANITIZE_MEMORY__ # include <sanitizer/msan_interface.h> /** @@ -73,19 +78,14 @@ #define sanitize_uninit(...) SANITIZE_CALL(__msan_allocated_memory, __VA_ARGS__) #else -# define sanitize_init(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) -# define sanitize_uninit(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) +# define sanitize_init(...) ((void)0) +# define sanitize_uninit(...) ((void)0) #endif /** - * Squelch unused variable warnings when not sanitizing. - */ -#define sanitize_ignore(ptr, size) ((void)(ptr), (void)(size)) - -/** * Initialize a variable, unless sanitizers would detect uninitialized uses. */ -#if SANITIZE_MEMORY +#if __SANITIZE_MEMORY__ # define uninit(value) #else # define uninit(value) = value diff --git a/src/sighook.c b/src/sighook.c new file mode 100644 index 0000000..a87bed5 --- /dev/null +++ b/src/sighook.c @@ -0,0 +1,692 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Dynamic (un)registration of signal handlers. + * + * Because signal handlers can interrupt any thread at an arbitrary point, they + * must be lock-free or risk deadlock. Therefore, we implement the global table + * of signal "hooks" with a simple read-copy-update (RCU) scheme. Readers get a + * reference-counted pointer (struct arc) to the table in a lock-free way, and + * release the reference count when finished. + * + * Updates are managed by struct rcu, which has two slots: one active and one + * inactive. Readers acquire a reference to the active slot. A single writer + * can safely update it by initializing the inactive slot, atomically swapping + * the slots, and waiting for the reference count of the newly inactive slot to + * drop to zero. Once it does, the old pointer can be safely freed. + */ + +#include "sighook.h" + +#include "alloc.h" +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "thread.h" + +#include <errno.h> +#include <pthread.h> +#include <signal.h> +#include <stdlib.h> +#include <unistd.h> + +#if __linux__ +# include <sys/syscall.h> +#endif + +// NetBSD opens a file descriptor for each sem_init() +#if defined(_POSIX_SEMAPHORES) && !__NetBSD__ +# define BFS_POSIX_SEMAPHORES _POSIX_SEMAPHORES +#else +# define BFS_POSIX_SEMAPHORES (-1) +#endif + +#if BFS_POSIX_SEMAPHORES >= 0 +# include <semaphore.h> +#endif + +/** + * An atomically reference-counted pointer. + */ +struct arc { + /** The current reference count (0 means empty). */ + atomic size_t refs; + /** The reference itself. */ + void *ptr; + +#if BFS_POSIX_SEMAPHORES >= 0 + /** A semaphore for arc_wait(). */ + sem_t sem; + /** sem_init() result. */ + int sem_status; +#endif +}; + +/** Initialize an arc. */ +static void arc_init(struct arc *arc) { + bfs_verify(atomic_is_lock_free(&arc->refs)); + + atomic_init(&arc->refs, 0); + arc->ptr = NULL; + +#if BFS_POSIX_SEMAPHORES >= 0 + if (sysoption(SEMAPHORES) > 0) { + arc->sem_status = sem_init(&arc->sem, false, 0); + } else { + arc->sem_status = -1; + } +#endif +} + +/** Get the current refcount. */ +static size_t arc_refs(const struct arc *arc) { + return load(&arc->refs, relaxed); +} + +/** Set the pointer in an empty arc. */ +static void arc_set(struct arc *arc, void *ptr) { + bfs_assert(arc_refs(arc) == 0); + bfs_assert(ptr); + + arc->ptr = ptr; + store(&arc->refs, 1, release); +} + +/** Acquire a reference. */ +static void *arc_get(struct arc *arc) { + size_t refs = arc_refs(arc); + do { + if (refs < 1) { + return NULL; + } + } while (!compare_exchange_weak(&arc->refs, &refs, refs + 1, acquire, relaxed)); + + return arc->ptr; +} + +/** Release a reference. */ +static void arc_put(struct arc *arc) { + size_t refs = fetch_sub(&arc->refs, 1, release); + + if (refs == 1) { +#if BFS_POSIX_SEMAPHORES >= 0 + if (arc->sem_status == 0 && sem_post(&arc->sem) != 0) { + abort(); + } +#endif + } +} + +/** Wait on the semaphore. */ +static int arc_sem_wait(struct arc *arc) { +#if BFS_POSIX_SEMAPHORES >= 0 + if (arc->sem_status == 0) { + while (sem_wait(&arc->sem) != 0) { + bfs_everify(errno == EINTR, "sem_wait()"); + } + return 0; + } +#endif + + return -1; +} + +/** Wait for all references to be released. */ +static void *arc_wait(struct arc *arc) { + size_t refs = fetch_sub(&arc->refs, 1, relaxed); + bfs_assert(refs > 0); + + --refs; + while (refs > 0) { + if (arc_sem_wait(arc) == 0) { + bfs_assert(arc_refs(arc) == 0); + // sem_wait() provides enough ordering, so we can skip the fence + goto done; + } + + // Some platforms (like macOS) don't support unnamed semaphores, + // but we can always busy-wait + spin_loop(); + refs = arc_refs(arc); + } + + thread_fence(&arc->refs, acquire); + +done:; + void *ptr = arc->ptr; + arc->ptr = NULL; + return ptr; +} + +/** Destroy an arc. */ +static void arc_destroy(struct arc *arc) { + bfs_assert(arc_refs(arc) == 0); + +#if BFS_POSIX_SEMAPHORES >= 0 + if (arc->sem_status == 0) { + bfs_everify(sem_destroy(&arc->sem) == 0, "sem_destroy()"); + } +#endif +} + +/** + * A simple read-copy-update memory reclamation scheme. + */ +struct rcu { + /** The currently active slot. */ + atomic size_t active; + /** The two slots. */ + struct arc slots[2]; +}; + +/** Sentinel value for RCU, since arc uses NULL already. */ +static void *RCU_NULL = &RCU_NULL; + +/** Map NULL -> RCU_NULL. */ +static void *rcu_encode(void *ptr) { + return ptr ? ptr : RCU_NULL; +} + +/** Map RCU_NULL -> NULL. */ +static void *rcu_decode(void *ptr) { + bfs_assert(ptr != NULL); + return ptr == RCU_NULL ? NULL : ptr; +} + +/** Initialize an RCU block. */ +static void rcu_init(struct rcu *rcu, void *ptr) { + bfs_verify(atomic_is_lock_free(&rcu->active)); + + atomic_init(&rcu->active, 0); + arc_init(&rcu->slots[0]); + arc_init(&rcu->slots[1]); + arc_set(&rcu->slots[0], rcu_encode(ptr)); +} + +/** Get the active slot. */ +static struct arc *rcu_active(struct rcu *rcu) { + size_t i = load(&rcu->active, relaxed); + return &rcu->slots[i]; +} + +/** Destroy an RCU block. */ +static void rcu_destroy(struct rcu *rcu) { + arc_wait(rcu_active(rcu)); + arc_destroy(&rcu->slots[1]); + arc_destroy(&rcu->slots[0]); +} + +/** Read an RCU-protected pointer. */ +static void *rcu_read(struct rcu *rcu, struct arc **slot) { + while (true) { + *slot = rcu_active(rcu); + void *ptr = arc_get(*slot); + if (ptr) { + return rcu_decode(ptr); + } + // Otherwise, the other slot became active; retry + } +} + +/** Get the RCU-protected pointer without acquiring a reference. */ +static void *rcu_peek(struct rcu *rcu) { + struct arc *arc = rcu_active(rcu); + return rcu_decode(arc->ptr); +} + +/** Update an RCU-protected pointer, and return the old one. */ +static void *rcu_update(struct rcu *rcu, void *ptr) { + size_t i = load(&rcu->active, relaxed); + struct arc *prev = &rcu->slots[i]; + + size_t j = i ^ 1; + struct arc *next = &rcu->slots[j]; + + arc_set(next, rcu_encode(ptr)); + store(&rcu->active, j, relaxed); + return rcu_decode(arc_wait(prev)); +} + +/** + * An RCU-protected linked list. + */ +struct rcu_list { + /** The first node in the list. */ + struct rcu head; + /** &last->next */ + struct rcu *tail; +}; + +/** + * An rcu_list node. + */ +struct rcu_node { + /** The RCU pointer to this node. */ + struct rcu *self; + /** The next node in the list. */ + struct rcu next; +}; + +/** Initialize an rcu_list. */ +static void rcu_list_init(struct rcu_list *list) { + rcu_init(&list->head, NULL); + list->tail = &list->head; +} + +/** Append to an rcu_list. */ +static void rcu_list_append(struct rcu_list *list, struct rcu_node *node) { + node->self = list->tail; + list->tail = &node->next; + rcu_init(&node->next, NULL); + rcu_update(node->self, node); +} + +/** Remove from an rcu_list. */ +static void rcu_list_remove(struct rcu_list *list, struct rcu_node *node) { + struct rcu_node *next = rcu_peek(&node->next); + rcu_update(node->self, next); + if (next) { + next->self = node->self; + } else { + list->tail = &list->head; + } + rcu_destroy(&node->next); +} + +/** + * Iterate over an rcu_list. + * + * It is save to `break` out of this loop, but `return` or `goto` will lead to + * a missed arc_put(). + */ +#define for_rcu(type, node, list) \ + for_rcu_(type, node, (list), node##_slot_, node##_prev_, node##_done_) + +#define for_rcu_(type, node, list, slot, prev, done) \ + for (struct arc *slot, *prev, **done = NULL; !done; arc_put(slot), done = &slot) \ + for (type *node = rcu_read(&list->head, &slot); \ + node; \ + prev = slot, \ + node = rcu_read(&((struct rcu_node *)node)->next, &slot), \ + arc_put(prev)) + +struct sighook { + /** The RCU list node (must be the first field). */ + struct rcu_node node; + + /** The signal being hooked, or 0 for atsigexit(). */ + int sig; + /** Signal hook flags. */ + enum sigflags flags; + /** The function to call. */ + sighook_fn *fn; + /** An argument to pass to the function. */ + void *arg; + /** Flag for SH_ONESHOT. */ + atomic bool armed; +}; + +/** The lists of signal hooks. */ +static struct rcu_list sighooks[64]; + +/** Get the hook list for a particular signal. */ +static struct rcu_list *siglist(int sig) { + return &sighooks[sig % countof(sighooks)]; +} + +/** Mutex for initialization and RCU writer exclusion. */ +static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER; + +/** Check if a signal was generated by userspace. */ +static bool is_user_generated(const siginfo_t *info) { + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_03 + // + // If si_code is SI_USER or SI_QUEUE, or any value less than or + // equal to 0, then the signal was generated by a process ... + int code = info->si_code; + return code == SI_USER || code == SI_QUEUE || code <= 0; +} + +/** Check if a signal is caused by a fault. */ +static bool is_fault(const siginfo_t *info) { + int sig = info->si_signo; + if (sig == SIGBUS || sig == SIGFPE || sig == SIGILL || sig == SIGSEGV) { + return !is_user_generated(info); + } else { + return false; + } +} + +// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/signal.h.html +static const int FATAL_SIGNALS[] = { + SIGABRT, + SIGALRM, + SIGBUS, + SIGFPE, + SIGHUP, + SIGILL, + SIGINT, +#ifdef SIGIO + SIGIO, +#endif + SIGPIPE, +#ifdef SIGPOLL + SIGPOLL, +#endif +#ifdef SIGPROF + SIGPROF, +#endif +#ifdef SIGPWR + SIGPWR, +#endif + SIGQUIT, + SIGSEGV, +#ifdef SIGSTKFLT + SIGSTKFLT, +#endif +#ifdef SIGSYS + SIGSYS, +#endif + SIGTERM, + SIGTRAP, + SIGUSR1, + SIGUSR2, +#ifdef SIGVTALRM + SIGVTALRM, +#endif + SIGXCPU, + SIGXFSZ, +}; + +/** Check if a signal's default action is to terminate the process. */ +static bool is_fatal(int sig) { + for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) { + if (sig == FATAL_SIGNALS[i]) { + return true; + } + } + +#ifdef SIGRTMIN + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_01 + // + // The default actions for the realtime signals in the range + // SIGRTMIN to SIGRTMAX shall be to terminate the process + // abnormally. + if (sig >= SIGRTMIN && sig <= SIGRTMAX) { + return true; + } +#endif + + return false; +} + +/** Reraise a fatal signal. */ +_noreturn +static void reraise(siginfo_t *info) { + int sig = info->si_signo; + + // Restore the default signal action + if (signal(sig, SIG_DFL) == SIG_ERR) { + goto fail; + } + + // Unblock the signal, since we didn't set SA_NODEFER + sigset_t mask; + if (sigemptyset(&mask) != 0 + || sigaddset(&mask, sig) != 0 + || pthread_sigmask(SIG_UNBLOCK, &mask, NULL) != 0) { + goto fail; + } + +#if __linux__ + // On Linux, try to re-raise the exact siginfo_t (since 3.9, a process can + // signal itself with any siginfo_t) + pid_t tid = syscall(SYS_gettid); + syscall(SYS_rt_tgsigqueueinfo, getpid(), tid, sig, info); +#endif + + raise(sig); +fail: + abort(); +} + +/** Check whether we should run a hook. */ +static bool should_run(int sig, struct sighook *hook) { + if (hook->sig != sig && hook->sig != 0) { + return false; + } + + if (hook->flags & SH_ONESHOT) { + if (!exchange(&hook->armed, false, relaxed)) { + return false; + } + } + + return true; +} + +/** Find any matching hooks and run them. */ +static enum sigflags run_hooks(struct rcu_list *list, int sig, siginfo_t *info) { + enum sigflags ret = 0; + + for_rcu (struct sighook, hook, list) { + if (should_run(sig, hook)) { + hook->fn(sig, info, hook->arg); + ret |= hook->flags; + } + } + + return ret; +} + +/** Dispatches a signal to the registered handlers. */ +static void sigdispatch(int sig, siginfo_t *info, void *context) { + // If we get a fault (e.g. a "real" SIGSEGV, not something like + // kill(..., SIGSEGV)), don't try to run signal hooks, since we could be + // in an arbitrarily corrupted state. + // + // POSIX says that returning normally from a signal handler for a fault + // is undefined. But in practice, it's better to uninstall the handler + // and return, which will re-run the faulting instruction and cause us + // to die "correctly" (e.g. with a core dump pointing at the faulting + // instruction, not reraise()). + if (is_fault(info)) { + // On macOS, we cannot reliably distinguish between faults and + // asynchronous signals. For example, pkill -SEGV bfs will + // result in si_code == SEGV_ACCERR. So we always re-raise the + // signal, because just returning would cause us to ignore + // asynchronous SIG{BUS,ILL,SEGV}. +#if !__APPLE__ + if (signal(sig, SIG_DFL) != SIG_ERR) { + return; + } +#endif + reraise(info); + } + + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_04 + // + // After returning from a signal-catching function, the value of + // errno is unspecified if the signal-catching function or any + // function it called assigned a value to errno and the signal- + // catching function did not save and restore the original value of + // errno. + int error = errno; + + // Run the normal hooks + struct rcu_list *list = siglist(sig); + enum sigflags flags = run_hooks(list, sig, info); + + // Run the atsigexit() hooks, if we're exiting + if (!(flags & SH_CONTINUE) && is_fatal(sig)) { + list = siglist(0); + run_hooks(list, sig, info); + reraise(info); + } + + errno = error; +} + +/** A saved signal handler, for sigreset() to restore. */ +struct sigsave { + struct rcu_node node; + int sig; + struct sigaction action; +}; + +/** The list of saved signal handlers. */ +static struct rcu_list saved; +/** `saved` initialization status (since rcu_list_init() isn't atomic). */ +static atomic bool initialized = false; + +/** Make sure our signal handler is installed for a given signal. */ +static int siginit(int sig) { +#ifdef SA_RESTART +# define BFS_SA_RESTART SA_RESTART +#else +# define BFS_SA_RESTART 0 +#endif + + static struct sigaction action = { + .sa_sigaction = sigdispatch, + .sa_flags = BFS_SA_RESTART | SA_SIGINFO, + }; + + static sigset_t signals; + + if (!load(&initialized, relaxed)) { + if (sigemptyset(&signals) != 0 + || sigemptyset(&action.sa_mask) != 0) { + return -1; + } + + for (size_t i = 0; i < countof(sighooks); ++i) { + rcu_list_init(&sighooks[i]); + } + + rcu_list_init(&saved); + store(&initialized, true, release); + } + + int installed = sigismember(&signals, sig); + if (installed < 0) { + return -1; + } else if (installed) { + return 0; + } + + sigset_t updated = signals; + if (sigaddset(&updated, sig) != 0) { + return -1; + } + + struct sigaction original; + if (sigaction(sig, NULL, &original) != 0) { + return -1; + } + + struct sigsave *save = ALLOC(struct sigsave); + if (!save) { + return -1; + } + + save->sig = sig; + save->action = original; + rcu_list_append(&saved, &save->node); + + if (sigaction(sig, &action, NULL) != 0) { + rcu_list_remove(&saved, &save->node); + free(save); + return -1; + } + + signals = updated; + return 0; +} + +/** Shared sighook()/atsigexit() implementation. */ +static struct sighook *sighook_impl(int sig, sighook_fn *fn, void *arg, enum sigflags flags) { + struct sighook *hook = ALLOC(struct sighook); + if (!hook) { + return NULL; + } + + hook->sig = sig; + hook->flags = flags; + hook->fn = fn; + hook->arg = arg; + atomic_init(&hook->armed, true); + + struct rcu_list *list = siglist(sig); + rcu_list_append(list, &hook->node); + return hook; +} + +struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) { + bfs_assert(sig > 0); + + mutex_lock(&sigmutex); + + struct sighook *ret = NULL; + if (siginit(sig) == 0) { + ret = sighook_impl(sig, fn, arg, flags); + } + + mutex_unlock(&sigmutex); + return ret; +} + +struct sighook *atsigexit(sighook_fn *fn, void *arg) { + mutex_lock(&sigmutex); + + for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) { + // Ignore errors; atsigexit() is best-effort anyway and things + // like sanitizer runtimes or valgrind may reserve signals for + // their own use + siginit(FATAL_SIGNALS[i]); + } + +#ifdef SIGRTMIN + for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) { + siginit(i); + } +#endif + + struct sighook *ret = sighook_impl(0, fn, arg, 0); + mutex_unlock(&sigmutex); + return ret; +} + +void sigunhook(struct sighook *hook) { + if (!hook) { + return; + } + + mutex_lock(&sigmutex); + + struct rcu_list *list = siglist(hook->sig); + rcu_list_remove(list, &hook->node); + + mutex_unlock(&sigmutex); + + free(hook); +} + +int sigreset(void) { + if (!load(&initialized, acquire)) { + return 0; + } + + int ret = 0; + + for_rcu (struct sigsave, save, &saved) { + if (sigaction(save->sig, &save->action, NULL) != 0) { + ret = -1; + break; + } + } + + return ret; +} diff --git a/src/sighook.h b/src/sighook.h new file mode 100644 index 0000000..7149229 --- /dev/null +++ b/src/sighook.h @@ -0,0 +1,83 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Signal hooks. + */ + +#ifndef BFS_SIGHOOK_H +#define BFS_SIGHOOK_H + +#include <signal.h> + +/** + * A dynamic signal hook. + */ +struct sighook; + +/** + * Signal hook flags. + */ +enum sigflags { + /** Suppress the default action for this signal. */ + SH_CONTINUE = 1 << 0, + /** Only run this hook once. */ + SH_ONESHOT = 1 << 1, +}; + +/** + * A signal hook callback. Hooks are executed from a signal handler, so must + * only call async-signal-safe functions. + * + * @sig + * The signal number. + * @info + * Additional information about the signal. + * @arg + * An arbitrary pointer passed to the hook. + */ +typedef void sighook_fn(int sig, siginfo_t *info, void *arg); + +/** + * Install a hook for a signal. + * + * @sig + * The signal to hook. + * @fn + * The function to call. + * @arg + * An argument passed to the function. + * @flags + * Flags for the new hook. + * @return + * The installed hook, or NULL on failure. + */ +struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags); + +/** + * On a best-effort basis, invoke the given hook just before the program is + * abnormally terminated by a signal. + * + * @fn + * The function to call. + * @arg + * An argument passed to the function. + * @return + * The installed hook, or NULL on failure. + */ +struct sighook *atsigexit(sighook_fn *fn, void *arg); + +/** + * Remove a signal hook. + */ +void sigunhook(struct sighook *hook); + +/** + * Restore all signal handlers to their original dispositions (e.g. after fork()). + * + * @return + * 0 on success, -1 on failure. + */ +int sigreset(void); + +#endif // BFS_SIGHOOK_H @@ -2,18 +2,20 @@ // SPDX-License-Identifier: 0BSD #include "stat.h" + #include "atomic.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "diag.h" #include "sanity.h" + #include <errno.h> #include <fcntl.h> #include <string.h> #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> @@ -49,9 +51,11 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) { return "change time"; case BFS_STAT_MTIME: return "modification time"; + case BFS_STAT_MNT_ID: + return "mount ID"; } - bfs_bug("Unrecognized stat field"); + bfs_bug("Unrecognized stat field %d", (int)field); return "???"; } @@ -62,7 +66,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 +103,33 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) { dest->rdev = src->st_rdev; dest->mask |= BFS_STAT_RDEV; -#if BSD + // No mount IDs in regular stat(), so use the dev_t as an approximation + dest->mnt_id = dest->dev; + dest->mask |= BFS_STAT_MNT_ID; + +#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_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 +151,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); @@ -161,6 +175,17 @@ int bfs_statx_flags(enum bfs_stat_flags flags) { return ret; } +unsigned int bfs_statx_mask(void) { + unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; +#ifdef STATX_MNT_ID + mask |= STATX_MNT_ID; +#endif +#ifdef STATX_MNT_ID_UNIQUE + mask |= STATX_MNT_ID_UNIQUE; +#endif + return mask; +} + int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { // Callers shouldn't have to check anything except the times const unsigned int guaranteed = STATX_BASIC_STATS & ~(STATX_ATIME | STATX_CTIME | STATX_MTIME); @@ -201,6 +226,18 @@ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { dest->attrs = src->stx_attributes; dest->mask |= BFS_STAT_ATTRS; + dest->mnt_id = dest->dev; +#ifdef STATX_MNT_ID + unsigned int mnt_mask = STATX_MNT_ID; +# ifdef STATX_MNT_ID_UNIQUE + mnt_mask |= STATX_MNT_ID_UNIQUE; +# endif + if (src->stx_mask & mnt_mask) { + dest->mnt_id = src->stx_mnt_id; + } +#endif + dest->mask |= BFS_STAT_MNT_ID; + if (src->stx_mask & STATX_ATIME) { dest->atime.tv_sec = src->stx_atime.tv_sec; dest->atime.tv_nsec = src->stx_atime.tv_nsec; @@ -232,7 +269,7 @@ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { * bfs_stat() implementation backed by statx(). */ static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) { - unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + unsigned int mask = bfs_statx_mask(); struct statx xbuf; int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); if (ret != 0) { @@ -294,27 +331,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) { @@ -12,24 +12,22 @@ #ifndef BFS_STAT_H #define BFS_STAT_H -#include "config.h" +#include "bfs.h" + +#include <stdint.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 +#if __has_include(<sys/param.h>) # include <sys/param.h> #endif @@ -59,6 +57,7 @@ enum bfs_stat_field { BFS_STAT_BTIME = 1 << 11, BFS_STAT_CTIME = 1 << 12, BFS_STAT_MTIME = 1 << 13, + BFS_STAT_MNT_ID = 1 << 14, }; /** @@ -105,6 +104,8 @@ struct bfs_stat { blkcnt_t blocks; /** The device ID represented by this file. */ dev_t rdev; + /** The ID of the mount point containing this file. */ + uint64_t mnt_id; /** Attributes/flags set on the file. */ unsigned long long attrs; @@ -122,14 +123,14 @@ struct bfs_stat { /** * Facade over fstatat(). * - * @param at_fd + * @at_fd * The base file descriptor for the lookup. - * @param at_path + * @at_path * The path to stat, relative to at_fd. Pass NULL to fstat() at_fd * itself. - * @param flags + * @flags * Flags that affect the lookup. - * @param[out] buf + * @buf[out] * A place to store the stat buffer, if successful. * @return * 0 on success, -1 on error. @@ -153,6 +154,11 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src); int bfs_statx_flags(enum bfs_stat_flags flags); /** + * Get the default statx() mask. + */ +unsigned int bfs_statx_mask(void); + +/** * Convert struct statx to struct bfs_stat. */ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src); diff --git a/src/thread.c b/src/thread.c index 200d8c3..b3604f8 100644 --- a/src/thread.c +++ b/src/thread.c @@ -2,12 +2,17 @@ // SPDX-License-Identifier: 0BSD #include "thread.h" + #include "bfstd.h" -#include "config.h" #include "diag.h" + #include <errno.h> #include <pthread.h> +#if __has_include(<pthread_np.h>) +# include <pthread_np.h> +#endif + #define THREAD_FALLIBLE(expr) \ do { \ int err = expr; \ @@ -31,6 +36,14 @@ int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, THREAD_FALLIBLE(pthread_create(thread, attr, fn, arg)); } +void thread_setname(pthread_t thread, const char *name) { +#if BFS_HAS_PTHREAD_SETNAME_NP + pthread_setname_np(thread, name); +#elif BFS_HAS_PTHREAD_SET_NAME_NP + pthread_set_name_np(thread, name); +#endif +} + void thread_join(pthread_t thread, void **ret) { THREAD_INFALLIBLE(pthread_join(thread, ret)); } diff --git a/src/thread.h b/src/thread.h index 8174fe4..3dd8422 100644 --- a/src/thread.h +++ b/src/thread.h @@ -8,17 +8,8 @@ #ifndef BFS_THREAD_H #define BFS_THREAD_H -#include "config.h" #include <pthread.h> -#if __STDC_VERSION__ < C23 && !defined(thread_local) -# if BFS_USE_THREADS_H -# include <threads.h> -# else -# define thread_local _Thread_local -# endif -#endif - /** Thread entry point type. */ typedef void *thread_fn(void *arg); @@ -31,6 +22,11 @@ typedef void *thread_fn(void *arg); int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg); /** + * Set the name of a thread. + */ +void thread_setname(pthread_t thread, const char *name); + +/** * Wrapper for pthread_join(). */ void thread_join(pthread_t thread, void **ret); @@ -82,20 +82,22 @@ */ #include "trie.h" + #include "alloc.h" +#include "bfs.h" #include "bit.h" -#include "config.h" #include "diag.h" #include "list.h" + #include <stdint.h> #include <string.h> -bfs_static_assert(CHAR_WIDTH == 8); +static_assert(CHAR_WIDTH == 8, "This trie implementation assumes 8-bit bytes."); #if __i386__ || __x86_64__ -# define trie_clones attr(target_clones("popcnt", "default")) +# define _trie_clones _target_clones("popcnt", "default") #else -# define trie_clones +# define _trie_clones #endif /** Number of bits for the sparse array bitmap, aka the range of a nibble. */ @@ -127,37 +129,37 @@ struct trie_node { * tag to distinguish internal nodes from leaves. This is safe as long * as all dynamic allocations are aligned to more than a single byte. */ - uintptr_t children[]; + uintptr_t children[]; // _counted_by(count_ones(bitmap)) }; -/** Check if an encoded pointer is to a leaf. */ -static bool trie_is_leaf(uintptr_t ptr) { +/** Check if an encoded pointer is to an internal node. */ +static bool trie_is_node(uintptr_t ptr) { return ptr & 1; } -/** Decode a pointer to a leaf. */ -static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { - bfs_assert(trie_is_leaf(ptr)); - return (struct trie_leaf *)(ptr ^ 1); +/** Decode a pointer to an internal node. */ +static struct trie_node *trie_decode_node(uintptr_t ptr) { + bfs_assert(trie_is_node(ptr)); + return (struct trie_node *)(ptr - 1); } -/** Encode a pointer to a leaf. */ -static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) { - uintptr_t ptr = (uintptr_t)leaf ^ 1; - bfs_assert(trie_is_leaf(ptr)); +/** Encode a pointer to an internal node. */ +static uintptr_t trie_encode_node(const struct trie_node *node) { + uintptr_t ptr = (uintptr_t)node + 1; + bfs_assert(trie_is_node(ptr)); return ptr; } -/** Decode a pointer to an internal node. */ -static struct trie_node *trie_decode_node(uintptr_t ptr) { - bfs_assert(!trie_is_leaf(ptr)); - return (struct trie_node *)ptr; +/** Decode a pointer to a leaf. */ +static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { + bfs_assert(!trie_is_node(ptr)); + return (struct trie_leaf *)ptr; } -/** Encode a pointer to an internal node. */ -static uintptr_t trie_encode_node(const struct trie_node *node) { - uintptr_t ptr = (uintptr_t)node; - bfs_assert(!trie_is_leaf(ptr)); +/** Encode a pointer to a leaf. */ +static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) { + uintptr_t ptr = (uintptr_t)leaf; + bfs_assert(!trie_is_node(ptr)); return ptr; } @@ -169,20 +171,32 @@ void trie_init(struct trie *trie) { } /** Extract the nibble at a certain offset from a byte sequence. */ -static unsigned char trie_key_nibble(const void *key, size_t offset) { +static unsigned char trie_key_nibble(const void *key, size_t length, size_t offset) { const unsigned char *bytes = key; - size_t byte = offset >> 1; + size_t byte = offset / 2; + bfs_assert(byte < length); // A branchless version of // if (offset & 1) { - // return bytes[byte] >> 4; - // } else { // return bytes[byte] & 0xF; + // } else { + // return bytes[byte] >> 4; // } - unsigned int shift = (offset & 1) << 2; + unsigned int shift = 4 * ((offset + 1) % 2); return (bytes[byte] >> shift) & 0xF; } +/** Extract the nibble at a certain offset from a leaf. */ +static unsigned char trie_leaf_nibble(const struct trie_leaf *leaf, size_t offset) { + return trie_key_nibble(leaf->key, leaf->length, offset); +} + +/** Get the number of children of an internal node. */ +_trie_clones +static unsigned int trie_node_size(const struct trie_node *node) { + return count_ones((unsigned int)node->bitmap); +} + /** * Finds a leaf in the trie that matches the key at every branch. If the key * exists in the trie, the representative will match the searched key. But @@ -190,26 +204,24 @@ static unsigned char trie_key_nibble(const void *key, size_t offset) { * that case, the first mismatch between the key and the representative will be * the depth at which to make a new branch to insert the key. */ -trie_clones +_trie_clones static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) { uintptr_t ptr = trie->root; - if (!ptr) { - return NULL; - } - size_t offset = 0; - while (!trie_is_leaf(ptr)) { + size_t offset = 0, limit = 2 * length; + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); offset += node->offset; unsigned int index = 0; - if ((offset >> 1) < length) { - unsigned char nibble = trie_key_nibble(key, offset); + if (offset < limit) { + unsigned char nibble = trie_key_nibble(key, length, offset); unsigned int bit = 1U << nibble; - // bits = bitmap & bit ? bitmap & (bit - 1) : 0 - unsigned int mask = -!!(node->bitmap & bit); - unsigned int bits = node->bitmap & (bit - 1) & mask; - index = count_ones(bits); + unsigned int map = node->bitmap; + unsigned int bits = map & (bit - 1); + unsigned int mask = -!!(map & bit); + // index = (map & bit) ? count_ones(bits) : 0; + index = count_ones(bits) & mask; } ptr = node->children[index]; } @@ -221,7 +233,8 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) { return trie_find_mem(trie, key, strlen(key) + 1); } -struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) { +_trie_clones +static struct trie_leaf *trie_find_mem_impl(const struct trie *trie, const void *key, size_t length) { struct trie_leaf *rep = trie_representative(trie, key, length); if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) { return rep; @@ -230,7 +243,22 @@ struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t } } -struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) { +struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) { + return trie_find_mem_impl(trie, key, length); +} + +void *trie_get_str(const struct trie *trie, const char *key) { + const struct trie_leaf *leaf = trie_find_str(trie, key); + return leaf ? leaf->value : NULL; +} + +void *trie_get_mem(const struct trie *trie, const void *key, size_t length) { + const struct trie_leaf *leaf = trie_find_mem(trie, key, length); + return leaf ? leaf->value : NULL; +} + +_trie_clones +static struct trie_leaf *trie_find_postfix_impl(const struct trie *trie, const char *key) { size_t length = strlen(key); struct trie_leaf *rep = trie_representative(trie, key, length + 1); if (rep && rep->length >= length && memcmp(rep->key, key, length) == 0) { @@ -240,6 +268,10 @@ struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) { } } +struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) { + return trie_find_postfix_impl(trie, key); +} + /** * Find a leaf that may end at the current node. */ @@ -251,10 +283,10 @@ static struct trie_leaf *trie_terminal_leaf(const struct trie_node *node) { } uintptr_t ptr = node->children[0]; - if (trie_is_leaf(ptr)) { - return trie_decode_leaf(ptr); - } else { + if (trie_is_node(ptr)) { node = trie_decode_node(ptr); + } else { + return trie_decode_leaf(ptr); } } @@ -270,7 +302,7 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k } } -trie_clones +_trie_clones static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) { uintptr_t ptr = trie->root; if (!ptr) { @@ -281,21 +313,21 @@ static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const ch size_t skip = 0; size_t length = strlen(key) + 1; - size_t offset = 0; - while (!trie_is_leaf(ptr)) { + size_t offset = 0, limit = 2 * length; + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); offset += node->offset; - if ((offset >> 1) >= length) { + if (offset >= limit) { return best; } struct trie_leaf *leaf = trie_terminal_leaf(node); if (trie_check_prefix(leaf, skip, key, length)) { best = leaf; - skip = offset >> 1; + skip = offset / 2; } - unsigned char nibble = trie_key_nibble(key, offset); + unsigned char nibble = trie_key_nibble(key, length, offset); unsigned int bit = 1U << nibble; if (node->bitmap & bit) { unsigned int index = count_ones(node->bitmap & (bit - 1)); @@ -355,16 +387,10 @@ static struct trie_node *trie_node_realloc(struct trie *trie, struct trie_node * /** Free a node. */ static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) { - bfs_assert(size == (size_t)count_ones(node->bitmap)); + bfs_assert(size == trie_node_size(node)); varena_free(&trie->nodes, node, size); } -#if ENDIAN_NATIVE == ENDIAN_LITTLE -# define TRIE_BSWAP(n) (n) -#elif ENDIAN_NATIVE == ENDIAN_BIG -# define TRIE_BSWAP(n) bswap(n) -#endif - /** Find the offset of the first nibble that differs between two keys. */ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t length) { if (!rep) { @@ -378,32 +404,34 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t const char *rep_bytes = rep->key; const char *key_bytes = key; - size_t i = 0; - for (size_t chunk = sizeof(chunk); i + chunk <= length; i += chunk) { - size_t rep_chunk, key_chunk; - memcpy(&rep_chunk, rep_bytes + i, sizeof(rep_chunk)); - memcpy(&key_chunk, key_bytes + i, sizeof(key_chunk)); - - if (rep_chunk != key_chunk) { -#ifdef TRIE_BSWAP - size_t diff = TRIE_BSWAP(rep_chunk ^ key_chunk); - i *= 2; - i += trailing_zeros(diff) / 4; - return i; + size_t ret = 0, i = 0; + +#define CHUNK(n) CHUNK_(uint##n##_t, load8_beu##n) +#define CHUNK_(type, load8) \ + (length - i >= sizeof(type)) { \ + type rep_chunk = load8(rep_bytes + i); \ + type key_chunk = load8(key_bytes + i); \ + type diff = rep_chunk ^ key_chunk; \ + ret += leading_zeros(diff) / 4; \ + if (diff) { \ + return ret; \ + } \ + i += sizeof(type); \ + } + +#if SIZE_WIDTH >= 64 + while CHUNK(64); + if CHUNK(32); #else - break; + while CHUNK(32); #endif - } - } + if CHUNK(16); + if CHUNK(8); - for (; i < length; ++i) { - unsigned char diff = rep_bytes[i] ^ key_bytes[i]; - if (diff) { - return 2 * i + !(diff & 0xF); - } - } +#undef CHUNK_ +#undef CHUNK - return 2 * i; + return ret; } /** @@ -428,10 +456,10 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t * | Z * +--->... */ -trie_clones +_trie_clones static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, unsigned char nibble) { struct trie_node *node = trie_decode_node(*ptr); - unsigned int size = count_ones(node->bitmap); + unsigned int size = trie_node_size(node); // Double the capacity every power of two if (has_single_bit(size)) { @@ -482,10 +510,10 @@ static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, str * | Y * +--->key */ -static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, size_t *offset) { +static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, size_t *offset) { // We only ever need to jump to leaf nodes, since internal nodes are // guaranteed to be within OFFSET_MAX anyway - bfs_assert(trie_is_leaf(*ptr)); + struct trie_leaf *leaf = trie_decode_leaf(*ptr); struct trie_node *node = trie_node_alloc(trie, 1); if (!node) { @@ -495,7 +523,7 @@ static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, *offset += OFFSET_MAX; node->offset = OFFSET_MAX; - unsigned char nibble = trie_key_nibble(key, *offset); + unsigned char nibble = trie_leaf_nibble(leaf, *offset); node->bitmap = 1 << nibble; node->children[0] = *ptr; @@ -521,8 +549,8 @@ static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, * +--->leaf */ static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, struct trie_leaf *rep, size_t offset, size_t mismatch) { - unsigned char key_nibble = trie_key_nibble(leaf->key, mismatch); - unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch); + unsigned char key_nibble = trie_leaf_nibble(leaf, mismatch); + unsigned char rep_nibble = trie_leaf_nibble(rep, mismatch); bfs_assert(key_nibble != rep_nibble); struct trie_node *node = trie_node_alloc(trie, 2); @@ -534,7 +562,7 @@ static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct tr node->bitmap = (1 << key_nibble) | (1 << rep_nibble); size_t delta = mismatch - offset; - if (!trie_is_leaf(*ptr)) { + if (trie_is_node(*ptr)) { struct trie_node *child = trie_decode_node(*ptr); child->offset -= delta; } @@ -551,12 +579,18 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) { return trie_insert_mem(trie, key, strlen(key) + 1); } -trie_clones +_trie_clones static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) { struct trie_leaf *rep = trie_representative(trie, key, length); size_t mismatch = trie_mismatch(rep, key, length); - if (mismatch >= (length << 1)) { + size_t misbyte = mismatch / 2; + if (misbyte >= length) { + bfs_assert(misbyte == length); return rep; + } else if (rep && misbyte >= rep->length) { + bfs_bug("trie keys must be prefix-free"); + errno = EINVAL; + return NULL; } struct trie_leaf *leaf = trie_leaf_alloc(trie, key, length); @@ -571,14 +605,14 @@ static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key size_t offset = 0; uintptr_t *ptr = &trie->root; - while (!trie_is_leaf(*ptr)) { + while (trie_is_node(*ptr)) { struct trie_node *node = trie_decode_node(*ptr); if (offset + node->offset > mismatch) { break; } offset += node->offset; - unsigned char nibble = trie_key_nibble(key, offset); + unsigned char nibble = trie_leaf_nibble(leaf, offset); unsigned int bit = 1U << nibble; if (node->bitmap & bit) { bfs_assert(offset < mismatch); @@ -591,7 +625,7 @@ static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key } while (mismatch - offset > OFFSET_MAX) { - ptr = trie_jump(trie, ptr, key, &offset); + ptr = trie_jump(trie, ptr, &offset); if (!ptr) { trie_leaf_free(trie, leaf); return NULL; @@ -605,13 +639,33 @@ struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t len return trie_insert_mem_impl(trie, key, length); } +int trie_set_str(struct trie *trie, const char *key, const void *value) { + struct trie_leaf *leaf = trie_insert_str(trie, key); + if (leaf) { + leaf->value = (void *)value; + return 0; + } else { + return -1; + } +} + +int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value) { + struct trie_leaf *leaf = trie_insert_mem(trie, key, length); + if (leaf) { + leaf->value = (void *)value; + return 0; + } else { + return -1; + } +} + /** Free a chain of singleton nodes. */ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { - while (!trie_is_leaf(ptr)) { + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); // Make sure the bitmap is a power of two, i.e. it has just one child - bfs_assert(has_single_bit(node->bitmap)); + bfs_assert(has_single_bit((size_t)node->bitmap)); ptr = node->children[0]; trie_node_free(trie, node, 1); @@ -639,7 +693,7 @@ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { */ static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) { uintptr_t other = parent_node->children[child_index ^ 1]; - if (!trie_is_leaf(other)) { + if (trie_is_node(other)) { struct trie_node *other_node = trie_decode_node(other); if (other_node->offset + parent_node->offset <= OFFSET_MAX) { other_node->offset += parent_node->offset; @@ -649,22 +703,21 @@ static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_ } *parent = other; - trie_node_free(trie, parent_node, 1); + trie_node_free(trie, parent_node, 2); return 0; } -trie_clones +_trie_clones static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { uintptr_t *child = &trie->root; uintptr_t *parent = NULL; unsigned int child_bit = 0, child_index = 0; size_t offset = 0; - while (!trie_is_leaf(*child)) { + while (trie_is_node(*child)) { struct trie_node *node = trie_decode_node(*child); offset += node->offset; - bfs_assert((offset >> 1) < leaf->length); - unsigned char nibble = trie_key_nibble(leaf->key, offset); + unsigned char nibble = trie_leaf_nibble(leaf, offset); unsigned int bit = 1U << nibble; unsigned int bitmap = node->bitmap; bfs_assert(bitmap & bit); @@ -689,19 +742,19 @@ static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { } struct trie_node *node = trie_decode_node(*parent); - child = node->children + child_index; - trie_free_singletons(trie, *child); + trie_free_singletons(trie, node->children[child_index]); - node->bitmap ^= child_bit; - unsigned int parent_size = count_ones(node->bitmap); - bfs_assert(parent_size > 0); - if (parent_size == 1 && trie_collapse_node(trie, parent, node, child_index) == 0) { + unsigned int parent_size = trie_node_size(node); + bfs_assert(parent_size > 1); + if (parent_size == 2 && trie_collapse_node(trie, parent, node, child_index) == 0) { return; } - if (child_index < parent_size) { - memmove(child, child + 1, (parent_size - child_index) * sizeof(*child)); + for (size_t i = child_index; i + 1 < parent_size; ++i) { + node->children[i] = node->children[i + 1]; } + node->bitmap &= ~child_bit; + --parent_size; if (has_single_bit(parent_size)) { node = trie_node_realloc(trie, node, 2 * parent_size, parent_size); @@ -6,6 +6,7 @@ #include "alloc.h" #include "list.h" + #include <stddef.h> #include <stdint.h> @@ -20,7 +21,7 @@ struct trie_leaf { /** The length of the key in bytes. */ size_t length; /** The key itself, stored inline. */ - char key[]; + char key[] _counted_by(length); }; /** @@ -45,9 +46,9 @@ void trie_init(struct trie *trie); /** * Find the leaf for a string key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * The found leaf, or NULL if the key is not present. @@ -57,11 +58,11 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); /** * Find the leaf for a fixed-size key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. - * @param length + * @length * The length of the key in bytes. * @return * The found leaf, or NULL if the key is not present. @@ -69,11 +70,37 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length); /** + * Get the value associated with a string key. + * + * @trie + * The trie to search. + * @key + * The key to look up. + * @return + * The found value, or NULL if the key is not present. + */ +void *trie_get_str(const struct trie *trie, const char *key); + +/** + * Get the value associated with a fixed-size key. + * + * @trie + * The trie to search. + * @key + * The key to look up. + * @length + * The length of the key in bytes. + * @return + * The found value, or NULL if the key is not present. + */ +void *trie_get_mem(const struct trie *trie, const void *key, size_t length); + +/** * Find the shortest leaf that starts with a given key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * A leaf that starts with the given key, or NULL. @@ -83,9 +110,9 @@ struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key); /** * Find the leaf that is the longest prefix of the given key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * The longest prefix match for the given key, or NULL. @@ -95,9 +122,9 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key); /** * Insert a string key into the trie. * - * @param trie + * @trie * The trie to modify. - * @param key + * @key * The key to insert. * @return * The inserted leaf, or NULL on failure. @@ -107,11 +134,11 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key); /** * Insert a fixed-size key into the trie. * - * @param trie + * @trie * The trie to modify. - * @param key + * @key * The key to insert. - * @param length + * @length * The length of the key in bytes. * @return * The inserted leaf, or NULL on failure. @@ -119,11 +146,41 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key); struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length); /** + * Set the value for a string key. + * + * @trie + * The trie to modify. + * @key + * The key to insert. + * @value + * The value to set. + * @return + * 0 on success, -1 on error. + */ +int trie_set_str(struct trie *trie, const char *key, const void *value); + +/** + * Set the value for a fixed-size key. + * + * @trie + * The trie to modify. + * @key + * The key to insert. + * @length + * The length of the key in bytes. + * @value + * The value to set. + * @return + * 0 on success, -1 on error. + */ +int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value); + +/** * Remove a leaf from a trie. * - * @param trie + * @trie * The trie to modify. - * @param leaf + * @leaf * The leaf to remove. */ void trie_remove(struct trie *trie, struct trie_leaf *leaf); @@ -2,6 +2,7 @@ // SPDX-License-Identifier: 0BSD #include "typo.h" + #include <limits.h> #include <stdint.h> #include <stdlib.h> @@ -7,9 +7,9 @@ /** * Find the "typo" distance between two strings. * - * @param actual + * @actual * The actual string typed by the user. - * @param expected + * @expected * The expected valid string. * @return The distance between the two strings. */ diff --git a/src/version.c b/src/version.c new file mode 100644 index 0000000..7479a9f --- /dev/null +++ b/src/version.c @@ -0,0 +1,32 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "bfs.h" + +const char bfs_version[] = { +#include "version.i" +}; + +const char bfs_confflags[] = { +#include "confflags.i" +}; + +const char bfs_cc[] = { +#include "cc.i" +}; + +const char bfs_cppflags[] = { +#include "cppflags.i" +}; + +const char bfs_cflags[] = { +#include "cflags.i" +}; + +const char bfs_ldflags[] = { +#include "ldflags.i" +}; + +const char bfs_ldlibs[] = { +#include "ldlibs.i" +}; diff --git a/src/xregex.c b/src/xregex.c index 3df27f0..796544e 100644 --- a/src/xregex.c +++ b/src/xregex.c @@ -2,18 +2,20 @@ // SPDX-License-Identifier: 0BSD #include "xregex.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "diag.h" #include "sanity.h" #include "thread.h" + #include <errno.h> #include <pthread.h> #include <stdlib.h> #include <string.h> -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA # include <langinfo.h> # include <oniguruma.h> #else @@ -21,7 +23,7 @@ #endif struct bfs_regex { -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA unsigned char *pattern; OnigRegex impl; int err; @@ -32,11 +34,17 @@ struct bfs_regex { #endif }; -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA static int bfs_onig_status; static OnigEncoding bfs_onig_enc; +static OnigSyntaxType bfs_onig_syntax_awk; +static OnigSyntaxType bfs_onig_syntax_gnu_awk; +static OnigSyntaxType bfs_onig_syntax_emacs; +static OnigSyntaxType bfs_onig_syntax_egrep; +static OnigSyntaxType bfs_onig_syntax_gnu_find; + /** pthread_once() callback. */ static void bfs_onig_once(void) { // Fall back to ASCII by default @@ -103,6 +111,35 @@ static void bfs_onig_once(void) { if (bfs_onig_status != ONIG_NORMAL) { bfs_onig_enc = NULL; } + + // Compute the GNU extensions + OnigSyntaxType *ere = ONIG_SYNTAX_POSIX_EXTENDED; + OnigSyntaxType *gnu = ONIG_SYNTAX_GNU_REGEX; + unsigned int gnu_op = gnu->op & ~ere->op; + unsigned int gnu_op2 = gnu->op2 & ~ere->op2; + unsigned int gnu_behavior = gnu->behavior & ~ere->behavior; + + onig_copy_syntax(&bfs_onig_syntax_awk, ONIG_SYNTAX_POSIX_EXTENDED); + bfs_onig_syntax_awk.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL; + bfs_onig_syntax_awk.behavior |= ONIG_SYN_BACKSLASH_ESCAPE_IN_CC; + + onig_copy_syntax(&bfs_onig_syntax_gnu_awk, &bfs_onig_syntax_awk); + bfs_onig_syntax_gnu_awk.op |= gnu_op; + bfs_onig_syntax_gnu_awk.op2 |= gnu_op2; + bfs_onig_syntax_gnu_awk.behavior |= gnu_behavior; + bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INDEP_REPEAT_OPS; + bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS; + + // https://github.com/kkos/oniguruma/issues/296 + onig_copy_syntax(&bfs_onig_syntax_emacs, ONIG_SYNTAX_EMACS); + bfs_onig_syntax_emacs.op2 |= ONIG_SYN_OP2_QMARK_GROUP_EFFECT; + + onig_copy_syntax(&bfs_onig_syntax_egrep, ONIG_SYNTAX_POSIX_EXTENDED); + bfs_onig_syntax_egrep.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL; + bfs_onig_syntax_egrep.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS; + + onig_copy_syntax(&bfs_onig_syntax_gnu_find, &bfs_onig_syntax_emacs); + bfs_onig_syntax_gnu_find.options |= ONIG_OPTION_MULTILINE; } /** Initialize Oniguruma. */ @@ -121,7 +158,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ return -1; } -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA // onig_error_code_to_str() says // // don't call this after the pattern argument of onig_new() is freed @@ -143,12 +180,24 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ case BFS_REGEX_POSIX_EXTENDED: syntax = ONIG_SYNTAX_POSIX_EXTENDED; break; + case BFS_REGEX_AWK: + syntax = &bfs_onig_syntax_awk; + break; + case BFS_REGEX_GNU_AWK: + syntax = &bfs_onig_syntax_gnu_awk; + break; case BFS_REGEX_EMACS: - syntax = ONIG_SYNTAX_EMACS; + syntax = &bfs_onig_syntax_emacs; break; case BFS_REGEX_GREP: syntax = ONIG_SYNTAX_GREP; break; + case BFS_REGEX_EGREP: + syntax = &bfs_onig_syntax_egrep; + break; + case BFS_REGEX_GNU_FIND: + syntax = &bfs_onig_syntax_gnu_find; + break; } bfs_assert(syntax, "Invalid regex type"); @@ -204,7 +253,7 @@ fail: int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) { size_t len = strlen(str); -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA const unsigned char *ustr = (const unsigned char *)str; const unsigned char *end = ustr + len; @@ -263,7 +312,7 @@ int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags void bfs_regfree(struct bfs_regex *regex) { if (regex) { -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA onig_free(regex->impl); free(regex->pattern); #else @@ -278,7 +327,7 @@ char *bfs_regerror(const struct bfs_regex *regex) { return strdup(xstrerror(ENOMEM)); } -#if BFS_USE_ONIGURUMA +#if BFS_WITH_ONIGURUMA unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN); if (str) { onig_error_code_to_str(str, regex->err, ®ex->einfo); diff --git a/src/xregex.h b/src/xregex.h index 998a2b0..c4504ee 100644 --- a/src/xregex.h +++ b/src/xregex.h @@ -15,8 +15,12 @@ struct bfs_regex; enum bfs_regex_type { BFS_REGEX_POSIX_BASIC, BFS_REGEX_POSIX_EXTENDED, + BFS_REGEX_AWK, + BFS_REGEX_GNU_AWK, BFS_REGEX_EMACS, BFS_REGEX_GREP, + BFS_REGEX_EGREP, + BFS_REGEX_GNU_FIND, }; /** @@ -38,13 +42,13 @@ enum bfs_regexec_flags { /** * Wrapper for regcomp() that supports additional regex types. * - * @param[out] preg + * @preg[out] * Will hold the compiled regex. - * @param pattern + * @pattern * The regular expression to compile. - * @param type + * @type * The regular expression syntax to use. - * @param flags + * @flags * Regex compilation flags. * @return * 0 on success, -1 on failure. @@ -54,11 +58,11 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ /** * Wrapper for regexec(). * - * @param regex + * @regex * The regular expression to execute. - * @param str + * @str * The string to match against. - * @param flags + * @flags * Regex execution flags. * @return * 1 for a match, 0 for no match, -1 on failure. @@ -73,7 +77,7 @@ void bfs_regfree(struct bfs_regex *regex); /** * Get a human-readable regex error message. * - * @param regex + * @regex * The compiled regex. * @return * A human-readable description of the error, which should be free()'d. diff --git a/src/xspawn.c b/src/xspawn.c index 065fbae..ee62c05 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -2,23 +2,28 @@ // SPDX-License-Identifier: 0BSD #include "xspawn.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" +#include "diag.h" #include "list.h" +#include "sighook.h" + #include <errno.h> #include <fcntl.h> +#include <signal.h> #include <stdlib.h> #include <string.h> #include <sys/resource.h> #include <sys/types.h> #include <unistd.h> -#if BFS_USE_PATHS_H +#if __has_include(<paths.h>) # include <paths.h> #endif -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 # include <spawn.h> #endif @@ -68,29 +73,42 @@ int bfs_spawn_init(struct bfs_spawn *ctx) { ctx->flags = 0; SLIST_INIT(ctx); -#if _POSIX_SPAWN > 0 - ctx->flags |= BFS_SPAWN_USE_POSIX; +#if BFS_POSIX_SPAWN >= 0 + if (sysoption(SPAWN) > 0) { + ctx->flags |= BFS_SPAWN_USE_POSIX; - errno = posix_spawn_file_actions_init(&ctx->actions); - if (errno != 0) { - return -1; - } + errno = posix_spawn_file_actions_init(&ctx->actions); + if (errno != 0) { + return -1; + } - errno = posix_spawnattr_init(&ctx->attr); - if (errno != 0) { - posix_spawn_file_actions_destroy(&ctx->actions); - return -1; + errno = posix_spawnattr_init(&ctx->attr); + if (errno != 0) { + posix_spawn_file_actions_destroy(&ctx->actions); + return -1; + } } #endif return 0; } -int bfs_spawn_destroy(struct bfs_spawn *ctx) { -#if _POSIX_SPAWN > 0 - posix_spawnattr_destroy(&ctx->attr); - posix_spawn_file_actions_destroy(&ctx->actions); +/** + * Clear the BFS_SPAWN_USE_POSIX flag and free the attributes. + */ +static void bfs_spawn_clear_posix(struct bfs_spawn *ctx) { + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + ctx->flags &= ~BFS_SPAWN_USE_POSIX; + +#if BFS_POSIX_SPAWN >= 0 + posix_spawnattr_destroy(&ctx->attr); + posix_spawn_file_actions_destroy(&ctx->actions); #endif + } +} + +int bfs_spawn_destroy(struct bfs_spawn *ctx) { + bfs_spawn_clear_posix(ctx); for_slist (struct bfs_spawn_action, action, ctx) { free(action); @@ -99,9 +117,9 @@ int bfs_spawn_destroy(struct bfs_spawn *ctx) { return 0; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** Set some posix_spawnattr flags. */ -attr(maybe_unused) +_maybe_unused static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { short prev; errno = posix_spawnattr_getflags(&ctx->attr, &prev); @@ -119,7 +137,7 @@ static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { return 0; } -#endif // _POSIX_SPAWN > 0 +#endif /** Allocate a spawn action. */ static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) { @@ -141,7 +159,7 @@ int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_addopen(&ctx->actions, fd, path, flags, mode); if (errno != 0) { @@ -165,7 +183,7 @@ int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_addclose(&ctx->actions, fd); if (errno != 0) { @@ -186,7 +204,7 @@ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_adddup2(&ctx->actions, oldfd, newfd); if (errno != 0) { @@ -209,10 +227,15 @@ 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__) + +/** + * NetBSD even resolves the executable before file actions with posix_spawn()! + */ +#define BFS_POSIX_SPAWN_AFTER_FCHDIR !__NetBSD__ int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR); @@ -220,36 +243,33 @@ 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 +#if __APPLE__ + // macOS has a bug that causes EBADF when an fchdir() action refers to a + // file opened by the file actions + for_slist (struct bfs_spawn_action, prev, ctx) { + if (fd == prev->out_fd) { + bfs_spawn_clear_posix(ctx); + break; + } + } #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 BFS_POSIX_SPAWN >= 0 && defined(BFS_POSIX_SPAWN_ADDFCHDIR) 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; } } #else - ctx->flags &= ~BFS_SPAWN_USE_POSIX; + bfs_spawn_clear_posix(ctx); #endif action->in_fd = fd; @@ -273,7 +293,7 @@ int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit goto fail; } #else - ctx->flags &= ~BFS_SPAWN_USE_POSIX; + bfs_spawn_clear_posix(ctx); #endif action->resource = resource; @@ -397,18 +417,40 @@ static bool bfs_resolve_relative(const struct bfs_resolver *res) { return false; } +/** Check if the actions include fchdir(). */ +static bool bfs_spawn_will_chdir(const struct bfs_spawn *ctx) { + if (ctx) { + for_slist (const struct bfs_spawn_action, action, ctx) { + if (action->op == BFS_SPAWN_FCHDIR) { + return true; + } + } + } + + return false; +} + +/** Check if we can call xfaccessat() before file actions. */ +static bool bfs_can_access_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (res->exe[0] == '/') { + return true; + } + + if (bfs_spawn_will_chdir(ctx)) { + return false; + } + + return true; +} + /** Check if we can resolve the executable before file actions. */ static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { if (!bfs_resolve_relative(res)) { return true; } - if (ctx) { - for_slist (const struct bfs_spawn_action, action, ctx) { - if (action->op == BFS_SPAWN_FCHDIR) { - return false; - } - } + if (bfs_spawn_will_chdir(ctx)) { + return false; } return true; @@ -438,6 +480,17 @@ static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const st }; if (bfs_can_skip_resolve(res, ctx)) { + if (bfs_can_access_early(res, ctx)) { + // Do this check eagerly, even though posix_spawn()/execv() also + // would, because: + // + // - faccessat() is faster than fork()/clone() + execv() + // - posix_spawn() is not guaranteed to report ENOENT + if (xfaccessat(AT_FDCWD, exe, X_OK) != 0) { + return -1; + } + } + res->done = true; return 0; } @@ -485,7 +538,7 @@ fail: return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** bfs_spawn() implementation using posix_spawn(). */ static pid_t bfs_posix_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { @@ -516,13 +569,20 @@ static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs } #endif +#if !BFS_POSIX_SPAWN_AFTER_FCHDIR + if (res->exe[0] != '/' && bfs_spawn_will_chdir(ctx)) { + return false; + } +#endif + return true; } -#endif // _POSIX_SPAWN > 0 +#endif // BFS_POSIX_SPAWN >= 0 /** Actually exec() the new process. */ -static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { +_noreturn +static void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, const sigset_t *mask, int pipefd[2]) { xclose(pipefd[0]); for_slist (const struct bfs_spawn_action, action, ctx) { @@ -583,6 +643,18 @@ static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_s goto fail; } + // Reset signal handlers to their original values before we unblock + // signals, so that handlers don't run in both the parent and the child + if (sigreset() != 0) { + goto fail; + } + + // Restore the original signal mask for the child process + errno = pthread_sigmask(SIG_SETMASK, mask, NULL); + if (errno != 0) { + goto fail; + } + execve(res->exe, argv, envp); fail:; @@ -604,35 +676,58 @@ static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ct return -1; } + // Block signals before fork() so handlers don't run in the child + sigset_t new_mask; + if (sigfillset(&new_mask) != 0) { + goto fail; + } + sigset_t old_mask; + errno = pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask); + if (errno != 0) { + goto fail; + } + +#if BFS_HAS__FORK + pid_t pid = _Fork(); +#else pid_t pid = fork(); - if (pid < 0) { - close_quietly(pipefd[1]); - close_quietly(pipefd[0]); - return -1; - } else if (pid == 0) { +#endif + if (pid == 0) { // Child - bfs_spawn_exec(res, ctx, argv, envp, pipefd); + bfs_spawn_exec(res, ctx, argv, envp, &old_mask, pipefd); + } + + // Restore the original signal mask + errno = pthread_sigmask(SIG_SETMASK, &old_mask, NULL); + bfs_everify(errno == 0, "pthread_sigmask()"); + + if (pid < 0) { + // fork() failed + goto fail; } - // Parent xclose(pipefd[1]); int error; ssize_t nbytes = xread(pipefd[0], &error, sizeof(error)); xclose(pipefd[0]); if (nbytes == sizeof(error)) { - int wstatus; - xwaitpid(pid, &wstatus, 0); + xwaitpid(pid, NULL, 0); errno = error; return -1; } return pid; + +fail: + close_quietly(pipefd[1]); + close_quietly(pipefd[0]); + return -1; } /** Call the right bfs_spawn() implementation. */ static pid_t bfs_spawn_impl(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (bfs_use_posix_spawn(res, ctx)) { return bfs_posix_spawn(res, ctx, argv, envp); } diff --git a/src/xspawn.h b/src/xspawn.h index a20cbd0..3c74ccd 100644 --- a/src/xspawn.h +++ b/src/xspawn.h @@ -8,12 +8,17 @@ #ifndef BFS_XSPAWN_H #define BFS_XSPAWN_H -#include "config.h" #include <sys/resource.h> #include <sys/types.h> #include <unistd.h> -#if _POSIX_SPAWN > 0 +#ifdef _POSIX_SPAWN +# define BFS_POSIX_SPAWN _POSIX_SPAWN +#else +# define BFS_POSIX_SPAWN (-1) +#endif + +#if BFS_POSIX_SPAWN >= 0 # include <spawn.h> #endif @@ -38,7 +43,7 @@ struct bfs_spawn { struct bfs_spawn_action *head; struct bfs_spawn_action **tail; -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** posix_spawn() context, for when we can use it. */ posix_spawn_file_actions_t actions; posix_spawnattr_t attr; @@ -104,13 +109,13 @@ int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit /** * Spawn a new process. * - * @param exe + * @exe * The executable to run. - * @param ctx + * @ctx * The context for the new process. - * @param argv + * @argv * The arguments for the new process. - * @param envp + * @envp * The environment variables for the new process (NULL for the current * environment). * @return @@ -122,7 +127,7 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char * Look up an executable in the current PATH, as BFS_SPAWN_USE_PATH or execvp() * would do. * - * @param exe + * @exe * The name of the binary to execute. Bare names without a '/' will be * searched on the provided PATH. * @return diff --git a/src/xtime.c b/src/xtime.c index bcf6dd3..6b8a141 100644 --- a/src/xtime.c +++ b/src/xtime.c @@ -2,9 +2,13 @@ // SPDX-License-Identifier: 0BSD #include "xtime.h" + +#include "alloc.h" +#include "bfs.h" #include "bfstd.h" -#include "config.h" #include "diag.h" +#include "sanity.h" + #include <errno.h> #include <limits.h> #include <sys/time.h> @@ -12,14 +16,14 @@ #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)) { - bfs_bug("localtime_r(-1): %s", xstrerror(errno)); + if (!localtime_r(&time, &tmp)) { + bfs_ebug("localtime_r(-1)"); return -1; } @@ -30,9 +34,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_ebug("gmtime_r(-1)"); + 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 +180,8 @@ overflow: return -1; } +#endif // !BFS_HAS_TIMEGM + /** Parse a decimal digit. */ static int xgetdigit(char c) { int ret = c - '0'; @@ -174,6 +209,23 @@ static int xgetpart(const char **str, size_t n, int *result) { } int xgetdate(const char *str, struct timespec *result) { + // Handle @epochseconds + if (str[0] == '@') { + long long value; + if (xstrtoll(str + 1, NULL, 10, &value) != 0) { + goto error; + } + + time_t time = (time_t)value; + if ((long long)time != value) { + errno = ERANGE; + goto error; + } + + result->tv_sec = time; + goto done; + } + struct tm tm = { .tm_isdst = -1, }; @@ -292,6 +344,7 @@ end: } } +done: result->tv_nsec = 0; return 0; @@ -301,16 +354,150 @@ error: return -1; } -int xgettime(struct timespec *result) { -#if _POSIX_TIMERS > 0 - return clock_gettime(CLOCK_REALTIME, result); +/** One nanosecond. */ +static const long NS = 1000L * 1000 * 1000; + +void timespec_add(struct timespec *lhs, const struct timespec *rhs) { + lhs->tv_sec += rhs->tv_sec; + lhs->tv_nsec += rhs->tv_nsec; + if (lhs->tv_nsec >= NS) { + lhs->tv_nsec -= NS; + lhs->tv_sec += 1; + } +} + +void timespec_sub(struct timespec *lhs, const struct timespec *rhs) { + lhs->tv_sec -= rhs->tv_sec; + lhs->tv_nsec -= rhs->tv_nsec; + if (lhs->tv_nsec < 0) { + lhs->tv_nsec += NS; + lhs->tv_sec -= 1; + } +} + +int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs) { + if (lhs->tv_sec < rhs->tv_sec) { + return -1; + } else if (lhs->tv_sec > rhs->tv_sec) { + return 1; + } + + if (lhs->tv_nsec < rhs->tv_nsec) { + return -1; + } else if (lhs->tv_nsec > rhs->tv_nsec) { + return 1; + } + + return 0; +} + +void timespec_min(struct timespec *dest, const struct timespec *src) { + if (timespec_cmp(src, dest) < 0) { + *dest = *src; + } +} + +void timespec_max(struct timespec *dest, const struct timespec *src) { + if (timespec_cmp(src, dest) > 0) { + *dest = *src; + } +} + +double timespec_ns(const struct timespec *ts) { + return 1.0e9 * ts->tv_sec + ts->tv_nsec; +} + +#if defined(_POSIX_TIMERS) && BFS_HAS_TIMER_CREATE +# define BFS_POSIX_TIMERS _POSIX_TIMERS #else - struct timeval tv; - int ret = gettimeofday(&tv, NULL); - if (ret == 0) { - result->tv_sec = tv.tv_sec; - result->tv_nsec = tv.tv_usec * 1000L; +# define BFS_POSIX_TIMERS (-1) +#endif + +struct timer { +#if BFS_POSIX_TIMERS >= 0 + /** The POSIX timer. */ + timer_t timer; +#endif + /** Whether to use timer_create() or setitimer(). */ + bool legacy; +}; + +struct timer *xtimer_start(const struct timespec *interval) { + struct timer *timer = ALLOC(struct timer); + if (!timer) { + return NULL; } - return ret; + +#if BFS_POSIX_TIMERS >= 0 + if (sysoption(TIMERS)) { + clockid_t clock = CLOCK_REALTIME; + +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clock = CLOCK_MONOTONIC; + } +#endif + + if (timer_create(clock, NULL, &timer->timer) != 0) { + goto fail; + } + + // https://github.com/llvm/llvm-project/issues/111847 + sanitize_init(&timer->timer); + + struct itimerspec spec = { + .it_value = *interval, + .it_interval = *interval, + }; + if (timer_settime(timer->timer, 0, &spec, NULL) != 0) { + timer_delete(timer->timer); + goto fail; + } + + timer->legacy = false; + return timer; + } +#endif + +#if BFS_POSIX_TIMERS <= 0 + struct timeval tv = { + .tv_sec = interval->tv_sec, + .tv_usec = (interval->tv_nsec + 999) / 1000, + }; + struct itimerval ival = { + .it_value = tv, + .it_interval = tv, + }; + if (setitimer(ITIMER_REAL, &ival, NULL) != 0) { + goto fail; + } + + timer->legacy = true; + return timer; #endif + +fail: + free(timer); + return NULL; +} + +void xtimer_stop(struct timer *timer) { + if (!timer) { + return; + } + + if (timer->legacy) { +#if BFS_POSIX_TIMERS <= 0 + struct itimerval ival = {0}; + int ret = setitimer(ITIMER_REAL, &ival, NULL); + bfs_everify(ret == 0, "setitimer()"); +#endif + } else { +#if BFS_POSIX_TIMERS >= 0 + int ret = timer_delete(timer->timer); + bfs_everify(ret == 0, "timer_delete()"); +#endif + } + + free(timer); } diff --git a/src/xtime.h b/src/xtime.h index fb60ae4..b76fef2 100644 --- a/src/xtime.h +++ b/src/xtime.h @@ -13,9 +13,9 @@ /** * mktime() wrapper that reports errors more reliably. * - * @param[in,out] tm - * The struct tm to convert. - * @param[out] timep + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -25,9 +25,9 @@ int xmktime(struct tm *tm, time_t *timep); /** * A portable timegm(), the inverse of gmtime(). * - * @param[in,out] tm - * The struct tm to convert. - * @param[out] timep + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -37,9 +37,9 @@ int xtimegm(struct tm *tm, time_t *timep); /** * Parse an ISO 8601-style timestamp. * - * @param[in] str + * @str * The string to parse. - * @param[out] result + * @result[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -47,13 +47,62 @@ int xtimegm(struct tm *tm, time_t *timep); int xgetdate(const char *str, struct timespec *result); /** - * Get the current time. + * Add to a timespec. + */ +void timespec_add(struct timespec *lhs, const struct timespec *rhs); + +/** + * Subtract from a timespec. + */ +void timespec_sub(struct timespec *lhs, const struct timespec *rhs); + +/** + * Compare two timespecs. * - * @param[out] result - * A pointer to the result. * @return - * 0 on success, -1 on failure. + * An integer with the sign of (*lhs - *rhs). + */ +int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs); + +/** + * Update a minimum timespec. + */ +void timespec_min(struct timespec *dest, const struct timespec *src); + +/** + * Update a maximum timespec. + */ +void timespec_max(struct timespec *dest, const struct timespec *src); + +/** + * Convert a timespec to floating point. + * + * @return + * The value in nanoseconds. + */ +double timespec_ns(const struct timespec *ts); + +/** + * A timer. + */ +struct timer; + +/** + * Start a timer. + * + * @interval + * The regular interval at which to send SIGALRM. + * @return + * The new timer on success, otherwise NULL. + */ +struct timer *xtimer_start(const struct timespec *interval); + +/** + * Stop a timer. + * + * @timer + * The timer to stop. */ -int xgettime(struct timespec *result); +void xtimer_stop(struct timer *timer); #endif // BFS_XTIME_H diff --git a/tests/alloc.c b/tests/alloc.c index 9f08111..4aae515 100644 --- a/tests/alloc.c +++ b/tests/alloc.c @@ -2,51 +2,77 @@ // SPDX-License-Identifier: 0BSD #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> -bool check_alloc(void) { - bool ret = true; +struct flexible { + alignas(64) int foo[8]; + int bar[]; +}; - // Check sizeof_flex() - struct flexible { - alignas(64) int foo[8]; - int bar[]; - }; - ret &= bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible)); - ret &= bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0); +/** Check varena_realloc() poisoning for a size combination. */ +static struct flexible *check_varena_realloc(struct varena *varena, struct flexible *flexy, size_t old_count, size_t new_count) { + flexy = varena_realloc(varena, flexy, old_count, new_count); + bfs_everify(flexy); - size_t too_many = SIZE_MAX / sizeof(int) + 1; - ret &= bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX)); + for (size_t i = 0; i < new_count; ++i) { + if (i < old_count) { + bfs_check(flexy->bar[i] == (int)i); + } else { + flexy->bar[i] = i; + } + } - // Corner case: sizeof(type) > align_ceil(alignof(type), offsetof(type, member)) - // Doesn't happen in typical ABIs - ret &= bfs_check(flex_size(8, 16, 4, 4, 1) == 16); + return flexy; +} - // Make sure we detect allocation size overflows -#if __GNUC__ && !__clang__ -# pragma GCC diagnostic ignored "-Walloc-size-larger-than=" -#endif +void check_alloc(void) { + // Check aligned allocation + void *ptr; + bfs_everify((ptr = zalloc(64, 129))); + bfs_check((uintptr_t)ptr % 64 == 0); + bfs_echeck((ptr = xrealloc(ptr, 64, 129, 65))); + bfs_check((uintptr_t)ptr % 64 == 0); + free(ptr); - ret &= bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + // Check sizeof_flex() + bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible)); + bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0); + + // volatile to suppress -Walloc-size-larger-than + volatile size_t too_many = SIZE_MAX / sizeof(int) + 1; + bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX)); + + // Make sure we detect allocation size overflows + bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); // varena tests struct varena varena; VARENA_INIT(&varena, struct flexible, bar); for (size_t i = 0; i < 256; ++i) { - bfs_verify(varena_alloc(&varena, i)); + bfs_everify(varena_alloc(&varena, i)); struct arena *arena = &varena.arenas[varena.narenas - 1]; - ret &= bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i)); + bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i)); } + // Check varena_realloc() (un)poisoning + struct flexible *flexy = varena_alloc(&varena, 160); + bfs_everify(flexy); + + flexy = check_varena_realloc(&varena, flexy, 0, 160); + flexy = check_varena_realloc(&varena, flexy, 160, 192); + flexy = check_varena_realloc(&varena, flexy, 192, 160); + flexy = check_varena_realloc(&varena, flexy, 160, 320); + flexy = check_varena_realloc(&varena, flexy, 320, 96); + varena_destroy(&varena); - return ret; } diff --git a/tests/gnu/gid.out b/tests/bfs/Dmulti.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid.out +++ b/tests/bfs/Dmulti.out diff --git a/tests/bfs/Dmulti.sh b/tests/bfs/Dmulti.sh new file mode 100644 index 0000000..35d64b1 --- /dev/null +++ b/tests/bfs/Dmulti.sh @@ -0,0 +1 @@ +bfs_diff -Dopt,tree,unknown basic diff --git a/tests/bfs/LD_stat.out b/tests/bfs/LD_stat.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/bfs/LD_stat.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/bfs/LD_stat.sh b/tests/bfs/LD_stat.sh new file mode 100644 index 0000000..d407de3 --- /dev/null +++ b/tests/bfs/LD_stat.sh @@ -0,0 +1 @@ +bfs_diff -LD stat links diff --git a/tests/bfs/LDstat.out b/tests/bfs/LDstat.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/bfs/LDstat.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/bfs/LDstat.sh b/tests/bfs/LDstat.sh new file mode 100644 index 0000000..ec6df0b --- /dev/null +++ b/tests/bfs/LDstat.sh @@ -0,0 +1 @@ +bfs_diff -LDstat links diff --git a/tests/bfs/L_noerror.out b/tests/bfs/L_noerror.out new file mode 100644 index 0000000..a514555 --- /dev/null +++ b/tests/bfs/L_noerror.out @@ -0,0 +1,11 @@ +loops +loops/broken +loops/deeply +loops/deeply/nested +loops/deeply/nested/dir +loops/file +loops/notdir +loops/skip +loops/skip/dir +loops/skip/loop +loops/symlink diff --git a/tests/bfs/L_noerror.sh b/tests/bfs/L_noerror.sh new file mode 100644 index 0000000..7db2a4d --- /dev/null +++ b/tests/bfs/L_noerror.sh @@ -0,0 +1 @@ +bfs_diff -L loops -noerror diff --git a/tests/bfs/O_3.sh b/tests/bfs/O_3.sh new file mode 100644 index 0000000..f159852 --- /dev/null +++ b/tests/bfs/O_3.sh @@ -0,0 +1 @@ +! invoke_bfs -O 3 basic diff --git a/tests/bfs/Sbfs.out b/tests/bfs/Sbfs.out new file mode 100644 index 0000000..bb3cd8d --- /dev/null +++ b/tests/bfs/Sbfs.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/e +basic/g +basic/i +basic/j +basic/k +basic/l +basic/c/d +basic/e/f +basic/g/h +basic/j/foo +basic/k/foo +basic/l/foo +basic/k/foo/bar +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/Sbfs.sh b/tests/bfs/Sbfs.sh new file mode 100644 index 0000000..72d92c8 --- /dev/null +++ b/tests/bfs/Sbfs.sh @@ -0,0 +1,2 @@ +invoke_bfs -Sbfs -s basic >"$OUT" +diff_output diff --git a/tests/bfs/color_ca.out b/tests/bfs/color_ca.out new file mode 100644 index 0000000..bf74202 --- /dev/null +++ b/tests/bfs/color_ca.out @@ -0,0 +1,4 @@ +[01;34m.[0m +[01;34m./[0m[01;36mlink[0m +[01;34m./[0m[30;41mcapable[0m +[01;34m./[0mnormal diff --git a/tests/bfs/color_ca.sh b/tests/bfs/color_ca.sh new file mode 100644 index 0000000..3aaaaf1 --- /dev/null +++ b/tests/bfs/color_ca.sh @@ -0,0 +1,10 @@ +test "$UNAME" = "Linux" || skip +invoke_bfs . -quit -capable || skip + +cd "$TEST" + +"$XTOUCH" normal capable +bfs_sudo setcap all+ep capable || skip +ln -s capable link + +LS_COLORS="ca=30;41:" bfs_diff . -color diff --git a/tests/bfs/color_ca_incapable.out b/tests/bfs/color_ca_incapable.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_ca_incapable.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.gz +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mlower.tar.gz +[01;34mrainbow/[0mlu.tar.GZ +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_ca_incapable.sh b/tests/bfs/color_ca_incapable.sh new file mode 100644 index 0000000..f46a127 --- /dev/null +++ b/tests/bfs/color_ca_incapable.sh @@ -0,0 +1 @@ +LS_COLORS="ca=30;41:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh index 4adba69..4c14610 100644 --- a/tests/bfs/color_ext_case.sh +++ b/tests/bfs/color_ext_case.sh @@ -1,6 +1,6 @@ -# *.gz=01;31:*.GZ=01;32 -- case sensitive -# *.tAr=01;33:*.TaR=01;33 -- case-insensitive -# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive -# *.txt=35:*TXT=36 -- case-insensitive -export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36" +# *.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32 -- case sensitive +# *.tAr=01;33:*.TaR=01;33 -- case-insensitive +# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive +# *.txt=35:*TXT=36 -- case-insensitive +export LS_COLORS="*.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_flipflop.out b/tests/bfs/color_ext_case_flipflop.out new file mode 100644 index 0000000..f4cc53c --- /dev/null +++ b/tests/bfs/color_ext_case_flipflop.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;33mlower.tar.gz[0m +[01;34mrainbow/[0m[01;33mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;33mul.TAR.gz[0m +[01;34mrainbow/[0m[01;33mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.gz +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_flipflop.sh b/tests/bfs/color_ext_case_flipflop.sh new file mode 100644 index 0000000..4d6f615 --- /dev/null +++ b/tests/bfs/color_ext_case_flipflop.sh @@ -0,0 +1 @@ +LS_COLORS="*.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33:*.tar.gz=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_nul.out b/tests/bfs/color_ext_case_nul.out new file mode 100644 index 0000000..8ccd9a7 --- /dev/null +++ b/tests/bfs/color_ext_case_nul.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;31mlower.gz[0m +[01;34mrainbow/[0m[01;31mlower.tar.gz[0m +[01;34mrainbow/[0m[01;31mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;31mul.TAR.gz[0m +[01;34mrainbow/[0m[01;31mupper.GZ[0m +[01;34mrainbow/[0m[01;31mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_nul.sh b/tests/bfs/color_ext_case_nul.sh new file mode 100644 index 0000000..68fea1c --- /dev/null +++ b/tests/bfs/color_ext_case_nul.sh @@ -0,0 +1,5 @@ +# Regression test: embedded NUL bytes in an extension caused an assertion +# failure in the trie implementation + +export LS_COLORS='*.gz=01;31:*\0.GZ=01;32:' +bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_priority.out b/tests/bfs/color_ext_case_priority.out new file mode 100644 index 0000000..4a6c9a0 --- /dev/null +++ b/tests/bfs/color_ext_case_priority.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;31mlower.tar.gz[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;32mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;33mlower.gz[0m +[01;34mrainbow/[0m[01;33mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;33mul.TAR.gz[0m +[01;34mrainbow/[0m[01;33mupper.GZ[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_priority.sh b/tests/bfs/color_ext_case_priority.sh new file mode 100644 index 0000000..f178c56 --- /dev/null +++ b/tests/bfs/color_ext_case_priority.sh @@ -0,0 +1 @@ +LS_COLORS="*.gz=01;33:*.tar.gz=01;31:*.TAR.GZ=01;32:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh index ac4c7fb..9f818c9 100644 --- a/tests/bfs/color_ext_override.sh +++ b/tests/bfs/color_ext_override.sh @@ -1 +1 @@ -LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color +LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;30:*.gz=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ls.out b/tests/bfs/color_ls.out index f69eb9c..cc64318 100644 --- a/tests/bfs/color_ls.out +++ b/tests/bfs/color_ls.out @@ -8,5 +8,5 @@ [01;34mfoo/bar/[0m[01;31mnowhere/nothing[0m [01;34mfoo/bar/[0mbaz [01;34mfoo/bar/[0mbaz -[01;34mfoo/bar/baz[0m[01;31m//qux[0m -[01;34mfoo/bar/baz[0m[01;31m//qux[0m +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m diff --git a/tests/bfs/color_notdir_slash_error.out b/tests/bfs/color_notdir_slash_error.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/bfs/color_notdir_slash_error.out diff --git a/tests/bfs/color_notdir_slash_error.sh b/tests/bfs/color_notdir_slash_error.sh new file mode 100644 index 0000000..ca26d50 --- /dev/null +++ b/tests/bfs/color_notdir_slash_error.sh @@ -0,0 +1,2 @@ +# Regression test: infinite loop printing the error message for .../notdir/nowhere +! bfs_diff -color links/notdir/nowhere diff --git a/tests/bfs/execdir_path_relative_slash.out b/tests/bfs/execdir_path_relative_slash.out new file mode 100644 index 0000000..62b31f6 --- /dev/null +++ b/tests/bfs/execdir_path_relative_slash.out @@ -0,0 +1,19 @@ +./a +./b +./bar +./bar +./basic +./baz +./c +./d +./e +./f +./foo +./foo +./foo +./g +./h +./i +./j +./k +./l diff --git a/tests/bfs/execdir_path_relative_slash.sh b/tests/bfs/execdir_path_relative_slash.sh new file mode 100644 index 0000000..fb5a924 --- /dev/null +++ b/tests/bfs/execdir_path_relative_slash.sh @@ -0,0 +1 @@ +PATH="foo:$PATH" bfs_diff basic -execdir /bin/sh -c 'printf "%s\\n" "$@"' sh {} + diff --git a/tests/bfs/files0_from_root.sh b/tests/bfs/files0_from_root.sh new file mode 100644 index 0000000..6ba5f00 --- /dev/null +++ b/tests/bfs/files0_from_root.sh @@ -0,0 +1,2 @@ +printf 'basic\0' >"$TEST/input" +! invoke_bfs basic -files0-from "$TEST/input" diff --git a/tests/bfs/noerror.out b/tests/bfs/noerror.out new file mode 100644 index 0000000..c4f8ce4 --- /dev/null +++ b/tests/bfs/noerror.out @@ -0,0 +1,4 @@ +inaccessible +inaccessible/dir +inaccessible/file +inaccessible/link diff --git a/tests/bfs/noerror.sh b/tests/bfs/noerror.sh new file mode 100644 index 0000000..e334f8f --- /dev/null +++ b/tests/bfs/noerror.sh @@ -0,0 +1 @@ +bfs_diff inaccessible -noerror diff --git a/tests/bfs/noerror_nowarn.sh b/tests/bfs/noerror_nowarn.sh new file mode 100644 index 0000000..26e7e68 --- /dev/null +++ b/tests/bfs/noerror_nowarn.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs inaccessible -noerror -nowarn 2>&1 >/dev/null) +[ -z "$stderr" ] diff --git a/tests/bfs/noerror_warn.sh b/tests/bfs/noerror_warn.sh new file mode 100644 index 0000000..ec85f4c --- /dev/null +++ b/tests/bfs/noerror_warn.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs inaccessible -noerror -warn 2>&1 >/dev/null) +[ -n "$stderr" ] diff --git a/tests/bfs/nohidden.out b/tests/bfs/nohidden.out index d3ec901..84e6bd2 100644 --- a/tests/bfs/nohidden.out +++ b/tests/bfs/nohidden.out @@ -1,4 +1,8 @@ + +/n weirdnames +weirdnames/ +weirdnames/ weirdnames/ weirdnames/ /j weirdnames/! @@ -11,6 +15,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -19,3 +25,5 @@ weirdnames/[ weirdnames/[/k weirdnames/\ weirdnames/\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bfs/nohidden_depth.out b/tests/bfs/nohidden_depth.out index d3ec901..84e6bd2 100644 --- a/tests/bfs/nohidden_depth.out +++ b/tests/bfs/nohidden_depth.out @@ -1,4 +1,8 @@ + +/n weirdnames +weirdnames/ +weirdnames/ weirdnames/ weirdnames/ /j weirdnames/! @@ -11,6 +15,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -19,3 +25,5 @@ weirdnames/[ weirdnames/[/k weirdnames/\ weirdnames/\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bfs/perm_leading_plus_symbolic.out b/tests/bfs/perm_leading_plus_symbolic.out index e69de29..09bc88f 100644 --- a/tests/bfs/perm_leading_plus_symbolic.out +++ b/tests/bfs/perm_leading_plus_symbolic.out @@ -0,0 +1,3 @@ +perms +perms/drwxr-xr-x +perms/frwxr-xr-x diff --git a/tests/bfs/printf_invalid_flag.sh b/tests/bfs/printf_invalid_flag.sh new file mode 100644 index 0000000..70dfe97 --- /dev/null +++ b/tests/bfs/printf_invalid_flag.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '% p' diff --git a/tests/bfs/warn_without_noerror.sh b/tests/bfs/warn_without_noerror.sh new file mode 100644 index 0000000..5167309 --- /dev/null +++ b/tests/bfs/warn_without_noerror.sh @@ -0,0 +1,2 @@ +# bfs shouldn't print "warning: Suppressed errors" without -noerror +! invoke_bfs inaccessible -warn 2>&1 >/dev/null | grep warning >&2 diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh index 02c8173..4683764 100644 --- a/tests/bfs/xtype_depth.sh +++ b/tests/bfs/xtype_depth.sh @@ -1,2 +1,2 @@ # Make sure -xtype is considered side-effecting for facts_when_impure -! invoke_bfs loops -xtype l -depth 100 +! invoke_bfs inaccessible/link -xtype l -depth 100 diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh index 8d75d7d..c1d94f3 100644 --- a/tests/bfs/xtype_reorder.sh +++ b/tests/bfs/xtype_reorder.sh @@ -1,4 +1,3 @@ # Make sure -xtype is not reordered in front of anything -- if -xtype runs # before -links 100, it will report an ELOOP error -bfs_diff loops -links 100 -xtype l -invoke_bfs loops -links 100 -xtype l +bfs_diff inaccessible/link -links 100 -xtype l diff --git a/tests/bfstd.c b/tests/bfstd.c index dc5ceaa..6e15e2b 100644 --- a/tests/bfstd.c +++ b/tests/bfstd.c @@ -2,92 +2,211 @@ // SPDX-License-Identifier: 0BSD #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 <limits.h> +#include <stdint.h> #include <stdlib.h> #include <string.h> -/** Check the result of xdirname()/xbasename(). */ -static bool check_base_dir(const char *path, const char *dir, const char *base) { - bool ret = true; +/** asciilen() test cases. */ +static void check_asciilen(void) { + bfs_check(asciilen("") == 0); + bfs_check(asciilen("@") == 1); + bfs_check(asciilen("@@") == 2); + bfs_check(asciilen("\xFF@") == 0); + bfs_check(asciilen("@\xFF") == 1); + bfs_check(asciilen("@@@@@@@@") == 8); + bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16); + bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24); + bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15); + bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16); + bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17); + bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); + bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); + bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); +} +/** Check the result of xdirname()/xbasename(). */ +static void check_base_dir(const char *path, const char *dir, const char *base) { char *xdir = xdirname(path); - bfs_verify(xdir, "xdirname(): %s", xstrerror(errno)); - ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); + bfs_everify(xdir, "xdirname()"); + bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); free(xdir); char *xbase = xbasename(path); - bfs_verify(xbase, "xbasename(): %s", xstrerror(errno)); - ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); + bfs_everify(xbase, "xbasename()"); + bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); free(xbase); +} - return ret; +/** xdirname()/xbasename() test cases. */ +static void check_basedirs(void) { + // From man 3p basename + check_base_dir("usr", ".", "usr"); + check_base_dir("usr/", ".", "usr"); + check_base_dir("", ".", "."); + check_base_dir("/", "/", "/"); + // check_base_dir("//", "/" or "//", "/" or "//"); + check_base_dir("///", "/", "/"); + check_base_dir("/usr/", "/", "usr"); + check_base_dir("/usr/lib", "/usr", "lib"); + check_base_dir("//usr//lib//", "//usr", "lib"); + check_base_dir("/home//dwc//test", "/home//dwc", "test"); } /** Check the result of wordesc(). */ -static bool check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { +static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { char buf[256]; char *end = buf + sizeof(buf); char *esc = wordesc(buf, end, str, flags); - return bfs_check(esc != end) - && bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + if (bfs_check(esc != end)) { + bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + } } -bool check_bfstd(void) { - bool ret = true; - - ret &= bfs_check(asciilen("") == 0); - ret &= bfs_check(asciilen("@") == 1); - ret &= bfs_check(asciilen("@@") == 2); - ret &= bfs_check(asciilen("\xFF@") == 0); - ret &= bfs_check(asciilen("@\xFF") == 1); - ret &= bfs_check(asciilen("@@@@@@@@") == 8); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17); - ret &= bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); - ret &= bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); - ret &= bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); - - // From man 3p basename - ret &= check_base_dir("usr", ".", "usr"); - ret &= check_base_dir("usr/", ".", "usr"); - ret &= check_base_dir("", ".", "."); - ret &= check_base_dir("/", "/", "/"); - // check_base_dir("//", "/" or "//", "/" or "//"); - ret &= check_base_dir("///", "/", "/"); - ret &= check_base_dir("/usr/", "/", "usr"); - ret &= check_base_dir("/usr/lib", "/usr", "lib"); - ret &= check_base_dir("//usr//lib//", "//usr", "lib"); - ret &= check_base_dir("/home//dwc//test", "/home//dwc", "test"); - - ret &= check_wordesc("", "\"\"", WESC_SHELL); - ret &= check_wordesc("word", "word", WESC_SHELL); - ret &= check_wordesc("two words", "\"two words\"", WESC_SHELL); - ret &= check_wordesc("word's", "\"word's\"", WESC_SHELL); - ret &= check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL); - ret &= check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL); - ret &= check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("~user", "\"~user\"", WESC_SHELL); +/** wordesc() test cases. */ +static void check_wordescs(void) { + check_wordesc("", "\"\"", WESC_SHELL); + check_wordesc("word", "word", WESC_SHELL); + check_wordesc("two words", "\"two words\"", WESC_SHELL); + check_wordesc("word's", "\"word's\"", WESC_SHELL); + check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL); + check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL); + check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY); + check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY); + check_wordesc("~user", "\"~user\"", WESC_SHELL); const char *charmap = nl_langinfo(CODESET); if (strcmp(charmap, "UTF-8") == 0) { - ret &= check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); + check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); + check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); } +} + +/** xstrto*() test cases. */ +static void check_strtox(void) { + short s; + unsigned short us; + int i; + unsigned int ui; + long l; + unsigned long ul; + long long ll; + unsigned long long ull; + char *end; + +#define check_strtouerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtous(str, end, base, &us) != 0 && errno == err); \ + bfs_echeck(xstrtoui(str, end, base, &ui) != 0 && errno == err); \ + bfs_echeck(xstrtoul(str, end, base, &ul) != 0 && errno == err); \ + bfs_echeck(xstrtoull(str, end, base, &ull) != 0 && errno == err); \ + } while (0) + + check_strtouerr(ERANGE, "-1", NULL, 0); + check_strtouerr(ERANGE, "-0x1", NULL, 0); - ret &= bfs_check(xstrwidth("Hello world") == 11); - ret &= bfs_check(xstrwidth("Hello\1world") == 10); + check_strtouerr(EINVAL, "-", NULL, 0); + check_strtouerr(EINVAL, "-q", NULL, 0); + check_strtouerr(EINVAL, "-1q", NULL, 0); + check_strtouerr(EINVAL, "-0x", NULL, 0); + +#define check_strtoerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtos(str, end, base, &s) != 0 && errno == err); \ + bfs_echeck(xstrtoi(str, end, base, &i) != 0 && errno == err); \ + bfs_echeck(xstrtol(str, end, base, &l) != 0 && errno == err); \ + bfs_echeck(xstrtoll(str, end, base, &ll) != 0 && errno == err); \ + check_strtouerr(err, str, end, base); \ + } while (0) + + check_strtoerr(EINVAL, "", NULL, 0); + check_strtoerr(EINVAL, "", &end, 0); + check_strtoerr(EINVAL, " 1 ", &end, 0); + check_strtoerr(EINVAL, " -1", NULL, 0); + check_strtoerr(EINVAL, " 123", NULL, 0); + check_strtoerr(EINVAL, "123 ", NULL, 0); + check_strtoerr(EINVAL, "0789", NULL, 0); + check_strtoerr(EINVAL, "789A", NULL, 0); + check_strtoerr(EINVAL, "0x", NULL, 0); + check_strtoerr(EINVAL, "0x789A", NULL, 10); + check_strtoerr(EINVAL, "0x-1", NULL, 0); + +#define check_strtotype(type, min, max, fmt, fn, str, base, v, n) \ + do { \ + if ((n) >= min && (n) <= max) { \ + bfs_echeck(fn(str, NULL, base, &v) == 0); \ + bfs_check(v == (type)(n), "%s('%s') == " fmt " (!= " fmt ")", #fn, str, v, (type)(n)); \ + } else { \ + bfs_echeck(fn(str, NULL, base, &v) != 0 && errno == ERANGE); \ + } \ + } while (0) + +#define check_strtoint(str, base, n) \ + do { \ + check_strtotype( signed short, SHRT_MIN, SHRT_MAX, "%d", xstrtos, str, base, s, n); \ + check_strtotype( signed int, INT_MIN, INT_MAX, "%d", xstrtoi, str, base, i, n); \ + check_strtotype( signed long, LONG_MIN, LONG_MAX, "%ld", xstrtol, str, base, l, n); \ + check_strtotype( signed long long, LLONG_MIN, LLONG_MAX, "%lld", xstrtoll, str, base, ll, n); \ + check_strtotype(unsigned short, 0, USHRT_MAX, "%u", xstrtous, str, base, us, n); \ + check_strtotype(unsigned int, 0, UINT_MAX, "%u", xstrtoui, str, base, ui, n); \ + check_strtotype(unsigned long, 0, ULONG_MAX, "%lu", xstrtoul, str, base, ul, n); \ + check_strtotype(unsigned long long, 0, ULLONG_MAX, "%llu", xstrtoull, str, base, ull, n); \ + } while (0) + + check_strtoint("123", 0, 123); + check_strtoint("+123", 0, 123); + check_strtoint("-123", 0, -123); + + check_strtoint("0123", 0, 0123); + check_strtoint("0x789A", 0, 0x789A); + + check_strtoint("0123", 10, 123); + check_strtoint("0789", 10, 789); + + check_strtoint("123", 16, 0x123); + + check_strtoint("0x7FFF", 0, 0x7FFF); + check_strtoint("-0x8000", 0, -0x8000); + + check_strtoint("0x7FFFFFFF", 0, 0x7FFFFFFFL); + check_strtoint("-0x80000000", 0, -0x7FFFFFFFL - 1); + + check_strtoint("0x7FFFFFFFFFFFFFFF", 0, 0x7FFFFFFFFFFFFFFFLL); + check_strtoint("-0x8000000000000000", 0, -0x7FFFFFFFFFFFFFFFLL - 1); + +#define check_strtoend(str, estr, base, n) \ + do { \ + bfs_echeck(xstrtoll(str, &end, base, &ll) == 0); \ + bfs_check(ll == (n), "xstrtoll('%s') == %lld (!= %lld)", str, ll, (long long)(n)); \ + bfs_check(strcmp(end, estr) == 0, "xstrtoll('%s'): end == '%s' (!= '%s')", str, end, estr); \ + } while (0) + + check_strtoend("123 ", " ", 0, 123); + check_strtoend("0789", "89", 0, 07); + check_strtoend("789A", "A", 0, 789); + check_strtoend("0xDEFG", "G", 0, 0xDEF); +} + +/** xstrwidth() test cases. */ +static void check_strwidth(void) { + bfs_check(xstrwidth("Hello world") == 11); + bfs_check(xstrwidth("Hello\1world") == 10); +} - return ret; +void check_bfstd(void) { + check_asciilen(); + check_basedirs(); + check_wordescs(); + check_strtox(); + check_strwidth(); } diff --git a/tests/bit.c b/tests/bit.c index 6548c30..09d470b 100644 --- a/tests/bit.c +++ b/tests/bit.c @@ -2,140 +2,159 @@ // SPDX-License-Identifier: 0BSD #include "tests.h" -#include "../src/bit.h" -#include "../src/config.h" -#include "../src/diag.h" + +#include "bfs.h" +#include "bit.h" + #include <limits.h> #include <stdint.h> #include <string.h> -bfs_static_assert(UMAX_WIDTH(0x1) == 1); -bfs_static_assert(UMAX_WIDTH(0x3) == 2); -bfs_static_assert(UMAX_WIDTH(0x7) == 3); -bfs_static_assert(UMAX_WIDTH(0xF) == 4); -bfs_static_assert(UMAX_WIDTH(0xFF) == 8); -bfs_static_assert(UMAX_WIDTH(0xFFF) == 12); -bfs_static_assert(UMAX_WIDTH(0xFFFF) == 16); +// Polyfill C23's one-argument static_assert() +#if __STDC_VERSION__ < C23 +# undef static_assert +# define static_assert(...) _Static_assert(__VA_ARGS__, #__VA_ARGS__) +#endif + +static_assert(UMAX_WIDTH(0x1) == 1); +static_assert(UMAX_WIDTH(0x3) == 2); +static_assert(UMAX_WIDTH(0x7) == 3); +static_assert(UMAX_WIDTH(0xF) == 4); +static_assert(UMAX_WIDTH(0xFF) == 8); +static_assert(UMAX_WIDTH(0xFFF) == 12); +static_assert(UMAX_WIDTH(0xFFFF) == 16); #define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1) #define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1) #define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1) -bfs_static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); -bfs_static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); -bfs_static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH)); +static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); +static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); +static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH)); -bfs_static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH)); -bfs_static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH)); -bfs_static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH)); +static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH)); +static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH)); +static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH)); -bfs_static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); -bfs_static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); -bfs_static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH)); +static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); +static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); +static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH)); -bfs_static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH)); -bfs_static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH)); -bfs_static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH)); +static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH)); +static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH)); +static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH)); -bfs_static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); -bfs_static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); -bfs_static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH)); +static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); +static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); +static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH)); -bfs_static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH)); -bfs_static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH)); -bfs_static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH)); +static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH)); +static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH)); +static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH)); -bfs_static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); -bfs_static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); -bfs_static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH)); +static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); +static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); +static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH)); -bfs_static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH)); -bfs_static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH)); -bfs_static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH)); +static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH)); +static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH)); +static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH)); #define check_eq(a, b) \ bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b)) -bool check_bit(void) { - bool ret = true; - - const char *str = "\x1\x2\x3\x4"; +void check_bit(void) { + const char *str = "\x1\x2\x3\x4\x5\x6\x7\x8"; uint32_t word; memcpy(&word, str, sizeof(word)); #if ENDIAN_NATIVE == ENDIAN_LITTLE - ret &= check_eq(word, 0x04030201); + check_eq(word, 0x04030201); #elif ENDIAN_NATIVE == ENDIAN_BIG - ret &= check_eq(word, 0x01020304); + check_eq(word, 0x01020304); #else # warning "Skipping byte order tests on mixed/unknown-endian machine" #endif - ret &= check_eq(bswap((uint8_t)0x12), 0x12); - ret &= check_eq(bswap((uint16_t)0x1234), 0x3412); - ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412); - ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412); - - ret &= check_eq(count_ones(0x0), 0); - ret &= check_eq(count_ones(0x1), 1); - ret &= check_eq(count_ones(0x2), 1); - ret &= check_eq(count_ones(0x3), 2); - ret &= check_eq(count_ones(0x137F), 10); - - ret &= check_eq(count_zeros(0), INT_WIDTH); - ret &= check_eq(count_zeros(0L), LONG_WIDTH); - ret &= check_eq(count_zeros(0LL), LLONG_WIDTH); - ret &= check_eq(count_zeros((uint8_t)0), 8); - ret &= check_eq(count_zeros((uint16_t)0), 16); - ret &= check_eq(count_zeros((uint32_t)0), 32); - ret &= check_eq(count_zeros((uint64_t)0), 64); - - ret &= check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A); - ret &= check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123); - ret &= check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345); - ret &= check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678); - - ret &= check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A); - ret &= check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341); - ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123); - ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678); - - for (int i = 0; i < 16; ++i) { + check_eq(bswap((uint8_t)0x12), 0x12); + check_eq(bswap((uint16_t)0x1234), 0x3412); + check_eq(bswap((uint32_t)0x12345678), 0x78563412); + check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412); + + // Make sure we can bswap() every unsigned type + (void)bswap((unsigned char)0); + (void)bswap((unsigned short)0); + (void)bswap(0U); + (void)bswap(0UL); + (void)bswap(0ULL); + + check_eq(load8_beu8(str), 0x01); + check_eq(load8_leu8(str), 0x01); + check_eq(load8_beu16(str), 0x0102); + check_eq(load8_leu16(str), 0x0201); + check_eq(load8_beu32(str), 0x01020304); + check_eq(load8_leu32(str), 0x04030201); + check_eq(load8_beu64(str), 0x0102030405060708ULL); + check_eq(load8_leu64(str), 0x0807060504030201ULL); + + check_eq(count_ones(0x0U), 0); + check_eq(count_ones(0x1U), 1); + check_eq(count_ones(0x2U), 1); + check_eq(count_ones(0x3U), 2); + check_eq(count_ones(0x137FU), 10); + + check_eq(count_zeros(0U), UINT_WIDTH); + check_eq(count_zeros(0UL), ULONG_WIDTH); + check_eq(count_zeros(0ULL), ULLONG_WIDTH); + check_eq(count_zeros((uint8_t)0), 8); + check_eq(count_zeros((uint16_t)0), 16); + check_eq(count_zeros((uint32_t)0), 32); + check_eq(count_zeros((uint64_t)0), 64); + + check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A); + check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123); + check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345); + check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678); + + check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A); + check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341); + check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123); + check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678); + + for (unsigned int i = 0; i < 16; ++i) { uint16_t n = (uint16_t)1 << i; - for (int j = i; j < 16; ++j) { + for (unsigned int j = i; j < 16; ++j) { uint16_t m = (uint16_t)1 << j; uint16_t nm = n | m; - ret &= check_eq(count_ones(nm), 1 + (n != m)); - ret &= check_eq(count_zeros(nm), 15 - (n != m)); - ret &= check_eq(leading_zeros(nm), 15 - j); - ret &= check_eq(trailing_zeros(nm), i); - ret &= check_eq(first_leading_one(nm), j + 1); - ret &= check_eq(first_trailing_one(nm), i + 1); - ret &= check_eq(bit_width(nm), j + 1); - ret &= check_eq(bit_floor(nm), m); + check_eq(count_ones(nm), 1 + (n != m)); + check_eq(count_zeros(nm), 15 - (n != m)); + check_eq(leading_zeros(nm), 15 - j); + check_eq(trailing_zeros(nm), i); + check_eq(first_leading_one(nm), 16 - j); + check_eq(first_trailing_one(nm), i + 1); + check_eq(bit_width(nm), j + 1); + check_eq(bit_floor(nm), m); if (n == m) { - ret &= check_eq(bit_ceil(nm), m); - ret &= bfs_check(has_single_bit(nm)); + check_eq(bit_ceil(nm), m); + bfs_check(has_single_bit(nm)); } else { if (j < 15) { - ret &= check_eq(bit_ceil(nm), (m << 1)); + check_eq(bit_ceil(nm), (m << 1)); } - ret &= bfs_check(!has_single_bit(nm)); + bfs_check(!has_single_bit(nm)); } } } - ret &= check_eq(leading_zeros((uint16_t)0), 16); - ret &= check_eq(trailing_zeros((uint16_t)0), 16); - ret &= check_eq(first_leading_one(0), 0); - ret &= check_eq(first_trailing_one(0), 0); - ret &= check_eq(bit_width(0), 0); - ret &= check_eq(bit_floor(0), 0); - ret &= check_eq(bit_ceil(0), 1); - - ret &= bfs_check(!has_single_bit(0)); - ret &= bfs_check(!has_single_bit(UINT32_MAX)); - ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1))); - - return ret; + check_eq(leading_zeros((uint16_t)0), 16); + check_eq(trailing_zeros((uint16_t)0), 16); + check_eq(first_leading_one(0U), 0); + check_eq(first_trailing_one(0U), 0); + check_eq(bit_width(0U), 0); + check_eq(bit_floor(0U), 0); + check_eq(bit_ceil(0U), 1); + + bfs_check(!has_single_bit(0U)); + bfs_check(!has_single_bit(UINT32_MAX)); + bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1))); } diff --git a/tests/bsd/Hf.out b/tests/bsd/Hf.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/bsd/Hf.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/bsd/Hf.sh b/tests/bsd/Hf.sh new file mode 100644 index 0000000..333280c --- /dev/null +++ b/tests/bsd/Hf.sh @@ -0,0 +1 @@ +bfs_diff -Hf links/deeply/nested/dir diff --git a/tests/bsd/X.out b/tests/bsd/X.out index afa84f7..dbe2408 100644 --- a/tests/bsd/X.out +++ b/tests/bsd/X.out @@ -9,6 +9,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -17,3 +19,5 @@ weirdnames/... weirdnames/.../h weirdnames/[ weirdnames/[/k +weirdnames/{ +weirdnames/{/l diff --git a/tests/bsd/perm_000_plus.out b/tests/bsd/perm_000_plus.out index d7494b8..e279684 100644 --- a/tests/bsd/perm_000_plus.out +++ b/tests/bsd/perm_000_plus.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out index 9a5b95a..1b6d885 100644 --- a/tests/bsd/perm_222_plus.out +++ b/tests/bsd/perm_222_plus.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out index 7e5ae98..eef88ca 100644 --- a/tests/bsd/perm_644_plus.out +++ b/tests/bsd/perm_644_plus.out @@ -1,7 +1,26 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/printx.out b/tests/bsd/printx.out index 04bf9a9..034b2da 100644 --- a/tests/bsd/printx.out +++ b/tests/bsd/printx.out @@ -1,3 +1,5 @@ + +/n weirdnames weirdnames/! weirdnames/!- @@ -9,6 +11,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -17,7 +21,11 @@ weirdnames/... weirdnames/.../h weirdnames/[ weirdnames/[/k +weirdnames/\ +weirdnames/\ weirdnames/\ weirdnames/\ /j weirdnames/\\ weirdnames/\\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bsd/s.out b/tests/bsd/s.out index 6b790c2..5c85ac8 100644 --- a/tests/bsd/s.out +++ b/tests/bsd/s.out @@ -1,12 +1,16 @@ weirdnames +weirdnames/ + weirdnames/ weirdnames/! weirdnames/!- weirdnames/( weirdnames/(- weirdnames/) +weirdnames/* weirdnames/, weirdnames/- weirdnames/... weirdnames/[ weirdnames/\ +weirdnames/{ diff --git a/tests/color.sh b/tests/color.sh index 805d2b8..9e2e0f6 100644 --- a/tests/color.sh +++ b/tests/color.sh @@ -35,3 +35,117 @@ color() { "$@" | "$SED" $'s/\e\\[[^m]*m//g' fi } + +## Status bar + +# Show the terminal status bar +show_bar() { + if [ -z "$TTY" ]; then + return 1 + fi + + # Name the pipe deterministically based on the ttyname, so that concurrent + # tests.sh runs on the same terminal (e.g. make -jN check) cooperate + local pipe="${TMPDIR:-/tmp}/bfs${TTY//\//-}.bar" + + if mkfifo "$pipe" 2>/dev/null; then + # We won the race, create the background process to manage the bar + bar_proc "$pipe" & + exec {BAR}>"$pipe" + elif [ -p "$pipe" ]; then + # We lost the race, connect to the existing process. + # There is a small TOCTTOU race here but I don't see how to avoid it. + exec {BAR}>"$pipe" + else + return 1 + fi +} + +# Print to the terminal status bar +print_bar() { + printf 'PRINT:%d:%s\0' $$ "$1" >&$BAR +} + +# Hide the terminal status bar +hide_bar() { + printf 'HIDE:%d:\0' $$ >&$BAR + exec {BAR}>&- + unset BAR +} + +# The background process that muxes multiple status bars for one TTY +bar_proc() { + # Read from the pipe, write to the TTY + exec <"$1" >/dev/tty + + # Delete the pipe when done + defer rm "$1" + # Reset the scroll region when done + defer printf '\e7\e[r\e8\e[J' + + # Workaround for bash 4: checkwinsize is off by default. We can turn it + # on, but we also have to explicitly trigger a foreground job to finish + # so that it will update the window size before we use $LINES + shopt -s checkwinsize + (:) + + BAR_HEIGHT=0 + resize_bar + # Adjust the bar when the TTY size changes + trap resize_bar WINCH + + # Map from PID to status bar + local -A pid2bar + + # Read commands of the form "OP:PID:STRING\0" + while IFS=':' read -r -d '' op pid str; do + # Map the pid to a bar, creating a new one if necessary + if [ -z "${pid2bar[$pid]:-}" ]; then + pid2bar["$pid"]=$((BAR_HEIGHT++)) + resize_bar + fi + bar="${pid2bar[$pid]}" + + case "$op" in + PRINT) + printf '\e7\e[%d;0f\e[K%s\e8' $((TTY_HEIGHT - bar)) "$str" + ;; + HIDE) + bar="${pid2bar[$pid]}" + # Delete this status bar + unset 'pid2bar[$pid]' + # Shift all higher status bars down + for i in "${!pid2bar[@]}"; do + ibar="${pid2bar[$i]}" + if ((ibar > bar)); then + pid2bar["$i"]=$((ibar - 1)) + fi + done + ((BAR_HEIGHT--)) + resize_bar + ;; + esac + done +} + +# Resize the status bar +resize_bar() { + # Bash gets $LINES from stderr, so if it's redirected use tput instead + TTY_HEIGHT="${LINES:-$(tput lines 2>/dev/tty)}" + + if ((BAR_HEIGHT == 0)); then + return + fi + + # Hide the bars temporarily + local seq='\e7\e[r\e8\e[J' + # Print \eD (IND) N times to ensure N blank lines at the bottom + for ((i = 0; i < BAR_HEIGHT; ++i)); do + seq="${seq}\\eD" + done + # Go back up N lines + seq="${seq}\\e[${BAR_HEIGHT}A" + # Create the new scroll region + seq="${seq}\\e7\\e[;$((TTY_HEIGHT - BAR_HEIGHT))r\\e8\\e[J" + printf "$seq" +} diff --git a/tests/common/HLP.out b/tests/common/HLP.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/common/HLP.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/common/HLP.sh b/tests/common/HLP.sh new file mode 100644 index 0000000..4b6d631 --- /dev/null +++ b/tests/common/HLP.sh @@ -0,0 +1 @@ +bfs_diff -HLP links/deeply/nested/dir diff --git a/tests/common/L_mount.out b/tests/common/L_mount.out deleted file mode 100644 index 788579d..0000000 --- a/tests/common/L_mount.out +++ /dev/null @@ -1,5 +0,0 @@ -. -./foo -./foo/bar -./foo/qux -./mnt diff --git a/tests/common/amin.out b/tests/common/amin.out new file mode 100644 index 0000000..af57325 --- /dev/null +++ b/tests/common/amin.out @@ -0,0 +1,6 @@ +-amin 1: ./one_minute_ago +-amin +1: ./one_hour_ago +-amin +1: ./two_minutes_ago +-amin -1: ./in_one_hour +-amin -1: ./in_one_minute +-amin -1: ./thirty_seconds_ago diff --git a/tests/common/amin.sh b/tests/common/amin.sh new file mode 100644 index 0000000..92c3531 --- /dev/null +++ b/tests/common/amin.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -at "@$((now - 121))" two_minutes_ago +"$XTOUCH" -at "@$((now - 61))" one_minute_ago +"$XTOUCH" -at "@$((now - 30))" thirty_seconds_ago +"$XTOUCH" -at "@$((now + 60))" in_one_minute +"$XTOUCH" -at "@$((now + 60 * 60))" in_one_hour + +bfs_diff . -mindepth 1 \ + \( -amin -1 -exec printf -- '-amin -1: %s\n' {} \; -o -true \) \ + \( -amin 1 -exec printf -- '-amin 1: %s\n' {} \; -o -true \) \ + \( -amin +1 -exec printf -- '-amin +1: %s\n' {} \; -o -true \) diff --git a/tests/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..49f773d --- /dev/null +++ b/tests/common/empty_error.out @@ -0,0 +1 @@ +inaccessible/file diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh new file mode 100644 index 0000000..3438cca --- /dev/null +++ b/tests/common/empty_error.sh @@ -0,0 +1 @@ +! bfs_diff inaccessible -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/gnu/gid_minus.out b/tests/common/gid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_minus.out +++ b/tests/common/gid.out diff --git a/tests/gnu/gid.sh b/tests/common/gid.sh index 2707b4a..2707b4a 100644 --- a/tests/gnu/gid.sh +++ b/tests/common/gid.sh diff --git a/tests/common/gid_invalid_id.sh b/tests/common/gid_invalid_id.sh new file mode 100644 index 0000000..74f0055 --- /dev/null +++ b/tests/common/gid_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -gid 1eW6f5RM9Qi diff --git a/tests/common/gid_invalid_name.sh b/tests/common/gid_invalid_name.sh new file mode 100644 index 0000000..0e2e5f5 --- /dev/null +++ b/tests/common/gid_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -gid eW6f5RM9Qi diff --git a/tests/gnu/gid_minus_plus.out b/tests/common/gid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_minus_plus.out +++ b/tests/common/gid_minus.out diff --git a/tests/gnu/gid_minus.sh b/tests/common/gid_minus.sh index e3822f0..e3822f0 100644 --- a/tests/gnu/gid_minus.sh +++ b/tests/common/gid_minus.sh diff --git a/tests/gnu/gid_plus.out b/tests/common/gid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_plus.out +++ b/tests/common/gid_minus_plus.out diff --git a/tests/gnu/gid_minus_plus.sh b/tests/common/gid_minus_plus.sh index 4ff0877..4ff0877 100644 --- a/tests/gnu/gid_minus_plus.sh +++ b/tests/common/gid_minus_plus.sh diff --git a/tests/gnu/gid_plus_plus.out b/tests/common/gid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_plus_plus.out +++ b/tests/common/gid_plus.out diff --git a/tests/gnu/gid_plus.sh b/tests/common/gid_plus.sh index ccba0e6..ccba0e6 100644 --- a/tests/gnu/gid_plus.sh +++ b/tests/common/gid_plus.sh diff --git a/tests/gnu/uid.out b/tests/common/gid_plus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid.out +++ b/tests/common/gid_plus_plus.out diff --git a/tests/gnu/gid_plus_plus.sh b/tests/common/gid_plus_plus.sh index ec7ae86..ec7ae86 100644 --- a/tests/gnu/gid_plus_plus.sh +++ b/tests/common/gid_plus_plus.sh diff --git a/tests/common/iname.sh b/tests/common/iname.sh deleted file mode 100644 index c25a646..0000000 --- a/tests/common/iname.sh +++ /dev/null @@ -1,2 +0,0 @@ -invoke_bfs -quit -iname PATTERN || skip -bfs_diff basic -iname '*F*' diff --git a/tests/common/mmin.out b/tests/common/mmin.out new file mode 100644 index 0000000..4c79a16 --- /dev/null +++ b/tests/common/mmin.out @@ -0,0 +1,6 @@ +-mmin 1: ./one_minute_ago +-mmin +1: ./one_hour_ago +-mmin +1: ./two_minutes_ago +-mmin -1: ./in_one_hour +-mmin -1: ./in_one_minute +-mmin -1: ./thirty_seconds_ago diff --git a/tests/common/mmin.sh b/tests/common/mmin.sh new file mode 100644 index 0000000..4e1d19c --- /dev/null +++ b/tests/common/mmin.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -mt "@$((now - 121))" two_minutes_ago +"$XTOUCH" -mt "@$((now - 61))" one_minute_ago +"$XTOUCH" -mt "@$((now - 30))" thirty_seconds_ago +"$XTOUCH" -mt "@$((now + 60))" in_one_minute +"$XTOUCH" -mt "@$((now + 60 * 60))" in_one_hour + +bfs_diff . -mindepth 1 \ + \( -mmin -1 -exec printf -- '-mmin -1: %s\n' {} \; -o -true \) \ + \( -mmin 1 -exec printf -- '-mmin 1: %s\n' {} \; -o -true \) \ + \( -mmin +1 -exec printf -- '-mmin +1: %s\n' {} \; -o -true \) diff --git a/tests/common/name_slash.sh b/tests/common/name_slash.sh deleted file mode 100644 index 8d89623..0000000 --- a/tests/common/name_slash.sh +++ /dev/null @@ -1 +0,0 @@ -bfs_diff / -maxdepth 0 -name / diff --git a/tests/common/name_slashes.sh b/tests/common/name_slashes.sh deleted file mode 100644 index 78d0a84..0000000 --- a/tests/common/name_slashes.sh +++ /dev/null @@ -1 +0,0 @@ -bfs_diff /// -maxdepth 0 -name / diff --git a/tests/gnu/uid_minus.out b/tests/common/uid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_minus.out +++ b/tests/common/uid.out diff --git a/tests/gnu/uid.sh b/tests/common/uid.sh index fb3cd93..fb3cd93 100644 --- a/tests/gnu/uid.sh +++ b/tests/common/uid.sh diff --git a/tests/common/uid_invalid_id.sh b/tests/common/uid_invalid_id.sh new file mode 100644 index 0000000..f5b952d --- /dev/null +++ b/tests/common/uid_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -uid 1eW6f5RM9Qi diff --git a/tests/common/uid_invalid_name.sh b/tests/common/uid_invalid_name.sh new file mode 100644 index 0000000..a2c359f --- /dev/null +++ b/tests/common/uid_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -uid eW6f5RM9Qi diff --git a/tests/gnu/uid_minus_plus.out b/tests/common/uid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_minus_plus.out +++ b/tests/common/uid_minus.out diff --git a/tests/gnu/uid_minus.sh b/tests/common/uid_minus.sh index 6d371f2..6d371f2 100644 --- a/tests/gnu/uid_minus.sh +++ b/tests/common/uid_minus.sh diff --git a/tests/gnu/uid_plus.out b/tests/common/uid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_plus.out +++ b/tests/common/uid_minus_plus.out diff --git a/tests/gnu/uid_minus_plus.sh b/tests/common/uid_minus_plus.sh index e7a0496..e7a0496 100644 --- a/tests/gnu/uid_minus_plus.sh +++ b/tests/common/uid_minus_plus.sh diff --git a/tests/gnu/uid_plus_plus.out b/tests/common/uid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_plus_plus.out +++ b/tests/common/uid_plus.out diff --git a/tests/gnu/uid_plus.sh b/tests/common/uid_plus.sh index 22b2c8e..22b2c8e 100644 --- a/tests/gnu/uid_plus.sh +++ b/tests/common/uid_plus.sh diff --git a/tests/common/uid_plus_plus.out b/tests/common/uid_plus_plus.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/common/uid_plus_plus.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/gnu/uid_plus_plus.sh b/tests/common/uid_plus_plus.sh index e021888..e021888 100644 --- a/tests/gnu/uid_plus_plus.sh +++ b/tests/common/uid_plus_plus.sh diff --git a/tests/getopts.sh b/tests/getopts.sh index 5214e9f..a16511f 100644 --- a/tests/getopts.sh +++ b/tests/getopts.sh @@ -5,11 +5,7 @@ ## Argument parsing -if command -v nproc &>/dev/null; then - JOBS=$(nproc) -else - JOBS=1 -fi +JOBS=$(_nproc) MAKE= PATTERNS=() SUDO=() @@ -23,7 +19,6 @@ VERBOSE_TESTS=0 # Print usage information usage() { - local pad=$(printf "%*s" ${#0} "") color cat <<EOF Usage: ${GRN}$0${RST} [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] diff --git a/tests/gnu/L_printf_types.out b/tests/gnu/L_printf_types.out new file mode 100644 index 0000000..734b15f --- /dev/null +++ b/tests/gnu/L_printf_types.out @@ -0,0 +1,17 @@ +(links) () d d +(links/broken) (nowhere) l N +(links/deeply) () d d +(links/deeply/nested) () d d +(links/deeply/nested/broken) (nowhere) l N +(links/deeply/nested/dir) () d d +(links/deeply/nested/file) () f f +(links/deeply/nested/link) () f f +(links/file) () f f +(links/hardlink) () f f +(links/notdir) (symlink/file) l N +(links/skip) () d d +(links/skip/broken) (nowhere) l N +(links/skip/dir) () d d +(links/skip/file) () f f +(links/skip/link) () f f +(links/symlink) () f f diff --git a/tests/gnu/L_printf_types.sh b/tests/gnu/L_printf_types.sh new file mode 100644 index 0000000..caa9083 --- /dev/null +++ b/tests/gnu/L_printf_types.sh @@ -0,0 +1 @@ +bfs_diff -L links -printf '(%p) (%l) %y %Y\n' diff --git a/tests/gnu/execdir_self.out b/tests/gnu/execdir_self.out new file mode 100644 index 0000000..3ad0640 --- /dev/null +++ b/tests/gnu/execdir_self.out @@ -0,0 +1 @@ +./bar.sh diff --git a/tests/gnu/execdir_self.sh b/tests/gnu/execdir_self.sh new file mode 100644 index 0000000..1fc5d04 --- /dev/null +++ b/tests/gnu/execdir_self.sh @@ -0,0 +1,9 @@ +cd "$TEST" +mkdir foo +cat >foo/bar.sh <<EOF +#!/bin/sh +printf '%s\n' "\$@" +EOF +chmod +x foo/bar.sh + +bfs_diff . -name bar.sh -execdir {} {} \; diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out index 49c1b21..e256554 100644 --- a/tests/gnu/executable.out +++ b/tests/gnu/executable.out @@ -1,4 +1,19 @@ perms -perms/rwx -perms/rx -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--x------ +perms/f--x--x--x +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-x------ +perms/fr-xr-xr-x +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh index 85eee8f..7b42772 100644 --- a/tests/gnu/files0_from_empty.sh +++ b/tests/gnu/files0_from_empty.sh @@ -1 +1 @@ -! printf "\0" | invoke_bfs -files0-from - +! printf '\0' | invoke_bfs -files0-from - diff --git a/tests/gnu/files0_from_file.out b/tests/gnu/files0_from_file.out index 1d87e6b..0f6b00d 100644 --- a/tests/gnu/files0_from_file.out +++ b/tests/gnu/files0_from_file.out @@ -1,3 +1,7 @@ + + + + /j /j @@ -16,6 +20,9 @@ ) )/g )/g +* +*/m +*/m , ,/f ,/f @@ -25,9 +32,14 @@ ... .../h .../h +/n +/n [ [/k [/k \ \/i \/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_file_file.out b/tests/gnu/files0_from_file_file.out new file mode 100644 index 0000000..fb683c7 --- /dev/null +++ b/tests/gnu/files0_from_file_file.out @@ -0,0 +1,2 @@ +basic/g +basic/g/h diff --git a/tests/gnu/files0_from_file_file.sh b/tests/gnu/files0_from_file_file.sh new file mode 100644 index 0000000..1119952 --- /dev/null +++ b/tests/gnu/files0_from_file_file.sh @@ -0,0 +1,3 @@ +printf 'basic/c\0' >"$TEST/in1" +printf 'basic/g\0' >"$TEST/in2" +bfs_diff -files0-from "$TEST/in1" -files0-from "$TEST/in2" diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh deleted file mode 100644 index 8e145ce..0000000 --- a/tests/gnu/files0_from_ok.sh +++ /dev/null @@ -1 +0,0 @@ -! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin.out b/tests/gnu/files0_from_stdin.out index 1d87e6b..0f6b00d 100644 --- a/tests/gnu/files0_from_stdin.out +++ b/tests/gnu/files0_from_stdin.out @@ -1,3 +1,7 @@ + + + + /j /j @@ -16,6 +20,9 @@ ) )/g )/g +* +*/m +*/m , ,/f ,/f @@ -25,9 +32,14 @@ ... .../h .../h +/n +/n [ [/k [/k \ \/i \/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_ok.sh b/tests/gnu/files0_from_stdin_ok.sh new file mode 100644 index 0000000..0283c8d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin_ok_file.out b/tests/gnu/files0_from_stdin_ok_file.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_ok_file.sh b/tests/gnu/files0_from_stdin_ok_file.sh new file mode 100644 index 0000000..028df0c --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.sh @@ -0,0 +1,4 @@ +FILE="$TMP/$TEST.in" +cd weirdnames +invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0" +yes | bfs_diff -files0-from - -ok printf '%s\n' {} \; -files0-from "$FILE" diff --git a/tests/gnu/files0_from_stdin_stdin.out b/tests/gnu/files0_from_stdin_stdin.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_stdin.sh b/tests/gnu/files0_from_stdin_stdin.sh new file mode 100644 index 0000000..8f6368f --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.sh @@ -0,0 +1,2 @@ +cd weirdnames +invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -files0-from - diff --git a/tests/gnu/fls_overflow.sh b/tests/gnu/fls_overflow.sh new file mode 100644 index 0000000..067bc86 --- /dev/null +++ b/tests/gnu/fls_overflow.sh @@ -0,0 +1,4 @@ +# Regression test: times that overflow localtime() should still print +cd "$TEST" +"$XTOUCH" -t "@1111111111111111111" overflow || skip +invoke_bfs . -fls "$OUT" diff --git a/tests/gnu/follow_comma.out b/tests/gnu/follow_comma.out index 920b3d3..5e4b806 100644 --- a/tests/gnu/follow_comma.out +++ b/tests/gnu/follow_comma.out @@ -1,4 +1,7 @@ + . +./ +./ ./ ./ /j ./! @@ -11,6 +14,8 @@ ./(/b ./) ./)/g +./* +./*/m ./, ./,/f ./- @@ -21,3 +26,6 @@ ./[/k ./\ ./\/i +./{ +./{/l +/n diff --git a/tests/gnu/follow_files0_from.out b/tests/gnu/follow_files0_from.out new file mode 100644 index 0000000..c77d546 --- /dev/null +++ b/tests/gnu/follow_files0_from.out @@ -0,0 +1,42 @@ +links +links/broken +links/broken +links/deeply +links/deeply +links/deeply/nested +links/deeply/nested +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/file +links/file +links/hardlink +links/hardlink +links/notdir +links/notdir +links/skip +links/skip +links/skip/broken +links/skip/broken +links/skip/dir +links/skip/dir +links/skip/file +links/skip/file +links/skip/link +links/skip/link +links/symlink +links/symlink diff --git a/tests/gnu/follow_files0_from.sh b/tests/gnu/follow_files0_from.sh new file mode 100644 index 0000000..8c20f6d --- /dev/null +++ b/tests/gnu/follow_files0_from.sh @@ -0,0 +1 @@ +invoke_bfs links -print0 | bfs_diff -follow -files0-from - diff --git a/tests/gnu/fprint_noerror.sh b/tests/gnu/fprint_unreached_error.sh index f13a62b..f13a62b 100644 --- a/tests/gnu/fprint_noerror.sh +++ b/tests/gnu/fprint_unreached_error.sh diff --git a/tests/gnu/fstype_btrfs_subvol.out b/tests/gnu/fstype_btrfs_subvol.out new file mode 100644 index 0000000..8871fb9 --- /dev/null +++ b/tests/gnu/fstype_btrfs_subvol.out @@ -0,0 +1,4 @@ +mnt +mnt/file +mnt/subvol +mnt/subvol/file diff --git a/tests/gnu/fstype_btrfs_subvol.sh b/tests/gnu/fstype_btrfs_subvol.sh new file mode 100644 index 0000000..71df45c --- /dev/null +++ b/tests/gnu/fstype_btrfs_subvol.sh @@ -0,0 +1,25 @@ +# Test that -fstype works in btrfs subvolumes + +command -v btrfs &>/dev/null || skip + +cd "$TEST" + +# Make a btrfs filesystem image +truncate -s128M img +mkfs.btrfs img >&2 + +# Mount it +mkdir mnt +bfs_sudo mount img mnt || skip +defer bfs_sudo umount mnt + +# Make it owned by us +bfs_sudo chown "$(id -u):$(id -g)" mnt + +# Create a subvolume inside it +btrfs subvolume create mnt/subvol >&2 + +# Make a file in and outside the subvolume +"$XTOUCH" mnt/file mnt/subvol/file + +bfs_diff mnt -fstype btrfs -print -o -printf '%p %F\n' diff --git a/tests/gnu/ignore_readdir_race_loop.out b/tests/gnu/ignore_readdir_race_loop.out new file mode 100644 index 0000000..a514555 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_loop.out @@ -0,0 +1,11 @@ +loops +loops/broken +loops/deeply +loops/deeply/nested +loops/deeply/nested/dir +loops/file +loops/notdir +loops/skip +loops/skip/dir +loops/skip/loop +loops/symlink diff --git a/tests/gnu/ignore_readdir_race_loop.sh b/tests/gnu/ignore_readdir_race_loop.sh new file mode 100644 index 0000000..3329169 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_loop.sh @@ -0,0 +1,2 @@ +# Make sure -ignore_readdir_race doesn't suppress ELOOP from an actual filesystem loop +! bfs_diff -L loops -ignore_readdir_race diff --git a/tests/gnu/ignore_readdir_race_rmdir.out b/tests/gnu/ignore_readdir_race_rmdir.out new file mode 100644 index 0000000..ede8749 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_rmdir.out @@ -0,0 +1,2 @@ +./bar +./foo diff --git a/tests/gnu/ignore_readdir_race_rmdir.sh b/tests/gnu/ignore_readdir_race_rmdir.sh new file mode 100644 index 0000000..87f36a9 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_rmdir.sh @@ -0,0 +1,5 @@ +cd "$TEST" +"$XTOUCH" -p foo/ bar/ + +# Check that -ignore_readdir_race suppresses errors from opendir() +bfs_diff . -ignore_readdir_race -mindepth 1 -print -name foo -exec rmdir {} \; 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/not_comma.out b/tests/gnu/not_comma.out new file mode 100644 index 0000000..b90468e --- /dev/null +++ b/tests/gnu/not_comma.out @@ -0,0 +1,34 @@ +basic +basic +basic/a +basic/a +basic/b +basic/b +basic/c +basic/c +basic/c/d +basic/c/d +basic/e +basic/e +basic/e/f +basic/g +basic/g +basic/g/h +basic/g/h +basic/i +basic/i +basic/j +basic/j +basic/j/foo +basic/k +basic/k +basic/k/foo +basic/k/foo/bar +basic/k/foo/bar +basic/l +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar +basic/l/foo/bar/baz +basic/l/foo/bar/baz diff --git a/tests/gnu/not_comma.sh b/tests/gnu/not_comma.sh new file mode 100644 index 0000000..04c0195 --- /dev/null +++ b/tests/gnu/not_comma.sh @@ -0,0 +1,2 @@ +# Regression test: assertion failure in sink_not_comma() +bfs_diff basic -not \( -print , -name '*f*' \) -print diff --git a/tests/gnu/ok_files0_from_stdin.sh b/tests/gnu/ok_files0_from_stdin.sh new file mode 100644 index 0000000..2c4de7b --- /dev/null +++ b/tests/gnu/ok_files0_from_stdin.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -ok echo {} \; -files0-from - diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh index 87c7298..a5dc0d0 100644 --- a/tests/gnu/ok_flush.sh +++ b/tests/gnu/ok_flush.sh @@ -1,4 +1,4 @@ # I/O streams should be flushed before -ok prompts -yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT" +yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT" sort_output diff_output diff --git a/tests/gnu/perm_000_slash.out b/tests/gnu/perm_000_slash.out index d7494b8..e279684 100644 --- a/tests/gnu/perm_000_slash.out +++ b/tests/gnu/perm_000_slash.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out index 9a5b95a..1b6d885 100644 --- a/tests/gnu/perm_222_slash.out +++ b/tests/gnu/perm_222_slash.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out index 7e5ae98..eef88ca 100644 --- a/tests/gnu/perm_644_slash.out +++ b/tests/gnu/perm_644_slash.out @@ -1,7 +1,26 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out index 7e5ae98..fcbf49e 100644 --- a/tests/gnu/perm_leading_plus_symbolic_slash.out +++ b/tests/gnu/perm_leading_plus_symbolic_slash.out @@ -1,7 +1,28 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out index 7e5ae98..5a21321 100644 --- a/tests/gnu/perm_symbolic_slash.out +++ b/tests/gnu/perm_symbolic_slash.out @@ -1,7 +1,24 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr--r--r-- +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh index 2ef37ad..98e8faa 100644 --- a/tests/gnu/printf_flags.sh +++ b/tests/gnu/printf_flags.sh @@ -1 +1 @@ -bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n' +bfs_diff basic -printf '|%-10.10p| %+03d % #4m\n' diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out index 386feba..56d1f52 100644 --- a/tests/gnu/readable.out +++ b/tests/gnu/readable.out @@ -1,5 +1,19 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/regextype_awk.out b/tests/gnu/regextype_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_awk.sh b/tests/gnu/regextype_awk.sh new file mode 100644 index 0000000..3718473 --- /dev/null +++ b/tests/gnu/regextype_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype awk -quit || skip + +bfs_diff weirdnames -regextype awk -regex '.*/[\[\*]/.*' diff --git a/tests/gnu/regextype_egrep.out b/tests/gnu/regextype_egrep.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/gnu/regextype_egrep.out diff --git a/tests/gnu/regextype_egrep.sh b/tests/gnu/regextype_egrep.sh new file mode 100644 index 0000000..281d9c0 --- /dev/null +++ b/tests/gnu/regextype_egrep.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype egrep -quit || skip + +bfs_diff weirdnames -regextype egrep -regex '*.*/{l' diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh index 3cc388c..164d17a 100644 --- a/tests/gnu/regextype_emacs.sh +++ b/tests/gnu/regextype_emacs.sh @@ -1,3 +1,3 @@ invoke_bfs -regextype emacs -quit || skip -bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)' +bfs_diff basic -regextype emacs -regex '.*/\(?:f+o?o?\|bar\)' diff --git a/tests/gnu/regextype_findutils_default.out b/tests/gnu/regextype_findutils_default.out new file mode 100644 index 0000000..709a7ba --- /dev/null +++ b/tests/gnu/regextype_findutils_default.out @@ -0,0 +1,3 @@ +/n +weirdnames/ +weirdnames/*/m diff --git a/tests/gnu/regextype_findutils_default.sh b/tests/gnu/regextype_findutils_default.sh new file mode 100644 index 0000000..c870312 --- /dev/null +++ b/tests/gnu/regextype_findutils_default.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype findutils-default -quit || skip + +bfs_diff weirdnames -regextype findutils-default -regex '.*/./\(m\|n\)' diff --git a/tests/gnu/regextype_gnu_awk.out b/tests/gnu/regextype_gnu_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_gnu_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_gnu_awk.sh b/tests/gnu/regextype_gnu_awk.sh new file mode 100644 index 0000000..6b66496 --- /dev/null +++ b/tests/gnu/regextype_gnu_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype gnu-awk -quit || skip + +bfs_diff weirdnames -regextype gnu-awk -regex '.*/[\[\*]/(\<.\>)' diff --git a/tests/gnu/regextype_posix_awk.out b/tests/gnu/regextype_posix_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_posix_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_posix_awk.sh b/tests/gnu/regextype_posix_awk.sh new file mode 100644 index 0000000..86377d7 --- /dev/null +++ b/tests/gnu/regextype_posix_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype posix-awk -quit || skip + +bfs_diff weirdnames -regextype posix-awk -regex '.*/[\[\*]/.*' diff --git a/tests/gnu/regextype_posix_minimal_basic.out b/tests/gnu/regextype_posix_minimal_basic.out new file mode 100644 index 0000000..0f0971e --- /dev/null +++ b/tests/gnu/regextype_posix_minimal_basic.out @@ -0,0 +1 @@ +./( diff --git a/tests/gnu/regextype_posix_minimal_basic.sh b/tests/gnu/regextype_posix_minimal_basic.sh new file mode 100644 index 0000000..ee324f3 --- /dev/null +++ b/tests/gnu/regextype_posix_minimal_basic.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype posix-minimal-basic -regex '\./\((\)' diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh index 5e5d4e9..fe0a778 100644 --- a/tests/gnu/used.sh +++ b/tests/gnu/used.sh @@ -1,37 +1,18 @@ -iso8601() { - printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@" -} - cd "$TEST" -now=$(date '+%Y-%m-%dT%H:%M:%S') - -# Parse the current date -[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail -# Treat leading zeros as decimal, not octal -YYYY=$((10#${BASH_REMATCH[1]})) -MM=$((10#${BASH_REMATCH[2]})) -DD=$((10#${BASH_REMATCH[3]})) -hh=$((10#${BASH_REMATCH[4]})) -mm=$((10#${BASH_REMATCH[5]})) -ss=$((10#${BASH_REMATCH[6]})) +now=$(epoch_time) # -used is always false if atime < ctime -yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss) -"$XTOUCH" -at "$yesterday" yesterday +"$XTOUCH" -at "@$((now - 60 * 60 * 24))" yesterday # -used rounds up -tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$tomorrow" tomorrow +"$XTOUCH" -at "@$((now + 60 * 60))" tomorrow -dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$dayafter" dayafter +"$XTOUCH" -at "@$((now + 60 * 60 * 25))" dayafter -nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$nextweek" nextweek +"$XTOUCH" -at "@$((now + 60 * 60 * (24 * 6 + 1)))" nextweek -nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss) -"$XTOUCH" -at "$nextyear" nextyear +"$XTOUCH" -at "@$((now + 60 * 60 * 24 * 365))" nextyear bfs_diff -mindepth 1 \ -a -used 1 -printf '-used 1: %p\n' \ diff --git a/tests/gnu/writable.out b/tests/gnu/writable.out index 9a5b95a..1b6d885 100644 --- a/tests/gnu/writable.out +++ b/tests/gnu/writable.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/xtype_l_loops.out b/tests/gnu/xtype_l_loops.out new file mode 100644 index 0000000..fdaccab --- /dev/null +++ b/tests/gnu/xtype_l_loops.out @@ -0,0 +1,3 @@ +loops/broken +loops/loop +loops/notdir diff --git a/tests/gnu/xtype_l_loops.sh b/tests/gnu/xtype_l_loops.sh new file mode 100644 index 0000000..6893134 --- /dev/null +++ b/tests/gnu/xtype_l_loops.sh @@ -0,0 +1 @@ +bfs_diff loops -xtype l diff --git a/tests/ioq.c b/tests/ioq.c index 1ce8f75..1a0da97 100644 --- a/tests/ioq.c +++ b/tests/ioq.c @@ -2,12 +2,11 @@ // SPDX-License-Identifier: 0BSD #include "tests.h" -#include "../src/ioq.h" -#include "../src/bfstd.h" -#include "../src/config.h" -#include "../src/diag.h" -#include "../src/dir.h" -#include <errno.h> + +#include "diag.h" +#include "dir.h" +#include "ioq.h" + #include <fcntl.h> #include <stdlib.h> @@ -40,16 +39,17 @@ static void check_ioq_push_block(void) { const size_t depth = 2; struct ioq *ioq = ioq_create(depth, 1); - bfs_verify(ioq, "ioq_create(): %s", xstrerror(errno)); + bfs_everify(ioq, "ioq_create()"); // Push enough operations to fill the queue for (size_t i = 0; i < depth; ++i) { struct bfs_dir *dir = bfs_allocdir(); - bfs_verify(dir, "bfs_allocdir(): %s", xstrerror(errno)); + bfs_everify(dir, "bfs_allocdir()"); int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL); - bfs_verify(ret == 0, "ioq_opendir(): %s", xstrerror(errno)); + bfs_everify(ret == 0, "ioq_opendir()"); } + ioq_submit(ioq); bfs_verify(ioq_capacity(ioq) == 0); // Now cancel the queue, pushing an additional IOQ_STOP message @@ -71,7 +71,6 @@ static void check_ioq_push_block(void) { ioq_destroy(ioq); } -bool check_ioq(void) { +void check_ioq(void) { check_ioq_push_block(); - return true; } diff --git a/tests/list.c b/tests/list.c new file mode 100644 index 0000000..5d0403f --- /dev/null +++ b/tests/list.c @@ -0,0 +1,99 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "bfs.h" +#include "diag.h" +#include "list.h" + +#include <stddef.h> + +struct item { + int n; + struct item *next; +}; + +struct list { + struct item *head; + struct item **tail; +}; + +static bool check_list_items(struct list *list, int *array, size_t size) { + struct item **cur = &list->head; + for (size_t i = 0; i < size; ++i) { + if (!bfs_check(*cur != NULL)) { + return false; + } + int n = (*cur)->n; + if (!bfs_check(n == array[i], "%d != %d", n, array[i])) { + return false; + } + cur = &(*cur)->next; + } + + if (!bfs_check(*cur == NULL)) { + return false; + } + if (!bfs_check(list->tail == cur)) { + return false; + } + + return true; +} + +#define ARRAY(...) (int[]){ __VA_ARGS__ }, countof((int[]){ __VA_ARGS__ }) +#define EMPTY() NULL, 0 + +void check_list(void) { + struct list l1; + SLIST_INIT(&l1); + bfs_verify(check_list_items(&l1, EMPTY())); + + struct list l2; + SLIST_INIT(&l2); + bfs_verify(check_list_items(&l2, EMPTY())); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, EMPTY())); + + struct item i10 = { .n = 10 }; + SLIST_APPEND(&l1, &i10); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + SLIST_SPLICE(&l1, &l1.head, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + struct item i20 = { .n = 20 }; + SLIST_PREPEND(&l2, &i20); + bfs_verify(check_list_items(&l2, ARRAY(20))); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 20))); + bfs_verify(check_list_items(&l2, EMPTY())); + + struct item i15 = { .n = 15 }; + SLIST_APPEND(&l2, &i15); + SLIST_SPLICE(&l1, &i10.next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + bfs_verify(check_list_items(&l2, EMPTY())); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + SLIST_SPLICE(&l1, &i10.next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + SLIST_SPLICE(&l1, &l1.head, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + struct item i11 = { .n = 11 }; + struct item i12 = { .n = 12 }; + SLIST_APPEND(&l2, &i11); + SLIST_APPEND(&l2, &i12); + SLIST_SPLICE(&l1, &l1.head->next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 11, 12, 15, 20))); +} diff --git a/tests/main.c b/tests/main.c index 69903d4..9240e1c 100644 --- a/tests/main.c +++ b/tests/main.c @@ -6,18 +6,44 @@ */ #include "tests.h" -#include "../src/bfstd.h" -#include "../src/color.h" -#include "../src/config.h" -#include <errno.h> + +#include "alloc.h" +#include "bfstd.h" +#include "color.h" +#include "list.h" + #include <locale.h> #include <stdio.h> #include <stdlib.h> +#include <stdint.h> #include <string.h> +#include <sys/wait.h> #include <time.h> +#include <unistd.h> + +/** Result of the current test. */ +static bool pass; + +bool bfs_check_impl(bool result) { + pass &= result; + return result; +} + +/** + * A running test. + */ +struct test_proc { + /** Linked list links. */ + struct test_proc *prev, *next; + + /** The PID of this test. */ + pid_t pid; + /** The name of this test. */ + const char *name; +}; /** - * Test context. + * Global test context. */ struct test_ctx { /** Number of command line arguments. */ @@ -25,6 +51,17 @@ struct test_ctx { /** The arguments themselves. */ char **argv; + /** Maximum jobs (-j). */ + int jobs; + /** Current jobs. */ + int running; + /** Completed jobs. */ + int done; + /** List of running tests. */ + struct { + struct test_proc *head, *tail; + } procs; + /** Parsed colors. */ struct colors *colors; /** Colorized output stream. */ @@ -35,10 +72,15 @@ struct test_ctx { }; /** Initialize the test context. */ -static int test_init(struct test_ctx *ctx, int argc, char **argv) { +static int test_init(struct test_ctx *ctx, int jobs, int argc, char **argv) { ctx->argc = argc; ctx->argv = argv; + ctx->jobs = jobs; + ctx->running = 0; + ctx->done = 0; + LIST_INIT(&ctx->procs); + ctx->colors = parse_colors(); ctx->cout = cfwrap(stdout, ctx->colors, false); if (!ctx->cout) { @@ -50,26 +92,15 @@ static int test_init(struct test_ctx *ctx, int argc, char **argv) { return 0; } -/** Finalize the test context. */ -static int test_fini(struct test_ctx *ctx) { - if (ctx->cout) { - cfclose(ctx->cout); - } - - free_colors(ctx->colors); - - return ctx->ret; -} - /** Check if a test case is enabled for this run. */ static bool should_run(const struct test_ctx *ctx, const char *test) { // Run all tests by default - if (ctx->argc < 2) { + if (ctx->argc == 0) { return true; } // With args, run only specified tests - for (int i = 1; i < ctx->argc; ++i) { + for (int i = 0; i < ctx->argc; ++i) { if (strcmp(test, ctx->argv[i]) == 0) { return true; } @@ -78,20 +109,104 @@ static bool should_run(const struct test_ctx *ctx, const char *test) { return false; } -/** Run a test if it's enabled. */ -static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) { - if (should_run(ctx, test)) { - if (fn()) { - cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test); +/** Wait for a test to finish. */ +static void wait_test(struct test_ctx *ctx) { + int wstatus; + pid_t pid = xwaitpid(0, &wstatus, 0); + bfs_everify(pid > 0, "xwaitpid()"); + + struct test_proc *proc = NULL; + for_list (struct test_proc, i, &ctx->procs) { + if (i->pid == pid) { + proc = i; + break; + } + } + + bfs_verify(proc, "No test_proc for PID %ju", (intmax_t)pid); + + bool passed = false; + + if (WIFEXITED(wstatus)) { + int status = WEXITSTATUS(wstatus); + if (status == EXIT_SUCCESS) { + cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", proc->name); + passed = true; + } else if (status == EXIT_FAILURE) { + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", proc->name); } else { - cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test); - ctx->ret = EXIT_FAILURE; + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (Exit %d)\n", proc->name, status); } + } else { + const char *str = NULL; + if (WIFSIGNALED(wstatus)) { + str = strsignal(WTERMSIG(wstatus)); + } + if (!str) { + str = "Unknown"; + } + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (%s)\n", proc->name, str); + } + + if (!passed) { + ctx->ret = EXIT_FAILURE; } + + --ctx->running; + ++ctx->done; + LIST_REMOVE(&ctx->procs, proc); + free(proc); } -const char *bfs_errstr(void) { - return xstrerror(errno); +/** Unit test function type. */ +typedef void test_fn(void); + +/** Run a test if it's enabled. */ +static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) { + if (!should_run(ctx, test)) { + return; + } + + while (ctx->running >= ctx->jobs) { + wait_test(ctx); + } + + struct test_proc *proc = ALLOC(struct test_proc); + bfs_everify(proc, "alloc()"); + + LIST_ITEM_INIT(proc); + proc->name = test; + + fflush(NULL); + proc->pid = fork(); + bfs_everify(proc->pid >= 0, "fork()"); + + if (proc->pid > 0) { + // Parent + ++ctx->running; + LIST_APPEND(&ctx->procs, proc); + return; + } + + // Child + pass = true; + fn(); + exit(pass ? EXIT_SUCCESS : EXIT_FAILURE); +} + +/** Finalize the test context. */ +static int test_fini(struct test_ctx *ctx) { + while (ctx->running > 0) { + wait_test(ctx); + } + + if (ctx->cout) { + cfclose(ctx->cout); + } + + free_colors(ctx->colors); + + return ctx->ret; } int main(int argc, char *argv[]) { @@ -107,8 +222,37 @@ int main(int argc, char *argv[]) { } tzset(); + unsigned int jobs = 0; + + const char *cmd = argc > 0 ? argv[0] : "units"; + int c; + while (c = getopt(argc, argv, ":j:"), c != -1) { + switch (c) { + case 'j': + if (xstrtoui(optarg, NULL, 10, &jobs) != 0) { + fprintf(stderr, "%s: Bad job count '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case ':': + fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt); + return EXIT_FAILURE; + case '?': + fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt); + return EXIT_FAILURE; + } + } + + if (!jobs) { + jobs = nproc(); + } + + if (optind > argc) { + optind = argc; + } + struct test_ctx ctx; - if (test_init(&ctx, argc, argv) != 0) { + if (test_init(&ctx, jobs, argc - optind, argv + optind) != 0) { goto done; } @@ -116,6 +260,8 @@ int main(int argc, char *argv[]) { run_test(&ctx, "bfstd", check_bfstd); run_test(&ctx, "bit", check_bit); run_test(&ctx, "ioq", check_ioq); + run_test(&ctx, "list", check_list); + run_test(&ctx, "sighook", check_sighook); run_test(&ctx, "trie", check_trie); run_test(&ctx, "xspawn", check_xspawn); run_test(&ctx, "xtime", check_xtime); diff --git a/tests/mksock.c b/tests/mksock.c index f3b61da..f46df96 100644 --- a/tests/mksock.c +++ b/tests/mksock.c @@ -6,7 +6,8 @@ * 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/HL.out b/tests/posix/HL.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/posix/HL.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/posix/HL.sh b/tests/posix/HL.sh new file mode 100644 index 0000000..1858982 --- /dev/null +++ b/tests/posix/HL.sh @@ -0,0 +1 @@ +bfs_diff -HL links diff --git a/tests/posix/LH.out b/tests/posix/LH.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/posix/LH.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/posix/LH.sh b/tests/posix/LH.sh new file mode 100644 index 0000000..ef1d980 --- /dev/null +++ b/tests/posix/LH.sh @@ -0,0 +1 @@ +bfs_diff -LH links/deeply/nested/dir diff --git a/tests/posix/L_mount.out b/tests/posix/L_mount.out new file mode 100644 index 0000000..7ed5f0d --- /dev/null +++ b/tests/posix/L_mount.out @@ -0,0 +1,2 @@ +. +./foo diff --git a/tests/common/L_mount.sh b/tests/posix/L_mount.sh index fd8042a..fd8042a 100644 --- a/tests/common/L_mount.sh +++ b/tests/posix/L_mount.sh diff --git a/tests/posix/atime.out b/tests/posix/atime.out new file mode 100644 index 0000000..5ed206b --- /dev/null +++ b/tests/posix/atime.out @@ -0,0 +1,6 @@ +-atime 1: ./yesterday +-atime +1: ./last_week +-atime +1: ./two_days_ago +-atime -1: ./now +-atime -1: ./one_hour_ago +-atime -1: ./tomorrow diff --git a/tests/posix/atime.sh b/tests/posix/atime.sh new file mode 100644 index 0000000..25dfd7e --- /dev/null +++ b/tests/posix/atime.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -at "@$((now - 60 * 60 * 24 * 7))" last_week +"$XTOUCH" -at "@$((now - 60 * 60 * 49))" two_days_ago +"$XTOUCH" -at "@$((now - 60 * 60 * 25))" yesterday +"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -at "@$((now))" now +"$XTOUCH" -at "@$((now + 60 * 60 * 24))" tomorrow + +bfs_diff . \! -name . \ + \( -atime -1 -exec printf -- '-atime -1: %s\n' {} \; -o -prune \) \ + \( -atime 1 -exec printf -- '-atime 1: %s\n' {} \; -o -prune \) \ + \( -atime +1 -exec printf -- '-atime +1: %s\n' {} \; -o -prune \) diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out index 7ed5f0d..c4f8ce4 100644 --- a/tests/posix/depth_error.out +++ b/tests/posix/depth_error.out @@ -1,2 +1,4 @@ -. -./foo +inaccessible +inaccessible/dir +inaccessible/file +inaccessible/link diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh index db414ba..9b29385 100644 --- a/tests/posix/depth_error.sh +++ b/tests/posix/depth_error.sh @@ -1,7 +1 @@ -cd "$TEST" -"$XTOUCH" -p foo/bar - -chmod a-r foo -defer chmod +r foo - -! bfs_diff . -depth +! bfs_diff inaccessible -depth diff --git a/tests/posix/exec_return.out b/tests/posix/exec_return.out new file mode 100644 index 0000000..600c93a --- /dev/null +++ b/tests/posix/exec_return.out @@ -0,0 +1,18 @@ +basic +basic/a +basic/b +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/exec_return.sh b/tests/posix/exec_return.sh new file mode 100644 index 0000000..cfa0f5d --- /dev/null +++ b/tests/posix/exec_return.sh @@ -0,0 +1 @@ +bfs_diff basic -exec test {} = basic/c \; -o -print diff --git a/tests/posix/exec_sigmask.out b/tests/posix/exec_sigmask.out new file mode 100644 index 0000000..bb646f3 --- /dev/null +++ b/tests/posix/exec_sigmask.out @@ -0,0 +1 @@ +SigBlk: 0000000000000000 diff --git a/tests/posix/exec_sigmask.sh b/tests/posix/exec_sigmask.sh new file mode 100644 index 0000000..2907458 --- /dev/null +++ b/tests/posix/exec_sigmask.sh @@ -0,0 +1,16 @@ +# Regression test: restore the signal mask after fork() + +cd "$TEST" +mkfifo p1 p2 + +{ + # Get the PID of `sh` + read -r pid <p1 + # Send SIGTERM -- this will hang forever if signals are blocked + kill $pid +} & + +# Write the `sh` PID to p1, then hang reading p2 until we're killed +! invoke_bfs p1 -exec bash -c 'echo $$ >p1 && read -r _ <p2' bash {} + || fail + +_wait diff --git a/tests/posix/exec_substring_plus.out b/tests/posix/exec_substring_plus.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_substring_plus.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/exec_substring_plus.sh b/tests/posix/exec_substring_plus.sh new file mode 100644 index 0000000..90309b0 --- /dev/null +++ b/tests/posix/exec_substring_plus.sh @@ -0,0 +1,14 @@ +# https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html +# +# Only a <plus-sign> that immediately follows an argument containing only +# the two characters "{}" shall punctuate the end of the primary expression. +# Other uses of the <plus-sign> shall not be treated as special. +# ... +# If a utility_name or argument string contains the two characters "{}", but +# not just the two characters "{}", it is implementation-defined whether +# find replaces those two characters or uses the string without change. + +invoke_bfs basic -exec printf '%s %s %s %s\n' {} {}+ +{} + \; | sed 's/ .*//' >"$OUT" +sort_output +diff_output + diff --git a/tests/posix/group_invalid_id.sh b/tests/posix/group_invalid_id.sh new file mode 100644 index 0000000..1a89747 --- /dev/null +++ b/tests/posix/group_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -group 1eW6f5RM9Qi diff --git a/tests/posix/group_invalid_name.sh b/tests/posix/group_invalid_name.sh new file mode 100644 index 0000000..a08dc72 --- /dev/null +++ b/tests/posix/group_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -group eW6f5RM9Qi diff --git a/tests/posix/group_o_group.out b/tests/posix/group_o_group.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/group_o_group.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/group_o_group.sh b/tests/posix/group_o_group.sh new file mode 100644 index 0000000..60aefc0 --- /dev/null +++ b/tests/posix/group_o_group.sh @@ -0,0 +1,3 @@ +# Regression test for +# https://github.com/tavianator/bfs/issues/155 +bfs_diff basic -group 0 -o -group "$(id -g)" diff --git a/tests/common/iname.out b/tests/posix/iname.out index a9e5d42..a9e5d42 100644 --- a/tests/common/iname.out +++ b/tests/posix/iname.out diff --git a/tests/posix/iname.sh b/tests/posix/iname.sh new file mode 100644 index 0000000..a9297ac --- /dev/null +++ b/tests/posix/iname.sh @@ -0,0 +1 @@ +bfs_diff basic -iname '*F*' diff --git a/tests/common/mount.out b/tests/posix/mount.out index 6253434..b0ad937 100644 --- a/tests/common/mount.out +++ b/tests/posix/mount.out @@ -1,4 +1,3 @@ . ./foo ./foo/bar -./mnt diff --git a/tests/common/mount.sh b/tests/posix/mount.sh index c9abde5..c9abde5 100644 --- a/tests/common/mount.sh +++ b/tests/posix/mount.sh diff --git a/tests/posix/mtime.out b/tests/posix/mtime.out new file mode 100644 index 0000000..91f0114 --- /dev/null +++ b/tests/posix/mtime.out @@ -0,0 +1,6 @@ +-mtime 1: ./yesterday +-mtime +1: ./last_week +-mtime +1: ./two_days_ago +-mtime -1: ./now +-mtime -1: ./one_hour_ago +-mtime -1: ./tomorrow diff --git a/tests/posix/mtime.sh b/tests/posix/mtime.sh new file mode 100644 index 0000000..8367631 --- /dev/null +++ b/tests/posix/mtime.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -mt "@$((now - 60 * 60 * 24 * 7))" last_week +"$XTOUCH" -mt "@$((now - 60 * 60 * 49))" two_days_ago +"$XTOUCH" -mt "@$((now - 60 * 60 * 25))" yesterday +"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -mt "@$((now))" now +"$XTOUCH" -mt "@$((now + 60 * 60 * 24))" tomorrow + +bfs_diff . \! -name . \ + \( -mtime -1 -exec printf -- '-mtime -1: %s\n' {} \; -o -prune \) \ + \( -mtime 1 -exec printf -- '-mtime 1: %s\n' {} \; -o -prune \) \ + \( -mtime +1 -exec printf -- '-mtime +1: %s\n' {} \; -o -prune \) diff --git a/tests/common/name_slash.out b/tests/posix/name_slash.out index b498fd4..b498fd4 100644 --- a/tests/common/name_slash.out +++ b/tests/posix/name_slash.out diff --git a/tests/posix/name_slash.sh b/tests/posix/name_slash.sh new file mode 100644 index 0000000..b42b145 --- /dev/null +++ b/tests/posix/name_slash.sh @@ -0,0 +1 @@ +bfs_diff / -prune -name / diff --git a/tests/common/name_slashes.out b/tests/posix/name_slashes.out index 187b81f..187b81f 100644 --- a/tests/common/name_slashes.out +++ b/tests/posix/name_slashes.out diff --git a/tests/posix/name_slashes.sh b/tests/posix/name_slashes.sh new file mode 100644 index 0000000..45a39d3 --- /dev/null +++ b/tests/posix/name_slashes.sh @@ -0,0 +1 @@ +bfs_diff /// -prune -name / 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/posix/perm_000.out b/tests/posix/perm_000.out index 5fd30bc..9df7f46 100644 --- a/tests/posix/perm_000.out +++ b/tests/posix/perm_000.out @@ -1 +1 @@ -perms/0 +perms/f--------- diff --git a/tests/posix/perm_000_minus.out b/tests/posix/perm_000_minus.out index d7494b8..e279684 100644 --- a/tests/posix/perm_000_minus.out +++ b/tests/posix/perm_000_minus.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out index 1690e43..bdc5590 100644 --- a/tests/posix/perm_222.out +++ b/tests/posix/perm_222.out @@ -1 +1 @@ -perms/w +perms/f-w--w--w- diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out index 1690e43..342b285 100644 --- a/tests/posix/perm_222_minus.out +++ b/tests/posix/perm_222_minus.out @@ -1 +1,5 @@ -perms/w +perms/drwxrwxrwx +perms/f-w--w--w- +perms/f-wx-wx-wx +perms/frw-rw-rw- +perms/frwxrwxrwx diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out index 4e64e49..9f77ce6 100644 --- a/tests/posix/perm_644.out +++ b/tests/posix/perm_644.out @@ -1 +1 @@ -perms/rw +perms/frw-r--r-- diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out index 2e2576b..84f69f5 100644 --- a/tests/posix/perm_644_minus.out +++ b/tests/posix/perm_644_minus.out @@ -1,3 +1,10 @@ perms -perms/rw -perms/rwx +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_symbolic_minus.out b/tests/posix/perm_leading_plus_symbolic_minus.out index e69de29..38d0e1c 100644 --- a/tests/posix/perm_leading_plus_symbolic_minus.out +++ b/tests/posix/perm_leading_plus_symbolic_minus.out @@ -0,0 +1,7 @@ +perms +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_umask.out b/tests/posix/perm_leading_plus_umask.out new file mode 100644 index 0000000..6ed4b7f --- /dev/null +++ b/tests/posix/perm_leading_plus_umask.out @@ -0,0 +1,10 @@ +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_umask.sh b/tests/posix/perm_leading_plus_umask.sh new file mode 100644 index 0000000..948b4ad --- /dev/null +++ b/tests/posix/perm_leading_plus_umask.sh @@ -0,0 +1,3 @@ +# Test for https://www.austingroupbugs.net/view.php?id=1392 +umask 002 +bfs_diff perms -perm -+w diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out index 2e2576b..84f69f5 100644 --- a/tests/posix/perm_symbolic_minus.out +++ b/tests/posix/perm_symbolic_minus.out @@ -1,3 +1,10 @@ perms -perms/rw -perms/rwx +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out index 4e64e49..9f77ce6 100644 --- a/tests/posix/permcopy.out +++ b/tests/posix/permcopy.out @@ -1 +1 @@ -perms/rw +perms/frw-r--r-- diff --git a/tests/gnu/print0.out b/tests/posix/print0.out Binary files differindex 1347444..1347444 100644 --- a/tests/gnu/print0.out +++ b/tests/posix/print0.out diff --git a/tests/gnu/print0.sh b/tests/posix/print0.sh index b916172..b916172 100644 --- a/tests/gnu/print0.sh +++ b/tests/posix/print0.sh diff --git a/tests/posix/prune_error.out b/tests/posix/prune_error.out new file mode 100644 index 0000000..436c48e --- /dev/null +++ b/tests/posix/prune_error.out @@ -0,0 +1 @@ +inaccessible diff --git a/tests/posix/prune_error.sh b/tests/posix/prune_error.sh new file mode 100644 index 0000000..07a2523 --- /dev/null +++ b/tests/posix/prune_error.sh @@ -0,0 +1 @@ +! bfs_diff -L inaccessible -path '*/*' -prune -o -print diff --git a/tests/posix/root_order.out b/tests/posix/root_order.out new file mode 100644 index 0000000..ea94276 --- /dev/null +++ b/tests/posix/root_order.out @@ -0,0 +1,4 @@ +basic/a +basic/b +basic/c/d +basic/e/f diff --git a/tests/posix/root_order.sh b/tests/posix/root_order.sh new file mode 100644 index 0000000..86adf20 --- /dev/null +++ b/tests/posix/root_order.sh @@ -0,0 +1,6 @@ +# Root paths must be processed in order +# https://www.austingroupbugs.net/view.php?id=1859 + +# -size forces a stat(), which we don't want to be async +invoke_bfs basic/{a,b,c/d,e/f} -size -1000 >"$OUT" +diff_output diff --git a/tests/posix/user_invalid_id.sh b/tests/posix/user_invalid_id.sh new file mode 100644 index 0000000..c378f7e --- /dev/null +++ b/tests/posix/user_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -user 1eW6f5RM9Qi diff --git a/tests/posix/user_invalid_name.sh b/tests/posix/user_invalid_name.sh new file mode 100644 index 0000000..bbf3031 --- /dev/null +++ b/tests/posix/user_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -user eW6f5RM9Qi diff --git a/tests/posix/user_o_user.out b/tests/posix/user_o_user.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/user_o_user.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/user_o_user.sh b/tests/posix/user_o_user.sh new file mode 100644 index 0000000..7c143ae --- /dev/null +++ b/tests/posix/user_o_user.sh @@ -0,0 +1,3 @@ +# Regression test for +# https://github.com/tavianator/bfs/issues/155 +bfs_diff basic -user 0 -o -user "$(id -u)" diff --git a/tests/ptyx.c b/tests/ptyx.c new file mode 100644 index 0000000..59292df --- /dev/null +++ b/tests/ptyx.c @@ -0,0 +1,252 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Execute a command in a pseudo-terminal. + * + * $ ptyx [-w WIDTH] [-h HEIGHT] [--] COMMAND [ARGS...] + */ + +#include "bfs.h" +#include "bfstd.h" + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> + +#if __has_include(<stropts.h>) +# include <stropts.h> +#endif + +#if __sun +/** + * Push a STREAMS module, if it's not already there. + * + * See https://www.illumos.org/issues/9042. + */ +static int i_push(int fd, const char *name) { + int ret = ioctl(fd, I_FIND, name); + if (ret < 0) { + return ret; + } else if (ret == 0) { + return ioctl(fd, I_PUSH, name); + } else { + return 0; + } +} +#endif + +int main(int argc, char *argv[]) { + const char *cmd = argc > 0 ? argv[0] : "ptyx"; + +/** Report an error message and exit. */ +#define die(...) die_(__VA_ARGS__, ) + +#define die_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format "%s", cmd, __VA_ARGS__ "\n"); \ + exit(EXIT_FAILURE); \ + } while (0) + +/** Report an error code and exit. */ +#define edie(...) edie_(__VA_ARGS__, ) + +#define edie_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format ": %s\n", cmd, __VA_ARGS__ errstr()); \ + exit(EXIT_FAILURE); \ + } while (0) + + unsigned short width = 0; + unsigned short height = 0; + + // Parse the command line + int c; + while (c = getopt(argc, argv, "+:w:h:"), c != -1) { + switch (c) { + case 'w': + if (xstrtous(optarg, NULL, 10, &width) != 0) { + edie("Bad width '%s'", optarg); + } + break; + case 'h': + if (xstrtous(optarg, NULL, 10, &height) != 0) { + edie("Bad height '%s'", optarg); + } + break; + case ':': + die("Missing argument to -%c", optopt); + case '?': + die("Unrecognized option -%c", optopt); + } + } + + if (optind >= argc) { + die("Missing command"); + } + char **args = argv + optind; + + // Create a new pty, and set it up + int ptm = posix_openpt(O_RDWR | O_NOCTTY); + if (ptm < 0) { + edie("posix_openpt()"); + } + if (grantpt(ptm) != 0) { + edie("grantpt()"); + } + if (unlockpt(ptm) != 0) { + edie("unlockpt()"); + } + + // Get the subsidiary device path + char *name = ptsname(ptm); + if (!name) { + edie("ptsname()"); + } + + // Open the subsidiary device + int pts = open(name, O_RDWR | O_NOCTTY); + if (pts < 0) { + edie("%s", name); + } + +#if __sun + // On Solaris/illumos, a pty doesn't behave like a terminal until we + // push some STREAMS modules (see ptm(4D), ptem(4M), ldterm(4M)). + if (i_push(pts, "ptem") != 0) { + die("ioctl(I_PUSH, ptem)"); + } + if (i_push(pts, "ldterm") != 0) { + die("ioctl(I_PUSH, ldterm)"); + } +#endif + + // A new pty starts at 0x0, which is not very useful. Instead, grab the + // default size from the current controlling terminal, if possible. + if (!width || !height) { + int tty = open_cterm(O_RDONLY | O_CLOEXEC); + if (tty >= 0) { + struct winsize ws; + if (xtcgetwinsize(tty, &ws) != 0) { + edie("tcgetwinsize()"); + } + if (!width) { + width = ws.ws_col; + } + if (!height) { + height = ws.ws_row; + } + xclose(tty); + } + } + if (!width) { + width = 80; + } + if (!height) { + height = 24; + } + + // Update the pty size + struct winsize ws; + if (xtcgetwinsize(pts, &ws) != 0) { + edie("tcgetwinsize()"); + } + ws.ws_col = width; + ws.ws_row = height; + if (xtcsetwinsize(pts, &ws) != 0) { + edie("tcsetwinsize()"); + } + + // Set custom terminal attributes + struct termios attrs; + if (tcgetattr(pts, &attrs) != 0) { + edie("tcgetattr()"); + } + attrs.c_oflag &= ~OPOST; // Don't convert \n to \r\n + if (tcsetattr(pts, TCSANOW, &attrs) != 0) { + edie("tcsetattr()"); + } + + pid_t pid = fork(); + if (pid < 0) { + edie("fork()"); + } else if (pid == 0) { + // Child + close(ptm); + + // Make ourselves a session leader so we can have our own + // controlling terminal + if (setsid() < 0) { + edie("setsid()"); + } + +#ifdef TIOCSCTTY + // Set the pty as the controlling terminal + if (ioctl(pts, TIOCSCTTY, 0) != 0) { + edie("ioctl(TIOCSCTTY)"); + } +#endif + + // Redirect std{in,out,err} to the pty + if (dup2(pts, STDIN_FILENO) < 0 + || dup2(pts, STDOUT_FILENO) < 0 + || dup2(pts, STDERR_FILENO) < 0) { + edie("dup2()"); + } + if (pts > STDERR_FILENO) { + xclose(pts); + } + + // Run the requested command + execvp(args[0], args); + edie("execvp(): %s", args[0]); + } + + // Parent + xclose(pts); + + // Read output from the pty and copy it to stdout + char buf[1024]; + while (true) { + ssize_t len = read(ptm, buf, sizeof(buf)); + if (len > 0) { + if (xwrite(STDOUT_FILENO, buf, len) < 0) { + edie("write()"); + } + } else if (len == 0) { + break; + } else if (errno == EINTR) { + continue; + } else if (errno == EIO) { + // Linux reports EIO rather than EOF when pts is closed + break; + } else { + die("read()"); + } + } + + xclose(ptm); + + int wstatus; + if (xwaitpid(pid, &wstatus, 0) < 0) { + edie("waitpid()"); + } + + if (WIFEXITED(wstatus)) { + return WEXITSTATUS(wstatus); + } else if (WIFSIGNALED(wstatus)) { + int sig = WTERMSIG(wstatus); + fprintf(stderr, "%s: %s: %s\n", cmd, args[0], strsignal(sig)); + return 128 + sig; + } else { + return 128; + } +} diff --git a/tests/run.sh b/tests/run.sh index 720515d..3ed2a9c 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -5,23 +5,6 @@ ## Running test cases -# Beginning/end of line escape sequences -BOL=$'\n' -EOL=$'\n' - -# Update $EOL for the terminal size -update_eol() { - # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead - local cols="${COLUMNS-}" - if [ -z "$cols" ]; then - cols=$(tput cols 2>/dev/tty) - fi - - # Put the cursor at the last column, then write a space so the next - # character will wrap - EOL=$'\e['"${cols}G " -} - # ERR trap for tests debug_err() { local ret=$? line func file @@ -64,19 +47,19 @@ run_test() { case $ret in 0) if ((VERBOSE_TESTS)); then - color printf "${BOL}${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST" + color printf "${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST" fi ;; $EX_SKIP) if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then - color printf "${BOL}${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST" + color printf "${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST" fi ;; *) if ((!VERBOSE_ERRORS)); then cat "$TMP/$TEST.err" >&2 fi - color printf "${BOL}${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST" + color printf "${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST" ;; esac @@ -111,13 +94,20 @@ reap_test() { # Wait for a background test to finish wait_test() { - local pid - wait -n -ppid - ret=$? - if [ -z "${pid:-}" ]; then - debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR - exit 1 - fi + local pid line ret + + while :; do + line=$((LINENO + 1)) + _wait -n -ppid + ret=$? + + if [ "${pid:-}" ]; then + break + else + debug "${BASH_SOURCE[0]}" $line "${RED}error $ret${RST}" >&$DUPERR + exit 1 + fi + done reap_test $ret } @@ -128,7 +118,9 @@ wait_ready() { # We'd like to parse the output of jobs -n, but we can't run it in a # subshell or we won't get the right output jobs -n >"$TMP/jobs" - while read -r job status ret foo; do + + local job status ret rest + while read -r job status ret rest; do case "$status" in Done) reap_test 0 @@ -156,7 +148,7 @@ comake() { -f "$TESTS/tests.mk" \ DONE=$DONE_PIPE \ READY=$READY_PIPE \ - "${TEST_CASES[@]/#/tests/}" \ + "${!TEST_CASES[@]}" \ </dev/null >/dev/null } @@ -164,35 +156,24 @@ comake() { exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]} } -# Run all the tests -run_tests() { - if ((VERBOSE_TESTS)); then - BOL='' - elif ((COLOR_STDOUT)); then - # Carriage return + clear line - BOL=$'\r\e[K' - - # Workaround for bash 4: checkwinsize is off by default. We can turn it - # on, but we also have to explicitly trigger a foreground job to finish - # so that it will update the window size before we use $COLUMNS - shopt -s checkwinsize - (:) - - update_eol - trap update_eol WINCH +# Print the current test progress +progress() { + if [ "${BAR:-}" ]; then + print_bar "$(printf "$@")" + elif ((VERBOSE_TESTS)); then + color printf "$@" fi +} +# Run all the tests +run_tests() { passed=0 failed=0 skipped=0 ran=0 total=${#TEST_CASES[@]} - if ((COLOR_STDOUT || VERBOSE_TESTS)); then - TEST_FMT="${BOL}${YLW}[%3d%%]${RST} ${BLD}%s${RST}${EOL}" - else - TEST_FMT="." - fi + TEST_FMT="${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" if ((${#MAKE[@]})); then comake @@ -201,6 +182,10 @@ run_tests() { # Turn off set -e (but turn it back on in run_test) set +e + if ((COLOR_STDOUT && !VERBOSE_TESTS)); then + show_bar + fi + for TEST in "${TEST_CASES[@]}"; do wait_ready if ((STOP && failed > 0)); then @@ -208,7 +193,7 @@ run_tests() { fi percent=$((100 * ran / total)) - color printf "$TEST_FMT" $percent "$TEST" + progress "${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" $percent "$TEST" mkdir -p "$TMP/$TEST" OUT="$TMP/$TEST.out" @@ -221,7 +206,9 @@ run_tests() { wait_test done - printf "${BOL}" + if [ "${BAR:-}" ]; then + hide_bar + fi if ((passed > 0)); then color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total @@ -253,7 +240,6 @@ skip() { if ((VERBOSE_SKIPPED)); then caller | { read -r line file - printf "${BOL}" debug "$file" $line "" >&$DUPOUT } fi @@ -373,20 +359,12 @@ invoke_bfs() { fi } -if command -v unbuffer &>/dev/null; then - UNBUFFER=unbuffer -elif command -v expect_unbuffer &>/dev/null; then - UNBUFFER=expect_unbuffer -fi - # Run bfs with a pseudo-terminal attached bfs_pty() { - test -n "${UNBUFFER:-}" || skip - bfs_verbose "$@" local ret=0 - "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$? + "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$? if ((ret > 125)); then exit $ret @@ -424,14 +402,25 @@ make_xattrs() { esac } +# Get the Unix epoch time in seconds +epoch_time() { + if [ "${EPOCHSECONDS:-}" ]; then + # Added in bash 5 + printf '%d' "$EPOCHSECONDS" + else + # https://stackoverflow.com/a/12746260/502399 + awk 'BEGIN { srand(); print srand(); }' + fi +} + ## Snapshot testing # Return value when a difference is detected EX_DIFF=20 # Detect colored diff support -if diff --color /dev/null /dev/null 2>/dev/null; then - DIFF="diff --color" +if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then + DIFF="diff --color=always" else DIFF="diff" fi @@ -447,7 +436,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/sighook.c b/tests/sighook.c new file mode 100644 index 0000000..82e0ae5 --- /dev/null +++ b/tests/sighook.c @@ -0,0 +1,228 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "atomic.h" +#include "bfstd.h" +#include "sighook.h" +#include "thread.h" +#include "xtime.h" + +#include <errno.h> +#include <pthread.h> +#include <signal.h> +#include <stddef.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> + +/** Counts SIGALRM deliveries. */ +static atomic size_t count = 0; + +/** SIGALRM handler. */ +static void alrm_hook(int sig, siginfo_t *info, void *arg) { + fetch_add(&count, 1, relaxed); +} + +/** SH_ONESHOT counter. */ +static atomic size_t shots = 0; + +/** SH_ONESHOT hook. */ +static void alrm_oneshot(int sig, siginfo_t *info, void *arg) { + fetch_add(&shots, 1, relaxed); +} + +/** Keeps the background thread alive. */ +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; +static bool done = false; + +/** Background thread that receives signals. */ +static void *hook_thread(void *ptr) { + mutex_lock(&mutex); + while (!done) { + cond_wait(&cond, &mutex); + } + mutex_unlock(&mutex); + return NULL; +} + +/** Block a signal in this thread. */ +static int block_signal(int sig, sigset_t *old) { + sigset_t set; + if (sigemptyset(&set) != 0) { + return -1; + } + if (sigaddset(&set, sig) != 0) { + return -1; + } + + errno = pthread_sigmask(SIG_BLOCK, &set, old); + if (errno != 0) { + return -1; + } + + return 0; +} + +/** Tests for sighook(). */ +static void check_hooks(void) { + struct sighook *hook = NULL; + struct sighook *oneshot = NULL; + + hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(hook, "sighook(SIGALRM)")) { + return; + } + + // Create a background thread to receive SIGALRM + pthread_t thread; + if (!bfs_echeck(thread_create(&thread, NULL, hook_thread, NULL) == 0)) { + goto unhook; + } + + // Block SIGALRM in this thread so the handler runs concurrently with + // sighook()/sigunhook() + sigset_t mask; + if (!bfs_echeck(block_signal(SIGALRM, &mask) == 0)) { + goto unthread; + } + + // Check that we can unregister and re-register a hook + sigunhook(hook); + hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(hook, "sighook(SIGALRM)")) { + goto unblock; + } + + // Test SH_ONESHOT + oneshot = sighook(SIGALRM, alrm_oneshot, NULL, SH_ONESHOT); + if (!bfs_echeck(oneshot, "sighook(SH_ONESHOT)")) { + goto unblock; + } + + // Create a timer that sends SIGALRM every 100 microseconds + const struct timespec ival = { .tv_nsec = 100 * 1000 }; + struct timer *timer = xtimer_start(&ival); + if (!bfs_echeck(timer, "xtimer_start()")) { + goto unblock; + } + + // Rapidly register/unregister SIGALRM hooks + size_t alarms; + while (alarms = load(&count, relaxed), alarms < 1000) { + size_t nshots = load(&shots, relaxed); + bfs_check(nshots <= 1); + if (alarms > 1) { + bfs_check(nshots == 1); + } + if (alarms >= 500) { + sigunhook(oneshot); + oneshot = NULL; + } + + struct sighook *next = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(next, "sighook(SIGALRM)")) { + break; + } + + sigunhook(hook); + hook = next; + } + + // Stop the timer + xtimer_stop(timer); +unblock: + // Restore the old signal mask + errno = pthread_sigmask(SIG_SETMASK, &mask, NULL); + bfs_echeck(errno == 0, "pthread_sigmask()"); +unthread: + // Quit the background thread + mutex_lock(&mutex); + done = true; + mutex_unlock(&mutex); + cond_signal(&cond); + thread_join(thread, NULL); +unhook: + // Unregister the SIGALRM hooks + sigunhook(oneshot); + sigunhook(hook); +} + +/** atsigexit() hook. */ +static void exit_hook(int sig, siginfo_t *info, void *arg) { + // Write the signal that's killing us to the pipe + int *pipes = arg; + if (xwrite(pipes[1], &sig, sizeof(sig)) != sizeof(sig)) { + abort(); + } +} + +/** Tests for atsigexit(). */ +static void check_sigexit(int sig) { + // To wait for the child to call atsigexit() + int ready[2]; + bfs_everify(pipe(ready) == 0); + + // Written in the atsigexit() handler + int killed[2]; + bfs_everify(pipe(killed) == 0); + + pid_t pid; + bfs_everify((pid = fork()) >= 0); + + if (pid > 0) { + // Parent + xclose(ready[1]); + xclose(killed[1]); + + // Wait for the child to call atsigexit() + char c; + bfs_everify(xread(ready[0], &c, 1) == 1); + + // Kill the child with the signal + bfs_everify(kill(pid, sig) == 0); + + // Check that the child died to the right signal + int wstatus; + if (bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)) { + bfs_check(WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == sig); + } + + // Check that the signal hook wrote the signal number to the pipe + int hsig; + if (bfs_echeck(xread(killed[0], &hsig, sizeof(hsig)) == sizeof(hsig))) { + bfs_check(hsig == sig); + } + } else { + // Child + xclose(ready[0]); + xclose(killed[0]); + + // exit_hook() will write to killed[1] + bfs_everify(atsigexit(exit_hook, killed) != NULL); + + // Tell the parent we're ready + bfs_everify(xwrite(ready[1], "A", 1) == 1); + + // Wait until we're killed + const struct timespec dur = { .tv_nsec = 1 }; + while (true) { + nanosleep(&dur, NULL); + } + } +} + +void check_sighook(void) { + check_hooks(); + + check_sigexit(SIGINT); + check_sigexit(SIGQUIT); + check_sigexit(SIGPIPE); + + // macOS cannot distinguish between sync and async SIG{BUS,ILL,SEGV} +#if !__APPLE__ + check_sigexit(SIGSEGV); +#endif +} diff --git a/tests/stddirs.sh b/tests/stddirs.sh index e08e6bf..1569fee 100644 --- a/tests/stddirs.sh +++ b/tests/stddirs.sh @@ -14,13 +14,31 @@ make_basic() { # Creates a file+directory structure with various permissions for tests make_perms() { - "$XTOUCH" -p -M000 "$1/0" - "$XTOUCH" -p -M444 "$1/r" - "$XTOUCH" -p -M222 "$1/w" - "$XTOUCH" -p -M644 "$1/rw" - "$XTOUCH" -p -M555 "$1/rx" - "$XTOUCH" -p -M311 "$1/wx" - "$XTOUCH" -p -M755 "$1/rwx" + "$XTOUCH" -p -M000 "$1/f---------" + "$XTOUCH" -p -M111 "$1/f--x--x--x" + "$XTOUCH" -p -M222 "$1/f-w--w--w-" + "$XTOUCH" -p -M333 "$1/f-wx-wx-wx" + "$XTOUCH" -p -M444 "$1/fr--r--r--" + "$XTOUCH" -p -M555 "$1/fr-xr-xr-x" "$1/dr-xr-xr-x/" + "$XTOUCH" -p -M666 "$1/frw-rw-rw-" + "$XTOUCH" -p -M777 "$1/frwxrwxrwx" "$1/drwxrwxrwx/" + + "$XTOUCH" -p -M220 "$1/f-w--w----" + "$XTOUCH" -p -M331 "$1/f-wx-wx--x" + "$XTOUCH" -p -M664 "$1/frw-rw-r--" + "$XTOUCH" -p -M775 "$1/frwxrwxr-x" "$1/drwxrwxr-x/" + + "$XTOUCH" -p -M311 "$1/f-wx--x--x" + "$XTOUCH" -p -M644 "$1/frw-r--r--" + "$XTOUCH" -p -M755 "$1/frwxr-xr-x" "$1/drwxr-xr-x/" + + "$XTOUCH" -p -M100 "$1/f--x------" + "$XTOUCH" -p -M200 "$1/f-w-------" + "$XTOUCH" -p -M300 "$1/f-wx------" + "$XTOUCH" -p -M400 "$1/fr--------" + "$XTOUCH" -p -M500 "$1/fr-x------" "$1/dr-x------/" + "$XTOUCH" -p -M600 "$1/frw-------" + "$XTOUCH" -p -M700 "$1/frwxr-----" "$1/drwx------/" } # Creates a file+directory structure with various symbolic and hard links @@ -48,6 +66,12 @@ make_loops() { ln -s deeply/nested/loop/nested "$1/skip" } +# Creates a file+directory structure with inaccessible files +make_inaccessible() { + "$XTOUCH" -p -M000 "$1/file" "$1/dir/" + ln -s dir/file "$1/link" +} + # Creates a file+directory structure with varying timestamps make_times() { "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a" @@ -71,6 +95,9 @@ make_weirdnames() { "$XTOUCH" -p "$1/\\/i" "$XTOUCH" -p "$1/ /j" "$XTOUCH" -p "$1/[/k" + "$XTOUCH" -p "$1/{/l" + "$XTOUCH" -p "$1/*/m" + "$XTOUCH" -p "$1/"$'\n/n' } # Creates a very deep directory structure for testing PATH_MAX handling @@ -124,7 +151,7 @@ make_stddirs() { if ((CLEAN)); then defer clean_stddirs else - printf "Test files saved to ${BLD}%s${RST}\n" "$TMP" + color printf "Test files saved to ${BLD}%s${RST}\n" "$TMP" fi chown "$(id -u):$(id -g)" "$TMP" @@ -133,6 +160,7 @@ make_stddirs() { make_perms "$TMP/perms" make_links "$TMP/links" make_loops "$TMP/loops" + make_inaccessible "$TMP/inaccessible" make_times "$TMP/times" make_weirdnames "$TMP/weirdnames" make_deep "$TMP/deep" @@ -148,5 +176,6 @@ clean_stddirs() { fi done + chmod -R +rwX "$TMP" rm -rf "$TMP" } diff --git a/tests/tests.h b/tests/tests.h index 351badb..d395c7c 100644 --- a/tests/tests.h +++ b/tests/tests.h @@ -8,66 +8,67 @@ #ifndef BFS_TESTS_H #define BFS_TESTS_H -#include "../src/config.h" -#include "../src/diag.h" - -/** Unit test function type. */ -typedef bool test_fn(void); +#include "bfstd.h" +#include "diag.h" /** Memory allocation tests. */ -bool check_alloc(void); +void check_alloc(void); /** Standard library wrapper tests. */ -bool check_bfstd(void); +void check_bfstd(void); /** Bit manipulation tests. */ -bool check_bit(void); +void check_bit(void); /** I/O queue tests. */ -bool check_ioq(void); +void check_ioq(void); + +/** Linked list tests. */ +void check_list(void); + +/** Signal hook tests. */ +void check_sighook(void); /** Trie tests. */ -bool check_trie(void); +void check_trie(void); /** Process spawning tests. */ -bool check_xspawn(void); +void check_xspawn(void); /** Time tests. */ -bool check_xtime(void); +void check_xtime(void); -/** Don't ignore the bfs_check() return value. */ -attr(nodiscard) -static inline bool bfs_check(bool ret) { - return ret; -} +/** Record a single check and return the result. */ +bool bfs_check_impl(bool result); /** * Check a condition, logging a message on failure but continuing. */ #define bfs_check(...) \ - bfs_check(bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", "")) + bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", ) #define bfs_check_(str, cond, format, ...) \ - ((cond) ? true : (bfs_diag( \ - sizeof(format) > 1 \ - ? "%.0s" format "%s%s" \ - : "Check failed: `%s`%s", \ - str, __VA_ARGS__), false)) + bfs_check_impl((cond) || (bfs_check__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false)) -/** Get a string description of the last error. */ -const char *bfs_errstr(void); +#define bfs_check__(format, ...) \ + bfs_diagf(sizeof(format) > 1 \ + ? BFS_DIAG_FORMAT_("%s" format "%s") \ + : BFS_DIAG_FORMAT_("Check failed: `%s`"), \ + BFS_DIAG_ARGS_(__VA_ARGS__)) /** * Check a condition, logging the current error string on failure. */ -#define bfs_pcheck(...) \ - bfs_pcheck_(#__VA_ARGS__, __VA_ARGS__, "", "") - -#define bfs_pcheck_(str, cond, format, ...) \ - ((cond) ? true : (bfs_diag( \ - sizeof(format) > 1 \ - ? "%.0s" format "%s%s: %s" \ - : "Check failed: `%s`%s: %s", \ - str, __VA_ARGS__, bfs_errstr()), false)) +#define bfs_echeck(...) \ + bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", ) + +#define bfs_echeck_(str, cond, format, ...) \ + bfs_check_impl((cond) || (bfs_echeck__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false)) + +#define bfs_echeck__(format, ...) \ + bfs_diagf(sizeof(format) > 1 \ + ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \ + : BFS_DIAG_FORMAT_("Check failed: `%s`: %s"), \ + BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), )) #endif // BFS_TESTS_H 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..59bde40 100644 --- a/tests/trie.c +++ b/tests/trie.c @@ -2,13 +2,15 @@ // SPDX-License-Identifier: 0BSD #include "tests.h" -#include "../src/trie.h" -#include "../src/config.h" -#include "../src/diag.h" + +#include "bfs.h" +#include "diag.h" +#include "trie.h" + #include <stdlib.h> #include <string.h> -const char *keys[] = { +static const char *keys[] = { "foo", "bar", "baz", @@ -18,9 +20,11 @@ const char *keys[] = { "quuuux", "pre", - "pref", "prefi", + "pref", "prefix", + "p", + "pRefix", "AAAA", "AADD", @@ -35,18 +39,20 @@ const char *keys[] = { ">>>>>>", ">>><<<", ">>>", -}; -const size_t nkeys = countof(keys); + "AAAAAAA", + "AAAAAAAB", + "AAAAAAAa", +}; -bool check_trie(void) { - bool ret = true; +static const size_t nkeys = countof(keys); +void check_trie(void) { struct trie trie; trie_init(&trie); for (size_t i = 0; i < nkeys; ++i) { - ret &= bfs_check(!trie_find_str(&trie, keys[i])); + bfs_check(!trie_find_str(&trie, keys[i])); const char *prefix = NULL; for (size_t j = 0; j < i; ++j) { @@ -60,37 +66,38 @@ bool check_trie(void) { struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]); if (prefix) { bfs_verify(leaf); - ret &= bfs_check(strcmp(prefix, leaf->key) == 0); + bfs_check(strcmp(prefix, leaf->key) == 0); } else { - ret &= bfs_check(!leaf); + bfs_check(!leaf); } leaf = trie_insert_str(&trie, keys[i]); bfs_verify(leaf); - ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); - ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); + bfs_check(strcmp(keys[i], leaf->key) == 0); + bfs_check(leaf->length == strlen(keys[i]) + 1); } { size_t i = 0; for_trie (leaf, &trie) { - ret &= bfs_check(leaf == trie_find_str(&trie, keys[i])); - ret &= bfs_check(!leaf->prev || leaf->prev->next == leaf); - ret &= bfs_check(!leaf->next || leaf->next->prev == leaf); + bfs_check(leaf == trie_find_str(&trie, keys[i])); + bfs_check(leaf == trie_insert_str(&trie, keys[i])); + bfs_check(!leaf->prev || leaf->prev->next == leaf); + bfs_check(!leaf->next || leaf->next->prev == leaf); ++i; } - ret &= bfs_check(i == nkeys); + bfs_check(i == nkeys); } for (size_t i = 0; i < nkeys; ++i) { struct trie_leaf *leaf = trie_find_str(&trie, keys[i]); bfs_verify(leaf); - ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); - ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); + bfs_check(strcmp(keys[i], leaf->key) == 0); + bfs_check(leaf->length == strlen(keys[i]) + 1); trie_remove(&trie, leaf); leaf = trie_find_str(&trie, keys[i]); - ret &= bfs_check(!leaf); + bfs_check(!leaf); const char *postfix = NULL; for (size_t j = i + 1; j < nkeys; ++j) { @@ -104,14 +111,14 @@ bool check_trie(void) { leaf = trie_find_postfix(&trie, keys[i]); if (postfix) { bfs_verify(leaf); - ret &= bfs_check(strcmp(postfix, leaf->key) == 0); + bfs_check(strcmp(postfix, leaf->key) == 0); } else { - ret &= bfs_check(!leaf); + bfs_check(!leaf); } } for_trie (leaf, &trie) { - ret &= bfs_check(false, "trie should be empty"); + bfs_check(false, "trie should be empty"); } // This tests the "jump" node handling on 32-bit platforms @@ -120,18 +127,17 @@ bool check_trie(void) { bfs_verify(longstr); memset(longstr, 0xAC, longsize); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); memset(longstr + longsize / 2, 0xAB, longsize / 2); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); memset(longstr, 0xAA, longsize / 2); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); free(longstr); trie_destroy(&trie); - return ret; } diff --git a/tests/util.sh b/tests/util.sh index 7dba9fb..1718a1a 100644 --- a/tests/util.sh +++ b/tests/util.sh @@ -12,13 +12,11 @@ _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" +PTYX="$BIN/tests/ptyx" XTOUCH="$BIN/tests/xtouch" UNAME=$(uname) @@ -62,9 +60,18 @@ stdenv() { # Close stdin so bfs doesn't think we're interactive # dup() the standard fds for logging even when redirected exec </dev/null {DUPOUT}>&1 {DUPERR}>&2 + + # Get the ttyname + if [ -t $DUPOUT ]; then + TTY=$(tty <&$DUPOUT) + elif [ -t $DUPERR ]; then + TTY=$(tty <&$DUPERR) + else + TTY= + fi } -# Drop root priviliges or bail +# Drop root privileges or bail drop_root() { if command -v capsh &>/dev/null; then if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then @@ -183,3 +190,28 @@ pop_defers() { return $ret } + +## Parallelism + +# Get the number of processors +_nproc() { + { + nproc \ + || sysctl -n hw.ncpu \ + || getconf _NPROCESSORS_ONLN \ + || echo 1 + } 2>/dev/null +} + +# Run wait, looping if interrupted +_wait() { + local ret=130 + + # "If wait is interrupted by a signal, the return status will be greater than 128" + while ((ret > 128)); do + ret=0 + wait "$@" || ret=$? + done + + return $ret +} diff --git a/tests/xspawn.c b/tests/xspawn.c index c1bac36..6864192 100644 --- a/tests/xspawn.c +++ b/tests/xspawn.c @@ -2,11 +2,12 @@ // SPDX-License-Identifier: 0BSD #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> @@ -49,111 +50,123 @@ fail: return NULL; } -/** Check that we resolve executables in $PATH correctly. */ -static bool check_use_path(bool use_posix) { - bool ret = true; +/** Add an entry to $PATH. */ +static int add_path(const char *entry, char **old_path) { + int ret = -1; + const char *new_path = NULL; - struct bfs_spawn spawn; - ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0); - if (!ret) { - goto out; - } + *old_path = getenv("PATH"); + if (*old_path) { + *old_path = strdup(*old_path); + if (!*old_path) { + goto done; + } - spawn.flags |= BFS_SPAWN_USE_PATH; - if (!use_posix) { - spawn.flags &= ~BFS_SPAWN_USE_POSIX; + new_path = dstrprintf("%s:%s", entry, *old_path); + if (!new_path) { + goto done; + } + } else { + new_path = entry; } - const char *builddir = getenv("BUILDDIR"); - dchar *bin = dstrprintf("%s/bin", builddir ? builddir : "."); - ret &= bfs_pcheck(bin, "dstrprintf()"); - if (!ret) { - goto destroy; - } + ret = setenv("PATH", new_path, true); - 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; +done: + if (new_path && new_path != entry) { + dstrfree((dchar *)new_path); } - // Check that $PATH is resolved in the parent's environment - char **envp; - ret &= bfs_pcheck(envp = envdup()); - if (!ret) { - goto bin; + if (ret != 0) { + free(*old_path); + *old_path = NULL; } - // Check that $PATH is resolved after the file actions - char *old_path = getenv("PATH"); - dchar *new_path = NULL; + return ret; +} + +/** Undo add_path(). */ +static int reset_path(char *old_path) { + int ret; + if (old_path) { - ret &= bfs_pcheck(old_path = strdup(old_path)); - if (!ret) { - goto env; - } - new_path = dstrprintf("tests:%s", old_path); + ret = setenv("PATH", old_path, true); + free(old_path); } else { - new_path = dstrdup("tests"); - } - ret &= bfs_check(new_path); - if (!ret) { - goto path; + ret = unsetenv("PATH"); } - ret &= bfs_pcheck(setenv("PATH", new_path, true) == 0); - if (!ret) { - goto path; - } + return ret; +} - char *argv[] = {"xspawnee", old_path, NULL}; - pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp); - ret &= bfs_pcheck(pid >= 0, "bfs_spawn()"); - if (!ret) { - goto unset; +/** Spawn the test binary and check for success. */ +static void check_spawnee(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { + pid_t pid = bfs_spawn(exe, ctx, argv, envp); + if (!bfs_echeck(pid >= 0, "bfs_spawn('%s')", exe)) { + return; } int wstatus; - ret &= bfs_pcheck(xwaitpid(pid, &wstatus, 0) == pid) + bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid) && bfs_check(WIFEXITED(wstatus)); - if (ret) { + if (exited) { int wexit = WEXITSTATUS(wstatus); - ret &= bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); + bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); } +} -unset: - if (old_path) { - ret &= bfs_pcheck(setenv("PATH", old_path, true) == 0); - } else { - ret &= bfs_pcheck(unsetenv("PATH") == 0); +/** Check that we resolve executables in $PATH correctly. */ +static void check_use_path(bool use_posix) { + struct bfs_spawn spawn; + if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) { + return; + } + + spawn.flags |= BFS_SPAWN_USE_PATH; + if (!use_posix) { + spawn.flags &= ~BFS_SPAWN_USE_POSIX; + } + + bool init = bfs_echeck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0) + && bfs_echeck(bfs_spawn_adddup2(&spawn, 10, 11) == 0) + && bfs_echeck(bfs_spawn_addclose(&spawn, 10) == 0) + && bfs_echeck(bfs_spawn_addfchdir(&spawn, 11) == 0) + && bfs_echeck(bfs_spawn_addclose(&spawn, 11) == 0); + if (!init) { + goto destroy; + } + + // Check that $PATH is resolved in the parent's environment + char **envp = envdup(); + if (!bfs_echeck(envp, "envdup()")) { + goto destroy; + } + + // Check that $PATH is resolved after the file actions + char *old_path; + if (!bfs_echeck(add_path("tests", &old_path) == 0)) { + goto env; } -path: - dstrfree(new_path); - free(old_path); + + char *argv[] = {"xspawnee", old_path, NULL}; + check_spawnee("xspawnee", &spawn, argv, envp); + check_spawnee("tests/xspawnee", &spawn, argv, envp); + + bfs_echeck(reset_path(old_path) == 0); env: for (char **var = envp; *var; ++var) { free(*var); } free(envp); -bin: - dstrfree(bin); destroy: - ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0); -out: - return ret; + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); } /** Check path resolution of non-existent executables. */ -static bool check_enoent(bool use_posix) { - bool ret = true; - +static void check_enoent(bool use_posix) { struct bfs_spawn spawn; - ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0); - if (!ret) { - goto out; + if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) { + return; } spawn.flags |= BFS_SPAWN_USE_PATH; @@ -163,44 +176,45 @@ static bool check_enoent(bool use_posix) { char *argv[] = {"eW6f5RM9Qi", NULL}; pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL); - ret &= bfs_pcheck(pid < 0 && errno == ENOENT, "bfs_spawn()"); + bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()"); - ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0); -out: - return ret; + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); } -static bool check_resolve(void) { - bool ret = true; +static void check_resolve(void) { char *exe; exe = bfs_spawn_resolve("sh"); - ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')"); + bfs_echeck(exe, "bfs_spawn_resolve('sh')"); free(exe); exe = bfs_spawn_resolve("/bin/sh"); - ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0); + bfs_echeck(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); + bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); free(exe); - ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); - - return ret; -} + bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); -bool check_xspawn(void) { - bool ret = true; + bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT); - ret &= check_use_path(true); - ret &= check_use_path(false); + char *old_path; + if (bfs_echeck(add_path("bin/tests", &old_path) == 0)) { + exe = bfs_spawn_resolve("xspawnee"); + bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); + free(exe); + bfs_echeck(reset_path(old_path) == 0); + } +} - ret &= check_enoent(true); - ret &= check_enoent(false); +void check_xspawn(void) { + check_use_path(true); + check_use_path(false); - ret &= check_resolve(); + check_enoent(true); + check_enoent(false); - return ret; + check_resolve(); } diff --git a/tests/xtime.c b/tests/xtime.c index f85402e..c890a1e 100644 --- a/tests/xtime.c +++ b/tests/xtime.c @@ -2,10 +2,11 @@ // SPDX-License-Identifier: 0BSD #include "tests.h" -#include "../src/xtime.h" -#include "../src/bfstd.h" -#include "../src/config.h" -#include "../src/diag.h" + +#include "bfs.h" +#include "diag.h" +#include "xtime.h" + #include <errno.h> #include <limits.h> #include <stdint.h> @@ -29,9 +30,9 @@ static bool check_one_xgetdate(const char *str, int error, time_t expected) { int ret = xgetdate(str, &ts); if (error) { - return bfs_pcheck(ret == -1 && errno == error, "xgetdate('%s')", str); + return bfs_echeck(ret == -1 && errno == error, "xgetdate('%s')", str); } else { - return bfs_pcheck(ret == 0, "xgetdate('%s')", str) + return bfs_echeck(ret == 0, "xgetdate('%s')", str) && bfs_check(ts.tv_sec == expected && ts.tv_nsec == 0, "xgetdate('%s'): %jd.%09jd != %jd", str, (intmax_t)ts.tv_sec, (intmax_t)ts.tv_nsec, (intmax_t)expected); @@ -39,35 +40,31 @@ static bool check_one_xgetdate(const char *str, int error, time_t expected) { } /** xgetdate() tests. */ -static bool check_xgetdate(void) { - bool ret = true; - - ret &= check_one_xgetdate("", EINVAL, 0); - ret &= check_one_xgetdate("????", EINVAL, 0); - ret &= check_one_xgetdate("1991", EINVAL, 0); - ret &= check_one_xgetdate("1991-??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14", 0, 692668800); - ret &= check_one_xgetdate("1991-12-14-", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T10:??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10:11", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472); - ret &= check_one_xgetdate("1991-12-14T10Z", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472); - ret &= check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T03-07", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472); - ret &= check_one_xgetdate("19911214 031112-0700", 0, 692705472);; - - return ret; +static void check_xgetdate(void) { + check_one_xgetdate("", EINVAL, 0); + check_one_xgetdate("????", EINVAL, 0); + check_one_xgetdate("1991", EINVAL, 0); + check_one_xgetdate("1991-??", EINVAL, 0); + check_one_xgetdate("1991-12", EINVAL, 0); + check_one_xgetdate("1991-12-", EINVAL, 0); + check_one_xgetdate("1991-12-??", EINVAL, 0); + check_one_xgetdate("1991-12-14", 0, 692668800); + check_one_xgetdate("1991-12-14-", EINVAL, 0); + check_one_xgetdate("1991-12-14T", EINVAL, 0); + check_one_xgetdate("1991-12-14T??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10", 0, 692704800); + check_one_xgetdate("1991-12-14T10:??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10:11", 0, 692705460); + check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472); + check_one_xgetdate("1991-12-14T10Z", 0, 692704800); + check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460); + check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472); + check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0); + check_one_xgetdate("1991-12-14T03-07", 0, 692704800); + check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460); + check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472); + check_one_xgetdate("19911214 031112-0700", 0, 692705472);; } #define TM_FORMAT "%04d-%02d-%02d %02d:%02d:%02d (%d/7, %d/365%s)" @@ -82,21 +79,19 @@ static bool check_xgetdate(void) { static bool check_one_xmktime(time_t expected) { struct tm tm; if (!localtime_r(&expected, &tm)) { - bfs_diag("localtime_r(%jd): %s", (intmax_t)expected, xstrerror(errno)); + bfs_ediag("localtime_r(%jd)", (intmax_t)expected); return false; } time_t actual; - return bfs_pcheck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm)) + return bfs_echeck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm)) && bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected); } /** xmktime() tests. */ -static bool check_xmktime(void) { - bool ret = true; - +static void check_xmktime(void) { for (time_t time = -10; time <= 10; ++time) { - ret &= check_one_xmktime(time); + check_one_xmktime(time); } // Attempt to trigger overflow (but don't test for it, since it's not mandatory) @@ -111,12 +106,10 @@ static bool check_xmktime(void) { }; time_t time; xmktime(&tm, &time); - - return ret; } /** Check one xtimegm() result. */ -static bool check_one_xtimegm(const struct tm *tm) { +static void check_one_xtimegm(const struct tm *tm) { struct tm tma = *tm, tmb = *tm; time_t ta, tb; ta = mktime(&tma); @@ -124,45 +117,51 @@ static bool check_one_xtimegm(const struct tm *tm) { tb = -1; } - bool ret = true; - ret &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb); - ret &= bfs_check(ta == -1 || tm_equal(&tma, &tmb)); + bool pass = true; + pass &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb); + if (ta != -1) { + pass &= bfs_check(tm_equal(&tma, &tmb)); + } - if (!ret) { + if (!pass) { bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma)); bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb)); bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); } - - return ret; } +#if !BFS_HAS_TIMEGM /** Check an overflowing xtimegm() call. */ -static bool check_xtimegm_overflow(const struct tm *tm) { +static void check_xtimegm_overflow(const struct tm *tm) { struct tm copy = *tm; time_t time = 123; - bool ret = true; - ret &= bfs_check(xtimegm(©, &time) == -1 && errno == EOVERFLOW); - ret &= bfs_check(tm_equal(©, tm)); - ret &= bfs_check(time == 123); + bool pass = true; + pass &= bfs_check(xtimegm(©, &time) == -1 && errno == EOVERFLOW); + pass &= bfs_check(tm_equal(©, tm)); + pass &= bfs_check(time == 123); - if (!ret) { + if (!pass) { bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy)); bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); } - - return ret; } +#endif /** xtimegm() tests. */ -static bool check_xtimegm(void) { - bool ret = true; - +static void check_xtimegm(void) { struct tm tm = { .tm_isdst = -1, }; +#if BFS_HAS_TIMEGM + // Check that xtimegm(-1) isn't an error + for (time_t time = -10; time <= 10; ++time) { + if (bfs_check(gmtime_r(&time, &tm), "gmtime_r(%jd)", (intmax_t)time)) { + check_one_xtimegm(&tm); + } + } +#else // Check equivalence with mktime() for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10) for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3) @@ -170,7 +169,7 @@ static bool check_xtimegm(void) { for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5) for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31) for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) { - ret &= check_one_xtimegm(&tm); + check_one_xtimegm(&tm); } // Check integer overflow cases @@ -178,14 +177,11 @@ static bool check_xtimegm(void) { 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 }); - - return ret; +#endif // !BFS_HAS_TIMEGM } -bool check_xtime(void) { - bool ret = true; - ret &= check_xgetdate(); - ret &= check_xmktime(); - ret &= check_xtimegm(); - return ret; +void check_xtime(void) { + check_xgetdate(); + check_xmktime(); + check_xtimegm(); } diff --git a/tests/xtouch.c b/tests/xtouch.c index fad272f..f33c573 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 "bfstd.h" +#include "sanity.h" +#include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <stdio.h> @@ -120,7 +120,7 @@ static int at_flags(const struct args *args) { /** Touch one path. */ static int xtouch(const struct args *args, const char *path) { int dfd = open_parent(args, &path); - if (dfd < 0 && dfd != AT_FDCWD) { + if (dfd < 0 && dfd != (int)AT_FDCWD) { return -1; } @@ -217,11 +217,8 @@ int main(int argc, char *argv[]) { } if (marg) { - char *end; - long mode = strtol(marg, &end, 8); - // https://github.com/llvm/llvm-project/issues/64946 - sanitize_init(&end); - if (*marg && !*end && mode >= 0 && mode < 01000) { + unsigned int mode; + if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) { args.fmode = args.dmode = mode; } else { fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg); @@ -237,8 +234,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, ×[0]) != 0) { fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno)); @@ -247,8 +244,8 @@ int main(int argc, char *argv[]) { times[1] = times[0]; } else { // Don't use UTIME_NOW, so that multiple paths all get the same timestamp - if (xgettime(×[0]) != 0) { - perror("xgettime()"); + if (clock_gettime(CLOCK_REALTIME, ×[0]) != 0) { + perror("clock_gettime()"); return EXIT_FAILURE; } times[1] = times[0]; |