diff options
1227 files changed, 29516 insertions, 12360 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 9db363d..4075eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,85 +3,253 @@ name: CI on: [push, pull_request] jobs: - linux: - name: Linux + linux-x86: + name: Linux (x86) + runs-on: ubuntu-24.04 - runs-on: ubuntu-latest + # 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@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | sudo dpkg --add-architecture i386 sudo apt-get update -y sudo apt-get install -y \ + mandoc \ gcc-multilib \ + libgcc-s1:i386 \ acl \ libacl1-dev \ libacl1:i386 \ attr \ - libattr1-dev \ - libattr1:i386 \ libcap2-bin \ libcap-dev \ libcap2:i386 \ libonig-dev \ - libonig5:i386 + libonig5:i386 \ + liburing-dev # 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 -lattr -lcap + # 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 - 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-latest + 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@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install coreutils + brew install bash - name: Run tests run: | - make -j$(sysctl -n hw.ncpu) distcheck + jobs=$(sysctl -n hw.ncpu) + .github/diag.sh make -j$jobs distcheck freebsd: name: FreeBSD + runs-on: ubuntu-24.04 + + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name - if: ${{ github.repository_owner == 'tavianator' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + steps: + - uses: actions/checkout@v4 - runs-on: ubuntu-latest + - name: Run tests + uses: cross-platform-actions/action@v0.28.0 + with: + operating_system: freebsd + version: "14.2" + + run: | + sudo pkg install -y \ + bash \ + oniguruma \ + pkgconf + sudo mount -t fdescfs none /dev/fd + .github/diag.sh make -j$(nproc) distcheck + + - uses: actions/upload-artifact@v4 + with: + name: freebsd-config.log + path: distcheck-*/gen/config.log - concurrency: spurion + openbsd: + name: OpenBSD + runs-on: ubuntu-24.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@v3 + - uses: actions/checkout@v4 - - uses: tailscale/github-action@main + - name: Run tests + uses: cross-platform-actions/action@v0.28.0 with: - authkey: ${{ secrets.TAILSCALE_KEY }} + operating_system: openbsd + version: "7.7" + + run: | + sudo pkg_add \ + bash \ + gmake \ + oniguruma + jobs=$(sysctl -n hw.ncpu) + ./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 - - name: Configure SSH - env: - SSH_KEY: ${{ secrets.SSH_KEY }} - run: | - mkdir ~/.ssh - printf '%s' "$SSH_KEY" >~/.ssh/github-actions - chmod 0600 ~/.ssh/github-actions - printf 'Host %s\n\tStrictHostKeyChecking=accept-new\n\tUser github\n\tIdentityFile ~/.ssh/github-actions\n' "$(tailscale ip -6 spurion)" >~/.ssh/config + netbsd: + name: NetBSD + runs-on: ubuntu-24.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 - run: | - spurion=$(tailscale ip -6 spurion) - rsync -rl --delete . "[$spurion]:bfs" - ssh "$spurion" 'gmake -C bfs -j$(sysctl -n hw.ncpu) distcheck' + uses: cross-platform-actions/action@v0.28.0 + with: + operating_system: netbsd + version: "10.1" + + run: | + PATH="/sbin:/usr/sbin:$PATH" + sudo pkgin -y install \ + bash \ + oniguruma \ + pkgconf + jobs=$(sysctl -n hw.ncpu) + ./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 + + 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/dragonflybsd-vm@v1 + with: + release: "6.4.0" + usesh: true + + prepare: | + pkg install -y \ + bash \ + oniguruma \ + 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 ./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 + + 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/omnios-vm@v1 + with: + release: "r151052" + usesh: true + + prepare: | + pkg install \ + bash \ + build-essential \ + gnu-make \ + onig \ + sudo + useradd -m -g staff action + echo "%staff ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers + + run: | + PATH="/usr/xpg4/bin:$PATH" + chown -R action:staff . + jobs=$(getconf NPROCESSORS_ONLN) + 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 c8808d3..e4e8f71 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -1,13 +1,13 @@ name: codecov.io -on: [push, pull_request] +on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -17,16 +17,18 @@ jobs: acl \ libacl1-dev \ attr \ - libattr1-dev \ libcap2-bin \ libcap-dev \ - libonig-dev + libonig-dev \ + liburing-dev - name: Generate coverage run: | - make -j$(nproc) gcov check TEST_FLAGS="--sudo" - gcov -abcfu obj/*/*.o + ./configure --enable-gcov + make -j$(nproc) check TEST_FLAGS="--sudo" + gcov -abcfpu obj/*/*.o - - uses: codecov/codecov-action@v3.1.0 + - 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 new file mode 100644 index 0000000..1f2041c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,60 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "10 14 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y \ + gcc \ + acl \ + libacl1-dev \ + attr \ + 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: | + make -j$(nproc) all + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:cpp" @@ -1,2 +1,4 @@ /bin/ +/gen/ /obj/ +/distcheck-*/ @@ -1,12 +1,11 @@ -Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> +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. +Permission to use, copy, modify, and/or distribute this software for any purpose with or +without fee is hereby granted. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT +SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. @@ -1,313 +1,310 @@ -############################################################################ -# bfs # -# Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> # -# # -# Permission to use, copy, modify, and/or distribute this software for any # -# purpose with or without fee is hereby granted. # -# # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -############################################################################ - -ifneq ($(wildcard .git),) -VERSION := $(shell git describe --always 2>/dev/null) -endif - -ifndef VERSION -VERSION := 2.6.1 -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 \ - -Wmissing-declarations \ - -Wshadow \ - -Wsign-compare \ - -Wstrict-prototypes \ - -Wimplicit-fallthrough - -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_FILE_OFFSET_BITS=64 \ - -D_TIME_BITS=64 \ - -D_GNU_SOURCE \ - -DBFS_VERSION=\"$(VERSION)\" - -LOCAL_CFLAGS := -std=c11 -LOCAL_LDFLAGS := -LOCAL_LDLIBS := - -ASAN := $(filter asan,$(MAKECMDGOALS)) -LSAN := $(filter lsan,$(MAKECMDGOALS)) -MSAN := $(filter msan,$(MAKECMDGOALS)) -TSAN := $(filter tsan,$(MAKECMDGOALS)) -UBSAN := $(filter ubsan,$(MAKECMDGOALS)) - -ifndef MSAN -WITH_ONIGURUMA := y -endif - -ifdef WITH_ONIGURUMA -LOCAL_CPPFLAGS += -DBFS_WITH_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 - -ifeq ($(OS),Linux) -ifndef MSAN # These libraries are not built with msan -WITH_ACL := y -WITH_ATTR := y -WITH_LIBCAP := y -endif - -ifdef WITH_ACL -LOCAL_LDLIBS += -lacl -else -LOCAL_CPPFLAGS += -DBFS_HAS_SYS_ACL=0 -endif - -ifdef WITH_ATTR -LOCAL_LDLIBS += -lattr -else -LOCAL_CPPFLAGS += -DBFS_HAS_SYS_XATTR=0 -endif - -ifdef WITH_LIBCAP -LOCAL_LDLIBS += -lcap -else -LOCAL_CPPFLAGS += -DBFS_HAS_SYS_CAPABILITY=0 -endif - -LOCAL_LDFLAGS += -Wl,--as-needed -LOCAL_LDLIBS += -lrt -endif - -ifeq ($(OS),NetBSD) -LOCAL_LDLIBS += -lutil -endif - -ifdef ASAN -LOCAL_CFLAGS += -fsanitize=address -SANITIZE := y -endif - -ifdef LSAN -LOCAL_CFLAGS += -fsanitize=leak -SANITIZE := y -endif - -ifdef MSAN -LOCAL_CFLAGS += -fsanitize=memory -fsanitize-memory-track-origins -SANITIZE := y -endif - -ifdef TSAN -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 - -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 release,$(MAKECMDGOALS)),) -CFLAGS := $(DEFAULT_CFLAGS) -O3 -flto -DNDEBUG -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) - -# Goals that are treated like flags by this Makefile -FLAG_GOALS := asan lsan msan tsan ubsan gcov 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 - -# The different search strategies that we test -STRATEGIES := bfs dfs ids eds -STRATEGY_CHECKS := $(STRATEGIES:%=check-%) - -# All the different checks we run -CHECKS := $(STRATEGY_CHECKS) check-trie check-xtimegm - -# Custom test flags for distcheck -DISTCHECK_FLAGS := -s TEST_FLAGS="--sudo --verbose=skipped" - -bfs: $(BIN)/bfs +# 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: $(BIN)/bfs $(BIN)/tests/mksock $(BIN)/tests/trie $(BIN)/tests/xtimegm +# 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 -$(BIN)/bfs: \ - $(OBJ)/src/bar.o \ - $(OBJ)/src/bftw.o \ - $(OBJ)/src/color.o \ - $(OBJ)/src/ctx.o \ - $(OBJ)/src/darray.o \ - $(OBJ)/src/diag.o \ - $(OBJ)/src/dir.o \ - $(OBJ)/src/dstring.o \ - $(OBJ)/src/eval.o \ - $(OBJ)/src/exec.o \ - $(OBJ)/src/fsade.o \ - $(OBJ)/src/main.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/trie.o \ - $(OBJ)/src/typo.o \ - $(OBJ)/src/util.o \ - $(OBJ)/src/xregex.o \ - $(OBJ)/src/xspawn.o \ - $(OBJ)/src/xtime.o - -$(BIN)/tests/mksock: $(OBJ)/tests/mksock.o -$(BIN)/tests/trie: $(OBJ)/src/trie.o $(OBJ)/tests/trie.o -$(BIN)/tests/xtimegm: $(OBJ)/src/xtime.o $(OBJ)/tests/xtimegm.o - -$(BIN)/%: - @$(MKDIR) $(@D) - +$(CC) $(ALL_LDFLAGS) $^ $(ALL_LDLIBS) -o $@ - -$(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 $< $@ - -# Make sure that "make release" builds everything, but "make release obj/src/main.o" doesn't -$(FLAG_GOALS): $(FLAG_PREREQS) - @: -.PHONY: $(FLAG_GOALS) - -check: $(CHECKS) -.PHONY: check $(CHECKS) - -$(STRATEGY_CHECKS): check-%: $(BIN)/bfs $(BIN)/tests/mksock - ./tests/tests.sh --bfs="$(BIN)/bfs -S $*" $(TEST_FLAGS) - -check-trie check-xtimegm: check-%: $(BIN)/tests/% - $< - +# 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) -B asan ubsan check $(DISTCHECK_FLAGS) -ifneq ($(OS),Darwin) - +$(MAKE) -B msan check CC=clang $(DISTCHECK_FLAGS) -endif -ifeq ($(OS) $(ARCH),Linux x86_64) - +$(MAKE) -B check EXTRA_CFLAGS="-m32" ONIG_CONFIG= $(DISTCHECK_FLAGS) -endif - +$(MAKE) -B release check $(DISTCHECK_FLAGS) - +$(MAKE) -B check $(DISTCHECK_FLAGS) - +$(MAKE) check-install $(DISTCHECK_FLAGS) + @+${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 -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) +# 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 @@ -1,24 +1,30 @@ <div align="center"> -`bfs` -===== - +<h1> +<code>bfs</code> +<br clear="all"> <a href="https://github.com/tavianator/bfs/releases"><img src="https://img.shields.io/github/v/tag/tavianator/bfs?label=version" alt="Version" align="left"></a> <a href="/LICENSE"><img src="https://img.shields.io/badge/license-0BSD-blue.svg" alt="License" align="left"></a> -<a href="https://github.com/tavianator/bfs/actions/workflows/ci.yml"><img src="https://img.shields.io/github/workflow/status/tavianator/bfs/CI?label=CI" alt="CI Status" align="right"></a> +<a href="https://github.com/tavianator/bfs/actions/workflows/ci.yml"><img src="https://github.com/tavianator/bfs/actions/workflows/ci.yml/badge.svg" alt="CI Status" align="right"></a> <a href="https://codecov.io/gh/tavianator/bfs"><img src="https://img.shields.io/codecov/c/github/tavianator/bfs?token=PpBVuozOVC" alt="Code coverage" align="right"/></a> - -***Breadth-first search for your files.*** - -[ **[Features](#features)** ]  -[ **[Installation](#installation)** ]  -[ **[Usage](/docs/USAGE.md)** ]  -[ **[Building](/docs/BUILDING.md)** ]  -[ **[Hacking](/docs/HACKING.md)** ]  -[ **[Changelog](/docs/CHANGELOG.md)** ] - -<img src="https://tavianator.github.io/bfs/animation.svg" alt="Screenshot"> +</h1> + +**[Features] • [Installation] • [Usage] • [Building] • [Contributing] • [Changelog]** + +[Features]: #features +[Installation]: #installation +[Usage]: /docs/USAGE.md +[Building]: /docs/BUILDING.md +[Contributing]: /docs/CONTRIBUTING.md +[Changelog]: /docs/CHANGELOG.md + +<picture> + <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-dark.svg"> + <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg"> + <img alt="Screencast" src="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg"> +</picture> <p></p> + </div> `bfs` is a variant of the UNIX `find` command that operates [**breadth-first**](https://en.wikipedia.org/wiki/Breadth-first_search) rather than [**depth-first**](https://en.wikipedia.org/wiki/Depth-first_search). @@ -26,12 +32,14 @@ It is otherwise compatible with many versions of `find`, including <div align="center"> -[ **[POSIX](http://pubs.opengroup.org/onlinepubs/9699919799/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)** ]  -[ **[NetBSD](https://man.netbsd.org/find.1)** ]  -[ **[macOS](https://ss64.com/osx/find.html)** ] +**[POSIX] • [GNU] • [FreeBSD] • [OpenBSD] • [NetBSD] • [macOS]** + +[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 +[NetBSD]: https://man.netbsd.org/find.1 +[macOS]: https://ss64.com/osx/find.html </div> @@ -44,8 +52,8 @@ Features <details> <summary> <code>bfs</code> operates breadth-first, which typically finds the file(s) you're looking for faster. -<p></p> </summary> +<p></p> Imagine the following directory tree: @@ -62,47 +70,57 @@ haystack </pre> `find` will explore the entire `deep` directory tree before it ever gets to the `shallow` one that contains what you're looking for. +On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree. -<pre> -$ <strong>find</strong> haystack +<table> +<tbody> +<tr><th><code>bfs</code></th><th><code>find</code></th></tr> +<tr> +<td width="506" valign="top"> + +```console +$ bfs haystack haystack haystack/deep +haystack/shallow haystack/deep/1 -haystack/deep/1/2 -haystack/deep/1/2/3 -haystack/deep/1/2/3/4 +haystack/shallow/needle ... -haystack/shallow -<strong>haystack/shallow/needle</strong> -</pre> +``` -On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree. +</td> +<td width="506" valign="top"> -<pre> -$ <strong>bfs</strong> haystack +```console +$ find haystack haystack haystack/deep -haystack/shallow haystack/deep/1 -<strong>haystack/shallow/needle</strong> haystack/deep/1/2 haystack/deep/1/2/3 haystack/deep/1/2/3/4 ... -</pre> +haystack/shallow +haystack/shallow/needle +``` + +</td> +</tr> +</tbody> +</table> </details> <details> <summary> <code>bfs</code> tries to be easier to use than <code>find</code>, while remaining compatible. -<p></p> </summary> +<p></p> For example, `bfs` is less picky about where you put its arguments: <table> <tbody> -<tr></tr> +<tr><th><code>bfs</code></th><th><code>find</code></th></tr> <tr> <td width="506"> @@ -140,33 +158,34 @@ haystack/needle <details> <summary> <code>bfs</code> gives helpful errors and warnings. -<p></p> </summary> +<p></p> For example, `bfs` will detect and suggest corrections for typos: -<pre> +```console $ bfs -nam needle -<strong>bfs: error:</strong> bfs <strong>-nam</strong> needle -<strong>bfs: error:</strong> <strong>~~~~</strong> -<strong>bfs: error:</strong> Unknown argument; did you mean <strong>-name</strong>? -</pre> +bfs: error: bfs -nam needle +bfs: error: ~~~~ +bfs: error: Unknown argument; did you mean -name? +``` `bfs` also includes a powerful static analysis to help catch mistakes: -<pre> +```console $ bfs -print -name 'needle' -<strong>bfs: warning:</strong> bfs -print <strong>-name needle</strong> -<strong>bfs: warning:</strong> <strong>~~~~~~~~~~~~</strong> -<strong>bfs: warning:</strong> The result of this expression is ignored. -</pre> +bfs: warning: bfs -print -name needle +bfs: warning: ~~~~~~~~~~~~ +bfs: warning: The result of this expression is ignored. +``` + </details> <details> <summary> <code>bfs</code> adds some options that make common tasks easier. -<p></p> </summary> +<p></p> For example, the `-exclude` operator skips over entire subtrees whenever an expression matches. `-exclude` is both more powerful and easier to use than the standard `-prune` action; compare @@ -192,45 +211,78 @@ Installation <details open> <summary> <code>bfs</code> may already be packaged for your operating system. -<p></p> </summary> +<p></p> + +<table> +<tbody> +<tr><th>Linux</th><th>macOS</th></tr> + +<tr> +<td width="506" valign="top" rowspan="3"> <pre> <strong><a href="https://pkgs.alpinelinux.org/packages?name=bfs">Alpine Linux</a></strong> # apk add bfs -<strong><a href="https://aur.archlinux.org/packages/bfs">Arch Linux</a></strong> -(Available in the AUR) +<strong><a href="https://archlinux.org/packages/extra/x86_64/bfs/">Arch Linux</a></strong> +# pacman -S bfs <strong><a href="https://packages.debian.org/sid/bfs">Debian</a>/<a href="https://packages.ubuntu.com/kinetic/bfs">Ubuntu</a></strong> # apt install bfs -<strong><a href="https://copr.fedorainfracloud.org/coprs/xfgusta/bfs/">Fedora</a></strong> -# dnf copr enable xfgusta/bfs +<strong><a href="https://src.fedoraproject.org/rpms/bfs">Fedora Linux</a></strong> # dnf install bfs +<strong><a href="https://packages.gentoo.org/packages/sys-apps/bfs">Gentoo</a></strong> +# emerge sys-apps/bfs + +<strong><a href="https://packages.guix.gnu.org/packages/bfs/">GNU Guix</a></strong> +# guix install bfs + <strong><a href="https://search.nixos.org/packages?channel=unstable&show=bfs&from=0&size=1&sort=relevance&type=packages&query=bfs">NixOS</a></strong> # nix-env -i bfs <strong><a href="https://voidlinux.org/packages/?arch=x86_64&q=bfs">Void Linux</a></strong> # xbps-install -S bfs +</pre> -<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong> -# pkg install bfs +</td> +<td width="506" valign="top"> + +<pre> +<strong><a href="https://formulae.brew.sh/formula/bfs">Homebrew</a></strong> +$ brew install bfs <strong><a href="https://ports.macports.org/port/bfs/">MacPorts</a></strong> # port install bfs +</pre> + +</td> +</tr> +<tr><th height="1">BSD</th></tr> +<tr> +<td width="506" valign="top"> + +<pre> +<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong> +# pkg install bfs -<strong><a href="https://github.com/tavianator/homebrew-tap/blob/master/Formula/bfs.rb">Homebrew</a></strong> -$ brew install tavianator/tap/bfs +<strong><a href="https://openports.pl/path/sysutils/bfs">OpenBSD</a></strong> +# pkg_add bfs </pre> + +</td> +</tr> +</tbody> +</table> </details> <details> <summary> To build <code>bfs</code> from source, you may need to install some dependencies. -<p></p> </summary> +<p></p> The only absolute requirements for building `bfs` are a C compiler, [GNU make](https://www.gnu.org/software/make/), and [Bash](https://www.gnu.org/software/bash/). These are installed by default on many systems, and easy to install on most others. @@ -241,31 +293,31 @@ Here's how to install them on some common platforms: <pre> <strong>Alpine Linux</strong> -# apk add acl{,-dev} attr{,-dev} libcap{,-dev} oniguruma-dev +# apk add acl{,-dev} attr libcap{,-dev} liburing-dev oniguruma-dev <strong>Arch Linux</strong> -# pacman -S acl attr libcap oniguruma +# pacman -S acl attr libcap liburing oniguruma <strong>Debian/Ubuntu</strong> -# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev libonig-dev +# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev liburing-dev libonig-dev <strong>Fedora</strong> -# dnf install libacl-devel libattr-devel libcap-devel oniguruma-devel +# dnf install acl libacl-devel attr libcap-devel liburing-devel oniguruma-devel <strong>NixOS</strong> -# nix-env -i acl attr libcap oniguruma +# nix-env -i acl attr libcap liburing oniguruma <strong>Void Linux</strong> -# xbps-install -S acl-{devel,progs} attr-{devel,progs} libcap-{devel,progs} oniguruma-devel +# xbps-install -S acl-{devel,progs} attr-progs libcap-{devel,progs} liburing-devel oniguruma-devel -<strong>FreeBSD</strong> -# pkg install oniguruma +<strong>Homebrew</strong> +$ brew install oniguruma <strong>MacPorts</strong> # port install oniguruma6 -<strong>Homebrew</strong> -$ brew install oniguruma +<strong>FreeBSD</strong> +# pkg install oniguruma </pre> These dependencies are technically optional, though strongly recommended. @@ -275,12 +327,13 @@ See the [build documentation](/docs/BUILDING.md#dependencies) for how to disable <details> <summary> Once you have the dependencies, you can build <code>bfs</code>. -<p></p> </summary> +<p></p> 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. @@ -290,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/.gitignore b/bench/.gitignore new file mode 100644 index 0000000..170d850 --- /dev/null +++ b/bench/.gitignore @@ -0,0 +1,3 @@ +/corpus/ +/results/ +/worktree/ diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..56157a0 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,51 @@ +This directory contains a suite of benchmarks used to evaluate `bfs` and detect performance regressions. +To run them, you'll need the [tailfin] benchmark harness. +You can read the full usage information with + +[tailfin]: https://github.com/tavianator/tailfin + +```console +$ tailfin -n run bench/bench.sh --help +Usage: tailfin run bench/bench.sh [--default] + [--complete] [--early-quit] [--print] [--strategies] + [--build=...] [--bfs] [--find] [--fd] + [--no-clean] [--help] +... +``` + +The benchmarks use various git repositories to have a realistic and reproducible directory structure as a corpus. +Currently, those are the [Linux], [Rust], and [Chromium] repos. +The scripts will automatically clone those repos using [partial clone] filters to avoid downloading the actual file contents, saving bandwidth and space. + +[Linux]: https://github.com/torvalds/linux.git +[Rust]: https://github.com/rust-lang/rust.git +[Chromium]: https://chromium.googlesource.com/chromium/src.git +[partial clone]: https://git-scm.com/docs/partial-clone + +You can try out a quick benchmark by running + +```console +$ tailfin run bench/bench.sh --build=main --complete=linux +``` + +This will build the `main` branch, and measure the complete traversal of the Linux repo. +Results will be both printed to the console and saved in a Markdown file, which you can find by running + +```console +$ tailfin latest +results/2023/09/29/15:32:49 +$ cat results/2023/09/29/15:32:49/runs/1/bench.md +## Complete traversal +... +``` + +To measure performance improvements/regressions of a change, compare the `main` branch to the topic branch on the full benchmark suite: + +```console +$ tailfin run bench/bench.sh --build=main --build=branch --default +``` + +This will take a few minutes. +Results from the full benchmark suite can be seen in performance-related pull requests, for example [#126]. + +[#126]: https://github.com/tavianator/bfs/pull/126 diff --git a/bench/bench.sh b/bench/bench.sh new file mode 100644 index 0000000..c9ed978 --- /dev/null +++ b/bench/bench.sh @@ -0,0 +1,749 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +declare -gA URLS=( + [chromium]="https://chromium.googlesource.com/chromium/src.git" + [linux]="https://github.com/torvalds/linux.git" + [rust]="https://github.com/rust-lang/rust.git" +) + +declare -gA TAGS=( + [chromium]=119.0.6036.2 + [linux]=v6.5 + [rust]=1.72.1 +) + +COMPLETE_DEFAULT=(linux rust chromium) +EARLY_QUIT_DEFAULT=(chromium) +STAT_DEFAULT=(rust) +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]}" + printf ' [--default] [--<BENCHMARK> [--<BENCHMARK>...]]\n' + printf ' [--build=...] [--bfs] [--find] [--fd]\n' + printf ' [--no-clean] [--help]\n\n' + + printf ' --default\n' + printf ' Run the default set of benchmarks\n\n' + + printf ' --complete[=CORPUS]\n' + printf ' Complete traversal benchmark.\n' + printf ' Default corpus is --complete="%s"\n\n' "${COMPLETE_DEFAULT[*]}" + + printf ' --early-quit[=CORPUS]\n' + printf ' Early quitting benchmark.\n' + printf ' Default corpus is --early-quit=%s\n\n' "${EARLY_QUIT_DEFAULT[*]}" + + printf ' --stat[=CORPUS]\n' + printf ' Traversal with stat().\n' + printf ' Default corpus is --stat=%s\n\n' "${STAT_DEFAULT[*]}" + + printf ' --print[=CORPUS]\n' + printf ' Path printing benchmark.\n' + printf ' Default corpus is --print=%s\n\n' "${PRINT_DEFAULT[*]}" + + printf ' --strategies[=CORPUS]\n' + printf ' Search strategy benchmark.\n' + printf ' Default corpus is --strategies=%s\n\n' "${STRATEGIES_DEFAULT[*]}" + + printf ' --jobs[=CORPUS]\n' + printf ' Parallelism benchmark.\n' + printf ' Default corpus is --jobs=%s\n\n' "${JOBS_DEFAULT[*]}" + + printf ' --exec[=CORPUS]\n' + 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' + + printf ' --bfs[=COMMAND]\n' + printf ' Benchmark an existing build of bfs\n\n' + + printf ' --find[=COMMAND]\n' + printf ' Compare against find\n\n' + + printf ' --fd[=COMMAND]\n' + printf ' Compare against fd\n\n' + + printf ' --no-clean\n' + printf ' Use any existing corpora as-is\n\n' + + printf ' --help\n' + printf ' This message\n\n' +} + +# Hack to export an array +export_array() { + local str=$(declare -p "$1" | sed 's/ -a / -ga /') + unset "$1" + export "$1=$str" +} + +# Hack to import an array +import_array() { + local cmd="${!1}" + unset "$1" + eval "$cmd" +} + +# Set up the benchmarks +setup() { + ROOT=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")/..") + if ! [ "$PWD" -ef "$ROOT" ]; then + printf 'error: Please run this script from %s\n\n' "$ROOT" >&2 + usage >&2 + exit $EX_USAGE + fi + + nproc=$(nproc) + + # Options + + CLEAN=1 + + BUILD=() + BFS=() + FIND=() + FD=() + + COMPLETE=() + EARLY_QUIT=() + STAT=() + PRINT=() + STRATEGIES=() + JOBS=() + EXEC=() + SORTED=() + + for arg; do + case "$arg" in + # Flags + --no-clean) + CLEAN=0 + ;; + # bfs commits/tags to benchmark + --build=*) + BUILD+=("${arg#*=}") + BFS+=("bfs-${arg#*=}") + ;; + # Utilities to benchmark against + --bfs) + BFS+=(bfs) + ;; + --bfs=*) + BFS+=("${arg#*=}") + ;; + --find) + FIND+=(find) + ;; + --find=*) + FIND+=("${arg#*=}") + ;; + --fd) + FD+=(fd) + ;; + --fd=*) + FD+=("${arg#*=}") + ;; + # Benchmark groups + --complete) + COMPLETE=("${COMPLETE_DEFAULT[@]}") + ;; + --complete=*) + read -ra COMPLETE <<<"${arg#*=}" + ;; + --early-quit) + EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}") + ;; + --early-quit=*) + read -ra EARLY_QUIT <<<"${arg#*=}" + ;; + --stat) + STAT=("${STAT_DEFAULT[@]}") + ;; + --stat=*) + read -ra STAT <<<"${arg#*=}" + ;; + --print) + PRINT=("${PRINT_DEFAULT[@]}") + ;; + --print=*) + read -ra PRINT <<<"${arg#*=}" + ;; + --strategies) + STRATEGIES=("${STRATEGIES_DEFAULT[@]}") + ;; + --strategies=*) + read -ra STRATEGIES <<<"${arg#*=}" + ;; + --jobs) + JOBS=("${JOBS_DEFAULT[@]}") + ;; + --jobs=*) + read -ra JOBS <<<"${arg#*=}" + ;; + --exec) + EXEC=("${EXEC_DEFAULT[@]}") + ;; + --exec=*) + read -ra EXEC <<<"${arg#*=}" + ;; + --sorted) + SORTED=("${SORTED_DEFAULT[@]}") + ;; + --sorted=*) + read -ra SORTED <<<"${arg#*=}" + ;; + --default) + COMPLETE=("${COMPLETE_DEFAULT[@]}") + EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}") + STAT=("${STAT_DEFAULT[@]}") + PRINT=("${PRINT_DEFAULT[@]}") + STRATEGIES=("${STRATEGIES_DEFAULT[@]}") + JOBS=("${JOBS_DEFAULT[@]}") + EXEC=("${EXEC_DEFAULT[@]}") + SORTED=("${SORTED_DEFAULT[@]}") + ;; + --help) + usage + exit + ;; + *) + printf 'error: Unknown option %q\n\n' "$arg" >&2 + usage >&2 + exit $EX_USAGE + ;; + esac + done + + if ((UID == 0)); then + max-freq + fi + + echo "Building bfs ..." + 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[@]}" "${SORTED[@]}"; do + if ((cloned["$corpus"])); then + continue + fi + cloned["$corpus"]=1 + + dir="bench/corpus/$corpus" + if ((CLEAN)) || ! [ -e "$dir" ]; then + as-user ./bench/clone-tree.sh "${URLS[$corpus]}" "${TAGS[$corpus]}" "$dir"{,.git} + fi + done + + if ((${#BUILD[@]} > 0)); then + echo "Creating bfs worktree ..." + + worktree="bench/worktree" + as-user git worktree add -qd "$worktree" + defer as-user git worktree remove "$worktree" + + bin="$(realpath -- "$SETUP_DIR")/bin" + as-user mkdir "$bin" + + for commit in "${BUILD[@]}"; do + ( + echo "Building bfs $commit ..." + cd "$worktree" + as-user git checkout -qd "$commit" -- + 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 + as-user cp ./bfs "$bin/bfs-$commit" + fi + as-user make -s clean + ) + done + + export PATH="$bin:$PATH" + fi + + export_array BFS + export_array FIND + export_array FD + + export_array COMPLETE + export_array EARLY_QUIT + export_array STAT + export_array PRINT + export_array STRATEGIES + export_array JOBS + export_array EXEC + export_array SORTED + + if ((UID == 0)); then + turbo-off + fi + + sync +} + +# Runs hyperfine and saves the output +do-hyperfine() { + local tmp_md="$BENCH_DIR/.bench.md" + local md="$BENCH_DIR/bench.md" + local tmp_json="$BENCH_DIR/.bench.json" + local json="$BENCH_DIR/bench.json" + + if (($# == 0)); then + printf 'Nothing to do\n\n' | tee -a "$md" + return 1 + fi + + hyperfine -w2 -M20 --export-markdown="$tmp_md" --export-json="$tmp_json" "$@" &>/dev/tty + cat "$tmp_md" >>"$md" + cat "$tmp_json" >>"$json" + rm "$tmp_md" "$tmp_json" + + printf '\n' | tee -a "$md" +} + +# Print the header for a benchmark group +group() { + printf "## $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md" +} + +# Print the header for a benchmark subgroup +subgroup() { + printf "### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md" +} + +# Print the header for a benchmark sub-subgroup +subsubgroup() { + printf "#### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md" +} + +# Benchmark the complete traversal of a directory tree +# (without printing anything) +bench-complete-corpus() { + total=$(./bin/bfs "$2" -printf '.' | wc -c) + + subgroup "%s (%'d files)" "$1" "$total" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs $2 -false") + done + + for find in "${FIND[@]}"; do + cmds+=("$find $2 -false") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u '^$' $2") + done + + do-hyperfine "${cmds[@]}" +} + +# All complete traversal benchmarks +bench-complete() { + if (($#)); then + group "Complete traversal" + + for corpus; do + bench-complete-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# 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) + + subgroup '%s (depth %d)' "$1" "$max_depth" + + # Save the list of unique filenames, along with their depth + UNIQ="$BENCH_DIR/uniq" + ./bin/bfs "$dir" -printf '%d %f\n' | sort -k2 | uniq -uf1 >"$UNIQ" + + for ((i = 2; i <= max_depth; i *= 2)); do + subsubgroup 'Depth %d' "$i" + + # Sample random uniquely-named files at depth $i + export FILES="$BENCH_DIR/uniq-$i" + sed -n "s/^$i //p" "$UNIQ" | shuf -n20 >"$FILES" + if ! [ -s "$FILES" ]; then + continue + fi + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs $dir -name \$(shuf -n1 \$FILES) -print -quit") + done + + for find in "${FIND[@]}"; do + cmds+=("$find $dir -name \$(shuf -n1 \$FILES) -print -quit") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -usg1 \$(shuf -n1 \$FILES) $dir") + done + + do-hyperfine "${cmds[@]}" + done +} + +# All early-quitting benchmarks +bench-early-quit() { + if (($#)); then + group "Early termination" + + for corpus; do + bench-early-quit-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# Benchmark traversal with stat() +bench-stat-corpus() { + total=$(./bin/bfs "$2" -printf '.' | wc -c) + + subgroup "%s (%'d files)" "$1" "$total" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs $2 -size 1024G") + done + + for find in "${FIND[@]}"; do + cmds+=("$find $2 -size 1024G") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u --search-path $2 --size 1024Gi") + done + + do-hyperfine "${cmds[@]}" +} + +# stat() benchmarks +bench-stat() { + if (($#)); then + group "Traversal with stat()" + + for corpus; do + bench-stat-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# Benchmark printing paths without colors +bench-print-nocolor() { + subsubgroup '%s' "$1" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs $2") + done + + for find in "${FIND[@]}"; do + cmds+=("$find $2") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u --search-path $2") + done + + do-hyperfine "${cmds[@]}" +} + +# Benchmark printing paths with colors +bench-print-color() { + subsubgroup '%s' "$1" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs $2 -color") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u --search-path $2 --color=always") + done + + do-hyperfine "${cmds[@]}" +} + +# All printing benchmarks +bench-print() { + if (($#)); then + group "Printing paths" + + subgroup "Without colors" + for corpus; do + bench-print-nocolor "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + + subgroup "With colors" + for corpus; do + bench-print-color "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# Benchmark search strategies +bench-strategies-corpus() { + subgroup '%s' "$1" + + if ((${#BFS[@]} == 1)); then + cmds=("$BFS -S "{bfs,dfs,ids,eds}" $2 -false") + do-hyperfine "${cmds[@]}" + else + for S in bfs dfs ids eds; do + subsubgroup '`-S %s`' "$S" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs -S $S $2 -false") + done + do-hyperfine "${cmds[@]}" + done + fi +} + +# All search strategy benchmarks +bench-strategies() { + if (($#)); then + group "Search strategies" + + for corpus; do + bench-strategies-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# Benchmark parallelism +bench-jobs-corpus() { + subgroup '%s' "$1" + + if ((${#BFS[@]} + ${#FD[@]} == 1)); then + cmds=() + for bfs in "${BFS[@]}"; do + if "$bfs" -j1 -quit &>/dev/null; then + cmds+=("$bfs -j"{1,2,3,4,6,8,12,16}" $2 -false") + else + cmds+=("$bfs $2 -false") + fi + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -j"{1,2,3,4,6,8,12,16}" -u '^$' $2") + done + + do-hyperfine "${cmds[@]}" + else + for j in 1 2 3 4 6 8 12 16; do + subsubgroup '`-j%d`' $j + + cmds=() + for bfs in "${BFS[@]}"; do + if "$bfs" -j1 -quit &>/dev/null; then + cmds+=("$bfs -j$j $2 -false") + elif ((j == 1)); then + cmds+=("$bfs $2 -false") + fi + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -j$j -u '^$' $2") + done + + if ((${#cmds[@]})); then + do-hyperfine "${cmds[@]}" + fi + done + fi +} + +# All parallelism benchmarks +bench-jobs() { + if (($#)); then + group "Parallelism" + + for corpus; do + bench-jobs-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + +# One file/process +bench-exec-single() { + subsubgroup "One file per process" + + cmds=() + for cmd in "${BFS[@]}" "${FIND[@]}"; do + cmds+=("$cmd $1 -maxdepth 2 -exec true -- {} \;") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u --search-path $1 --max-depth=2 -x true --") + # Without -j1, fd runs multiple processes in parallel, which is unfair + cmds+=("$fd -j1 -u --search-path $1 --max-depth=2 -x true --") + done + + do-hyperfine "${cmds[@]}" +} + +# Many files/process +bench-exec-multi() { + subsubgroup "Many files per process" + + cmds=() + for cmd in "${BFS[@]}" "${FIND[@]}"; do + cmds+=("$cmd $1 -exec true -- {} +") + done + + for fd in "${FD[@]}"; do + cmds+=("$fd -u --search-path $1 -X true --") + done + + do-hyperfine "${cmds[@]}" +} + +# Many files, same dir +bench-exec-chdir() { + if ((${#BFS[@]} + ${#FIND[@]} == 0)); then + return + fi + + subsubgroup "Spawn in parent directory" + + cmds=() + for cmd in "${BFS[@]}" "${FIND[@]}"; do + cmds+=("$cmd $1 -maxdepth 3 -execdir true -- {} +") + done + + do-hyperfine "${cmds[@]}" +} + +# Benchmark process spawning +bench-exec-corpus() { + subgroup '%s' "$1" + + bench-exec-single "$2" + bench-exec-multi "$2" + bench-exec-chdir "$2" +} + +# All process spawning benchmarks +bench-exec() { + if (($#)); then + group "Process spawning" + + for corpus; do + bench-exec-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + 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" + + local md="$BENCH_DIR/bench.md" + + printf '```console\n' >>"$md" + + { + for bfs in "${BFS[@]}"; do + printf '$ %s --version | head -n1\n' "$bfs" + "$bfs" --version | head -n1 + done + + for find in "${FIND[@]}"; do + printf '$ %s --version | head -n1\n' "$find" + "$find" --version | head -n1 + done + + for fd in "${FD[@]}"; do + printf '$ %s --version\n' "$fd" + "$fd" --version + done + } | tee -a "$md" + + printf '```' >>"$md" +} + +# Print benchmark details +bench-details() { + group "Details" + + bench-versions +} + +# Run all the benchmarks +bench() { + import_array BFS + import_array FIND + import_array FD + + import_array COMPLETE + import_array EARLY_QUIT + import_array STAT + import_array PRINT + import_array STRATEGIES + import_array JOBS + import_array EXEC + import_array SORTED + + bench-complete "${COMPLETE[@]}" + bench-early-quit "${EARLY_QUIT[@]}" + bench-stat "${STAT[@]}" + bench-print "${PRINT[@]}" + bench-strategies "${STRATEGIES[@]}" + bench-jobs "${JOBS[@]}" + bench-exec "${EXEC[@]}" + bench-sorted "${SORTED[@]}" + bench-details +} diff --git a/bench/clone-tree.sh b/bench/clone-tree.sh new file mode 100755 index 0000000..744b5f4 --- /dev/null +++ b/bench/clone-tree.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Creates a directory tree that matches a git repo, but with empty files. E.g. +# +# $ ./bench/clone-tree.sh "https://.../linux.git" v6.5 ./linux ./linux.git +# +# will create or update a shallow clone at ./linux.git, then create a directory +# tree at ./linux with the same directory tree as the tag v6.5, except all files +# will be empty. + +set -eu + +if (($# != 4)); then + printf 'Usage: %s https://url/of/repo.git <TAG> path/to/checkout path/to/repo.git\n' "$0" >&2 + exit 1 +fi + +URL="$1" +TAG="$2" +DIR="$3" +REPO="$4" + +BENCH=$(dirname -- "${BASH_SOURCE[0]}") +BIN=$(realpath -- "$BENCH/../bin") +BFS="$BIN/bfs" +XTOUCH="$BIN/tests/xtouch" + +if [ "${NPROC-}" ]; then + # Use fewer cores in recursive calls + export NPROC=$(((NPROC + 1) / 2)) +else + export NPROC=$(nproc) +fi + +JOBS=$((NPROC < 8 ? NPROC : 8)) + +do-git() { + git -C "$REPO" "$@" +} + +if ! [ -e "$REPO" ]; then + mkdir -p -- "$REPO" + do-git init -q --bare +fi + +has-ref() { + do-git rev-list --quiet -1 --missing=allow-promisor "$1" &>/dev/null +} + +sparse-fetch() { + do-git -c fetch.negotiationAlgorithm=noop fetch -q --filter=blob:none --depth=1 --no-tags --no-write-fetch-head --no-auto-gc "$@" +} + +if ! has-ref "$TAG"; then + printf 'Fetching %s ...\n' "$TAG" >&2 + do-git config remote.origin.url "$URL" + if ((${#TAG} >= 40)); then + sparse-fetch origin "$TAG" + else + sparse-fetch origin tag "$TAG" + fi +fi + +# Delete a tree in parallel +clean() { + local d=5 + "$BFS" -f "$1" -mindepth $d -maxdepth $d -type d -print0 \ + | xargs -0r -n1 -P$JOBS -- "$BFS" -j1 -mindepth 1 -delete -f + "$BFS" -f "$1" -delete +} + +if [ -e "$DIR" ]; then + printf 'Cleaning old directory tree %s ...\n' "$DIR" >&2 + TMP=$(mktemp -dp "$(dirname -- "$DIR")") + mv -- "$DIR" "$TMP" + clean "$TMP" & +fi + +# List gitlinks (submodule references) in the tree +ls-gitlinks() { + do-git ls-tree -zr "$TAG" \ + | sed -zn 's/.* commit //p' +} + +# Get the submodule ID for a path +submodule-for-path() { + do-git config --blob "$TAG:.gitmodules" \ + --name-only \ + --fixed-value \ + --get-regexp 'submodule\..**\.path' "$1" \ + | sed -En 's/submodule\.(.*)\.path/\1/p' +} + +# Get the URL for a submodule +submodule-url() { + # - https://chrome-internal.googlesource.com/ + # - not publicly accessible + # - https://chromium.googlesource.com/external/github.com/WebKit/webkit.git + # - is accessible, but the commit (59e9de61b7b3) isn't + # - https://android.googlesource.com/ + # - is accessible, but you need an account + + do-git config --blob "$TAG:.gitmodules" \ + --get "submodule.$1.url" \ + | sed -E \ + -e '\|^https://chrome-internal.googlesource.com/|Q1' \ + -e '\|^https://chromium.googlesource.com/external/github.com/WebKit/webkit.git|Q1' \ + -e '\|^https://android.googlesource.com/|Q1' +} + +# Recursively checkout submodules +while read -rd '' SUBREF SUBDIR; do + SUBNAME=$(submodule-for-path "$SUBDIR") + SUBURL=$(submodule-url "$SUBNAME") || continue + + if (($(jobs -pr | wc -w) >= JOBS)); then + wait -n + fi + "$0" "$SUBURL" "$SUBREF" "$DIR/$SUBDIR" "$REPO/modules/$SUBNAME" & +done < <(ls-gitlinks) + +# Touch files in parallel +xtouch() ( + cd "$DIR" + if ((JOBS > 1)); then + xargs -0r -n4096 -P$JOBS -- "$XTOUCH" -p -- + else + xargs -0r -- "$XTOUCH" -p -- + fi +) + +# Check out files +printf 'Checking out %s ...\n' "$DIR" >&2 +mkdir -p -- "$DIR" +do-git ls-tree -zr "$TAG"\ + | sed -zn 's/.* blob .*\t//p' \ + | xtouch + +# Wait for cleaning/submodules +wait 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 f734ab1..0dd39f4 100644 --- a/completions/bfs.bash +++ b/completions/bfs.bash @@ -1,21 +1,8 @@ -# bash completion script for bfs +# Copyright © Benjamin Mundt <benMundt@ibm.com> +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD -############################################################################ -# bfs # -# Copyright (C) 2020 Benjamin Mundt <benMundt@ibm.com> # -# Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> # -# # -# Permission to use, copy, modify, and/or distribute this software for any # -# purpose with or without fee is hereby granted. # -# # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -############################################################################ +# bash completion script for bfs _bfs() { local cur prev words cword @@ -31,6 +18,7 @@ _bfs() { -fstype -gid -group + -j -ok -okdir -regextype @@ -44,12 +32,14 @@ _bfs() { # (e.g. because they are numeric, glob, regexp, time, etc.) local nocomp=( -{a,B,c,m}{min,since,time} + -context -ilname -iname -inum -ipath -iregex -iwholename + -limit -links -lname -maxdepth @@ -107,10 +97,9 @@ _bfs() { -depth -follow -ignore_readdir_race - -maxdepth - -mindepth -mount -nocolor + -noerror -noignore_readdir_race -noleaf -nowarn diff --git a/completions/bfs.fish b/completions/bfs.fish index 3f399e7..7182bee 100644 --- a/completions/bfs.fish +++ b/completions/bfs.fish @@ -20,6 +20,7 @@ complete -c bfs -o f -d "Treat specified path as a path to search" -a "(__fish_c complete -c bfs -o D -d "Turn on a debugging flag" -a $debug_flag_comp -x complete -c bfs -s O -d "Enable specified optimization level" -a $optimization_comp -x complete -c bfs -o S -d "Choose the search strategy" -a $strategy_comp -x +complete -c bfs -s j -d "Use this many threads" -x # Operators @@ -41,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 @@ -70,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" @@ -133,6 +136,7 @@ complete -c bfs -o fls -d "Like -ls, but write to specified file" -F complete -c bfs -o fprint -d "Like -print, but write to specified file" -F complete -c bfs -o fprint0 -d "Like -print0, but write to specified file" -F complete -c bfs -o fprintf -d "Like -printf, but write to specified file" -F +complete -c bfs -o limit -d "Limit the number of results" -x complete -c bfs -o ls -d "List files like ls -dils" complete -c bfs -o print -d "Print the path to the found file" complete -c bfs -o print0 -d "Like -print, but use the null character as a separator rather than newlines" diff --git a/completions/bfs.zsh b/completions/bfs.zsh index 3d7dc3a..6b46f83 100644 --- a/completions/bfs.zsh +++ b/completions/bfs.zsh @@ -10,7 +10,7 @@ args=( '-D[print diagnostics]:debug option:(cost exec opt rates search stat time tree all help)' '-E[use extended regular expressions with -regex/-iregex]' '-f[specify file hierarchy to traverse]:path:_directories' - '-O+[enable query optimisation]:level:(1 2 3)' + '-O+[enable query optimisation]:level:(0 1 2 3 4 fast)' '-s[traverse directories in sorted order]' '-X[warn if filename contains characters special to xargs]' "-x[don't span filesystems]" @@ -19,6 +19,7 @@ args=( '(-L -P)-H[only follow symlinks when resolving command-line arguments]' "-S[select search method]:value:(bfs dfs ids eds)" '-f[treat path as path to search]:path:_files -/' + '-j+[use this many threads]:threads:' # Operators '*-and' @@ -40,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)' @@ -73,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]' @@ -81,18 +84,18 @@ args=( '*-false[always false]' '*-true[always true]' '*-fstype[find files on file systems with the given type]:file system type:_file_systems' - + '*-gid[find files owned by group ID N]:numeric group ID:' '*-group[find files owned by group NAME]:group:_groups' '*-uid[find files owned by user ID N]:numeric user ID' '*-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:' @@ -117,7 +120,7 @@ args=( '*-xattr[find files with extended attributes]' '*-xattrname[find files with extended attribute NAME]:name:' '*-xtype[find files of the given type following links when -type would not, and vice versa]:file type:((b\:block\ device c\:character\ device d\:directory p\:named\ pipe f\:normal\ file l\:symbolic\ link s\:socket w\:whiteout D\:Door))' - + # Actions '*-delete[delete any found files (-implies -depth)]' '*-rm[delete any found files (-implies -depth)]' @@ -126,13 +129,14 @@ args=( '*-execdir[execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal' '*-ok[prompt the user whether to execute a command]:program: _command_names -e:*(\;|+)::program arguments: _normal' '*-okdir[prompt the user whether to execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal' - + '-exit[exit with status if found, default 0]' '*-fls[list files like ls -dils, but write to FILE instead of standard output]:output file:_files' '*-fprint[print the path to the found file, but write to FILE instead of standard output]:output file:_files' '*-fprint0[print the path to the found file using null character as separator, but write to FILE instead of standard output]:output file:_files' '*-fprintf[print according to format string, but write to FILE instead of standard output]:output file:_files:output format' + '*-limit[quit after N results]:maximum result count' '*-ls[list files like ls -dils]' '*-print[print the path to the found file]' '*-print0[print the path to the found file using null character as separator]' 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 932845b..69a997c 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,103 +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 [`Makefile`](/Makefile) 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 | -| `WITH_ACL`<br>`WITH_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 WITH_ONIGURUMA=`). +External dependencies are auto-detected by default, but you can build `--with` or `--without` them explicitly: -| Dependency | Platforms | `make` flag | -|-------------|------------|------------------| -| [acl] | Linux only | `WITH_ACL` | -| [attr] | Linux only | `WITH_ATTR` | -| [libcap] | Linux only | `WITH_LIBCAP` | -| [Oniguruma] | All | `WITH_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/ -[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 [`Makefile`](/Makefile)). -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 @@ -107,27 +161,28 @@ Testing $ make check -Most of the testsuite is implemented in the file [`tests.sh`](/tests.sh). -This script contains hundreds of separate test cases. -Most of them are *snapshot tests* which compare `bfs`'s output to a known-good copy saved under [`tests`](/tests). +The test harness is implemented in the file [`tests/tests.sh`](/tests/tests.sh). +Individual test cases are found in `tests/*/*.sh`. +Most of them are *snapshot tests* which compare `bfs`'s output to a known-good copy saved under the matching `tests/*/*.out`. You can pass the name of a particular test case (or a few) to run just those tests. For example: - $ ./tests/tests.sh test_basic + $ ./tests/tests.sh posix/basic If you need to update the reference snapshot, pass `--update`. It can be handy to generate the snapshot with a different `find` implementation to ensure the output is correct, for example: - $ ./tests/tests.sh test_basic --bfs=find --update + $ ./tests/tests.sh posix/basic --bfs=find --update 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: 89 - tests failed: 5 + [PASS] 104 / 119 + [SKIP] 1 / 119 + [FAIL] 14 / 119 Run diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9d02e8e..56f53b4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,547 @@ +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 +----- + +**March 6, 2024** + +### Bug fixes + +- On Linux, the `io_uring` feature probing introduced in `bfs` 3.1.2 only applied to one thread, causing all other threads to avoid using io_uring entirely. + The probe results are now copied to all threads correctly. + ([`f64f76b`](https://github.com/tavianator/bfs/commit/f64f76b55400b71e8576ed7e4a377eb5ef9576aa)) + + +3.1.2 +----- + +**February 29, 2024** + +### Bug fixes + +- On Linux, we now check for supported `io_uring` operations before using them, which should fix `bfs` on 5.X series kernels that support `io_uring` but not all of `openat()`/`close()`/`statx()` ([`8bc72d6`](https://github.com/tavianator/bfs/commit/8bc72d6c20c5e38783c4956c4d9fde9b3ee9140c)) + +- Fixed a test failure triggered by certain filesystem types for `/tmp` ([#131](https://github.com/tavianator/bfs/issues/131)) + +- Fixed parsing and interpretation of timezone offsets for explicit reference times used in `-*since` and `-newerXt` ([`a9f3cde`](https://github.com/tavianator/bfs/commit/a9f3cde30426b546ba6e3172e1a7951213a72049)) + +- Fixed the build on m68k ([`c749c11`](https://github.com/tavianator/bfs/commit/c749c11b04444ca40941dd2ddc5802faed148f6a)) + + +3.1.1 +----- + +**February 16, 2024** + +### Changes + +- Performance and scalability improvements + +- The file count in `bfs -status` now has a thousands separator + + +3.1 +--- + +**February 6, 2024** + +### New features + +- On Linux, `bfs` now uses [io_uring](https://en.wikipedia.org/wiki/Io_uring) for async I/O + +- On all platforms, `bfs` can now perform `stat()` calls in parallel, accelerating queries like `-links`, `-newer`, and `-size`, as well as colorized output + +- On FreeBSD, `-type w` now works to find whiteouts like the system `find` + +### Changes + +- Improved `bfs -j2` performance ([`b2ab7a1`](https://github.com/tavianator/bfs/commit/b2ab7a151fca517f4879e76e626ec85ad3de97c7)) + +- Optimized `-exec` by using `posix_spawn()` when possible, which can avoid the overhead of `fork()` ([`95fbde1`](https://github.com/tavianator/bfs/commit/95fbde17a66377b6fbe7ff1f014301dbbf09270d)) + +- `-execdir` and `-okdir` are now rejected if `$PATH` contains a relative path, matching the behaviour of GNU find ([`163baf1`](https://github.com/tavianator/bfs/commit/163baf1c9af13be0ce705b133e41e0c3d6427398)) + +- Leading whitespace is no longer accepted in integer command line arguments like `-links ' 1'` ([`e0d7dc5`](https://github.com/tavianator/bfs/commit/e0d7dc5dfd7bdaa62b6bc18e9c1cce00bbe08577)) + +### Bug fixes + +- `-quit` and `-exit` could be ignored in the iterative deepening modes (`-S {ids,eds}`). + This is now fixed ([`670ebd9`](https://github.com/tavianator/bfs/commit/670ebd97fb431e830b1500b2e7e8013b121fb2c5)). + The bug was introduced in version 3.0.3 (commit [`5f16169`]). + +- Fixed two possible errors in sort mode (`-s`): + - Too many open files ([`710c083`](https://github.com/tavianator/bfs/commit/710c083ff02eb1cc5b8daa6778784f3d1cd3c08d)) + - Out of memory ([`76ffc8d`](https://github.com/tavianator/bfs/commit/76ffc8d30cb1160d55d855d8ac630a2b9075fbcf)) + +- Fixed handling of FreeBSD union mounts ([`3ac3bee`](https://github.com/tavianator/bfs/commit/3ac3bee7b0d9c9be693415206efa664bf4a7d4a7)) + +- Fixed `NO_COLOR` handling when it's set to the empty string ([`79aee58`](https://github.com/tavianator/bfs/commit/79aee58a4621d01c4b1e98c332775f3b87213ddb)) + +- Fixed some portability issues: + - [OpenBSD](https://github.com/tavianator/bfs/compare/ee200c07643801c8b53e5b80df704ecbf77a884e...79f1521b0e628be72bed3a648f0ae90b62fc69b8) + - [NetBSD](https://github.com/tavianator/bfs/compare/683f2c41c72efcb82ce866e3dcc311ac9bd8b66d...6435684a7d515e18247ae1b3dd9ec8681fee22d0) + - [DragonFly BSD](https://github.com/tavianator/bfs/compare/08867473e75e8e20ca76c7fb181204839e28b271...45fb1d952c3b262278a3b22e9c7d60cca19a5407) + - [Illumos](https://github.com/tavianator/bfs/compare/4010140cb748cc4f7f57b0a3d514485796c665ce...ae94cdc00136685abe61d55e1e357caaa636d785) + + +3.0.4 +----- + +**October 12, 2023** + +### Bug fixes + +- Fixed a segfault when reporting errors under musl ([`d40eb87`]) + +[`d40eb87`]: https://github.com/tavianator/bfs/commit/d40eb87cc00f50a5debb8899eacb7fcf1065badf + + +3.0.3 +----- + +**October 12, 2023** + +### Changes + +- Iterative deepening modes (`-S {ids,eds}`) were optimized by delaying teardown until the very end ([`5f16169`]) + +- Parallel depth-first search (`-S dfs`) was optimized to avoid enqueueing every file separately ([`2572273`]) + +### Bug fixes + +- Iterative deepening modes (`-S {ids,eds}`) were performing iterative *breadth*-first searches since `bfs` 3.0, negating any advantages they may have had over normal breadth-first search. + They now do iterative *depth*-first searches as expected. + ([`a029d95`]) + +- Fixed a linked-list corruption that could lead to an infinite loop on macOS and other non-Linux, non-FreeBSD platforms ([`773f4a4`]) + +[`5f16169`]: https://github.com/tavianator/bfs/commit/5f1616912ba3a7a23ce6bce02df3791b73da38ab +[`2572273`]: https://github.com/tavianator/bfs/commit/257227326fe60fe70e80433fd34d1ebcb2f9f623 +[`a029d95`]: https://github.com/tavianator/bfs/commit/a029d95b5736a74879f32089514a5a6b63d6efbc +[`773f4a4`]: https://github.com/tavianator/bfs/commit/773f4a446f03da62d88e6d17be49fdc0a3e38465 + + +3.0.2 +----- + +**September 6, 2023** + +### Changes + +- `-files0-from` now allows an empty set of paths to be given, matching GNU findutils 4.9.0 + +- Reduced memory consumption in multi-threaded searches + +- Many man page updates + +### Bug fixes + +- Fixed an out-of-bounds memory read that could occur when escaping a string containing an incomplete multi-byte character + + +3.0.1 +----- + +**July 18, 2023** + +### Bug fixes + +- Traversal fixes that mostly affect large directory trees ([#107]) + + - `bfs` could encounter `EMFILE`, close a file, and retry many times, particularly with `-j1` + + - Breadth-first search could become highly unbalanced, negating many of the benefits of `bfs` + + - On non-{Linux,FreeBSD} platforms, directories could stay open longer than necessary, consuming extra memory + +[#107]: https://github.com/tavianator/bfs/pull/107 + + +3.0 +--- + +**July 13, 2023** + +### New features + +- `bfs` now reads directories asynchronously and in parallel ([#101]). + Performance is significantly improved as a result. + Parallelism is controlled by the new `-j` flag, e.g. `-j1`, `-j2`, etc. + +[#101]: https://github.com/tavianator/bfs/issues/101 + +### Changes + +- `bfs` now uses the [C17] standard version, up from C11 + +- Due to [#101], `bfs` now requires some additional C and POSIX features: + - [Standard C atomics] (`<stdatomic.h>`) + - [POSIX threads] (`<pthread.h>`) + +- `$LS_COLORS` extensions written in different cases (e.g. `*.jpg=35:*.JPG=01;35`) are now matched case-sensitively, to match the new behaviour of GNU ls since coreutils version 9.2 + +- Added a warning/error if `$LS_COLORS` can't be parsed, depending on whether `-color` is requested explicitly + +- Filenames with control characters are now escaped when printing with `-color` + +- Build flags like `WITH_ONIGURUMA` have been renamed to `USE_ONIGURUMA` + +[C17]: https://en.cppreference.com/w/c/17 +[Standard C atomics]: https://en.cppreference.com/w/c/atomic +[POSIX threads]: https://pubs.opengroup.org/onlinepubs/9699919799/idx/threads.html + +### Bug fixes + +- Fixed handling of the "normal text" color (`no` in `$LS_COLORS`) to match GNU ls + + 2.* === +2.6.3 +----- + +**January 31, 2023** + +- Fixed running the tests as root on Linux [`8b24de3`] + +- Fixed some tests on Android [`2724dfb`] [`0a5a80c`] + +- Stopped relying on non-POSIX touch(1) features in the tests. + This should fix the tests on at least OpenBSD. + [`2d5edb3`] + +- User/group caches are now filled lazily instead of eagerly [`b41dca5`] + +- More caches and I/O streams are flushed before -exec/-ok [`f98a1c4`] + +- Fixed various memory safety issues found by fuzzing \ + [`712b137`] [`5ce883d`] [`da02def`] [`c55e855`] + +- Fixed a test failure on certain macOS versions [`8b24de3`] + +- Mitigated a race condition when determining filesystem types ([#97]) + +- Lots of refactoring and optimization + +[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559 +[`2724dfb`]: https://github.com/tavianator/bfs/commit/2724dfbd17552f892a0d8b39b96cbe9e49d66fdb +[`0a5a80c`]: https://github.com/tavianator/bfs/commit/0a5a80c98cc7e5d8735b615fa197a6cff2bb08cc +[`2d5edb3`]: https://github.com/tavianator/bfs/commit/2d5edb37b924715b4fbee4d917ac334c773fca61 +[`b41dca5`]: https://github.com/tavianator/bfs/commit/b41dca52762c5188638236ae81b9f4597bb29ac9 +[`f98a1c4`]: https://github.com/tavianator/bfs/commit/f98a1c4a1cf61ff7d6483388ca1fac365fb0b31b +[`712b137`]: https://github.com/tavianator/bfs/commit/712b13756a09014ef730c8f9b96da4dc2f09b762 +[`5ce883d`]: https://github.com/tavianator/bfs/commit/5ce883daaafc69f83b01dac5db0647e9662a6e87 +[`da02def`]: https://github.com/tavianator/bfs/commit/da02defb91c3a1bda0ea7e653d81f997f1c8884a +[`c55e855`]: https://github.com/tavianator/bfs/commit/c55e85580df10c5afdc6fc0710e756a456aa8e93 +[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559 +[#97]: https://github.com/tavianator/bfs/issues/97 + + +2.6.2 +----- + +**October 21, 2022** + +- Fixed use of uninitialized memory on parsing errors involving `-fprintf` + +- Fixed Android build issues ([#96]) + +- Refactored the test suite + +[#96]: https://github.com/tavianator/bfs/issues/96 + + 2.6.1 ----- diff --git a/docs/HACKING.md b/docs/CONTRIBUTING.md index 08ddac2..099157d 100644 --- a/docs/HACKING.md +++ b/docs/CONTRIBUTING.md @@ -1,5 +1,5 @@ -Hacking on `bfs` -================ +Contributing to `bfs` +===================== License ------- @@ -7,11 +7,17 @@ License `bfs` is licensed under the [Zero-Clause BSD License](https://opensource.org/licenses/0BSD), a maximally permissive license. Contributions must use the same license. +Individual files contain the following tag instead of the full license text: + + SPDX-License-Identifier: 0BSD + +This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/ + Implementation -------------- -`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C11](https://en.wikipedia.org/wiki/C11_(C_standard_revision)). +`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision)). You can get a feel for the coding style by skimming the source code. [`main.c`](/src/main.c) contains an overview of the rest of source files. A quick summary: @@ -30,18 +36,26 @@ Tests `bfs` includes an extensive test suite. See the [build documentation](BUILDING.md#testing) for details on running the tests. +Test cases are grouped by the standard or `find` implementation that supports the tested feature(s): + +| Group | Description | +|---------------------------------|---------------------------------------| +| [`tests/posix`](/tests/posix) | POSIX compatibility tests | +| [`tests/bsd`](/tests/bsd) | BSD `find` features | +| [`tests/gnu`](/tests/gnu) | GNU `find` features | +| [`tests/common`](/tests/common) | Features common to BSD and GNU `find` | +| [`tests/bfs`](/tests/bfs) | `bfs`-specific tests | + Both new features and bug fixes should have associated tests. -To add a test, create a new function in `tests.sh` called `test_<something>`. +To add a test, create a new `*.sh` file in the appropriate group. Snapshot tests use the `bfs_diff` function to automatically compare the generated and expected outputs. For example, ```bash -function test_something() { - bfs_diff basic -name something -} +# posix/something.sh +bfs_diff basic -name something ``` `basic` is one of the directory trees generated for test cases; others include `links`, `loops`, `deep`, and `rainbow`. -Run `./tests/tests.sh test_something --update` to generate the reference snapshot (and don't forget to `git add` it). -Finally, add the test case to one of the arrays `posix_tests`, `bsd_tests`, `gnu_tests`, or `bfs_tests`, depending on which `find` implementations it should be compatible with. +Run `./tests/tests.sh posix/something --update` to generate the reference snapshot (and don't forget to `git add` it). diff --git a/docs/RELATED.md b/docs/RELATED.md new file mode 100644 index 0000000..6e7bd38 --- /dev/null +++ b/docs/RELATED.md @@ -0,0 +1,43 @@ +# Related utilities + +There are many tools that can be used to find files. +This is a catalogue of some of the most important/interesting ones. + +## `find`-compatible + +### System `find` implementations + +These `find` implementations are commonly installed as the system `find` utility in UNIX-like operating systems: + +- [GNU findutils](https://www.gnu.org/software/findutils/) ([manual](https://www.gnu.org/software/findutils/manual/html_node/find_html/index.html), [source](https://git.savannah.gnu.org/cgit/findutils.git)) +- BSD `find` + - FreeBSD `find` ([manual](https://www.freebsd.org/cgi/man.cgi?find(1)), [source](https://cgit.freebsd.org/src/tree/usr.bin/find)) + - OpenBSD `find` ([manual](https://man.openbsd.org/find.1), [source](https://cvsweb.openbsd.org/src/usr.bin/find/)) + - NetBSD `find` ([manual](https://man.netbsd.org/find.1), [source](http://cvsweb.netbsd.org/bsdweb.cgi/src/usr.bin/find/)) +- macOS `find` ([manual](https://ss64.com/osx/find.html), [source](https://github.com/apple-oss-distributions/shell_cmds/tree/main/find)) +- Solaris `find` + - [Illumos](https://illumos.org/) `find` ([manual](https://illumos.org/man/1/find), [source](https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/find/find.c)) + +### Alternative `find` implementations + +These are not usually installed as the system `find`, but are designed to be `find`-compatible + +- [`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](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 + +These utilities are not `find`-compatible, but serve a similar purpose: + +- [`fd`](https://github.com/sharkdp/fd): A simple, fast and user-friendly alternative to 'find' +- `locate` + - [GNU `locate`](https://www.gnu.org/software/findutils/locate) + - [`mlocate`](https://pagure.io/mlocate) ([manual](), [source](https://pagure.io/mlocate/tree/master)) + - [`plocate`](https://plocate.sesse.net/) ([manual](https://plocate.sesse.net/plocate.1.html), [source](https://git.sesse.net/?p=plocate)) +- [`walk`](https://github.com/google/walk): Plan 9 style utilities to replace find(1) +- [fselect](https://github.com/jhspetersson/fselect): Find files with SQL-like queries +- [rawhide](https://github.com/raforg/rawhide): find files using pretty C expressions 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 e2cff44..16aeaf6 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -18,10 +18,9 @@ $ bfs ./completions/bfs.zsh ./docs/BUILDING.md ./docs/CHANGELOG.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ./docs/USAGE.md ./docs/bfs.1 -./src/bfs.h ... ``` @@ -54,7 +53,7 @@ $ bfs -name '*.md' ./README.md ./docs/BUILDING.md ./docs/CHANGELOG.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ./docs/USAGE.md ``` @@ -65,7 +64,7 @@ When you put multiple expressions next to each other, both of them must match: ```console $ bfs -name '*.md' -name '*ING*' ./docs/BUILDING.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ``` This works because the expressions are implicitly combined with *logical and*. @@ -78,17 +77,16 @@ $ bfs -name '*.md' -and -name '*ING'` There are other operators like `-or`: ```console -$ bfs -name '*.md' -or -name '*.sh' +$ bfs -name '*.md' -or -name 'bfs.*' ./README.md -./tests/find-color.sh -./tests/ls-color.sh -./tests/remove-sibling.sh -./tests/sort-args.sh -./tests/tests.sh -./docs/CHANGELOG.md -./docs/HACKING.md +./completions/bfs.bash +./completions/bfs.fish +./completions/bfs.zsh ./docs/BUILDING.md +./docs/CHANGELOG.md +./docs/CONTRIBUTING.md ./docs/USAGE.md +./docs/bfs.1 ``` and `-not`: @@ -107,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`). @@ -132,6 +130,40 @@ Unlike `-prune`, `-exclude` even works in combination with `-depth`/`-delete`. --- +### `-limit` + +The `-limit N` action makes `bfs` quit once it gets evaluated `N` times. +Placing it after an action like `-print` limits the number of results that get printed, for example: + +```console +$ bfs -s -type f -name '*.txt' +./1.txt +./2.txt +./3.txt +./4.txt +$ bfs -s -type f -name '*.txt' -print -limit 2 +./1.txt +./2.txt +``` + +This is similar to + +```console +$ bfs -s -type f -name '*.txt' | head -n2 +``` + +but more powerful because you can apply separate limits to different expressions: + +```console +$ bfs \( -name '*.txt' -print -limit 3 -o -name '*.log' -print -limit 4 \) -limit 5 +[At most 3 .txt files, at most 4 .log files, and at most 5 in total] +``` + +and more efficient because it will quit immediately. +When piping to `head`, `bfs` will only quit *after* it tries to output too many results. + +--- + ### `-hidden`/`-nohidden` `-hidden` matches "hidden" files (dotfiles). @@ -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 posix-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 @@ -171,36 +175,49 @@ consumes too much memory. .TP .I eds Exponential deepening search. -A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g 0-1, 1-2, 2-4, 4-8, etc.). +A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g. 0-1, 1-2, 2-4, 4-8, etc.). Provides many of the benefits of breadth-first search with depth-first's reduced memory consumption. Typically far faster than .B \-S .IR ids . .RE +.TP +.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 @@ -212,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 @@ -226,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. @@ -245,8 +266,21 @@ or .B \-mindepth for example. Exclusions are always applied before other expressions, so it may be least confusing to put them first on the command line. -.SH OPTIONS .PP +.B \-help +.br +.B \-\-help +.RS +Print usage information, and exit immediately (without parsing the rest of the command line or processing any files). +.RE +.PP +.B \-version +.br +.B \-\-version +.RS +Print version information, and exit immediately. +.RE +.SH OPTIONS .B \-color .br .B \-nocolor @@ -268,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 @@ -277,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 @@ -287,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. @@ -306,14 +343,43 @@ 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 -regexes (default: -.IR posix-basic ; -see -.B \-regextype -.IR help ). +regular expressions. +The possible types are +.RS +.TP +.I posix-basic +POSIX basic regular expressions (the default). +.TP +.I posix-extended +POSIX extended regular expressions. +.TP +.I ed +Like +.BR ed (1) +(same as +.IR posix-basic ). +.TP +.I emacs +Like +.BR emacs (1). +.TP +.I grep +Like +.BR grep (1). +.TP +.I sed +Like +.BR sed (1) +(same as +.IR posix-basic ). +.PP +See +.BR regex (7) +for a description of regular expression syntax. +.RE .TP .B \-status Display a status bar while searching. @@ -332,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 @@ -352,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 @@ -367,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 @@ -403,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 . @@ -426,9 +507,13 @@ Find files the current user can execute/read/write. Always false/true. .RE .TP -.B \-fstype TYPE +\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 -.BR TYPE . +.IR TYPE . .PP \fB\-gid\fR [\fI\-+\fR]\fIN\fR .br @@ -438,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 . @@ -450,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 . @@ -473,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 @@ -506,13 +598,12 @@ to parse as an ISO 8601-style timestamp. For example: .PP .RS -1991-12-14 -.br -1991-12-14T03:00 -.br -1991-12-14T03:00-07:00 -.br -1991-12-14T10:00Z +.nf +\(bu \fI1991-12-14\fR +\(bu \fI1991-12-14T03:00\fR +\(bu \fI1991-12-14T03:00-07:00\fR +\(bu '\fI1991-12-14 10:00Z\fR' +.fi .RE .PP .B \-nogroup @@ -522,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 @@ -549,35 +642,67 @@ See for examples of the timestamp format. .TP \fB\-size\fR [\fI\-+\fR]\fIN\fR[\fIcwbkMGTP\fR] -Find files with the given size, in 1-byte -.IR c haracters, -2-byte -.IR w ords, -512-byte -.IR b locks -(default), or -.IR k iB/ M iB/ G iB/ T iB/ P iB. +Find files with the given size. +The unit can be one of +.PP +.RS +.nf +\(bu \fIc\fRhars (1 byte) +\(bu \fIw\fRords (2 bytes) +\(bu \fIb\fRlocks (512 bytes, the default) +\(bu \fIk\fRiB (1024 bytes) +\(bu \fIM\fRiB (1024 kiB) +\(bu \fIG\fRiB (1024 MiB) +\(bu \fIT\fRiB (1024 GiB) +\(bu \fIP\fRiB (1024 TiB) +.fi +.RE .TP .B \-sparse Find files that occupy fewer disk blocks than expected. .TP \fB\-type\fR [\fIbcdlpfswD\fR] Find files of the given type. -Possible types are +The possible types are +.PP +.RS +\(bu .IR b lock -device, +device +.br +\(bu .IR c haracter -device, -.IR d irectory, -symbolic -.IR l ink, -.IR p ipe, -regular -.IR f ile, -.IR s ocket, -.IR w hiteout, -and -.IR D oor. +device +.br +\(bu +.IR d irectory +.br +\(bu +.IR l ink +(symbolic) +.br +\(bu +.IR p ipe +.br +\(bu +.IR f ile +(regular) +.br +\(bu +.IR s ocket +.br +\(bu +.IR w hiteout +.br +\(bu +.IR D oor +.PP +Multiple types can be given at once, separated by commas. +For example, +.B \-type +.I d,f +matches both directories and regular files. +.RE .TP \fB\-used\fR [\fI\-+\fR]\fIN\fR Find files last accessed @@ -588,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 @@ -597,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 , @@ -626,15 +754,21 @@ but run the command in the same directory as the found file(s). .RE .TP \fB\-exit\fR [\fISTATUS\fR] -Exit immediately with the given status (0 if unspecified). +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 , @@ -643,6 +777,11 @@ but write to instead of standard output. .RE .TP +.BI "\-limit " N +Quit once this action is evaluated +.I N +times. +.TP .B \-ls List files like .B ls @@ -654,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: @@ -689,15 +828,16 @@ instead. .TP .B \-prune Don't descend into this directory. +This has no effect if +.B \-depth +is enabled (either explicitly, or implicitly by +.BR \-delete ). +Use +.B \-exclude +instead in that case. .TP .B \-quit Quit immediately. -.TP -.B \-version -Print version information. -.TP -.B \-help -Print usage information. .SH ENVIRONMENT Certain environment variables affect the behavior of .BR bfs . @@ -748,17 +888,48 @@ Specifies the pager used for .B \-help output. Defaults to +.BR less (1), +if found on the current +.BR PATH , +otherwise .BR more (1). .TP +.B PATH +Used to resolve executables for +.BR \-exec [ dir ] +and +.BR \-ok [ dir ]. +.TP .B POSIXLY_CORRECT Makes .B bfs conform more strictly to the POSIX.1-2017 specification for .BR find (1). -Currently this just disables warnings by default. +Currently this has two effects: +.RS +.IP \(bu +Disables warnings by default, because POSIX prohibits writing to standard error (except for the +.B \-ok +prompt), unless the command also fails with a non-zero exit status. +.IP \(bu +Makes +.B \-ls +and +.B \-fls +use 512-byte blocks instead of 1024-byte blocks. +(POSIX does not specify these actions, but BSD +.BR find (1) +implementations use 512-byte blocks, while GNU +.BR find (1) +uses 1024-byte blocks by default.) +.PP It does not disable .BR bfs 's various extensions to the base POSIX functionality. +.B POSIXLY_CORRECT +has the same effects on GNU +.BR find (1). +.RE .SH EXAMPLES .TP .B bfs @@ -773,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 @@ -782,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 @@ -790,12 +961,12 @@ 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. .TP -.B bfs \-type f \-executable \-exec strip '{}' + +.B bfs \-type f \-executable \-exec strip {} + Runs .BR strip (1) on all executable files it finds, passing it multiple files at a time. diff --git a/src/alloc.c b/src/alloc.c new file mode 100644 index 0000000..f505eda --- /dev/null +++ b/src/alloc.c @@ -0,0 +1,382 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "alloc.h" + +#include "bfs.h" +#include "bit.h" +#include "diag.h" +#include "sanity.h" + +#include <errno.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> + +/** The largest possible allocation size. */ +#if PTRDIFF_MAX < SIZE_MAX / 2 +# define ALLOC_MAX ((size_t)PTRDIFF_MAX) +#else +# define ALLOC_MAX (SIZE_MAX / 2) +#endif + +/** posix_memalign() wrapper. */ +static void *xmemalign(size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + bfs_assert(align >= sizeof(void *)); + + // 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; +} + +void *alloc(size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + + if (size > ALLOC_MAX) { + errno = EOVERFLOW; + return NULL; + } + + if (align <= alignof(max_align_t)) { + return malloc(size); + } else { + return xmemalign(align, size); + } +} + +void *zalloc(size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + + if (size > ALLOC_MAX) { + errno = EOVERFLOW; + return NULL; + } + + if (align <= alignof(max_align_t)) { + return calloc(1, size); + } + + void *ret = xmemalign(align, size); + if (ret) { + memset(ret, 0, size); + } + return ret; +} + +void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size) { + bfs_assert(has_single_bit(align)); + + if (new_size == 0) { + free(ptr); + return NULL; + } else if (new_size > ALLOC_MAX) { + errno = EOVERFLOW; + return NULL; + } + + if (align <= alignof(max_align_t)) { + return realloc(ptr, new_size); + } + + // There is no aligned_realloc(), so reallocate and copy manually + void *ret = xmemalign(align, new_size); + if (!ret) { + return NULL; + } + + size_t min_size = old_size < new_size ? old_size : new_size; + if (min_size) { + memcpy(ret, ptr, min_size); + } + + free(ptr); + return ret; +} + +void *reserve(void *ptr, size_t align, size_t size, size_t count) { + // No need to overflow-check the current size + 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, reuse ptr. + if (count & (count - 1)) { + // Tell sanitizers about the new array element + sanitize_resize(ptr, old_size, old_size + size, bit_ceil(count) * size); + errno = 0; + return ptr; + } + + // No need to overflow-check; xrealloc() will fail before we overflow + size_t new_size = count ? 2 * old_size : size; + void *ret = xrealloc(ptr, align, old_size, new_size); + if (!ret) { + // errno is used to communicate success/failure to the RESERVE() macro + bfs_assert(errno != 0); + return ptr; + } + + // Pretend we only allocated one more element + sanitize_resize(ret, new_size, old_size + size, new_size); + errno = 0; + return ret; +} + +/** + * An arena allocator chunk. + */ +union chunk { + /** + * Free chunks are stored in a singly linked list. The pointer to the + * next chunk is represented by an offset from the chunk immediately + * after this one in memory, so that zalloc() correctly initializes a + * linked list of chunks (except for the last one). + */ + uintptr_t next; + + // char object[]; +}; + +/** Decode the next chunk. */ +static union chunk *chunk_next(const struct arena *arena, const union chunk *chunk) { + uintptr_t base = (uintptr_t)chunk + arena->size; + return (union chunk *)(base + chunk->next); +} + +/** Encode the next chunk. */ +static void chunk_set_next(const struct arena *arena, union chunk *chunk, union chunk *next) { + uintptr_t base = (uintptr_t)chunk + arena->size; + chunk->next = (uintptr_t)next - base; +} + +void arena_init(struct arena *arena, size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + bfs_assert(is_aligned(align, size)); + + if (align < alignof(union chunk)) { + align = alignof(union chunk); + } + if (size < sizeof(union chunk)) { + size = sizeof(union chunk); + } + bfs_assert(is_aligned(align, size)); + + arena->chunks = NULL; + arena->nslabs = 0; + arena->slabs = NULL; + arena->align = align; + arena->size = size; +} + +/** Allocate a new slab. */ +_cold +static int slab_alloc(struct arena *arena) { + // Make the initial allocation size ~4K + size_t size = 4096; + if (size < arena->size) { + size = arena->size; + } + // Trim off the excess + size -= size % arena->size; + // Double the size for every slab + size <<= arena->nslabs; + + // Allocate the slab + void *slab = zalloc(arena->align, size); + if (!slab) { + return -1; + } + + // Grow the slab array + void **pslab = RESERVE(void *, &arena->slabs, &arena->nslabs); + if (!pslab) { + free(slab); + return -1; + } + + // Fix the last chunk->next offset + void *last = (char *)slab + size - arena->size; + chunk_set_next(arena, last, arena->chunks); + + // We can rely on zero-initialized slabs, but others shouldn't + sanitize_uninit(slab, size); + + arena->chunks = *pslab = slab; + return 0; +} + +void *arena_alloc(struct arena *arena) { + if (!arena->chunks && slab_alloc(arena) != 0) { + return NULL; + } + + union chunk *chunk = arena->chunks; + sanitize_alloc(chunk, arena->size); + + sanitize_init(chunk); + arena->chunks = chunk_next(arena, chunk); + sanitize_uninit(chunk, arena->size); + + return chunk; +} + +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); +} + +void arena_clear(struct arena *arena) { + for (size_t i = 0; i < arena->nslabs; ++i) { + free(arena->slabs[i]); + } + free(arena->slabs); + + arena->chunks = NULL; + arena->nslabs = 0; + arena->slabs = NULL; +} + +void arena_destroy(struct arena *arena) { + arena_clear(arena); + sanitize_uninit(arena); +} + +void varena_init(struct varena *varena, size_t align, size_t offset, size_t size) { + varena->align = align; + varena->offset = offset; + varena->size = size; + varena->narenas = 0; + varena->arenas = NULL; + + // The smallest size class is at least as many as fit in the smallest + // aligned allocation size + size_t min_count = (flex_size(align, offset, size, 1) - offset + size - 1) / size; + varena->shift = bit_width(min_count - 1); +} + +/** Get the size class for the given array length. */ +static size_t varena_size_class(struct varena *varena, size_t count) { + // Since powers of two are common array lengths, make them the + // (inclusive) upper bound for each size class + return bit_width((count - !!count) >> varena->shift); +} + +/** 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, varena->offset, varena->size, count); +} + +/** Get the arena for the given array length. */ +static struct arena *varena_get(struct varena *varena, size_t count) { + size_t i = varena_size_class(varena, count); + + while (i >= varena->narenas) { + size_t j = varena->narenas; + struct arena *arena = RESERVE(struct arena, &varena->arenas, &varena->narenas); + if (!arena) { + return NULL; + } + + size_t shift = j + varena->shift; + size_t size = varena_exact_size(varena, (size_t)1 << shift); + arena_init(arena, varena->align, size); + } + + return &varena->arenas[i]; +} + +void *varena_alloc(struct varena *varena, size_t count) { + struct arena *arena = varena_get(varena, count); + if (!arena) { + return NULL; + } + + void *ret = arena_alloc(arena); + if (!ret) { + return NULL; + } + + // Tell the sanitizers the exact size of the allocated struct + sanitize_resize(ret, arena->size, varena_exact_size(varena, count), arena->size); + + return ret; +} + +void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count) { + struct arena *new_arena = varena_get(varena, new_count); + struct arena *old_arena = varena_get(varena, old_count); + if (!new_arena) { + return NULL; + } + + size_t old_size = old_arena->size; + size_t new_size = new_arena->size; + + if (new_arena == old_arena) { + sanitize_resize(ptr, + varena_exact_size(varena, old_count), + varena_exact_size(varena, new_count), + new_size); + return ptr; + } + + void *ret = arena_alloc(new_arena); + if (!ret) { + return NULL; + } + + // 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 min_size = new_size < old_size ? new_size : old_size; + memcpy(ret, ptr, min_size); + + arena_free(old_arena, ptr); + + sanitize_resize(ret, new_size, varena_exact_size(varena, new_count), new_size); + return ret; +} + +void *varena_grow(struct varena *varena, void *ptr, size_t *count) { + size_t old_count = *count; + + // Round up to the limit of the current size class. If we're already at + // the limit, go to the next size class. + size_t new_shift = varena_size_class(varena, old_count + 1) + varena->shift; + size_t new_count = (size_t)1 << new_shift; + + ptr = varena_realloc(varena, ptr, old_count, new_count); + if (ptr) { + *count = new_count; + } + return ptr; +} + +void varena_free(struct varena *varena, void *ptr, size_t count) { + struct arena *arena = varena_get(varena, count); + arena_free(arena, ptr); +} + +void varena_clear(struct varena *varena) { + for (size_t i = 0; i < varena->narenas; ++i) { + arena_clear(&varena->arenas[i]); + } +} + +void varena_destroy(struct varena *varena) { + for (size_t i = 0; i < varena->narenas; ++i) { + arena_destroy(&varena->arenas[i]); + } + free(varena->arenas); + sanitize_uninit(varena); +} diff --git a/src/alloc.h b/src/alloc.h new file mode 100644 index 0000000..1fafbab --- /dev/null +++ b/src/alloc.h @@ -0,0 +1,401 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Memory allocation. + */ + +#ifndef BFS_ALLOC_H +#define BFS_ALLOC_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 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 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_CEIL(align, size); +} + +/** + * 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 size_mul(size_t size, size_t count) { + size_t ret = size * count; + return ret / size == count ? ret : (size_t)-1; +} + +/** Saturating array sizeof. */ +#define sizeof_array(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. + * + * @align + * Struct alignment. + * @offset + * Flexible array member offset. + * @size + * Flexible array element size. + * @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 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); + return ret; +} + +/** + * Computes the size of a flexible struct. + * + * @type + * The type of the struct containing the flexible array. + * @member + * The name of the flexible array member. + * @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) \ + (ASSERT_FLEX_ABI(type, member), flex_size( \ + alignof(type), offsetof(type, member), sizeof_member(type, member[0]), count)) + +/** + * General memory allocator. + * + * @align + * The required alignment. + * @size + * The size of the allocation. + * @return + * The allocated memory, or NULL on failure. + */ +_malloc(free, 1) +_aligned_alloc(1, 2) +void *alloc(size_t align, size_t size); + +/** + * Zero-initialized memory allocator. + * + * @align + * The required alignment. + * @size + * The size of the allocation. + * @return + * The allocated memory, or NULL on failure. + */ +_malloc(free, 1) +_aligned_alloc(1, 2) +void *zalloc(size_t align, size_t size); + +/** Allocate memory for the given type. */ +#define ALLOC(type) \ + (type *)alloc(alignof(type), sizeof(type)) + +/** Allocate zeroed memory for the given type. */ +#define ZALLOC(type) \ + (type *)zalloc(alignof(type), sizeof(type)) + +/** Allocate memory for an array. */ +#define ALLOC_ARRAY(type, count) \ + (type *)alloc(alignof(type), sizeof_array(type, count)) + +/** Allocate zeroed memory for an array. */ +#define ZALLOC_ARRAY(type, count) \ + (type *)zalloc(alignof(type), sizeof_array(type, count)) + +/** Allocate memory for a flexible struct. */ +#define ALLOC_FLEX(type, member, count) \ + (type *)alloc(alignof(type), sizeof_flex(type, member, count)) + +/** Allocate zeroed memory for a flexible struct. */ +#define ZALLOC_FLEX(type, member, count) \ + (type *)zalloc(alignof(type), sizeof_flex(type, member, count)) + +/** + * Alignment-aware realloc(). + * + * @ptr + * The pointer to reallocate. + * @align + * The required alignment. + * @old_size + * The previous allocation size. + * @new_size + * The new allocation size. + * @return + * The reallocated memory, or NULL on failure. + */ +_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. */ +#define REALLOC_ARRAY(type, ptr, old_count, new_count) \ + (type *)xrealloc((ptr), alignof(type), sizeof_array(type, old_count), sizeof_array(type, new_count)) + +/** Reallocate memory for a flexible struct. */ +#define REALLOC_FLEX(type, member, ptr, old_count, new_count) \ + (type *)xrealloc((ptr), alignof(type), sizeof_flex(type, member, old_count), sizeof_flex(type, member, new_count)) + +/** + * Reserve space for one more element in a dynamic array. + * + * @ptr + * The pointer to reallocate. + * @align + * The required alignment. + * @count + * The current size of the array. + * @return + * The reallocated memory, on both success *and* failure. On success, + * errno will be set to zero, and the returned pointer will have room + * for (count + 1) elements. On failure, errno will be non-zero, and + * ptr will returned unchanged. + */ +_nodiscard +void *reserve(void *ptr, size_t align, size_t size, size_t count); + +/** + * Convenience macro to grow a dynamic array. + * + * @type + * The array element type. + * @type **ptr + * A pointer to the array. + * @size_t *count + * A pointer to the array's size. + * @return + * On success, a pointer to the newly reserved array element, i.e. + * `*ptr + *count++`. On failure, NULL is returned, and both *ptr and + * *count remain unchanged. + */ +#define RESERVE(type, ptr, count) \ + ((*ptr) = reserve((*ptr), alignof(type), sizeof(type), (*count)), \ + errno ? NULL : (*ptr) + (*count)++) + +/** + * An arena allocator for fixed-size types. + * + * Arena allocators are intentionally not thread safe. + */ +struct arena { + /** The list of free chunks. */ + void *chunks; + /** The number of allocated slabs. */ + size_t nslabs; + /** The array of slabs. */ + void **slabs; + /** Chunk alignment. */ + size_t align; + /** Chunk size. */ + size_t size; +}; + +/** + * Initialize an arena for chunks of the given size and alignment. + */ +void arena_init(struct arena *arena, size_t align, size_t size); + +/** + * Initialize an arena for the given type. + */ +#define ARENA_INIT(arena, type) \ + arena_init((arena), alignof(type), sizeof(type)) + +/** + * Free an object from the arena. + */ +void arena_free(struct arena *arena, void *ptr); + +/** + * Allocate an object out of the arena. + */ +_malloc(arena_free, 2) +void *arena_alloc(struct arena *arena); + +/** + * Free all allocations from an arena. + */ +void arena_clear(struct arena *arena); + +/** + * Destroy an arena, freeing all allocations. + */ +void arena_destroy(struct arena *arena); + +/** + * An arena allocator for flexibly-sized types. + */ +struct varena { + /** The alignment of the struct. */ + size_t align; + /** The offset of the flexible array. */ + size_t offset; + /** The size of the flexible array elements. */ + size_t size; + /** Shift amount for the smallest size class. */ + size_t shift; + /** The number of arenas of different sizes. */ + size_t narenas; + /** The array of differently-sized arenas. */ + struct arena *arenas; +}; + +/** + * Initialize a varena for a struct with the given layout. + * + * @varena + * The varena to initialize. + * @align + * alignof(type) + * @offset + * offsetof(type, flexible_array) + * @size + * sizeof(flexible_array[i]) + */ +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. + * + * @varena + * The varena to initialize. + * @type + * A struct type containing a flexible array. + * @member + * The name of the flexible array member. + */ +#define VARENA_INIT(varena, type, member) \ + (ASSERT_FLEX_ABI(type, member), varena_init( \ + varena, alignof(type), offsetof(type, member), sizeof_member(type, member[0]))) + +/** + * Free an arena-allocated flexible struct. + * + * @varena + * The that allocated the object. + * @ptr + * The object to free. + * @count + * The length of the flexible array. + */ +void varena_free(struct varena *varena, void *ptr, size_t count); + +/** + * Arena-allocate a flexible struct. + * + * @varena + * The varena to allocate from. + * @count + * The length of the flexible array. + * @return + * The allocated struct, or NULL on failure. + */ +_malloc(varena_free, 2) +void *varena_alloc(struct varena *varena, size_t count); + +/** + * Resize a flexible struct. + * + * @varena + * The varena to allocate from. + * @ptr + * The object to resize. + * @old_count + * The old array length. + * @new_count + * The new array length. + * @return + * The resized struct, or NULL on failure. + */ +_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. + * + * @varena + * The varena to allocate from. + * @ptr + * The object to resize. + * @count + * Pointer to the flexible array length. + * @return + * The resized struct, or NULL on failure. + */ +_nodiscard +void *varena_grow(struct varena *varena, void *ptr, size_t *count); + +/** + * Free all allocations from a varena. + */ +void varena_clear(struct varena *varena); + +/** + * Destroy a varena, freeing all allocations. + */ +void varena_destroy(struct varena *varena); + +#endif // BFS_ALLOC_H diff --git a/src/atomic.h b/src/atomic.h new file mode 100644 index 0000000..5c2826f --- /dev/null +++ b/src/atomic.h @@ -0,0 +1,118 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Shorthand for standard C atomic operations. + */ + +#ifndef BFS_ATOMIC_H +#define BFS_ATOMIC_H + +#include "bfs.h" + +#include <stdatomic.h> + +/** + * Prettier spelling of _Atomic. + */ +#define atomic _Atomic + +/** + * Shorthand for atomic_load_explicit(). + * + * @obj + * A pointer to the atomic object. + * @order + * The memory ordering to use, without the memory_order_ prefix. + * @return + * The loaded value. + */ +#define load(obj, order) \ + atomic_load_explicit(obj, memory_order_##order) + +/** + * Shorthand for atomic_store_explicit(). + */ +#define store(obj, value, order) \ + atomic_store_explicit(obj, value, memory_order_##order) + +/** + * Shorthand for atomic_exchange_explicit(). + */ +#define exchange(obj, value, order) \ + atomic_exchange_explicit(obj, value, memory_order_##order) + +/** + * Shorthand for atomic_compare_exchange_weak_explicit(). + */ +#define compare_exchange_weak(obj, expected, desired, succ, fail) \ + atomic_compare_exchange_weak_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail) + +/** + * Shorthand for atomic_compare_exchange_strong_explicit(). + */ +#define compare_exchange_strong(obj, expected, desired, succ, fail) \ + atomic_compare_exchange_strong_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail) + +/** + * Shorthand for atomic_fetch_add_explicit(). + */ +#define fetch_add(obj, arg, order) \ + atomic_fetch_add_explicit(obj, arg, memory_order_##order) + +/** + * Shorthand for atomic_fetch_sub_explicit(). + */ +#define fetch_sub(obj, arg, order) \ + atomic_fetch_sub_explicit(obj, arg, memory_order_##order) + +/** + * Shorthand for atomic_fetch_or_explicit(). + */ +#define fetch_or(obj, arg, order) \ + atomic_fetch_or_explicit(obj, arg, memory_order_##order) + +/** + * Shorthand for atomic_fetch_xor_explicit(). + */ +#define fetch_xor(obj, arg, order) \ + atomic_fetch_xor_explicit(obj, arg, memory_order_##order) + +/** + * Shorthand for atomic_fetch_and_explicit(). + */ +#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 @@ -1,68 +1,59 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "bar.h" + +#include "alloc.h" +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "bit.h" #include "dstring.h" -#include "util.h" +#include "sighook.h" + #include <errno.h> #include <fcntl.h> -#include <limits.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; - volatile sig_atomic_t width; - volatile sig_atomic_t height; -}; + 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; } - bar->width = ws.ws_col; - bar->height = ws.ws_row; + 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. */ -#define ITOA_DIGITS ((sizeof(unsigned short) * CHAR_BIT + 2) / 3) +#define ITOA_DIGITS ((USHRT_WIDTH + 2) / 3) /** Async Signal Safe itoa(). */ static char *ass_itoa(char *str, unsigned int n) { @@ -80,140 +71,127 @@ 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 - char *ptr = esc_seq + strlen(esc_seq); - ptr = ass_itoa(ptr, bar->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(). */ -BFS_FORMATTER(2, 3) +_printf(2, 3) static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) { va_list args; va_start(args, format); - char *str = dstrvprintf(format, args); + dchar *str = dstrvprintf(format, args); va_end(args); if (!str) { 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 - 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 - (unsigned int)(the_bar.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; } unsigned int bfs_bar_width(const struct bfs_bar *bar) { - return bar->width; + return load(&bar->width, relaxed); } 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 @@ -222,7 +200,7 @@ int bfs_bar_update(struct bfs_bar *bar, const char *str) { "%s" "\033[27m" // SGR reverse video off "\0338", // DECRC: Restore cursor - (unsigned int)bar->height, + height, str ); } @@ -232,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); } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A terminal status bar. @@ -40,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. @@ -1,32 +1,241 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -/** - * Constants about the bfs program itself. +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Configuration and fundamental utilities. */ #ifndef BFS_H #define BFS_H -#ifndef BFS_VERSION -# define BFS_VERSION "2.6.1" +// 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" +# 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 new file mode 100644 index 0000000..b78af7a --- /dev/null +++ b/src/bfstd.c @@ -0,0 +1,1270 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "bfstd.h" + +#include "bfs.h" +#include "bit.h" +#include "diag.h" +#include "sanity.h" +#include "thread.h" +#include "xregex.h" + +#include <errno.h> +#include <fcntl.h> +#include <langinfo.h> +#include <limits.h> +#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 __has_include(<sys/sysmacros.h>) +# include <sys/sysmacros.h> +#elif __has_include(<sys/mkdev.h>) +# include <sys/mkdev.h> +#endif + +#if __has_include(<util.h>) +# include <util.h> +#endif + +bool error_is_like(int error, int category) { + if (error == category) { + return true; + } + + switch (category) { + case ENOENT: + return error == ENOTDIR; + + case ENOSYS: + // https://github.com/opencontainers/runc/issues/2151 + return errno == EPERM; + +#if __DragonFly__ + // https://twitter.com/tavianator/status/1742991411203485713 + case ENAMETOOLONG: + return error == EFAULT; +#endif + } + + return false; +} + +bool errno_is_like(int category) { + return error_is_like(errno, category); +} + +int try(int ret) { + if (ret >= 0) { + return ret; + } else { + bfs_assert(errno > 0, "errno should be positive, was %d\n", errno); + return -errno; + } +} + +char *xdirname(const char *path) { + size_t i = xbaseoff(path); + + // Skip trailing slashes + while (i > 0 && path[i - 1] == '/') { + --i; + } + + if (i > 0) { + return strndup(path, i); + } else if (path[i] == '/') { + return strdup("/"); + } else { + return strdup("."); + } +} + +char *xbasename(const char *path) { + size_t i = xbaseoff(path); + size_t len = strcspn(path + i, "/"); + if (len > 0) { + return strndup(path + i, len); + } else if (path[i] == '/') { + return strdup("/"); + } else { + return strdup("."); + } +} + +size_t xbaseoff(const char *path) { + size_t i = strlen(path); + + // Skip trailing slashes + while (i > 0 && path[i - 1] == '/') { + --i; + } + + // Find the beginning of the name + while (i > 0 && path[i - 1] != '/') { + --i; + } + + // Skip leading slashes + while (path[i] == '/' && path[i + 1]) { + ++i; + } + + return i; +} + +FILE *xfopen(const char *path, int flags) { + char mode[4]; + + switch (flags & O_ACCMODE) { + case O_RDONLY: + strcpy(mode, "rb"); + break; + case O_WRONLY: + strcpy(mode, "wb"); + break; + case O_RDWR: + strcpy(mode, "r+b"); + break; + default: + bfs_bug("Invalid access mode"); + errno = EINVAL; + return NULL; + } + + if (flags & O_APPEND) { + mode[0] = 'a'; + } + + int fd; + if (flags & O_CREAT) { + fd = open(path, flags, 0666); + } else { + fd = open(path, flags); + } + + if (fd < 0) { + return NULL; + } + + FILE *ret = fdopen(fd, mode); + if (!ret) { + close_quietly(fd); + return NULL; + } + + return ret; +} + +char *xgetdelim(FILE *file, char delim) { + char *chunk = NULL; + size_t n = 0; + ssize_t len = getdelim(&chunk, &n, delim, file); + if (len >= 0) { + if (chunk[len] == delim) { + chunk[len] = '\0'; + } + return chunk; + } else { + free(chunk); + if (!ferror(file)) { + errno = 0; + } + return NULL; + } +} + +const char *xgetprogname(void) { + const char *cmd = NULL; +#if BFS_HAS_GETPROGNAME + cmd = getprogname(); +#elif BFS_HAS_GETPROGNAME_GNU + cmd = program_invocation_short_name; +#endif + + if (!cmd) { + cmd = BFS_COMMAND; + } + + 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); + if (!pattern) { + return -1; + } + + struct bfs_regex *regex; + int ret = bfs_regcomp(®ex, pattern, BFS_REGEX_POSIX_EXTENDED, 0); + if (ret == 0) { + ret = bfs_regexec(regex, response, 0); + } + + bfs_regfree(regex); + return ret; +} + +/** Check if a response is affirmative or negative. */ +static int xrpmatch(const char *response) { + int ret = xrpregex(NOEXPR, response); + if (ret > 0) { + return 0; + } else if (ret < 0) { + return -1; + } + + ret = xrpregex(YESEXPR, response); + if (ret > 0) { + return 1; + } else if (ret < 0) { + return -1; + } + + // Failsafe: always handle y/n + char c = response[0]; + if (c == 'n' || c == 'N') { + return 0; + } else if (c == 'y' || c == 'Y') { + return 1; + } else { + return -1; + } +} + +int ynprompt(void) { + fflush(stderr); + + char *line = xgetdelim(stdin, '\n'); + int ret = line ? xrpmatch(line) : -1; + free(line); + return ret; +} + +void *xmemdup(const void *src, size_t size) { + void *ret = malloc(size); + if (ret) { + memcpy(ret, src, size); + } + return ret; +} + +char *xstpecpy(char *dest, char *end, const char *src) { + return xstpencpy(dest, end, src, SIZE_MAX); +} + +char *xstpencpy(char *dest, char *end, const char *src, size_t n) { + size_t space = end - dest; + n = space < n ? space : n; + n = strnlen(src, n); + memcpy(dest, src, n); + if (n < space) { + dest[n] = '\0'; + return dest + n; + } else { + end[-1] = '\0'; + return end; + } +} + +const char *xstrerror(int errnum) { + int saved = errno; + const char *ret = NULL; + static thread_local char buf[256]; + + // 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 + + bool free_loc = false; + if (loc == LC_GLOBAL_LOCALE) { + loc = duplocale(loc); + free_loc = true; + } + + 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) { + // Fallback for strerror_[lr]() or duplocale() failures + snprintf(buf, sizeof(buf), "Unknown error %d", errnum); + ret = buf; + } + + errno = saved; + 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, "----------"); + + str[0] = type_char(mode); + + if (mode & 00400) { + str[1] = 'r'; + } + if (mode & 00200) { + str[2] = 'w'; + } + if ((mode & 04100) == 04000) { + str[3] = 'S'; + } else if (mode & 04000) { + str[3] = 's'; + } else if (mode & 00100) { + str[3] = 'x'; + } + + if (mode & 00040) { + str[4] = 'r'; + } + if (mode & 00020) { + str[5] = 'w'; + } + if ((mode & 02010) == 02000) { + str[6] = 'S'; + } else if (mode & 02000) { + str[6] = 's'; + } else if (mode & 00010) { + str[6] = 'x'; + } + + if (mode & 00004) { + str[7] = 'r'; + } + if (mode & 00002) { + str[8] = 'w'; + } + if ((mode & 01001) == 01000) { + str[9] = 'T'; + } else if (mode & 01000) { + str[9] = 't'; + } else if (mode & 00001) { + str[9] = 'x'; + } +} + +/** Check if an rlimit value is infinite. */ +static bool rlim_isinf(rlim_t r) { + // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite + if (r == RLIM_INFINITY) { + return true; + } + +#ifdef RLIM_SAVED_CUR + if (r == RLIM_SAVED_CUR) { + return true; + } +#endif + +#ifdef RLIM_SAVED_MAX + if (r == RLIM_SAVED_MAX) { + return true; + } +#endif + + return false; +} + +int rlim_cmp(rlim_t a, rlim_t b) { + bool a_inf = rlim_isinf(a); + bool b_inf = rlim_isinf(b); + if (a_inf || b_inf) { + return a_inf - b_inf; + } + + return (a > b) - (a < b); +} + +dev_t xmakedev(int ma, int mi) { +#if __QNX__ + return makedev(0, ma, mi); +#elif defined(makedev) + return makedev(ma, mi); +#else + return (ma << 8) | mi; +#endif +} + +int xmajor(dev_t dev) { +#ifdef major + return major(dev); +#else + return dev >> 8; +#endif +} + +int xminor(dev_t dev) { +#ifdef minor + return minor(dev); +#else + return dev & 0xFF; +#endif +} + +pid_t xwaitpid(pid_t pid, int *status, int flags) { + pid_t ret; + do { + ret = waitpid(pid, status, flags); + } while (ret < 0 && errno == EINTR); + 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); +#else + int ret = dup(fd); + if (ret < 0) { + return -1; + } + + if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) { + close_quietly(ret); + return -1; + } + + return ret; +#endif +} + +int pipe_cloexec(int pipefd[2]) { +#if BFS_HAS_PIPE2 + return pipe2(pipefd, O_CLOEXEC); +#else + if (pipe(pipefd) != 0) { + return -1; + } + + if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) { + close_quietly(pipefd[1]); + close_quietly(pipefd[0]); + return -1; + } + + return 0; +#endif +} + +size_t xread(int fd, void *buf, size_t nbytes) { + size_t count = 0; + + while (count < nbytes) { + ssize_t ret = read(fd, (char *)buf + count, nbytes - count); + if (ret < 0) { + if (errno == EINTR) { + continue; + } else { + break; + } + } else if (ret == 0) { + // EOF + errno = 0; + break; + } else { + count += ret; + } + } + + return count; +} + +size_t xwrite(int fd, const void *buf, size_t nbytes) { + size_t count = 0; + + while (count < nbytes) { + ssize_t ret = write(fd, (const char *)buf + count, nbytes - count); + if (ret < 0) { + if (errno == EINTR) { + continue; + } else { + break; + } + } else if (ret == 0) { + // EOF? + errno = 0; + break; + } else { + count += ret; + } + } + + return count; +} + +void close_quietly(int fd) { + int error = errno; + xclose(fd); + errno = error; +} + +int xclose(int fd) { + int ret = close(fd); + if (ret != 0) { + bfs_verify(errno != EBADF); + } + return ret; +} + +int xfaccessat(int fd, const char *path, int amode) { + int ret = faccessat(fd, path, amode, 0); + +#ifdef AT_EACCESS + // Some platforms, like Hurd, only support AT_EACCESS. Other platforms, + // like Android, don't support AT_EACCESS at all. + if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) { + ret = faccessat(fd, path, amode, AT_EACCESS); + } +#endif + + return ret; +} + +char *xconfstr(int name) { +#if BFS_HAS_CONFSTR + size_t len = confstr(name, NULL, 0); + if (len == 0) { + return NULL; + } + + char *str = malloc(len); + if (!str) { + return NULL; + } + + if (confstr(name, str, len) != len) { + free(str); + return NULL; + } + + return str; +#else + errno = ENOTSUP; + return NULL; +#endif +} + +char *xreadlinkat(int fd, const char *path, size_t size) { + ssize_t len; + char *name = NULL; + + if (size == 0) { + size = 64; + } else { + ++size; // NUL terminator + } + + while (true) { + char *new_name = realloc(name, size); + if (!new_name) { + goto error; + } + name = new_name; + + len = readlinkat(fd, path, name, size); + if (len < 0) { + goto error; + } else if ((size_t)len >= size) { + size *= 2; + } else { + break; + } + } + + name[len] = '\0'; + return name; + +error: + free(name); + 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) { +#ifdef BFS_STRTOFFLAGS + char *str_arg = (char *)*str; + +#if __OpenBSD__ + typedef uint32_t bfs_fflags_t; +#else + typedef unsigned long bfs_fflags_t; +#endif + bfs_fflags_t set_arg = 0; + bfs_fflags_t clear_arg = 0; + + int ret = BFS_STRTOFFLAGS(&str_arg, &set_arg, &clear_arg); + + *str = str_arg; + *set = set_arg; + *clear = clear_arg; + + if (ret != 0) { + errno = EINVAL; + } + return ret; +#else // !BFS_STRTOFFLAGS + errno = ENOTSUP; + return -1; +#endif +} + +long xsysconf(int name) { +#if __FreeBSD__ && __SANITIZE_MEMORY__ + // Work around https://github.com/llvm/llvm-project/issues/88163 + __msan_scoped_disable_interceptor_checks(); +#endif + + long ret = sysconf(name); + +#if __FreeBSD__ && __SANITIZE_MEMORY__ + __msan_scoped_enable_interceptor_checks(); +#endif + + 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 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 + + if (ret < 1) { + ret = xsysconf(_SC_NPROCESSORS_ONLN); + } + + 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; +} + +wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb) { + wchar_t wc; + size_t mblen = mbrtowc(&wc, str + *i, len - *i, mb); + switch (mblen) { + case -1: // Invalid byte sequence + case -2: // Incomplete byte sequence + *i += 1; + *mb = (mbstate_t){0}; + return WEOF; + default: + *i += mblen; + return wc; + } +} + +size_t xstrwidth(const char *str) { + size_t len = strlen(str); + size_t ret = 0; + + size_t asclen = asciinlen(str, len); + size_t i; + for (i = 0; i < asclen; ++i) { + // Assume all ASCII printables have width 1 + if (xisprint(str[i])) { + ++ret; + } + } + + mbstate_t mb = {0}; + while (i < len) { + wint_t wc = xmbrtowc(str, &i, len, &mb); + if (wc == WEOF) { + // Assume a single-width '?' + ++ret; + continue; + } + + int width = xwcwidth(wc); + if (width > 0) { + ret += width; + } + } + + return ret; +} + +/** + * Character type flags. + */ +enum ctype { + IS_PRINT = 1 << 0, + IS_SPACE = 1 << 1, +}; + +/** Cached ctypes. */ +static unsigned char ctype_cache[UCHAR_MAX + 1]; + +/** Initialize the ctype cache. */ +static void char_cache_init(void) { + for (size_t c = 0; c <= UCHAR_MAX; ++c) { + if (xisprint(c)) { + ctype_cache[c] |= IS_PRINT; + } + if (xisspace(c)) { + ctype_cache[c] |= IS_SPACE; + } + } +} + +/** Check if a character is printable. */ +static bool wesc_isprint(unsigned char c, enum wesc_flags flags) { + if (ctype_cache[c] & IS_PRINT) { + return true; + } + + // Technically a literal newline is safe inside single quotes, but $'\n' + // is much nicer than ' + // ' + if (!(flags & WESC_SHELL) && (ctype_cache[c] & IS_SPACE)) { + return true; + } + + return false; +} + +/** Check if a wide character is printable. */ +static bool wesc_iswprint(wchar_t c, enum wesc_flags flags) { + if (xiswprint(c)) { + return true; + } + + if (!(flags & WESC_SHELL) && xiswspace(c)) { + return true; + } + + return false; +} + +/** Get the length of the longest printable prefix of a string. */ +static size_t printable_len(const char *str, size_t len, enum wesc_flags flags) { + static pthread_once_t once = PTHREAD_ONCE_INIT; + invoke_once(&once, char_cache_init); + + // Fast path: avoid multibyte checks + size_t asclen = asciinlen(str, len); + size_t i; + for (i = 0; i < asclen; ++i) { + if (!wesc_isprint(str[i], flags)) { + return i; + } + } + + mbstate_t mb = {0}; + for (size_t j = i; i < len; i = j) { + wint_t wc = xmbrtowc(str, &j, len, &mb); + if (wc == WEOF) { + break; + } + if (!wesc_iswprint(wc, flags)) { + break; + } + } + + return i; +} + +/** Convert a special char into a well-known escape sequence like "\n". */ +static const char *dollar_esc(char c) { + // https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html + switch (c) { + case '\a': + return "\\a"; + case '\b': + return "\\b"; + case '\033': + return "\\e"; + case '\f': + return "\\f"; + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + case '\v': + return "\\v"; + case '\'': + return "\\'"; + case '\\': + return "\\\\"; + default: + return NULL; + } +} + +/** $'Quote' a string for the shell. */ +static char *dollar_quote(char *dest, char *end, const char *str, size_t len, enum wesc_flags flags) { + dest = xstpecpy(dest, end, "$'"); + + mbstate_t mb = {0}; + for (size_t i = 0; i < len;) { + size_t start = i; + bool safe = false; + + wint_t wc = xmbrtowc(str, &i, len, &mb); + if (wc != WEOF) { + safe = wesc_iswprint(wc, flags); + } + + for (size_t j = start; safe && j < i; ++j) { + if (str[j] == '\'' || str[j] == '\\') { + safe = false; + } + } + + if (safe) { + dest = xstpencpy(dest, end, str + start, i - start); + } else { + for (size_t j = start; j < i; ++j) { + unsigned char byte = str[j]; + const char *esc = dollar_esc(byte); + if (esc) { + dest = xstpecpy(dest, end, esc); + } else { + static const char *hex[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; + dest = xstpecpy(dest, end, "\\x"); + dest = xstpecpy(dest, end, hex[byte / 0x10]); + dest = xstpecpy(dest, end, hex[byte % 0x10]); + } + } + } + } + + return xstpecpy(dest, end, "'"); +} + +/** 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/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/9799919799/utilities/V3_chap02.html#tag_19_02_03 + size_t ret = strcspn(str, "`$\\\"!"); + return ret < len ? ret : len; +} + +/** "Quote" a string for the shell. */ +static char *double_quote(char *dest, char *end, const char *str, size_t len) { + dest = xstpecpy(dest, end, "\""); + dest = xstpencpy(dest, end, str, len); + return xstpecpy(dest, end, "\""); +} + +/** 'Quote' a string for the shell. */ +static char *single_quote(char *dest, char *end, const char *str, size_t len) { + bool open = false; + + while (len > 0) { + size_t chunk = strcspn(str, "'"); + chunk = chunk < len ? chunk : len; + if (chunk > 0) { + if (!open) { + dest = xstpecpy(dest, end, "'"); + open = true; + } + dest = xstpencpy(dest, end, str, chunk); + str += chunk; + len -= chunk; + } + + while (len > 0 && *str == '\'') { + if (open) { + dest = xstpecpy(dest, end, "'"); + open = false; + } + dest = xstpecpy(dest, end, "\\'"); + ++str; + --len; + } + } + + if (open) { + dest = xstpecpy(dest, end, "'"); + } + + return dest; +} + +char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags) { + return wordnesc(dest, end, str, SIZE_MAX, flags); +} + +char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags) { + size_t len = strnlen(str, n); + char *start = dest; + + if (printable_len(str, len, flags) < len) { + // String contains unprintable chars, use $'this\x7Fsyntax' + dest = dollar_quote(dest, end, str, len, flags); + } else if (!(flags & WESC_SHELL) || bare_len(str, len) == len) { + // Whole string is safe as a bare word + dest = xstpencpy(dest, end, str, len); + } else if (quotable_len(str, len) == len) { + // Whole string is safe to double-quote + dest = double_quote(dest, end, str, len); + } else { + // Single-quote the whole string + dest = single_quote(dest, end, str, len); + } + + if (dest == start) { + dest = xstpecpy(dest, end, "\"\""); + } + + return dest; +} diff --git a/src/bfstd.h b/src/bfstd.h new file mode 100644 index 0000000..15dd949 --- /dev/null +++ b/src/bfstd.h @@ -0,0 +1,619 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Standard library wrappers and polyfills. + */ + +#ifndef BFS_BFSTD_H +#define BFS_BFSTD_H + +#include "bfs.h" + +#include <stddef.h> + +#include <ctype.h> + +/** + * Work around https://github.com/llvm/llvm-project/issues/65532 by forcing a + * function, not a macro, to be called. + */ +#if __FreeBSD__ && __SANITIZE_MEMORY__ +# define BFS_INTERCEPT(fn) (fn) +#else +# define BFS_INTERCEPT(fn) fn +#endif + +/** + * Wrap isalpha()/isdigit()/etc. + */ +#define BFS_ISCTYPE(fn, c) BFS_INTERCEPT(fn)((unsigned char)(c)) + +#define xisalnum(c) BFS_ISCTYPE(isalnum, c) +#define xisalpha(c) BFS_ISCTYPE(isalpha, c) +#define xisascii(c) BFS_ISCTYPE(isascii, c) +#define xiscntrl(c) BFS_ISCTYPE(iscntrl, c) +#define xisdigit(c) BFS_ISCTYPE(isdigit, c) +#define xislower(c) BFS_ISCTYPE(islower, c) +#define xisgraph(c) BFS_ISCTYPE(isgraph, c) +#define xisprint(c) BFS_ISCTYPE(isprint, c) +#define xispunct(c) BFS_ISCTYPE(ispunct, c) +#define xisspace(c) BFS_ISCTYPE(isspace, c) +#define xisupper(c) BFS_ISCTYPE(isupper, c) +#define xisxdigit(c) BFS_ISCTYPE(isxdigit, c) + +// #include <errno.h> + +/** + * 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. + * + * @error + * The error code to check. + * @category + * The category to test for. Known categories include ENOENT and + * ENAMETOOLONG. + * @return + * Whether the error belongs to the given category. + */ +bool error_is_like(int error, int category); + +/** + * Equivalent to error_is_like(errno, category). + */ +bool errno_is_like(int category); + +/** + * Apply the "negative errno" convention. + * + * @ret + * The return value of the attempted operation. + * @return + * ret, if non-negative, otherwise -errno. + */ +int try(int ret); + +#include <fcntl.h> + +#ifndef O_EXEC +# ifdef O_PATH +# define O_EXEC O_PATH +# else +# define O_EXEC O_RDONLY +# endif +#endif + +#ifndef O_SEARCH +# ifdef O_PATH +# define O_SEARCH O_PATH +# else +# define O_SEARCH O_RDONLY +# endif +#endif + +#ifndef O_DIRECTORY +# define O_DIRECTORY 0 +#endif + +#include <fnmatch.h> + +#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE) +# define FNM_CASEFOLD FNM_IGNORECASE +#endif + +// #include <libgen.h> + +/** + * Re-entrant dirname() variant that always allocates a copy. + * + * @path + * The path in question. + * @return + * The parent directory of the path. + */ +char *xdirname(const char *path); + +/** + * Re-entrant basename() variant that always allocates a copy. + * + * @path + * The path in question. + * @return + * The final component of the path. + */ +char *xbasename(const char *path); + +/** + * Find the offset of the final component of a path. + * + * @path + * The path in question. + * @return + * The offset of the basename. + */ +size_t xbaseoff(const char *path); + +#include <stdio.h> + +/** + * fopen() variant that takes open() style flags. + * + * @path + * The path to open. + * @flags + * Flags to pass to open(). + */ +FILE *xfopen(const char *path, int flags); + +/** + * Convenience wrapper for getdelim(). + * + * @file + * The file to read. + * @delim + * The delimiter character to split on. + * @return + * The read chunk (without the delimiter), allocated with malloc(). + * NULL is returned on error (errno != 0) or end of file (errno == 0). + */ +char *xgetdelim(FILE *file, char delim); + +// #include <stdlib.h> + +/** + * Wrapper for getprogname() or equivalent functionality. + * + * @return + * The basename of the currently running program. + */ +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. + */ +int ynprompt(void); + +// #include <string.h> + +/** + * Get the length of the pure-ASCII prefix of a string. + */ +size_t asciilen(const char *str); + +/** + * Get the length of the pure-ASCII prefix of a string. + * + * @str + * The string to check. + * @n + * The maximum prefix length. + */ +size_t asciinlen(const char *str, size_t n); + +/** + * Allocate a copy of a region of memory. + * + * @src + * The memory region to copy. + * @size + * The size of the memory region. + * @return + * A copy of the region, allocated with malloc(), or NULL on failure. + */ +void *xmemdup(const void *src, size_t size); + +/** + * A nice string copying function. + * + * @dest + * The NUL terminator of the destination string, or `end` if it is + * already truncated. + * @end + * The end of the destination buffer. + * @src + * The string to copy from. + * @return + * The new NUL terminator of the destination, or `end` on truncation. + */ +char *xstpecpy(char *dest, char *end, const char *src); + +/** + * A nice string copying function. + * + * @dest + * The NUL terminator of the destination string, or `end` if it is + * already truncated. + * @end + * The end of the destination buffer. + * @src + * The string to copy from. + * @n + * The maximum number of characters to copy. + * @return + * The new NUL terminator of the destination, or `end` on truncation. + */ +char *xstpencpy(char *dest, char *end, const char *src, size_t n); + +/** + * Thread-safe strerror(). + * + * @errnum + * An error number. + * @return + * A string describing that error, which remains valid until the next + * xstrerror() call in the same thread. + */ +const char *xstrerror(int errnum); + +/** + * Shorthand for xstrerror(errno). + */ +const char *errstr(void); + +/** + * Format a mode like ls -l (e.g. -rw-r--r--). + * + * @mode + * The mode to format. + * @str + * The string to hold the formatted mode. + */ +void xstrmode(mode_t mode, char str[11]); + +#include <sys/resource.h> + +/** + * Compare two rlim_t values, accounting for infinite limits. + */ +int rlim_cmp(rlim_t a, rlim_t b); + +#include <sys/types.h> + +/** + * Portable version of makedev(). + */ +dev_t xmakedev(int ma, int mi); + +/** + * Portable version of major(). + */ +int xmajor(dev_t dev); + +/** + * Portable version of minor(). + */ +int xminor(dev_t dev); + +// #include <sys/stat.h> + +/** + * 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> + +/** + * waitpid() wrapper that handles EINTR. + */ +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. + * + * @fd + * The file descriptor to duplicate. + * @return + * A duplicated file descriptor, or -1 on failure. + */ +int dup_cloexec(int fd); + +/** + * Like pipe(), but set the FD_CLOEXEC flag. + * + * @pipefd + * The array to hold the two file descriptors. + * @return + * 0 on success, -1 on failure. + */ +int pipe_cloexec(int pipefd[2]); + +/** + * A safe version of read() that handles interrupted system calls and partial + * reads. + * + * @return + * The number of bytes read. A value != nbytes indicates an error + * (errno != 0) or end of file (errno == 0). + */ +size_t xread(int fd, void *buf, size_t nbytes); + +/** + * A safe version of write() that handles interrupted system calls and partial + * writes. + * + * @return + * 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. + * + * @fd + * The file descriptor to close. + */ +void close_quietly(int fd); + +/** + * close() wrapper that asserts the file descriptor is valid. + * + * @fd + * The file descriptor to close. + * @return + * 0 on success, or -1 on error. + */ +int xclose(int fd); + +/** + * Wrapper for faccessat() that handles some portability issues. + */ +int xfaccessat(int fd, const char *path, int amode); + +/** + * readlinkat() wrapper that dynamically allocates the result. + * + * @fd + * The base directory descriptor. + * @path + * The path to the link, relative to fd. + * @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. + */ +char *xreadlinkat(int fd, const char *path, size_t size); + +/** + * Wrapper for confstr() that allocates with malloc(). + * + * @name + * The ID of the confstr to look up. + * @return + * The value of the confstr, or NULL on failure. + */ +char *xconfstr(int name); + +/** + * Portability wrapper for strtofflags(). + * + * @str + * The string to parse. The pointee will be advanced to the first + * invalid position on error. + * @set + * The flags that are set in the string. + * @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. + * + * @str + * The string to convert. + * @i + * The current index. + * @len + * The length of the string. + * @mb + * The multi-byte decoding state. + * @return + * The wide character at index *i, or WEOF if decoding fails. In either + * case, *i will be advanced to the next multi-byte character. + */ +wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb); + +/** + * wcswidth() variant that works on narrow strings. + * + * @str + * The string to measure. + * @return + * The likely width of that string in a terminal. + */ +size_t xstrwidth(const char *str); + +/** + * wcwidth() wrapper that works around LLVM bug #65532. + */ +#define xwcwidth BFS_INTERCEPT(wcwidth) + +#include <wctype.h> + +/** + * Wrap iswalpha()/iswdigit()/etc. + */ +#define BFS_ISWCTYPE(fn, c) BFS_INTERCEPT(fn)(c) + +#define xiswalnum(c) BFS_ISWCTYPE(iswalnum, c) +#define xiswalpha(c) BFS_ISWCTYPE(iswalpha, c) +#define xiswcntrl(c) BFS_ISWCTYPE(iswcntrl, c) +#define xiswdigit(c) BFS_ISWCTYPE(iswdigit, c) +#define xiswlower(c) BFS_ISWCTYPE(iswlower, c) +#define xiswgraph(c) BFS_ISWCTYPE(iswgraph, c) +#define xiswprint(c) BFS_ISWCTYPE(iswprint, c) +#define xiswpunct(c) BFS_ISWCTYPE(iswpunct, c) +#define xiswspace(c) BFS_ISWCTYPE(iswspace, c) +#define xiswupper(c) BFS_ISWCTYPE(iswupper, c) +#define xiswxdigit(c) BFS_ISWCTYPE(iswxdigit, c) + +// #include <wordexp.h> + +/** + * Flags for wordesc(). + */ +enum wesc_flags { + /** + * Escape special characters so that the shell will treat the escaped + * string as a single word. + */ + WESC_SHELL = 1 << 0, + /** + * Escape special characters so that the escaped string is safe to print + * to a TTY. + */ + WESC_TTY = 1 << 1, +}; + +/** + * Escape a string as a single shell word. + * + * @dest + * The destination string to fill. + * @end + * The end of the destination buffer. + * @src + * The string to escape. + * @flags + * Controls which characters to escape. + * @return + * The new NUL terminator of the destination, or `end` on truncation. + */ +char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags); + +/** + * Escape a string as a single shell word. + * + * @dest + * The destination string to fill. + * @end + * The end of the destination buffer. + * @src + * The string to escape. + * @n + * The maximum length of the string. + * @flags + * Controls which characters to escape. + * @return + * The new NUL terminator of the destination, or `end` on truncation. + */ +char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags); + +#endif // BFS_BFSTD_H @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * The bftw() implementation consists of the following components: @@ -20,32 +7,188 @@ * - struct bftw_file: A file that has been encountered during the traversal. * They have reference-counted links to their parents in the directory tree. * + * - struct bftw_list: A linked list of bftw_file's. + * + * - struct bftw_queue: A multi-stage queue of bftw_file's. + * * - struct bftw_cache: An LRU list of bftw_file's with open file descriptors, * used for openat() to minimize the amount of path re-traversals. * - * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented - * as a simple circular buffer. - * * - struct bftw_state: Represents the current state of the traversal, allowing * various helper functions to take fewer parameters. */ #include "bftw.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" #include "dir.h" -#include "darray.h" #include "dstring.h" +#include "ioq.h" +#include "list.h" #include "mtab.h" #include "stat.h" #include "trie.h" -#include "util.h" -#include <assert.h> + #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> -#include <unistd.h> +#include <sys/types.h> + +/** Initialize a bftw_stat cache. */ +static void bftw_stat_init(struct bftw_stat *bufs, struct bfs_stat *stat_buf, struct bfs_stat *lstat_buf) { + bufs->stat_buf = stat_buf; + bufs->lstat_buf = lstat_buf; + bufs->stat_err = -1; + bufs->lstat_err = -1; +} + +/** Fill a bftw_stat cache from another one. */ +static void bftw_stat_fill(struct bftw_stat *dest, const struct bftw_stat *src) { + if (dest->stat_err < 0 && src->stat_err >= 0) { + dest->stat_buf = src->stat_buf; + dest->stat_err = src->stat_err; + } + + if (dest->lstat_err < 0 && src->lstat_err >= 0) { + dest->lstat_buf = src->lstat_buf; + dest->lstat_err = src->lstat_err; + } +} + +/** Cache a bfs_stat() result. */ +static void bftw_stat_cache(struct bftw_stat *bufs, enum bfs_stat_flags flags, const struct bfs_stat *buf, int err) { + if (flags & BFS_STAT_NOFOLLOW) { + bufs->lstat_buf = buf; + bufs->lstat_err = err; + if (err || !S_ISLNK(buf->mode)) { + // Non-link, so share stat info + bufs->stat_buf = buf; + bufs->stat_err = err; + } + } else if (flags & BFS_STAT_TRYFOLLOW) { + if (err) { + bufs->stat_err = err; + } else if (S_ISLNK(buf->mode)) { + bufs->lstat_buf = buf; + bufs->lstat_err = err; + bufs->stat_err = ENOENT; + } else { + bufs->stat_buf = buf; + bufs->stat_err = err; + } + } else { + bufs->stat_buf = buf; + bufs->stat_err = err; + } +} + +/** Caching bfs_stat(). */ +static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + struct bftw_stat *bufs = &ftwbuf->stat_bufs; + struct bfs_stat *buf; + + if (flags & BFS_STAT_NOFOLLOW) { + buf = (struct bfs_stat *)bufs->lstat_buf; + if (bufs->lstat_err == 0) { + return buf; + } else if (bufs->lstat_err > 0) { + errno = bufs->lstat_err; + return NULL; + } + } else { + buf = (struct bfs_stat *)bufs->stat_buf; + if (bufs->stat_err == 0) { + return buf; + } else if (bufs->stat_err > 0) { + errno = bufs->stat_err; + return NULL; + } + } + + struct bfs_stat *ret; + int err; + if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, buf) == 0) { + ret = buf; + err = 0; +#ifdef S_IFWHT + } else if (errno == ENOENT && ftwbuf->type == BFS_WHT) { + // This matches the behavior of FTS_WHITEOUT on BSD + ret = memset(buf, 0, sizeof(*buf)); + ret->mode = S_IFWHT; + err = 0; +#endif + } else { + ret = NULL; + err = errno; + } + + bftw_stat_cache(bufs, flags, ret, err); + return ret; +} + +const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + struct BFTW *mutbuf = (struct BFTW *)ftwbuf; + const struct bfs_stat *ret; + + if (flags & BFS_STAT_TRYFOLLOW) { + ret = bftw_stat_impl(mutbuf, BFS_STAT_FOLLOW); + if (!ret && errno_is_like(ENOENT)) { + ret = bftw_stat_impl(mutbuf, BFS_STAT_NOFOLLOW); + } + } else { + ret = bftw_stat_impl(mutbuf, flags); + } + + return ret; +} + +const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + const struct bftw_stat *bufs = &ftwbuf->stat_bufs; + + if (flags & BFS_STAT_NOFOLLOW) { + if (bufs->lstat_err == 0) { + return bufs->lstat_buf; + } + } else if (bufs->stat_err == 0) { + return bufs->stat_buf; + } else if ((flags & BFS_STAT_TRYFOLLOW) && error_is_like(bufs->stat_err, ENOENT)) { + if (bufs->lstat_err == 0) { + return bufs->lstat_buf; + } + } + + return NULL; +} + +enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + if (flags & BFS_STAT_NOFOLLOW) { + if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { + return ftwbuf->type; + } + } else if (flags & BFS_STAT_TRYFOLLOW) { + if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) { + return ftwbuf->type; + } + } else { + if (ftwbuf->type != BFS_LNK) { + return ftwbuf->type; + } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) { + return BFS_ERROR; + } + } + + const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags); + if (statbuf) { + return bfs_mode_to_type(statbuf->mode); + } else { + return BFS_ERROR; + } +} /** * A file. @@ -55,21 +198,45 @@ struct bftw_file { struct bftw_file *parent; /** The root under which this file was found. */ struct bftw_file *root; - /** The next file in the queue, if any. */ + + /** + * List node for: + * + * bftw_queue::buffer + * bftw_queue::waiting + * bftw_file_open()::parents + */ struct bftw_file *next; - /** The previous file in the LRU list. */ - struct bftw_file *lru_prev; - /** The next file in the LRU list. */ - struct bftw_file *lru_next; + /** + * List node for: + * + * bftw_queue::ready + * bftw_state::to_close + */ + struct { struct bftw_file *next; } ready; + + /** + * List node for bftw_cache. + */ + struct { + struct bftw_file *prev; + struct bftw_file *next; + } lru; /** This file's depth in the walk. */ size_t depth; - /** Reference count. */ + /** Reference count (for ->parent). */ size_t refcount; + /** Pin count (for ->fd). */ + size_t pincount; /** An open descriptor to this file, or -1. */ int fd; + /** Whether this file has a pending ioq request. */ + bool ioqueued; + /** An open directory for this file, if any. */ + struct bfs_dir *dir; /** This file's type, if known. */ enum bfs_type type; @@ -78,154 +245,457 @@ struct bftw_file { /** The inode number, for cycle detection. */ ino_t ino; + /** Cached bfs_stat() info. */ + struct bftw_stat stat_bufs; + /** The offset of this file in the full path. */ size_t nameoff; /** The length of the file's name. */ size_t namelen; /** The file's name. */ - char name[]; + char name[]; // _counted_by(namelen + 1) +}; + +/** + * A linked list of bftw_file's. + */ +struct bftw_list { + struct bftw_file *head; + struct bftw_file **tail; +}; + +/** + * bftw_queue flags. + */ +enum bftw_qflags { + /** Track the sync/async service balance. */ + BFTW_QBALANCE = 1 << 0, + /** Buffer files before adding them to the queue. */ + BFTW_QBUFFER = 1 << 1, + /** Use LIFO (stack/DFS) ordering. */ + BFTW_QLIFO = 1 << 2, + /** Maintain a strict order. */ + BFTW_QORDER = 1 << 3, +}; + +/** + * A queue of bftw_file's that may be serviced asynchronously. + * + * A bftw_queue comprises three linked lists each tracking different stages. + * When BFTW_QBUFFER is set, files are initially pushed to the buffer: + * + * ╔═══╗ ╔═══╦═══╗ + * buffer: ║ 𝘩 ║ ║ 𝘩 ║ 𝘪 ║ + * ╠═══╬═══╦═══╗ ╠═══╬═══╬═══╗ + * waiting: ║ e ║ f ║ g ║ → ║ e ║ f ║ g ║ + * ╠═══╬═══╬═══╬═══╗ ╠═══╬═══╬═══╬═══╗ + * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ + * ╚═══╩═══╩═══╩═══╝ ╚═══╩═══╩═══╩═══╝ + * + * When bftw_queue_flush() is called, the files in the buffer are appended to + * the waiting list (or prepended, if BFTW_QLIFO is set): + * + * ╔═╗ + * buffer: ║ ║ + * ╠═╩═╦═══╦═══╦═══╦═══╗ + * waiting: ║ e ║ f ║ g ║ h ║ i ║ + * ╠═══╬═══╬═══╬═══╬═══╝ + * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ + * ╚═══╩═══╩═══╩═══╝ + * + * Using the buffer gives a more natural ordering for BFTW_QLIFO, and allows + * files to be sorted before adding them to the waiting list. If BFTW_QBUFFER + * is not set, files are pushed directly to the waiting list instead. + * + * Files on the waiting list are waiting to be "serviced" asynchronously by the + * ioq (for example, by an ioq_opendir() or ioq_stat() call). While they are + * being serviced, they are detached from the queue by bftw_queue_detach() and + * are not tracked by the queue at all: + * + * ╔═╗ + * buffer: ║ ║ + * ╠═╩═╦═══╦═══╗ ⎛ ┌───┬───┐ ⎞ + * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ 𝓯 │ ⎟ + * ╠═══╬═══╬═══╬═══╗ ⎝ └───┴───┘ ⎠ + * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ + * ╚═══╩═══╩═══╩═══╝ + * + * When their async service is complete, files are reattached to the queue by + * bftw_queue_attach(), this time on the ready list: + * + * ╔═╗ + * buffer: ║ ║ + * ╠═╩═╦═══╦═══╗ ⎛ ┌───┐ ⎞ + * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ ⎟ + * ╠═══╬═══╬═══╬═══╦═══╗ ⎝ └───┘ ⎠ + * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ 𝕗 ║ + * ╚═══╩═══╩═══╩═══╩═══╝ + * + * Files are added to the ready list in the order they are finished by the ioq. + * bftw_queue_pop() pops a file from the ready list if possible. Otherwise, it + * pops from the waiting list, and the file must be serviced synchronously. + * + * However, if BFTW_QORDER is set, files must be popped in the exact order they + * are added to the waiting list (to maintain sorted order). In this case, + * files are added to the waiting and ready lists at the same time. The + * file->ioqueued flag is set while it is in-service, so that bftw() can wait + * for it to be truly ready before using it. + * + * ╔═╗ + * buffer: ║ ║ + * ╠═╩═╦═══╦═══╗ ⎛ ┌───┐ ⎞ + * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ ⎟ + * ╠═══╬═══╬═══╬═══╦═══╦═══╦═══╦═══╦═══╗ ⎝ └───┘ ⎠ + * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ 𝓮 ║ 𝕗 ║ g ║ h ║ i ║ + * ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝ + * + * If BFTW_QBALANCE is set, queue->imbalance tracks the delta between async + * service (negative) and synchronous service (positive). The queue is + * considered "balanced" when this number is non-negative. Only a balanced + * queue will perform any async service, ensuring work is fairly distributed + * between the main thread and the ioq. + * + * BFTW_QBALANCE is only set for single-threaded ioqs. When an ioq has multiple + * threads, it is faster to wait for the ioq to complete an operation than it is + * to perform it on the main thread. + */ +struct bftw_queue { + /** Queue flags. */ + enum bftw_qflags flags; + /** A buffer of files to be enqueued together. */ + struct bftw_list buffer; + /** A list of files which are waiting to be serviced. */ + struct bftw_list waiting; + /** A list of already-serviced files. */ + struct bftw_list ready; + /** The current size of the queue. */ + size_t size; + /** The number of files currently in-service. */ + size_t ioqueued; + /** Tracks the imbalance between synchronous and async service. */ + unsigned long imbalance; }; +/** Initialize a queue. */ +static void bftw_queue_init(struct bftw_queue *queue, enum bftw_qflags flags) { + queue->flags = flags; + SLIST_INIT(&queue->buffer); + SLIST_INIT(&queue->waiting); + SLIST_INIT(&queue->ready); + queue->size = 0; + queue->ioqueued = 0; + queue->imbalance = 0; +} + +/** Add a file to the queue. */ +static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) { + if (queue->flags & BFTW_QBUFFER) { + SLIST_APPEND(&queue->buffer, file); + } else if (queue->flags & BFTW_QLIFO) { + SLIST_PREPEND(&queue->waiting, file); + if (queue->flags & BFTW_QORDER) { + SLIST_PREPEND(&queue->ready, file, ready); + } + } else { + SLIST_APPEND(&queue->waiting, file); + if (queue->flags & BFTW_QORDER) { + SLIST_APPEND(&queue->ready, file, ready); + } + } + + ++queue->size; +} + +/** Add any buffered files to the queue. */ +static void bftw_queue_flush(struct bftw_queue *queue) { + if (!(queue->flags & BFTW_QBUFFER)) { + return; + } + + if (queue->flags & BFTW_QORDER) { + // When sorting, add files to the ready list at the same time + // (and in the same order) as they are added to the waiting list + struct bftw_file **cursor = (queue->flags & BFTW_QLIFO) + ? &queue->ready.head + : queue->ready.tail; + for_slist (struct bftw_file, file, &queue->buffer) { + cursor = SLIST_INSERT(&queue->ready, cursor, file, ready); + } + } + + if (queue->flags & BFTW_QLIFO) { + SLIST_EXTEND(&queue->buffer, &queue->waiting); + } + + SLIST_EXTEND(&queue->waiting, &queue->buffer); +} + +/** Check if the queue is properly balanced for async work. */ +static bool bftw_queue_balanced(const struct bftw_queue *queue) { + if (queue->flags & BFTW_QBALANCE) { + return (long)queue->imbalance >= 0; + } else { + return true; + } +} + +/** Update the queue balance for (a)sync service. */ +static void bftw_queue_rebalance(struct bftw_queue *queue, bool async) { + if (async) { + --queue->imbalance; + } else { + ++queue->imbalance; + } +} + +/** Detach the next waiting file. */ +static void bftw_queue_detach(struct bftw_queue *queue, struct bftw_file *file, bool async) { + bfs_assert(!file->ioqueued); + + if (file == SLIST_HEAD(&queue->buffer)) { + // To maintain order, we can't detach any files until they're + // added to the waiting/ready lists + bfs_assert(!(queue->flags & BFTW_QORDER)); + SLIST_POP(&queue->buffer); + } else if (file == SLIST_HEAD(&queue->waiting)) { + SLIST_POP(&queue->waiting); + } else { + bfs_bug("Detached file was not buffered or waiting"); + } + + if (async) { + file->ioqueued = true; + ++queue->ioqueued; + bftw_queue_rebalance(queue, true); + } +} + +/** Reattach a serviced file to the queue. */ +static void bftw_queue_attach(struct bftw_queue *queue, struct bftw_file *file, bool async) { + if (async) { + bfs_assert(file->ioqueued); + file->ioqueued = false; + --queue->ioqueued; + } else { + bfs_assert(!file->ioqueued); + } + + if (!(queue->flags & BFTW_QORDER)) { + SLIST_APPEND(&queue->ready, file, ready); + } +} + +/** Make a file ready immediately. */ +static void bftw_queue_skip(struct bftw_queue *queue, struct bftw_file *file) { + bftw_queue_detach(queue, file, false); + bftw_queue_attach(queue, file, false); +} + +/** Get the next waiting file. */ +static struct bftw_file *bftw_queue_waiting(const struct bftw_queue *queue) { + if (!(queue->flags & BFTW_QBUFFER)) { + return SLIST_HEAD(&queue->waiting); + } + + if (queue->flags & BFTW_QORDER) { + // Don't detach files until they're on the waiting/ready lists + return SLIST_HEAD(&queue->waiting); + } + + const struct bftw_list *prefix = &queue->waiting; + const struct bftw_list *suffix = &queue->buffer; + if (queue->flags & BFTW_QLIFO) { + prefix = &queue->buffer; + suffix = &queue->waiting; + } + + struct bftw_file *file = SLIST_HEAD(prefix); + if (!file) { + file = SLIST_HEAD(suffix); + } + return file; +} + +/** Get the next ready file. */ +static struct bftw_file *bftw_queue_ready(const struct bftw_queue *queue) { + return SLIST_HEAD(&queue->ready); +} + +/** Pop a file from the queue. */ +static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) { + // Don't pop until we've had a chance to sort the buffer + bfs_assert(SLIST_EMPTY(&queue->buffer)); + + struct bftw_file *file = SLIST_POP(&queue->ready, ready); + + if (!file || file == SLIST_HEAD(&queue->waiting)) { + // If no files are ready, try the waiting list. Or, if + // BFTW_QORDER is set, we may need to pop from both lists. + file = SLIST_POP(&queue->waiting); + } + + if (file) { + --queue->size; + } + + return file; +} + /** * A cache of open directories. */ struct bftw_cache { /** The head of the LRU list. */ struct bftw_file *head; - /** The insertion target for the LRU list. */ - struct bftw_file *target; /** The tail of the LRU list. */ struct bftw_file *tail; + /** The insertion target for the LRU list. */ + struct bftw_file *target; /** The remaining capacity of the LRU list. */ size_t capacity; + + /** bftw_file arena. */ + struct varena files; + + /** bfs_dir arena. */ + struct arena dirs; + /** Remaining bfs_dir capacity. */ + int dir_limit; + + /** bfs_stat arena. */ + struct arena stat_bufs; }; /** Initialize a cache. */ static void bftw_cache_init(struct bftw_cache *cache, size_t capacity) { - cache->head = NULL; + LIST_INIT(cache); cache->target = NULL; - cache->tail = NULL; cache->capacity = capacity; -} -/** Destroy a cache. */ -static void bftw_cache_destroy(struct bftw_cache *cache) { - assert(!cache->tail); - assert(!cache->target); - assert(!cache->head); -} + VARENA_INIT(&cache->files, struct bftw_file, name); -/** Add a bftw_file to the cache. */ -static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) { - assert(cache->capacity > 0); - assert(file->fd >= 0); - assert(!file->lru_prev); - assert(!file->lru_next); - - if (cache->target) { - file->lru_prev = cache->target; - file->lru_next = cache->target->lru_next; - } else { - file->lru_next = cache->head; - } + bfs_dir_arena(&cache->dirs); - if (file->lru_prev) { - file->lru_prev->lru_next = file; + if (cache->capacity > 1024) { + cache->dir_limit = 1024; } else { - cache->head = file; + cache->dir_limit = capacity - 1; } - if (file->lru_next) { - file->lru_next->lru_prev = file; - } else { - cache->tail = file; + ARENA_INIT(&cache->stat_bufs, struct bfs_stat); +} + +/** Allocate a directory. */ +static struct bfs_dir *bftw_allocdir(struct bftw_cache *cache, bool force) { + if (!force && cache->dir_limit <= 0) { + errno = ENOMEM; + return NULL; } - // Prefer to keep the root paths open by keeping them at the head of the list - if (file->depth == 0) { - cache->target = file; + struct bfs_dir *dir = arena_alloc(&cache->dirs); + if (dir) { + --cache->dir_limit; } + return dir; +} - --cache->capacity; +/** Free a directory. */ +static void bftw_freedir(struct bftw_cache *cache, struct bfs_dir *dir) { + ++cache->dir_limit; + arena_free(&cache->dirs, dir); } -/** Remove a bftw_file from the cache. */ -static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) { +/** Remove a bftw_file from the LRU list. */ +static void bftw_lru_remove(struct bftw_cache *cache, struct bftw_file *file) { if (cache->target == file) { - cache->target = file->lru_prev; + cache->target = file->lru.prev; } - if (file->lru_prev) { - assert(cache->head != file); - file->lru_prev->lru_next = file->lru_next; - } else { - assert(cache->head == file); - cache->head = file->lru_next; - } - - if (file->lru_next) { - assert(cache->tail != file); - file->lru_next->lru_prev = file->lru_prev; - } else { - assert(cache->tail == file); - cache->tail = file->lru_prev; - } - - file->lru_prev = NULL; - file->lru_next = NULL; - ++cache->capacity; + LIST_REMOVE(cache, file, lru); } -/** Mark a cache entry as recently used. */ -static void bftw_cache_use(struct bftw_cache *cache, struct bftw_file *file) { - bftw_cache_remove(cache, file); - bftw_cache_add(cache, file); +/** Remove a bftw_file from the cache. */ +static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) { + bftw_lru_remove(cache, file); + ++cache->capacity; } /** Close a bftw_file. */ static void bftw_file_close(struct bftw_cache *cache, struct bftw_file *file) { - assert(file->fd >= 0); + bfs_assert(file->fd >= 0); + bfs_assert(file->pincount == 0); - bftw_cache_remove(cache, file); + if (file->dir) { + bfs_closedir(file->dir); + bftw_freedir(cache, file->dir); + file->dir = NULL; + } else { + xclose(file->fd); + } - xclose(file->fd); file->fd = -1; + bftw_cache_remove(cache, file); } -/** Pop a directory from the cache. */ -static void bftw_cache_pop(struct bftw_cache *cache) { - assert(cache->tail); - bftw_file_close(cache, cache->tail); -} - -/** - * Shrink the cache, to recover from EMFILE. - * - * @param cache - * The cache in question. - * @param saved - * A bftw_file that must be preserved. - * @return - * 0 if successfully shrunk, otherwise -1. - */ -static int bftw_cache_shrink(struct bftw_cache *cache, const struct bftw_file *saved) { +/** Pop the least recently used directory from the cache. */ +static int bftw_cache_pop(struct bftw_cache *cache) { struct bftw_file *file = cache->tail; if (!file) { return -1; } - if (file == saved) { - file = file->lru_prev; - if (!file) { - return -1; - } + bftw_file_close(cache, file); + return 0; +} + +/** Add a bftw_file to the LRU list. */ +static void bftw_lru_add(struct bftw_cache *cache, struct bftw_file *file) { + bfs_assert(file->fd >= 0); + + LIST_INSERT(cache, cache->target, file, lru); + + // Prefer to keep the root paths open by keeping them at the head of the list + if (file->depth == 0) { + cache->target = file; } +} - bftw_file_close(cache, file); - cache->capacity = 0; +/** Add a bftw_file to the cache. */ +static int bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) { + bfs_assert(file->fd >= 0); + + if (cache->capacity == 0 && bftw_cache_pop(cache) != 0) { + bftw_file_close(cache, file); + errno = EMFILE; + return -1; + } + + bfs_assert(cache->capacity > 0); + --cache->capacity; + + bftw_lru_add(cache, file); return 0; } +/** Pin a cache entry so it won't be closed. */ +static void bftw_cache_pin(struct bftw_cache *cache, struct bftw_file *file) { + bfs_assert(file->fd >= 0); + + if (file->pincount++ == 0) { + bftw_lru_remove(cache, file); + } +} + +/** Unpin a cache entry. */ +static void bftw_cache_unpin(struct bftw_cache *cache, struct bftw_file *file) { + bfs_assert(file->fd >= 0); + bfs_assert(file->pincount > 0); + + if (--file->pincount == 0) { + bftw_lru_add(cache, file); + } +} + /** Compute the name offset of a child path. */ static size_t bftw_child_nameoff(const struct bftw_file *parent) { size_t ret = parent->nameoff + parent->namelen; @@ -235,12 +705,20 @@ static size_t bftw_child_nameoff(const struct bftw_file *parent) { return ret; } +/** Destroy a cache. */ +static void bftw_cache_destroy(struct bftw_cache *cache) { + bfs_assert(LIST_EMPTY(cache)); + bfs_assert(!cache->target); + + arena_destroy(&cache->stat_bufs); + arena_destroy(&cache->dirs); + varena_destroy(&cache->files); +} + /** Create a new bftw_file. */ -static struct bftw_file *bftw_file_new(struct bftw_file *parent, const char *name) { +static struct bftw_file *bftw_file_new(struct bftw_cache *cache, struct bftw_file *parent, const char *name) { size_t namelen = strlen(name); - size_t size = BFS_FLEX_SIZEOF(struct bftw_file, name, namelen + 1); - - struct bftw_file *file = malloc(size); + struct bftw_file *file = varena_alloc(&cache->files, namelen + 1); if (!file) { return NULL; } @@ -258,63 +736,408 @@ static struct bftw_file *bftw_file_new(struct bftw_file *parent, const char *nam file->nameoff = 0; } - file->next = NULL; - - file->lru_prev = NULL; - file->lru_next = NULL; + SLIST_ITEM_INIT(file); + SLIST_ITEM_INIT(file, ready); + LIST_ITEM_INIT(file, lru); file->refcount = 1; + file->pincount = 0; file->fd = -1; + file->ioqueued = false; + file->dir = NULL; file->type = BFS_UNKNOWN; file->dev = -1; file->ino = -1; + bftw_stat_init(&file->stat_bufs, NULL, NULL); + file->namelen = namelen; memcpy(file->name, name, namelen + 1); return file; } +/** Associate an open directory with a bftw_file. */ +static void bftw_file_set_dir(struct bftw_cache *cache, struct bftw_file *file, struct bfs_dir *dir) { + bfs_assert(!file->dir); + file->dir = dir; + + if (file->fd >= 0) { + bfs_assert(file->fd == bfs_dirfd(dir)); + } else { + file->fd = bfs_dirfd(dir); + bftw_cache_add(cache, file); + } +} + +/** Free a file's cached stat() buffers. */ +static void bftw_stat_recycle(struct bftw_cache *cache, struct bftw_file *file) { + struct bftw_stat *bufs = &file->stat_bufs; + + struct bfs_stat *stat_buf = (struct bfs_stat *)bufs->stat_buf; + struct bfs_stat *lstat_buf = (struct bfs_stat *)bufs->lstat_buf; + if (stat_buf) { + arena_free(&cache->stat_bufs, stat_buf); + } else if (lstat_buf) { + arena_free(&cache->stat_bufs, lstat_buf); + } + + bftw_stat_init(bufs, NULL, NULL); +} + +/** Free a bftw_file. */ +static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) { + bfs_assert(file->refcount == 0); + + if (file->fd >= 0) { + bftw_file_close(cache, file); + } + + bftw_stat_recycle(cache, file); + + varena_free(&cache->files, file, file->namelen + 1); +} + /** - * Open a bftw_file relative to another one. - * - * @param cache - * The cache to hold the file. - * @param file - * The file to open. - * @param base - * The base directory for the relative path (may be NULL). - * @param at_fd - * The base file descriptor, AT_FDCWD if base == NULL. - * @param at_path - * The relative path to the file. - * @return - * The opened file descriptor, or negative on error. + * Holds the current state of the bftw() traversal. */ -static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, struct bftw_file *base, const char *at_path) { - assert(file->fd < 0); +struct bftw_state { + /** The path(s) to start from. */ + const char **paths; + /** The number of starting paths. */ + size_t npaths; + /** bftw() callback. */ + bftw_callback *callback; + /** bftw() callback data. */ + void *ptr; + /** bftw() flags. */ + enum bftw_flags flags; + /** Search strategy. */ + enum bftw_strategy strategy; + /** The mount table. */ + const struct bfs_mtab *mtab; + /** bfs_opendir() flags. */ + enum bfs_dir_flags dir_flags; + + /** The appropriate errno value, if any. */ + int error; + + /** The cache of open directories. */ + struct bftw_cache cache; + + /** The async I/O queue. */ + struct ioq *ioq; + /** The number of I/O threads. */ + size_t nthreads; + + /** The queue of unpinned directories to unwrap. */ + struct bftw_list to_close; + /** The queue of files to visit. */ + struct bftw_queue fileq; + /** The queue of directories to open/read. */ + struct bftw_queue dirq; + + /** The current path. */ + dchar *path; + /** The current file. */ + struct bftw_file *file; + /** The previous file. */ + struct bftw_file *previous; + + /** The currently open directory. */ + struct bfs_dir *dir; + /** The current directory entry. */ + struct bfs_dirent *de; + /** Storage for the directory entry. */ + struct bfs_dirent de_storage; + /** Any error encountered while reading the directory. */ + int direrror; + + /** Extra data about the current file. */ + struct BFTW ftwbuf; + /** stat() buffer storage. */ + struct bfs_stat stat_buf; + /** lstat() buffer storage. */ + struct bfs_stat lstat_buf; +}; + +/** Check if we have to buffer files before visiting them. */ +static bool bftw_must_buffer(const struct bftw_state *state) { + if (state->flags & BFTW_SORT) { + // Have to buffer the files to sort them + return true; + } + + if (state->strategy == BFTW_DFS && state->nthreads == 0) { + // Without buffering, we would get a not-quite-depth-first + // ordering: + // + // a + // b + // a/c + // a/c/d + // b/e + // b/e/f + // + // This is okay for iterative deepening, since the caller only + // sees files at the target depth. We also deem it okay for + // parallel searches, since the order is unpredictable anyway. + return true; + } + + if ((state->flags & BFTW_STAT) && state->nthreads > 1) { + // We will be buffering every file anyway for ioq_stat() + return true; + } + + return false; +} + +/** Initialize the bftw() state. */ +static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) { + state->paths = args->paths; + state->npaths = args->npaths; + state->callback = args->callback; + state->ptr = args->ptr; + state->flags = args->flags; + state->strategy = args->strategy; + state->mtab = args->mtab; + state->dir_flags = 0; + state->error = 0; + + if (args->nopenfd < 2) { + errno = EMFILE; + return -1; + } + + size_t nopenfd = args->nopenfd; + size_t qdepth = 4096; + size_t nthreads = args->nthreads; + +#if BFS_WITH_LIBURING + // io_uring uses one fd per ring, ioq uses one ring per thread + if (nthreads >= nopenfd - 1) { + nthreads = nopenfd - 2; + } + nopenfd -= nthreads; +#endif + + bftw_cache_init(&state->cache, nopenfd); + + if (nthreads > 0) { + state->ioq = ioq_create(qdepth, nthreads); + if (!state->ioq) { + return -1; + } + } else { + state->ioq = NULL; + } + state->nthreads = nthreads; + + if (bftw_must_buffer(state)) { + state->flags |= BFTW_BUFFER; + } + + if (state->flags & BFTW_WHITEOUTS) { + state->dir_flags |= BFS_DIR_WHITEOUTS; + } + + SLIST_INIT(&state->to_close); + + enum bftw_qflags qflags = 0; + if (state->strategy != BFTW_BFS) { + qflags |= BFTW_QBUFFER | BFTW_QLIFO; + } + if (state->flags & BFTW_BUFFER) { + qflags |= BFTW_QBUFFER; + } + if (state->flags & BFTW_SORT) { + qflags |= BFTW_QORDER; + } else if (nthreads == 1) { + qflags |= BFTW_QBALANCE; + } + bftw_queue_init(&state->fileq, qflags); + + if (state->strategy == BFTW_BFS || (state->flags & BFTW_BUFFER)) { + // In breadth-first mode, or if we're already buffering files, + // directories can be queued in FIFO order + qflags &= ~(BFTW_QBUFFER | BFTW_QLIFO); + } + bftw_queue_init(&state->dirq, qflags); + + state->path = NULL; + state->file = NULL; + state->previous = NULL; + + state->dir = NULL; + state->de = NULL; + state->direrror = 0; + + return 0; +} + +/** Queue a directory for unwrapping. */ +static void bftw_delayed_unwrap(struct bftw_state *state, struct bftw_file *file) { + bfs_assert(file->dir); + + if (!SLIST_ATTACHED(&state->to_close, file, ready)) { + SLIST_APPEND(&state->to_close, file, ready); + } +} + +/** Unpin a file's parent. */ +static void bftw_unpin_parent(struct bftw_state *state, struct bftw_file *file, bool unwrap) { + struct bftw_file *parent = file->parent; + if (!parent) { + return; + } + + bftw_cache_unpin(&state->cache, parent); + + if (unwrap && parent->dir && parent->pincount == 0) { + bftw_delayed_unwrap(state, parent); + } +} + +/** Pop a response from the I/O queue. */ +static int bftw_ioq_pop(struct bftw_state *state, bool block) { + struct bftw_cache *cache = &state->cache; + struct ioq *ioq = state->ioq; + if (!ioq) { + return -1; + } + + ioq_submit(ioq); + struct ioq_ent *ent = ioq_pop(ioq, block); + if (!ent) { + return -1; + } + + struct bftw_file *file = ent->ptr; + if (file) { + bftw_unpin_parent(state, file, true); + } + + enum ioq_op op = ent->op; + switch (op) { + case IOQ_CLOSE: + ++cache->capacity; + break; + + case IOQ_CLOSEDIR: + ++cache->capacity; + bftw_freedir(cache, ent->closedir.dir); + break; + + case IOQ_OPENDIR: + ++cache->capacity; + + if (ent->result >= 0) { + bftw_file_set_dir(cache, file, ent->opendir.dir); + } else { + bftw_freedir(cache, ent->opendir.dir); + } + + bftw_queue_attach(&state->dirq, file, true); + break; + + case IOQ_STAT: + if (ent->result >= 0) { + bftw_stat_cache(&file->stat_bufs, ent->stat.flags, ent->stat.buf, 0); + } else { + arena_free(&cache->stat_bufs, ent->stat.buf); + bftw_stat_cache(&file->stat_bufs, ent->stat.flags, NULL, -ent->result); + } + + bftw_queue_attach(&state->fileq, file, true); + break; + + default: + bfs_bug("Unexpected ioq op %d", (int)op); + break; + } + + ioq_free(ioq, ent); + return op; +} + +/** Try to reserve space in the I/O queue. */ +static int bftw_ioq_reserve(struct bftw_state *state) { + struct ioq *ioq = state->ioq; + if (!ioq) { + return -1; + } + + if (ioq_capacity(ioq) > 0) { + return 0; + } + + // With more than one background thread, it's faster to wait on + // background I/O than it is to do it on the main thread + bool block = state->nthreads > 1; + if (bftw_ioq_pop(state, block) < 0) { + return -1; + } + + return 0; +} + +/** Try to reserve space in the cache. */ +static int bftw_cache_reserve(struct bftw_state *state) { + struct bftw_cache *cache = &state->cache; + if (cache->capacity > 0) { + return 0; + } + + while (bftw_ioq_pop(state, true) >= 0) { + if (cache->capacity > 0) { + return 0; + } + } + + if (bftw_cache_pop(cache) != 0) { + errno = EMFILE; + return -1; + } + + bfs_assert(cache->capacity > 0); + return 0; +} + +/** Open a bftw_file relative to another one. */ +static int bftw_file_openat(struct bftw_state *state, struct bftw_file *file, struct bftw_file *base, const char *at_path) { + bfs_assert(file->fd < 0); + + struct bftw_cache *cache = &state->cache; int at_fd = AT_FDCWD; if (base) { - bftw_cache_use(cache, base); + bftw_cache_pin(cache, base); at_fd = base->fd; } + int fd = -1; + if (bftw_cache_reserve(state) != 0) { + goto unpin; + } + int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY; - int fd = openat(at_fd, at_path, flags); + fd = openat(at_fd, at_path, flags); if (fd < 0 && errno == EMFILE) { - if (bftw_cache_shrink(cache, base) == 0) { + if (bftw_cache_pop(cache) == 0) { fd = openat(at_fd, at_path, flags); } + cache->capacity = 1; } - if (fd >= 0) { - if (cache->capacity == 0) { - bftw_cache_pop(cache); - } +unpin: + if (base) { + bftw_cache_unpin(cache, base); + } + if (fd >= 0) { file->fd = fd; bftw_cache_add(cache, file); } @@ -322,19 +1145,8 @@ static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, st return fd; } -/** - * Open a bftw_file. - * - * @param cache - * The cache to hold the file. - * @param file - * The file to open. - * @param path - * The full path to the file. - * @return - * The opened file descriptor, or negative on error. - */ -static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) { +/** Open a bftw_file. */ +static int bftw_file_open(struct bftw_state *state, struct bftw_file *file, const char *path) { // Find the nearest open ancestor struct bftw_file *base = file; do { @@ -346,394 +1158,509 @@ static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, cons at_path += bftw_child_nameoff(base); } - int fd = bftw_file_openat(cache, file, base, at_path); - if (fd >= 0 || errno != ENAMETOOLONG) { + int fd = bftw_file_openat(state, file, base, at_path); + if (fd >= 0 || !errno_is_like(ENAMETOOLONG)) { return fd; } // Handle ENAMETOOLONG by manually traversing the path component-by-component + struct bftw_list parents; + SLIST_INIT(&parents); - // Use the ->next linked list to temporarily hold the reversed parent - // chain between base and file - struct bftw_file *cur; - for (cur = file; cur->parent != base; cur = cur->parent) { - cur->parent->next = cur; + // Reverse the chain of parents + for (struct bftw_file *cur = file; cur != base; cur = cur->parent) { + SLIST_PREPEND(&parents, cur); } - // Open the files in the chain one by one - for (base = cur; base; base = base->next) { - fd = bftw_file_openat(cache, base, base->parent, base->name); - if (fd < 0 || base == file) { - break; + // 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); } } - // Clear out the linked list - for (struct bftw_file *next = cur->next; cur != file; cur = next, next = next->next) { - cur->next = NULL; + return file->fd; +} + +/** Close a directory, asynchronously if possible. */ +static int bftw_ioq_closedir(struct bftw_state *state, struct bfs_dir *dir) { + if (bftw_ioq_reserve(state) == 0) { + if (ioq_closedir(state->ioq, dir, NULL) == 0) { + return 0; + } } - return fd; + struct bftw_cache *cache = &state->cache; + int ret = bfs_closedir(dir); + bftw_freedir(cache, dir); + ++cache->capacity; + return ret; } -/** - * Open a bftw_file as a directory. - * - * @param cache - * The cache to hold the file. - * @param file - * The directory to open. - * @param path - * The full path to the directory. - * @return - * The opened directory, or NULL on error. - */ -static struct bfs_dir *bftw_file_opendir(struct bftw_cache *cache, struct bftw_file *file, const char *path) { - int fd = bftw_file_open(cache, file, path); - if (fd < 0) { - return NULL; +/** Close a file descriptor, asynchronously if possible. */ +static int bftw_ioq_close(struct bftw_state *state, int fd) { + if (bftw_ioq_reserve(state) == 0) { + if (ioq_close(state->ioq, fd, NULL) == 0) { + return 0; + } } - return bfs_opendir(fd, NULL); + struct bftw_cache *cache = &state->cache; + int ret = xclose(fd); + ++cache->capacity; + return ret; } -/** Free a bftw_file. */ -static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) { - assert(file->refcount == 0); +/** Close a file, asynchronously if possible. */ +static int bftw_close(struct bftw_state *state, struct bftw_file *file) { + bfs_assert(file->fd >= 0); + bfs_assert(file->pincount == 0); - if (file->fd >= 0) { - bftw_file_close(cache, file); - } + struct bfs_dir *dir = file->dir; + int fd = file->fd; - free(file); + bftw_lru_remove(&state->cache, file); + file->dir = NULL; + file->fd = -1; + + if (dir) { + return bftw_ioq_closedir(state, dir); + } else { + return bftw_ioq_close(state, fd); + } } -/** - * A queue of bftw_file's to examine. - */ -struct bftw_queue { - /** The head of the queue. */ - struct bftw_file *head; - /** The insertion target. */ - struct bftw_file **target; -}; +/** Free an open directory. */ +static int bftw_unwrapdir(struct bftw_state *state, struct bftw_file *file) { + struct bfs_dir *dir = file->dir; + if (!dir) { + return 0; + } -/** Initialize a bftw_queue. */ -static void bftw_queue_init(struct bftw_queue *queue) { - queue->head = NULL; - queue->target = &queue->head; -} + struct bftw_cache *cache = &state->cache; -/** Add a file to a bftw_queue. */ -static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) { - assert(file->next == NULL); + // Try to keep an open fd if any children exist + bool reffed = file->refcount > 1; + // Keep the fd the same if it's pinned + bool pinned = file->pincount > 0; + +#if BFS_USE_UNWRAPDIR + if (reffed || pinned) { + bfs_unwrapdir(dir); + bftw_freedir(cache, dir); + file->dir = NULL; + return 0; + } +#else + if (pinned) { + return -1; + } +#endif + + if (!reffed) { + return bftw_close(state, file); + } + + // Make room for dup() + bftw_cache_pin(cache, file); + int ret = bftw_cache_reserve(state); + bftw_cache_unpin(cache, file); + if (ret != 0) { + return ret; + } + + int fd = dup_cloexec(file->fd); + if (fd < 0) { + return -1; + } + --cache->capacity; - file->next = *queue->target; - *queue->target = file; - queue->target = &file->next; + file->dir = NULL; + file->fd = fd; + return bftw_ioq_closedir(state, dir); } -/** Pop the next file from the head of the queue. */ -static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) { - struct bftw_file *file = queue->head; - queue->head = file->next; - file->next = NULL; - if (queue->target == &file->next) { - queue->target = &queue->head; +/** Try to pin a file's parent. */ +static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) { + struct bftw_file *parent = file->parent; + if (!parent) { + return AT_FDCWD; } - return file; + + int fd = parent->fd; + if (fd < 0) { + // Don't confuse failures with AT_FDCWD + return (int)AT_FDCWD == -1 ? -2 : -1; + } + + bftw_cache_pin(&state->cache, parent); + return fd; } -/** The split phase of mergesort. */ -static struct bftw_file **bftw_sort_split(struct bftw_file **head, struct bftw_file **tail) { - struct bftw_file **tortoise = head, **hare = head; +/** Open a directory asynchronously. */ +static int bftw_ioq_opendir(struct bftw_state *state, struct bftw_file *file) { + struct bftw_cache *cache = &state->cache; - while (*hare != *tail) { - tortoise = &(*tortoise)->next; - hare = &(*hare)->next; - if (*hare != *tail) { - hare = &(*hare)->next; - } + if (bftw_ioq_reserve(state) != 0) { + goto fail; + } + + int dfd = bftw_pin_parent(state, file); + if (dfd < 0 && dfd != (int)AT_FDCWD) { + goto fail; + } + + if (bftw_cache_reserve(state) != 0) { + goto unpin; + } + + struct bfs_dir *dir = bftw_allocdir(cache, false); + if (!dir) { + goto unpin; + } + + if (ioq_opendir(state->ioq, dir, dfd, file->name, state->dir_flags, file) != 0) { + goto free; } - return tortoise; + --cache->capacity; + return 0; + +free: + bftw_freedir(cache, dir); +unpin: + bftw_unpin_parent(state, file, false); +fail: + return -1; } -/** The merge phase of mergesort. */ -static struct bftw_file **bftw_sort_merge(struct bftw_file **head, struct bftw_file **mid, struct bftw_file **tail) { - struct bftw_file *left = *head, *right = *mid, *end = *tail; - *mid = NULL; - *tail = NULL; +/** Open a batch of directories asynchronously. */ +static void bftw_ioq_opendirs(struct bftw_state *state) { + while (bftw_queue_balanced(&state->dirq)) { + struct bftw_file *dir = bftw_queue_waiting(&state->dirq); + if (!dir) { + break; + } - while (left || right) { - struct bftw_file *next; - if (left && (!right || strcoll(left->name, right->name) <= 0)) { - next = left; - left = left->next; + if (bftw_ioq_opendir(state, dir) == 0) { + bftw_queue_detach(&state->dirq, dir, true); } else { - next = right; - right = right->next; + break; } - - *head = next; - head = &next->next; } +} - *head = end; - return head; +/** Push a directory onto the queue. */ +static void bftw_push_dir(struct bftw_state *state, struct bftw_file *file) { + bfs_assert(file->type == BFS_DIR); + bftw_queue_push(&state->dirq, file); + bftw_ioq_opendirs(state); } -/** - * Sort a (sub-)list of files. - * - * @param head - * The head of the (sub-)list to sort. - * @param tail - * The tail of the (sub-)list to sort. - * @return - * The new tail of the (sub-)list. - */ -static struct bftw_file **bftw_sort_files(struct bftw_file **head, struct bftw_file **tail) { - struct bftw_file **mid = bftw_sort_split(head, tail); - if (*mid == *head || *mid == *tail) { - return tail; +/** Pop a file from a queue, then activate it. */ +static bool bftw_pop(struct bftw_state *state, struct bftw_queue *queue) { + if (queue->size == 0) { + return false; } - mid = bftw_sort_files(head, mid); - tail = bftw_sort_files(mid, tail); + while (!bftw_queue_ready(queue) && queue->ioqueued > 0) { + bool block = true; + if (bftw_queue_waiting(queue) && state->nthreads == 1) { + // With only one background thread, balance the work + // between it and the main thread + block = false; + } - return bftw_sort_merge(head, mid, tail); + if (bftw_ioq_pop(state, block) < 0) { + break; + } + } + + struct bftw_file *file = bftw_queue_pop(queue); + if (!file) { + return false; + } + + while (file->ioqueued) { + bftw_ioq_pop(state, true); + } + + state->file = file; + return true; } -/** - * Holds the current state of the bftw() traversal. - */ -struct bftw_state { - /** bftw() callback. */ - bftw_callback *callback; - /** bftw() callback data. */ - void *ptr; - /** bftw() flags. */ - enum bftw_flags flags; - /** Search strategy. */ - enum bftw_strategy strategy; - /** The mount table. */ - const struct bfs_mtab *mtab; +/** Pop a directory to read from the queue. */ +static bool bftw_pop_dir(struct bftw_state *state) { + bfs_assert(!state->file); - /** The appropriate errno value, if any. */ - int error; + if (state->flags & BFTW_SORT) { + // Keep strict breadth-first order when sorting + if (state->strategy == BFTW_BFS && bftw_queue_ready(&state->fileq)) { + return false; + } + } else if (!bftw_queue_ready(&state->dirq)) { + // Don't block if we have files ready to visit + if (bftw_queue_ready(&state->fileq)) { + return false; + } + } - /** The cache of open directories. */ - struct bftw_cache cache; - /** The queue of directories left to explore. */ - struct bftw_queue queue; - /** The start of the current batch of files. */ - struct bftw_file **batch; + return bftw_pop(state, &state->dirq); +} - /** The current path. */ - char *path; - /** The current file. */ - struct bftw_file *file; - /** The previous file. */ - struct bftw_file *previous; +/** Figure out bfs_stat() flags. */ +static enum bfs_stat_flags bftw_stat_flags(const struct bftw_state *state, size_t depth) { + enum bftw_flags mask = BFTW_FOLLOW_ALL; + if (depth == 0) { + mask |= BFTW_FOLLOW_ROOTS; + } - /** The currently open directory. */ - struct bfs_dir *dir; - /** The current directory entry. */ - struct bfs_dirent *de; - /** Storage for the directory entry. */ - struct bfs_dirent de_storage; - /** Any error encountered while reading the directory. */ - int direrror; + if (state->flags & mask) { + return BFS_STAT_TRYFOLLOW; + } else { + return BFS_STAT_NOFOLLOW; + } +} - /** Extra data about the current file. */ - struct BFTW ftwbuf; -}; +/** Check if a stat() call is necessary. */ +static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bfs_type type, const char *name) { + if (state->flags & BFTW_STAT) { + return true; + } -/** - * Initialize the bftw() state. - */ -static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) { - state->callback = args->callback; - state->ptr = args->ptr; - state->flags = args->flags; - state->strategy = args->strategy; - state->mtab = args->mtab; + switch (type) { + case BFS_UNKNOWN: + return true; - state->error = 0; + case BFS_DIR: + return state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS); - if (args->nopenfd < 1) { - errno = EMFILE; - return -1; + case BFS_LNK: + if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) { + return true; + } + _fallthrough; + + default: +#if __linux__ + if (state->mtab && bfs_might_be_mount(state->mtab, name)) { + return true; + } +#endif + return false; } +} - state->path = dstralloc(0); - if (!state->path) { - return -1; +/** stat() a file asynchronously. */ +static int bftw_ioq_stat(struct bftw_state *state, struct bftw_file *file) { + if (bftw_ioq_reserve(state) != 0) { + goto fail; } - bftw_cache_init(&state->cache, args->nopenfd); - bftw_queue_init(&state->queue); - state->batch = NULL; + int dfd = bftw_pin_parent(state, file); + if (dfd < 0 && dfd != (int)AT_FDCWD) { + goto fail; + } - state->file = NULL; - state->previous = NULL; + struct bftw_cache *cache = &state->cache; + struct bfs_stat *buf = arena_alloc(&cache->stat_bufs); + if (!buf) { + goto unpin; + } - state->dir = NULL; - state->de = NULL; - state->direrror = 0; + enum bfs_stat_flags flags = bftw_stat_flags(state, file->depth); + if (ioq_stat(state->ioq, dfd, file->name, flags, buf, file) != 0) { + goto free; + } return 0; + +free: + arena_free(&cache->stat_bufs, buf); +unpin: + bftw_unpin_parent(state, file, false); +fail: + return -1; } -/** Cached bfs_stat(). */ -static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flags flags) { - if (!cache->buf) { - if (cache->error) { - errno = cache->error; - } else if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, &cache->storage) == 0) { - cache->buf = &cache->storage; - } else { - cache->error = errno; - } +/** Check if we should stat() a file asynchronously. */ +static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) { + // 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; + } + +#ifdef S_IFWHT + // ioq_stat() does not do whiteout emulation like bftw_stat_impl() + if (file->type == BFS_WHT) { + return false; } +#endif - return cache->buf; + return bftw_must_stat(state, file->depth, file->type, file->name); } -const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - struct BFTW *mutbuf = (struct BFTW *)ftwbuf; - const struct bfs_stat *ret; +/** Call stat() on files that need it. */ +static void bftw_stat_files(struct bftw_state *state) { + while (true) { + struct bftw_file *file = bftw_queue_waiting(&state->fileq); + if (!file) { + break; + } - if (flags & BFS_STAT_NOFOLLOW) { - ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW); - if (ret && !S_ISLNK(ret->mode) && !mutbuf->stat_cache.buf) { - // Non-link, so share stat info - mutbuf->stat_cache.buf = ret; + if (!bftw_should_ioq_stat(state, file)) { + bftw_queue_skip(&state->fileq, file); + continue; } - } else { - ret = bftw_stat_impl(mutbuf, &mutbuf->stat_cache, BFS_STAT_FOLLOW); - if (!ret && (flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(errno)) { - ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW); + + if (!bftw_queue_balanced(&state->fileq)) { + break; + } + + if (bftw_ioq_stat(state, file) == 0) { + bftw_queue_detach(&state->fileq, file, true); + } else { + break; } } +} - return ret; +/** Push a file onto the queue. */ +static void bftw_push_file(struct bftw_state *state, struct bftw_file *file) { + bftw_queue_push(&state->fileq, file); + bftw_stat_files(state); } -const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - if (flags & BFS_STAT_NOFOLLOW) { - return ftwbuf->lstat_cache.buf; - } else if (ftwbuf->stat_cache.buf) { - return ftwbuf->stat_cache.buf; - } else if ((flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(ftwbuf->stat_cache.error)) { - return ftwbuf->lstat_cache.buf; - } else { - return NULL; - } +/** Pop a file to visit from the queue. */ +static bool bftw_pop_file(struct bftw_state *state) { + bfs_assert(!state->file); + return bftw_pop(state, &state->fileq); } -enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - if (flags & BFS_STAT_NOFOLLOW) { - if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { - return ftwbuf->type; - } - } else if (flags & BFS_STAT_TRYFOLLOW) { - if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) { - return ftwbuf->type; - } - } else { - if (ftwbuf->type != BFS_LNK) { - return ftwbuf->type; - } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) { - return BFS_ERROR; - } +/** 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); +} - const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags); - if (statbuf) { - return bfs_mode_to_type(statbuf->mode); +/** 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 nameoff, namelen; + if (name) { + nameoff = file ? bftw_child_nameoff(file) : 0; + namelen = strlen(name); } else { - return BFS_ERROR; + nameoff = file->nameoff; + namelen = file->namelen; } -} -/** - * Update the path for the current file. - */ -static int bftw_update_path(struct bftw_state *state, const char *name) { - const struct bftw_file *file = state->file; - size_t length = file ? file->nameoff + file->namelen : 0; + size_t pathlen = nameoff + namelen; + if (dstresize(&state->path, pathlen) != 0) { + state->error = errno; + return -1; + } - assert(dstrlen(state->path) >= length); - dstresize(&state->path, length); + // Try to find a common ancestor with the existing path + const struct bftw_file *ancestor = state->previous; + while (ancestor && ancestor->depth > file->depth) { + ancestor = ancestor->parent; + } + // Build the path backwards if (name) { - if (length > 0 && state->path[length - 1] != '/') { - if (dstrapp(&state->path, '/') != 0) { - return -1; - } - } - if (dstrcat(&state->path, name) != 0) { - return -1; + bftw_prepend_path(state->path, nameoff, namelen, name); + } + while (file && file != ancestor) { + bftw_prepend_path(state->path, file->nameoff, file->namelen, file->name); + + if (ancestor && ancestor->depth == file->depth) { + ancestor = ancestor->parent; } + file = file->parent; } + state->previous = state->file; return 0; } -/** Check if a stat() call is needed for this visit. */ -static bool bftw_need_stat(const struct bftw_state *state) { - if (state->flags & BFTW_STAT) { - return true; +/** Open a bftw_file as a directory. */ +static struct bfs_dir *bftw_file_opendir(struct bftw_state *state, struct bftw_file *file, const char *path) { + int fd = bftw_file_open(state, file, path); + if (fd < 0) { + return NULL; } - const struct BFTW *ftwbuf = &state->ftwbuf; - if (ftwbuf->type == BFS_UNKNOWN) { - return true; + struct bftw_cache *cache = &state->cache; + struct bfs_dir *dir = bftw_allocdir(cache, true); + if (!dir) { + return NULL; } - if (ftwbuf->type == BFS_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { - return true; + if (bfs_opendir(dir, fd, NULL, state->dir_flags) != 0) { + bftw_freedir(cache, dir); + return NULL; } - if (ftwbuf->type == BFS_DIR) { - if (state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) { - return true; - } -#if __linux__ - } else if (state->mtab) { - // Linux fills in d_type from the underlying inode, even when - // the directory entry is a bind mount point. In that case, we - // need to stat() to get the correct type. We don't need to - // check for directories because they can only be mounted over - // by other directories. - if (bfs_might_be_mount(state->mtab, ftwbuf->path)) { - return true; - } -#endif + bftw_file_set_dir(cache, file, dir); + return dir; +} + +/** Open the current directory. */ +static int bftw_opendir(struct bftw_state *state) { + bfs_assert(!state->dir); + bfs_assert(!state->de); + + state->direrror = 0; + + struct bftw_file *file = state->file; + state->dir = file->dir; + if (state->dir) { + goto pin; } - return false; + if (bftw_build_path(state, NULL) != 0) { + return -1; + } + + bftw_queue_rebalance(&state->dirq, false); + + state->dir = bftw_file_opendir(state, file, state->path); + if (!state->dir) { + state->direrror = errno; + return 0; + } + +pin: + bftw_cache_pin(&state->cache, file); + return 0; } -/** Initialize bftw_stat cache. */ -static void bftw_stat_init(struct bftw_stat *cache) { - cache->buf = NULL; - cache->error = 0; +/** Read an entry from the current directory. */ +static int bftw_readdir(struct bftw_state *state) { + if (!state->dir) { + return -1; + } + + int ret = bfs_readdir(state->dir, &state->de_storage); + if (ret > 0) { + state->de = &state->de_storage; + } else if (ret == 0) { + state->de = NULL; + } else { + state->de = NULL; + state->direrror = errno; + } + + return ret; } -/** - * Open a file if necessary. - * - * @param file - * The file to open. - * @param path - * The path to that file or one of its descendants. - * @return - * The opened file descriptor, or -1 on error. - */ -static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) { +/** Open a file if necessary. */ +static int bftw_ensure_open(struct bftw_state *state, struct bftw_file *file, const char *path) { int ret = file->fd; if (ret < 0) { @@ -742,16 +1669,14 @@ static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, co return -1; } - ret = bftw_file_open(cache, file, copy); + ret = bftw_file_open(state, file, copy); free(copy); } return ret; } -/** - * Initialize the buffers with data about the current path. - */ +/** Initialize the buffers with data about the current path. */ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { struct bftw_file *file = state->file; const struct bfs_dirent *de = state->de; @@ -763,11 +1688,10 @@ 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; - ftwbuf->stat_flags = BFS_STAT_NOFOLLOW; - bftw_stat_init(&ftwbuf->lstat_cache); - bftw_stat_init(&ftwbuf->stat_cache); + bftw_stat_init(&ftwbuf->stat_bufs, &state->stat_buf, &state->lstat_buf); struct bftw_file *parent = NULL; if (de) { @@ -780,11 +1704,12 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { ftwbuf->depth = file->depth; ftwbuf->type = file->type; ftwbuf->nameoff = file->nameoff; + bftw_stat_fill(&ftwbuf->stat_bufs, &file->stat_bufs); } if (parent) { // Try to ensure the immediate parent is open, to avoid ENAMETOOLONG - if (bftw_ensure_open(&state->cache, parent, state->path) >= 0) { + if (bftw_ensure_open(state, parent, state->path) >= 0) { ftwbuf->at_fd = parent->fd; ftwbuf->at_path += ftwbuf->nameoff; } else { @@ -794,25 +1719,18 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { if (ftwbuf->depth == 0) { // Compute the name offset for root paths like "foo/bar" - ftwbuf->nameoff = xbasename(ftwbuf->path) - ftwbuf->path; + ftwbuf->nameoff = xbaseoff(ftwbuf->path); } + ftwbuf->stat_flags = bftw_stat_flags(state, ftwbuf->depth); + if (ftwbuf->error != 0) { ftwbuf->type = BFS_ERROR; return; } - int follow_flags = BFTW_FOLLOW_ALL; - if (ftwbuf->depth == 0) { - follow_flags |= BFTW_FOLLOW_ROOTS; - } - bool follow = state->flags & follow_flags; - if (follow) { - ftwbuf->stat_flags = BFS_STAT_TRYFOLLOW; - } - const struct bfs_stat *statbuf = NULL; - if (bftw_need_stat(state)) { + if (bftw_must_stat(state, ftwbuf->depth, ftwbuf->type, ftwbuf->path + ftwbuf->nameoff)) { statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (statbuf) { ftwbuf->type = bfs_mode_to_type(statbuf->mode); @@ -828,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; } } @@ -851,24 +1770,18 @@ static bool bftw_is_mount(struct bftw_state *state, const char *name) { return statbuf && statbuf->dev != parent->dev; } -/** Fill file identity information from an ftwbuf. */ -static void bftw_fill_id(struct bftw_file *file, const struct BFTW *ftwbuf) { - const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf; - if (!statbuf || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) { - statbuf = ftwbuf->lstat_cache.buf; - } - if (statbuf) { - file->dev = statbuf->dev; - file->ino = statbuf->ino; - } +/** Check if bfs_stat() was called from the main thread. */ +static bool bftw_stat_was_sync(const struct bftw_state *state, const struct bfs_stat *buf) { + return buf == &state->stat_buf || buf == &state->lstat_buf; } -/** - * Visit a path, invoking the callback. - */ -static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, enum bftw_visit visit) { - if (bftw_update_path(state, name) != 0) { - state->error = errno; +/** Invoke the callback. */ +static enum bftw_action bftw_call_back(struct bftw_state *state, const char *name, enum bftw_visit visit) { + if (visit == BFTW_POST && !(state->flags & BFTW_POST_ORDER)) { + return BFTW_PRUNE; + } + + if (bftw_build_path(state, name) != 0) { return BFTW_STOP; } @@ -881,246 +1794,282 @@ static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, e return BFTW_STOP; } + enum bftw_action ret = BFTW_PRUNE; if ((state->flags & BFTW_SKIP_MOUNTS) && bftw_is_mount(state, name)) { - return BFTW_PRUNE; + goto done; } - enum bftw_action ret = state->callback(ftwbuf, state->ptr); + ret = state->callback(ftwbuf, state->ptr); switch (ret) { case BFTW_CONTINUE: + if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) { + ret = BFTW_PRUNE; + } else if (state->flags & BFTW_PRUNE_MOUNTS) { + if (bftw_is_mount(state, name)) { + ret = BFTW_PRUNE; + } + } break; + case BFTW_PRUNE: case BFTW_STOP: - goto done; + break; + default: state->error = EINVAL; return BFTW_STOP; } - if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) { - ret = BFTW_PRUNE; - goto done; - } - - if ((state->flags & BFTW_PRUNE_MOUNTS) && bftw_is_mount(state, name)) { - ret = BFTW_PRUNE; - goto done; - } - done: - if (state->file && !name) { - bftw_fill_id(state->file, ftwbuf); + if (state->fileq.flags & BFTW_QBALANCE) { + // Detect any main-thread stat() calls to rebalance the queue + const struct bfs_stat *buf = bftw_cached_stat(ftwbuf, BFS_STAT_FOLLOW); + const struct bfs_stat *lbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW); + if (bftw_stat_was_sync(state, buf) || bftw_stat_was_sync(state, lbuf)) { + bftw_queue_rebalance(&state->fileq, false); + } } return ret; } /** - * Push a new file onto the queue. + * Flags controlling which files get visited when done with a directory. */ -static int bftw_push(struct bftw_state *state, const char *name, bool fill_id) { - struct bftw_file *parent = state->file; - struct bftw_file *file = bftw_file_new(parent, name); - if (!file) { - state->error = errno; - return -1; - } +enum bftw_gc_flags { + /** Don't visit anything. */ + BFTW_VISIT_NONE = 0, + /** Report directory errors. */ + BFTW_VISIT_ERROR = 1 << 0, + /** Visit the file itself. */ + BFTW_VISIT_FILE = 1 << 1, + /** Visit the file's ancestors. */ + BFTW_VISIT_PARENTS = 1 << 2, + /** Visit both the file and its ancestors. */ + BFTW_VISIT_ALL = BFTW_VISIT_ERROR | BFTW_VISIT_FILE | BFTW_VISIT_PARENTS, +}; - if (state->de) { - file->type = state->de->type; - } +/** Garbage collect the current file and its parents. */ +static int bftw_gc(struct bftw_state *state, enum bftw_gc_flags flags) { + int ret = 0; - if (fill_id) { - bftw_fill_id(file, &state->ftwbuf); + struct bftw_file *file = state->file; + if (file) { + if (state->dir) { + bftw_cache_unpin(&state->cache, file); + } + if (file->dir) { + bftw_delayed_unwrap(state, file); + } } + state->dir = NULL; + state->de = NULL; - bftw_queue_push(&state->queue, file); - - return 0; -} - -/** - * Build the path to the current file. - */ -static int bftw_build_path(struct bftw_state *state) { - const struct bftw_file *file = state->file; - - size_t pathlen = file->nameoff + file->namelen; - if (dstresize(&state->path, pathlen) != 0) { - state->error = errno; - return -1; + if (state->direrror != 0) { + if (flags & BFTW_VISIT_ERROR) { + if (bftw_call_back(state, NULL, BFTW_PRE) == BFTW_STOP) { + ret = -1; + flags = 0; + } + } else { + state->error = state->direrror; + } } + state->direrror = 0; - // Try to find a common ancestor with the existing path - const struct bftw_file *ancestor = state->previous; - while (ancestor && ancestor->depth > file->depth) { - ancestor = ancestor->parent; + drain_slist (struct bftw_file, dead, &state->to_close, ready) { + bftw_unwrapdir(state, dead); } - // Build the path backwards - while (file && file != ancestor) { - if (file->nameoff > 0) { - state->path[file->nameoff - 1] = '/'; + enum bftw_gc_flags visit = BFTW_VISIT_FILE; + while ((file = state->file)) { + if (--file->refcount > 0) { + state->file = NULL; + break; } - memcpy(state->path + file->nameoff, file->name, file->namelen); - if (ancestor && ancestor->depth == file->depth) { - ancestor = ancestor->parent; + if (flags & visit) { + if (bftw_call_back(state, NULL, BFTW_POST) == BFTW_STOP) { + ret = -1; + flags = 0; + } } - file = file->parent; + visit = BFTW_VISIT_PARENTS; + + struct bftw_file *parent = file->parent; + if (state->previous == file) { + state->previous = parent; + } + state->file = parent; + + if (file->fd >= 0) { + bftw_close(state, file); + } + bftw_file_free(&state->cache, file); } - state->previous = state->file; - return 0; + return ret; } -/** - * Pop the next file from the queue. - */ -static int bftw_pop(struct bftw_state *state) { - if (!state->queue.head) { - return 0; +/** Sort a bftw_list by filename. */ +static void bftw_list_sort(struct bftw_list *list) { + if (!list->head || !list->head->next) { + return; } - state->file = bftw_queue_pop(&state->queue); + struct bftw_list left, right; + SLIST_INIT(&left); + SLIST_INIT(&right); - if (bftw_build_path(state) != 0) { - return -1; + // Split + for (struct bftw_file *hare = list->head; hare && (hare = hare->next); hare = hare->next) { + struct bftw_file *tortoise = SLIST_POP(list); + SLIST_APPEND(&left, tortoise); } + SLIST_EXTEND(&right, list); - return 1; + // Recurse + bftw_list_sort(&left); + bftw_list_sort(&right); + + // Merge + while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) { + struct bftw_file *lf = left.head; + struct bftw_file *rf = right.head; + + if (strcoll(lf->name, rf->name) <= 0) { + SLIST_POP(&left); + SLIST_APPEND(list, lf); + } else { + SLIST_POP(&right); + SLIST_APPEND(list, rf); + } + } + SLIST_EXTEND(list, &left); + SLIST_EXTEND(list, &right); } -/** - * Open the current directory. - */ -static void bftw_opendir(struct bftw_state *state) { - assert(!state->dir); - assert(!state->de); +/** Flush all the queue buffers. */ +static void bftw_flush(struct bftw_state *state) { + if (state->flags & BFTW_SORT) { + bftw_list_sort(&state->fileq.buffer); + } + bftw_queue_flush(&state->fileq); + bftw_stat_files(state); - state->direrror = 0; + bftw_queue_flush(&state->dirq); + bftw_ioq_opendirs(state); - state->dir = bftw_file_opendir(&state->cache, state->file, state->path); - if (!state->dir) { - state->direrror = errno; + if (state->ioq) { + ioq_submit(state->ioq); } } -/** - * Read an entry from the current directory. - */ -static int bftw_readdir(struct bftw_state *state) { - if (!state->dir) { +/** Close the current directory. */ +static int bftw_closedir(struct bftw_state *state) { + if (bftw_gc(state, BFTW_VISIT_ALL) != 0) { return -1; } - int ret = bfs_readdir(state->dir, &state->de_storage); - if (ret > 0) { - state->de = &state->de_storage; - } else if (ret == 0) { - state->de = NULL; - } else { - state->de = NULL; - state->direrror = errno; - } - - return ret; + bftw_flush(state); + return 0; } -/** - * Flags controlling which files get visited when done with a directory. - */ -enum bftw_gc_flags { - /** Don't visit anything. */ - BFTW_VISIT_NONE = 0, - /** Visit the file itself. */ - BFTW_VISIT_FILE = 1 << 0, - /** Visit the file's ancestors. */ - BFTW_VISIT_PARENTS = 1 << 1, - /** Visit both the file and its ancestors. */ - BFTW_VISIT_ALL = BFTW_VISIT_FILE | BFTW_VISIT_PARENTS, -}; - -/** - * Close the current directory. - */ -static enum bftw_action bftw_closedir(struct bftw_state *state, enum bftw_gc_flags flags) { - struct bftw_file *file = state->file; - enum bftw_action ret = BFTW_CONTINUE; +/** Fill file identity information from an ftwbuf. */ +static void bftw_save_ftwbuf(struct bftw_file *file, const struct BFTW *ftwbuf) { + file->type = ftwbuf->type; - if (state->dir) { - assert(file->fd >= 0); + const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, ftwbuf->stat_flags); + if (statbuf) { + file->dev = statbuf->dev; + file->ino = statbuf->ino; + } +} - if (file->refcount > 1) { - // Keep the fd around if any subdirectories exist - file->fd = bfs_freedir(state->dir); - } else { - bfs_closedir(state->dir); - file->fd = -1; - } +/** Check if we should buffer a file instead of visiting it. */ +static bool bftw_buffer_file(const struct bftw_state *state, const struct bftw_file *file, const char *name) { + if (!name) { + // Already buffered + return false; + } - if (file->fd < 0) { - bftw_cache_remove(&state->cache, file); - } + if (state->flags & BFTW_BUFFER) { + return true; } - state->de = NULL; - state->dir = NULL; + // If we need to call stat(), and can do it async, buffer this file + if (!state->ioq) { + return false; + } - if (state->direrror != 0) { - if (flags & BFTW_VISIT_FILE) { - ret = bftw_visit(state, NULL, BFTW_PRE); - } else { - state->error = state->direrror; - } - state->direrror = 0; + if (!bftw_queue_balanced(&state->fileq)) { + // stat() would run synchronously anyway + return false; } - return ret; + size_t depth = file ? file->depth + 1 : 1; + enum bfs_type type = state->de ? state->de->type : BFS_UNKNOWN; + return bftw_must_stat(state, depth, type, name); } -/** - * Finalize and free a file we're done with. - */ -static enum bftw_action bftw_gc_file(struct bftw_state *state, enum bftw_gc_flags flags) { - enum bftw_action ret = BFTW_CONTINUE; +/** Visit and/or enqueue the current file. */ +static int bftw_visit(struct bftw_state *state, const char *name) { + struct bftw_cache *cache = &state->cache; + struct bftw_file *file = state->file; + + if (bftw_buffer_file(state, file, name)) { + file = bftw_file_new(cache, file, name); + if (!file) { + state->error = errno; + return -1; + } - if (!(state->flags & BFTW_POST_ORDER)) { - flags = 0; + if (state->de) { + file->type = state->de->type; + } + + bftw_push_file(state, file); + return 0; } - bool visit = flags & BFTW_VISIT_FILE; - while (state->file) { - struct bftw_file *file = state->file; - if (--file->refcount > 0) { + switch (bftw_call_back(state, name, BFTW_PRE)) { + case BFTW_CONTINUE: + if (name) { + file = bftw_file_new(cache, state->file, name); + } else { state->file = NULL; - break; } + if (!file) { + state->error = errno; + return -1; + } + + bftw_save_ftwbuf(file, &state->ftwbuf); + bftw_stat_recycle(cache, file); + bftw_push_dir(state, file); + return 0; - if (visit && bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) { - ret = BFTW_STOP; - flags &= ~BFTW_VISIT_PARENTS; + case BFTW_PRUNE: + if (file && !name) { + return bftw_gc(state, BFTW_VISIT_PARENTS); + } else { + return 0; } - visit = flags & BFTW_VISIT_PARENTS; - struct bftw_file *parent = file->parent; - if (state->previous == file) { - state->previous = parent; + default: + if (file && !name) { + bftw_gc(state, BFTW_VISIT_NONE); } - bftw_file_free(&state->cache, file); - state->file = parent; + return -1; } - - return ret; } -/** - * Drain all the entries from a bftw_queue. - */ -static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue) { - while (queue->head) { - state->file = bftw_queue_pop(queue); - bftw_gc_file(state, BFTW_VISIT_NONE); +/** Drain a bftw_queue. */ +static void bftw_drain(struct bftw_state *state, struct bftw_queue *queue) { + bftw_queue_flush(queue); + + while (bftw_pop(state, queue)) { + bftw_gc(state, BFTW_VISIT_NONE); } } @@ -1133,10 +2082,18 @@ static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue) static int bftw_state_destroy(struct bftw_state *state) { dstrfree(state->path); - bftw_closedir(state, BFTW_VISIT_NONE); + struct ioq *ioq = state->ioq; + if (ioq) { + ioq_cancel(ioq); + while (bftw_ioq_pop(state, true) >= 0); + state->ioq = NULL; + } + + bftw_gc(state, BFTW_VISIT_NONE); + bftw_drain(state, &state->dirq); + bftw_drain(state, &state->fileq); - bftw_gc_file(state, BFTW_VISIT_NONE); - bftw_drain_queue(state, &state->queue); + ioq_destroy(ioq); bftw_cache_destroy(&state->cache); @@ -1144,152 +2101,63 @@ static int bftw_state_destroy(struct bftw_state *state) { return state->error ? -1 : 0; } -/** Start a batch of files. */ -static void bftw_batch_start(struct bftw_state *state) { - if (state->strategy == BFTW_DFS) { - state->queue.target = &state->queue.head; - } - state->batch = state->queue.target; -} - -/** Finish adding a batch of files. */ -static void bftw_batch_finish(struct bftw_state *state) { - if (state->flags & BFTW_SORT) { - state->queue.target = bftw_sort_files(state->batch, state->queue.target); - } -} - /** - * Streaming mode: visit files as they are encountered. + * Shared implementation for all search strategies. */ -static int bftw_stream(const struct bftw_args *args) { - struct bftw_state state; - if (bftw_state_init(&state, args) != 0) { - return -1; - } - - assert(!(state.flags & (BFTW_SORT | BFTW_BUFFER))); - - bftw_batch_start(&state); - for (size_t i = 0; i < args->npaths; ++i) { - const char *path = args->paths[i]; - - switch (bftw_visit(&state, path, BFTW_PRE)) { - case BFTW_CONTINUE: - break; - case BFTW_PRUNE: - continue; - case BFTW_STOP: - goto done; - } - - if (bftw_push(&state, path, true) != 0) { - goto done; +static int bftw_impl(struct bftw_state *state) { + for (size_t i = 0; i < state->npaths; ++i) { + if (bftw_visit(state, state->paths[i]) != 0) { + return -1; } } - bftw_batch_finish(&state); + bftw_flush(state); - while (bftw_pop(&state) > 0) { - bftw_opendir(&state); - - bftw_batch_start(&state); - while (bftw_readdir(&state) > 0) { - const char *name = state.de->name; - - switch (bftw_visit(&state, name, BFTW_PRE)) { - case BFTW_CONTINUE: - break; - case BFTW_PRUNE: - continue; - case BFTW_STOP: - goto done; + while (true) { + while (bftw_pop_dir(state)) { + if (bftw_opendir(state) != 0) { + return -1; } - - if (bftw_push(&state, name, true) != 0) { - goto done; + while (bftw_readdir(state) > 0) { + if (bftw_visit(state, state->de->name) != 0) { + return -1; + } + } + if (bftw_closedir(state) != 0) { + return -1; } } - bftw_batch_finish(&state); - if (bftw_closedir(&state, BFTW_VISIT_ALL) == BFTW_STOP) { - goto done; + if (!bftw_pop_file(state)) { + break; } - if (bftw_gc_file(&state, BFTW_VISIT_ALL) == BFTW_STOP) { - goto done; + if (bftw_visit(state, NULL) != 0) { + return -1; } + bftw_flush(state); } -done: - return bftw_state_destroy(&state); + return 0; } /** - * Batching mode: queue up all children before visiting them. + * bftw() implementation for simple breadth-/depth-first search. */ -static int bftw_batch(const struct bftw_args *args) { +static int bftw_walk(const struct bftw_args *args) { struct bftw_state state; if (bftw_state_init(&state, args) != 0) { return -1; } - bftw_batch_start(&state); - for (size_t i = 0; i < args->npaths; ++i) { - if (bftw_push(&state, args->paths[i], false) != 0) { - goto done; - } - } - bftw_batch_finish(&state); - - while (bftw_pop(&state) > 0) { - enum bftw_gc_flags gcflags = BFTW_VISIT_ALL; - - switch (bftw_visit(&state, NULL, BFTW_PRE)) { - case BFTW_CONTINUE: - break; - case BFTW_PRUNE: - gcflags &= ~BFTW_VISIT_FILE; - goto next; - case BFTW_STOP: - goto done; - } - - bftw_opendir(&state); - - bftw_batch_start(&state); - while (bftw_readdir(&state) > 0) { - if (bftw_push(&state, state.de->name, false) != 0) { - goto done; - } - } - bftw_batch_finish(&state); - - if (bftw_closedir(&state, gcflags) == BFTW_STOP) { - goto done; - } - - next: - if (bftw_gc_file(&state, gcflags) == BFTW_STOP) { - goto done; - } - } - -done: + bftw_impl(&state); return bftw_state_destroy(&state); } -/** Select bftw_stream() or bftw_batch() appropriately. */ -static int bftw_auto(const struct bftw_args *args) { - if (args->flags & (BFTW_SORT | BFTW_BUFFER)) { - return bftw_batch(args); - } else { - return bftw_stream(args); - } -} - /** * Iterative deepening search state. */ struct bftw_ids_state { + /** Nested walk state. */ + struct bftw_state nested; /** The wrapped callback. */ bftw_callback *delegate; /** The wrapped callback arguments. */ @@ -1304,12 +2172,8 @@ struct bftw_ids_state { size_t max_depth; /** The set of pruned paths. */ struct trie pruned; - /** An error code to report. */ - int error; /** Whether the bottom has been found. */ bool bottom; - /** Whether to quit the search. */ - bool quit; }; /** Iterative deepening callback function. */ @@ -1353,17 +2217,17 @@ static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr) ret = BFTW_PRUNE; } break; + case BFTW_PRUNE: if (ftwbuf->type == BFS_DIR) { if (!trie_insert_str(&state->pruned, ftwbuf->path)) { - state->error = errno; - state->quit = true; + state->nested.error = errno; ret = BFTW_STOP; } } break; + case BFTW_STOP: - state->quit = true; break; } @@ -1371,7 +2235,7 @@ static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr) } /** Initialize iterative deepening state. */ -static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *state, struct bftw_args *ids_args) { +static int bftw_ids_init(struct bftw_ids_state *state, const struct bftw_args *args) { state->delegate = args->callback; state->ptr = args->ptr; state->visit = BFTW_PRE; @@ -1379,31 +2243,19 @@ static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *s state->min_depth = 0; state->max_depth = 1; trie_init(&state->pruned); - state->error = 0; state->bottom = false; - state->quit = false; - *ids_args = *args; - ids_args->callback = bftw_ids_callback; - ids_args->ptr = state; - ids_args->flags &= ~BFTW_POST_ORDER; - ids_args->strategy = BFTW_DFS; + struct bftw_args ids_args = *args; + ids_args.callback = bftw_ids_callback; + ids_args.ptr = state; + ids_args.flags &= ~BFTW_POST_ORDER; + return bftw_state_init(&state->nested, &ids_args); } /** Finish an iterative deepening search. */ -static int bftw_ids_finish(struct bftw_ids_state *state) { - int ret = 0; - - if (state->error) { - ret = -1; - } else { - state->error = errno; - } - +static int bftw_ids_destroy(struct bftw_ids_state *state) { trie_destroy(&state->pruned); - - errno = state->error; - return ret; + return bftw_state_destroy(&state->nested); } /** @@ -1411,15 +2263,15 @@ static int bftw_ids_finish(struct bftw_ids_state *state) { */ static int bftw_ids(const struct bftw_args *args) { struct bftw_ids_state state; - struct bftw_args ids_args; - bftw_ids_init(args, &state, &ids_args); + if (bftw_ids_init(&state, args) != 0) { + return -1; + } - while (!state.quit && !state.bottom) { + while (!state.bottom) { state.bottom = true; - if (bftw_auto(&ids_args) != 0) { - state.error = errno; - state.quit = true; + if (bftw_impl(&state.nested) != 0) { + goto done; } ++state.min_depth; @@ -1430,18 +2282,18 @@ static int bftw_ids(const struct bftw_args *args) { state.visit = BFTW_POST; state.force_visit = true; - while (!state.quit && state.min_depth > 0) { + while (state.min_depth > 0) { --state.max_depth; --state.min_depth; - if (bftw_auto(&ids_args) != 0) { - state.error = errno; - state.quit = true; + if (bftw_impl(&state.nested) != 0) { + goto done; } } } - return bftw_ids_finish(&state); +done: + return bftw_ids_destroy(&state); } /** @@ -1449,40 +2301,38 @@ static int bftw_ids(const struct bftw_args *args) { */ static int bftw_eds(const struct bftw_args *args) { struct bftw_ids_state state; - struct bftw_args ids_args; - bftw_ids_init(args, &state, &ids_args); + if (bftw_ids_init(&state, args) != 0) { + return -1; + } - while (!state.quit && !state.bottom) { + while (!state.bottom) { state.bottom = true; - if (bftw_auto(&ids_args) != 0) { - state.error = errno; - state.quit = true; + if (bftw_impl(&state.nested) != 0) { + goto done; } state.min_depth = state.max_depth; state.max_depth *= 2; } - if (!state.quit && (args->flags & BFTW_POST_ORDER)) { + if (args->flags & BFTW_POST_ORDER) { state.visit = BFTW_POST; state.min_depth = 0; - ids_args.flags |= BFTW_POST_ORDER; + state.nested.flags |= BFTW_POST_ORDER; - if (bftw_auto(&ids_args) != 0) { - state.error = errno; - } + bftw_impl(&state.nested); } - return bftw_ids_finish(&state); +done: + return bftw_ids_destroy(&state); } int bftw(const struct bftw_args *args) { switch (args->strategy) { case BFTW_BFS: - return bftw_auto(args); case BFTW_DFS: - return bftw_batch(args); + return bftw_walk(args); case BFTW_IDS: return bftw_ids(args); case BFTW_EDS: @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A file-walking API based on nftw(). @@ -23,6 +10,7 @@ #include "dir.h" #include "stat.h" + #include <stddef.h> /** @@ -39,12 +27,14 @@ enum bftw_visit { * Cached bfs_stat() info for a file. */ struct bftw_stat { - /** A pointer to the bfs_stat() buffer, if available. */ - const struct bfs_stat *buf; - /** Storage for the bfs_stat() buffer, if needed. */ - struct bfs_stat storage; - /** The cached error code, if any. */ - int error; + /** The bfs_stat(BFS_STAT_FOLLOW) buffer. */ + const struct bfs_stat *stat_buf; + /** The bfs_stat(BFS_STAT_NOFOLLOW) buffer. */ + const struct bfs_stat *lstat_buf; + /** The cached bfs_stat(BFS_STAT_FOLLOW) error. */ + int stat_err; + /** The cached bfs_stat(BFS_STAT_NOFOLLOW) error. */ + int lstat_err; }; /** @@ -65,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; @@ -75,19 +67,17 @@ struct BFTW { /** Flags for bfs_stat(). */ enum bfs_stat_flags stat_flags; - /** Cached bfs_stat() info for BFS_STAT_NOFOLLOW. */ - struct bftw_stat lstat_cache; - /** Cached bfs_stat() info for BFS_STAT_FOLLOW. */ - struct bftw_stat stat_cache; + /** Cached bfs_stat() info. */ + struct bftw_stat stat_bufs; }; /** * 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. @@ -98,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. @@ -112,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); @@ -136,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. @@ -169,6 +159,8 @@ enum bftw_flags { BFTW_SORT = 1 << 8, /** Read each directory into memory before processing its children. */ BFTW_BUFFER = 1 << 9, + /** Include whiteouts in the search results. */ + BFTW_WHITEOUTS = 1 << 10, }; /** @@ -193,16 +185,22 @@ struct bftw_args { const char **paths; /** The number of starting paths. */ size_t npaths; + /** The callback to invoke. */ bftw_callback *callback; /** A pointer which is passed to the callback. */ void *ptr; + /** The maximum number of file descriptors to keep open. */ int nopenfd; + /** The maximum number of threads to use. */ + int nthreads; + /** Flags that control bftw() behaviour. */ enum bftw_flags flags; /** The search strategy to use. */ enum bftw_strategy strategy; + /** The parsed mount table, if available. */ const struct bfs_mtab *mtab; }; @@ -213,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. diff --git a/src/bit.h b/src/bit.h new file mode 100644 index 0000000..5d6fb9d --- /dev/null +++ b/src/bit.h @@ -0,0 +1,473 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Bits & bytes. + */ + +#ifndef BFS_BIT_H +#define BFS_BIT_H + +#include "bfs.h" + +#include <limits.h> +#include <stdint.h> + +#if __has_include(<stdbit.h>) +# include <stdbit.h> +#endif + +// C23 polyfill: _WIDTH macros + +// The U*_MAX macros are of the form 2**n - 1, and we want to extract the n. +// One way would be *_WIDTH = popcount(*_MAX). Alternatively, we can use +// Hallvard B. Furuseth's technique from [1], which is shorter. +// +// [1]: https://groups.google.com/g/comp.lang.c/c/NfedEFBFJ0k + +// Let mask be of the form 2**m - 1, e.g. 0b111, and let n range over +// [0b0, 0b1, 0b11, 0b111, 0b1111, ...]. Then we have +// +// n % 0b111 +// == [0b0, 0b1, 0b11, 0b0, 0b1, 0b11, ...] +// n / (n % 0b111 + 1) +// == [0b0 (x3), 0b111 (x3), 0b111111 (x3), ...] +// n / (n % 0b111 + 1) / 0b111 +// == [0b0 (x3), 0b1 (x3), 0b1001 (x3), 0b1001001 (x3), ...] +// n / (n % 0b111 + 1) / 0b111 % 0b111 +// == [0 (x3), 1 (x3), 2 (x3), ...] +// == UMAX_CHUNK(n, 0b111) +#define UMAX_CHUNK(n, mask) (n / (n % mask + 1) / mask % mask) + +// 8 * UMAX_CHUNK(n, 255) gives [0 (x8), 8 (x8), 16 (x8), ...]. To that we add +// [0, 1, 2, ..., 6, 7, 0, 1, ...], which we get from a linear interpolation on +// n % 255: +// +// n % 255 +// == [0, 1, 3, 7, 15, 31, 63, 127, 0, ...] +// 86 / (n % 255 + 12) +// == [7, 6, 5, 4, 3, 2, 1, 0, 7, ...] +#define UMAX_INTERP(n) (7 - 86 / (n % 255 + 12)) + +#define UMAX_WIDTH(n) (8 * UMAX_CHUNK(n, 255) + UMAX_INTERP(n)) + +#ifndef CHAR_WIDTH +# define CHAR_WIDTH CHAR_BIT +#endif + +// See https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html + +#ifndef USHRT_WIDTH +# ifdef __SHRT_WIDTH__ +# define USHRT_WIDTH __SHRT_WIDTH__ +# else +# define USHRT_WIDTH UMAX_WIDTH(USHRT_MAX) +# endif +#endif + +#ifndef UINT_WIDTH +# ifdef __INT_WIDTH__ +# define UINT_WIDTH __INT_WIDTH__ +# else +# define UINT_WIDTH UMAX_WIDTH(UINT_MAX) +# endif +#endif + +#ifndef ULONG_WIDTH +# ifdef __LONG_WIDTH__ +# define ULONG_WIDTH __LONG_WIDTH__ +# else +# define ULONG_WIDTH UMAX_WIDTH(ULONG_MAX) +# endif +#endif + +#ifndef ULLONG_WIDTH +# ifdef __LONG_LONG_WIDTH__ +# define ULLONG_WIDTH __LONG_LONG_WIDTH__ +# elif defined(__LLONG_WIDTH__) // Clang +# define ULLONG_WIDTH __LLONG_WIDTH__ +# else +# define ULLONG_WIDTH UMAX_WIDTH(ULLONG_MAX) +# endif +#endif + +#ifndef SIZE_WIDTH +# ifdef __SIZE_WIDTH__ +# define SIZE_WIDTH __SIZE_WIDTH__ +# else +# define SIZE_WIDTH UMAX_WIDTH(SIZE_MAX) +# endif +#endif + +#ifndef PTRDIFF_WIDTH +# ifdef __PTRDIFF_WIDTH__ +# define PTRDIFF_WIDTH __PTRDIFF_WIDTH__ +# else +# define PTRDIFF_WIDTH UMAX_WIDTH(PTRDIFF_MAX) +# endif +#endif + +#ifndef UINTPTR_WIDTH +# ifdef __INTPTR_WIDTH__ +# define UINTPTR_WIDTH __INTPTR_WIDTH__ +# else +# define UINTPTR_WIDTH UMAX_WIDTH(UINTPTR_MAX) +# endif +#endif + +#ifndef UINTMAX_WIDTH +# ifdef __INTMAX_WIDTH__ +# define UINTMAX_WIDTH __INTMAX_WIDTH__ +# else +# define UINTMAX_WIDTH UMAX_WIDTH(UINTMAX_MAX) +# endif +#endif + +#ifndef UCHAR_WIDTH +# define UCHAR_WIDTH CHAR_WIDTH +#endif +#ifndef SCHAR_WIDTH +# define SCHAR_WIDTH CHAR_WIDTH +#endif +#ifndef SHRT_WIDTH +# define SHRT_WIDTH USHRT_WIDTH +#endif +#ifndef INT_WIDTH +# define INT_WIDTH UINT_WIDTH +#endif +#ifndef LONG_WIDTH +# define LONG_WIDTH ULONG_WIDTH +#endif +#ifndef LLONG_WIDTH +# define LLONG_WIDTH ULLONG_WIDTH +#endif +#ifndef INTPTR_WIDTH +# define INTPTR_WIDTH UINTPTR_WIDTH +#endif +#ifndef INTMAX_WIDTH +# define INTMAX_WIDTH UINTMAX_WIDTH +#endif + +// N3022 polyfill: byte order + +#ifdef __STDC_ENDIAN_LITTLE__ +# define ENDIAN_LITTLE __STDC_ENDIAN_LITTLE__ +#elif defined(__ORDER_LITTLE_ENDIAN__) +# define ENDIAN_LITTLE __ORDER_LITTLE_ENDIAN__ +#else +# define ENDIAN_LITTLE 1234 +#endif + +#ifdef __STDC_ENDIAN_BIG__ +# define ENDIAN_BIG __STDC_ENDIAN_BIG__ +#elif defined(__ORDER_BIG_ENDIAN__) +# define ENDIAN_BIG __ORDER_BIG_ENDIAN__ +#else +# define ENDIAN_BIG 4321 +#endif + +#ifdef __STDC_ENDIAN_NATIVE__ +# define ENDIAN_NATIVE __STDC_ENDIAN_NATIVE__ +#elif defined(__BYTE_ORDER__) +# define ENDIAN_NATIVE __BYTE_ORDER__ +#else +# define ENDIAN_NATIVE 0 +#endif + +#if __GNUC__ +# define bswap_u16 __builtin_bswap16 +# define bswap_u32 __builtin_bswap32 +# define bswap_u64 __builtin_bswap64 +#else + +static inline uint16_t bswap_u16(uint16_t n) { + return (n << 8) | (n >> 8); +} + +static inline uint32_t bswap_u32(uint32_t n) { + return ((uint32_t)bswap_u16(n) << 16) | bswap_u16(n >> 16); +} + +static inline uint64_t bswap_u64(uint64_t n) { + return ((uint64_t)bswap_u32(n) << 32) | bswap_u32(n >> 32); +} + +#endif + +static inline uint8_t bswap_u8(uint8_t n) { + return 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) \ + macro(unsigned char, _uc, UCHAR_WIDTH) \ + macro(unsigned short, _us, USHRT_WIDTH) \ + macro(unsigned int, _ui, UINT_WIDTH) \ + macro(unsigned long, _ul, ULONG_WIDTH) \ + macro(unsigned long long, _ull, ULLONG_WIDTH) + +// Select an overload based on an unsigned integer type +#define UINT_SELECT(n, name) \ + _Generic((n), \ + unsigned char: name##_uc, \ + unsigned short: name##_us, \ + unsigned int: name##_ui, \ + unsigned long: name##_ul, \ + 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_STDBIT_H__ >= C23 +# define count_ones stdc_count_ones +# define count_zeros stdc_count_zeros +# define leading_zeros stdc_leading_zeros +# define leading_ones stdc_leading_ones +# define trailing_zeros stdc_trailing_zeros +# define trailing_ones stdc_trailing_ones +# define first_leading_zero stdc_first_leading_zero +# define first_leading_one stdc_first_leading_one +# define first_trailing_zero stdc_first_trailing_zero +# define first_trailing_one stdc_first_trailing_one +# define has_single_bit stdc_has_single_bit +# define bit_width stdc_bit_width +# define bit_ceil stdc_bit_ceil +# define bit_floor stdc_bit_floor +#else + +#if __GNUC__ + +// GCC provides builtins for unsigned {int,long,long long}, so promote char/short +#define UINT_BUILTIN_uc(name) __builtin_##name +#define UINT_BUILTIN_us(name) __builtin_##name +#define UINT_BUILTIN_ui(name) __builtin_##name +#define UINT_BUILTIN_ul(name) __builtin_##name##l +#define UINT_BUILTIN_ull(name) __builtin_##name##ll +#define UINT_BUILTIN(name, suffix) UINT_BUILTIN##suffix(name) + +#define BUILTIN_WIDTH_uc UINT_WIDTH +#define BUILTIN_WIDTH_us UINT_WIDTH +#define BUILTIN_WIDTH_ui UINT_WIDTH +#define BUILTIN_WIDTH_ul ULONG_WIDTH +#define BUILTIN_WIDTH_ull ULLONG_WIDTH +#define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix + +#define COUNT_ONES(type, suffix, width) \ + static inline unsigned int count_ones##suffix(type n) { \ + return UINT_BUILTIN(popcount, suffix)(n); \ + } + +#define LEADING_ZEROS(type, suffix, width) \ + 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 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 unsigned int first_trailing_one##suffix(type n) { \ + return UINT_BUILTIN(ffs, suffix)(n); \ + } + +#else // !__GNUC__ + +#define COUNT_ONES(type, suffix, width) \ + static inline unsigned int count_ones##suffix(type n) { \ + int ret; \ + for (ret = 0; n; ++ret) { \ + n &= n - 1; \ + } \ + return ret; \ + } + +#define LEADING_ZEROS(type, suffix, width) \ + 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); \ + return ret; \ + } + +#define TRAILING_ZEROS(type, suffix, width) \ + static inline unsigned int trailing_zeros##suffix(type n) { \ + type bit = 1; \ + int ret; \ + for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \ + return ret; \ + } + +#define FIRST_TRAILING_ONE(type, suffix, width) \ + static inline unsigned int first_trailing_one##suffix(type n) { \ + return n ? trailing_zeros##suffix(n) + 1 : 0; \ + } + +#endif // !__GNUC__ + +UINT_OVERLOADS(COUNT_ONES) +UINT_OVERLOADS(LEADING_ZEROS) +UINT_OVERLOADS(TRAILING_ZEROS) +UINT_OVERLOADS(FIRST_TRAILING_ONE) + +#define FIRST_LEADING_ONE(type, suffix, width) \ + 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) \ + static inline bool has_single_bit##suffix(type n) { \ + /** Branchless n && !(n & (n - 1)) */ \ + return n - 1 < (n ^ (n - 1)); \ + } + +#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 leading_zeros(n) UINT_SELECT(n, leading_zeros)(n) +#define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n)) + +#define trailing_zeros(n) UINT_SELECT(n, trailing_zeros)(n) +#define trailing_ones(n) UINT_SELECT(n, trailing_zeros)(~(n)) + +#define first_leading_one(n) UINT_SELECT(n, first_leading_one)(n) +#define first_leading_zero(n) UINT_SELECT(n, first_leading_one)(~(n)) + +#define first_trailing_one(n) UINT_SELECT(n, first_trailing_one)(n) +#define first_trailing_zero(n) UINT_SELECT(n, first_trailing_one)(~(n)) + +#define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n) + +#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) + +#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)); \ + } + +#define ROTATE_RIGHT(type, suffix, width) \ + static inline type rotate_right##suffix(type n, int c) { \ + return (n >> c) | (n << ((width - c) % width)); \ + } + +UINT_OVERLOADS(ROTATE_LEFT) +UINT_OVERLOADS(ROTATE_RIGHT) + +#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 c510fa8..a026831 100644 --- a/src/color.c +++ b/src/color.c @@ -1,197 +1,397 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "color.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" +#include "diag.h" #include "dir.h" #include "dstring.h" #include "expr.h" #include "fsade.h" #include "stat.h" #include "trie.h" -#include "util.h" -#include <assert.h> + #include <errno.h> #include <fcntl.h> #include <stdarg.h> -#include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> +/** + * An escape sequence, which may contain embedded NUL bytes. + */ +struct esc_seq { + /** The length of the escape sequence. */ + size_t len; + /** The escape sequence itself, without a terminating NUL. */ + char seq[] _counted_by(len); +}; + +/** + * A colored file extension, like `*.tar=01;31`. + */ +struct ext_color { + /** Priority, to disambiguate case-sensitive and insensitive matches. */ + size_t priority; + /** The escape sequence associated with this extension. */ + struct esc_seq *esc; + /** The length of the extension to match. */ + size_t len; + /** Whether the comparison should be case-sensitive. */ + bool case_sensitive; + /** The extension to match (NUL-terminated). */ + char ext[]; // _counted_by(len + 1); +}; + struct colors { - char *reset; - char *leftcode; - char *rightcode; - char *endcode; - char *clear_to_eol; - - char *bold; - char *gray; - char *red; - char *green; - char *yellow; - char *blue; - char *magenta; - char *cyan; - char *white; - - char *warning; - char *error; - - char *normal; - - char *file; - char *multi_hard; - char *executable; - char *capable; - char *setgid; - char *setuid; - - char *directory; - char *sticky; - char *other_writable; - char *sticky_other_writable; - - char *link; - char *orphan; - char *missing; + /** esc_seq allocator. */ + struct varena esc_arena; + /** ext_color allocator. */ + struct varena ext_arena; + + // Known dircolors keys + + struct esc_seq *reset; + struct esc_seq *leftcode; + struct esc_seq *rightcode; + struct esc_seq *endcode; + struct esc_seq *clear_to_eol; + + struct esc_seq *bold; + struct esc_seq *gray; + struct esc_seq *red; + struct esc_seq *green; + struct esc_seq *yellow; + struct esc_seq *blue; + struct esc_seq *magenta; + struct esc_seq *cyan; + struct esc_seq *white; + + struct esc_seq *warning; + struct esc_seq *error; + + struct esc_seq *normal; + + struct esc_seq *file; + struct esc_seq *multi_hard; + struct esc_seq *executable; + struct esc_seq *capable; + struct esc_seq *setgid; + struct esc_seq *setuid; + + struct esc_seq *directory; + struct esc_seq *sticky; + struct esc_seq *other_writable; + struct esc_seq *sticky_other_writable; + + struct esc_seq *link; + struct esc_seq *orphan; + struct esc_seq *missing; bool link_as_target; - char *blockdev; - char *chardev; - char *door; - char *pipe; - char *socket; + struct esc_seq *blockdev; + struct esc_seq *chardev; + struct esc_seq *door; + struct esc_seq *pipe; + struct esc_seq *socket; /** A mapping from color names (fi, di, ln, etc.) to struct fields. */ struct trie names; - /** A mapping from file extensions to colors. */ - struct trie ext_colors; + /** Number of extensions. */ + size_t ext_count; + /** Longest extension. */ + size_t ext_len; + /** Case-sensitive extension trie. */ + struct trie ext_trie; + /** Case-insensitive extension trie. */ + struct trie iext_trie; }; +/** Allocate an escape sequence. */ +static struct esc_seq *new_esc(struct colors *colors, const char *seq, size_t len) { + struct esc_seq *esc = varena_alloc(&colors->esc_arena, len); + if (esc) { + esc->len = len; + memcpy(esc->seq, seq, len); + } + return esc; +} + +/** Free an escape sequence. */ +static void free_esc(struct colors *colors, struct esc_seq *seq) { + varena_free(&colors->esc_arena, seq, seq->len); +} + /** Initialize a color in the table. */ -static int init_color(struct colors *colors, const char *name, const char *value, char **field) { +static int init_esc(struct colors *colors, const char *name, const char *value, struct esc_seq **field) { + struct esc_seq *esc = NULL; if (value) { - *field = dstrdup(value); - if (!*field) { + esc = new_esc(colors, value, strlen(value)); + if (!esc) { return -1; } - } else { - *field = NULL; } - struct trie_leaf *leaf = trie_insert_str(&colors->names, name); - if (leaf) { - leaf->value = field; + *field = esc; + + return trie_set_str(&colors->names, name, field); +} + +/** Check if an escape sequence is equal to a string. */ +static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) { + return esc->len == len && memcmp(esc->seq, str, len) == 0; +} + +/** Get an escape sequence from the table. */ +static struct esc_seq **get_esc(const struct colors *colors, const char *name) { + 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, dchar *value) { + struct esc_seq **field = get_esc(colors, name); + if (!field) { return 0; - } else { - return -1; } -} -/** Get a color from the table. */ -static char **get_color(const struct colors *colors, const char *name) { - const struct trie_leaf *leaf = trie_find_str(&colors->names, name); - if (leaf) { - return (char **)leaf->value; - } else { - return NULL; + if (*field) { + free_esc(colors, *field); + *field = NULL; } + + if (value) { + *field = new_esc(colors, value, dstrlen(value)); + if (!*field) { + return -1; + } + } + + return 0; } -/** Set the value of a color. */ -static int set_color(struct colors *colors, const char *name, char *value) { - char **color = get_color(colors, name); - if (color) { - dstrfree(*color); - *color = value; - return 0; - } else { - return -1; +/** Reverse a string, to turn suffix matches into prefix matches. */ +static void ext_reverse(char *ext, size_t len) { + for (size_t i = 0, j = len - 1; len && i < j; ++i, --j) { + char c = ext[i]; + ext[i] = ext[j]; + ext[j] = c; } } -/** - * Transform a file extension for fast lookups, by reversing and lowercasing it. - */ -static void extxfrm(char *ext) { - size_t len = strlen(ext); - for (size_t i = 0; i < len - i; ++i) { - char a = ext[i]; - char b = ext[len - i - 1]; +/** Convert a string to lowercase for case-insensitive matching. */ +static void ext_tolower(char *ext, size_t len) { + for (size_t i = 0; i < len; ++i) { + char c = ext[i]; // What's internationalization? Doesn't matter, this is what // GNU ls does. Luckily, since there's no standard C way to // casefold. Not using tolower() here since it respects the // current locale, which GNU ls doesn't do. - if (a >= 'A' && a <= 'Z') { - a += 'a' - 'A'; - } - if (b >= 'A' && b <= 'Z') { - b += 'a' - 'A'; + if (c >= 'A' && c <= 'Z') { + c += 'a' - 'A'; } - ext[i] = b; - ext[len - i - 1] = a; + ext[i] = c; } } -/** - * Set the color for an extension. - */ -static int set_ext_color(struct colors *colors, char *key, const char *value) { - extxfrm(key); - +/** 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 *match; - while ((match = trie_find_postfix(&colors->ext_colors, key))) { - dstrfree(match->value); - trie_remove(&colors->ext_colors, match); + struct trie_leaf *leaf; + while ((leaf = trie_find_postfix(trie, ext->ext))) { + trie_remove(trie, leaf); } - struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key); - if (leaf) { - leaf->value = (char *)value; + 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, 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; - } else { + } + + struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1); + if (!ext) { return -1; } + + ext->priority = colors->ext_count++; + ext->len = len; + ext->case_sensitive = false; + ext->esc = new_esc(colors, value, dstrlen(value)); + if (!ext->esc) { + goto fail; + } + + memcpy(ext->ext, key, len + 1); + + // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix() + ext_reverse(ext->ext, len); + + // Insert the extension into the case-sensitive trie + if (insert_ext(&colors->ext_trie, ext) != 0) { + goto fail; + } + + if (colors->ext_len < len) { + colors->ext_len = len; + } + + return 0; + +fail: + if (ext->esc) { + free_esc(colors, ext->esc); + } + varena_free(&colors->ext_arena, ext, len + 1); + return -1; +} + +/** + * 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) { + 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; + } + + // We already lowercased the extension above + if (insert_ext(&colors->iext_trie, ext) != 0) { + return -1; + } + } + + return 0; } /** * Find a color by an extension. */ -static const char *get_ext_color(const struct colors *colors, const char *filename) { - char *xfrm = strdup(filename); - if (!xfrm) { - return NULL; +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; + if (name_len < ext_len) { + ext_len = name_len; } - extxfrm(xfrm); + const char *suffix = filename + name_len - ext_len; - const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm); - free(xfrm); - if (leaf) { - return leaf->value; + char buf[256]; + char *copy; + if (ext_len < sizeof(buf)) { + copy = memcpy(buf, suffix, ext_len); + copy[ext_len] = '\0'; } else { - return NULL; + copy = strndup(suffix, ext_len); + if (!copy) { + return NULL; + } } + + ext_reverse(copy, ext_len); + const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_trie, copy); + const struct ext_color *ext = leaf ? leaf->value : NULL; + + ext_tolower(copy, ext_len); + const struct trie_leaf *ileaf = trie_find_prefix(&colors->iext_trie, copy); + const struct ext_color *iext = ileaf ? ileaf->value : NULL; + + if (iext && (!ext || ext->priority < iext->priority)) { + ext = iext; + } + + if (copy != buf) { + free(copy); + } + + return ext ? ext->esc : NULL; } /** @@ -215,23 +415,27 @@ static const char *get_ext_color(const struct colors *colors, const char *filena * * See man dir_colors. * - * @param value + * @str + * A dstring to fill with the unescaped chunk. + * @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 - * The parsed chunk as a dstring. + * 0 on success, -1 on failure. */ -static char *unescape(const char *value, char end, const char **next) { +static int unescape(char **str, const char *value, char end, const char **next) { + *next = NULL; + if (!value) { - goto fail; + errno = EINVAL; + return -1; } - char *str = dstralloc(0); - if (!str) { - goto fail_str; + if (dstresize(str, 0) != 0) { + return -1; } const char *i; @@ -308,7 +512,8 @@ static char *unescape(const char *value, char end, const char **next) { break; case '\0': - goto fail_str; + errno = EINVAL; + return -1; default: c = *i; @@ -322,7 +527,8 @@ static char *unescape(const char *value, char end, const char **next) { c = '\177'; break; case '\0': - goto fail_str; + errno = EINVAL; + return -1; default: // CTRL masks bits 6 and 7 c = *i & 0x1F; @@ -335,177 +541,193 @@ static char *unescape(const char *value, char end, const char **next) { break; } - if (dstrapp(&str, c) != 0) { - goto fail_str; + if (dstrapp(str, c) != 0) { + return -1; } } if (*i) { *next = i + 1; - } else { - *next = NULL; } - return str; - -fail_str: - dstrfree(str); -fail: - *next = NULL; - return NULL; + return 0; } /** Parse the GNU $LS_COLORS format. */ -static void parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { +static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { + int ret = -1; + dchar *key = NULL; + dchar *value = NULL; + for (const char *chunk = ls_colors, *next; chunk; chunk = next) { if (chunk[0] == '*') { - char *key = unescape(chunk + 1, '=', &next); - if (!key) { - continue; + if (unescape(&key, chunk + 1, '=', &next) != 0) { + goto fail; } - - char *value = unescape(next, ':', &next); - if (value) { - if (set_ext_color(colors, key, value) != 0) { - dstrfree(value); - } + if (unescape(&value, next, ':', &next) != 0) { + goto fail; + } + if (set_ext(colors, key, value) != 0) { + goto fail; } - - dstrfree(key); } else { const char *equals = strchr(chunk, '='); if (!equals) { break; } - char *value = unescape(equals + 1, ':', &next); - if (!value) { - continue; + if (dstrxcpy(&key, chunk, equals - chunk) != 0) { + goto fail; } - - char *key = strndup(chunk, equals - chunk); - if (!key) { - dstrfree(value); - continue; + if (unescape(&value, equals + 1, ':', &next) != 0) { + goto fail; } // All-zero values should be treated like NULL, to fall // back on any other relevant coloring for that file - 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 && strcmp(key, "ec") != 0) { - dstrfree(value); - value = NULL; + esc = NULL; } - if (set_color(colors, key, value) != 0) { - dstrfree(value); + if (set_esc(colors, key, esc) != 0) { + goto fail; } - free(key); } } + + ret = 0; +fail: + dstrfree(value); + dstrfree(key); + return ret; } -struct colors *parse_colors() { - struct colors *colors = malloc(sizeof(struct colors)); +struct colors *parse_colors(void) { + struct colors *colors = ALLOC(struct colors); if (!colors) { return NULL; } + VARENA_INIT(&colors->esc_arena, struct esc_seq, seq); + VARENA_INIT(&colors->ext_arena, struct ext_color, ext); trie_init(&colors->names); - trie_init(&colors->ext_colors); + colors->ext_count = 0; + colors->ext_len = 0; + trie_init(&colors->ext_trie); + trie_init(&colors->iext_trie); - int ret = 0; + bool fail = false; // From man console_codes - ret |= init_color(colors, "rs", "0", &colors->reset); - ret |= init_color(colors, "lc", "\033[", &colors->leftcode); - ret |= init_color(colors, "rc", "m", &colors->rightcode); - ret |= init_color(colors, "ec", NULL, &colors->endcode); - ret |= init_color(colors, "cl", "\033[K", &colors->clear_to_eol); - - ret |= init_color(colors, "bld", "01;39", &colors->bold); - ret |= init_color(colors, "gry", "01;30", &colors->gray); - ret |= init_color(colors, "red", "01;31", &colors->red); - ret |= init_color(colors, "grn", "01;32", &colors->green); - ret |= init_color(colors, "ylw", "01;33", &colors->yellow); - ret |= init_color(colors, "blu", "01;34", &colors->blue); - ret |= init_color(colors, "mag", "01;35", &colors->magenta); - ret |= init_color(colors, "cyn", "01;36", &colors->cyan); - ret |= init_color(colors, "wht", "01;37", &colors->white); - - ret |= init_color(colors, "wrn", "01;33", &colors->warning); - ret |= init_color(colors, "err", "01;31", &colors->error); + fail = fail || init_esc(colors, "rs", "0", &colors->reset); + fail = fail || init_esc(colors, "lc", "\033[", &colors->leftcode); + fail = fail || init_esc(colors, "rc", "m", &colors->rightcode); + fail = fail || init_esc(colors, "ec", NULL, &colors->endcode); + fail = fail || init_esc(colors, "cl", "\033[K", &colors->clear_to_eol); + + fail = fail || init_esc(colors, "bld", "01;39", &colors->bold); + fail = fail || init_esc(colors, "gry", "01;30", &colors->gray); + fail = fail || init_esc(colors, "red", "01;31", &colors->red); + fail = fail || init_esc(colors, "grn", "01;32", &colors->green); + fail = fail || init_esc(colors, "ylw", "01;33", &colors->yellow); + fail = fail || init_esc(colors, "blu", "01;34", &colors->blue); + fail = fail || init_esc(colors, "mag", "01;35", &colors->magenta); + fail = fail || init_esc(colors, "cyn", "01;36", &colors->cyan); + fail = fail || init_esc(colors, "wht", "01;37", &colors->white); + + fail = fail || init_esc(colors, "wrn", "01;33", &colors->warning); + fail = fail || init_esc(colors, "err", "01;31", &colors->error); // Defaults from man dir_colors + // "" means fall back to ->normal - ret |= init_color(colors, "no", NULL, &colors->normal); + fail = fail || init_esc(colors, "no", NULL, &colors->normal); - ret |= init_color(colors, "fi", NULL, &colors->file); - ret |= init_color(colors, "mh", NULL, &colors->multi_hard); - ret |= init_color(colors, "ex", "01;32", &colors->executable); - ret |= init_color(colors, "ca", NULL, &colors->capable); - ret |= init_color(colors, "sg", "30;43", &colors->setgid); - ret |= init_color(colors, "su", "37;41", &colors->setuid); + fail = fail || init_esc(colors, "fi", "", &colors->file); + fail = fail || init_esc(colors, "mh", NULL, &colors->multi_hard); + fail = fail || init_esc(colors, "ex", "01;32", &colors->executable); + fail = fail || init_esc(colors, "ca", NULL, &colors->capable); + fail = fail || init_esc(colors, "sg", "30;43", &colors->setgid); + fail = fail || init_esc(colors, "su", "37;41", &colors->setuid); - ret |= init_color(colors, "di", "01;34", &colors->directory); - ret |= init_color(colors, "st", "37;44", &colors->sticky); - ret |= init_color(colors, "ow", "34;42", &colors->other_writable); - ret |= init_color(colors, "tw", "30;42", &colors->sticky_other_writable); + fail = fail || init_esc(colors, "di", "01;34", &colors->directory); + fail = fail || init_esc(colors, "st", "37;44", &colors->sticky); + fail = fail || init_esc(colors, "ow", "34;42", &colors->other_writable); + fail = fail || init_esc(colors, "tw", "30;42", &colors->sticky_other_writable); - ret |= init_color(colors, "ln", "01;36", &colors->link); - ret |= init_color(colors, "or", NULL, &colors->orphan); - ret |= init_color(colors, "mi", NULL, &colors->missing); + fail = fail || init_esc(colors, "ln", "01;36", &colors->link); + fail = fail || init_esc(colors, "or", NULL, &colors->orphan); + fail = fail || init_esc(colors, "mi", NULL, &colors->missing); colors->link_as_target = false; - ret |= init_color(colors, "bd", "01;33", &colors->blockdev); - ret |= init_color(colors, "cd", "01;33", &colors->chardev); - ret |= init_color(colors, "do", "01;35", &colors->door); - ret |= init_color(colors, "pi", "33", &colors->pipe); - ret |= init_color(colors, "so", "01;35", &colors->socket); + fail = fail || init_esc(colors, "bd", "01;33", &colors->blockdev); + fail = fail || init_esc(colors, "cd", "01;33", &colors->chardev); + fail = fail || init_esc(colors, "do", "01;35", &colors->door); + fail = fail || init_esc(colors, "pi", "33", &colors->pipe); + fail = fail || init_esc(colors, "so", "01;35", &colors->socket); - if (ret) { - free_colors(colors); - return NULL; + if (fail) { + goto fail; } - parse_gnu_ls_colors(colors, getenv("LS_COLORS")); - parse_gnu_ls_colors(colors, getenv("BFS_COLORS")); + if (parse_gnu_ls_colors(colors, getenv("LS_COLORS")) != 0) { + goto fail; + } + if (parse_gnu_ls_colors(colors, getenv("BFS_COLORS")) != 0) { + goto fail; + } + if (build_iext_trie(colors) != 0) { + goto fail; + } - if (colors->link && strcmp(colors->link, "target") == 0) { + if (colors->link && esc_eq(colors->link, "target", strlen("target"))) { colors->link_as_target = true; - dstrfree(colors->link); - colors->link = NULL; + 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: + free_colors(colors); + return NULL; } void free_colors(struct colors *colors) { - if (colors) { - struct trie_leaf *leaf; - while ((leaf = trie_first_leaf(&colors->ext_colors))) { - dstrfree(leaf->value); - trie_remove(&colors->ext_colors, leaf); - } - trie_destroy(&colors->ext_colors); + if (!colors) { + return; + } - while ((leaf = trie_first_leaf(&colors->names))) { - char **field = leaf->value; - dstrfree(*field); - trie_remove(&colors->names, leaf); - } - trie_destroy(&colors->names); + trie_destroy(&colors->iext_trie); + trie_destroy(&colors->ext_trie); + trie_destroy(&colors->names); + varena_destroy(&colors->ext_arena); + varena_destroy(&colors->esc_arena); - free(colors); - } + free(colors); } CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { - CFILE *cfile = malloc(sizeof(*cfile)); + CFILE *cfile = ALLOC(CFILE); if (!cfile) { return NULL; } @@ -517,9 +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; @@ -544,29 +768,207 @@ 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; } } /** Get the color for a file. */ -static const char *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; } const struct bfs_stat *statbuf = NULL; - const char *color = NULL; + const struct esc_seq *color = NULL; 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; } @@ -576,7 +978,7 @@ static const char *file_color(const struct colors *colors, const char *filename, 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; @@ -585,7 +987,9 @@ static const char *file_color(const struct colors *colors, const char *filename, } if (!color) { - color = get_ext_color(colors, filename); + const char *name = cpath->path + cpath->nameoff; + size_t namelen = cpath->valid - cpath->nameoff; + color = get_ext(colors, name, namelen); } if (!color) { @@ -596,7 +1000,7 @@ static const char *file_color(const struct colors *colors, const char *filename, 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; } @@ -615,7 +1019,7 @@ static const char *file_color(const struct colors *colors, const char *filename, 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; @@ -642,7 +1046,7 @@ static const char *file_color(const struct colors *colors, const char *filename, break; } - if (!color) { + if (color && color->len == 0) { color = colors->normal; } @@ -656,17 +1060,29 @@ error: } } +/** Print an escape sequence chunk. */ +static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) { + return cat_esc(&cfile->buffer, esc); +} + /** Print an ANSI escape sequence. */ -static int print_esc(CFILE *cfile, const char *esc) { +static int print_esc(CFILE *cfile, const struct esc_seq *esc) { + if (!esc) { + return 0; + } + const struct colors *colors = cfile->colors; + if (esc != colors->reset) { + cfile->need_reset = true; + } - if (dstrdcat(&cfile->buffer, colors->leftcode) != 0) { + if (print_esc_chunk(cfile, cfile->colors->leftcode) != 0) { return -1; } - if (dstrdcat(&cfile->buffer, esc) != 0) { + if (print_esc_chunk(cfile, esc) != 0) { return -1; } - if (dstrdcat(&cfile->buffer, colors->rightcode) != 0) { + if (print_esc_chunk(cfile, cfile->colors->rightcode) != 0) { return -1; } @@ -675,117 +1091,77 @@ static int print_esc(CFILE *cfile, const char *esc) { /** Reset after an ANSI escape sequence. */ static int print_reset(CFILE *cfile) { - const struct colors *colors = cfile->colors; - - if (colors->endcode) { - return dstrdcat(&cfile->buffer, colors->endcode); - } else { - return print_esc(cfile, colors->reset); + if (!cfile->need_reset) { + return 0; } + cfile->need_reset = false; + + return print_esc_chunk(cfile, cfile->colors->endcode); +} + +/** Print a shell-escaped string. */ +static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flags flags) { + return dstrnescat(&cfile->buffer, str, n, flags); } /** Print a string with an optional color. */ -static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t len) { - if (esc) { - if (print_esc(cfile, esc) != 0) { - return -1; - } +static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) { + if (len == 0) { + return 0; } - if (dstrncat(&cfile->buffer, str, len) != 0) { + + if (print_esc(cfile, esc) != 0) { return -1; } - if (esc) { - if (print_reset(cfile) != 0) { - return -1; - } + + // Don't let the string itself interfere with the colors + if (print_wordesc(cfile, str, len, WESC_TTY) != 0) { + return -1; + } + + if (print_reset(cfile) != 0) { + return -1; } 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; - assert(ret >= 0); - - if (bftw_type(ftwbuf, flags) != BFS_ERROR) { - goto out; - } - - char *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); - } +/** Print a path with colors. */ +static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + struct cpath cpath; + if (cpath_init(&cpath, path, ftwbuf, flags) != 0) { + return -1; } - if (!at_path) { - ret = -1; - goto out; + const struct colors *colors = cfile->colors; + const struct esc_seq *dirs_color = colors->directory; + const struct esc_seq *name_color = NULL; + const struct esc_seq *err_color = colors->missing; + if (!err_color) { + err_color = colors->orphan; } - 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; - } - while (ret && at_path[len - 1] != '/') { - --len, --ret; + if (cpath.nameoff < cpath.valid) { + name_color = file_color(colors, &cpath); + if (name_color == dirs_color) { + cpath.nameoff = cpath.valid; } - - dstresize(&at_path, len); } -out_path: - dstrfree(at_path); -out: - return ret; -} - -/** Print the directories leading up to a file. */ -static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t nameoff) { - const struct colors *colors = cfile->colors; - - ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); - if (broken < 0) { + if (print_colored(cfile, dirs_color, path, cpath.nameoff) != 0) { return -1; } - if (broken > 0) { - if (print_colored(cfile, colors->directory, path, broken) != 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; } - if ((size_t)broken < nameoff) { - const char *color = colors->missing; - if (!color) { - color = colors->orphan; - } - if (print_colored(cfile, color, path + broken, nameoff - broken) != 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; @@ -793,24 +1169,18 @@ static int print_dirs_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 char *color = file_color(cfile->colors, name, ftwbuf, flags); - return print_colored(cfile, color, name, strlen(name)); -} - -/** 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 = xbasename(path) - path; - } - - if (print_dirs_colored(cfile, path, ftwbuf, flags, nameoff) != 0) { - return -1; - } - - return print_name_colored(cfile, path + nameoff, ftwbuf, flags); + 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. */ @@ -867,64 +1237,80 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { } /** Format some colored output to the buffer. */ -BFS_FORMATTER(2, 3) +_printf(2, 3) static int cbuff(CFILE *cfile, const char *format, ...); -/** Dump a parsed expression tree, for debugging. */ -static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) { - if (dstrcat(&cfile->buffer, "(") != 0) { - return -1; +/** 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]); } +} - const struct bfs_expr *lhs = NULL; - const struct bfs_expr *rhs = NULL; - - if (bfs_expr_has_children(expr)) { - lhs = expr->lhs; - rhs = expr->rhs; - - if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) { - return -1; - } - } else { - if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) { - return -1; - } +/** 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}%s${rs}", expr->argv[i]) < 0) { + 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) { + return dstrcat(&cfile->buffer, "(...)"); + } + + if (!expr) { + return dstrcat(&cfile->buffer, "(null)"); + } + + if (dstrcat(&cfile->buffer, "(") != 0) { + return -1; + } + + if (print_expr_args(cfile, expr) != 0) { + return -1; + } + if (verbose) { double rate = 0.0, time = 0.0; if (expr->evaluations) { - rate = 100.0*expr->successes/expr->evaluations; - time = (1.0e9*expr->elapsed.tv_sec + expr->elapsed.tv_nsec)/expr->evaluations; + rate = 100.0 * expr->successes / expr->evaluations; + time = (1.0e9 * expr->elapsed.tv_sec + expr->elapsed.tv_nsec) / expr->evaluations; } if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]", - expr->successes, expr->evaluations, rate, time)) { - return -1; - } - } - - if (lhs) { - if (dstrcat(&cfile->buffer, " ") != 0) { - return -1; - } - if (print_expr(cfile, lhs, verbose) != 0) { + expr->successes, expr->evaluations, rate, time)) { return -1; } } - if (rhs) { + int count = 0; + for_expr (child, expr) { if (dstrcat(&cfile->buffer, " ") != 0) { return -1; } - if (print_expr(cfile, rhs, verbose) != 0) { - return -1; + if (++count >= 3) { + if (dstrcat(&cfile->buffer, "...") != 0) { + return -1; + } + break; + } else { + if (print_expr(cfile, child, verbose, depth + 1) != 0) { + return -1; + } } } @@ -935,17 +1321,23 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) { return 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; + const char *end; + size_t len; + char name[4]; 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; + switch (*i) { case '%': switch (*++i) { @@ -989,14 +1381,19 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { } break; - case 'm': - if (dstrcat(&cfile->buffer, strerror(error)) != 0) { - return -1; - } - break; - case 'p': switch (*++i) { + case 'q': + if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_SHELL | WESC_TTY) != 0) { + return -1; + } + break; + case 'Q': + if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_TTY) != 0) { + return -1; + } + break; + case 'F': if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) { return -1; @@ -1016,12 +1413,22 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { break; case 'e': - if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false) != 0) { + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false, 0) != 0) { return -1; } break; case 'E': - if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true) != 0) { + if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true, 0) != 0) { + 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; @@ -1045,9 +1452,9 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { } break; - case '{': { + case '{': ++i; - const char *end = strchr(i, '}'); + end = strchr(i, '}'); if (!end) { goto invalid; } @@ -1056,16 +1463,22 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { break; } - size_t len = end - i; - char name[len + 1]; + len = end - i; + if (len >= sizeof(name)) { + goto invalid; + } memcpy(name, i, len); name[len] = '\0'; - char **esc = get_color(colors, name); - if (!esc) { - goto invalid; - } - if (*esc) { + if (strcmp(name, "rs") == 0) { + if (print_reset(cfile) != 0) { + return -1; + } + } else { + esc = get_esc(colors, name); + if (!esc) { + goto invalid; + } if (print_esc(cfile, *esc) != 0) { return -1; } @@ -1073,7 +1486,6 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { i = end; break; - } default: goto invalid; @@ -1088,7 +1500,7 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { return 0; invalid: - assert(!"Invalid format string"); + bfs_bug("Invalid format string '%s'", format); errno = EINVAL; return -1; } @@ -1102,7 +1514,7 @@ static int cbuff(CFILE *cfile, const char *format, ...) { } int cvfprintf(CFILE *cfile, const char *format, va_list args) { - assert(dstrlen(cfile->buffer) == 0); + bfs_assert(dstrlen(cfile->buffer) == 0); int ret = -1; if (cvbuff(cfile, format, args) == 0) { @@ -1112,7 +1524,7 @@ int cvfprintf(CFILE *cfile, const char *format, va_list args) { } } - dstresize(&cfile->buffer, 0); + dstrshrink(cfile->buffer, 0); return ret; } @@ -1123,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 edf1ef7..aac8b33 100644 --- a/src/color.h +++ b/src/color.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Utilities for colored output on ANSI terminals. @@ -21,9 +8,9 @@ #ifndef BFS_COLOR_H #define BFS_COLOR_H -#include "util.h" -#include <stdarg.h> -#include <stdbool.h> +#include "bfs.h" +#include "dstring.h" + #include <stdio.h> /** @@ -32,17 +19,17 @@ struct colors; /** - * Parse a color table. - * - * @return The parsed color table. + * Parse the color table from the environment. */ struct colors *parse_colors(void); /** + * Check if stat() info is required to color a file correctly. + */ +bool colors_need_stat(const struct colors *colors); + +/** * Free a color table. - * - * @param colors - * The color table to free. */ void free_colors(struct colors *colors); @@ -55,7 +42,11 @@ typedef struct CFILE { /** The color table to use, if any. */ const struct colors *colors; /** A buffer for colored formatting. */ - char *buffer; + dchar *buffer; + /** Cached file descriptor number. */ + int fd; + /** Whether the next ${rs} is actually necessary. */ + bool need_reset; /** Whether to close the underlying stream. */ bool close; } CFILE; @@ -63,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. @@ -77,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. @@ -87,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 @@ -97,24 +88,33 @@ 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 * %pP: A colored file path, from a const struct BFTW * argument * %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. */ -BFS_FORMATTER(2, 3) +_printf(2, 3) int cfprintf(CFILE *cfile, const char *format, ...); /** * cfprintf() variant that takes a va_list. */ +_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 @@ -1,139 +1,79 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "ctx.h" + +#include "alloc.h" +#include "bfstd.h" #include "color.h" -#include "darray.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 <assert.h> + #include <errno.h> #include <limits.h> +#include <signal.h> #include <stdio.h> #include <stdlib.h> - -const char *debug_flag_name(enum debug_flags flag) { - switch (flag) { - case DEBUG_COST: - return "cost"; - case DEBUG_EXEC: - return "exec"; - case DEBUG_OPT: - return "opt"; - case DEBUG_RATES: - return "rates"; - case DEBUG_SEARCH: - return "search"; - case DEBUG_STAT: - return "stat"; - case DEBUG_TREE: - return "tree"; - - case DEBUG_ALL: - break; - } - - assert(!"Unrecognized debug flag"); - return "???"; -} +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> struct bfs_ctx *bfs_ctx_new(void) { - struct bfs_ctx *ctx = malloc(sizeof(*ctx)); + struct bfs_ctx *ctx = ZALLOC(struct bfs_ctx); if (!ctx) { return NULL; } - ctx->argv = NULL; - ctx->paths = NULL; - ctx->expr = NULL; - ctx->exclude = NULL; + SLIST_INIT(&ctx->expr_list); + ARENA_INIT(&ctx->expr_arena, struct bfs_expr); - ctx->mindepth = 0; ctx->maxdepth = INT_MAX; ctx->flags = BFTW_RECOVER; ctx->strategy = BFTW_BFS; ctx->optlevel = 3; - ctx->debug = 0; - ctx->ignore_races = false; - ctx->posixly_correct = false; - ctx->status = false; - ctx->unique = false; - ctx->warn = false; - ctx->xargs_safe = false; - - ctx->colors = NULL; - ctx->colors_error = 0; - ctx->cout = NULL; - ctx->cerr = NULL; - - ctx->users = NULL; - ctx->users_error = 0; - ctx->groups = NULL; - ctx->groups_error = 0; - - ctx->mtab = NULL; - ctx->mtab_error = 0; - trie_init(&ctx->files); - ctx->nfiles = 0; - - struct rlimit rl; - if (getrlimit(RLIMIT_NOFILE, &rl) == 0) { - ctx->nofile_soft = rl.rlim_cur; - ctx->nofile_hard = rl.rlim_max; - } else { - ctx->nofile_soft = 1024; - ctx->nofile_hard = RLIM_INFINITY; + ctx->threads = nproc(); + if (ctx->threads > 8) { + // Not much speedup after 8 threads + ctx->threads = 8; } - return ctx; -} + trie_init(&ctx->files); -const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx) { - struct bfs_ctx *mut = (struct bfs_ctx *)ctx; + ctx->umask = umask(0); + umask(ctx->umask); - if (mut->users_error) { - errno = mut->users_error; - } else if (!mut->users) { - mut->users = bfs_users_parse(); - if (!mut->users) { - mut->users_error = errno; - } + if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) { + goto fail; } + ctx->cur_nofile = ctx->orig_nofile; + ctx->raise_nofile = true; - return mut->users; -} + ctx->users = bfs_users_new(); + if (!ctx->users) { + goto fail; + } -const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx) { - struct bfs_ctx *mut = (struct bfs_ctx *)ctx; + ctx->groups = bfs_groups_new(); + if (!ctx->groups) { + goto fail; + } - if (mut->groups_error) { - errno = mut->groups_error; - } else if (!mut->groups) { - mut->groups = bfs_groups_parse(); - if (!mut->groups) { - mut->groups_error = errno; - } + if (clock_gettime(CLOCK_REALTIME, &ctx->now) != 0) { + goto fail; } - return mut->groups; + return ctx; + +fail: + bfs_ctx_free(ctx); + return NULL; } const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx) { @@ -159,11 +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; } @@ -181,20 +130,33 @@ CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) { return ctx_file->cfile; } - leaf->value = ctx_file = malloc(sizeof(*ctx_file)); + 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) { @@ -202,10 +164,28 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx) { // - the user sees everything relevant before an -ok[dir] prompt // - output from commands is interleaved consistently with bfs // - executed commands can rely on I/O from other bfs actions - // - // We do not check errors here, but they will be caught at cleanup time - // with ferror(). - fflush(NULL); + for_trie (leaf, &ctx->files) { + struct bfs_ctx_file *ctx_file = leaf->value; + CFILE *cfile = ctx_file->cfile; + if (fflush(cfile->file) == 0) { + continue; + } + + ctx_file->error = errno; + clearerr(cfile->file); + + const char *path = ctx_file->path; + if (path) { + bfs_error(ctx, "%pq: %s.\n", path, errstr()); + } else if (cfile == ctx->cout) { + bfs_error(ctx, "(standard output): %s.\n", errstr()); + } + } + + // Flush the user/group caches, in case the executed command edits the + // user/group tables + bfs_users_flush(ctx->users); + bfs_groups_flush(ctx->groups); } /** Flush a file and report any errors. */ @@ -228,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; } @@ -262,47 +259,34 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { CFILE *cout = ctx->cout; CFILE *cerr = ctx->cerr; - bfs_expr_free(ctx->exclude); - bfs_expr_free(ctx->expr); - bfs_mtab_free(ctx->mtab); bfs_groups_free(ctx->groups); bfs_users_free(ctx->users); - struct trie_leaf *leaf; - while ((leaf = trie_first_leaf(&ctx->files))) { + for_trie (leaf, &ctx->files) { struct bfs_ctx_file *ctx_file = leaf->value; - 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_remove(&ctx->files, leaf); } 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 (size_t i = 0; i < darray_length(ctx->paths); ++i) { + for_slist (struct bfs_expr, expr, &ctx->expr_list, freelist) { + bfs_expr_clear(expr); + } + arena_destroy(&ctx->expr_arena); + + for (size_t i = 0; i < ctx->npaths; ++i) { free((char *)ctx->paths[i]); } - darray_free(ctx->paths); + free(ctx->paths); + free(ctx->kinds); free(ctx->argv); free(ctx); } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * bfs execution context. @@ -21,37 +8,18 @@ #ifndef BFS_CTX_H #define BFS_CTX_H +#include "alloc.h" #include "bftw.h" +#include "diag.h" +#include "expr.h" #include "trie.h" -#include <stdbool.h> -#include <sys/resource.h> -/** - * Various debugging flags. - */ -enum debug_flags { - /** Print cost estimates. */ - DEBUG_COST = 1 << 0, - /** Print executed command details. */ - DEBUG_EXEC = 1 << 1, - /** Print optimization details. */ - DEBUG_OPT = 1 << 2, - /** Print rate information. */ - DEBUG_RATES = 1 << 3, - /** Trace the filesystem traversal. */ - DEBUG_SEARCH = 1 << 4, - /** Trace all stat() calls. */ - DEBUG_STAT = 1 << 5, - /** Print the parse tree. */ - DEBUG_TREE = 1 << 6, - /** All debug flags. */ - DEBUG_ALL = (1 << 7) - 1, -}; +#include <stddef.h> +#include <sys/resource.h> +#include <sys/types.h> +#include <time.h> -/** - * Convert a debug flag to a string. - */ -const char *debug_flag_name(enum debug_flags flag); +struct CFILE; /** * The execution context for bfs. @@ -61,13 +29,22 @@ 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; + /** The number of root paths. */ + size_t npaths; + /** The main command line expression. */ struct bfs_expr *expr; /** An expression for files to filter out. */ struct bfs_expr *exclude; + /** A list of allocated expressions. */ + struct bfs_exprs expr_list; + /** bfs_expr arena. */ + struct arena expr_arena; /** -mindepth option. */ int mindepth; @@ -79,6 +56,8 @@ struct bfs_ctx { /** bftw() search strategy. */ enum bftw_strategy strategy; + /** Threads (-j). */ + int threads; /** Optimization level (-O). */ int optlevel; /** Debugging flags (-D). */ @@ -91,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. */ @@ -105,10 +91,8 @@ struct bfs_ctx { /** Colored stderr. */ struct CFILE *cerr; - /** User table. */ + /** User cache. */ struct bfs_users *users; - /** The error that occurred parsing the user table, if any. */ - int users_error; /** Group table. */ struct bfs_groups *groups; /** The error that occurred parsing the group table, if any. */ @@ -124,10 +108,18 @@ struct bfs_ctx { /** The number of files owned by the context. */ int nfiles; - /** The initial RLIMIT_NOFILE soft limit. */ - rlim_t nofile_soft; - /** The initial RLIMIT_NOFILE hard limit. */ - rlim_t nofile_hard; + /** 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; }; /** @@ -137,29 +129,9 @@ struct bfs_ctx { struct bfs_ctx *bfs_ctx_new(void); /** - * Get the users table. - * - * @param ctx - * The bfs context. - * @return - * The cached users table, or NULL on failure. - */ -const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx); - -/** - * Get the groups table. - * - * @param ctx - * The bfs context. - * @return - * The cached groups table, or NULL on failure. - */ -const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx); - -/** * Get the mount table. * - * @param ctx + * @ctx * The bfs context. * @return * The cached mount table, or NULL on failure. @@ -169,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, @@ -184,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); @@ -192,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); @@ -202,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. diff --git a/src/darray.c b/src/darray.c deleted file mode 100644 index 6585d30..0000000 --- a/src/darray.c +++ /dev/null @@ -1,103 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -#include "darray.h" -#include <stdlib.h> -#include <string.h> - -/** - * The darray header. - */ -struct darray { - /** The current capacity of the array, as a count of elements. */ - size_t capacity; - /** The current length of the array. */ - size_t length; - - // The array elements are stored after this header in memory. Not using - // a flexible array member to avoid worrying about strict aliasing. We - // assume that 2*sizeof(size_t) keeps any memory allocation suitably - // aligned for the element type. -}; - -/** Get the header for a darray. */ -static struct darray *darray_header(const void *da) { - return (struct darray *)da - 1; -} - -/** Get the array from a darray header. */ -static char *darray_data(struct darray *header) { - return (char *)(header + 1); -} - -size_t darray_length(const void *da) { - if (da) { - return darray_header(da)->length; - } else { - return 0; - } -} - -void *darray_push(void *da, const void *item, size_t size) { - struct darray *header; - if (da) { - header = darray_header(da); - } else { - header = malloc(sizeof(*header) + size); - if (!header) { - return NULL; - } - header->capacity = 1; - header->length = 0; - } - - size_t capacity = header->capacity; - size_t i = header->length++; - if (i >= capacity) { - capacity *= 2; - header = realloc(header, sizeof(*header) + capacity*size); - if (!header) { - // This failure will be detected by darray_check() - return da; - } - header->capacity = capacity; - } - - char *data = darray_data(header); - memcpy(data + i*size, item, size); - return data; -} - -int darray_check(void *da) { - if (!da) { - return -1; - } - - struct darray *header = darray_header(da); - if (header->length <= header->capacity) { - return 0; - } else { - // realloc() failed in darray_push(), so reset the length and report the failure - header->length = header->capacity; - return -1; - } -} - -void darray_free(void *da) { - if (da) { - free(darray_header(da)); - } -} diff --git a/src/darray.h b/src/darray.h deleted file mode 100644 index 4464381..0000000 --- a/src/darray.h +++ /dev/null @@ -1,110 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -/** - * A dynamic array library. - * - * darrays are represented by a simple pointer to the array element type, like - * any other array. Behind the scenes, the capacity and current length of the - * array are stored along with it. NULL is a valid way to initialize an empty - * darray: - * - * int *darray = NULL; - * - * To append an element to a darray, use the DARRAY_PUSH macro: - * - * int e = 42; - * if (DARRAY_PUSH(&darray, &e) != 0) { - * // Report the error... - * } - * - * The length can be retrieved by darray_length(). Iterating over the array - * works like normal arrays: - * - * for (size_t i = 0; i < darray_length(darray); ++i) { - * printf("%d\n", darray[i]); - * } - * - * To free a darray, use darray_free(): - * - * darray_free(darray); - */ - -#ifndef BFS_DARRAY_H -#define BFS_DARRAY_H - -#include <stddef.h> - -/** - * Get the length of a darray. - * - * @param da - * The array in question. - * @return - * The length of the array. - */ -size_t darray_length(const void *da); - -/** - * @internal Use DARRAY_PUSH(). - * - * Push an element into a darray. - * - * @param da - * The array to append to. - * @param item - * The item to append. - * @param size - * The size of the item. - * @return - * The (new) location of the array. - */ -void *darray_push(void *da, const void *item, size_t size); - -/** - * @internal Use DARRAY_PUSH(). - * - * Check if the last darray_push() call failed. - * - * @param da - * The darray to check. - * @return - * 0 on success, -1 on failure. - */ -int darray_check(void *da); - -/** - * Free a darray. - * - * @param da - * The darray to free. - */ -void darray_free(void *da); - -/** - * Push an item into a darray. - * - * @param da - * The array to append to. - * @param item - * A pointer to the item to append. - * @return - * 0 on success, -1 on failure. - */ -#define DARRAY_PUSH(da, item) \ - (darray_check(*(da) = darray_push(*(da), (item), sizeof(**(da) = *(item))))) - -#endif // BFS_DARRAY_H @@ -1,40 +1,85 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "diag.h" -#include "ctx.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" #include "color.h" +#include "ctx.h" +#include "dstring.h" #include "expr.h" -#include "util.h" -#include <assert.h> -#include <errno.h> + #include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#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); + veprintf(format, args); + va_end(args); +} + +_noreturn +void bfs_abortf(const char *format, ...) { + va_list args; + va_start(args, format); + veprintf(format, args); + va_end(args); + + abort(); +} + +const char *debug_flag_name(enum debug_flags flag) { + switch (flag) { + case DEBUG_COST: + return "cost"; + case DEBUG_EXEC: + return "exec"; + case DEBUG_OPT: + return "opt"; + case DEBUG_RATES: + return "rates"; + case DEBUG_SEARCH: + return "search"; + case DEBUG_STAT: + return "stat"; + case DEBUG_TREE: + return "tree"; + + case DEBUG_ALL: + break; + } + + bfs_bug("Unrecognized debug flag"); + return "???"; +} 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, ...) { +void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) { va_list args; va_start(args, format); bfs_verror(ctx, format, args); va_end(args); } -bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) { +bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) { va_list args; va_start(args, format); bool ret = bfs_vwarning(ctx, format, args); @@ -42,7 +87,7 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) { return ret; } -bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) { +bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) { va_list args; va_start(args, format); bool ret = bfs_vdebug(ctx, flag, format, args); @@ -51,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 { @@ -72,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 { @@ -83,13 +118,18 @@ bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *fo } } +/** Get the command name without any leading directories. */ +static const char *bfs_cmd(const struct bfs_ctx *ctx) { + return ctx->argv[0] + xbaseoff(ctx->argv[0]); +} + void bfs_error_prefix(const struct bfs_ctx *ctx) { - cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", xbasename(ctx->argv[0])); + cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", bfs_cmd(ctx)); } bool bfs_warning_prefix(const struct bfs_ctx *ctx) { if (ctx->warn) { - cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", xbasename(ctx->argv[0])); + cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", bfs_cmd(ctx)); return true; } else { return false; @@ -98,7 +138,7 @@ bool bfs_warning_prefix(const struct bfs_ctx *ctx) { bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) { if (ctx->debug & flag) { - cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", xbasename(ctx->argv[0]), debug_flag_name(flag)); + cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", bfs_cmd(ctx), debug_flag_name(flag)); return true; } else { return false; @@ -106,32 +146,33 @@ bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) { } /** Recursive part of highlight_expr(). */ -static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) { +static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) { if (!expr) { return false; } bool ret = false; - if (!expr->synthetic) { - size_t i = expr->argv - ctx->argv; - for (size_t j = 0; j < expr->argc; ++j) { - assert(i + j < ctx->argc); - args[i + j] = true; - ret = true; + for (size_t i = 0; i < ctx->argc; ++i) { + if (&ctx->argv[i] == expr->argv) { + for (size_t j = 0; j < expr->argc; ++j) { + bfs_assert(i + j < ctx->argc); + args[i + j] = true; + ret = true; + } + break; } } - if (bfs_expr_has_children(expr)) { - ret |= highlight_expr_recursive(ctx, expr->lhs, args); - ret |= highlight_expr_recursive(ctx, expr->rhs, args); + for_expr (child, expr) { + ret |= highlight_expr_recursive(ctx, child, args); } return ret; } /** Highlight an expression in the command line. */ -static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) { +static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) { for (size_t i = 0; i < ctx->argc; ++i) { args[i] = false; } @@ -140,13 +181,24 @@ static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *exp } /** Print a highlighted portion of the command line. */ -static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warning) { +static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool args[], bool warning) { if (warning) { bfs_warning_prefix(ctx); } else { bfs_error_prefix(ctx); } + dchar **argv = ZALLOC_ARRAY(dchar *, ctx->argc); + if (!argv) { + return; + } + + for (size_t i = 0; i < ctx->argc; ++i) { + if (dstrescat(&argv[i], ctx->argv[i], WESC_SHELL | WESC_TTY) != 0) { + goto done; + } + } + size_t max_argc = 0; for (size_t i = 0; i < ctx->argc; ++i) { if (i > 0) { @@ -155,9 +207,9 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn if (args[i]) { max_argc = i + 1; - cfprintf(ctx->cerr, "${bld}%s${rs}", ctx->argv[i]); + cfprintf(ctx->cerr, "${bld}%s${rs}", argv[i]); } else { - cfprintf(ctx->cerr, "%s", ctx->argv[i]); + cfprintf(ctx->cerr, "%s", argv[i]); } } @@ -186,7 +238,7 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn } } - size_t len = xstrwidth(ctx->argv[i]); + size_t len = xstrwidth(argv[i]); for (size_t j = 0; j < len; ++j) { if (args[i]) { cfprintf(ctx->cerr, "~"); @@ -201,9 +253,15 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn } cfprintf(ctx->cerr, "\n"); + +done: + for (size_t i = 0; i < ctx->argc; ++i) { + dstrfree(argv[i]); + } + free(argv); } -void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args) { +void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]) { bfs_argv_diag(ctx, args, false); } @@ -214,7 +272,7 @@ void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr) { } } -bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args) { +bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]) { if (!ctx->warn) { return false; } @@ -1,42 +1,184 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -/** - * Formatters for diagnostic messages. +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Diagnostic messages. */ #ifndef BFS_DIAG_H #define BFS_DIAG_H -#include "ctx.h" -#include "util.h" +#include "bfs.h" +#include "bfstd.h" + #include <stdarg.h> -#include <stdbool.h> +/** + * Wrap a diagnostic format string so it looks like + * + * bfs: func@src/file.c:0: Message + */ +#define BFS_DIAG_FORMAT_(format) \ + ((format) ? "%s: %s@%s:%d: " format "%s" : "") + +/** + * Add arguments to match a BFS_DIAG_FORMAT string. + */ +#define BFS_DIAG_ARGS_(...) \ + xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ "\n" + +/** + * Print a low-level diagnostic message to standard error. + */ +_printf(1, 2) +void bfs_diagf(const char *format, ...); + +/** + * Unconditional diagnostic message. + */ +#define bfs_diag(...) \ + bfs_diag_(__VA_ARGS__, ) + +#define bfs_diag_(format, ...) \ + bfs_diagf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__)) + +/** + * Print a diagnostic message including the last error. + */ +#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. + */ +_cold +_printf(1, 2) +_noreturn +void bfs_abortf(const char *format, ...); + +/** + * Unconditional abort with a message. + */ +#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__, "", ) + +#define bfs_verify_(str, cond, format, ...) \ + ((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 \ + ? 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; struct bfs_expr; /** + * Various debugging flags. + */ +enum debug_flags { + /** Print cost estimates. */ + DEBUG_COST = 1 << 0, + /** Print executed command details. */ + DEBUG_EXEC = 1 << 1, + /** Print optimization details. */ + DEBUG_OPT = 1 << 2, + /** Print rate information. */ + DEBUG_RATES = 1 << 3, + /** Trace the filesystem traversal. */ + DEBUG_SEARCH = 1 << 4, + /** Trace all stat() calls. */ + DEBUG_STAT = 1 << 5, + /** Print the parse tree. */ + DEBUG_TREE = 1 << 6, + /** All debug flags. */ + DEBUG_ALL = (1 << 7) - 1, +}; + +/** + * Convert a debug flag to a string. + */ +const char *debug_flag_name(enum debug_flags flag); + +/** * Like perror(), but decorated like bfs_error(). */ +_cold void bfs_perror(const struct bfs_ctx *ctx, const char *str); /** * Shorthand for printing error messages. */ -BFS_FORMATTER(2, 3) +_cold +_printf(2, 3) void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -44,7 +186,8 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a warning was printed. */ -BFS_FORMATTER(2, 3) +_cold +_printf(2, 3) bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -52,57 +195,71 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a debug message was printed. */ -BFS_FORMATTER(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. */ +_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. */ +_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. */ +_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. */ +_cold void bfs_error_prefix(const struct bfs_ctx *ctx); /** * Print the warning message prefix. */ +_cold bool bfs_warning_prefix(const struct bfs_ctx *ctx); /** * Print the debug message prefix. */ +_cold bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag); /** * Highlight parts of the command line in an error message. */ -void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args); +_cold +void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in an error message. */ +_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. */ -bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args); +_cold +bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in a warning message. */ +_cold bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr); #endif // BFS_DIAG_H @@ -1,33 +1,65 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2021-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "dir.h" -#include "util.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "sanity.h" +#include "trie.h" + #include <dirent.h> #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <unistd.h> -#if __linux__ -# include <sys/syscall.h> -#endif // __linux__ +#if BFS_USE_GETDENTS +# if BFS_HAS_GETDENTS64_SYSCALL +# include <sys/syscall.h> +# endif + +/** getdents() syscall wrapper. */ +static ssize_t bfs_getdents(int fd, void *buf, size_t size) { + sanitize_uninit(buf, size); + +#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 +# error "No getdents() implementation" +#endif + + if (ret > 0) { + sanitize_init(buf, ret); + } + + return ret; +} + +#endif // BFS_USE_GETDENTS + +/** Directory entry type for bfs_getdents() */ +#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) { switch (mode & S_IFMT) { @@ -77,227 +109,265 @@ enum bfs_type bfs_mode_to_type(mode_t mode) { } } -#if __linux__ /** - * This is not defined in the kernel headers for some reason, callers have to - * define it themselves. + * Private directory flags. */ -struct linux_dirent64 { - ino64_t d_ino; - off64_t d_off; - unsigned short d_reclen; - unsigned char d_type; - char d_name[]; +enum { + /** We've reached the end of the directory. */ + BFS_DIR_EOF = BFS_DIR_PRIVATE << 0, + /** This directory is a union mount we need to dedup manually. */ + BFS_DIR_UNION = BFS_DIR_PRIVATE << 1, }; -// Make the whole allocation 64k -#define BUF_SIZE ((64 << 10) - 8) -#endif - struct bfs_dir { -#if __linux__ + unsigned int flags; + +#if BFS_USE_GETDENTS int fd; unsigned short pos; unsigned short size; +# if __FreeBSD__ + struct trie trie; +# endif + alignas(sys_dirent) char buf[]; #else DIR *dir; struct dirent *de; #endif }; -struct bfs_dir *bfs_opendir(int at_fd, const char *at_path) { -#if __linux__ - struct bfs_dir *dir = malloc(sizeof(*dir) + BUF_SIZE); +#if BFS_USE_GETDENTS +# define DIR_SIZE (64 << 10) +# define BUF_SIZE (DIR_SIZE - sizeof(struct bfs_dir)) #else - struct bfs_dir *dir = malloc(sizeof(*dir)); +# define DIR_SIZE sizeof(struct bfs_dir) #endif - if (!dir) { - return NULL; - } +struct bfs_dir *bfs_allocdir(void) { + return malloc(DIR_SIZE); +} + +void bfs_dir_arena(struct arena *arena) { + arena_init(arena, alignof(struct bfs_dir), DIR_SIZE); +} + +int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags) { int fd; if (at_path) { fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (fd < 0) { + return -1; + } } else if (at_fd >= 0) { fd = at_fd; } else { - free(dir); errno = EBADF; - return NULL; + return -1; } - if (fd < 0) { - free(dir); - return NULL; - } + dir->flags = flags; -#if __linux__ +#if BFS_USE_GETDENTS dir->fd = fd; dir->pos = 0; dir->size = 0; -#else + +# if __FreeBSD__ && defined(F_ISUNIONSTACK) + if (fcntl(fd, F_ISUNIONSTACK) > 0) { + dir->flags |= BFS_DIR_UNION; + trie_init(&dir->trie); + } +# endif +#else // !BFS_USE_GETDENTS dir->dir = fdopendir(fd); if (!dir->dir) { if (at_path) { close_quietly(fd); } - free(dir); - return NULL; + return -1; } - dir->de = NULL; -#endif // __linux__ +#endif - return dir; + return 0; } int bfs_dirfd(const struct bfs_dir *dir) { -#if __linux__ +#if BFS_USE_GETDENTS return dir->fd; #else return dirfd(dir->dir); #endif } -/** Convert a dirent type to a bfs_type. */ -static enum bfs_type translate_type(int d_type) { - switch (d_type) { -#ifdef DT_BLK - case DT_BLK: - return BFS_BLK; -#endif -#ifdef DT_CHR - case DT_CHR: - return BFS_CHR; -#endif -#ifdef DT_DIR - case DT_DIR: - return BFS_DIR; -#endif -#ifdef DT_DOOR - case DT_DOOR: - return BFS_DOOR; -#endif -#ifdef DT_FIFO - case DT_FIFO: - return BFS_FIFO; -#endif -#ifdef DT_LNK - case DT_LNK: - return BFS_LNK; -#endif -#ifdef DT_PORT - case DT_PORT: - return BFS_PORT; -#endif -#ifdef DT_REG - case DT_REG: - return BFS_REG; -#endif -#ifdef DT_SOCK - case DT_SOCK: - return BFS_SOCK; -#endif -#ifdef DT_WHT - case DT_WHT: - return BFS_WHT; -#endif +int bfs_polldir(struct bfs_dir *dir) { +#if BFS_USE_GETDENTS + if (dir->pos < dir->size) { + return 1; + } else if (dir->flags & BFS_DIR_EOF) { + return 0; } - return BFS_UNKNOWN; + char *buf = (char *)(dir + 1); + ssize_t size = bfs_getdents(dir->fd, buf, BUF_SIZE); + if (size == 0) { + dir->flags |= BFS_DIR_EOF; + return 0; + } else if (size < 0) { + return -1; + } + + dir->pos = 0; + dir->size = size; + + // Like read(), getdents() doesn't indicate EOF until another call returns zero. + // Check that eagerly here to hopefully avoid a syscall in the last bfs_readdir(). + size_t rest = BUF_SIZE - size; + if (rest >= sizeof(sys_dirent)) { + size = bfs_getdents(dir->fd, buf + size, rest); + if (size > 0) { + dir->size += size; + } else if (size == 0) { + dir->flags |= BFS_DIR_EOF; + } + } + + return 1; +#else // !BFS_USE_GETDENTS + if (dir->de) { + return 1; + } else if (dir->flags & BFS_DIR_EOF) { + return 0; + } + + errno = 0; + dir->de = readdir(dir->dir); + if (dir->de) { + return 1; + } else if (errno == 0) { + dir->flags |= BFS_DIR_EOF; + return 0; + } else { + return -1; + } +#endif } -#if !__linux__ -/** Get the type from a struct dirent if it exists, and convert it. */ -static enum bfs_type dirent_type(const struct dirent *de) { -#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN) - return translate_type(de->d_type); +/** Read a single directory entry. */ +static int bfs_getdent(struct bfs_dir *dir, const sys_dirent **de) { + int ret = bfs_polldir(dir); + if (ret > 0) { +#if BFS_USE_GETDENTS + char *buf = (char *)(dir + 1); + *de = (const sys_dirent *)(buf + dir->pos); + dir->pos += (*de)->d_reclen; #else - return BFS_UNKNOWN; + *de = dir->de; + dir->de = NULL; #endif + } + return ret; } -#endif -/** Check if a name is . or .. */ -static bool is_dot(const char *name) { +/** Skip ".", "..", and deleted/empty dirents. */ +static int bfs_skipdent(struct bfs_dir *dir, const sys_dirent *de) { +#if BFS_USE_GETDENTS +# if __FreeBSD__ + // Union mounts on FreeBSD have to be de-duplicated in userspace + if (dir->flags & BFS_DIR_UNION) { + struct trie_leaf *leaf = trie_insert_str(&dir->trie, de->d_name); + if (!leaf) { + return -1; + } else if (leaf->value) { + return 1; + } else { + leaf->value = leaf; + } + } + + // NFS mounts on FreeBSD can return empty dirents with inode number 0 + if (de->d_ino == 0) { + return 1; + } +# endif + +# ifdef DT_WHT + if (de->d_type == DT_WHT && !(dir->flags & BFS_DIR_WHITEOUTS)) { + return 1; + } +# endif +#endif // BFS_USE_GETDENTS + + const char *name = de->d_name; return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')); } -int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) { - while (true) { -#if __linux__ - char *buf = (char *)(dir + 1); - - if (dir->pos >= dir->size) { -#if BFS_HAS_FEATURE(memory_sanitizer, false) - // Make sure msan knows the buffer is initialized - memset(buf, 0, BUF_SIZE); +/** Convert de->d_type to a bfs_type, if it exists. */ +static enum bfs_type bfs_d_type(const sys_dirent *de) { +#ifdef DTTOIF + return bfs_mode_to_type(DTTOIF(de->d_type)); +#else + return BFS_UNKNOWN; #endif +} - ssize_t size = syscall(__NR_getdents64, dir->fd, buf, BUF_SIZE); - if (size <= 0) { - return size; - } - dir->pos = 0; - dir->size = size; +int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) { + while (true) { + const sys_dirent *sysde; + int ret = bfs_getdent(dir, &sysde); + if (ret <= 0) { + return ret; } - const struct linux_dirent64 *lde = (void *)(buf + dir->pos); - dir->pos += lde->d_reclen; - - if (is_dot(lde->d_name)) { + int skip = bfs_skipdent(dir, sysde); + if (skip < 0) { + return skip; + } else if (skip) { continue; } if (de) { - de->type = translate_type(lde->d_type); - de->name = lde->d_name; + de->type = bfs_d_type(sysde); + de->name = sysde->d_name; } return 1; -#else // !__linux__ - errno = 0; - dir->de = readdir(dir->dir); - if (dir->de) { - if (is_dot(dir->de->d_name)) { - continue; - } - if (de) { - de->type = dirent_type(dir->de); - de->name = dir->de->d_name; - } - return 1; - } else if (errno != 0) { - return -1; - } else { - return 0; - } -#endif // !__linux__ } } +static void bfs_destroydir(struct bfs_dir *dir) { +#if BFS_USE_GETDENTS && __FreeBSD__ + if (dir->flags & BFS_DIR_UNION) { + trie_destroy(&dir->trie); + } +#endif + + sanitize_uninit(dir, DIR_SIZE); +} + int bfs_closedir(struct bfs_dir *dir) { -#if __linux__ +#if BFS_USE_GETDENTS int ret = xclose(dir->fd); #else int ret = closedir(dir->dir); + if (ret != 0) { + bfs_verify(errno != EBADF); + } #endif - free(dir); + + bfs_destroydir(dir); return ret; } -int bfs_freedir(struct bfs_dir *dir) { -#if __linux__ +#if BFS_USE_UNWRAPDIR +int bfs_unwrapdir(struct bfs_dir *dir) { +#if BFS_USE_GETDENTS int ret = dir->fd; - free(dir); - return ret; -#elif __FreeBSD__ +#elif BFS_HAS_FDCLOSEDIR int ret = fdclosedir(dir->dir); - free(dir); - return ret; -#else - int ret = dup_cloexec(dirfd(dir->dir)); - bfs_closedir(dir); - return ret; #endif + + bfs_destroydir(dir); + return ret; } +#endif @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Directories and their contents. @@ -21,9 +8,25 @@ #ifndef BFS_DIR_H #define BFS_DIR_H +#include "bfs.h" + #include <sys/types.h> /** + * Whether the implementation uses the getdents() syscall directly, rather than + * libc's readdir(). + */ +#ifndef BFS_USE_GETDENTS +# 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 + +/** * A directory. */ struct bfs_dir; @@ -74,17 +77,49 @@ struct bfs_dirent { }; /** + * Allocate space for a directory. + * + * @return + * An allocated, unopen directory, or NULL on failure. + */ +struct bfs_dir *bfs_allocdir(void); + +struct arena; + +/** + * Initialize an arena for directories. + * + * @arena + * The arena to initialize. + */ +void bfs_dir_arena(struct arena *arena); + +/** + * bfs_opendir() flags. + */ +enum bfs_dir_flags { + /** Include whiteouts in the results. */ + BFS_DIR_WHITEOUTS = 1 << 0, + /** @internal Start of private flags. */ + BFS_DIR_PRIVATE = 1 << 1, +}; + +/** * Open a directory. * - * @param at_fd + * @dir + * The allocated directory. + * @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. + * @flags + * Flags that control which directory entries are listed. * @return - * The opened directory, or NULL on failure. + * 0 on success, or -1 on failure. */ -struct bfs_dir *bfs_opendir(int at_fd, const char *at_path); +int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags); /** * Get the file descriptor for a directory. @@ -92,11 +127,21 @@ struct bfs_dir *bfs_opendir(int at_fd, const char *at_path); int bfs_dirfd(const struct bfs_dir *dir); /** + * Performs any I/O necessary for the next bfs_readdir() call. + * + * @dir + * The directory to poll. + * @return + * 1 on success, 0 on EOF, or -1 on failure. + */ +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. @@ -112,13 +157,22 @@ int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de); int bfs_closedir(struct bfs_dir *dir); /** - * Free a directory, keeping an open file descriptor to it. + * Whether the bfs_unwrapdir() function is supported. + */ +#ifndef BFS_USE_UNWRAPDIR +# 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 - * The directory to free. + * @dir + * The directory to detach. * @return - * The file descriptor on success, or -1 on failure. + * The file descriptor of the directory. */ -int bfs_freedir(struct bfs_dir *dir); +int bfs_unwrapdir(struct bfs_dir *dir); +#endif #endif // BFS_DIR_H diff --git a/src/dstring.c b/src/dstring.c index f344d09..678d685 100644 --- a/src/dstring.c +++ b/src/dstring.c @@ -1,155 +1,204 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "dstring.h" -#include <assert.h> + +#include "alloc.h" +#include "bit.h" +#include "diag.h" + #include <stdarg.h> +#include <stddef.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> /** - * The memory representation of a dynamic string. Users get a pointer to data. + * The memory representation of a dynamic string. Users get a pointer to str. */ struct dstring { - size_t capacity; - size_t length; - char data[]; + /** Capacity of the string, *including* the terminating NUL. */ + size_t cap; + /** Length of the string, *excluding* the terminating NUL. */ + size_t len; + /** The string itself. */ + alignas(dchar) char str[] _counted_by(cap); }; -/** Get the string header from the string data pointer. */ -static struct dstring *dstrheader(const char *dstr) { - return (struct dstring *)(dstr - offsetof(struct dstring, data)); +#define DSTR_OFFSET offsetof(struct dstring, str) + +/** Back up to the header from a pointer to dstring::str. */ +static struct dstring *dstrheader(const dchar *dstr) { + return (struct dstring *)(dstr - DSTR_OFFSET); +} + +/** + * In some provenance models, the expression `header->str` has its provenance + * restricted to just the `str` field itself, making a future dstrheader() + * illegal. This alternative is guaranteed to preserve provenance for the entire + * allocation. + * + * - https://stackoverflow.com/q/25296019 + * - https://mastodon.social/@void_friend@tech.lgbt/111144859908104311 + */ +static dchar *dstrdata(struct dstring *header) { + return (char *)header + DSTR_OFFSET; } -/** Get the correct size for a dstring with the given capacity. */ -static size_t dstrsize(size_t capacity) { - return BFS_FLEX_SIZEOF(struct dstring, data, capacity + 1); +/** 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 char *dstralloc_impl(size_t capacity, size_t length, const char *data) { +static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) { // Avoid reallocations for small strings - if (capacity < 7) { - capacity = 7; + if (cap < DSTR_OFFSET) { + cap = DSTR_OFFSET; } - struct dstring *header = malloc(dstrsize(capacity)); + struct dstring *header = ALLOC_FLEX(struct dstring, str, cap); if (!header) { return NULL; } - header->capacity = capacity; - header->length = length; + header->cap = cap; + dstrsetlen(header, len); - memcpy(header->data, data, length); - header->data[length] = '\0'; - return header->data; + dchar *ret = dstrdata(header); + memcpy(ret, str, len); + return ret; } -char *dstralloc(size_t capacity) { - return dstralloc_impl(capacity, 0, ""); +dchar *dstralloc(size_t cap) { + return dstralloc_impl(cap + 1, 0, ""); } -char *dstrdup(const char *str) { - size_t len = strlen(str); - return dstralloc_impl(len, len, str); +dchar *dstrdup(const char *str) { + return dstrxdup(str, strlen(str)); } -char *dstrndup(const char *str, size_t n) { - size_t len = strnlen(str, n); - return dstralloc_impl(len, len, str); +dchar *dstrndup(const char *str, size_t n) { + return dstrxdup(str, strnlen(str, n)); } -size_t dstrlen(const char *dstr) { - return dstrheader(dstr)->length; +dchar *dstrddup(const dchar *dstr) { + return dstrxdup(dstr, dstrlen(dstr)); } -int dstreserve(char **dstr, size_t capacity) { - struct dstring *header = dstrheader(*dstr); +dchar *dstrxdup(const char *str, size_t len) { + return dstralloc_impl(len + 1, len, str); +} - if (capacity > header->capacity) { - capacity *= 2; +size_t dstrlen(const dchar *dstr) { + return dstrheader(dstr)->len; +} - header = realloc(header, dstrsize(capacity)); - if (!header) { - return -1; - } - header->capacity = capacity; +int dstreserve(dchar **dstr, size_t cap) { + if (!*dstr) { + *dstr = dstralloc(cap); + return *dstr ? 0 : -1; + } - *dstr = header->data; + struct dstring *header = dstrheader(*dstr); + size_t old_cap = header->cap; + size_t new_cap = cap + 1; // Terminating NUL + if (old_cap >= new_cap) { + return 0; } + new_cap = bit_ceil(new_cap); + header = REALLOC_FLEX(struct dstring, str, header, old_cap, new_cap); + if (!header) { + return -1; + } + + header->cap = new_cap; + *dstr = dstrdata(header); return 0; } -int dstresize(char **dstr, size_t length) { - if (dstreserve(dstr, length) != 0) { +int dstresize(dchar **dstr, size_t len) { + if (dstreserve(dstr, len) != 0) { return -1; } struct dstring *header = dstrheader(*dstr); - header->length = length; - header->data[length] = '\0'; - + dstrsetlen(header, len); return 0; } -/** Common implementation of dstr{cat,ncat,app}. */ -static int dstrcat_impl(char **dest, const char *src, size_t srclen) { +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)); +} + +int dstrncat(dchar **dest, const char *src, size_t n) { + return dstrxcat(dest, src, strnlen(src, n)); +} + +int dstrdcat(dchar **dest, const dchar *src) { + return dstrxcat(dest, src, dstrlen(src)); +} + +int dstrxcat(dchar **dest, const char *src, size_t len) { size_t oldlen = dstrlen(*dest); - size_t newlen = oldlen + srclen; + size_t newlen = oldlen + len; if (dstresize(dest, newlen) != 0) { return -1; } - memcpy(*dest + oldlen, src, srclen); + memcpy(*dest + oldlen, src, len); return 0; } -int dstrcat(char **dest, const char *src) { - return dstrcat_impl(dest, src, strlen(src)); +int dstrapp(dchar **str, char c) { + return dstrxcat(str, &c, 1); +} + +int dstrcpy(dchar **dest, const char *src) { + return dstrxcpy(dest, src, strlen(src)); } -int dstrncat(char **dest, const char *src, size_t n) { - return dstrcat_impl(dest, src, strnlen(src, n)); +int dstrncpy(dchar **dest, const char *src, size_t n) { + return dstrxcpy(dest, src, strnlen(src, n)); } -int dstrdcat(char **dest, const char *src) { - return dstrcat_impl(dest, src, dstrlen(src)); +int dstrdcpy(dchar **dest, const dchar *src) { + return dstrxcpy(dest, src, dstrlen(src)); } -int dstrapp(char **str, char c) { - return dstrcat_impl(str, &c, 1); +int dstrxcpy(dchar **dest, const char *src, size_t len) { + if (dstresize(dest, len) != 0) { + return -1; + } + + memcpy(*dest, src, len); + return 0; } -char *dstrprintf(const char *format, ...) { +dchar *dstrprintf(const char *format, ...) { va_list args; va_start(args, format); - char *str = dstrvprintf(format, args); + dchar *str = dstrvprintf(format, args); va_end(args); 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 - char *str = dstralloc(2*strlen(format)); + dchar *str = dstralloc(2 * strlen(format)); if (!str) { return NULL; } @@ -162,7 +211,7 @@ char *dstrvprintf(const char *format, va_list args) { return str; } -int dstrcatf(char **str, const char *format, ...) { +int dstrcatf(dchar **str, const char *format, ...) { va_list args; va_start(args, format); @@ -172,49 +221,88 @@ int dstrcatf(char **str, const char *format, ...) { return ret; } -int dstrvcatf(char **str, const char *format, va_list args) { +int dstrvcatf(dchar **str, const char *format, va_list args) { // Guess a capacity to try to avoid calling vsnprintf() twice size_t len = dstrlen(*str); - dstreserve(str, len + 2*strlen(format)); - size_t cap = dstrheader(*str)->capacity; + dstreserve(str, len + 2 * strlen(format)); + size_t cap = dstrheader(*str)->cap; va_list copy; va_copy(copy, args); char *tail = *str + len; - int ret = vsnprintf(tail, cap - len + 1, format, args); + size_t tail_cap = cap - len; + int ret = vsnprintf(tail, tail_cap, format, args); if (ret < 0) { goto fail; } size_t tail_len = ret; - if (tail_len > cap - len) { - cap = len + tail_len; - if (dstreserve(str, cap) != 0) { + if (tail_len >= tail_cap) { + if (dstreserve(str, len + tail_len) != 0) { goto fail; } tail = *str + len; ret = vsnprintf(tail, tail_len + 1, format, copy); if (ret < 0 || (size_t)ret != tail_len) { - assert(!"Length of formatted string changed"); + bfs_bug("Length of formatted string changed"); goto fail; } } va_end(copy); - struct dstring *header = dstrheader(*str); - header->length += tail_len; + dstrheader(*str)->len += tail_len; return 0; fail: + va_end(copy); *tail = '\0'; return -1; } -void dstrfree(char *dstr) { +int dstrescat(dchar **dest, const char *str, enum wesc_flags flags) { + return dstrnescat(dest, str, SIZE_MAX, flags); +} + +int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags) { + size_t len = *dest ? dstrlen(*dest) : 0; + + // Worst case growth is `ccc...` => $'\xCC\xCC\xCC...' + n = strnlen(str, n); + size_t cap = len + 4 * n + 3; + if (dstreserve(dest, cap) != 0) { + return -1; + } + + char *cur = *dest + len; + char *end = *dest + cap + 1; + cur = wordnesc(cur, end, str, n, flags); + bfs_assert(cur != end, "wordesc() result truncated"); + + return dstresize(dest, cur - *dest); +} + +void dstrfree(dchar *dstr) { if (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 54106f3..ce7ef86 100644 --- a/src/dstring.h +++ b/src/dstring.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016-2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A dynamic string library. @@ -21,174 +8,348 @@ #ifndef BFS_DSTRING_H #define BFS_DSTRING_H -#include "util.h" +#include "bfs.h" +#include "bfstd.h" + #include <stdarg.h> #include <stddef.h> +/** Marker type for dynamic strings. */ +#if BFS_LINT && __clang__ +// Abuse __attribute__(aligned) to make a type that allows +// +// dchar * -> char * +// +// conversions, but warns (with Clang's -Walign-mismatch) on +// +// char * -> dchar * +typedef __attribute__((aligned(alignof(size_t)))) char dchar; +#else +typedef char dchar; +#endif + +/** + * Free a dynamic string. + * + * @dstr + * The string to free. + */ +void dstrfree(dchar *dstr); + /** * Allocate a dynamic string. * - * @param capacity + * @cap * The initial capacity of the string. */ -char *dstralloc(size_t capacity); +_malloc(dstrfree, 1) +dchar *dstralloc(size_t cap); /** * Create a dynamic copy of a string. * - * @param str + * @str * The NUL-terminated string to copy. */ -char *dstrdup(const char *str); +_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. */ -char *dstrndup(const char *str, size_t n); +_malloc(dstrfree, 1) +dchar *dstrndup(const char *str, size_t n); + +/** + * Create a dynamic copy of a dynamic string. + * + * @dstr + * The dynamic string to copy. + */ +_malloc(dstrfree, 1) +dchar *dstrddup(const dchar *dstr); + +/** + * Create an exact-sized dynamic copy of a string. + * + * @str + * The string to copy. + * @len + * The length of the string, which may include internal NUL bytes. + */ +_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. + * @return + * The length of dstr. */ -size_t dstrlen(const char *dstr); +size_t dstrlen(const dchar *dstr); /** * Reserve some capacity in a dynamic string. * - * @param dstr + * @dstr * The dynamic string to preallocate. - * @param capacity + * @cap * The new capacity for the string. - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ -int dstreserve(char **dstr, size_t capacity); +int dstreserve(dchar **dstr, size_t cap); /** * Resize a dynamic string. * - * @param dstr + * @dstr * The dynamic string to resize. - * @param length + * @len * The new length for the dynamic string. - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ -int dstresize(char **dstr, size_t length); +_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. */ -int dstrcat(char **dest, const char *src); +_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. + * @return + * 0 on success, -1 on failure. */ -int dstrncat(char **dest, const char *src, size_t n); +_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. */ -int dstrdcat(char **dest, const char *src); +_nodiscard +int dstrdcat(dchar **dest, const dchar *src); + +/** + * Append to a dynamic string. + * + * @dest + * The destination dynamic string. + * @src + * The string to append. + * @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. + * @return + * 0 on success, -1 on failure. + */ +_nodiscard +int dstrapp(dchar **str, char c); + +/** + * Copy a string into a dynamic string. + * + * @dest + * The destination dynamic string. + * @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. + * + * @dest + * The destination dynamic string. + * @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. + * + * @dest + * The destination dynamic string. + * @src + * The dynamic string to copy. + * @n + * The maximum number of characters to take from src. + * @returns + * 0 on success, -1 on failure. */ -int dstrapp(char **str, char c); +_nodiscard +int dstrncpy(dchar **dest, const char *str, size_t n); + +/** + * Copy a string into a dynamic string. + * + * @dest + * The destination dynamic string. + * @src + * The dynamic string to copy. + * @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. */ -BFS_FORMATTER(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. */ -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. */ -BFS_FORMATTER(2, 3) -int dstrcatf(char **str, const char *format, ...); +_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. */ -int dstrvcatf(char **str, const char *format, va_list args); +_nodiscard +_printf(2, 0) +int dstrvcatf(dchar **str, const char *format, va_list args); /** - * Free a dynamic string. + * Concatenate while shell-escaping. * - * @param dstr - * The string to free. + * @dest + * The destination dynamic string. + * @str + * The string to escape. + * @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. + * + * @dest + * The destination dynamic string. + * @str + * The string to escape. + * @n + * The maximum length of the string. + * @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. */ -void dstrfree(char *dstr); +_nodiscard +dchar *dstrepeat(const char *str, size_t n); #endif // BFS_DSTRING_H @@ -1,29 +1,19 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Implementation of all the primary expressions. */ #include "eval.h" + +#include "atomic.h" #include "bar.h" +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "color.h" #include "ctx.h" -#include "darray.h" #include "diag.h" #include "dir.h" #include "dstring.h" @@ -33,24 +23,28 @@ #include "mtab.h" #include "printf.h" #include "pwcache.h" +#include "sanity.h" +#include "sighook.h" #include "stat.h" #include "trie.h" -#include "util.h" #include "xregex.h" #include "xtime.h" -#include <assert.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> #include <stdlib.h> #include <string.h> +#include <strings.h> #include <sys/resource.h> -#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> #include <time.h> #include <unistd.h> #include <wchar.h> @@ -64,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; }; @@ -71,20 +67,24 @@ struct bfs_eval { /** * Print an error message. */ -BFS_FORMATTER(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); } @@ -94,7 +94,7 @@ static void eval_error(struct bfs_eval *state, const char *format, ...) { */ static bool eval_should_ignore(const struct bfs_eval *state, int error) { return state->ctx->ignore_races - && is_nonexistence_error(error) + && error_is_like(error, ENOENT) && state->ftwbuf->depth > 0; } @@ -103,11 +103,25 @@ 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()); } } /** + * Report an I/O error that occurs during evaluation. + */ +static void eval_io_error(const struct bfs_expr *expr, struct bfs_eval *state) { + if (expr->path) { + eval_error(state, "'%s': %s.\n", expr->path, errstr()); + } else { + eval_error(state, "(standard output): %s.\n", errstr()); + } + + // Don't report the error again in bfs_ctx_free() + clearerr(expr->cfile->file); +} + +/** * Perform a bfs_stat() call if necessary. */ static const struct bfs_stat *eval_stat(struct bfs_eval *state) { @@ -123,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) { @@ -140,10 +152,24 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) { return n > expr->num; } - assert(!"Invalid comparison mode"); + bfs_bug("Invalid comparison mode"); 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. */ @@ -193,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; } @@ -217,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; } /** @@ -238,11 +278,11 @@ bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) { time_t diff = timespec_diff(&expr->reftime, time); switch (expr->time_unit) { case BFS_DAYS: - diff /= 60*24; - BFS_FALLTHROUGH; + diff /= 60 * 24; + _fallthrough; case BFS_MINUTES: diff /= 60; - BFS_FALLTHROUGH; + _fallthrough; case BFS_SECONDS: break; } @@ -270,7 +310,7 @@ bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - long long day_seconds = 60*60*24; + long long day_seconds = 60 * 60 * 24; diff = (diff + day_seconds - 1) / day_seconds; return bfs_expr_cmp(expr, diff); } @@ -308,13 +348,11 @@ bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); - if (!groups) { + const struct group *grp = bfs_getgrgid(state->ctx->groups, statbuf->gid); + if (errno != 0) { eval_report_error(state); - return false; } - - return bfs_getgrgid(groups, statbuf->gid) == NULL; + return grp == NULL; } /** @@ -326,13 +364,11 @@ bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - const struct bfs_users *users = bfs_ctx_users(state->ctx); - if (!users) { + const struct passwd *pwd = bfs_getpwuid(state->ctx->users, statbuf->uid); + if (errno != 0) { eval_report_error(state); - return false; } - - return bfs_getpwuid(users, statbuf->uid) == NULL; + return pwd == NULL; } /** @@ -372,15 +408,14 @@ 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; } - } else if (bfs_expr_has_children(expr)) { - if (expr->lhs && eval_exec_finish(expr->lhs, ctx) != 0) { - ret = -1; - } - if (expr->rhs && eval_exec_finish(expr->rhs, ctx) != 0) { + } + + for_expr (child, expr) { + if (eval_exec_finish(child, ctx) != 0) { ret = -1; } } @@ -394,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; } @@ -420,33 +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; - if (ftwbuf->type == BFS_DIR) { - struct bfs_dir *dir = bfs_opendir(ftwbuf->at_fd, ftwbuf->at_path); + switch (ftwbuf->type) { + case BFS_REG: + statbuf = eval_stat(state); + return statbuf && statbuf->size == 0; + + case BFS_DIR: + dir = bfs_allocdir(); if (!dir) { - eval_report_error(state); - goto done; + goto error; } - int did_read = bfs_readdir(dir, NULL); - if (did_read < 0) { - eval_report_error(state); - } else { - ret = !did_read; + if (bfs_opendir(dir, ftwbuf->at_fd, ftwbuf->at_path, 0) != 0) { + goto error; } + int did_read = bfs_readdir(dir, NULL); bfs_closedir(dir); - } else if (ftwbuf->type == BFS_REG) { - const struct bfs_stat *statbuf = eval_stat(state); - if (statbuf) { - ret = statbuf->size == 0; + + if (did_read < 0) { + goto error; } - } -done: - return ret; + free(dir); + return did_read == 0; + error: + eval_report_error(state); + free(dir); + return false; + + default: + return false; + } } /** @@ -478,7 +522,7 @@ bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state) { return (flags & set) || (flags & clear) != clear; } - assert(!"Invalid comparison mode"); + bfs_bug("Invalid comparison mode"); return false; } @@ -498,6 +542,11 @@ bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state) { } const char *type = bfs_fstype(mtab, statbuf); + if (!type) { + eval_report_error(state); + return false; + } + return strcmp(type, expr->argv[1]) == 0; } @@ -561,7 +610,7 @@ bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state) { goto done; } - ret = fnmatch(expr->argv[1], name, expr->num) == 0; + ret = eval_fnmatch(expr, name); done: free(name); @@ -572,6 +621,7 @@ done: * -i?name test. */ bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) { + bool ret = false; const struct BFTW *ftwbuf = state->ftwbuf; const char *name = ftwbuf->path + ftwbuf->nameoff; @@ -579,18 +629,16 @@ bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) { if (ftwbuf->depth == 0) { // Any trailing slashes are not part of the name. This can only // happen for the root path. - const char *slash = strchr(name, '/'); - if (slash && slash > name) { - copy = strndup(name, slash - name); - if (!copy) { - eval_report_error(state); - return false; - } - name = copy; + name = copy = xbasename(name); + if (!name) { + eval_report_error(state); + goto done; } } - bool ret = fnmatch(expr->argv[1], name, expr->num) == 0; + ret = eval_fnmatch(expr, name); + +done: free(copy); return ret; } @@ -599,8 +647,7 @@ bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) { * -i?path test. */ bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state) { - const struct BFTW *ftwbuf = state->ftwbuf; - return fnmatch(expr->argv[1], ftwbuf->path, expr->num) == 0; + return eval_fnmatch(expr, state->ftwbuf->path); } /** @@ -631,27 +678,77 @@ bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state) { return !(mode & target) == !target; } - assert(!"Invalid comparison mode"); + bfs_bug("Invalid comparison mode"); return false; } +/** Print a user/group name/id, and update the column width. */ +static int print_owner(FILE *file, const char *name, uintmax_t id, int *width) { + if (name) { + int len = xstrwidth(name); + if (*width < len) { + *width = len; + } + + return fprintf(file, " %s%*s", name, *width - len, ""); + } else { + int ret = fprintf(file, " %-*ju", *width, id); + if (ret >= 0 && *width < ret - 1) { + *width = ret - 1; + } + return ret; + } +} + +/** 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. */ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { CFILE *cfile = expr->cfile; FILE *file = cfile->file; - const struct bfs_users *users = bfs_ctx_users(state->ctx); - const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); + const struct bfs_ctx *ctx = state->ctx; const struct BFTW *ftwbuf = state->ftwbuf; const struct bfs_stat *statbuf = eval_stat(state); if (!statbuf) { goto done; } + // ls -l prints non-path text in the "normal" color, so do the same + if (cfprintf(cfile, "${no}") < 0) { + goto error; + } + uintmax_t ino = statbuf->ino; - uintmax_t block_size = state->ctx->posixly_correct ? 512 : 1024; - uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + block_size - 1)/block_size; + uintmax_t block_size = ctx->posixly_correct ? 512 : 1024; + uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + block_size - 1) / block_size; char mode[11]; xstrmode(statbuf->mode, mode); char acl = bfs_check_acl(ftwbuf) > 0 ? '+' : ' '; @@ -660,33 +757,21 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { goto error; } - uintmax_t uid = statbuf->uid; - const struct passwd *pwd = users ? bfs_getpwuid(users, uid) : NULL; - if (pwd) { - if (fprintf(file, " %-8s", pwd->pw_name) < 0) { - goto error; - } - } else { - if (fprintf(file, " %-8ju", uid) < 0) { - goto error; - } + const struct passwd *pwd = bfs_getpwuid(ctx->users, statbuf->uid); + static int uwidth = 8; + if (print_owner(file, pwd ? pwd->pw_name : NULL, statbuf->uid, &uwidth) < 0) { + goto error; } - uintmax_t gid = statbuf->gid; - const struct group *grp = groups ? bfs_getgrgid(groups, gid) : NULL; - if (grp) { - if (fprintf(file, " %-8s", grp->gr_name) < 0) { - goto error; - } - } else { - if (fprintf(file, " %-8ju", gid) < 0) { - goto error; - } + const struct group *grp = bfs_getgrgid(ctx->groups, statbuf->gid); + static int gwidth = 8; + if (print_owner(file, grp ? grp->gr_name : NULL, statbuf->gid, &gwidth) < 0) { + goto error; } if (ftwbuf->type == BFS_BLK || ftwbuf->type == BFS_CHR) { - int ma = bfs_major(statbuf->rdev); - int mi = bfs_minor(statbuf->rdev); + int ma = xmajor(statbuf->rdev); + int mi = xminor(statbuf->rdev); if (fprintf(file, " %3d, %3d", ma, mi) < 0) { goto error; } @@ -698,27 +783,12 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { } time_t time = statbuf->mtime.tv_sec; - time_t now = expr->reftime.tv_sec; - time_t six_months_ago = now - 6*30*24*60*60; - time_t tomorrow = now + 24*60*60; - struct tm tm; - if (xlocaltime(&time, &tm) != 0) { - goto error; - } - char time_str[256]; - const char *time_format = "%b %e %H:%M"; - if (time <= six_months_ago || time >= tomorrow) { - time_format = "%b %e %Y"; - } - if (!strftime(time_str, sizeof(time_str), time_format, &tm)) { - errno = EOVERFLOW; - goto error; - } - if (fprintf(file, " %s", time_str) < 0) { + time_t now = ctx->now.tv_sec; + if (print_time(file, time, now) < 0) { goto error; } - if (cfprintf(cfile, " %pP", ftwbuf) < 0) { + if (cfprintf(cfile, "${rs} %pP", ftwbuf) < 0) { goto error; } @@ -736,7 +806,7 @@ done: return true; error: - eval_report_error(state); + eval_io_error(expr, state); return true; } @@ -745,7 +815,7 @@ error: */ bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state) { if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) { - eval_report_error(state); + eval_io_error(expr, state); } return true; } @@ -757,7 +827,7 @@ bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) { const char *path = state->ftwbuf->path; size_t length = strlen(path) + 1; if (fwrite(path, 1, length, expr->cfile->file) != length) { - eval_report_error(state); + eval_io_error(expr, state); } return true; } @@ -767,7 +837,7 @@ bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) { */ bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state) { if (bfs_printf(expr->cfile, expr->printf, state->ftwbuf) != 0) { - eval_report_error(state); + eval_io_error(expr, state); } return true; @@ -799,7 +869,6 @@ bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) { ++path; } - if (fputc('\n', file) == EOF) { goto error; } @@ -807,7 +876,20 @@ bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) { return true; error: - eval_report_error(state); + eval_io_error(expr, state); + return true; +} + +/** + * -limit action. + */ +bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state) { + long long evals = expr->evaluations + 1; + if (evals >= expr->num) { + state->action = BFTW_STOP; + state->quit = true; + } + return true; } @@ -841,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()); } } @@ -881,7 +963,7 @@ bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state) { }; off_t scale = scales[expr->size_unit]; - off_t size = (statbuf->size + scale - 1)/scale; // Round up + off_t size = (statbuf->size + scale - 1) / scale; // Round up return bfs_expr_cmp(expr, size); } @@ -894,7 +976,7 @@ bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1)/BFS_STAT_BLKSIZE; + blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1) / BFS_STAT_BLKSIZE; return statbuf->blocks < expected; } @@ -938,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; @@ -946,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; } /** @@ -994,13 +1066,14 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { } } - assert(!state->quit); + bfs_assert(!state->quit); bool ret = expr->eval_fn(expr, state); if (time) { if (eval_gettime(state, &end) == 0) { - timespec_elapsed(&expr->elapsed, &start, &end); + timespec_sub(&end, &start); + timespec_add(&expr->elapsed, &end); } } @@ -1010,10 +1083,10 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { } if (bfs_expr_never_returns(expr)) { - assert(state->quit); + bfs_assert(state->quit); } else if (!state->quit) { - assert(!expr->always_true || ret); - assert(!expr->always_false || !ret); + bfs_assert(!expr->always_true || ret); + bfs_assert(!expr->always_false || !ret); } return ret; @@ -1023,67 +1096,53 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { * Evaluate a negation. */ bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) { - return !eval_expr(expr->rhs, state); + return !eval_expr(bfs_expr_children(expr), state); } /** * Evaluate a conjunction. */ bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) { - if (!eval_expr(expr->lhs, state)) { - return false; - } - - if (state->quit) { - return false; + for_expr (child, expr) { + if (!eval_expr(child, state) || state->quit) { + return false; + } } - return eval_expr(expr->rhs, state); + return true; } /** * Evaluate a disjunction. */ bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) { - if (eval_expr(expr->lhs, state)) { - return true; - } - - if (state->quit) { - return false; + for_expr (child, expr) { + if (eval_expr(child, state) || state->quit) { + return true; + } } - return eval_expr(expr->rhs, state); + return false; } /** * Evaluate the comma operator. */ bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) { - eval_expr(expr->lhs, state); + bool ret uninit(false); - if (state->quit) { - return false; + for_expr (child, expr) { + ret = eval_expr(child, state); + if (state->quit) { + break; + } } - return eval_expr(expr->rhs, state); + return ret; } /** 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; @@ -1091,20 +1150,21 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time const struct BFTW *ftwbuf = state->ftwbuf; - char *rhs = dstrprintf(" (visited: %zu, depth: %2zu)", count, ftwbuf->depth); + dchar *status = NULL; + dchar *rhs = dstrprintf(" (visited: %'zu; depth: %2zu)", count, ftwbuf->depth); if (!rhs) { return; } - size_t rhslen = dstrlen(rhs); + size_t rhslen = xstrwidth(rhs); if (3 + rhslen > width) { - dstresize(&rhs, 0); + dstrshrink(rhs, 0); rhslen = 0; } - char *status = dstralloc(0); + status = dstralloc(0); if (!status) { - goto out_rhs; + goto out; } const char *path = ftwbuf->path; @@ -1113,26 +1173,25 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time pathlen = strlen(path); } + // Escape weird filename characters + if (dstrnescat(&status, path, pathlen, WESC_TTY) != 0) { + goto out; + } + pathlen = dstrlen(status); + // Try to make sure even wide characters fit in the status bar size_t pathmax = width - rhslen - 3; size_t pathwidth = 0; - mbstate_t mb; - memset(&mb, 0, sizeof(mb)); - while (pathlen > 0) { - wchar_t wc; - size_t len = mbrtowc(&wc, path, pathlen, &mb); + size_t lhslen = 0; + mbstate_t mb = {0}; + for (size_t i = lhslen; lhslen < pathlen; lhslen = i) { + wint_t wc = xmbrtowc(status, &i, pathlen, &mb); int cwidth; - if (len == (size_t)-1) { + if (wc == WEOF) { // Invalid byte sequence, assume a single-width '?' - len = 1; - cwidth = 1; - memset(&mb, 0, sizeof(mb)); - } else if (len == (size_t)-2) { - // Incomplete byte sequence, assume a single-width '?' - len = pathlen; cwidth = 1; } else { - cwidth = wcwidth(wc); + cwidth = xwcwidth(wc); if (cwidth < 0) { cwidth = 0; } @@ -1141,35 +1200,29 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time if (pathwidth + cwidth > pathmax) { break; } - - if (dstrncat(&status, path, len) != 0) { - goto out_rhs; - } - - path += len; - pathlen -= len; pathwidth += cwidth; } + dstrshrink(status, lhslen); if (dstrcat(&status, "...") != 0) { - goto out_rhs; + goto out; } while (pathwidth < pathmax) { if (dstrapp(&status, ' ') != 0) { - goto out_rhs; + goto out; } ++pathwidth; } - if (dstrcat(&status, rhs) != 0) { - goto out_rhs; + if (dstrdcat(&status, rhs) != 0) { + goto out; } bfs_bar_update(bar, status); +out: dstrfree(status); -out_rhs: dstrfree(rhs); } @@ -1198,25 +1251,25 @@ static bool eval_file_unique(struct bfs_eval *state, struct trie *seen) { } } -#define DEBUG_FLAG(flags, flag) \ - do { \ - if ((flags & flag) || flags == flag) { \ - fputs(#flag, stderr); \ - flags ^= flag; \ - if (flags) { \ - fputs(" | ", stderr); \ - } \ - } \ +#define DEBUG_FLAG(flags, flag) \ + do { \ + if ((flags & flag) || flags == flag) { \ + fputs(#flag, stderr); \ + flags ^= flag; \ + if (flags) { \ + fputs(" | ", stderr); \ + } \ + } \ } while (0) /** * Log a stat() call. */ -static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flags flags) { +static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, int err) { 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); @@ -1230,11 +1283,12 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, con DEBUG_FLAG(flags, BFS_STAT_FOLLOW); DEBUG_FLAG(flags, BFS_STAT_NOFOLLOW); DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW); + DEBUG_FLAG(flags, BFS_STAT_NOSYNC); - fprintf(stderr, ") == %d", cache->buf ? 0 : -1); + fprintf(stderr, ") == %d", err == 0 ? 0 : -1); - if (cache->error) { - fprintf(stderr, " [%d]", cache->error); + if (err) { + fprintf(stderr, " [%d]", err); } fprintf(stderr, "\n"); @@ -1248,14 +1302,14 @@ static void debug_stats(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf) { return; } - const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf; - if (statbuf || ftwbuf->stat_cache.error) { - debug_stat(ctx, ftwbuf, &ftwbuf->stat_cache, BFS_STAT_FOLLOW); + const struct bftw_stat *bufs = &ftwbuf->stat_bufs; + + if (bufs->stat_err >= 0) { + debug_stat(ctx, ftwbuf, BFS_STAT_FOLLOW, bufs->stat_err); } - const struct bfs_stat *lstatbuf = ftwbuf->lstat_cache.buf; - if ((lstatbuf && lstatbuf != statbuf) || ftwbuf->lstat_cache.error) { - debug_stat(ctx, ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW); + if (bufs->lstat_err >= 0) { + debug_stat(ctx, ftwbuf, BFS_STAT_NOFOLLOW, bufs->lstat_err); } } @@ -1318,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. */ @@ -1344,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", strerror(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; } @@ -1409,59 +1550,51 @@ done: return state.action; } -/** Check if an rlimit value is infinite. */ -static bool rlim_isinf(rlim_t r) { - // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite - if (r == RLIM_INFINITY) { - return true; +/** Raise RLIMIT_NOFILE if possible, and return the new limit. */ +static int raise_fdlimit(struct bfs_ctx *ctx) { + rlim_t cur = ctx->orig_nofile.rlim_cur; + rlim_t max = ctx->orig_nofile.rlim_max; + if (!ctx->raise_nofile) { + max = cur; } -#ifdef RLIM_SAVED_CUR - if (r == RLIM_SAVED_CUR) { - return true; + rlim_t target = 64 << 10; + if (rlim_cmp(target, max) > 0) { + target = max; } -#endif -#ifdef RLIM_SAVED_MAX - if (r == RLIM_SAVED_MAX) { - return true; + if (rlim_cmp(target, cur) <= 0) { + return target; } -#endif - return false; -} + const struct rlimit rl = { + .rlim_cur = target, + .rlim_max = max, + }; -/** Compare two rlimit values, accounting for RLIM_INFINITY etc. */ -static int rlim_cmp(rlim_t a, rlim_t b) { - bool a_inf = rlim_isinf(a); - bool b_inf = rlim_isinf(b); - if (a_inf || b_inf) { - return a_inf - b_inf; + if (setrlimit(RLIMIT_NOFILE, &rl) != 0) { + return cur; } - return (a > b) - (a < b); + ctx->cur_nofile = rl; + return target; } -/** Raise RLIMIT_NOFILE if possible, and return the new limit. */ -static int raise_fdlimit(const struct bfs_ctx *ctx) { - rlim_t target = 64 << 10; - if (rlim_cmp(target, ctx->nofile_hard) > 0) { - target = ctx->nofile_hard; - } - - int ret = target; +/** Preallocate the fd table in the kernel. */ +static void reserve_fds(int limit) { + // Kernels typically implement the fd table as a dynamic array. + // Growing the array can be expensive, especially if files are being + // opened in parallel. We can work around this by allocating the + // highest possible fd, forcing the kernel to grow the table upfront. - if (rlim_cmp(target, ctx->nofile_soft) > 0) { - const struct rlimit rl = { - .rlim_cur = target, - .rlim_max = ctx->nofile_hard, - }; - if (setrlimit(RLIMIT_NOFILE, &rl) != 0) { - ret = ctx->nofile_soft; - } +#ifdef F_DUPFD_CLOEXEC + int fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, limit - 1); +#else + int fd = fcntl(STDIN_FILENO, F_DUPFD, limit - 1); +#endif + if (fd >= 0) { + xclose(fd); } - - return ret; } /** Infer the number of file descriptors available to bftw(). */ @@ -1471,20 +1604,25 @@ static int infer_fdlimit(const struct bfs_ctx *ctx, int limit) { // Check /proc/self/fd for the current number of open fds, if possible // (we may have inherited more than just the standard ones) - struct bfs_dir *dir = bfs_opendir(AT_FDCWD, "/proc/self/fd"); + struct bfs_dir *dir = bfs_allocdir(); if (!dir) { - dir = bfs_opendir(AT_FDCWD, "/dev/fd"); + goto done; } - if (dir) { - // Account for 'dir' itself - nopen = -1; - while (bfs_readdir(dir, NULL) > 0) { - ++nopen; - } + if (bfs_opendir(dir, AT_FDCWD, "/proc/self/fd", 0) != 0 + && bfs_opendir(dir, AT_FDCWD, "/dev/fd", 0) != 0) { + goto done; + } - bfs_closedir(dir); + // Account for 'dir' itself + nopen = -1; + + while (bfs_readdir(dir, NULL) > 0) { + ++nopen; } + bfs_closedir(dir); +done: + free(dir); int ret = limit - nopen; ret -= ctx->expr->persistent_fds; @@ -1513,8 +1651,9 @@ static void dump_bftw_flags(enum bftw_flags flags) { DEBUG_FLAG(flags, BFTW_PRUNE_MOUNTS); DEBUG_FLAG(flags, BFTW_SORT); DEBUG_FLAG(flags, BFTW_BUFFER); + DEBUG_FLAG(flags, BFTW_WHITEOUTS); - assert(!flags); + bfs_assert(flags == 0, "Missing bftw flag 0x%X", flags); } /** @@ -1546,12 +1685,8 @@ static bool eval_must_buffer(const struct bfs_expr *expr) { return true; } - if (bfs_expr_has_children(expr)) { - if (expr->lhs && eval_must_buffer(expr->lhs)) { - return true; - } - - if (expr->rhs && eval_must_buffer(expr->rhs)) { + for_expr (child, expr) { + if (eval_must_buffer(child)) { return true; } } @@ -1560,7 +1695,7 @@ static bool eval_must_buffer(const struct bfs_expr *expr) { return false; } -int bfs_eval(const struct bfs_ctx *ctx) { +int bfs_eval(struct bfs_ctx *ctx) { if (!ctx->expr) { return EXIT_SUCCESS; } @@ -1571,12 +1706,16 @@ int bfs_eval(const 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); @@ -1584,14 +1723,19 @@ int bfs_eval(const struct bfs_ctx *ctx) { } int fdlimit = raise_fdlimit(ctx); + reserve_fds(fdlimit); fdlimit = infer_fdlimit(ctx, fdlimit); + // -1 for the main thread + int nthreads = ctx->threads - 1; + struct bftw_args bftw_args = { .paths = ctx->paths, - .npaths = darray_length(ctx->paths), + .npaths = ctx->npaths, .callback = eval_callback, .ptr = &args, .nopenfd = fdlimit, + .nthreads = nthreads, .flags = ctx->flags, .strategy = ctx->strategy, .mtab = bfs_ctx_mtab(ctx), @@ -1611,6 +1755,7 @@ int bfs_eval(const struct bfs_ctx *ctx) { fprintf(stderr, "\t.callback = eval_callback,\n"); fprintf(stderr, "\t.ptr = &args,\n"); fprintf(stderr, "\t.nopenfd = %d,\n", bftw_args.nopenfd); + fprintf(stderr, "\t.nthreads = %d,\n", bftw_args.nthreads); fprintf(stderr, "\t.flags = "); dump_bftw_flags(bftw_args.flags); fprintf(stderr, ",\n\t.strategy = %s,\n", dump_bftw_strategy(bftw_args.strategy)); @@ -1638,7 +1783,14 @@ int bfs_eval(const 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; } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * The evaluation functions that implement primary expressions like -name, @@ -22,8 +9,6 @@ #ifndef BFS_EVAL_H #define BFS_EVAL_H -#include <stdbool.h> - struct bfs_ctx; struct bfs_expr; @@ -35,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. @@ -47,12 +32,12 @@ 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. */ -int bfs_eval(const struct bfs_ctx *ctx); +int bfs_eval(struct bfs_ctx *ctx); // Predicate evaluation functions @@ -62,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); @@ -101,6 +87,7 @@ bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state); @@ -1,32 +1,21 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "exec.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" -#include "ctx.h" #include "color.h" +#include "ctx.h" #include "diag.h" #include "dstring.h" -#include "util.h" #include "xspawn.h" -#include <assert.h> + #include <errno.h> #include <fcntl.h> #include <stdarg.h> -#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -35,7 +24,7 @@ #include <unistd.h> /** Print some debugging info. */ -BFS_FORMATTER(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; @@ -69,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; @@ -95,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; } @@ -138,26 +127,16 @@ static void bfs_exec_parse_error(const struct bfs_ctx *ctx, const struct bfs_exe } struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags) { - struct bfs_exec *execbuf = malloc(sizeof(*execbuf)); + struct bfs_exec *execbuf = ZALLOC(struct bfs_exec); if (!execbuf) { - bfs_perror(ctx, "malloc()"); + bfs_perror(ctx, "zalloc()"); goto fail; } execbuf->flags = flags; execbuf->ctx = ctx; execbuf->tmpl_argv = argv + 1; - execbuf->tmpl_argc = 0; - execbuf->argv = NULL; - execbuf->argc = 0; - execbuf->argv_cap = 0; - execbuf->arg_size = 0; - execbuf->arg_max = 0; - execbuf->arg_min = 0; execbuf->wd_fd = -1; - execbuf->wd_path = NULL; - execbuf->wd_len = 0; - execbuf->ret = 0; while (true) { const char *arg = execbuf->tmpl_argv[execbuf->tmpl_argc]; @@ -172,7 +151,7 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs goto fail; } else if (strcmp(arg, ";") == 0) { break; - } else if (strcmp(arg, "+") == 0) { + } else if (execbuf->tmpl_argc > 0 && strcmp(arg, "+") == 0) { const char *prev = execbuf->tmpl_argv[execbuf->tmpl_argc - 1]; if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(prev, "{}") == 0) { execbuf->flags |= BFS_EXEC_MULTI; @@ -190,9 +169,9 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs } execbuf->argv_cap = execbuf->tmpl_argc + 1; - execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv)); + execbuf->argv = ALLOC_ARRAY(char *, execbuf->argv_cap); if (!execbuf->argv) { - bfs_perror(ctx, "malloc()"); + bfs_perror(ctx, "alloc()"); goto fail; } @@ -238,9 +217,8 @@ static char *bfs_exec_format_path(const struct bfs_exec *execbuf, const struct B return NULL; } - strcpy(path, "./"); - strcpy(path + 2, name); - + char *cur = stpcpy(path, "./"); + cur = stpcpy(cur, name); return path; } @@ -251,14 +229,14 @@ static char *bfs_exec_format_arg(char *arg, const char *path) { return arg; } - char *ret = dstralloc(0); + dchar *ret = dstralloc(0); if (!ret) { return NULL; } 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) { @@ -283,18 +261,18 @@ err: /** Free a formatted argument. */ static void bfs_exec_free_arg(char *arg, const char *tmpl) { if (arg != tmpl) { - dstrfree(arg); + dstrfree((dchar *)arg); } } /** Open a file to use as the working directory. */ static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - assert(execbuf->wd_fd < 0); - assert(!execbuf->wd_path); + 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 - assert(ftwbuf->at_path == xbasename(ftwbuf->at_path)); + bfs_assert(xbaseoff(ftwbuf->at_path) == 0); execbuf->wd_fd = ftwbuf->at_fd; if (!(execbuf->flags & BFS_EXEC_MULTI)) { @@ -351,6 +329,11 @@ static void bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf /** Actually spawn the process. */ static int bfs_exec_spawn(const struct bfs_exec *execbuf) { + const struct bfs_ctx *ctx = execbuf->ctx; + + // Flush the context state for consistency with the external process + bfs_ctx_flush(ctx); + if (execbuf->flags & BFS_EXEC_CONFIRM) { for (size_t i = 0; i < execbuf->argc; ++i) { if (fprintf(stderr, "%s ", execbuf->argv[i]) < 0) { @@ -367,54 +350,48 @@ static int bfs_exec_spawn(const struct bfs_exec *execbuf) { } } - // Flush cached state for consistency with the external process - bfs_ctx_flush(execbuf->ctx); - if (execbuf->flags & BFS_EXEC_MULTI) { bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments] (size %zu)\n", - execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size); + execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size); } else { bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments]\n", execbuf->argv[0], execbuf->argc - 1); } pid_t pid = -1; - int error; - struct bfs_spawn ctx; - if (bfs_spawn_init(&ctx) != 0) { + struct bfs_spawn spawn; + if (bfs_spawn_init(&spawn) != 0) { return -1; } - if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) { - goto fail; - } + spawn.flags |= BFS_SPAWN_USE_PATH; - // Reset RLIMIT_NOFILE, to avoid breaking applications that use select() - struct rlimit rl = { - .rlim_cur = execbuf->ctx->nofile_soft, - .rlim_max = execbuf->ctx->nofile_hard, - }; - if (bfs_spawn_addsetrlimit(&ctx, RLIMIT_NOFILE, &rl) != 0) { - goto fail; + if (execbuf->wd_fd >= 0) { + if (bfs_spawn_addfchdir(&spawn, execbuf->wd_fd) != 0) { + goto fail; + } } - if (execbuf->wd_fd >= 0) { - if (bfs_spawn_addfchdir(&ctx, execbuf->wd_fd) != 0) { + // Reset RLIMIT_NOFILE if necessary, to avoid breaking applications that use select() + if (rlim_cmp(ctx->orig_nofile.rlim_cur, ctx->cur_nofile.rlim_cur) < 0) { + if (bfs_spawn_setrlimit(&spawn, RLIMIT_NOFILE, &ctx->orig_nofile) != 0) { goto fail; } } - pid = bfs_spawn(execbuf->argv[0], &ctx, execbuf->argv, NULL); -fail: - error = errno; - bfs_spawn_destroy(&ctx); + pid = bfs_spawn(execbuf->argv[0], &spawn, execbuf->argv, NULL); + +fail:; + int error = errno; + + bfs_spawn_destroy(&spawn); if (pid < 0) { errno = error; return -1; } int wstatus; - if (waitpid(pid, &wstatus, 0) < 0) { + if (xwaitpid(pid, &wstatus, 0) < 0) { return -1; } @@ -433,9 +410,9 @@ fail: if (!str) { str = "unknown"; } - bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str); + bfs_warning(ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str); } else { - bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]); + bfs_warning(ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]); } errno = 0; @@ -495,7 +472,7 @@ static bool bfs_exec_args_remain(const struct bfs_exec *execbuf) { static size_t bfs_exec_estimate_max(const struct bfs_exec *execbuf) { size_t min = execbuf->arg_min; size_t max = execbuf->arg_max; - return min + (max - min)/2; + return min + (max - min) / 2; } /** Update the ARG_MAX lower bound from a successful execution. */ @@ -510,7 +487,7 @@ static void bfs_exec_update_min(struct bfs_exec *execbuf) { size_t estimate = bfs_exec_estimate_max(execbuf); bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", - execbuf->arg_min, execbuf->arg_max, estimate); + execbuf->arg_min, execbuf->arg_max, estimate); } } @@ -526,7 +503,7 @@ static size_t bfs_exec_update_max(struct bfs_exec *execbuf) { // Trim a fraction off the max size to avoid repeated failures near the // top end of the working range - size -= size/16; + size -= size / 16; if (size < execbuf->arg_max) { execbuf->arg_max = size; @@ -539,7 +516,7 @@ static size_t bfs_exec_update_max(struct bfs_exec *execbuf) { // Binary search for a more precise bound size_t estimate = bfs_exec_estimate_max(execbuf); bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", - execbuf->arg_min, execbuf->arg_max, estimate); + execbuf->arg_min, execbuf->arg_max, estimate); return estimate; } @@ -613,7 +590,7 @@ static bool bfs_exec_would_overflow(const struct bfs_exec *execbuf, const char * size_t next_size = execbuf->arg_size + bfs_exec_arg_size(arg); if (next_size > arg_max) { bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n", - next_size, arg_max); + next_size, arg_max); return true; } @@ -625,8 +602,8 @@ static int bfs_exec_push(struct bfs_exec *execbuf, char *arg) { execbuf->argv[execbuf->argc] = arg; if (execbuf->argc + 1 >= execbuf->argv_cap) { - size_t cap = 2*execbuf->argv_cap; - char **argv = realloc(execbuf->argv, cap*sizeof(*argv)); + size_t cap = 2 * execbuf->argv_cap; + char **argv = REALLOC_ARRAY(char *, execbuf->argv, execbuf->argv_cap, cap); if (!argv) { return -1; } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Implementation of -exec/-execdir/-ok/-okdir. @@ -80,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. @@ -94,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 @@ -107,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. */ diff --git a/src/expr.c b/src/expr.c new file mode 100644 index 0000000..ca37ffc --- /dev/null +++ b/src/expr.c @@ -0,0 +1,89 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "expr.h" + +#include "alloc.h" +#include "ctx.h" +#include "diag.h" +#include "eval.h" +#include "exec.h" +#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, enum bfs_kind kind) { + bfs_assert(kind != BFS_PATH); + + struct bfs_expr *expr = arena_alloc(&ctx->expr_arena); + if (!expr) { + return NULL; + } + + memset(expr, 0, sizeof(*expr)); + 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); + + if (bfs_expr_is_parent(expr)) { + SLIST_INIT(&expr->children); + } + + return expr; +} + +bool bfs_expr_is_parent(const struct bfs_expr *expr) { + return expr->eval_fn == eval_and + || expr->eval_fn == eval_or + || expr->eval_fn == eval_not + || expr->eval_fn == eval_comma; +} + +struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr) { + if (bfs_expr_is_parent(expr)) { + return expr->children.head; + } else { + return NULL; + } +} + +void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child) { + bfs_assert(bfs_expr_is_parent(expr)); + + SLIST_APPEND(&expr->children, child); + + if (!child->pure) { + expr->pure = false; + } + + expr->persistent_fds += child->persistent_fds; + if (expr->ephemeral_fds < child->ephemeral_fds) { + expr->ephemeral_fds = child->ephemeral_fds; + } +} + +void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children) { + drain_slist (struct bfs_expr, child, children) { + bfs_expr_append(expr, child); + } +} + +bool bfs_expr_never_returns(const struct bfs_expr *expr) { + // Expressions that never return are vacuously both always true and always false + return expr->always_true && expr->always_false; +} + +void bfs_expr_clear(struct bfs_expr *expr) { + if (expr->eval_fn == eval_exec) { + bfs_exec_free(expr->exec); + } else if (expr->eval_fn == eval_fprintf) { + bfs_printf_free(expr->printf); + } else if (expr->eval_fn == eval_regex) { + bfs_regfree(expr->regex); + } +} @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * The expression tree representation. @@ -24,12 +11,35 @@ #include "color.h" #include "eval.h" #include "stat.h" -#include <stdbool.h> -#include <stddef.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 { @@ -88,9 +98,22 @@ enum bfs_size_unit { }; /** + * A linked list of expressions. + */ +struct bfs_exprs { + struct bfs_expr *head; + struct bfs_expr **tail; +}; + +/** * A command line expression. */ struct bfs_expr { + /** This expression's next sibling, if any. */ + struct bfs_expr *next; + /** The next allocated expression. */ + struct { struct bfs_expr *next; } freelist; + /** The function that evaluates this expression. */ bfs_eval_fn *eval_fn; @@ -98,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; @@ -110,8 +135,8 @@ struct bfs_expr { bool always_true; /** Whether this expression always evaluates to false. */ bool always_false; - /** Whether this expression doesn't appear on the command line. */ - bool synthetic; + /** Whether this expression uses stat(). */ + bool calls_stat; /** Estimated cost. */ float cost; @@ -124,15 +149,10 @@ 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 { - /** The left hand side of the expression. */ - struct bfs_expr *lhs; - /** The right hand side of the expression. */ - struct bfs_expr *rhs; - }; + struct bfs_exprs children; /** Integer comparisons. */ struct { @@ -141,27 +161,33 @@ struct bfs_expr { /** The comparison mode. */ enum bfs_int_cmp int_cmp; - /** Optional extra data. */ - union { - /** -size data. */ - enum bfs_size_unit size_unit; - - /** Timestamp comparison data. */ - struct { - /** The stat field to look at. */ - enum bfs_stat_field stat_field; - /** The reference time. */ - struct timespec reftime; - /** The time unit. */ - enum bfs_time_unit time_unit; - }; - }; + /** -size data. */ + enum bfs_size_unit size_unit; + + /** The stat field to look at. */ + enum bfs_stat_field stat_field; + /** The time unit. */ + enum bfs_time_unit time_unit; + /** The reference time. */ + struct timespec reftime; + }; + + /** String comparisons. */ + struct { + /** String pattern. */ + const char *pattern; + /** fnmatch() flags. */ + int fnm_flags; + /** Whether strcmp() can be used instead of fnmatch(). */ + bool literal; }; /** Printing actions. */ struct { /** The output stream. */ CFILE *cfile; + /** Optional file path. */ + const char *path; /** Optional -printf format. */ struct bfs_printf *printf; }; @@ -202,20 +228,32 @@ struct bfs_expr { }; }; -/** Singleton true expression instance. */ -extern struct bfs_expr bfs_true; -/** Singleton false expression instance. */ -extern struct bfs_expr bfs_false; +struct bfs_ctx; /** * Create a new expression. */ -struct bfs_expr *bfs_expr_new(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. + */ +bool bfs_expr_is_parent(const struct bfs_expr *expr); + +/** + * @return The first child of this expression, or NULL if it has none. + */ +struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr); + +/** + * Add a child to an expression. + */ +void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child); /** - * @return Whether the expression has child expressions. + * Add a list of children to an expression. */ -bool bfs_expr_has_children(const struct bfs_expr *expr); +void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children); /** * @return Whether expr is known to always quit. @@ -228,8 +266,14 @@ bool bfs_expr_never_returns(const struct bfs_expr *expr); bool bfs_expr_cmp(const struct bfs_expr *expr, long long n); /** - * Free an expression tree. + * Free any resources owned by an expression. + */ +void bfs_expr_clear(struct bfs_expr *expr); + +/** + * Iterate over the children of an expression. */ -void bfs_expr_free(struct bfs_expr *expr); +#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 1444cf4..dfdf125 100644 --- a/src/fsade.c +++ b/src/fsade.c @@ -1,55 +1,58 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2021 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "fsade.h" + +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "dir.h" #include "dstring.h" -#include "util.h" +#include "sanity.h" + #include <errno.h> #include <fcntl.h> #include <stddef.h> #include <unistd.h> #if BFS_CAN_CHECK_ACL -# include <sys/acl.h> +# include <sys/acl.h> #endif #if BFS_CAN_CHECK_CAPABILITIES -# include <sys/capability.h> +# include <sys/capability.h> +#endif + +#if BFS_CAN_CHECK_CONTEXT +# include <selinux/selinux.h> #endif -#if BFS_HAS_SYS_EXTATTR -# include <sys/extattr.h> -#elif BFS_HAS_SYS_XATTR -# include <sys/xattr.h> +#if __has_include(<sys/extattr.h>) +# include <sys/extattr.h> +# define BFS_USE_EXTATTR true +#elif __has_include(<sys/xattr.h>) +# include <sys/xattr.h> +# define BFS_USE_XATTR true #endif -#if BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS +#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. */ +_maybe_unused static const char *fake_at(const struct BFTW *ftwbuf) { - static bool proc_works = true; - static bool proc_checked = false; + static atomic int proc_works = -1; - char *path = NULL; - if (!proc_works || ftwbuf->at_fd == AT_FDCWD) { + dchar *path = NULL; + if (ftwbuf->at_fd == (int)AT_FDCWD || load(&proc_works, relaxed) == 0) { goto fail; } @@ -58,11 +61,12 @@ static const char *fake_at(const struct BFTW *ftwbuf) { goto fail; } - if (!proc_checked) { - proc_checked = true; + if (load(&proc_works, relaxed) < 0) { if (xfaccessat(AT_FDCWD, path, F_OK) != 0) { - proc_works = false; + store(&proc_works, 0, relaxed); goto fail; + } else { + store(&proc_works, 1, relaxed); } } @@ -77,15 +81,17 @@ fail: return ftwbuf->path; } +_maybe_unused static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { if (path != ftwbuf->path) { - dstrfree((char *)path); + dstrfree((dchar *)path); } } /** * Check if an error was caused by the absence of support or data for a feature. */ +_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 @@ -124,28 +130,73 @@ static bool is_absence_error(int error) { return false; } -#endif // BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS - #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 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: + *entry = &acl->acl_entry[0]; + break; + case ACL_NEXT_ENTRY: + ++*entry; + break; + default: + errno = EINVAL; + return -1; + } + + acl_entry_t last = &acl->acl_entry[acl->acl_cnt]; + return *entry == last; +#else + errno = ENOTSUP; + return -1; +#endif +} + +/** Unified interface for acl_get_tag_type(). */ +_maybe_unused +static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) { +#if BFS_HAS_ACL_GET_TAG_TYPE + return acl_get_tag_type(entry, tag); +#elif __DragonFly__ + *tag = entry->ae_tag; + return 0; +#else + errno = ENOTSUP; + return -1; +#endif +} + /** Check if a POSIX.1e ACL is non-trivial. */ static int bfs_check_posix1e_acl(acl_t acl, bool ignore_required) { int ret = 0; acl_entry_t entry; - for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); -#if __APPLE__ - // POSIX.1e specifies a return value of 1 for success, but macOS - // returns 0 instead - status == 0; -#else + for (int status = bfs_acl_entry(acl, ACL_FIRST_ENTRY, &entry); status > 0; -#endif - status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { + status = bfs_acl_entry(acl, ACL_NEXT_ENTRY, &entry)) + { #if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER) if (ignore_required) { acl_tag_t tag; - if (acl_get_tag_type(entry, &tag) != 0) { + if (bfs_acl_tag_type(entry, &tag) != 0) { ret = -1; continue; } @@ -169,52 +220,56 @@ 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); -#if BFS_HAS_FEATURE(memory_sanitizer, false) - // msan seems to be missing an interceptor for acl_is_trivial_np() - trivial = 0; -#endif + // msan seems to be missing an interceptor for acl_is_trivial_np() + sanitize_init(&trivial); - if (acl_is_trivial_np(acl, &trivial) < 0) { + if (ret < 0) { return -1; } else if (trivial) { return 0; } 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 }; - static const size_t n_acl_types = sizeof(acl_types)/sizeof(acl_types[0]); - - if (ftwbuf->type == BFS_LNK) { - return 0; - } - - const char *path = fake_at(ftwbuf); int ret = -1, error = 0; - for (size_t i = 0; i < n_acl_types && ret <= 0; ++i) { + for (size_t i = 0; i < countof(acl_types) && ret <= 0; ++i) { acl_type_t type = acl_types[i]; if (type == ACL_TYPE_DEFAULT && ftwbuf->type != BFS_DIR) { @@ -236,6 +291,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) { error = errno; acl_free(acl); } +#endif free_fake_at(ftwbuf, path); errno = error; @@ -299,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_HAS_SYS_EXTATTR - 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; @@ -342,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_HAS_SYS_EXTATTR - 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; @@ -390,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 e964112..fbe02d8 100644 --- a/src/fsade.h +++ b/src/fsade.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019-2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A facade over (file)system features that are (un)implemented differently @@ -22,26 +9,26 @@ #ifndef BFS_FSADE_H #define BFS_FSADE_H -#include "util.h" -#include <stdbool.h> +#include "bfs.h" -#define BFS_CAN_CHECK_ACL BFS_HAS_SYS_ACL +#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL) -#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__ -# 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_HAS_SYS_EXTATTR || BFS_HAS_SYS_XATTR) +#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. @@ -51,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. @@ -61,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. @@ -71,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 diff --git a/src/ioq.c b/src/ioq.c new file mode 100644 index 0000000..57eb4a5 --- /dev/null +++ b/src/ioq.c @@ -0,0 +1,1330 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * An asynchronous I/O queue implementation. + * + * struct ioq is composed of two separate queues: + * + * struct ioqq *pending; // Pending I/O requests + * struct ioqq *ready; // Ready I/O responses + * + * Worker threads pop requests from `pending`, execute them, and push them back + * to the `ready` queue. The main thread pushes requests to `pending` and pops + * them from `ready`. + * + * struct ioqq is a blocking MPMC queue (though it could be SPMC/MPSC for + * pending/ready respectively). It is implemented as a circular buffer: + * + * size_t mask; // (1 << N) - 1 + * [padding] + * size_t head; // Writer index + * [padding] + * size_t tail; // Reader index + * [padding] + * ioq_slot slots[1 << N]; // Queue contents + * + * Pushes are implemented with an unconditional + * + * fetch_add(&ioqq->head, 1) + * + * which scales better on many architectures than compare-and-swap (see [1] for + * details). Pops are implemented similarly. Since the fetch-and-adds are + * unconditional, non-blocking readers can get ahead of writers: + * + * Reader Writer + * ──────────────── ────────────────────── + * head: 0 → 1 + * slots[0]: empty + * tail: 0 → 1 + * slots[0]: empty → full + * head: 1 → 2 + * slots[1]: empty! + * + * To avoid this, non-blocking reads (ioqq_pop(ioqq, false)) must mark the slots + * somehow so that writers can skip them: + * + * Reader Writer + * ─────────────────────── ─────────────────────── + * head: 0 → 1 + * slots[0]: empty → skip + * tail: 0 → 1 + * slots[0]: skip → empty + * tail: 1 → 2 + * slots[1]: empty → full + * head: 1 → 2 + * slots[1]: full → empty + * + * As well, a reader might "lap" a writer (or another reader), so slots need to + * count how many times they should be skipped: + * + * Reader Writer + * ────────────────────────── ───────────────────────── + * head: 0 → 1 + * slots[0]: empty → skip(1) + * head: 1 → 2 + * slots[1]: empty → skip(1) + * ... + * head: M → 0 + * slots[M]: empty → skip(1) + * head: 0 → 1 + * slots[0]: skip(1 → 2) + * tail: 0 → 1 + * slots[0]: skip(2 → 1) + * tail: 1 → 2 + * slots[1]: skip(1) → empty + * ... + * tail: M → 0 + * slots[M]: skip(1) → empty + * tail: 0 → 1 + * slots[0]: skip(1) → empty + * tail: 1 → 2 + * slots[1]: empty → full + * head: 1 → 2 + * slots[1]: full → empty + * + * As described in [1], this approach is susceptible to livelock if readers stay + * ahead of writers. This is okay for us because we don't retry failed non- + * blocking reads. + * + * The slot representation uses tag bits to hold either a pointer or skip(N): + * + * IOQ_SKIP (highest bit) IOQ_BLOCKED (lowest bit) + * ↓ ↓ + * 0 0 0 ... 0 0 0 + * └──────────┬──────────┘ + * │ + * value bits + * + * If IOQ_SKIP is unset, the value bits hold a pointer (or zero/NULL for empty). + * If IOQ_SKIP is set, the value bits hold a negative skip count. Writers can + * reduce the skip count by adding 1 to the value bits, and when the count hits + * zero, the carry will automatically clear IOQ_SKIP: + * + * IOQ_SKIP IOQ_BLOCKED + * ↓ ↓ + * 1 1 1 ... 1 0 0 skip(2) + * 1 1 1 ... 1 1 0 skip(1) + * 0 0 0 ... 0 0 0 empty + * + * The IOQ_BLOCKED flag is used to track sleeping waiters, futex-style. To wait + * for a slot to change, waiters call ioq_slot_wait() which sets IOQ_BLOCKED and + * goes to sleep. Whenever a slot is updated, if the old value had IOQ_BLOCKED + * set, ioq_slot_wake() must be called to wake up that waiter. + * + * Blocking/waking uses a pool of monitors (mutex, condition variable pairs). + * Slots are assigned round-robin to a monitor from the pool. + * + * [1]: https://arxiv.org/abs/2201.02179 + */ + +#include "ioq.h" + +#include "alloc.h" +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "bit.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_WITH_LIBURING +# include <liburing.h> +#endif + +/** + * A monitor for an I/O queue slot. + */ +struct ioq_monitor { + cache_align pthread_mutex_t mutex; + pthread_cond_t cond; +}; + +/** Initialize an ioq_monitor. */ +static int ioq_monitor_init(struct ioq_monitor *monitor) { + if (mutex_init(&monitor->mutex, NULL) != 0) { + return -1; + } + + if (cond_init(&monitor->cond, NULL) != 0) { + mutex_destroy(&monitor->mutex); + return -1; + } + + return 0; +} + +/** Destroy an ioq_monitor. */ +static void ioq_monitor_destroy(struct ioq_monitor *monitor) { + cond_destroy(&monitor->cond); + mutex_destroy(&monitor->mutex); +} + +/** A single entry in a command queue. */ +typedef atomic uintptr_t ioq_slot; + +/** Someone might be waiting on this slot. */ +#define IOQ_BLOCKED ((uintptr_t)1) + +/** Bit for IOQ_SKIP. */ +#define IOQ_SKIP_BIT (UINTPTR_WIDTH - 1) +/** The next push(es) should skip this slot. */ +#define IOQ_SKIP ((uintptr_t)1 << IOQ_SKIP_BIT) +/** Amount to add for an additional skip. */ +#define IOQ_SKIP_ONE (~IOQ_BLOCKED) + +static_assert(alignof(struct ioq_ent) >= (1 << 2), "struct ioq_ent is underaligned"); + +/** + * An MPMC queue of I/O commands. + */ +struct ioqq { + /** Circular buffer index mask. */ + size_t slot_mask; + + /** Monitor index mask. */ + size_t monitor_mask; + /** Array of monitors used by the slots. */ + struct ioq_monitor *monitors; + + /** Index of next writer. */ + cache_align atomic size_t head; + /** Index of next reader. */ + cache_align atomic size_t tail; + + /** The circular buffer itself. */ + cache_align ioq_slot slots[]; // _counted_by(slot_mask + 1) +}; + +/** Destroy an I/O command queue. */ +static void ioqq_destroy(struct ioqq *ioqq) { + if (!ioqq) { + return; + } + + for (size_t i = 0; i < ioqq->monitor_mask + 1; ++i) { + ioq_monitor_destroy(&ioqq->monitors[i]); + } + free(ioqq->monitors); + free(ioqq); +} + +/** Create an I/O command queue. */ +static struct ioqq *ioqq_create(size_t size) { + // Circular buffer size must be a power of two + size = bit_ceil(size); + + struct ioqq *ioqq = ALLOC_FLEX(struct ioqq, slots, size); + if (!ioqq) { + return NULL; + } + + ioqq->slot_mask = size - 1; + ioqq->monitor_mask = -1; + + // Use a pool of monitors + size_t nmonitors = size < 64 ? size : 64; + ioqq->monitors = ALLOC_ARRAY(struct ioq_monitor, nmonitors); + if (!ioqq->monitors) { + ioqq_destroy(ioqq); + return NULL; + } + + for (size_t i = 0; i < nmonitors; ++i) { + if (ioq_monitor_init(&ioqq->monitors[i]) != 0) { + ioqq_destroy(ioqq); + return NULL; + } + ++ioqq->monitor_mask; + } + + atomic_init(&ioqq->head, 0); + atomic_init(&ioqq->tail, 0); + + for (size_t i = 0; i < size; ++i) { + atomic_init(&ioqq->slots[i], 0); + } + + return ioqq; +} + +/** Get the monitor associated with a slot. */ +static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) { + 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. */ +_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); + + ret = load(slot, relaxed); + if (ret != value) { + goto done; + } + + if (!(value & IOQ_BLOCKED)) { + value |= IOQ_BLOCKED; + if (!compare_exchange_strong(slot, &ret, value, relaxed, relaxed)) { + goto done; + } + } + + do { + // To avoid missed wakeups, it is important that + // cond_broadcast() is not called right here + cond_wait(&monitor->cond, &monitor->mutex); + ret = load(slot, relaxed); + } while (ret == value); + +done: + mutex_unlock(&monitor->mutex); + return ret; +} + +/** Wake up any threads waiting on a slot. */ +_noinline +static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) { + struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); + + // The following implementation would clearly avoid the missed wakeup + // issue mentioned above in ioq_slot_wait(): + // + // mutex_lock(&monitor->mutex); + // cond_broadcast(&monitor->cond); + // mutex_unlock(&monitor->mutex); + // + // As a minor optimization, we move the broadcast outside of the lock. + // This optimization is correct, even though it leads to a seemingly- + // useless empty critical section. + + mutex_lock(&monitor->mutex); + mutex_unlock(&monitor->mutex); + cond_broadcast(&monitor->cond); +} + +/** 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. */ +static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent) { + uintptr_t prev = load(slot, relaxed); + + while (true) { + 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; + // skip(1) → empty + // skip(n) → skip(n - 1) + next = ioq_slot_blend(prev, prev - IOQ_SKIP_ONE, next); + + if (compare_exchange_weak(slot, &prev, next, release, relaxed)) { + break; + } + } + + if (prev & IOQ_BLOCKED) { + ioq_slot_wake(ioqq, slot); + } + + return !(prev & IOQ_SKIP); +} + +/** (Try to) pop an entry from a slot. */ +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; + // full(ptr) → 0 + next = ioq_slot_blend(next, next, 0); + + if (block && next) { + prev = ioq_slot_wait(ioqq, slot, prev); + continue; + } + + if (compare_exchange_weak(slot, &prev, next, acquire, relaxed)) { + break; + } + } + + if (prev & IOQ_BLOCKED) { + ioq_slot_wake(ioqq, slot); + } + + // empty → 0 + // skip(n) → 0 + // full(ptr) → ptr + prev = ioq_slot_blend(prev, 0, prev); + return (struct ioq_ent *)(prev << 1); +} + +/** Push an entry onto the queue. */ +static void ioqq_push(struct ioqq *ioqq, struct ioq_ent *ent) { + while (true) { + size_t i = fetch_add(&ioqq->head, 1, relaxed); + ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask]; + if (ioq_slot_push(ioqq, slot, ent)) { + break; + } + } +} + +/** Push a batch of entries to the queue. */ +static void ioqq_push_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size) { + size_t mask = ioqq->slot_mask; + do { + size_t i = fetch_add(&ioqq->head, size, relaxed); + for (size_t j = i + size; i != j; ++i) { + ioq_slot *slot = &ioqq->slots[i & mask]; + if (ioq_slot_push(ioqq, slot, *batch)) { + ++batch; + --size; + } + } + } while (size > 0); +} + +/** 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; + size_t i = fetch_add(&ioqq->tail, size, relaxed); + for (size_t j = i + size; i != j; ++i) { + ioq_slot *slot = &ioqq->slots[i & mask]; + *batch++ = ioq_slot_pop(ioqq, slot, block); + block = false; + } +} + +/** Use cache-line-sized batches. */ +#define IOQ_BATCH (FALSE_SHARING_SIZE / sizeof(ioq_slot)) + +/** + * A batch of I/O queue entries. + */ +struct ioq_batch { + /** 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]; +}; + +/** 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->tail > 0) { + ioqq_push_batch(ioqq, batch->entries, batch->tail); + ioq_batch_reset(batch); + } +} + +/** 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) { + 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); + + 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_WITH_LIBURING +/** + * Supported io_uring operations. + */ +enum ioq_ring_ops { + IOQ_RING_OPENAT = 1 << 0, + IOQ_RING_CLOSE = 1 << 1, + IOQ_RING_STATX = 1 << 2, +}; +#endif + +/** I/O queue thread-specific data. */ +struct ioq_thread { + /** The thread handle. */ + pthread_t id; + /** Pointer back to the I/O queue. */ + struct ioq *parent; + +#if BFS_WITH_LIBURING + /** io_uring instance. */ + struct io_uring ring; + /** Any error that occurred initializing the ring. */ + int ring_err; + /** Bitmask of supported io_uring operations. */ + enum ioq_ring_ops ring_ops; +#endif +}; + +struct ioq { + /** The depth of the queue. */ + size_t depth; + /** The current size of the queue. */ + size_t size; + /** Cancellation flag. */ + atomic bool cancel; + + /** ioq_ent arena. */ + struct arena ents; +#if BFS_WITH_LIBURING && BFS_USE_STATX + /** struct statx arena. */ + struct arena xbufs; +#endif + + /** Pending I/O request queue. */ + struct ioqq *pending; + /** 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[] _counted_by(nthreads); +}; + +/** Cancel a request if we need to. */ +static bool ioq_check_cancel(struct ioq *ioq, struct ioq_ent *ent) { + if (!load(&ioq->cancel, relaxed)) { + return false; + } + + // Always close(), even if we're cancelled, just like a real EINTR + if (ent->op == IOQ_CLOSE || ent->op == IOQ_CLOSEDIR) { + return false; + } + + ent->result = -EINTR; + return true; +} + +/** 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; + + case IOQ_OPENDIR: { + struct ioq_opendir *args = &ent->opendir; + ent->result = try(bfs_opendir(args->dir, args->dfd, args->path, args->flags)); + if (ent->result >= 0) { + bfs_polldir(args->dir); + } + return; + } + + case IOQ_CLOSEDIR: + ent->result = try(bfs_closedir(ent->closedir.dir)); + return; + + case IOQ_STAT: { + struct ioq_stat *args = &ent->stat; + ent->result = try(bfs_stat(args->dfd, args->path, args->flags, args->buf)); + return; + } + } + + bfs_bug("Unknown ioq_op %d", (int)ent->op); + ent->result = -ENOSYS; +} + +#if BFS_WITH_LIBURING + +/** io_uring worker state. */ +struct ioq_ring_state { + /** The I/O queue. */ + struct ioq *ioq; + /** The io_uring. */ + struct io_uring *ring; + /** Supported io_uring operations. */ + enum ioq_ring_ops ops; + /** Number of prepped, unsubmitted SQEs. */ + size_t prepped; + /** Number of submitted, unreaped SQEs. */ + size_t submitted; + /** Whether to stop the loop. */ + bool stop; + /** A batch of ready entries. */ + 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) { + 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 = ioq_get_sqe(state); + io_uring_prep_close(sqe, ent->close.fd); + } + return sqe; + + case IOQ_OPENDIR: + if (ops & IOQ_RING_OPENAT) { + 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); + } + return sqe; + + case IOQ_CLOSEDIR: +#if BFS_USE_UNWRAPDIR + if (ops & IOQ_RING_CLOSE) { + sqe = ioq_get_sqe(state); + io_uring_prep_close(sqe, bfs_unwrapdir(ent->closedir.dir)); + } +#endif + return sqe; + + case IOQ_STAT: +#if BFS_USE_STATX + if (ops & IOQ_RING_STATX) { + sqe = ioq_get_sqe(state); + struct ioq_stat *args = &ent->stat; + int flags = bfs_statx_flags(args->flags); + unsigned int mask = bfs_statx_mask(); + io_uring_prep_statx(sqe, args->dfd, args->path, flags, mask, args->xbuf); + } +#endif + return sqe; + } + + bfs_bug("Unknown ioq_op %d", (int)ent->op); + return NULL; +} + +/** Check if ioq_ring_reap() has work to do. */ +static bool ioq_ring_empty(struct ioq_ring_state *state) { + return !state->prepped && !state->submitted && ioq_batch_empty(&state->ready); +} + +/** Prep a single SQE. */ +static void ioq_prep_sqe(struct ioq_ring_state *state, struct ioq_ent *ent) { + struct ioq *ioq = state->ioq; + if (ioq_check_cancel(ioq, ent)) { + ioq_batch_push(ioq->ready, &state->ready, ent); + return; + } + + struct io_uring_sqe *sqe = ioq_dispatch_async(state, ent); + if (sqe) { + io_uring_sqe_set_data(sqe, ent); + ++state->prepped; + } else { + ioq_dispatch_sync(ioq, ent); + ioq_batch_push(ioq->ready, &state->ready, ent); + } +} + +/** Prep a batch of SQEs. */ +static bool ioq_ring_prep(struct ioq_ring_state *state) { + if (state->stop) { + return false; + } + + struct ioq *ioq = state->ioq; + + 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; + } else if (ent) { + ioq_prep_sqe(state, ent); + } else { + break; + } + } + + bfs_assert(ioq_batch_empty(&pending)); + return !ioq_ring_empty(state); +} + +/** io_uring worker loop. */ +static int ioq_ring_work(struct ioq_thread *thread) { + struct io_uring *ring = &thread->ring; + +#ifdef IORING_SETUP_R_DISABLED + if (ring->flags & IORING_SETUP_R_DISABLED) { + if (io_uring_enable_rings(ring) != 0) { + return -1; + } + } +#endif + + struct ioq_ring_state state = { + .ioq = thread->parent, + .ring = ring, + .ops = thread->ring_ops, + }; + + while (ioq_ring_prep(&state)) { + ioq_ring_submit(&state); + } + + ioq_ring_drain(&state, state.submitted); + return 0; +} + +#endif // BFS_WITH_LIBURING + +/** Synchronous syscall loop. */ +static void ioq_sync_work(struct ioq_thread *thread) { + struct ioq *ioq = thread->parent; + + 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; + } + + 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_WITH_LIBURING + if (thread->ring_err == 0) { + if (ioq_ring_work(thread) == 0) { + return NULL; + } + } +#endif + + ioq_sync_work(thread); + 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_WITH_LIBURING + struct ioq_thread *prev = NULL; + if (thread > ioq->threads) { + prev = thread - 1; + } + + if (prev && prev->ring_err) { + thread->ring_err = prev->ring_err; + return -1; + } + + struct io_uring_params params = {0}; + + if (prev) { + // 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 + size_t entries = 4096 / sizeof(struct io_uring_sqe); + thread->ring_err = -io_uring_queue_init_params(entries, &thread->ring, ¶ms); + if (thread->ring_err) { + return -1; + } + + if (prev) { + // Initial setup already complete + thread->ring_ops = prev->ring_ops; + return 0; + } + + // Check for supported operations + struct io_uring_probe *probe = io_uring_get_probe_ring(&thread->ring); + if (probe) { + if (io_uring_opcode_supported(probe, IORING_OP_OPENAT)) { + thread->ring_ops |= IOQ_RING_OPENAT; + } + if (io_uring_opcode_supported(probe, IORING_OP_CLOSE)) { + thread->ring_ops |= IOQ_RING_CLOSE; + } +#if BFS_USE_STATX + if (io_uring_opcode_supported(probe, IORING_OP_STATX)) { + thread->ring_ops |= IOQ_RING_STATX; + } +#endif + io_uring_free_probe(probe); + } + if (!thread->ring_ops) { + io_uring_queue_exit(&thread->ring); + thread->ring_err = ENOTSUP; + return -1; + } + +#if BFS_HAS_IO_URING_MAX_WORKERS + // Limit the number of io_uring workers + unsigned int values[] = { + ioq->nthreads, // [IO_WQ_BOUND] + 0, // [IO_WQ_UNBOUND] + }; + 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_WITH_LIBURING + if (thread->ring_err == 0) { + io_uring_queue_exit(&thread->ring); + } +#endif +} + +/** Create an I/O queue 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); + + if (thread_create(&thread->id, NULL, ioq_work, thread) != 0) { + ioq_ring_exit(thread); + return -1; + } + + char name[16]; + if (snprintf(name, sizeof(name), "ioq-%zu", i) >= 0) { + thread_setname(thread->id, name); + } + + return 0; +} + +/** Join an I/O queue thread. */ +static void ioq_thread_join(struct ioq_thread *thread) { + thread_join(thread->id, NULL); + ioq_ring_exit(thread); +} + +struct ioq *ioq_create(size_t depth, size_t nthreads) { + struct ioq *ioq = ZALLOC_FLEX(struct ioq, threads, nthreads); + if (!ioq) { + goto fail; + } + + ioq->depth = depth; + + ARENA_INIT(&ioq->ents, struct ioq_ent); +#if BFS_WITH_LIBURING && BFS_USE_STATX + ARENA_INIT(&ioq->xbufs, struct statx); +#endif + + ioq->pending = ioqq_create(depth); + if (!ioq->pending) { + goto fail; + } + + ioq->ready = ioqq_create(depth); + if (!ioq->ready) { + goto fail; + } + + ioq->nthreads = nthreads; + for (size_t i = 0; i < nthreads; ++i) { + if (ioq_thread_create(ioq, i) != 0) { + ioq->nthreads = i; + goto fail; + } + } + + return ioq; + + int err; +fail: + err = errno; + ioq_destroy(ioq); + errno = err; + return NULL; +} + +size_t ioq_capacity(const struct ioq *ioq) { + return ioq->depth - ioq->size; +} + +static struct ioq_ent *ioq_request(struct ioq *ioq, enum ioq_op op, void *ptr) { + if (load(&ioq->cancel, relaxed)) { + errno = EINTR; + return NULL; + } + + if (ioq->size >= ioq->depth) { + errno = EAGAIN; + return NULL; + } + + struct ioq_ent *ent = arena_alloc(&ioq->ents); + if (!ent) { + return NULL; + } + + ent->op = op; + ent->ptr = ptr; + ++ioq->size; + 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) { + return -1; + } + + ent->close.fd = fd; + + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); + return 0; +} + +int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr) { + struct ioq_ent *ent = ioq_request(ioq, IOQ_OPENDIR, ptr); + if (!ent) { + return -1; + } + + struct ioq_opendir *args = &ent->opendir; + args->dir = dir; + args->dfd = dfd; + args->path = path; + args->flags = flags; + + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); + return 0; +} + +int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr) { + struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSEDIR, ptr); + if (!ent) { + return -1; + } + + ent->closedir.dir = dir; + + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); + return 0; +} + +int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr) { + struct ioq_ent *ent = ioq_request(ioq, IOQ_STAT, ptr); + if (!ent) { + return -1; + } + + struct ioq_stat *args = &ent->stat; + args->dfd = dfd; + args->path = path; + args->flags = flags; + args->buf = buf; + +#if BFS_WITH_LIBURING && BFS_USE_STATX + args->xbuf = arena_alloc(&ioq->xbufs); + if (!args->xbuf) { + ioq_free(ioq, ent); + return -1; + } +#endif + + 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 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_WITH_LIBURING && BFS_USE_STATX + if (ent->op == IOQ_STAT && ent->stat.xbuf) { + arena_free(&ioq->xbufs, ent->stat.xbuf); + } +#endif + + arena_free(&ioq->ents, ent); +} + +void ioq_cancel(struct ioq *ioq) { + if (!exchange(&ioq->cancel, true, relaxed)) { + ioq_batch_push(ioq->pending, &ioq->pending_batch, &IOQ_STOP); + ioq_submit(ioq); + } +} + +void ioq_destroy(struct ioq *ioq) { + if (!ioq) { + return; + } + + if (ioq->nthreads > 0) { + ioq_cancel(ioq); + } + + for (size_t i = 0; i < ioq->nthreads; ++i) { + ioq_thread_join(&ioq->threads[i]); + } + + ioqq_destroy(ioq->ready); + ioqq_destroy(ioq->pending); + +#if BFS_WITH_LIBURING && BFS_USE_STATX + arena_destroy(&ioq->xbufs); +#endif + arena_destroy(&ioq->ents); + + free(ioq); +} diff --git a/src/ioq.h b/src/ioq.h new file mode 100644 index 0000000..5eaa066 --- /dev/null +++ b/src/ioq.h @@ -0,0 +1,227 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Asynchronous I/O queues. + */ + +#ifndef BFS_IOQ_H +#define BFS_IOQ_H + +#include "bfs.h" +#include "dir.h" +#include "stat.h" + +#include <stddef.h> + +/** + * An queue of asynchronous I/O operations. + */ +struct ioq; + +/** + * I/O queue operations. + */ +enum ioq_op { + /** ioq_nop(). */ + IOQ_NOP, + /** ioq_close(). */ + IOQ_CLOSE, + /** ioq_opendir(). */ + IOQ_OPENDIR, + /** ioq_closedir(). */ + IOQ_CLOSEDIR, + /** ioq_stat(). */ + IOQ_STAT, +}; + +/** + * ioq_nop() types. + */ +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. */ + cache_align enum ioq_op op; + + /** The return value (on success) or negative error code (on failure). */ + int result; + + /** Arbitrary user data. */ + void *ptr; + + /** Operation-specific arguments. */ + union { + /** ioq_nop() args. */ + struct ioq_nop { + enum ioq_nop_type type; + } nop; + /** ioq_close() args. */ + struct ioq_close { + int fd; + } close; + /** ioq_opendir() args. */ + struct ioq_opendir { + struct bfs_dir *dir; + const char *path; + int dfd; + enum bfs_dir_flags flags; + } opendir; + /** ioq_closedir() args. */ + struct ioq_closedir { + struct bfs_dir *dir; + } closedir; + /** ioq_stat() args. */ + struct ioq_stat { + const char *path; + struct bfs_stat *buf; + void *xbuf; + int dfd; + enum bfs_stat_flags flags; + } stat; + }; +}; + +/** + * Create an I/O queue. + * + * @depth + * The maximum depth of the queue. + * @nthreads + * The maximum number of background threads. + * @return + * The new I/O queue, or NULL on failure. + */ +struct ioq *ioq_create(size_t depth, size_t nthreads); + +/** + * Check the remaining capacity of a queue. + */ +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(). + * + * @ioq + * The I/O queue. + * @fd + * The fd to close. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +int ioq_close(struct ioq *ioq, int fd, void *ptr); + +/** + * Asynchronous bfs_opendir(). + * + * @ioq + * The I/O queue. + * @dir + * The allocated directory. + * @dfd + * The base file descriptor. + * @path + * The path to open, relative to dfd. + * @flags + * Flags that control which directory entries are listed. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr); + +/** + * Asynchronous bfs_closedir(). + * + * @ioq + * The I/O queue. + * @dir + * The directory to close. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr); + +/** + * Asynchronous bfs_stat(). + * + * @ioq + * The I/O queue. + * @dfd + * The base file descriptor. + * @path + * The path to stat, relative to dfd. + * @flags + * Flags that affect the lookup. + * @buf + * A place to store the stat buffer, if successful. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +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. + * + * @ioq + * The I/O queue. + * @return + * The next response, or NULL. + */ +struct ioq_ent *ioq_pop(struct ioq *ioq, bool block); + +/** + * Free a queue entry. + * + * @ioq + * The I/O queue. + * @ent + * The entry to free. + */ +void ioq_free(struct ioq *ioq, struct ioq_ent *ent); + +/** + * Cancel any pending I/O operations. + */ +void ioq_cancel(struct ioq *ioq); + +/** + * Stop and destroy an I/O queue. + */ +void ioq_destroy(struct ioq *ioq); + +#endif // BFS_IOQ_H diff --git a/src/list.h b/src/list.h new file mode 100644 index 0000000..276c610 --- /dev/null +++ b/src/list.h @@ -0,0 +1,613 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Intrusive linked lists. + * + * Singly-linked lists are declared like this: + * + * struct item { + * struct item *next; + * }; + * + * struct list { + * struct item *head; + * struct item **tail; + * }; + * + * The SLIST_*() macros manipulate singly-linked lists. + * + * struct list list; + * SLIST_INIT(&list); + * + * struct item item; + * SLIST_ITEM_INIT(&item); + * SLIST_APPEND(&list, &item); + * + * Doubly linked lists are similar: + * + * struct item { + * struct item *next; + * struct item *prev; + * }; + * + * struct list { + * struct item *head; + * struct item *tail; + * }; + * + * struct list list; + * LIST_INIT(&list); + * + * struct item item; + * LIST_ITEM_INIT(&item); + * LIST_APPEND(&list, &item); + * + * Items can be on multiple lists at once: + * + * struct item { + * struct { + * struct item *next; + * } chain; + * + * struct { + * struct item *next; + * struct item *prev; + * } lru; + * }; + * + * struct items { + * struct { + * struct item *head; + * struct item **tail; + * } queue; + * + * struct { + * struct item *head; + * struct item *tail; + * } cache; + * }; + * + * struct items items; + * SLIST_INIT(&items.queue); + * LIST_INIT(&items.cache); + * + * struct item item; + * SLIST_ITEM_INIT(&item, chain); + * SLIST_APPEND(&items.queue, &item, chain); + * LIST_ITEM_INIT(&item, lru); + * LIST_APPEND(&items.cache, &item, lru); + */ + +#ifndef BFS_LIST_H +#define BFS_LIST_H + +#include "diag.h" + +#include <stddef.h> +#include <string.h> + +/** + * Initialize a singly-linked list. + * + * @list + * The list to initialize. + * + * --- + * + * Like many macros in this file, this macro delegates the bulk of its work to + * some helper macros. We explicitly parenthesize (list) here so the helpers + * don't have to. + */ +#define SLIST_INIT(list) \ + SLIST_INIT_((list)) + +/** + * Helper for SLIST_INIT(). + */ +#define SLIST_INIT_(list) LIST_VOID_( \ + list->head = NULL, \ + list->tail = &list->head) + +/** + * Cast a list of expressions to void. + */ +#define LIST_VOID_(...) ((void)(__VA_ARGS__)) + +/** + * Initialize a singly-linked list item. + * + * @item + * The item to initialize. + * @node (optional) + * If specified, use item->node.next rather than item->next. + * + * --- + * + * We play some tricks with variadic macros to handle the optional parameter: + * + * SLIST_ITEM_INIT(item) => item->next = NULL + * SLIST_ITEM_INIT(item, node) => item->node.next = NULL + * + * The first trick is that + * + * #define SLIST_ITEM_INIT(item, ...) + * + * won't work because both commas are required (until C23; see N3033). As a + * workaround, we dispatch to another macro and add a trailing comma. + * + * SLIST_ITEM_INIT(item) => SLIST_ITEM_INIT_(item, ) + * SLIST_ITEM_INIT(item, node) => SLIST_ITEM_INIT_(item, node, ) + */ +#define SLIST_ITEM_INIT(...) \ + SLIST_ITEM_INIT_(__VA_ARGS__, ) + +/** + * Now we need a way to generate either ->next or ->node.next depending on + * whether the node parameter was passed. The approach is based on + * + * #define FOO(...) BAR(__VA_ARGS__, 1, 2, ) + * #define BAR(x, y, z, ...) z + * + * FOO(a) => 2 + * FOO(a, b) => 1 + * + * The LIST_NEXT_() macro uses this technique: + * + * LIST_NEXT_() => LIST_NODE_(next, ) + * LIST_NEXT_(node, ) => LIST_NODE_(next, node, ) + */ +#define LIST_NEXT_(...) \ + LIST_NODE_(next, __VA_ARGS__) + +/** + * LIST_NODE_() dispatches to yet another macro: + * + * LIST_NODE_(next, ) => LIST_NODE__(next, , . , , ) + * LIST_NODE_(next, node, ) => LIST_NODE__(next, node, , . , , ) + */ +#define LIST_NODE_(dir, ...) \ + LIST_NODE__(dir, __VA_ARGS__, . , , ) + +/** + * And finally, LIST_NODE__() adds the node and the dot if necessary. + * + * dir node ignored dot + * v v v v + * LIST_NODE__(next, , . , , ) => next + * LIST_NODE__(next, node, , . , , ) => node . next + * ^ ^ ^ ^ + * dir node ignored dot + */ +#define LIST_NODE__(dir, node, ignored, dot, ...) \ + node dot dir + +/** + * SLIST_ITEM_INIT_() uses LIST_NEXT_() to generate the right name for the list + * node, and finally delegates to the actual implementation. + */ +#define SLIST_ITEM_INIT_(item, ...) \ + SLIST_ITEM_INIT__((item), LIST_NEXT_(__VA_ARGS__)) + +#define SLIST_ITEM_INIT__(item, next) \ + LIST_VOID_(item->next = NULL) + +/** + * Type-checking macro for singly-linked lists. + */ +#define SLIST_CHECK_(list) \ + (void)sizeof(list->tail - &list->head) + +/** + * Get the head of a singly-linked list. + * + * @list + * The list in question. + * @return + * The first item in the list. + */ +#define SLIST_HEAD(list) \ + SLIST_HEAD_((list)) + +#define SLIST_HEAD_(list) \ + (SLIST_CHECK_(list), list->head) + +/** + * Check if a singly-linked list is empty. + */ +#define SLIST_EMPTY(list) \ + (!SLIST_HEAD(list)) + +/** + * Like container_of(), but using the head pointer instead of offsetof() since + * we don't have the type around. + */ +#define SLIST_CONTAINER_(tail, head, next) \ + (void *)((char *)tail - ((char *)&head->next - (char *)head)) + +/** + * Get the tail of a singly-linked list. + * + * @list + * The list in question. + * @node (optional) + * If specified, use item->node.next rather than item->next. + * @return + * The last item in the list. + */ +#define SLIST_TAIL(...) \ + SLIST_TAIL_(__VA_ARGS__, ) + +#define SLIST_TAIL_(list, ...) \ + SLIST_TAIL__((list), LIST_NEXT_(__VA_ARGS__)) + +#define SLIST_TAIL__(list, next) \ + (list->head ? SLIST_CONTAINER_(list->tail, list->head, next) : NULL) + +/** + * Check if an item is attached to a singly-linked list. + * + * @list + * The list to check. + * @item + * The item to check. + * @node (optional) + * If specified, use item->node.next rather than item->next. + * @return + * Whether the item is attached to the list. + */ +#define SLIST_ATTACHED(list, ...) \ + SLIST_ATTACHED_(list, __VA_ARGS__, ) + +#define SLIST_ATTACHED_(list, item, ...) \ + SLIST_ATTACHED__((list), (item), LIST_NEXT_(__VA_ARGS__)) + +#define SLIST_ATTACHED__(list, item, next) \ + (item->next || list->tail == &item->next) + +/** + * Insert an item into a singly-linked list. + * + * @list + * The list to modify. + * @cursor + * A pointer to the item to insert after, e.g. &list->head or list->tail. + * @item + * The item to insert. + * @node (optional) + * If specified, use item->node.next rather than item->next. + * @return + * A cursor for the next item. + */ +#define SLIST_INSERT(list, cursor, ...) \ + SLIST_INSERT_(list, cursor, __VA_ARGS__, ) + +#define SLIST_INSERT_(list, cursor, item, ...) \ + SLIST_INSERT__((list), (cursor), (item), LIST_NEXT_(__VA_ARGS__)) + +#define SLIST_INSERT__(list, cursor, item, next) \ + (bfs_assert(!SLIST_ATTACHED__(list, item, next)), \ + item->next = *cursor, \ + *cursor = item, \ + list->tail = item->next ? list->tail : &item->next, \ + &item->next) + +/** + * Add an item to the tail of a singly-linked list. + * + * @list + * The list to modify. + * @item + * The item to append. + * @node (optional) + * If specified, use item->node.next rather than item->next. + */ +#define SLIST_APPEND(list, ...) \ + SLIST_APPEND_(list, __VA_ARGS__, ) + +#define SLIST_APPEND_(list, item, ...) \ + LIST_VOID_(SLIST_INSERT_(list, (list)->tail, item, __VA_ARGS__)) + +/** + * Add an item to the head of a singly-linked list. + * + * @list + * The list to modify. + * @item + * The item to prepend. + * @node (optional) + * If specified, use item->node.next rather than item->next. + */ +#define SLIST_PREPEND(list, ...) \ + SLIST_PREPEND_(list, __VA_ARGS__, ) + +#define SLIST_PREPEND_(list, item, ...) \ + 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. + * + * @dest + * The destination list. + * @src + * The source list. + */ +#define SLIST_EXTEND(dest, src) \ + SLIST_SPLICE(dest, (dest)->tail, src) + +/** + * Remove an item from a singly-linked list. + * + * @list + * The list to modify. + * @cursor + * A pointer to the item to remove, either &list->head or &prev->next. + * @node (optional) + * If specified, use item->node.next rather than item->next. + * @return + * The removed item. + */ +#define SLIST_REMOVE(list, ...) \ + SLIST_REMOVE_(list, __VA_ARGS__, ) + +#define SLIST_REMOVE_(list, cursor, ...) \ + SLIST_REMOVE__((list), (cursor), LIST_NEXT_(__VA_ARGS__)) + +#define SLIST_REMOVE__(list, cursor, next) \ + (list->tail = (*cursor)->next ? list->tail : cursor, \ + slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) + +// Helper for SLIST_REMOVE() +static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) { + // ret = *cursor; + // *cursor = ret->next; + memcpy(cursor, next, size); + // ret->next = NULL; + memset(next, 0, size); + return ret; +} + +/** + * Pop the head off a singly-linked list. + * + * @list + * The list to modify. + * @node (optional) + * If specified, use head->node.next rather than head->next. + * @return + * The popped item, or NULL if the list was empty. + */ +#define SLIST_POP(...) \ + SLIST_POP_(__VA_ARGS__, ) + +#define SLIST_POP_(list, ...) \ + SLIST_POP__((list), __VA_ARGS__) + +#define SLIST_POP__(list, ...) \ + (list->head ? SLIST_REMOVE_(list, &list->head, __VA_ARGS__) : NULL) + +/** + * Loop over the items in a singly-linked list. + * + * @type + * The list item type. + * @item + * The induction variable name. + * @list + * The list to iterate. + * @node (optional) + * If specified, use head->node.next rather than head->next. + */ +#define for_slist(type, item, ...) \ + for_slist_(type, item, __VA_ARGS__, ) + +#define for_slist_(type, item, list, ...) \ + for_slist__(type, item, (list), LIST_NEXT_(__VA_ARGS__)) + +#define for_slist__(type, item, list, next) \ + for (type *item = list->head, *_next; \ + item && (SLIST_CHECK_(list), _next = item->next, true); \ + 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. + * + * @list + * The list to initialize. + */ +#define LIST_INIT(list) \ + LIST_INIT_((list)) + +#define LIST_INIT_(list) \ + LIST_VOID_(list->head = list->tail = NULL) + +/** + * LIST_PREV_() => prev + * LIST_PREV_(node, ) => node.prev + */ +#define LIST_PREV_(...) \ + LIST_NODE_(prev, __VA_ARGS__) + +/** + * Initialize a doubly-linked list item. + * + * @item + * The item to initialize. + * @node (optional) + * If specified, use item->node.next rather than item->next. + */ +#define LIST_ITEM_INIT(...) \ + LIST_ITEM_INIT_(__VA_ARGS__, ) + +#define LIST_ITEM_INIT_(item, ...) \ + LIST_ITEM_INIT__((item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__)) + +#define LIST_ITEM_INIT__(item, prev, next) \ + LIST_VOID_(item->prev = item->next = NULL) + +/** + * Type-checking macro for doubly-linked lists. + */ +#define LIST_CHECK_(list) \ + (void)sizeof(list->tail - list->head) + +/** + * Check if a doubly-linked list is empty. + */ +#define LIST_EMPTY(list) \ + LIST_EMPTY_((list)) + +#define LIST_EMPTY_(list) \ + (LIST_CHECK_(list), !list->head) + +/** + * Add an item to the tail of a doubly-linked list. + * + * @list + * The list to modify. + * @item + * The item to append. + * @node (optional) + * If specified, use item->node.{prev,next} rather than item->{prev,next}. + */ +#define LIST_APPEND(list, ...) \ + LIST_INSERT(list, (list)->tail, __VA_ARGS__) + +/** + * Add an item to the head of a doubly-linked list. + * + * @list + * The list to modify. + * @item + * The item to prepend. + * @node (optional) + * If specified, use item->node.{prev,next} rather than item->{prev,next}. + */ +#define LIST_PREPEND(list, ...) \ + LIST_INSERT(list, NULL, __VA_ARGS__) + +/** + * Check if an item is attached to a doubly-linked list. + * + * @list + * The list to check. + * @item + * The item to check. + * @node (optional) + * If specified, use item->node.{prev,next} rather than item->{prev,next}. + * @return + * Whether the item is attached to the list. + */ +#define LIST_ATTACHED(list, ...) \ + LIST_ATTACHED_(list, __VA_ARGS__, ) + +#define LIST_ATTACHED_(list, item, ...) \ + LIST_ATTACHED__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__)) + +#define LIST_ATTACHED__(list, item, prev, next) \ + (item->prev || item->next || list->head == item || list->tail == item) + +/** + * Insert into a doubly-linked list after the given cursor. + * + * @list + * The list to modify. + * @cursor + * Insert after this element. + * @item + * The item to insert. + * @node (optional) + * If specified, use item->node.{prev,next} rather than item->{prev,next}. + */ +#define LIST_INSERT(list, cursor, ...) \ + LIST_INSERT_(list, cursor, __VA_ARGS__, ) + +#define LIST_INSERT_(list, cursor, item, ...) \ + LIST_INSERT__((list), (cursor), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__)) + +#define LIST_INSERT__(list, cursor, item, prev, next) LIST_VOID_( \ + bfs_assert(!LIST_ATTACHED__(list, item, prev, next)), \ + item->prev = cursor, \ + item->next = cursor ? cursor->next : list->head, \ + *(item->prev ? &item->prev->next : &list->head) = item, \ + *(item->next ? &item->next->prev : &list->tail) = item) + +/** + * Remove an item from a doubly-linked list. + * + * @list + * The list to modify. + * @item + * The item to remove. + * @node (optional) + * If specified, use item->node.{prev,next} rather than item->{prev,next}. + */ +#define LIST_REMOVE(list, ...) \ + LIST_REMOVE_(list, __VA_ARGS__, ) + +#define LIST_REMOVE_(list, item, ...) \ + LIST_REMOVE__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__)) + +#define LIST_REMOVE__(list, item, prev, next) LIST_VOID_( \ + *(item->prev ? &item->prev->next : &list->head) = item->next, \ + *(item->next ? &item->next->prev : &list->tail) = item->prev, \ + item->prev = item->next = NULL) + +/** + * Loop over the items in a doubly-linked list. + * + * @type + * The list item type. + * @item + * The induction variable name. + * @list + * The list to iterate. + * @node (optional) + * If specified, use head->node.next rather than head->next. + */ +#define for_list(type, item, ...) \ + for_list_(type, item, __VA_ARGS__, ) + +#define for_list_(type, item, list, ...) \ + for_list__(type, item, (list), LIST_NEXT_(__VA_ARGS__)) + +#define for_list__(type, item, list, next) \ + for (type *item = list->head, *_next; \ + item && (LIST_CHECK_(list), _next = item->next, true); \ + item = _next) + +#endif // BFS_LIST_H @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * - main(): the entry point for bfs(1), a breadth-first version of find(1) @@ -33,35 +20,46 @@ * - bftw.[ch] (an extended version of nftw(3)) * * - Utilities: - * - bfs.h (constants about bfs itself) + * - 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) - * - darray.[ch] (a dynamic array library) * - diag.[ch] (formats diagnostic messages) * - dir.[ch] (a directory API facade) * - dstring.[ch] (a dynamic string library) * - fsade.[ch] (a facade over non-standard filesystem features) + * - ioq.[ch] (an async I/O queue) + * - list.h (linked list macros) * - 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) - * - util.[ch] (everything else) + * - version.c (embeds version information) * - xregex.[ch] (regular expression support) * - xspawn.[ch] (spawns processes) * - xtime.[ch] (date/time handling utilities) */ +#include "bfstd.h" #include "ctx.h" +#include "diag.h" #include "eval.h" #include "parse.h" -#include "util.h" + #include <errno.h> #include <fcntl.h> #include <locale.h> -#include <stdbool.h> #include <stdio.h> #include <stdlib.h> +#include <time.h> #include <unistd.h> /** @@ -123,15 +121,29 @@ int main(int argc, char *argv[]) { } // Use the system locale instead of "C" - setlocale(LC_ALL, ""); + int locale_err = 0; + if (!setlocale(LC_ALL, "")) { + locale_err = errno; + } + + // Apply the environment's timezone + tzset(); + // Parse the command line struct bfs_ctx *ctx = bfs_parse_cmdline(argc, argv); if (!ctx) { return EXIT_FAILURE; } + // Warn if setlocale() failed, unless there's no expression to evaluate + if (locale_err && ctx->warn && ctx->expr) { + bfs_warning(ctx, "Failed to set locale: %s\n\n", xstrerror(locale_err)); + } + + // Walk the file system tree, evaluating the expression on each file int ret = bfs_eval(ctx); + // Free the parsed command line, and detect any last-minute errors if (bfs_ctx_free(ctx) != 0 && ret == EXIT_SUCCESS) { ret = EXIT_FAILURE; } @@ -1,68 +1,62 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "mtab.h" -#include "darray.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" #include "stat.h" #include "trie.h" -#include "util.h" + #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> -#if BFS_HAS_SYS_PARAM -# include <sys/param.h> +#ifndef BFS_USE_MNTENT +# define BFS_USE_MNTENT BFS_HAS_GETMNTENT_1 #endif - -#if BFS_HAS_MNTENT -# define BFS_MNTENT 1 -#elif BSD -# define BFS_MNTINFO 1 -#elif __SVR4 -# define BFS_MNTTAB 1 +#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_MNTENT -# include <mntent.h> -# include <paths.h> -# include <stdio.h> -#elif BFS_MNTINFO -# include <sys/mount.h> -# include <sys/ucred.h> -#elif BFS_MNTTAB -# include <stdio.h> -# include <sys/mnttab.h> +#if BFS_USE_MNTENT +# include <mntent.h> +# include <paths.h> +# include <stdio.h> +#elif BFS_USE_MNTINFO +# include <sys/mount.h> +#elif BFS_USE_MNTTAB +# include <stdio.h> +# include <sys/mnttab.h> #endif /** * A mount point in the table. */ -struct bfs_mtab_entry { +struct bfs_mount { /** The path to the mount point. */ char *path; /** The filesystem type. */ char *type; + /** Buffer for the strings. */ + char buf[]; }; struct bfs_mtab { - /** The list of mount points. */ - struct bfs_mtab_entry *entries; + /** Mount point arena. */ + struct varena varena; + + /** The array of mount points. */ + struct bfs_mount **mounts; + /** The number of mount points. */ + size_t nmounts; + /** The basenames of every mount point. */ struct trie names; @@ -75,47 +69,56 @@ struct bfs_mtab { /** * Add an entry to the mount table. */ +_maybe_unused static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) { - struct bfs_mtab_entry entry = { - .path = strdup(path), - .type = strdup(type), - }; - - if (!entry.path || !entry.type) { - goto fail_entry; + 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; } - if (DARRAY_PUSH(&mtab->entries, &entry) != 0) { - goto fail_entry; + struct bfs_mount **ptr = RESERVE(struct bfs_mount *, &mtab->mounts, &mtab->nmounts); + if (!ptr) { + goto free; } + *ptr = mount; - if (!trie_insert_str(&mtab->names, xbasename(path))) { - goto fail; + 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 shrink; } return 0; -fail_entry: - free(entry.type); - free(entry.path); -fail: +shrink: + --mtab->nmounts; +free: + varena_free(&mtab->varena, mount, size); return -1; } struct bfs_mtab *bfs_mtab_parse(void) { - struct bfs_mtab *mtab = malloc(sizeof(*mtab)); + struct bfs_mtab *mtab = ZALLOC(struct bfs_mtab); if (!mtab) { return NULL; } - mtab->entries = NULL; + VARENA_INIT(&mtab->varena, struct bfs_mount, buf); + trie_init(&mtab->names); trie_init(&mtab->types); - mtab->types_filled = false; int error = 0; -#if BFS_MNTENT +#if BFS_USE_MNTENT FILE *file = setmntent(_PATH_MOUNTED, "r"); if (!file) { @@ -138,7 +141,7 @@ struct bfs_mtab *bfs_mtab_parse(void) { endmntent(file); -#elif BFS_MNTINFO +#elif BFS_USE_MNTINFO #if __NetBSD__ typedef struct statvfs bfs_statfs; @@ -148,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; } @@ -160,7 +163,7 @@ struct bfs_mtab *bfs_mtab_parse(void) { } } -#elif BFS_MNTTAB +#elif BFS_USE_MNTTAB FILE *file = xfopen(MNTTAB, O_RDONLY | O_CLOEXEC); if (!file) { @@ -194,39 +197,97 @@ fail: return NULL; } -static void bfs_mtab_fill_types(struct bfs_mtab *mtab) { - for (size_t i = 0; i < darray_length(mtab->entries); ++i) { - struct bfs_mtab_entry *entry = &mtab->entries[i]; +static int bfs_mtab_fill_types(struct bfs_mtab *mtab) { + const enum bfs_stat_flags flags = BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC; + int ret = -1; + + // It's possible that /path/to/mount was unmounted between bfs_mtab_parse() and bfs_mtab_fill_types(). + // In that case, the dev_t of /path/to/mount will be the same as /path/to, which should not get its + // fstype from the old mount record of /path/to/mount. + // + // Detect this by comparing the st_dev of the parent (/path/to) and child (/path/to/mount). Only when + // they differ can the filesystem type actually change between them. As a minor optimization, we keep + // the parent directory open in case multiple mounts have the same parent (e.g. /mnt). + char *parent_dir = NULL; + int parent_fd = -1; + int parent_ret = -1; + struct bfs_stat parent_stat; + + for (size_t i = 0; i < mtab->nmounts; ++i) { + struct bfs_mount *mount = mtab->mounts[i]; + const char *path = mount->path; + int fd = AT_FDCWD; + + char *dir = xdirname(path); + if (!dir) { + goto fail; + } + + if (parent_dir && strcmp(parent_dir, dir) == 0) { + // Same parent + free(dir); + } else { + free(parent_dir); + parent_dir = dir; + + if (parent_fd >= 0) { + xclose(parent_fd); + } + parent_fd = open(parent_dir, O_SEARCH | O_CLOEXEC | O_DIRECTORY); + + parent_ret = -1; + if (parent_fd >= 0) { + parent_ret = bfs_stat(parent_fd, NULL, flags, &parent_stat); + } + } + + if (parent_fd >= 0) { + fd = parent_fd; + path += xbaseoff(path); + } struct bfs_stat sb; - if (bfs_stat(AT_FDCWD, entry->path, BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC, &sb) != 0) { + if (bfs_stat(fd, path, flags, &sb) != 0) { continue; } - struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev)); - if (leaf) { - leaf->value = entry->type; + if (parent_ret == 0 && parent_stat.dev == sb.dev && parent_stat.ino != sb.ino) { + // Not a mount point any more (or a bind mount, but with the same fstype) + continue; + } + + if (trie_set_mem(&mtab->types, &sb.mnt_id, sizeof(sb.mnt_id), mount->type) != 0) { + goto fail; } } mtab->types_filled = true; + ret = 0; + +fail: + if (parent_fd >= 0) { + xclose(parent_fd); + } + free(parent_dir); + return ret; } const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) { if (!mtab->types_filled) { - bfs_mtab_fill_types((struct bfs_mtab *)mtab); + if (bfs_mtab_fill_types((struct bfs_mtab *)mtab) != 0) { + return NULL; + } } - 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"; } } -bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path) { - const char *name = xbasename(path); +bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name) { return trie_find_str(&mtab->names, name); } @@ -235,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 < darray_length(mtab->entries); ++i) { - free(mtab->entries[i].type); - free(mtab->entries[i].path); - } - darray_free(mtab->entries); + free(mtab->mounts); + varena_destroy(&mtab->varena); free(mtab); } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A facade over platform-specific APIs for enumerating mounted filesystems. @@ -21,8 +8,6 @@ #ifndef BFS_MTAB_H #define BFS_MTAB_H -#include <stdbool.h> - struct bfs_stat; /** @@ -41,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, @@ -54,14 +39,14 @@ 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 path - * The path to check. + * @name + * The name of the file to check. * @return * Whether the named file could be a mount point. */ -bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path); +bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name); /** * Free a mount table. @@ -1,32 +1,18 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * The expression optimizer. Different optimization levels are supported: * * -O1: basic logical simplifications, like folding (-true -and -foo) to -foo. * - * -O2: dead code elimination and data flow analysis. struct opt_facts is used + * -O2: dead code elimination and data flow analysis. struct df_domain is used * to record data flow facts that are true at various points of evaluation. - * Specifically, struct opt_facts records the facts that must be true before an - * expression is evaluated (state->facts), and those that must be true after the - * expression is evaluated, given that it returns true (state->facts_when_true) - * or false (state->facts_when_true). Additionally, state->facts_when_impure - * records the possible data flow facts before any expressions with side effects - * are evaluated. + * Specifically, struct df_domain records the state before an expression is + * evaluated (opt->before), and after an expression returns true + * (opt->after_true) or false (opt->after_false). Additionally, opt->impure + * records the possible state before any expression with side effects is + * evaluated. * * -O3: expression re-ordering to reduce expected cost. In an expression like * (-foo -and -bar), if both -foo and -bar are pure (no side effects), they can @@ -35,41 +21,139 @@ * -bar is likely to return false. * * -O4/-Ofast: aggressive optimizations that may affect correctness in corner - * cases. The main effect is to use facts_when_impure to determine if any side- - * effects are reachable at all, and skipping the traversal if not. + * cases. The main effect is to use opt->impure to determine if any side- + * effects are reachable at all, skipping the traversal if not. */ #include "opt.h" + +#include "bfs.h" +#include "bfstd.h" +#include "bftw.h" +#include "bit.h" #include "color.h" #include "ctx.h" #include "diag.h" +#include "dir.h" #include "eval.h" +#include "exec.h" #include "expr.h" +#include "list.h" #include "pwcache.h" -#include "util.h" -#include <assert.h> +#include "xspawn.h" + +#include <errno.h> #include <limits.h> #include <stdarg.h> -#include <stdbool.h> -#include <stdint.h> #include <stdio.h> -#include <string.h> #include <unistd.h> -static char *fake_and_arg = "-a"; -static char *fake_or_arg = "-o"; -static char *fake_not_arg = "!"; +static char *fake_and_arg = "-and"; +static char *fake_or_arg = "-or"; +static char *fake_not_arg = "-not"; + +/** + * The data flow domain for predicates. + */ +enum df_pred { + /** The bottom state (unreachable). */ + PRED_BOTTOM = 0, + /** The predicate is known to be false. */ + PRED_FALSE = 1 << false, + /** The predicate is known to be true. */ + PRED_TRUE = 1 << true, + /** The top state (unknown). */ + PRED_TOP = PRED_FALSE | PRED_TRUE, +}; + +/** Make a predicate known. */ +static void constrain_pred(enum df_pred *pred, bool value) { + *pred &= 1 << value; +} + +/** Compute the join (union) of two predicates. */ +static void pred_join(enum df_pred *dest, enum df_pred src) { + *dest |= src; +} /** - * A contrained integer range. + * Types of predicates we track. */ -struct range { +enum pred_type { + /** -readable */ + READABLE_PRED, + /** -writable */ + WRITABLE_PRED, + /** -executable */ + EXECUTABLE_PRED, + /** -acl */ + ACL_PRED, + /** -capable */ + CAPABLE_PRED, + /** -empty */ + EMPTY_PRED, + /** -hidden */ + HIDDEN_PRED, + /** -nogroup */ + NOGROUP_PRED, + /** -nouser */ + NOUSER_PRED, + /** -sparse */ + SPARSE_PRED, + /** -xattr */ + XATTR_PRED, + /** The number of pred_types. */ + PRED_TYPES, +}; + +/** 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 constrained integer range. + */ +struct df_range { /** The (inclusive) minimum value. */ long long min; /** The (inclusive) maximum value. */ long long max; }; +/** Initialize an empty range. */ +static void range_init_bottom(struct df_range *range) { + range->min = LLONG_MAX; + range->max = LLONG_MIN; +} + +/** Check if a range is empty. */ +static bool range_is_bottom(const struct df_range *range) { + return range->min > range->max; +} + +/** Initialize a full range. */ +static void range_init_top(struct df_range *range) { + // All ranges we currently track are non-negative + range->min = 0; + range->max = LLONG_MAX; +} + +/** Check for an infinite range. */ +static bool range_is_top(const struct df_range *range) { + return range->min == 0 && range->max == LLONG_MAX; +} + /** Compute the minimum of two values. */ static long long min_value(long long a, long long b) { if (a < b) { @@ -89,17 +173,23 @@ static long long max_value(long long a, long long b) { } /** Constrain the minimum of a range. */ -static void constrain_min(struct range *range, long long value) { +static void constrain_min(struct df_range *range, long long value) { range->min = max_value(range->min, value); } -/** Contrain the maximum of a range. */ -static void constrain_max(struct range *range, long long value) { +/** 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 range *range, long long value) { +static void range_remove(struct df_range *range, long long value) { if (range->min == value) { if (range->min == LLONG_MAX) { range->max = LLONG_MIN; @@ -118,20 +208,9 @@ static void range_remove(struct range *range, long long value) { } /** Compute the union of two ranges. */ -static void range_union(struct range *result, const struct range *lhs, const struct range *rhs) { - result->min = min_value(lhs->min, rhs->min); - result->max = max_value(lhs->max, rhs->max); -} - -/** Check if a range contains no values. */ -static bool range_is_impossible(const struct range *range) { - return range->min > range->max; -} - -/** Set a range to contain no values. */ -static void set_range_impossible(struct range *range) { - range->min = LLONG_MAX; - range->max = LLONG_MIN; +static void range_join(struct df_range *dest, const struct df_range *src) { + dest->min = min_value(dest->min, src->min); + dest->max = max_value(dest->max, src->max); } /** @@ -154,179 +233,157 @@ enum range_type { RANGE_TYPES, }; -/** - * A possibly-known value of a predicate. - */ -enum known_pred { - /** The state is impossible to reach. */ - PRED_IMPOSSIBLE = -2, - /** The value of the predicate is not known. */ - PRED_UNKNOWN = -1, - /** The predicate is known to be false. */ - PRED_FALSE = false, - /** The predicate is known to be true. */ - PRED_TRUE = true, +/** 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", }; -/** Make a predicate known. */ -static void constrain_pred(enum known_pred *pred, bool value) { - if (*pred == PRED_UNKNOWN) { - *pred = value; - } else if (*pred == !value) { - *pred = PRED_IMPOSSIBLE; - } -} - -/** Compute the union of two known predicates. */ -static enum known_pred pred_union(enum known_pred lhs, enum known_pred rhs) { - if (lhs == PRED_IMPOSSIBLE) { - return rhs; - } else if (rhs == PRED_IMPOSSIBLE) { - return lhs; - } else if (lhs == rhs) { - return lhs; - } else { - return PRED_UNKNOWN; - } -} - /** - * Types of predicates we track. + * The data flow analysis domain. */ -enum pred_type { - /** -readable */ - READABLE_PRED, - /** -writable */ - WRITABLE_PRED, - /** -executable */ - EXECUTABLE_PRED, - /** -acl */ - ACL_PRED, - /** -capable */ - CAPABLE_PRED, - /** -empty */ - EMPTY_PRED, - /** -hidden */ - HIDDEN_PRED, - /** -nogroup */ - NOGROUP_PRED, - /** -nouser */ - NOUSER_PRED, - /** -sparse */ - SPARSE_PRED, - /** -xattr */ - XATTR_PRED, - /** The number of pred_types. */ - PRED_TYPES, -}; +struct df_domain { + /** The predicates we track. */ + enum df_pred preds[PRED_TYPES]; -/** - * Data flow facts about an evaluation point. - */ -struct opt_facts { /** The value ranges we track. */ - struct range ranges[RANGE_TYPES]; - - /** The predicates we track. */ - enum known_pred preds[PRED_TYPES]; + struct df_range ranges[RANGE_TYPES]; - /** Bitmask of possible file types. */ + /** Bitmask of possible -types. */ unsigned int types; - /** Bitmask of possible link target types. */ + /** Bitmask of possible -xtypes. */ unsigned int xtypes; }; -/** Initialize some data flow facts. */ -static void facts_init(struct opt_facts *facts) { - for (int i = 0; i < RANGE_TYPES; ++i) { - struct range *range = &facts->ranges[i]; - range->min = 0; // All ranges we currently track are non-negative - range->max = LLONG_MAX; - } - +/** Set a data flow value to bottom. */ +static void df_init_bottom(struct df_domain *value) { for (int i = 0; i < PRED_TYPES; ++i) { - facts->preds[i] = PRED_UNKNOWN; + value->preds[i] = PRED_BOTTOM; } - facts->types = ~0; - facts->xtypes = ~0; -} - -/** Compute the union of two fact sets. */ -static void facts_union(struct opt_facts *result, const struct opt_facts *lhs, const struct opt_facts *rhs) { for (int i = 0; i < RANGE_TYPES; ++i) { - range_union(&result->ranges[i], &lhs->ranges[i], &rhs->ranges[i]); + range_init_bottom(&value->ranges[i]); } - for (int i = 0; i < PRED_TYPES; ++i) { - result->preds[i] = pred_union(lhs->preds[i], rhs->preds[i]); - } - - result->types = lhs->types | rhs->types; - result->xtypes = lhs->xtypes | rhs->xtypes; + value->types = 0; + value->xtypes = 0; } /** Determine whether a fact set is impossible. */ -static bool facts_are_impossible(const struct opt_facts *facts) { +static bool df_is_bottom(const struct df_domain *value) { for (int i = 0; i < RANGE_TYPES; ++i) { - if (range_is_impossible(&facts->ranges[i])) { + if (range_is_bottom(&value->ranges[i])) { return true; } } for (int i = 0; i < PRED_TYPES; ++i) { - if (facts->preds[i] == PRED_IMPOSSIBLE) { + if (value->preds[i] == PRED_BOTTOM) { return true; } } - if (!facts->types || !facts->xtypes) { + if (!value->types || !value->xtypes) { return true; } return false; } -/** Set some facts to be impossible. */ -static void set_facts_impossible(struct opt_facts *facts) { +/** Initialize some data flow value. */ +static void df_init_top(struct df_domain *value) { + for (int i = 0; i < PRED_TYPES; ++i) { + value->preds[i] = PRED_TOP; + } + for (int i = 0; i < RANGE_TYPES; ++i) { - set_range_impossible(&facts->ranges[i]); + range_init_top(&value->ranges[i]); } + value->types = ~0; + value->xtypes = ~0; +} + +/** Check for the top element. */ +static bool df_is_top(const struct df_domain *value) { for (int i = 0; i < PRED_TYPES; ++i) { - facts->preds[i] = PRED_IMPOSSIBLE; + 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; + } + } + + if (value->types != ~0U) { + return false; } - facts->types = 0; - facts->xtypes = 0; + if (value->xtypes != ~0U) { + return false; + } + + return true; +} + +/** Compute the union of two fact sets. */ +static void df_join(struct df_domain *dest, const struct df_domain *src) { + for (int i = 0; i < PRED_TYPES; ++i) { + pred_join(&dest->preds[i], src->preds[i]); + } + + for (int i = 0; i < RANGE_TYPES; ++i) { + range_join(&dest->ranges[i], &src->ranges[i]); + } + + dest->types |= src->types; + dest->xtypes |= src->xtypes; } /** * Optimizer state. */ -struct opt_state { +struct bfs_opt { /** The context we're optimizing. */ - const struct bfs_ctx *ctx; - - /** Data flow facts before this expression is evaluated. */ - struct opt_facts facts; - /** Data flow facts after this expression returns true. */ - struct opt_facts facts_when_true; - /** Data flow facts after this expression returns false. */ - struct opt_facts facts_when_false; - /** Data flow facts before any side-effecting expressions are evaluated. */ - struct opt_facts *facts_when_impure; + struct bfs_ctx *ctx; + /** Optimization level (ctx->optlevel). */ + int level; + /** Recursion depth. */ + int depth; + + /** Whether to produce warnings. */ + bool warn; + /** Whether the result of this expression is ignored. */ + bool ignore_result; + + /** Data flow state before this expression is evaluated. */ + struct df_domain before; + /** Data flow state after this expression returns true. */ + struct df_domain after_true; + /** Data flow state after this expression returns false. */ + struct df_domain after_false; + /** Data flow state before any side-effecting expressions are evaluated. */ + struct df_domain *impure; }; /** Log an optimization. */ -BFS_FORMATTER(3, 4) -static bool opt_debug(const struct opt_state *state, int level, const char *format, ...) { - assert(state->ctx->optlevel >= level); +_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) { + cfprintf(opt->ctx->cerr, "│ "); + } - if (bfs_debug(state->ctx, DEBUG_OPT, "${cyn}-O%d${rs}: ", level)) { va_list args; va_start(args, format); - cvfprintf(state->ctx->cerr, format, args); + cvfprintf(opt->ctx->cerr, format, args); va_end(args); return true; } else { @@ -334,755 +391,1967 @@ static bool opt_debug(const struct opt_state *state, int level, const char *form } } -/** Warn about an expression. */ -BFS_FORMATTER(3, 4) -static void opt_warning(const struct opt_state *state, const struct bfs_expr *expr, const char *format, ...) { - if (bfs_expr_warning(state->ctx, expr)) { +/** Log a recursive call. */ +_printf(2, 3) +static bool opt_enter(struct bfs_opt *opt, const char *format, ...) { + int depth = opt->depth; + if (depth > 0) { + --opt->depth; + } + + bool debug = opt_debug(opt, "%s", depth > 0 ? "├─╮ " : ""); + if (debug) { va_list args; va_start(args, format); - bfs_warning(state->ctx, format, args); + cvfprintf(opt->ctx->cerr, format, args); va_end(args); } + + opt->depth = depth + 1; + return debug; } -/** Extract a child expression, freeing the outer expression. */ -static struct bfs_expr *extract_child_expr(struct bfs_expr *expr, struct bfs_expr **child) { - struct bfs_expr *ret = *child; - *child = NULL; - bfs_expr_free(expr); - return ret; +/** Log a recursive return. */ +_printf(2, 3) +static bool opt_leave(struct bfs_opt *opt, const char *format, ...) { + bool debug = false; + int depth = opt->depth; + + if (format) { + if (depth > 1) { + opt->depth -= 2; + } + + debug = opt_debug(opt, "%s", depth > 1 ? "├─╯ " : ""); + if (debug) { + va_list args; + va_start(args, format); + cvfprintf(opt->ctx->cerr, format, args); + va_end(args); + } + } + + opt->depth = depth - 1; + return debug; } -/** - * Negate an expression. - */ -static struct bfs_expr *negate_expr(struct bfs_expr *rhs, char **argv) { - if (rhs->eval_fn == eval_not) { - return extract_child_expr(rhs, &rhs->rhs); +/** Log a shallow visit. */ +_printf(2, 3) +static bool opt_visit(struct bfs_opt *opt, const char *format, ...) { + int depth = opt->depth; + if (depth > 0) { + --opt->depth; } - struct bfs_expr *expr = bfs_expr_new(eval_not, 1, argv); - if (!expr) { - bfs_expr_free(rhs); - return NULL; + bool debug = opt_debug(opt, "%s", depth > 0 ? "├─◯ " : ""); + if (debug) { + va_list args; + va_start(args, format); + cvfprintf(opt->ctx->cerr, format, args); + va_end(args); + } + + opt->depth = depth; + return debug; +} + +/** Log the deletion of an expression. */ +_printf(2, 3) +static bool opt_delete(struct bfs_opt *opt, const char *format, ...) { + int depth = opt->depth; + + if (depth > 0) { + --opt->depth; } - if (argv == &fake_not_arg) { - expr->synthetic = true; + bool debug = opt_debug(opt, "%s", depth > 0 ? "├─✘ " : ""); + if (debug) { + va_list args; + va_start(args, format); + cvfprintf(opt->ctx->cerr, format, args); + va_end(args); } - expr->lhs = NULL; - expr->rhs = rhs; - return expr; + opt->depth = depth; + return debug; } -static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr); -static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr); -static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr); +typedef bool dump_fn(struct bfs_opt *opt, const char *format, ...); -/** - * Apply De Morgan's laws. - */ -static struct bfs_expr *de_morgan(const struct opt_state *state, struct bfs_expr *expr, char **argv) { - bool debug = opt_debug(state, 1, "De Morgan's laws: %pe ", expr); +/** 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_names[type]); - struct bfs_expr *parent = negate_expr(expr, argv); - if (!parent) { - return NULL; + FILE *file = opt->ctx->cerr->file; + switch (value->preds[type]) { + case PRED_BOTTOM: + fprintf(file, "⊥\n"); + break; + case PRED_TOP: + fprintf(file, "⊤\n"); + break; + case PRED_TRUE: + fprintf(file, "true\n"); + break; + case PRED_FALSE: + fprintf(file, "false\n"); + break; + } +} + +/** 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_names[type]); + + FILE *file = opt->ctx->cerr->file; + const struct df_range *range = &value->ranges[type]; + if (range_is_bottom(range)) { + fprintf(file, "⊥\n"); + } else if (range_is_top(range)) { + fprintf(file, "⊤\n"); + } else if (range->min == range->max) { + fprintf(file, "%lld\n", range->min); + } else { + if (range->min == LLONG_MIN) { + fprintf(file, "(-∞, "); + } else { + fprintf(file, "[%lld, ", range->min); + } + if (range->max == LLONG_MAX) { + fprintf(file, "∞)\n"); + } else { + fprintf(file, "%lld]\n", range->max); + } } +} - bool has_parent = true; - if (parent->eval_fn != eval_not) { - expr = parent; - has_parent = false; +/** Print a set of types. */ +static void types_dump(dump_fn *dump, struct bfs_opt *opt, const char *name, unsigned int types) { + dump(opt, "${blu}%s${rs}: ", name); + + FILE *file = opt->ctx->cerr->file; + if (types == 0) { + fprintf(file, " ⊥\n"); + } else if (types == ~0U) { + fprintf(file, " ⊤\n"); + } else if (count_ones(types) < count_ones(~types)) { + fprintf(file, " 0x%X\n", types); + } else { + fprintf(file, "~0x%X\n", ~types); } +} - assert(expr->eval_fn == eval_and || expr->eval_fn == eval_or); - if (expr->eval_fn == eval_and) { - expr->eval_fn = eval_or; - expr->argv = &fake_or_arg; +/** Calculate the number of lines of df_dump() output. */ +static int df_dump_lines(const struct df_domain *value) { + int lines = 0; + + for (int i = 0; i < PRED_TYPES; ++i) { + lines += value->preds[i] != PRED_TOP; + } + + for (int i = 0; i < RANGE_TYPES; ++i) { + lines += !range_is_top(&value->ranges[i]); + } + + lines += value->types != ~0U; + lines += value->xtypes != ~0U; + + return lines; +} + +/** Get the right debugging function for a df_dump() line. */ +static dump_fn *df_dump_line(int lines, int *line) { + ++*line; + + if (lines == 1) { + return opt_visit; + } else if (*line == 1) { + return opt_enter; + } else if (*line == lines) { + return opt_leave; } else { - expr->eval_fn = eval_and; - expr->argv = &fake_and_arg; + return opt_debug; } - expr->synthetic = true; +} - expr->lhs = negate_expr(expr->lhs, argv); - expr->rhs = negate_expr(expr->rhs, argv); - if (!expr->lhs || !expr->rhs) { - bfs_expr_free(parent); - return NULL; +/** Print a data flow value. */ +static void df_dump(struct bfs_opt *opt, const char *str, const struct df_domain *value) { + if (df_is_bottom(value)) { + opt_debug(opt, "%s: ⊥\n", str); + return; + } else if (df_is_top(value)) { + opt_debug(opt, "%s: ⊤\n", str); + return; } - if (debug) { - cfprintf(state->ctx->cerr, "<==> %pe\n", parent); + if (!opt_debug(opt, "%s:\n", str)) { + return; } - if (expr->lhs->eval_fn == eval_not) { - expr->lhs = optimize_not_expr(state, expr->lhs); + int lines = df_dump_lines(value); + int line = 0; + + for (int i = 0; i < PRED_TYPES; ++i) { + if (value->preds[i] != PRED_TOP) { + pred_dump(df_dump_line(lines, &line), opt, value, i); + } } - if (expr->rhs->eval_fn == eval_not) { - expr->rhs = optimize_not_expr(state, expr->rhs); + + for (int i = 0; i < RANGE_TYPES; ++i) { + if (!range_is_top(&value->ranges[i])) { + range_dump(df_dump_line(lines, &line), opt, value, i); + } } - if (!expr->lhs || !expr->rhs) { - bfs_expr_free(parent); - return NULL; + + if (value->types != ~0U) { + types_dump(df_dump_line(lines, &line), opt, "-type", value->types); } - if (expr->eval_fn == eval_and) { - expr = optimize_and_expr(state, expr); - } else { - expr = optimize_or_expr(state, expr); + if (value->xtypes != ~0U) { + types_dump(df_dump_line(lines, &line), opt, "-xtype", value->xtypes); } - if (has_parent) { - parent->rhs = expr; - } else { - parent = expr; +} + +/** Check if an expression is constant. */ +static bool is_const(const struct bfs_expr *expr) { + return expr->eval_fn == eval_true || expr->eval_fn == eval_false; +} + +/** Warn about an expression. */ +_printf(3, 4) +static bool opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { + if (!opt->warn) { + return false; } - if (!expr) { - bfs_expr_free(parent); + + if (bfs_expr_is_parent(expr) || is_const(expr)) { + return false; + } + + 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. */ +static void foster_children(struct bfs_expr *expr, struct bfs_exprs *children) { + bfs_assert(bfs_expr_is_parent(expr)); + + SLIST_INIT(children); + SLIST_EXTEND(children, &expr->children); + + expr->persistent_fds = 0; + expr->ephemeral_fds = 0; + expr->pure = true; +} + +/** Return an expression's only child. */ +static struct bfs_expr *only_child(struct bfs_expr *expr) { + bfs_assert(bfs_expr_is_parent(expr)); + struct bfs_expr *child = bfs_expr_children(expr); + bfs_assert(child && !child->next); + return child; +} + +/** Foster an expression's only child. */ +static struct bfs_expr *foster_only_child(struct bfs_expr *expr) { + struct bfs_expr *child = only_child(expr); + struct bfs_exprs children; + foster_children(expr, &children); + return child; +} + +/** An expression visitor. */ +struct visitor; + +/** An expression-visiting function. */ +typedef struct bfs_expr *visit_fn(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor); + +/** An entry in a visitor lookup table. */ +struct visitor_table { + /** The evaluation function to match on. */ + bfs_eval_fn *eval_fn; + /** The visitor function. */ + visit_fn *visit; +}; + +/** Look up a visitor in a table. */ +static visit_fn *look_up_visitor(const struct bfs_expr *expr, const struct visitor_table table[]) { + for (size_t i = 0; table[i].eval_fn; ++i) { + if (expr->eval_fn == table[i].eval_fn) { + return table[i].visit; + } + } + + return NULL; +} + +struct visitor { + /** The name of this visitor. */ + const char *name; + + /** A function to call before visiting children. */ + visit_fn *enter; + /** The default visitor. */ + visit_fn *visit; + /** A function to call after visiting children. */ + visit_fn *leave; + + /** A visitor lookup table. */ + const struct visitor_table *table; +}; + +/** Recursive visitor implementation. */ +static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor); + +/** Visit a negation. */ +static struct bfs_expr *visit_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_expr *rhs = foster_only_child(expr); + + struct bfs_opt nested = *opt; + rhs = visit_deep(&nested, rhs, visitor); + if (!rhs) { return NULL; } - if (has_parent) { - parent = optimize_not_expr(state, parent); + opt->after_true = nested.after_false; + opt->after_false = nested.after_true; + + bfs_expr_append(expr, rhs); + return expr; +} + +/** Visit a conjunction. */ +static struct bfs_expr *visit_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); + + // Base case (-and) == (-true) + df_init_bottom(&opt->after_false); + struct bfs_opt nested = *opt; + + drain_slist (struct bfs_expr, child, &children) { + if (SLIST_EMPTY(&children)) { + nested.ignore_result = opt->ignore_result; + } else { + nested.ignore_result = false; + } + + child = visit_deep(&nested, child, visitor); + if (!child) { + return NULL; + } + + df_join(&opt->after_false, &nested.after_false); + nested.before = nested.after_true; + + bfs_expr_append(expr, child); } - return parent; + + opt->after_true = nested.after_true; + + return expr; } -/** Optimize an expression recursively. */ -static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr); +/** Visit a disjunction. */ +static struct bfs_expr *visit_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); -/** - * Optimize a negation. - */ -static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr) { - assert(expr->eval_fn == eval_not); - - struct bfs_expr *rhs = expr->rhs; - - int optlevel = state->ctx->optlevel; - if (optlevel >= 1) { - if (rhs == &bfs_true) { - opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_false); - bfs_expr_free(expr); - return &bfs_false; - } else if (rhs == &bfs_false) { - opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_true); - bfs_expr_free(expr); - return &bfs_true; - } else if (rhs->eval_fn == eval_not) { - opt_debug(state, 1, "double negation: %pe <==> %pe\n", expr, rhs->rhs); - return extract_child_expr(expr, &rhs->rhs); - } else if (bfs_expr_never_returns(rhs)) { - opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, rhs); - return extract_child_expr(expr, &expr->rhs); - } else if ((rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) - && (rhs->lhs->eval_fn == eval_not || rhs->rhs->eval_fn == eval_not)) { - return de_morgan(state, expr, expr->argv); + // Base case (-or) == (-false) + df_init_bottom(&opt->after_true); + struct bfs_opt nested = *opt; + + drain_slist (struct bfs_expr, child, &children) { + if (SLIST_EMPTY(&children)) { + nested.ignore_result = opt->ignore_result; + } else { + nested.ignore_result = false; + } + + child = visit_deep(&nested, child, visitor); + if (!child) { + return NULL; } + + df_join(&opt->after_true, &nested.after_true); + nested.before = nested.after_false; + + bfs_expr_append(expr, child); } - expr->pure = rhs->pure; - expr->always_true = rhs->always_false; - expr->always_false = rhs->always_true; - expr->cost = rhs->cost; - expr->probability = 1.0 - rhs->probability; + opt->after_false = nested.after_false; return expr; } -/** Optimize a negation recursively. */ -static struct bfs_expr *optimize_not_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - struct opt_state rhs_state = *state; - expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); - if (!expr->rhs) { - goto fail; +/** Visit a comma expression. */ +static struct bfs_expr *visit_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); + + struct bfs_opt nested = *opt; + + drain_slist (struct bfs_expr, child, &children) { + if (SLIST_EMPTY(&children)) { + nested.ignore_result = opt->ignore_result; + } else { + nested.ignore_result = true; + } + + child = visit_deep(&nested, child, visitor); + if (!child) { + return NULL; + } + + nested.before = nested.after_true; + df_join(&nested.before, &nested.after_false); + + bfs_expr_append(expr, child); } - state->facts_when_true = rhs_state.facts_when_false; - state->facts_when_false = rhs_state.facts_when_true; + opt->after_true = nested.after_true; + opt->after_false = nested.after_false; - return optimize_not_expr(state, expr); + return expr; +} -fail: - bfs_expr_free(expr); - return NULL; +/** Default enter() function. */ +static struct bfs_expr *visit_enter(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt_enter(opt, "%pe\n", expr); + opt->after_true = opt->before; + opt->after_false = opt->before; + return expr; } -/** Optimize a conjunction. */ -static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr) { - assert(expr->eval_fn == eval_and); - - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; - - const struct bfs_ctx *ctx = state->ctx; - int optlevel = ctx->optlevel; - if (optlevel >= 1) { - if (lhs == &bfs_true) { - opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, rhs); - return extract_child_expr(expr, &expr->rhs); - } else if (rhs == &bfs_true) { - opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, lhs); - return extract_child_expr(expr, &expr->lhs); - } else if (lhs->always_false) { - opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs); - opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); - return extract_child_expr(expr, &expr->lhs); - } else if (lhs->always_true && rhs == &bfs_false) { - bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr); - struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs); - ret = negate_expr(ret, &fake_not_arg); - if (debug && ret) { - cfprintf(ctx->cerr, "%pe\n", ret); +/** Default leave() function. */ +static struct bfs_expr *visit_leave(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt_leave(opt, "%pe\n", expr); + return expr; +} + +static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + bool entered = false; + + visit_fn *enter = visitor->enter ? visitor->enter : visit_enter; + visit_fn *leave = visitor->leave ? visitor->leave : visit_leave; + + static const struct visitor_table table[] = { + {eval_not, visit_not}, + {eval_and, visit_and}, + {eval_or, visit_or}, + {eval_comma, visit_comma}, + {NULL, NULL}, + }; + visit_fn *recursive = look_up_visitor(expr, table); + if (recursive) { + if (!entered) { + expr = enter(opt, expr, visitor); + if (!expr) { + return NULL; } - return ret; - } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_false) { - opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); - opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); - return extract_child_expr(expr, &expr->rhs); - } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) { - return de_morgan(state, expr, expr->lhs->argv); + entered = true; + } + + expr = recursive(opt, expr, visitor); + if (!expr) { + return NULL; } } - expr->pure = lhs->pure && rhs->pure; - expr->always_true = lhs->always_true && rhs->always_true; - expr->always_false = lhs->always_false || rhs->always_false; - expr->cost = lhs->cost + lhs->probability*rhs->cost; - expr->probability = lhs->probability*rhs->probability; + visit_fn *general = visitor->visit; + if (general) { + if (!entered) { + expr = enter(opt, expr, visitor); + if (!expr) { + return NULL; + } + entered = true; + } + expr = general(opt, expr, visitor); + if (!expr) { + return NULL; + } + } + + visit_fn *specific = look_up_visitor(expr, visitor->table); + if (specific) { + if (!entered) { + expr = enter(opt, expr, visitor); + if (!expr) { + return NULL; + } + entered = true; + } + + expr = specific(opt, expr, visitor); + if (!expr) { + return NULL; + } + } + + if (entered) { + expr = leave(opt, expr, visitor); + } else { + opt_visit(opt, "%pe\n", expr); + } + + return expr; +} + +/** Visit an expression recursively. */ +static struct bfs_expr *visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt_enter(opt, "%s()\n", visitor->name); + expr = visit_deep(opt, expr, visitor); + opt_leave(opt, "\n"); return expr; } -/** Optimize a conjunction recursively. */ -static struct bfs_expr *optimize_and_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - struct opt_state lhs_state = *state; - expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); - if (!expr->lhs) { - goto fail; +/** Visit an expression non-recursively. */ +static struct bfs_expr *visit_shallow(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + visit_fn *general = visitor->visit; + if (expr && general) { + expr = general(opt, expr, visitor); } - struct opt_state rhs_state = *state; - rhs_state.facts = lhs_state.facts_when_true; - expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); - if (!expr->rhs) { - goto fail; + if (!expr) { + return NULL; } - state->facts_when_true = rhs_state.facts_when_true; - facts_union(&state->facts_when_false, &lhs_state.facts_when_false, &rhs_state.facts_when_false); + visit_fn *specific = look_up_visitor(expr, visitor->table); + if (specific) { + expr = specific(opt, expr, visitor); + } - return optimize_and_expr(state, expr); + return expr; +} -fail: - bfs_expr_free(expr); - return NULL; +/** Annotate -{execut,read,writ}able. */ +static struct bfs_expr *annotate_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + expr->probability = 1.0; + if (expr->num & R_OK) { + expr->probability *= 0.99; + } + if (expr->num & W_OK) { + expr->probability *= 0.8; + } + if (expr->num & X_OK) { + expr->probability *= 0.2; + } + + return expr; } -/** Optimize a disjunction. */ -static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr) { - assert(expr->eval_fn == eval_or); - - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; - - const struct bfs_ctx *ctx = state->ctx; - int optlevel = ctx->optlevel; - if (optlevel >= 1) { - if (lhs->always_true) { - opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs); - opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); - return extract_child_expr(expr, &expr->lhs); - } else if (lhs == &bfs_false) { - opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, rhs); - return extract_child_expr(expr, &expr->rhs); - } else if (rhs == &bfs_false) { - opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, lhs); - return extract_child_expr(expr, &expr->lhs); - } else if (lhs->always_false && rhs == &bfs_true) { - bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr); - struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs); - ret = negate_expr(ret, &fake_not_arg); - if (debug && ret) { - cfprintf(ctx->cerr, "%pe\n", ret); - } - return ret; - } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_true) { - opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); - opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); - return extract_child_expr(expr, &expr->rhs); - } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) { - return de_morgan(state, expr, expr->lhs->argv); - } +/** Annotate -empty. */ +static struct bfs_expr *annotate_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (opt->level >= 4) { + // Since -empty attempts to open and read directories, it may + // have side effects such as reporting permission errors, and + // thus shouldn't be re-ordered without aggressive optimizations + expr->pure = true; + } + + return expr; +} + +/** Annotate -exec. */ +static struct bfs_expr *annotate_exec(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (expr->exec->flags & BFS_EXEC_MULTI) { + expr->always_true = true; + } else { + expr->cost = 1000000.0; + } + + return expr; +} + +/** Annotate -name/-lname/-path. */ +static struct bfs_expr *annotate_fnmatch(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (expr->literal) { + expr->probability = 0.1; + } else { + expr->probability = 0.5; + } + + return expr; +} + +/** Annotate -f?print. */ +static struct bfs_expr *annotate_fprint(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + const struct colors *colors = expr->cfile->colors; + expr->calls_stat = colors && colors_need_stat(colors); + return expr; +} + +/** Estimate probability for -x?type. */ +static void estimate_type_probability(struct bfs_expr *expr) { + unsigned int types = expr->num; + + expr->probability = 0.0; + if (types & (1 << BFS_BLK)) { + expr->probability += 0.00000721183; + } + if (types & (1 << BFS_CHR)) { + expr->probability += 0.0000499855; + } + if (types & (1 << BFS_DIR)) { + expr->probability += 0.114475; + } + if (types & (1 << BFS_DOOR)) { + expr->probability += 0.000001; + } + if (types & (1 << BFS_FIFO)) { + expr->probability += 0.00000248684; + } + if (types & (1 << BFS_REG)) { + expr->probability += 0.859772; + } + if (types & (1 << BFS_LNK)) { + expr->probability += 0.0256816; + } + if (types & (1 << BFS_SOCK)) { + expr->probability += 0.0000116881; + } + if (types & (1 << BFS_WHT)) { + expr->probability += 0.000001; + } +} + +/** Annotate -type. */ +static struct bfs_expr *annotate_type(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + estimate_type_probability(expr); + return expr; +} + +/** Annotate -xtype. */ +static struct bfs_expr *annotate_xtype(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (opt->level >= 4) { + // Since -xtype dereferences symbolic links, it may have side + // effects such as reporting permission errors, and thus + // shouldn't be re-ordered without aggressive optimizations + expr->pure = true; } - expr->pure = lhs->pure && rhs->pure; - expr->always_true = lhs->always_true || rhs->always_true; - expr->always_false = lhs->always_false && rhs->always_false; - expr->cost = lhs->cost + (1 - lhs->probability)*rhs->cost; - expr->probability = lhs->probability + rhs->probability - lhs->probability*rhs->probability; + estimate_type_probability(expr); + return expr; +} +/** Annotate a negation. */ +static struct bfs_expr *annotate_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_expr *rhs = only_child(expr); + expr->pure = rhs->pure; + expr->always_true = rhs->always_false; + expr->always_false = rhs->always_true; + expr->cost = rhs->cost; + expr->probability = 1.0 - rhs->probability; return expr; } -/** Optimize a disjunction recursively. */ -static struct bfs_expr *optimize_or_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - struct opt_state lhs_state = *state; - expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); - if (!expr->lhs) { - goto fail; +/** Annotate a conjunction. */ +static struct bfs_expr *annotate_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + expr->pure = true; + expr->always_true = true; + expr->always_false = false; + expr->cost = 0.0; + expr->probability = 1.0; + + for_expr (child, expr) { + expr->pure &= child->pure; + expr->always_true &= child->always_true; + expr->always_false |= child->always_false; + expr->cost += expr->probability * child->cost; + expr->probability *= child->probability; } - struct opt_state rhs_state = *state; - rhs_state.facts = lhs_state.facts_when_false; - expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); - if (!expr->rhs) { - goto fail; + return expr; +} + +/** Annotate a disjunction. */ +static struct bfs_expr *annotate_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + expr->pure = true; + expr->always_true = false; + expr->always_false = true; + expr->cost = 0.0; + + float false_prob = 1.0; + for_expr (child, expr) { + expr->pure &= child->pure; + expr->always_true |= child->always_true; + expr->always_false &= child->always_false; + expr->cost += false_prob * child->cost; + false_prob *= (1.0 - child->probability); } + expr->probability = 1.0 - false_prob; + + return expr; +} - facts_union(&state->facts_when_true, &lhs_state.facts_when_true, &rhs_state.facts_when_true); - state->facts_when_false = rhs_state.facts_when_false; +/** Annotate a comma expression. */ +static struct bfs_expr *annotate_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + expr->pure = true; + expr->cost = 0.0; - return optimize_or_expr(state, expr); + for_expr (child, expr) { + expr->pure &= child->pure; + expr->always_true = child->always_true; + expr->always_false = child->always_false; + expr->cost += child->cost; + expr->probability = child->probability; + } -fail: - bfs_expr_free(expr); - return NULL; + return expr; } -/** Optimize an expression in an ignored-result context. */ -static struct bfs_expr *ignore_result(const struct opt_state *state, struct bfs_expr *expr) { - int optlevel = state->ctx->optlevel; - - if (optlevel >= 1) { - while (true) { - if (expr->eval_fn == eval_not) { - opt_debug(state, 1, "ignored result: %pe --> %pe\n", expr, expr->rhs); - opt_warning(state, expr, "The result of this expression is ignored.\n\n"); - expr = extract_child_expr(expr, &expr->rhs); - } else if (optlevel >= 2 - && (expr->eval_fn == eval_and || expr->eval_fn == eval_or || expr->eval_fn == eval_comma) - && expr->rhs->pure) { - opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, expr->lhs); - opt_warning(state, expr->rhs, "The result of this expression is ignored.\n\n"); - expr = extract_child_expr(expr, &expr->lhs); - } else { - break; - } +/** Annotate an arbitrary expression. */ +static struct bfs_expr *annotate_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + /** Table of pure expressions. */ + static bfs_eval_fn *const pure[] = { + eval_access, + eval_acl, + eval_capable, + eval_depth, + eval_false, + eval_flags, + eval_fstype, + eval_gid, + eval_hidden, + eval_inum, + eval_links, + eval_lname, + eval_name, + eval_newer, + eval_nogroup, + eval_nouser, + eval_path, + eval_perm, + eval_regex, + eval_samefile, + eval_size, + eval_sparse, + eval_time, + eval_true, + eval_type, + eval_uid, + eval_used, + eval_xattr, + eval_xattrname, + }; + + expr->pure = false; + for (size_t i = 0; i < countof(pure); ++i) { + if (expr->eval_fn == pure[i]) { + expr->pure = true; + break; + } + } + + /** Table of always-true expressions. */ + static bfs_eval_fn *const always_true[] = { + eval_fls, + eval_fprint, + eval_fprint0, + eval_fprintf, + eval_fprintx, + eval_limit, + eval_prune, + eval_true, + // Non-returning + eval_exit, + eval_quit, + }; + + expr->always_true = false; + for (size_t i = 0; i < countof(always_true); ++i) { + if (expr->eval_fn == always_true[i]) { + expr->always_true = true; + break; + } + } + + /** Table of always-false expressions. */ + static bfs_eval_fn *const always_false[] = { + eval_false, + // Non-returning + eval_exit, + eval_quit, + }; + + expr->always_false = false; + for (size_t i = 0; i < countof(always_false); ++i) { + if (expr->eval_fn == always_false[i]) { + expr->always_false = true; + break; } + } + + /** Table of stat-calling primaries. */ + static bfs_eval_fn *const calls_stat[] = { + eval_empty, + eval_flags, + eval_fls, + eval_fprintf, + eval_fstype, + eval_gid, + eval_inum, + eval_links, + eval_newer, + eval_nogroup, + eval_nouser, + eval_perm, + eval_samefile, + eval_size, + eval_sparse, + eval_time, + eval_uid, + eval_used, + eval_xattr, + eval_xattrname, + }; + + expr->calls_stat = false; + for (size_t i = 0; i < countof(calls_stat); ++i) { + if (expr->eval_fn == calls_stat[i]) { + expr->calls_stat = true; + break; + } + } + +#define FAST_COST 40.0 +#define FNMATCH_COST 400.0 +#define STAT_COST 1000.0 +#define PRINT_COST 20000.0 + + /** Table of expression costs. */ + static const struct { + bfs_eval_fn *eval_fn; + float cost; + } costs[] = { + {eval_access, STAT_COST}, + {eval_acl, STAT_COST}, + {eval_capable, STAT_COST}, + {eval_empty, 2 * STAT_COST}, // readdir() is worse than stat() + {eval_flags, STAT_COST}, + {eval_fls, PRINT_COST}, + {eval_fprint, PRINT_COST}, + {eval_fprint0, PRINT_COST}, + {eval_fprintf, PRINT_COST}, + {eval_fprintx, PRINT_COST}, + {eval_fstype, STAT_COST}, + {eval_gid, STAT_COST}, + {eval_inum, STAT_COST}, + {eval_links, STAT_COST}, + {eval_lname, FNMATCH_COST}, + {eval_name, FNMATCH_COST}, + {eval_newer, STAT_COST}, + {eval_nogroup, STAT_COST}, + {eval_nouser, STAT_COST}, + {eval_path, FNMATCH_COST}, + {eval_perm, STAT_COST}, + {eval_samefile, STAT_COST}, + {eval_size, STAT_COST}, + {eval_sparse, STAT_COST}, + {eval_time, STAT_COST}, + {eval_uid, STAT_COST}, + {eval_used, STAT_COST}, + {eval_xattr, STAT_COST}, + {eval_xattrname, STAT_COST}, + }; + + expr->cost = FAST_COST; + for (size_t i = 0; i < countof(costs); ++i) { + if (expr->eval_fn == costs[i].eval_fn) { + expr->cost = costs[i].cost; + break; + } + } + + /** Table of expression probabilities. */ + static const struct { + /** The evaluation function with this cost. */ + bfs_eval_fn *eval_fn; + /** The matching probability. */ + float probability; + } probs[] = { + {eval_acl, 0.00002}, + {eval_capable, 0.000002}, + {eval_empty, 0.01}, + {eval_false, 0.0}, + {eval_hidden, 0.01}, + {eval_nogroup, 0.01}, + {eval_nouser, 0.01}, + {eval_samefile, 0.01}, + {eval_true, 1.0}, + {eval_xattr, 0.01}, + {eval_xattrname, 0.01}, + }; - if (optlevel >= 2 && expr->pure && expr != &bfs_false) { - opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, &bfs_false); - opt_warning(state, expr, "The result of this expression is ignored.\n\n"); - bfs_expr_free(expr); - expr = &bfs_false; + expr->probability = 0.5; + for (size_t i = 0; i < countof(probs); ++i) { + if (expr->eval_fn == probs[i].eval_fn) { + expr->probability = probs[i].probability; + break; } } return expr; } -/** Optimize a comma expression. */ -static struct bfs_expr *optimize_comma_expr(const struct opt_state *state, struct bfs_expr *expr) { - assert(expr->eval_fn == eval_comma); +/** + * Annotating visitor. + */ +static const struct visitor annotate = { + .name = "annotate", + .visit = annotate_visit, + .table = (const struct visitor_table[]) { + {eval_access, annotate_access}, + {eval_empty, annotate_empty}, + {eval_exec, annotate_exec}, + {eval_fprint, annotate_fprint}, + {eval_lname, annotate_fnmatch}, + {eval_name, annotate_fnmatch}, + {eval_path, annotate_fnmatch}, + {eval_type, annotate_type}, + {eval_xtype, annotate_xtype}, + + {eval_not, annotate_not}, + {eval_and, annotate_and}, + {eval_or, annotate_or}, + {eval_comma, annotate_comma}, + + {NULL, NULL}, + }, +}; + +/** Create a constant expression. */ +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], BFS_TEST); + return visit_shallow(opt, expr, &annotate); +} + +/** Negate an expression, keeping it canonical. */ +static struct bfs_expr *negate_expr(struct bfs_opt *opt, struct bfs_expr *expr, char **argv) { + if (expr->eval_fn == eval_not) { + return only_child(expr); + } else if (expr->eval_fn == eval_true) { + return opt_const(opt, false); + } else if (expr->eval_fn == eval_false) { + return opt_const(opt, true); + } - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; + struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv, BFS_OPERATOR); + if (!ret) { + return NULL; + } - int optlevel = state->ctx->optlevel; - if (optlevel >= 1) { - lhs = expr->lhs = ignore_result(state, lhs); + bfs_expr_append(ret, expr); + return visit_shallow(opt, ret, &annotate); +} - if (bfs_expr_never_returns(lhs)) { - opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, lhs); - opt_warning(state, expr->rhs, "This expression is unreachable.\n\n"); - return extract_child_expr(expr, &expr->lhs); - } else if ((lhs->always_true && rhs == &bfs_true) - || (lhs->always_false && rhs == &bfs_false)) { - opt_debug(state, 1, "redundancy elimination: %pe <==> %pe\n", expr, lhs); - return extract_child_expr(expr, &expr->lhs); - } else if (optlevel >= 2 && lhs->pure) { - opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs); - opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n"); - return extract_child_expr(expr, &expr->rhs); +/** Sink negations into a conjunction/disjunction using De Morgan's laws. */ +static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *expr) { + opt_debug(opt, "De Morgan's laws\n"); + + char **argv = expr->argv; + expr = only_child(expr); + opt_enter(opt, "%pe\n", expr); + + if (expr->eval_fn == eval_and) { + expr->eval_fn = eval_or; + expr->argv = &fake_or_arg; + } else { + bfs_assert(expr->eval_fn == eval_or); + expr->eval_fn = eval_and; + expr->argv = &fake_and_arg; + } + + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + opt_enter(opt, "%pe\n", child); + + child = negate_expr(opt, child, argv); + if (!child) { + return NULL; } + + opt_leave(opt, "%pe\n", child); + bfs_expr_append(expr, child); } - expr->pure = lhs->pure && rhs->pure; - expr->always_true = bfs_expr_never_returns(lhs) || rhs->always_true; - expr->always_false = bfs_expr_never_returns(lhs) || rhs->always_false; - expr->cost = lhs->cost + rhs->cost; - expr->probability = rhs->probability; + opt_leave(opt, "%pe\n", expr); + return visit_shallow(opt, expr, &annotate); +} + +/** Sink a negation into a comma expression. */ +static struct bfs_expr *sink_not_comma(struct bfs_opt *opt, struct bfs_expr *expr) { + char **argv = expr->argv; + expr = only_child(expr); + opt_enter(opt, "%pe\n", expr); - return expr; + bfs_assert(expr->eval_fn == eval_comma); + + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + if (SLIST_EMPTY(&children)) { + opt_enter(opt, "%pe\n", child); + opt_debug(opt, "sink\n"); + + child = negate_expr(opt, child, argv); + if (!child) { + return NULL; + } + + opt_leave(opt, "%pe\n", child); + } else { + opt_visit(opt, "%pe\n", child); + } + + bfs_expr_append(expr, child); + } + + opt_leave(opt, "%pe\n", expr); + return visit_shallow(opt, expr, &annotate); } -/** Optimize a comma expression recursively. */ -static struct bfs_expr *optimize_comma_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - struct opt_state lhs_state = *state; - expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs); - if (!expr->lhs) { - goto fail; +/** Canonicalize a negation. */ +static struct bfs_expr *canonicalize_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_expr *rhs = only_child(expr); + + if (rhs->eval_fn == eval_not) { + opt_debug(opt, "double negation\n"); + return only_child(rhs); + } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) { + return sink_not_andor(opt, expr); + } else if (rhs->eval_fn == eval_comma) { + return sink_not_comma(opt, expr); + } else if (is_const(rhs)) { + opt_debug(opt, "constant propagation\n"); + return opt_const(opt, rhs->eval_fn == eval_false); + } else { + return expr; } +} + +/** Canonicalize an associative operator. */ +static struct bfs_expr *canonicalize_assoc(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); + + struct bfs_exprs flat; + SLIST_INIT(&flat); + + 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); + + if (!head) { + opt_delete(opt, "%pe [empty]\n", child); + } else { + opt_enter(opt, "%pe\n", child); + opt_debug(opt, "associativity\n"); + if (head == tail) { + opt_leave(opt, "%pe\n", head); + } else if (head->next == tail) { + opt_leave(opt, "%pe %pe\n", head, tail); + } else { + opt_leave(opt, "%pe ... %pe\n", head, tail); + } + } - struct opt_state rhs_state = *state; - facts_union(&rhs_state.facts, &lhs_state.facts_when_true, &lhs_state.facts_when_false); - expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs); - if (!expr->rhs) { - goto fail; + SLIST_EXTEND(&flat, &child->children); + } else { + opt_visit(opt, "%pe\n", child); + SLIST_APPEND(&flat, child); + } } - return optimize_comma_expr(state, expr); + bfs_expr_extend(expr, &flat); -fail: - bfs_expr_free(expr); - return NULL; + return visit_shallow(opt, expr, &annotate); } -/** Infer data flow facts about a predicate. */ -static void infer_pred_facts(struct opt_state *state, enum pred_type pred) { - constrain_pred(&state->facts_when_true.preds[pred], true); - constrain_pred(&state->facts_when_false.preds[pred], false); +/** + * Canonicalizing visitor. + */ +static const struct visitor canonicalize = { + .name = "canonicalize", + .table = (const struct visitor_table[]) { + {eval_not, canonicalize_not}, + {eval_and, canonicalize_assoc}, + {eval_or, canonicalize_assoc}, + {eval_comma, canonicalize_assoc}, + {NULL, NULL}, + }, +}; + +/** Calculate the cost of an ordered pair of expressions. */ +static float expr_cost(const struct bfs_expr *parent, const struct bfs_expr *lhs, const struct bfs_expr *rhs) { + // https://cs.stackexchange.com/a/66921/21004 + float prob = lhs->probability; + if (parent->eval_fn == eval_or) { + prob = 1.0 - prob; + } + return lhs->cost + prob * rhs->cost; } -/** Infer data flow facts about an -{execut,read,writ}able expression. */ -static void infer_access_facts(struct opt_state *state, const struct bfs_expr *expr) { - if (expr->num & R_OK) { - infer_pred_facts(state, READABLE_PRED); +/** Sort a block of expressions. */ +static void sort_exprs(struct bfs_opt *opt, struct bfs_expr *parent, struct bfs_exprs *exprs) { + if (!exprs->head || !exprs->head->next) { + return; } - if (expr->num & W_OK) { - infer_pred_facts(state, WRITABLE_PRED); + + struct bfs_exprs left, right; + SLIST_INIT(&left); + SLIST_INIT(&right); + + // Split + for (struct bfs_expr *hare = exprs->head; hare && (hare = hare->next); hare = hare->next) { + struct bfs_expr *tortoise = SLIST_POP(exprs); + SLIST_APPEND(&left, tortoise); } - if (expr->num & X_OK) { - infer_pred_facts(state, EXECUTABLE_PRED); + SLIST_EXTEND(&right, exprs); + + // Recurse + sort_exprs(opt, parent, &left); + sort_exprs(opt, parent, &right); + + // Merge + while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) { + struct bfs_expr *lhs = left.head; + struct bfs_expr *rhs = right.head; + + float cost = expr_cost(parent, lhs, rhs); + float swapped = expr_cost(parent, rhs, lhs); + + if (cost <= swapped) { + SLIST_POP(&left); + SLIST_APPEND(exprs, lhs); + } else { + opt_enter(opt, "%pe %pe [${ylw}%g${rs}]\n", lhs, rhs, cost); + SLIST_POP(&right); + SLIST_APPEND(exprs, rhs); + opt_leave(opt, "%pe %pe [${ylw}%g${rs}]\n", rhs, lhs, swapped); + } } + SLIST_EXTEND(exprs, &left); + SLIST_EXTEND(exprs, &right); +} + +/** Reorder children to reduce cost. */ +static struct bfs_expr *reorder_andor(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); + + // Split into blocks of consecutive pure/impure expressions, and sort + // the pure blocks + struct bfs_exprs pure; + SLIST_INIT(&pure); + + drain_slist (struct bfs_expr, child, &children) { + if (child->pure) { + SLIST_APPEND(&pure, child); + } else { + sort_exprs(opt, expr, &pure); + bfs_expr_extend(expr, &pure); + bfs_expr_append(expr, child); + } + } + sort_exprs(opt, expr, &pure); + bfs_expr_extend(expr, &pure); + + return visit_shallow(opt, expr, &annotate); +} + +/** + * Reordering visitor. + */ +static const struct visitor reorder = { + .name = "reorder", + .table = (const struct visitor_table[]) { + {eval_and, reorder_andor}, + {eval_or, reorder_andor}, + {NULL, NULL}, + }, +}; + +/** Transfer function for simple predicates. */ +static void data_flow_pred(struct bfs_opt *opt, enum pred_type pred, bool value) { + constrain_pred(&opt->after_true.preds[pred], value); + constrain_pred(&opt->after_false.preds[pred], !value); } -/** Infer data flow facts about an icmp-style ([+-]N) expression. */ -static void infer_icmp_facts(struct opt_state *state, const struct bfs_expr *expr, enum range_type type) { - struct range *range_when_true = &state->facts_when_true.ranges[type]; - struct range *range_when_false = &state->facts_when_false.ranges[type]; +/** Transfer function for icmp-style ([+-]N) expressions. */ +static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enum range_type type) { + struct df_range *true_range = &opt->after_true.ranges[type]; + struct df_range *false_range = &opt->after_false.ranges[type]; long long value = expr->num; switch (expr->int_cmp) { case BFS_INT_EQUAL: - constrain_min(range_when_true, value); - constrain_max(range_when_true, value); - range_remove(range_when_false, value); + constrain_range(true_range, value); + range_remove(false_range, value); break; case BFS_INT_LESS: - constrain_min(range_when_false, value); - constrain_max(range_when_true, value); - range_remove(range_when_true, value); + constrain_min(false_range, value); + constrain_max(true_range, value); + range_remove(true_range, value); break; case BFS_INT_GREATER: - constrain_max(range_when_false, value); - constrain_min(range_when_true, value); - range_remove(range_when_true, value); + constrain_max(false_range, value); + constrain_min(true_range, value); + range_remove(true_range, value); break; } } -/** Infer data flow facts about a -gid expression. */ -static void infer_gid_facts(struct opt_state *state, const struct bfs_expr *expr) { - infer_icmp_facts(state, expr, GID_RANGE); +/** 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) { + switch (expr->num) { + case R_OK: + data_flow_pred(opt, READABLE_PRED, true); + break; + case W_OK: + data_flow_pred(opt, WRITABLE_PRED, true); + 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); - const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); - struct range *range = &state->facts_when_true.ranges[GID_RANGE]; - if (groups && range->min == range->max) { + 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; +} + +/** Transfer function for -gid. */ +static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *range = &opt->after_true.ranges[GID_RANGE]; + if (range->min == range->max) { gid_t gid = range->min; - bool nogroup = !bfs_getgrgid(groups, gid); - constrain_pred(&state->facts_when_true.preds[NOGROUP_PRED], nogroup); + bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid); + if (errno == 0) { + constrain_pred(&opt->after_true.preds[NOGROUP_PRED], nogroup); + } } + + return expr; } -/** Infer data flow facts about a -uid expression. */ -static void infer_uid_facts(struct opt_state *state, const struct bfs_expr *expr) { - infer_icmp_facts(state, expr, UID_RANGE); +/** Transfer function for -inum. */ +static struct bfs_expr *data_flow_inum(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *range = &opt->after_true.ranges[INUM_RANGE]; + if (range->min == range->max) { + expr->probability = 0.01; + } else { + expr->probability = 0.5; + } - const struct bfs_users *users = bfs_ctx_users(state->ctx); - struct range *range = &state->facts_when_true.ranges[UID_RANGE]; - if (users && range->min == range->max) { - uid_t uid = range->min; - bool nouser = !bfs_getpwuid(users, uid); - constrain_pred(&state->facts_when_true.preds[NOUSER_PRED], nouser); + return expr; +} + +/** Transfer function for -links. */ +static struct bfs_expr *data_flow_links(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *range = &opt->after_true.ranges[LINKS_RANGE]; + if (1 >= range->min && 1 <= range->max) { + expr->probability = 0.99; + } else { + expr->probability = 0.5; } + + return expr; } -/** Infer data flow facts about a -samefile expression. */ -static void infer_samefile_facts(struct opt_state *state, const struct bfs_expr *expr) { - struct range *range_when_true = &state->facts_when_true.ranges[INUM_RANGE]; - constrain_min(range_when_true, expr->ino); - constrain_max(range_when_true, expr->ino); +/** 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; } -/** Infer data flow facts about a -type expression. */ -static void infer_type_facts(struct opt_state *state, const struct bfs_expr *expr) { - state->facts_when_true.types &= expr->num; - state->facts_when_false.types &= ~expr->num; +/** 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_range(true_range, expr->ino); + + struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE]; + range_remove(false_range, expr->ino); + + return expr; } -/** Infer data flow facts about an -xtype expression. */ -static void infer_xtype_facts(struct opt_state *state, const struct bfs_expr *expr) { - state->facts_when_true.xtypes &= expr->num; - state->facts_when_false.xtypes &= ~expr->num; +/** Transfer function for -size. */ +static struct bfs_expr *data_flow_size(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *range = &opt->after_true.ranges[SIZE_RANGE]; + if (range->min == range->max) { + expr->probability = 0.01; + } else { + expr->probability = 0.5; + } + + return expr; } -static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - int optlevel = state->ctx->optlevel; +/** Transfer function for -type. */ +static struct bfs_expr *data_flow_type(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.types &= expr->num; + opt->after_false.types &= ~expr->num; + return expr; +} - state->facts_when_true = state->facts; - state->facts_when_false = state->facts; +/** Transfer function for -uid. */ +static struct bfs_expr *data_flow_uid(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *range = &opt->after_true.ranges[UID_RANGE]; + if (range->min == range->max) { + uid_t uid = range->min; + bool nouser = !bfs_getpwuid(opt->ctx->users, uid); + if (errno == 0) { + constrain_pred(&opt->after_true.preds[NOUSER_PRED], nouser); + } + } - if (optlevel >= 2 && facts_are_impossible(&state->facts)) { - opt_debug(state, 2, "reachability: %pe --> %pe\n", expr, &bfs_false); - opt_warning(state, expr, "This expression is unreachable.\n\n"); - bfs_expr_free(expr); - expr = &bfs_false; - goto done; + return expr; +} + +/** Transfer function for -xtype. */ +static struct bfs_expr *data_flow_xtype(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.xtypes &= expr->num; + opt->after_false.xtypes &= ~expr->num; + return expr; +} + +/** Data flow visitor entry. */ +static struct bfs_expr *data_flow_enter(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + visit_enter(opt, expr, visitor); + + df_dump(opt, "before", &opt->before); + + if (!bfs_expr_is_parent(expr) && !expr->pure) { + df_join(opt->impure, &opt->before); + df_dump(opt, "impure", opt->impure); } - if (!bfs_expr_has_children(expr) && !expr->pure) { - facts_union(state->facts_when_impure, state->facts_when_impure, &state->facts); - } - - if (expr->eval_fn == eval_access) { - infer_access_facts(state, expr); - } else if (expr->eval_fn == eval_acl) { - infer_pred_facts(state, ACL_PRED); - } else if (expr->eval_fn == eval_capable) { - infer_pred_facts(state, CAPABLE_PRED); - } else if (expr->eval_fn == eval_depth) { - infer_icmp_facts(state, expr, DEPTH_RANGE); - } else if (expr->eval_fn == eval_empty) { - infer_pred_facts(state, EMPTY_PRED); - } else if (expr->eval_fn == eval_gid) { - infer_gid_facts(state, expr); - } else if (expr->eval_fn == eval_hidden) { - infer_pred_facts(state, HIDDEN_PRED); - } else if (expr->eval_fn == eval_inum) { - infer_icmp_facts(state, expr, INUM_RANGE); - } else if (expr->eval_fn == eval_links) { - infer_icmp_facts(state, expr, LINKS_RANGE); - } else if (expr->eval_fn == eval_nogroup) { - infer_pred_facts(state, NOGROUP_PRED); - } else if (expr->eval_fn == eval_nouser) { - infer_pred_facts(state, NOUSER_PRED); - } else if (expr->eval_fn == eval_samefile) { - infer_samefile_facts(state, expr); - } else if (expr->eval_fn == eval_size) { - infer_icmp_facts(state, expr, SIZE_RANGE); - } else if (expr->eval_fn == eval_sparse) { - infer_pred_facts(state, SPARSE_PRED); - } else if (expr->eval_fn == eval_type) { - infer_type_facts(state, expr); - } else if (expr->eval_fn == eval_uid) { - infer_uid_facts(state, expr); - } else if (expr->eval_fn == eval_xattr) { - infer_pred_facts(state, XATTR_PRED); - } else if (expr->eval_fn == eval_xtype) { - infer_xtype_facts(state, expr); - } else if (expr->eval_fn == eval_not) { - expr = optimize_not_expr_recursive(state, expr); - } else if (expr->eval_fn == eval_and) { - expr = optimize_and_expr_recursive(state, expr); - } else if (expr->eval_fn == eval_or) { - expr = optimize_or_expr_recursive(state, expr); - } else if (expr->eval_fn == eval_comma) { - expr = optimize_comma_expr_recursive(state, expr); + return expr; +} + +/** Data flow visitor exit. */ +static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (expr->always_true) { + expr->probability = 1.0; + df_init_bottom(&opt->after_false); } - if (!expr) { - goto done; + if (expr->always_false) { + expr->probability = 0.0; + df_init_bottom(&opt->after_true); } - if (bfs_expr_has_children(expr)) { - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; - if (rhs) { - expr->persistent_fds = rhs->persistent_fds; - expr->ephemeral_fds = rhs->ephemeral_fds; + df_dump(opt, "after true", &opt->after_true); + df_dump(opt, "after false", &opt->after_false); + + if (df_is_bottom(&opt->after_false)) { + if (!expr->pure) { + expr->always_true = true; + 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"); + expr = opt_const(opt, true); + if (!expr) { + return NULL; + } } - if (lhs) { - expr->persistent_fds += lhs->persistent_fds; - if (lhs->ephemeral_fds > expr->ephemeral_fds) { - expr->ephemeral_fds = lhs->ephemeral_fds; + } + + if (df_is_bottom(&opt->after_true)) { + if (!expr->pure) { + expr->always_false = true; + expr->probability = 0.0; + } else if (expr->eval_fn != eval_false) { + opt_warning(opt, expr, "This expression is always false.\n\n"); + opt_debug(opt, "pure, always false\n"); + expr = opt_const(opt, false); + if (!expr) { + return NULL; } } } - if (expr->always_true) { - set_facts_impossible(&state->facts_when_false); + return visit_leave(opt, expr, visitor); +} + +/** 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"); } - if (expr->always_false) { - set_facts_impossible(&state->facts_when_true); + + if (expr->kind != BFS_TEST) { + goto done; } - if (optlevel < 2 || expr == &bfs_true || expr == &bfs_false) { + if (!opt_warning(opt, expr, "The result of this expression is ignored.\n")) { goto done; } - if (facts_are_impossible(&state->facts_when_true)) { - if (expr->pure) { - opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_false); - opt_warning(state, expr, "This expression is always false.\n\n"); - bfs_expr_free(expr); - expr = &bfs_false; - } else { - expr->always_false = true; - expr->probability = 0.0; - } - } else if (facts_are_impossible(&state->facts_when_false)) { - if (expr->pure) { - opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_true); - opt_warning(state, expr, "This expression is always true.\n\n"); - bfs_expr_free(expr); - expr = &bfs_true; - } else { - expr->always_true = true; - expr->probability = 1.0; + 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; } -/** Swap the children of a binary expression if it would reduce the cost. */ -static bool reorder_expr(const struct opt_state *state, struct bfs_expr *expr, float swapped_cost) { - if (swapped_cost < expr->cost) { - bool debug = opt_debug(state, 3, "cost: %pe <==> ", expr); - struct bfs_expr *lhs = expr->lhs; - expr->lhs = expr->rhs; - expr->rhs = lhs; - if (debug) { - cfprintf(state->ctx->cerr, "%pe (~${ylw}%g${rs} --> ~${ylw}%g${rs})\n", expr, expr->cost, swapped_cost); +/** 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; + } + } + + if (df_is_bottom(&opt->before)) { + opt_debug(opt, "unreachable\n"); + opt_warning(opt, expr, "This expression is unreachable.\n\n"); + expr = opt_const(opt, false); + if (!expr) { + return NULL; + } + } + + /** Table of simple predicates. */ + static const struct { + bfs_eval_fn *eval_fn; + enum pred_type pred; + } preds[] = { + {eval_acl, ACL_PRED}, + {eval_capable, CAPABLE_PRED}, + {eval_empty, EMPTY_PRED}, + {eval_hidden, HIDDEN_PRED}, + {eval_nogroup, NOGROUP_PRED}, + {eval_nouser, NOUSER_PRED}, + {eval_sparse, SPARSE_PRED}, + {eval_xattr, XATTR_PRED}, + }; + + for (size_t i = 0; i < countof(preds); ++i) { + if (preds[i].eval_fn == expr->eval_fn) { + data_flow_pred(opt, preds[i].pred, true); + break; + } + } + + /** Table of simple range comparisons. */ + static const struct { + bfs_eval_fn *eval_fn; + enum range_type range; + } ranges[] = { + {eval_depth, DEPTH_RANGE}, + {eval_gid, GID_RANGE}, + {eval_inum, INUM_RANGE}, + {eval_links, LINKS_RANGE}, + {eval_size, SIZE_RANGE}, + {eval_uid, UID_RANGE}, + }; + + for (size_t i = 0; i < countof(ranges); ++i) { + if (ranges[i].eval_fn == expr->eval_fn) { + data_flow_icmp(opt, expr, ranges[i].range); + break; } - expr->cost = swapped_cost; - return true; - } else { - return false; } + + return expr; } /** - * Recursively reorder sub-expressions to reduce the overall cost. - * - * @param expr - * The expression to optimize. - * @return - * Whether any subexpression was reordered. + * Data flow visitor. */ -static bool reorder_expr_recursive(const struct opt_state *state, struct bfs_expr *expr) { - if (!bfs_expr_has_children(expr)) { - return false; +static const struct visitor data_flow = { + .name = "data_flow", + .enter = data_flow_enter, + .visit = data_flow_visit, + .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}, + {eval_uid, data_flow_uid}, + {eval_xtype, data_flow_xtype}, + {NULL, NULL}, + }, +}; + +/** Simplify a negation. */ +static struct bfs_expr *simplify_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (opt->ignore_result) { + opt_debug(opt, "ignored result\n"); + expr = only_child(expr); } - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; + return expr; +} - bool ret = false; - if (lhs) { - ret |= reorder_expr_recursive(state, lhs); +/** Lift negations out of a conjunction/disjunction using De Morgan's laws. */ +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_expr (child, expr) { + if (child->eval_fn == eval_not) { + ++removed; + } else { + ++added; + } } - if (rhs) { - ret |= reorder_expr_recursive(state, rhs); + if (added >= removed) { + return visit_shallow(opt, expr, &annotate); } - if (expr->eval_fn == eval_and || expr->eval_fn == eval_or) { - if (lhs->pure && rhs->pure) { - float rhs_prob = expr->eval_fn == eval_and ? rhs->probability : 1.0 - rhs->probability; - float swapped_cost = rhs->cost + rhs_prob*lhs->cost; - ret |= reorder_expr(state, expr, swapped_cost); + opt_debug(opt, "De Morgan's laws\n"); + + if (expr->eval_fn == eval_and) { + expr->eval_fn = eval_or; + expr->argv = &fake_or_arg; + } else { + bfs_assert(expr->eval_fn == eval_or); + expr->eval_fn = eval_and; + expr->argv = &fake_and_arg; + } + + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + opt_enter(opt, "%pe\n", child); + + child = negate_expr(opt, child, &fake_not_arg); + if (!child) { + return NULL; + } + + opt_leave(opt, "%pe\n", child); + bfs_expr_append(expr, child); + } + + expr = visit_shallow(opt, expr, &annotate); + if (!expr) { + return NULL; + } + + return negate_expr(opt, expr, &fake_not_arg); +} + +/** Get the first ignorable expression in a conjunction/disjunction. */ +static struct bfs_expr *first_ignorable(struct bfs_opt *opt, struct bfs_expr *expr) { + if (opt->level < 2 || !opt->ignore_result) { + return NULL; + } + + struct bfs_expr *ret = NULL; + for_expr (child, expr) { + if (!child->pure) { + ret = NULL; + } else if (!ret) { + ret = child; } } return ret; } +/** Simplify a conjunction. */ +static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_expr *ignorable = first_ignorable(opt, expr); + bool ignore = false; + + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + if (child == ignorable) { + ignore = true; + } + + if (ignore) { + if (!opt_ignore(opt, child, true)) { + return NULL; + } + continue; + } + + if (child->eval_fn == eval_true) { + opt_delete(opt, "%pe [conjunction elimination]\n", child); + continue; + } + + opt_visit(opt, "%pe\n", child); + bfs_expr_append(expr, child); + + if (child->always_false) { + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); + } + } + } + + struct bfs_expr *child = bfs_expr_children(expr); + if (!child) { + opt_debug(opt, "nullary identity\n"); + return opt_const(opt, true); + } else if (!child->next) { + opt_debug(opt, "unary identity\n"); + return only_child(expr); + } + + return lift_andor_not(opt, expr); +} + +/** Simplify a disjunction. */ +static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_expr *ignorable = first_ignorable(opt, expr); + bool ignore = false; + + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + if (child == ignorable) { + ignore = true; + } + + if (ignore) { + if (!opt_ignore(opt, child, true)) { + return NULL; + } + continue; + } + + if (child->eval_fn == eval_false) { + opt_delete(opt, "%pe [disjunctive syllogism]\n", child); + continue; + } + + opt_visit(opt, "%pe\n", child); + bfs_expr_append(expr, child); + + if (child->always_true) { + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); + } + } + } + + struct bfs_expr *child = bfs_expr_children(expr); + if (!child) { + opt_debug(opt, "nullary identity\n"); + return opt_const(opt, false); + } else if (!child->next) { + opt_debug(opt, "unary identity\n"); + return only_child(expr); + } + + return lift_andor_not(opt, expr); +} + +/** Simplify a comma expression. */ +static struct bfs_expr *simplify_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct bfs_exprs children; + foster_children(expr, &children); + + drain_slist (struct bfs_expr, child, &children) { + if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) { + if (!opt_ignore(opt, child, true)) { + return NULL; + } + continue; + } + + opt_visit(opt, "%pe\n", child); + bfs_expr_append(expr, child); + } + + struct bfs_expr *child = bfs_expr_children(expr); + if (child && !child->next) { + opt_debug(opt, "unary identity\n"); + return only_child(expr); + } + + return expr; +} + /** - * Optimize a top-level expression. + * Logical simplification visitor. */ -static struct bfs_expr *optimize_expr(struct opt_state *state, struct bfs_expr *expr) { - struct opt_facts saved_impure = *state->facts_when_impure; +static const struct visitor simplify = { + .name = "simplify", + .table = (const struct visitor_table[]) { + {eval_not, simplify_not}, + {eval_and, simplify_and}, + {eval_or, simplify_or}, + {eval_comma, simplify_comma}, + {NULL, NULL}, + }, +}; - expr = optimize_expr_recursive(state, expr); - if (!expr) { - return NULL; - } +/** Optimize an expression. */ +static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { + opt_enter(opt, "pass 0:\n"); + expr = visit(opt, expr, &annotate); + opt_leave(opt, NULL); + + /** Table of optimization passes. */ + static const struct { + /** Minimum optlevel for this pass. */ + int level; + /** The visitor for this pass. */ + const struct visitor *visitor; + } passes[] = { + {1, &canonicalize}, + {3, &reorder}, + {2, &data_flow}, + {1, &simplify}, + }; - if (state->ctx->optlevel >= 3 && reorder_expr_recursive(state, expr)) { - // Re-do optimizations to account for the new ordering - *state->facts_when_impure = saved_impure; - expr = optimize_expr_recursive(state, expr); - if (!expr) { - return NULL; + 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; + nested.impure = &impure; + impure = *opt->impure; + + opt_enter(&nested, "pass %d:\n", i + 1); + + for (size_t j = 0; j < countof(passes); ++j) { + if (opt->level < passes[j].level) { + continue; + } + + const struct visitor *visitor = passes[j].visitor; + + // Skip reordering the first time through the passes, to + // make warnings more understandable + if (visitor == &reorder) { + if (i == 0) { + continue; + } else { + nested.warn = false; + } + } + + 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); + + if (!bfs_expr_is_parent(expr)) { + break; } } + *opt->impure = impure; return expr; } +/** 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 nonmatch_odds = 1.0; + float reached_odds = 1.0; + 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; + } else if (expr->eval_fn == eval_or) { + reached_odds *= 1.0 - child->probability; + } + } + + 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(). */ +static float estimate_stat_odds(struct bfs_ctx *ctx) { + if (ctx->unique) { + return 1.0; + } + + float nostat_odds = 1.0 - estimate_odds(ctx->exclude, calls_stat); + + float reached_odds = 1.0 - ctx->exclude->probability; + 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); - struct opt_facts facts_when_impure; - set_facts_impossible(&facts_when_impure); + struct df_domain impure; + df_init_bottom(&impure); - struct opt_state state = { + struct bfs_opt opt = { .ctx = ctx, - .facts_when_impure = &facts_when_impure, + .level = ctx->optlevel, + .depth = 0, + .warn = ctx->warn, + .ignore_result = false, + .impure = &impure, }; - facts_init(&state.facts); + df_init_top(&opt.before); - ctx->exclude = optimize_expr(&state, ctx->exclude); + ctx->exclude = optimize(&opt, ctx->exclude); if (!ctx->exclude) { return -1; } // Only non-excluded files are evaluated - state.facts = state.facts_when_false; + opt.before = opt.after_false; + opt.ignore_result = true; - struct range *depth = &state.facts.ranges[DEPTH_RANGE]; - constrain_min(depth, ctx->mindepth); - constrain_max(depth, ctx->maxdepth); + struct df_range *depth = &opt.before.ranges[DEPTH_RANGE]; + if (ctx->mindepth > 0) { + constrain_min(depth, ctx->mindepth); + } + if (ctx->maxdepth < INT_MAX) { + constrain_max(depth, ctx->maxdepth); + } - ctx->expr = optimize_expr(&state, ctx->expr); + ctx->expr = optimize(&opt, ctx->expr); if (!ctx->expr) { return -1; } - ctx->expr = ignore_result(&state, ctx->expr); - - if (facts_are_impossible(&facts_when_impure)) { + if (opt.level >= 2 && df_is_bottom(&impure)) { bfs_warning(ctx, "This command won't do anything.\n\n"); } - const struct range *depth_when_impure = &facts_when_impure.ranges[DEPTH_RANGE]; - long long mindepth = depth_when_impure->min; - long long maxdepth = depth_when_impure->max; + const struct df_range *impure_depth = &impure.ranges[DEPTH_RANGE]; + long long mindepth = impure_depth->min; + long long maxdepth = impure_depth->max; - int optlevel = ctx->optlevel; + opt_enter(&opt, "post-process:\n"); - if (optlevel >= 2 && mindepth > ctx->mindepth) { + if (opt.level >= 2 && mindepth > ctx->mindepth) { if (mindepth > INT_MAX) { mindepth = INT_MAX; } + opt_enter(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth); ctx->mindepth = mindepth; - opt_debug(&state, 2, "data flow: mindepth --> %d\n", ctx->mindepth); + opt_leave(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth); } - if (optlevel >= 4 && maxdepth < ctx->maxdepth) { + if (opt.level >= 4 && maxdepth < ctx->maxdepth) { if (maxdepth < INT_MIN) { maxdepth = INT_MIN; } + opt_enter(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth); ctx->maxdepth = maxdepth; - opt_debug(&state, 4, "data flow: maxdepth --> %d\n", ctx->maxdepth); + opt_leave(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth); } + if (opt.level >= 3) { + // bfs_eval() can do lazy stat() calls, but only on one thread. + float lazy_cost = estimate_stat_odds(ctx); + // bftw() can do eager stat() calls in parallel + float eager_cost = 1.0 / ctx->threads; + + if (eager_cost <= lazy_cost) { + opt_enter(&opt, "lazy stat cost: ${ylw}%g${rs}\n", lazy_cost); + ctx->flags |= BFTW_STAT; + 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); + return 0; } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Optimization. @@ -26,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. @@ -34,4 +21,3 @@ struct bfs_ctx; int bfs_optimize(struct bfs_ctx *ctx); #endif // BFS_OPT_H - diff --git a/src/parse.c b/src/parse.c index fbb095d..5ec4c0e 100644 --- a/src/parse.c +++ b/src/parse.c @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * The command line parser. Expressions are parsed by recursive descent, with a @@ -22,27 +9,30 @@ */ #include "parse.h" + +#include "alloc.h" #include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "color.h" #include "ctx.h" -#include "darray.h" #include "diag.h" #include "dir.h" #include "eval.h" #include "exec.h" #include "expr.h" #include "fsade.h" +#include "list.h" #include "opt.h" #include "printf.h" #include "pwcache.h" +#include "sanity.h" #include "stat.h" #include "typo.h" -#include "util.h" #include "xregex.h" #include "xspawn.h" #include "xtime.h" -#include <assert.h> + #include <errno.h> #include <fcntl.h> #include <fnmatch.h> @@ -50,170 +40,21 @@ #include <limits.h> #include <pwd.h> #include <stdarg.h> -#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/time.h> #include <sys/stat.h> -#include <sys/wait.h> +#include <sys/types.h> #include <time.h> #include <unistd.h> // Strings printed by -D tree for "fake" expressions -static char *fake_and_arg = "-a"; -static char *fake_false_arg = "-false"; +static char *fake_and_arg = "-and"; static char *fake_hidden_arg = "-hidden"; -static char *fake_or_arg = "-o"; +static char *fake_or_arg = "-or"; static char *fake_print_arg = "-print"; static char *fake_true_arg = "-true"; -// Cost estimation constants -#define FAST_COST 40.0 -#define STAT_COST 1000.0 -#define PRINT_COST 20000.0 - -struct bfs_expr bfs_true = { - .eval_fn = eval_true, - .argc = 1, - .argv = &fake_true_arg, - .pure = true, - .always_true = true, - .synthetic = true, - .cost = FAST_COST, - .probability = 1.0, -}; - -struct bfs_expr bfs_false = { - .eval_fn = eval_false, - .argc = 1, - .argv = &fake_false_arg, - .pure = true, - .always_false = true, - .synthetic = true, - .cost = FAST_COST, - .probability = 0.0, -}; - -void bfs_expr_free(struct bfs_expr *expr) { - if (!expr || expr == &bfs_true || expr == &bfs_false) { - return; - } - - if (bfs_expr_has_children(expr)) { - bfs_expr_free(expr->rhs); - bfs_expr_free(expr->lhs); - } else if (expr->eval_fn == eval_exec) { - bfs_exec_free(expr->exec); - } else if (expr->eval_fn == eval_fprintf) { - bfs_printf_free(expr->printf); - } else if (expr->eval_fn == eval_regex) { - bfs_regfree(expr->regex); - } - - free(expr); -} - -struct bfs_expr *bfs_expr_new(bfs_eval_fn *eval_fn, size_t argc, char **argv) { - struct bfs_expr *expr = malloc(sizeof(*expr)); - if (!expr) { - perror("malloc()"); - return NULL; - } - - expr->eval_fn = eval_fn; - expr->argc = argc; - expr->argv = argv; - expr->persistent_fds = 0; - expr->ephemeral_fds = 0; - expr->pure = false; - expr->always_true = false; - expr->always_false = false; - expr->synthetic = false; - expr->cost = FAST_COST; - expr->probability = 0.5; - expr->evaluations = 0; - expr->successes = 0; - expr->elapsed.tv_sec = 0; - expr->elapsed.tv_nsec = 0; - return expr; -} - -/** - * Create a new unary expression. - */ -static struct bfs_expr *new_unary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv); - if (!expr) { - bfs_expr_free(rhs); - return NULL; - } - - expr->lhs = NULL; - expr->rhs = rhs; - assert(bfs_expr_has_children(expr)); - - expr->persistent_fds = rhs->persistent_fds; - expr->ephemeral_fds = rhs->ephemeral_fds; - return expr; -} - -/** - * Create a new binary expression. - */ -static struct bfs_expr *new_binary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv); - if (!expr) { - bfs_expr_free(rhs); - bfs_expr_free(lhs); - return NULL; - } - - expr->lhs = lhs; - expr->rhs = rhs; - assert(bfs_expr_has_children(expr)); - - if (argv == &fake_and_arg || argv == &fake_or_arg) { - expr->synthetic = true; - } - - expr->persistent_fds = lhs->persistent_fds + rhs->persistent_fds; - if (lhs->ephemeral_fds > rhs->ephemeral_fds) { - expr->ephemeral_fds = lhs->ephemeral_fds; - } else { - expr->ephemeral_fds = rhs->ephemeral_fds; - } - - return expr; -} - -bool bfs_expr_has_children(const struct bfs_expr *expr) { - return expr->eval_fn == eval_and - || expr->eval_fn == eval_or - || expr->eval_fn == eval_not - || expr->eval_fn == eval_comma; -} - -bool bfs_expr_never_returns(const struct bfs_expr *expr) { - // Expressions that never return are vacuously both always true and always false - return expr->always_true && expr->always_false; -} - -/** - * Set an expression to always return true. - */ -static void expr_set_always_true(struct bfs_expr *expr) { - expr->always_true = true; - expr->probability = 1.0; -} - -/** - * Set an expression to never return. - */ -static void expr_set_never_returns(struct bfs_expr *expr) { - expr->always_true = expr->always_false = true; -} - /** * Color use flags. */ @@ -224,9 +65,9 @@ enum use_color { }; /** - * Ephemeral state for parsing the command line. + * Command line parser state. */ -struct parser_state { +struct bfs_parser { /** The command line being constructed. */ struct bfs_ctx *ctx; /** The command line arguments being parsed. */ @@ -239,14 +80,10 @@ struct parser_state { /** 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. */ @@ -256,48 +93,30 @@ struct parser_state { /** The last non-path argument. */ char **last_arg; - /** A "-depth"-type argument, if any. */ - char **depth_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_arg; - /** A "-files0-from -" argument, if any. */ - char **files0_stdin_arg; - /** An "-ok"-type expression, if any. */ - const struct bfs_expr *ok_expr; - - /** The current time. */ + /** 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 parser_state *state, const char *str) { - bfs_perror(state->ctx, str); +static void parse_perror(const struct bfs_parser *parser, const char *str) { + bfs_perror(parser->ctx, str); } /** Initialize an empty highlighted range. */ @@ -311,7 +130,7 @@ static void init_highlight(const struct bfs_ctx *ctx, bool *args) { static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, bool *args) { size_t i = argv - ctx->argv; for (size_t j = 0; j < argc; ++j) { - assert(i + j < ctx->argc); + bfs_assert(i + j < ctx->argc); args[i + j] = true; } } @@ -319,30 +138,27 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, /** * Print an error message during parsing. */ -BFS_FORMATTER(2, 3) -static void parse_error(const struct parser_state *state, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_printf(2, 3) +static void parse_error(const struct bfs_parser *parser, const char *format, ...) { + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, state->argv, 1, highlight); + highlight_args(ctx, parser->argv, 1, highlight); bfs_argv_error(ctx, highlight); va_list args; va_start(args, format); - errno = error; - bfs_verror(state->ctx, format, args); + bfs_verror(parser->ctx, format, args); va_end(args); } /** * Print an error about some command line arguments. */ -BFS_FORMATTER(4, 5) -static void parse_argv_error(const struct parser_state *state, char **argv, size_t argc, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_printf(4, 5) +static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) { + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); @@ -351,7 +167,6 @@ static void parse_argv_error(const struct parser_state *state, char **argv, size va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -359,20 +174,18 @@ static void parse_argv_error(const struct parser_state *state, char **argv, size /** * Print an error about conflicting command line arguments. */ -BFS_FORMATTER(6, 7) -static void parse_conflict_error(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_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); } @@ -380,16 +193,14 @@ static void parse_conflict_error(const struct parser_state *state, char **argv1, /** * Print an error about an expression. */ -BFS_FORMATTER(3, 4) -static void parse_expr_error(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_printf(3, 4) +static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { + 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); } @@ -397,22 +208,20 @@ static void parse_expr_error(const struct parser_state *state, const struct bfs_ /** * Print a warning message during parsing. */ -BFS_FORMATTER(2, 3) -static bool parse_warning(const struct parser_state *state, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_printf(2, 3) +static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) { + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, state->argv, 1, highlight); + highlight_args(ctx, parser->argv, 1, highlight); if (!bfs_argv_warning(ctx, highlight)) { return false; } va_list args; va_start(args, format); - errno = error; - bool ret = bfs_vwarning(state->ctx, format, args); + bool ret = bfs_vwarning(parser->ctx, format, args); va_end(args); return ret; } @@ -420,22 +229,20 @@ static bool parse_warning(const struct parser_state *state, const char *format, /** * Print a warning about conflicting command line arguments. */ -BFS_FORMATTER(6, 7) -static bool parse_conflict_warning(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_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; @@ -444,10 +251,9 @@ static bool parse_conflict_warning(const struct parser_state *state, char **argv /** * Print a warning about an expression. */ -BFS_FORMATTER(3, 4) -static bool parse_expr_warning(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; - const struct bfs_ctx *ctx = state->ctx; +_printf(3, 4) +static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { + const struct bfs_ctx *ctx = parser->ctx; if (!bfs_expr_warning(ctx, expr)) { return false; @@ -455,26 +261,79 @@ static bool parse_expr_warning(const struct parser_state *state, const struct bf 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, 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()"); + } + return expr; +} + +/** + * 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, BFS_OPERATOR); + if (!expr) { + return NULL; + } + + bfs_assert(bfs_expr_is_parent(expr)); + bfs_expr_append(expr, rhs); + return expr; +} + +/** + * 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, BFS_OPERATOR); + if (!expr) { + return NULL; + } + + bfs_assert(bfs_expr_is_parent(expr)); + bfs_expr_append(expr, lhs); + bfs_expr_append(expr, rhs); + return expr; +} + +/** * Fill in a "-print"-type expression. */ -static void init_print_expr(struct parser_state *state, struct bfs_expr *expr) { - expr_set_always_true(expr); - expr->cost = PRINT_COST; - expr->cfile = state->ctx->cout; +static void init_print_expr(struct bfs_parser *parser, struct bfs_expr *expr) { + expr->cfile = parser->ctx->cout; + expr->path = NULL; } /** * Open a file for an expression. */ -static int expr_open(struct parser_state *state, struct bfs_expr *expr, const char *path) { - struct bfs_ctx *ctx = state->ctx; +static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const char *path) { + struct bfs_ctx *ctx = parser->ctx; FILE *file = NULL; CFILE *cfile = NULL; @@ -484,7 +343,7 @@ static int expr_open(struct parser_state *state, struct bfs_expr *expr, const ch goto fail; } - cfile = cfwrap(file, state->use_color ? ctx->colors : NULL, true); + cfile = cfwrap(file, parser->use_color ? ctx->colors : NULL, true); if (!cfile) { goto fail; } @@ -499,10 +358,11 @@ static int expr_open(struct parser_state *state, struct bfs_expr *expr, const ch } expr->cfile = dedup; + expr->path = path; return 0; fail: - parse_expr_error(state, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); if (cfile) { cfclose(cfile); } else if (file) { @@ -514,15 +374,15 @@ fail: /** * Invoke bfs_stat() on an argument. */ -static int stat_arg(const struct parser_state *state, char **arg, struct bfs_stat *sb) { - const struct bfs_ctx *ctx = state->ctx; +static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat *sb) { + const struct bfs_ctx *ctx = parser->ctx; bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL); enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW; int ret = bfs_stat(AT_FDCWD, *arg, flags, sb); if (ret != 0) { - parse_argv_error(state, arg, 1, "%m.\n"); + parse_argv_error(parser, arg, 1, "%s.\n", errstr()); } return ret; } @@ -530,52 +390,57 @@ static int stat_arg(const struct parser_state *state, char **arg, struct bfs_sta /** * Parse the expression specified on the command line. */ -static struct bfs_expr *parse_expr(struct parser_state *state); +static struct bfs_expr *parse_expr(struct bfs_parser *parser); /** * Advance by a single token. */ -static char **parser_advance(struct parser_state *state, enum token_type type, size_t argc) { - if (type != T_FLAG && type != T_PATH) { - state->expr_started = true; +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) { - state->last_arg = state->argv; + if (kind != BFS_PATH) { + parser->last_arg = parser->argv; } - char **argv = state->argv; - state->argv += argc; + size_t i = parser->argv - ctx->argv; + ctx->kinds[i] = kind; + + char **argv = parser->argv; + parser->argv += argc; return argv; } /** * Parse a root path. */ -static int parse_root(struct parser_state *state, const char *path) { - char *copy = strdup(path); - if (!copy) { - parse_perror(state, "strdup()"); +static int parse_root(struct bfs_parser *parser, const char *path) { + struct bfs_ctx *ctx = parser->ctx; + const char **root = RESERVE(const char *, &ctx->paths, &ctx->npaths); + if (!root) { + parse_perror(parser, "RESERVE()"); return -1; } - struct bfs_ctx *ctx = state->ctx; - if (DARRAY_PUSH(&ctx->paths, ©) != 0) { - parse_perror(state, "DARRAY_PUSH()"); - free(copy); + *root = strdup(path); + if (!*root) { + --ctx->npaths; + parse_perror(parser, "strdup()"); return -1; } - state->implicit_root = false; return 0; } /** * While parsing an expression, skip any paths and add them to ctx->paths. */ -static int skip_paths(struct parser_state *state) { +static int skip_paths(struct bfs_parser *parser) { while (true) { - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (!arg) { return 0; } @@ -585,7 +450,7 @@ static int skip_paths(struct parser_state *state) { // 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(state, T_FLAG, 1); + parser_advance(parser, BFS_FLAG, 1); continue; } if (strcmp(arg, "-") != 0) { @@ -600,7 +465,7 @@ static int skip_paths(struct parser_state *state) { return 0; } - if (state->expr_started) { + if (parser->expr_started) { // By POSIX, these can be paths. We only treat them as // such at the beginning of the command line. if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) { @@ -608,16 +473,16 @@ static int skip_paths(struct parser_state *state) { } } - if (state->excluding) { - parse_warning(state, "This path will not be excluded. Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n"); - bfs_warning(state->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n"); + if (parser->excluding) { + parse_warning(parser, "This path will not be excluded. Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n"); + bfs_warning(parser->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n"); } - if (parse_root(state, arg) != 0) { + if (parse_root(parser, arg) != 0) { return -1; } - parser_advance(state, T_PATH, 1); + parser_advance(parser, BFS_PATH, 1); } } @@ -636,17 +501,15 @@ enum int_flags { /** * Parse an integer. */ -static const char *parse_int(const struct parser_state *state, char **arg, const char *str, void *result, enum int_flags flags) { - char *endptr; - +static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) { int base = flags & IF_BASE_MASK; if (base == 0) { base = 10; } - errno = 0; - long long value = strtoll(str, &endptr, base); - if (errno != 0) { + char *endptr; + long long value; + if (xstrtoll(str, &endptr, base, &value) != 0) { if (errno == ERANGE) { goto range; } else { @@ -654,10 +517,6 @@ static const char *parse_int(const struct parser_state *state, char **arg, const } } - if (endptr == str) { - goto bad; - } - if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') { goto bad; } @@ -686,7 +545,7 @@ static const char *parse_int(const struct parser_state *state, char **arg, const break; default: - assert(!"Invalid int size"); + bfs_bug("Invalid int size"); goto bad; } @@ -694,19 +553,19 @@ static const char *parse_int(const struct parser_state *state, char **arg, const bad: if (!(flags & IF_QUIET)) { - parse_argv_error(state, arg, 1, "${bld}%s${rs} is not a valid integer.\n", str); + parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is not a valid integer.\n", str); } return NULL; negative: if (!(flags & IF_QUIET)) { - parse_argv_error(state, arg, 1, "Negative integer ${bld}%s${rs} is not allowed here.\n", str); + parse_argv_error(parser, arg, 1, "Negative integer ${bld}%pq${rs} is not allowed here.\n", str); } return NULL; range: if (!(flags & IF_QUIET)) { - parse_argv_error(state, arg, 1, "${bld}%s${rs} is too large an integer.\n", str); + parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is too large an integer.\n", str); } return NULL; } @@ -714,7 +573,7 @@ range: /** * Parse an integer and a comparison flag. */ -static const char *parse_icmp(const struct parser_state *state, struct bfs_expr *expr, enum int_flags flags) { +static const char *parse_icmp(const struct bfs_parser *parser, struct bfs_expr *expr, enum int_flags flags) { char **arg = &expr->argv[1]; const char *str = *arg; switch (str[0]) { @@ -731,7 +590,7 @@ static const char *parse_icmp(const struct parser_state *state, struct bfs_expr break; } - return parse_int(state, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED); + return parse_int(parser, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED); } /** @@ -753,136 +612,164 @@ static bool looks_like_icmp(const char *str) { /** * Parse a single flag. */ -static struct bfs_expr *parse_flag(struct parser_state *state, size_t argc) { - parser_advance(state, T_FLAG, argc); - return &bfs_true; +static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) { + char **argv = parser_advance(parser, BFS_FLAG, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_FLAG); } /** * Parse a flag that doesn't take a value. */ -static struct bfs_expr *parse_nullary_flag(struct parser_state *state) { - return parse_flag(state, 1); +static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) { + return parse_flag(parser, 1); +} + +/** + * Parse a flag that takes a value. + */ +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}-%c${rs} needs a value.\n", flag); + return NULL; + } + + return parse_flag(parser, 2); +} + +/** + * 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 parser_state *state, size_t argc) { - parser_advance(state, T_OPTION, argc); - return &bfs_true; +static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) { + char **argv = parser_advance(parser, BFS_OPTION, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_OPTION); } /** * Parse an option that doesn't take a value. */ -static struct bfs_expr *parse_nullary_option(struct parser_state *state) { - return parse_option(state, 1); +static struct bfs_expr *parse_nullary_option(struct bfs_parser *parser) { + return parse_option(parser, 1); } /** * Parse an option that takes a value. */ -static struct bfs_expr *parse_unary_option(struct parser_state *state) { - return parse_option(state, 2); +static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) { + const char *arg = parser->argv[0]; + const char *value = parser->argv[1]; + if (!value) { + parse_error(parser, "${blu}%s${rs} needs a value.\n", arg); + return NULL; + } + + return parse_option(parser, 2); } /** * Parse a single test. */ -static struct bfs_expr *parse_test(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(state, T_TEST, argc); - struct bfs_expr *expr = bfs_expr_new(eval_fn, argc, argv); - if (expr) { - expr->pure = true; - } - return expr; +static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { + char **argv = parser_advance(parser, BFS_TEST, argc); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_TEST); } /** * Parse a test that doesn't take a value. */ -static struct bfs_expr *parse_nullary_test(struct parser_state *state, bfs_eval_fn *eval_fn) { - return parse_test(state, eval_fn, 1); +static struct bfs_expr *parse_nullary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) { + return parse_test(parser, eval_fn, 1); } /** * Parse a test that takes a value. */ -static struct bfs_expr *parse_unary_test(struct parser_state *state, bfs_eval_fn *eval_fn) { - const char *arg = state->argv[0]; - const char *value = state->argv[1]; +static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) { + const char *arg = parser->argv[0]; + const char *value = parser->argv[1]; if (!value) { - parse_error(state, "${blu}%s${rs} needs a value.\n", arg); + parse_error(parser, "${blu}%s${rs} needs a value.\n", arg); return NULL; } - return parse_test(state, eval_fn, 2); + return parse_test(parser, eval_fn, 2); } /** * Parse a single action. */ -static struct bfs_expr *parse_action(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(state, T_ACTION, argc); +static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { + char **argv = parser_advance(parser, BFS_ACTION, argc); - if (state->excluding) { - parse_argv_error(state, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n"); + if (parser->excluding) { + parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n"); return NULL; } - if (eval_fn != eval_prune && eval_fn != eval_quit) { - state->implicit_print = false; + if (eval_fn != eval_limit && eval_fn != eval_prune && eval_fn != eval_quit) { + parser->implicit_print = false; } - return bfs_expr_new(eval_fn, argc, argv); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_ACTION); } /** * Parse an action that takes no arguments. */ -static struct bfs_expr *parse_nullary_action(struct parser_state *state, bfs_eval_fn *eval_fn) { - return parse_action(state, eval_fn, 1); +static struct bfs_expr *parse_nullary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) { + return parse_action(parser, eval_fn, 1); } /** * Parse an action that takes one argument. */ -static struct bfs_expr *parse_unary_action(struct parser_state *state, bfs_eval_fn *eval_fn) { - const char *arg = state->argv[0]; - const char *value = state->argv[1]; +static struct bfs_expr *parse_unary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) { + const char *arg = parser->argv[0]; + const char *value = parser->argv[1]; if (!value) { - parse_error(state, "${blu}%s${rs} needs a value.\n", arg); + parse_error(parser, "${blu}%s${rs} needs a value.\n", arg); return NULL; } - return parse_action(state, eval_fn, 2); -} - -/** - * Add an expression to the exclusions. - */ -static int parse_exclude(struct parser_state *state, struct bfs_expr *expr) { - struct bfs_ctx *ctx = state->ctx; - ctx->exclude = new_binary_expr(eval_or, ctx->exclude, expr, &fake_or_arg); - if (ctx->exclude) { - return 0; - } else { - return -1; - } + return parse_action(parser, eval_fn, 2); } /** * Parse a test expression with integer data and a comparison flag. */ -static struct bfs_expr *parse_test_icmp(struct parser_state *state, bfs_eval_fn *eval_fn) { - struct bfs_expr *expr = parse_unary_test(state, eval_fn); +static struct bfs_expr *parse_test_icmp(struct bfs_parser *parser, bfs_eval_fn *eval_fn) { + struct bfs_expr *expr = parse_unary_test(parser, eval_fn); if (!expr) { return NULL; } - if (!parse_icmp(state, expr, 0)) { - bfs_expr_free(expr); + if (!parse_icmp(parser, expr, 0)) { return NULL; } @@ -918,19 +805,17 @@ static bool parse_debug_flag(const char *flag, size_t len, const char *expected) /** * Parse -D FLAG. */ -static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int arg2) { - struct bfs_ctx *ctx = state->ctx; +static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; - const char *arg = state->argv[0]; - const char *flags = state->argv[1]; - if (!flags) { - parse_error(state, "${cyn}%s${rs} needs a flag.\n\n", arg); + const char *flags; + struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags); + if (!expr) { + cfprintf(ctx->cerr, "\n"); debug_help(ctx->cerr); return NULL; } - parser_advance(state, T_FLAG, 1); - bool unrecognized = false; for (const char *flag = flags, *next; flag; flag = next) { @@ -943,7 +828,7 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar if (parse_debug_flag(flag, len, "help")) { debug_help(ctx->cout); - state->just_info = true; + parser->just_info = true; return NULL; } else if (parse_debug_flag(flag, len, "all")) { ctx->debug = DEBUG_ALL; @@ -961,7 +846,7 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar if (DEBUG_ALL & i) { ctx->debug |= i; } else { - if (parse_warning(state, "Unrecognized debug flag ${bld}")) { + if (parse_expr_warning(parser, expr, "Unrecognized debug flag ${bld}")) { fwrite(flag, 1, len, stderr); cfprintf(ctx->cerr, "${rs}.\n\n"); unrecognized = true; @@ -974,91 +859,75 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar cfprintf(ctx->cerr, "\n"); } - parser_advance(state, T_FLAG, 1); - return &bfs_true; + return expr; } /** * Parse -On. */ -static struct bfs_expr *parse_optlevel(struct parser_state *state, int arg1, int arg2) { - int *optlevel = &state->ctx->optlevel; +static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) { + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg); + if (!expr) { + return NULL; + } - if (strcmp(state->argv[0], "-Ofast") == 0) { + int *optlevel = &parser->ctx->optlevel; + + if (strcmp(arg, "fast") == 0) { *optlevel = 4; - } else if (!parse_int(state, state->argv, state->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_warning(state, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", state->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 parse_nullary_flag(state); + return expr; } /** * Parse -[PHL], -follow. */ -static struct bfs_expr *parse_follow(struct parser_state *state, int flags, int option) { - struct bfs_ctx *ctx = state->ctx; +static struct bfs_expr *parse_follow(struct bfs_parser *parser, int flags, int option) { + struct bfs_ctx *ctx = parser->ctx; ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL); ctx->flags |= flags; if (option) { - return parse_nullary_option(state); + return parse_nullary_option(parser); } else { - return parse_nullary_flag(state); + return parse_nullary_flag(parser); } } /** * Parse -X. */ -static struct bfs_expr *parse_xargs_safe(struct parser_state *state, int arg1, int arg2) { - state->ctx->xargs_safe = true; - return parse_nullary_flag(state); +static struct bfs_expr *parse_xargs_safe(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->xargs_safe = true; + return parse_nullary_flag(parser); } /** * Parse -executable, -readable, -writable */ -static struct bfs_expr *parse_access(struct parser_state *state, int flag, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_access); - if (!expr) { - return NULL; - } - - expr->num = flag; - expr->cost = STAT_COST; - - switch (flag) { - case R_OK: - expr->probability = 0.99; - break; - case W_OK: - expr->probability = 0.8; - break; - case X_OK: - expr->probability = 0.2; - break; +static struct bfs_expr *parse_access(struct bfs_parser *parser, int flag, int arg2) { + struct bfs_expr *expr = parse_nullary_test(parser, eval_access); + if (expr) { + expr->num = flag; } - return expr; } /** * Parse -acl. */ -static struct bfs_expr *parse_acl(struct parser_state *state, int flag, int arg2) { +static struct bfs_expr *parse_acl(struct bfs_parser *parser, int flag, int arg2) { #if BFS_CAN_CHECK_ACL - struct bfs_expr *expr = parse_nullary_test(state, eval_acl); - if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.00002; - } - return expr; + return parse_nullary_test(parser, eval_acl); #else - parse_error(state, "Missing platform support.\n"); + parse_error(parser, "Missing platform support.\n"); return NULL; #endif } @@ -1066,38 +935,32 @@ static struct bfs_expr *parse_acl(struct parser_state *state, int flag, int arg2 /** * Parse -[aBcm]?newer. */ -static struct bfs_expr *parse_newer(struct parser_state *state, int field, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_newer); +static struct bfs_expr *parse_newer(struct bfs_parser *parser, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_newer); if (!expr) { return NULL; } struct bfs_stat sb; - if (stat_arg(state, &expr->argv[1], &sb) != 0) { - goto fail; + if (stat_arg(parser, &expr->argv[1], &sb) != 0) { + return NULL; } - expr->cost = STAT_COST; expr->reftime = sb.mtime; expr->stat_field = field; return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -[aBcm]min. */ -static struct bfs_expr *parse_min(struct parser_state *state, int field, int arg2) { - struct bfs_expr *expr = parse_test_icmp(state, eval_time); +static struct bfs_expr *parse_min(struct bfs_parser *parser, int field, int arg2) { + struct bfs_expr *expr = parse_test_icmp(parser, eval_time); if (!expr) { return NULL; } - expr->cost = STAT_COST; - expr->reftime = state->now; + expr->reftime = parser->now; expr->stat_field = field; expr->time_unit = BFS_MINUTES; return expr; @@ -1106,19 +969,18 @@ static struct bfs_expr *parse_min(struct parser_state *state, int field, int arg /** * Parse -[aBcm]time. */ -static struct bfs_expr *parse_time(struct parser_state *state, int field, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_time); +static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_time); if (!expr) { return NULL; } - expr->cost = STAT_COST; - expr->reftime = state->now; + expr->reftime = parser->now; expr->stat_field = field; - const char *tail = parse_icmp(state, expr, IF_PARTIAL_OK); + const char *tail = parse_icmp(parser, expr, IF_PARTIAL_OK); if (!tail) { - goto fail; + return NULL; } if (!*tail) { @@ -1133,21 +995,21 @@ static struct bfs_expr *parse_time(struct parser_state *state, int field, int ar switch (*tail) { case 'w': time *= 7; - BFS_FALLTHROUGH; + _fallthrough; case 'd': time *= 24; - BFS_FALLTHROUGH; + _fallthrough; case 'h': time *= 60; - BFS_FALLTHROUGH; + _fallthrough; case 'm': time *= 60; - BFS_FALLTHROUGH; + _fallthrough; case 's': break; default: - parse_expr_error(state, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail); - goto fail; + parse_expr_error(parser, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail); + return NULL; } expr->num += time; @@ -1156,37 +1018,28 @@ static struct bfs_expr *parse_time(struct parser_state *state, int field, int ar break; } - tail = parse_int(state, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED); + tail = parse_int(parser, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED); if (!tail) { - goto fail; + return NULL; } if (!*tail) { - parse_expr_error(state, expr, "Missing time unit.\n"); - goto fail; + parse_expr_error(parser, expr, "Missing time unit.\n"); + return NULL; } } expr->time_unit = BFS_SECONDS; return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -capable. */ -static struct bfs_expr *parse_capable(struct parser_state *state, int flag, int arg2) { +static struct bfs_expr *parse_capable(struct bfs_parser *parser, int flag, int arg2) { #if BFS_CAN_CHECK_CAPABILITIES - struct bfs_expr *expr = parse_nullary_test(state, eval_capable); - if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.000002; - } - return expr; + return parse_nullary_test(parser, eval_capable); #else - parse_error(state, "Missing platform support.\n"); + parse_error(parser, "Missing platform support.\n"); return NULL; #endif } @@ -1194,47 +1047,112 @@ static struct bfs_expr *parse_capable(struct parser_state *state, int flag, int /** * Parse -(no)?color. */ -static struct bfs_expr *parse_color(struct parser_state *state, int color, int arg2) { - struct bfs_ctx *ctx = state->ctx; +static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int arg2) { + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + + struct bfs_ctx *ctx = parser->ctx; struct colors *colors = ctx->colors; if (color) { if (!colors) { - parse_error(state, "%s.\n", strerror(ctx->colors_error)); + parse_expr_error(parser, expr, "Error parsing $$LS_COLORS: %s.\n", xstrerror(ctx->colors_error)); return NULL; } - state->use_color = COLOR_ALWAYS; + parser->use_color = COLOR_ALWAYS; ctx->cout->colors = colors; ctx->cerr->colors = colors; } else { - state->use_color = COLOR_NEVER; + parser->use_color = COLOR_NEVER; ctx->cout->colors = NULL; ctx->cerr->colors = NULL; } - return parse_nullary_option(state); + 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/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 parser_state *state, int value, int arg2) { - parser_advance(state, T_TEST, 1); - return value ? &bfs_true : &bfs_false; +static struct bfs_expr *parse_const(struct bfs_parser *parser, int value, int arg2) { + return parse_nullary_test(parser, value ? eval_true : eval_false); } /** * Parse -daystart. */ -static struct bfs_expr *parse_daystart(struct parser_state *state, int arg1, int arg2) { +static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int arg2) { struct tm tm; - if (xlocaltime(&state->now.tv_sec, &tm) != 0) { - parse_perror(state, "xlocaltime()"); + if (!localtime_r(&parser->now.tv_sec, &tm)) { + parse_perror(parser, "localtime_r()"); return NULL; } - if (tm.tm_hour || tm.tm_min || tm.tm_sec || state->now.tv_nsec) { + if (tm.tm_hour || tm.tm_min || tm.tm_sec || parser->now.tv_nsec) { ++tm.tm_mday; } tm.tm_hour = 0; @@ -1243,100 +1161,139 @@ static struct bfs_expr *parse_daystart(struct parser_state *state, int arg1, int time_t time; if (xmktime(&tm, &time) != 0) { - parse_perror(state, "xmktime()"); + parse_perror(parser, "xmktime()"); return NULL; } - state->now.tv_sec = time; - state->now.tv_nsec = 0; + parser->now.tv_sec = time; + parser->now.tv_nsec = 0; - return parse_nullary_option(state); + return parse_nullary_option(parser); } /** * Parse -delete. */ -static struct bfs_expr *parse_delete(struct parser_state *state, int arg1, int arg2) { - state->ctx->flags |= BFTW_POST_ORDER; - state->depth_arg = state->argv; - return parse_nullary_action(state, eval_delete); +static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) { + 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 parser_state *state, int arg1, int arg2) { - state->ctx->flags |= BFTW_POST_ORDER; - state->depth_arg = state->argv; - return parse_nullary_flag(state); +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_expr = expr; + return expr; } /** * Parse -depth [N]. */ -static struct bfs_expr *parse_depth_n(struct parser_state *state, int arg1, int arg2) { - const char *arg = state->argv[1]; +static struct bfs_expr *parse_depth_n(struct bfs_parser *parser, int arg1, int arg2) { + const char *arg = parser->argv[1]; if (arg && looks_like_icmp(arg)) { - return parse_test_icmp(state, eval_depth); + return parse_test_icmp(parser, eval_depth); } else { - return parse_depth(state, arg1, arg2); + return parse_depth(parser, arg1, arg2); } } /** * Parse -{min,max}depth N. */ -static struct bfs_expr *parse_depth_limit(struct parser_state *state, int is_min, int arg2) { - struct bfs_ctx *ctx = state->ctx; - const char *arg = state->argv[0]; - const char *value = state->argv[1]; - if (!value) { - parse_error(state, "${blu}%s${rs} needs a value.\n", arg); +static struct bfs_expr *parse_depth_limit(struct bfs_parser *parser, int is_min, int arg2) { + struct bfs_expr *expr = parse_unary_option(parser); + if (!expr) { return NULL; } + struct bfs_ctx *ctx = parser->ctx; int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth; - if (!parse_int(state, &state->argv[1], value, depth, IF_INT | IF_UNSIGNED)) { + char **arg = &expr->argv[1]; + if (!parse_int(parser, arg, *arg, depth, IF_INT | IF_UNSIGNED)) { return NULL; } - return parse_unary_option(state); + return expr; } /** * Parse -empty. */ -static struct bfs_expr *parse_empty(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_empty); - if (!expr) { +static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(parser, eval_empty); + if (expr) { + // For opendir() + expr->ephemeral_fds = 1; + } + 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; } - expr->cost = 2000.0; - expr->probability = 0.01; + const char *exe = execbuf->tmpl_argv[0]; + if (strchr(exe, '/')) { + // No $PATH lookups for /foo or foo/bar + return NULL; + } - if (state->ctx->optlevel < 4) { - // Since -empty attempts to open and read directories, it may - // have side effects such as reporting permission errors, and - // thus shouldn't be re-ordered without aggressive optimizations - expr->pure = false; + if (strstr(exe, "{}")) { + // Substituted paths always contain a / + return NULL; } - expr->ephemeral_fds = 1; + const char *path = getenv("PATH"); + while (path) { + if (path[0] != '/') { + // Relative $PATH component! + return path; + } - return expr; + 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 parser_state *state, int flags, int arg2) { - struct bfs_exec *execbuf = bfs_exec_parse(state->ctx, state->argv, flags); +static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) { + struct bfs_ctx *ctx = parser->ctx; + + struct bfs_exec *execbuf = bfs_exec_parse(ctx, parser->argv, flags); if (!execbuf) { return NULL; } - struct bfs_expr *expr = parse_action(state, eval_exec, execbuf->tmpl_argc + 2); + struct bfs_expr *expr = parse_action(parser, eval_exec, execbuf->tmpl_argc + 2); if (!expr) { bfs_exec_free(execbuf); return NULL; @@ -1344,23 +1301,38 @@ static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int ar expr->exec = execbuf; - if (execbuf->flags & BFS_EXEC_MULTI) { - expr_set_always_true(expr); - } else { - expr->cost = 1000000.0; + // For pipe() in bfs_spawn() + expr->ephemeral_fds = 2; + + 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; } - expr->ephemeral_fds = 2; if (execbuf->flags & BFS_EXEC_CHDIR) { + // To dup() the parent directory if (execbuf->flags & BFS_EXEC_MULTI) { - expr->persistent_fds = 1; + ++expr->persistent_fds; } else { ++expr->ephemeral_fds; } } if (execbuf->flags & BFS_EXEC_CONFIRM) { - state->ok_expr = expr; + if (!consume_stdin(parser, expr)) { + return NULL; + } + } else { + ctx->dangerous = true; } return expr; @@ -1369,18 +1341,17 @@ static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int ar /** * Parse -exit [STATUS]. */ -static struct bfs_expr *parse_exit(struct parser_state *state, int arg1, int arg2) { +static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2) { size_t argc = 1; - const char *value = state->argv[1]; + const char *value = parser->argv[1]; int status = EXIT_SUCCESS; - if (value && parse_int(state, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) { + if (value && parse_int(parser, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) { argc = 2; } - struct bfs_expr *expr = parse_action(state, eval_exit, argc); + struct bfs_expr *expr = parse_action(parser, eval_exit, argc); if (expr) { - expr_set_never_returns(expr); expr->num = status; } return expr; @@ -1389,82 +1360,49 @@ static struct bfs_expr *parse_exit(struct parser_state *state, int arg1, int arg /** * Parse -f PATH. */ -static struct bfs_expr *parse_f(struct parser_state *state, int arg1, int arg2) { - const char *path = state->argv[1]; - if (!path) { - parse_error(state, "${cyn}-f${rs} requires a path.\n"); +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; } - if (parse_root(state, path) != 0) { + // 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; } - parser_advance(state, T_FLAG, 1); - parser_advance(state, T_PATH, 1); - return &bfs_true; + return expr; } /** * Parse -files0-from PATH. */ -static struct bfs_expr *parse_files0_from(struct parser_state *state, int arg1, int arg2) { - const char *arg = state->argv[0]; - const char *from = state->argv[1]; - if (!from) { - parse_error(state, "${blu}%s${rs} requires a path.\n", arg); - return NULL; - } - - state->files0_arg = parser_advance(state, T_OPTION, 1); - - FILE *file; - if (strcmp(from, "-") == 0) { - file = stdin; - } else { - file = xfopen(from, O_RDONLY | O_CLOEXEC); - } - if (!file) { - parse_error(state, "%m.\n"); +static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_option(parser); + if (!expr) { return NULL; } - struct bfs_expr *expr = &bfs_true; - - while (true) { - char *path = xgetdelim(file, '\0'); - if (!path) { - if (errno) { - parse_error(state, "%m.\n"); - expr = NULL; - } - break; - } - - int ret = parse_root(state, path); - free(path); - if (ret != 0) { - expr = NULL; - break; - } - } - - if (file == stdin) { - state->files0_stdin_arg = state->files0_arg; - } else { - fclose(file); - } - - state->implicit_root = false; - parser_advance(state, T_OPTION, 1); + // 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; } /** * Parse -flags FLAGS. */ -static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_flags); +static struct bfs_expr *parse_flags(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_flags); if (!expr) { return NULL; } @@ -1486,11 +1424,10 @@ static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int ar if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) { if (errno == ENOTSUP) { - parse_expr_error(state, expr, "Missing platform support.\n"); + parse_expr_error(parser, expr, "Missing platform support.\n"); } else { - parse_expr_error(state, expr, "Invalid flags.\n"); + parse_expr_error(parser, expr, "Invalid flags.\n"); } - bfs_expr_free(expr); return NULL; } @@ -1500,371 +1437,298 @@ static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int ar /** * Parse -fls FILE. */ -static struct bfs_expr *parse_fls(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_action(state, eval_fls); +static struct bfs_expr *parse_fls(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(parser, eval_fls); if (!expr) { - goto fail; + return NULL; } - if (expr_open(state, expr, expr->argv[1]) != 0) { - goto fail; + if (expr_open(parser, expr, expr->argv[1]) != 0) { + return NULL; } - expr_set_always_true(expr); - expr->cost = PRINT_COST; - expr->reftime = state->now; - - // We'll need these for user/group names, so initialize them now to - // avoid EMFILE later - bfs_ctx_users(state->ctx); - bfs_ctx_groups(state->ctx); - return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -fprint FILE. */ -static struct bfs_expr *parse_fprint(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_action(state, eval_fprint); - if (expr) { - expr_set_always_true(expr); - expr->cost = PRINT_COST; - if (expr_open(state, expr, expr->argv[1]) != 0) { - goto fail; - } +static struct bfs_expr *parse_fprint(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(parser, eval_fprint); + if (!expr) { + return NULL; } - return expr; -fail: - bfs_expr_free(expr); - return NULL; + if (expr_open(parser, expr, expr->argv[1]) != 0) { + return NULL; + } + + return expr; } /** * Parse -fprint0 FILE. */ -static struct bfs_expr *parse_fprint0(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_action(state, eval_fprint0); - if (expr) { - expr_set_always_true(expr); - expr->cost = PRINT_COST; - if (expr_open(state, expr, expr->argv[1]) != 0) { - goto fail; - } +static struct bfs_expr *parse_fprint0(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(parser, eval_fprint0); + if (!expr) { + return NULL; } - return expr; -fail: - bfs_expr_free(expr); - return NULL; + if (expr_open(parser, expr, expr->argv[1]) != 0) { + return NULL; + } + + return expr; } /** * Parse -fprintf FILE FORMAT. */ -static struct bfs_expr *parse_fprintf(struct parser_state *state, int arg1, int arg2) { - const char *arg = state->argv[0]; +static struct bfs_expr *parse_fprintf(struct bfs_parser *parser, int arg1, int arg2) { + const char *arg = parser->argv[0]; - const char *file = state->argv[1]; + const char *file = parser->argv[1]; if (!file) { - parse_error(state, "${blu}%s${rs} needs a file.\n", arg); + parse_error(parser, "${blu}%s${rs} needs a file.\n", arg); return NULL; } - const char *format = state->argv[2]; + const char *format = parser->argv[2]; if (!format) { - parse_error(state, "${blu}%s${rs} needs a format string.\n", arg); + parse_error(parser, "${blu}%s${rs} needs a format string.\n", arg); return NULL; } - struct bfs_expr *expr = parse_action(state, eval_fprintf, 3); + struct bfs_expr *expr = parse_action(parser, eval_fprintf, 3); if (!expr) { return NULL; } - expr_set_always_true(expr); - - expr->cost = PRINT_COST; - - if (expr_open(state, expr, file) != 0) { - goto fail; + if (expr_open(parser, expr, file) != 0) { + return NULL; } - if (bfs_printf_parse(state->ctx, expr, format) != 0) { - goto fail; + if (bfs_printf_parse(parser->ctx, expr, format) != 0) { + return NULL; } return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -fstype TYPE. */ -static struct bfs_expr *parse_fstype(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_fstype); +static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_fstype); if (!expr) { return NULL; } - if (!bfs_ctx_mtab(state->ctx)) { - parse_expr_error(state, expr, "Couldn't parse the mount table: %m.\n"); - bfs_expr_free(expr); + if (!bfs_ctx_mtab(parser->ctx)) { + parse_expr_error(parser, expr, "Couldn't parse the mount table: %s.\n", errstr()); return NULL; } - expr->cost = STAT_COST; return expr; } /** * Parse -gid/-group. */ -static struct bfs_expr *parse_group(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_gid); +static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_gid); if (!expr) { return NULL; } - const struct bfs_groups *groups = bfs_ctx_groups(state->ctx); - if (!groups) { - parse_expr_error(state, expr, "Couldn't parse the group table: %m.\n"); - goto fail; - } - - const struct group *grp = bfs_getgrnam(groups, expr->argv[1]); + const struct group *grp = bfs_getgrnam(parser->ctx->groups, expr->argv[1]); if (grp) { expr->num = grp->gr_gid; expr->int_cmp = BFS_INT_EQUAL; } else if (looks_like_icmp(expr->argv[1])) { - if (!parse_icmp(state, expr, 0)) { - goto fail; + if (!parse_icmp(parser, expr, 0)) { + return NULL; } + } else if (errno) { + parse_expr_error(parser, expr, "%s.\n", errstr()); + return NULL; } else { - parse_expr_error(state, expr, "No such group.\n"); - goto fail; + parse_expr_error(parser, expr, "No such group.\n"); + return NULL; } - expr->cost = STAT_COST; - return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -unique. */ -static struct bfs_expr *parse_unique(struct parser_state *state, int arg1, int arg2) { - state->ctx->unique = true; - return parse_nullary_option(state); +static struct bfs_expr *parse_unique(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->unique = true; + return parse_nullary_option(parser); } /** * Parse -used N. */ -static struct bfs_expr *parse_used(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_test_icmp(state, eval_used); - if (expr) { - expr->cost = STAT_COST; - } - return expr; +static struct bfs_expr *parse_used(struct bfs_parser *parser, int arg1, int arg2) { + return parse_test_icmp(parser, eval_used); } /** * Parse -uid/-user. */ -static struct bfs_expr *parse_user(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_uid); +static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_uid); if (!expr) { return NULL; } - const struct bfs_users *users = bfs_ctx_users(state->ctx); - if (!users) { - parse_expr_error(state, expr, "Couldn't parse the user table: %m.\n"); - goto fail; - } - - const struct passwd *pwd = bfs_getpwnam(users, expr->argv[1]); + const struct passwd *pwd = bfs_getpwnam(parser->ctx->users, expr->argv[1]); if (pwd) { expr->num = pwd->pw_uid; expr->int_cmp = BFS_INT_EQUAL; } else if (looks_like_icmp(expr->argv[1])) { - if (!parse_icmp(state, expr, 0)) { - goto fail; + if (!parse_icmp(parser, expr, 0)) { + return NULL; } + } else if (errno) { + parse_expr_error(parser, expr, "%s.\n", errstr()); + return NULL; } else { - parse_expr_error(state, expr, "No such user.\n"); - goto fail; + parse_expr_error(parser, expr, "No such user.\n"); + return NULL; } - expr->cost = STAT_COST; - return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -hidden. */ -static struct bfs_expr *parse_hidden(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_hidden); - if (expr) { - expr->probability = 0.01; - } - return expr; +static struct bfs_expr *parse_hidden(struct bfs_parser *parser, int arg1, int arg2) { + return parse_nullary_test(parser, eval_hidden); } /** * Parse -(no)?ignore_readdir_race. */ -static struct bfs_expr *parse_ignore_races(struct parser_state *state, int ignore, int arg2) { - state->ctx->ignore_races = ignore; - return parse_nullary_option(state); +static struct bfs_expr *parse_ignore_races(struct bfs_parser *parser, int ignore, int arg2) { + parser->ctx->ignore_races = ignore; + return parse_nullary_option(parser); } /** * Parse -inum N. */ -static struct bfs_expr *parse_inum(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_test_icmp(state, eval_inum); - if (expr) { - expr->cost = STAT_COST; - expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50; - } - return expr; +static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2) { + return parse_test_icmp(parser, eval_inum); } /** - * Parse -links N. + * Parse -j<n>. */ -static struct bfs_expr *parse_links(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_test_icmp(state, eval_links); - if (expr) { - expr->cost = STAT_COST; - expr->probability = bfs_expr_cmp(expr, 1) ? 0.99 : 0.01; +static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) { + 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, arg, &n, IF_INT | IF_UNSIGNED)) { + return NULL; + } + + if (n == 0) { + parse_expr_error(parser, expr, "${bld}0${rs} is not enough threads.\n"); + return NULL; } + + parser->ctx->threads = n; return expr; } /** - * Parse -ls. + * Parse -limit N. */ -static struct bfs_expr *parse_ls(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_action(state, eval_fls); +static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(parser, eval_limit); if (!expr) { return NULL; } - init_print_expr(state, expr); - expr->reftime = state->now; + char **arg = &expr->argv[1]; + if (!parse_int(parser, arg, *arg, &expr->num, IF_LONG_LONG)) { + return NULL; + } - // We'll need these for user/group names, so initialize them now to - // avoid EMFILE later - bfs_ctx_users(state->ctx); - bfs_ctx_groups(state->ctx); + if (expr->num <= 0) { + parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr); + return NULL; + } + parser->limit_expr = expr; return expr; } /** - * Parse -mount. + * Parse -links N. */ -static struct bfs_expr *parse_mount(struct parser_state *state, int arg1, int arg2) { - parse_warning(state, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", state->argv[0]); - bfs_warning(state->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n"); - - state->ctx->flags |= BFTW_PRUNE_MOUNTS; - state->mount_arg = state->argv; - return parse_nullary_option(state); +static struct bfs_expr *parse_links(struct bfs_parser *parser, int arg1, int arg2) { + return parse_test_icmp(parser, eval_links); } /** - * Common code for fnmatch() tests. + * Parse -ls. */ -static struct bfs_expr *parse_fnmatch(const struct parser_state *state, struct bfs_expr *expr, bool casefold) { +static struct bfs_expr *parse_ls(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_fls); if (!expr) { return NULL; } - if (casefold) { -#ifdef FNM_CASEFOLD - expr->num = FNM_CASEFOLD; -#else - parse_expr_error(state, expr, "Missing platform support.\n"); - bfs_expr_free(expr); - return NULL; -#endif - } else { - expr->num = 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. - const char *pattern = expr->argv[1]; - size_t i, len = strlen(pattern); - for (i = 0; i < len; ++i) { - if (pattern[len - i - 1] != '\\') { - break; - } - } - if (i % 2 != 0) { - parse_expr_warning(state, expr, "Unescaped trailing backslash.\n\n"); - bfs_expr_free(expr); - return &bfs_false; - } - - expr->cost = 400.0; + init_print_expr(parser, expr); + return expr; +} - if (strchr(pattern, '*')) { - expr->probability = 0.5; - } else { - expr->probability = 0.1; +/** + * Parse -mount. + */ +static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; } + parser->ctx->flags |= BFTW_SKIP_MOUNTS; + parser->mount_expr = expr; return expr; } /** * Parse -i?name. */ -static struct bfs_expr *parse_name(struct parser_state *state, int casefold, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_name); - return parse_fnmatch(state, expr, casefold); +static struct bfs_expr *parse_name(struct bfs_parser *parser, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_name); + return parse_fnmatch(parser, expr, casefold); } /** * Parse -i?path, -i?wholename. */ -static struct bfs_expr *parse_path(struct parser_state *state, int casefold, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_path); - return parse_fnmatch(state, expr, casefold); +static struct bfs_expr *parse_path(struct bfs_parser *parser, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_path); + return parse_fnmatch(parser, expr, casefold); } /** * Parse -i?lname. */ -static struct bfs_expr *parse_lname(struct parser_state *state, int casefold, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_lname); - return parse_fnmatch(state, expr, casefold); +static struct bfs_expr *parse_lname(struct bfs_parser *parser, int casefold, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_lname); + return parse_fnmatch(parser, expr, casefold); } /** Get the bfs_stat_field for X/Y in -newerXY. */ @@ -1884,20 +1748,20 @@ static enum bfs_stat_field parse_newerxy_field(char c) { } /** Parse an explicit reference timestamp for -newerXt and -*since. */ -static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr) { - if (parse_timestamp(expr->argv[1], &expr->reftime) == 0) { +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(state, expr, "%m.\n"); + parse_expr_error(parser, expr, "%s.\n", errstr()); return -1; } - parse_expr_error(state, expr, "Invalid timestamp.\n\n"); + parse_expr_error(parser, expr, "Invalid timestamp.\n\n"); fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n"); struct tm tm; - if (xlocaltime(&state->now.tv_sec, &tm) != 0) { - parse_perror(state, "xlocaltime()"); + if (!localtime_r(&parser->now.tv_sec, &tm)) { + parse_perror(parser, "localtime_r()"); return -1; } @@ -1906,18 +1770,18 @@ static int parse_reftime(const struct parser_state *state, 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; #endif - int tz_hour = gmtoff/3600; - int tz_min = (labs(gmtoff)/60)%60; + int tz_hour = gmtoff / 3600; + int tz_min = (labs(gmtoff) / 60) % 60; fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n", - year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min); + year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min); - if (xgmtime(&state->now.tv_sec, &tm) != 0) { - parse_perror(state, "xgmtime()"); + if (!gmtime_r(&parser->now.tv_sec, &tm)) { + parse_perror(parser, "gmtime_r()"); return -1; } @@ -1931,76 +1795,72 @@ static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr /** * Parse -newerXY. */ -static struct bfs_expr *parse_newerxy(struct parser_state *state, int arg1, int arg2) { - const char *arg = state->argv[0]; +static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int arg2) { + const char *arg = parser->argv[0]; if (strlen(arg) != 8) { - parse_error(state, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%s${rs}.\n", arg + 6); + parse_error(parser, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%pq${rs}.\n", arg + 6); return NULL; } - struct bfs_expr *expr = parse_unary_test(state, eval_newer); + struct bfs_expr *expr = parse_unary_test(parser, eval_newer); if (!expr) { - goto fail; + return NULL; } expr->stat_field = parse_newerxy_field(arg[6]); if (!expr->stat_field) { - parse_expr_error(state, expr, - "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n", - arg[6]); - goto fail; + parse_expr_error(parser, expr, + "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n", + arg[6]); + return NULL; } if (arg[7] == 't') { - if (parse_reftime(state, expr) != 0) { - goto fail; + if (parse_reftime(parser, expr) != 0) { + return NULL; } } else { enum bfs_stat_field field = parse_newerxy_field(arg[7]); if (!field) { - parse_expr_error(state, expr, - "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n", - arg[7]); - goto fail; + parse_expr_error(parser, expr, + "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n", + arg[7]); + return NULL; } struct bfs_stat sb; - if (stat_arg(state, &expr->argv[1], &sb) != 0) { - goto fail; + if (stat_arg(parser, &expr->argv[1], &sb) != 0) { + return NULL; } - const struct timespec *reftime = bfs_stat_time(&sb, field); if (!reftime) { - parse_expr_error(state, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field)); - goto fail; + parse_expr_error(parser, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field)); + return NULL; } expr->reftime = *reftime; } - expr->cost = STAT_COST; - return expr; +} -fail: - bfs_expr_free(expr); - return NULL; +/** + * 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 parser_state *state, int arg1, int arg2) { - if (!bfs_ctx_groups(state->ctx)) { - parse_error(state, "Couldn't parse the group table: %m.\n"); - return NULL; - } - - struct bfs_expr *expr = parse_nullary_test(state, eval_nogroup); +static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(parser, eval_nogroup); if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.01; + // Who knows how many FDs getgrgid_r() needs? + expr->ephemeral_fds = 3; } return expr; } @@ -2008,45 +1868,39 @@ static struct bfs_expr *parse_nogroup(struct parser_state *state, int arg1, int /** * Parse -nohidden. */ -static struct bfs_expr *parse_nohidden(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *hidden = bfs_expr_new(eval_hidden, 1, &fake_hidden_arg); +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, BFS_TEST); if (!hidden) { return NULL; } - hidden->probability = 0.01; - hidden->pure = true; - hidden->synthetic = true; - - if (parse_exclude(state, hidden) != 0) { - return NULL; - } - - parser_advance(state, T_OPTION, 1); - return &bfs_true; + bfs_expr_append(parser->ctx->exclude, hidden); + return parse_nullary_option(parser); } /** * Parse -noleaf. */ -static struct bfs_expr *parse_noleaf(struct parser_state *state, int arg1, int arg2) { - parse_warning(state, "${ex}bfs${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", state->argv[0]); - return parse_nullary_option(state); +static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) { + 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; } /** * Parse -nouser. */ -static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int arg2) { - if (!bfs_ctx_users(state->ctx)) { - parse_error(state, "Couldn't parse the user table: %m.\n"); - return NULL; - } - - struct bfs_expr *expr = parse_nullary_test(state, eval_nouser); +static struct bfs_expr *parse_nouser(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_test(parser, eval_nouser); if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.01; + // Who knows how many FDs getpwuid_r() needs? + expr->ephemeral_fds = 3; } return expr; } @@ -2054,10 +1908,10 @@ static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int a /** * Parse a permission mode like chmod(1). */ -static int parse_mode(const struct parser_state *state, const char *mode, struct bfs_expr *expr) { +static int parse_mode(const struct bfs_parser *parser, const char *mode, struct bfs_expr *expr) { if (mode[0] >= '0' && mode[0] <= '9') { unsigned int parsed; - if (!parse_int(state, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) { + if (!parse_int(parser, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) { goto fail; } if (parsed > 07777) { @@ -2069,6 +1923,8 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct return 0; } + mode_t umask = parser->ctx->umask; + expr->file_mode = 0; expr->dir_mode = 0; @@ -2097,25 +1953,27 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct MODE_ACTION_APPLY, MODE_OP, MODE_PERM, - } mstate = MODE_CLAUSE; + } state = MODE_CLAUSE; enum { MODE_PLUS, MODE_MINUS, MODE_EQUALS, - } op; + } op uninit(MODE_EQUALS); - mode_t who; - mode_t file_change; - mode_t dir_change; + 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 (mstate) { + switch (state) { case MODE_CLAUSE: who = 0; - mstate = MODE_WHO; - BFS_FALLTHROUGH; + mask = 0777; + state = MODE_WHO; + _fallthrough; case MODE_WHO: switch (*i) { @@ -2132,7 +1990,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct who |= 0777; break; default: - mstate = MODE_ACTION; + state = MODE_ACTION; continue; } break; @@ -2142,7 +2000,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct case MODE_EQUALS: expr->file_mode &= ~who; expr->dir_mode &= ~who; - BFS_FALLTHROUGH; + _fallthrough; case MODE_PLUS: expr->file_mode |= file_change; expr->dir_mode |= dir_change; @@ -2152,37 +2010,40 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct expr->dir_mode &= ~dir_change; break; } - BFS_FALLTHROUGH; + _fallthrough; case MODE_ACTION: if (who == 0) { who = 0777; + mask = who & ~umask; + } else { + mask = who; } switch (*i) { case '+': op = MODE_PLUS; - mstate = MODE_OP; + state = MODE_OP; break; case '-': op = MODE_MINUS; - mstate = MODE_OP; + state = MODE_OP; break; case '=': op = MODE_EQUALS; - mstate = MODE_OP; + state = MODE_OP; break; case ',': - if (mstate == MODE_ACTION_APPLY) { - mstate = MODE_CLAUSE; + if (state == MODE_ACTION_APPLY) { + state = MODE_CLAUSE; } else { goto fail; } break; case '\0': - if (mstate == MODE_ACTION_APPLY) { + if (state == MODE_ACTION_APPLY) { goto done; } else { goto fail; @@ -2211,32 +2072,32 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct default: file_change = 0; dir_change = 0; - mstate = 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; - mstate = 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; - BFS_FALLTHROUGH; + file_change |= mask & 0111; + _fallthrough; case 'X': - dir_change |= who & 0111; + dir_change |= mask & 0111; break; case 's': if (who & 0700) { @@ -2255,7 +2116,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct } break; default: - mstate = MODE_ACTION_APPLY; + state = MODE_ACTION_APPLY; continue; } break; @@ -2268,15 +2129,15 @@ done: return 0; fail: - parse_expr_error(state, expr, "Invalid mode.\n"); + parse_expr_error(parser, expr, "Invalid mode.\n"); return -1; } /** * Parse -perm MODE. */ -static struct bfs_expr *parse_perm(struct parser_state *state, int field, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_perm); +static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_perm); if (!expr) { return NULL; } @@ -2297,32 +2158,26 @@ static struct bfs_expr *parse_perm(struct parser_state *state, int field, int ar ++mode; break; } - BFS_FALLTHROUGH; + _fallthrough; default: expr->mode_cmp = BFS_MODE_EQUAL; break; } - if (parse_mode(state, mode, expr) != 0) { - goto fail; + if (parse_mode(parser, mode, expr) != 0) { + return NULL; } - expr->cost = STAT_COST; - return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -print. */ -static struct bfs_expr *parse_print(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_action(state, eval_fprint); +static struct bfs_expr *parse_print(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint); if (expr) { - init_print_expr(state, expr); + init_print_expr(parser, expr); } return expr; } @@ -2330,10 +2185,10 @@ static struct bfs_expr *parse_print(struct parser_state *state, int arg1, int ar /** * Parse -print0. */ -static struct bfs_expr *parse_print0(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_action(state, eval_fprint0); +static struct bfs_expr *parse_print0(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint0); if (expr) { - init_print_expr(state, expr); + init_print_expr(parser, expr); } return expr; } @@ -2341,16 +2196,15 @@ static struct bfs_expr *parse_print0(struct parser_state *state, int arg1, int a /** * Parse -printf FORMAT. */ -static struct bfs_expr *parse_printf(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_action(state, eval_fprintf); +static struct bfs_expr *parse_printf(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_action(parser, eval_fprintf); if (!expr) { return NULL; } - init_print_expr(state, expr); + init_print_expr(parser, expr); - if (bfs_printf_parse(state->ctx, expr, expr->argv[1]) != 0) { - bfs_expr_free(expr); + if (bfs_printf_parse(parser->ctx, expr, expr->argv[1]) != 0) { return NULL; } @@ -2360,10 +2214,10 @@ static struct bfs_expr *parse_printf(struct parser_state *state, int arg1, int a /** * Parse -printx. */ -static struct bfs_expr *parse_printx(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_action(state, eval_fprintx); +static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_fprintx); if (expr) { - init_print_expr(state, expr); + init_print_expr(parser, expr); } return expr; } @@ -2371,170 +2225,171 @@ static struct bfs_expr *parse_printx(struct parser_state *state, int arg1, int a /** * Parse -prune. */ -static struct bfs_expr *parse_prune(struct parser_state *state, int arg1, int arg2) { - state->prune_arg = state->argv; - - struct bfs_expr *expr = parse_nullary_action(state, eval_prune); - if (expr) { - expr_set_always_true(expr); +static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_action(parser, eval_prune); + if (!expr) { + return NULL; } + + parser->prune_expr = expr; return expr; } /** * Parse -quit. */ -static struct bfs_expr *parse_quit(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_action(state, eval_quit); - if (expr) { - expr_set_never_returns(expr); - } - return expr; +static struct bfs_expr *parse_quit(struct bfs_parser *parser, int arg1, int arg2) { + return parse_nullary_action(parser, eval_quit); } /** * Parse -i?regex. */ -static struct bfs_expr *parse_regex(struct parser_state *state, int flags, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_regex); +static struct bfs_expr *parse_regex(struct bfs_parser *parser, int flags, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_regex); if (!expr) { - goto fail; + return NULL; } - if (bfs_regcomp(&expr->regex, expr->argv[1], state->regex_type, flags) != 0) { - if (!expr->regex) { - parse_perror(state, "bfs_regcomp()"); - goto fail; - } - - char *str = bfs_regerror(expr->regex); - if (!str) { - parse_perror(state, "bfs_regerror()"); - goto fail; + if (bfs_regcomp(&expr->regex, expr->argv[1], parser->regex_type, flags) != 0) { + if (expr->regex) { + char *str = bfs_regerror(expr->regex); + if (str) { + parse_expr_error(parser, expr, "%s.\n", str); + free(str); + } else { + parse_perror(parser, "bfs_regerror()"); + } + } else { + parse_perror(parser, "bfs_regcomp()"); } - parse_expr_error(state, expr, "%s.\n", str); - free(str); - goto fail; + return NULL; } return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -E. */ -static struct bfs_expr *parse_regex_extended(struct parser_state *state, int arg1, int arg2) { - state->regex_type = BFS_REGEX_POSIX_EXTENDED; - return parse_nullary_flag(state); +static struct bfs_expr *parse_regex_extended(struct bfs_parser *parser, int arg1, int arg2) { + parser->regex_type = BFS_REGEX_POSIX_EXTENDED; + return parse_nullary_flag(parser); } /** * Parse -regextype TYPE. */ -static struct bfs_expr *parse_regextype(struct parser_state *state, int arg1, int arg2) { - struct bfs_ctx *ctx = state->ctx; +static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; CFILE *cfile = ctx->cerr; - const char *arg = state->argv[0]; - const char *type = state->argv[1]; - if (!type) { - parse_error(state, "${blu}%s${rs} needs a value.\n\n", arg); + struct bfs_expr *expr = parse_unary_option(parser); + if (!expr) { + cfprintf(cfile, "\n"); goto list_types; } - parser_advance(state, T_OPTION, 1); - // 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) { - state->regex_type = BFS_REGEX_POSIX_BASIC; + parser->regex_type = BFS_REGEX_POSIX_BASIC; } else if (strcmp(type, "posix-extended") == 0) { - state->regex_type = BFS_REGEX_POSIX_EXTENDED; + parser->regex_type = BFS_REGEX_POSIX_EXTENDED; #if BFS_WITH_ONIGURUMA + } else if (strcmp(type, "awk") == 0 + || strcmp(type, "posix-awk") == 0) { + parser->regex_type = BFS_REGEX_AWK; + } else if (strcmp(type, "gnu-awk") == 0) { + parser->regex_type = BFS_REGEX_GNU_AWK; } else if (strcmp(type, "emacs") == 0) { - state->regex_type = BFS_REGEX_EMACS; + parser->regex_type = BFS_REGEX_EMACS; } else if (strcmp(type, "grep") == 0) { - state->regex_type = BFS_REGEX_GREP; + 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) { - state->just_info = true; + parser->just_info = true; cfile = ctx->cout; goto list_types; } else { - parse_error(state, "Unsupported regex type.\n\n"); + parse_expr_error(parser, expr, "Unsupported regex type.\n\n"); goto list_types; } - parser_advance(state, T_OPTION, 1); - return &bfs_true; + return expr; list_types: cfprintf(cfile, "Supported types are:\n\n"); - cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); - cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n"); - cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); + cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); + cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); + cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n"); + + cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n"); + #if BFS_WITH_ONIGURUMA - cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n"); - cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n"); + cfprintf(cfile, " ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n"); + + cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n"); + + cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n"); + + cfprintf(cfile, " ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n"); #endif - cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n"); return NULL; } /** * Parse -s. */ -static struct bfs_expr *parse_s(struct parser_state *state, int arg1, int arg2) { - state->ctx->flags |= BFTW_SORT; - return parse_nullary_flag(state); +static struct bfs_expr *parse_s(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->flags |= BFTW_SORT; + return parse_nullary_flag(parser); } /** * Parse -samefile FILE. */ -static struct bfs_expr *parse_samefile(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_samefile); +static struct bfs_expr *parse_samefile(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_samefile); if (!expr) { return NULL; } struct bfs_stat sb; - if (stat_arg(state, &expr->argv[1], &sb) != 0) { - bfs_expr_free(expr); + if (stat_arg(parser, &expr->argv[1], &sb) != 0) { return NULL; } expr->dev = sb.dev; expr->ino = sb.ino; - - expr->cost = STAT_COST; - expr->probability = 0.01; - return expr; } /** * Parse -S STRATEGY. */ -static struct bfs_expr *parse_search_strategy(struct parser_state *state, int arg1, int arg2) { - struct bfs_ctx *ctx = state->ctx; +static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; CFILE *cfile = ctx->cerr; - const char *flag = state->argv[0]; - const char *arg = state->argv[1]; - if (!arg) { - parse_error(state, "${cyn}%s${rs} needs an argument.\n\n", flag); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg); + if (!expr) { + cfprintf(cfile, "\n"); goto list_strategies; } - parser_advance(state, T_FLAG, 1); - if (strcmp(arg, "bfs") == 0) { ctx->strategy = BFTW_BFS; } else if (strcmp(arg, "dfs") == 0) { @@ -2544,16 +2399,15 @@ static struct bfs_expr *parse_search_strategy(struct parser_state *state, int ar } else if (strcmp(arg, "eds") == 0) { ctx->strategy = BFTW_EDS; } else if (strcmp(arg, "help") == 0) { - state->just_info = true; + parser->just_info = true; cfile = ctx->cout; goto list_strategies; } else { - parse_error(state, "Unrecognized search strategy.\n\n"); + parse_expr_error(parser, expr, "Unrecognized search strategy.\n\n"); goto list_strategies; } - parser_advance(state, T_FLAG, 1); - return &bfs_true; + return expr; list_strategies: cfprintf(cfile, "Supported search strategies:\n\n"); @@ -2567,37 +2421,32 @@ list_strategies: /** * Parse -[aBcm]?since. */ -static struct bfs_expr *parse_since(struct parser_state *state, int field, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_newer); +static struct bfs_expr *parse_since(struct bfs_parser *parser, int field, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_newer); if (!expr) { return NULL; } - if (parse_reftime(state, expr) != 0) { - goto fail; + if (parse_reftime(parser, expr) != 0) { + return NULL; } - expr->cost = STAT_COST; expr->stat_field = field; return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -size N[cwbkMGTP]?. */ -static struct bfs_expr *parse_size(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_unary_test(state, eval_size); +static struct bfs_expr *parse_size(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_unary_test(parser, eval_size); if (!expr) { return NULL; } - const char *unit = parse_icmp(state, expr, IF_PARTIAL_OK); + const char *unit = parse_icmp(parser, expr, IF_PARTIAL_OK); if (!unit) { - goto fail; + return NULL; } if (strlen(unit) > 1) { @@ -2632,109 +2481,82 @@ static struct bfs_expr *parse_size(struct parser_state *state, int arg1, int arg break; default: - goto bad_unit; + bad_unit: + parse_expr_error(parser, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%pq${rs}.\n", unit); + return NULL; } - expr->cost = STAT_COST; - expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50; - return expr; - -bad_unit: - parse_expr_error(state, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%s${rs}.\n", unit); -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -sparse. */ -static struct bfs_expr *parse_sparse(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_sparse); - if (expr) { - expr->cost = STAT_COST; - } - return expr; +static struct bfs_expr *parse_sparse(struct bfs_parser *parser, int arg1, int arg2) { + return parse_nullary_test(parser, eval_sparse); } /** * Parse -status. */ -static struct bfs_expr *parse_status(struct parser_state *state, int arg1, int arg2) { - state->ctx->status = true; - return parse_nullary_option(state); +static struct bfs_expr *parse_status(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->status = true; + return parse_nullary_option(parser); } /** * Parse -x?type [bcdpflsD]. */ -static struct bfs_expr *parse_type(struct parser_state *state, int x, int arg2) { +static struct bfs_expr *parse_type(struct bfs_parser *parser, int x, int arg2) { + struct bfs_ctx *ctx = parser->ctx; + bfs_eval_fn *eval = x ? eval_xtype : eval_type; - struct bfs_expr *expr = parse_unary_test(state, eval); + struct bfs_expr *expr = parse_unary_test(parser, eval); if (!expr) { return NULL; } - unsigned int types = 0; - float probability = 0.0; + expr->num = 0; const char *c = expr->argv[1]; while (true) { - enum bfs_type type; - float type_prob; - switch (*c) { case 'b': - type = BFS_BLK; - type_prob = 0.00000721183; + expr->num |= 1 << BFS_BLK; break; case 'c': - type = BFS_CHR; - type_prob = 0.0000499855; + expr->num |= 1 << BFS_CHR; break; case 'd': - type = BFS_DIR; - type_prob = 0.114475; + expr->num |= 1 << BFS_DIR; break; case 'D': - type = BFS_DOOR; - type_prob = 0.000001; + expr->num |= 1 << BFS_DOOR; break; case 'p': - type = BFS_FIFO; - type_prob = 0.00000248684; + expr->num |= 1 << BFS_FIFO; break; case 'f': - type = BFS_REG; - type_prob = 0.859772; + expr->num |= 1 << BFS_REG; break; case 'l': - type = BFS_LNK; - type_prob = 0.0256816; + expr->num |= 1 << BFS_LNK; break; case 's': - type = BFS_SOCK; - type_prob = 0.0000116881; + expr->num |= 1 << BFS_SOCK; break; case 'w': - type = BFS_WHT; - type_prob = 0.000001; + expr->num |= 1 << BFS_WHT; + ctx->flags |= BFTW_WHITEOUTS; break; case '\0': - parse_expr_error(state, expr, "Expected a type flag.\n"); - goto fail; + parse_expr_error(parser, expr, "Expected a type flag.\n"); + return NULL; default: - parse_expr_error(state, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c); - goto fail; - } - - unsigned int flag = 1 << type; - if (!(types & flag)) { - types |= flag; - probability += type_prob; + parse_expr_error(parser, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c); + return NULL; } ++c; @@ -2744,49 +2566,30 @@ static struct bfs_expr *parse_type(struct parser_state *state, int x, int arg2) ++c; continue; } else { - parse_expr_error(state, expr, "Types must be comma-separated.\n"); - goto fail; + parse_expr_error(parser, expr, "Types must be comma-separated.\n"); + return NULL; } } - expr->num = types; - expr->probability = probability; - - if (x && state->ctx->optlevel < 4) { - // Since -xtype dereferences symbolic links, it may have side - // effects such as reporting permission errors, and thus - // shouldn't be re-ordered without aggressive optimizations - expr->pure = false; - } - return expr; - -fail: - bfs_expr_free(expr); - return NULL; } /** * Parse -(no)?warn. */ -static struct bfs_expr *parse_warn(struct parser_state *state, int warn, int arg2) { - state->ctx->warn = warn; - return parse_nullary_option(state); +static struct bfs_expr *parse_warn(struct bfs_parser *parser, int warn, int arg2) { + parser->ctx->warn = warn; + return parse_nullary_option(parser); } /** * Parse -xattr. */ -static struct bfs_expr *parse_xattr(struct parser_state *state, int arg1, int arg2) { +static struct bfs_expr *parse_xattr(struct bfs_parser *parser, int arg1, int arg2) { #if BFS_CAN_CHECK_XATTRS - struct bfs_expr *expr = parse_nullary_test(state, eval_xattr); - if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.01; - } - return expr; + return parse_nullary_test(parser, eval_xattr); #else - parse_error(state, "Missing platform support.\n"); + parse_error(parser, "Missing platform support.\n"); return NULL; #endif } @@ -2794,16 +2597,11 @@ static struct bfs_expr *parse_xattr(struct parser_state *state, int arg1, int ar /** * Parse -xattrname. */ -static struct bfs_expr *parse_xattrname(struct parser_state *state, int arg1, int arg2) { +static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int arg2) { #if BFS_CAN_CHECK_XATTRS - struct bfs_expr *expr = parse_unary_test(state, eval_xattrname); - if (expr) { - expr->cost = STAT_COST; - expr->probability = 0.01; - } - return expr; + return parse_unary_test(parser, eval_xattrname); #else - parse_error(state, "Missing platform support.\n"); + parse_error(parser, "Missing platform support.\n"); return NULL; #endif } @@ -2811,10 +2609,15 @@ static struct bfs_expr *parse_xattrname(struct parser_state *state, int arg1, in /** * Parse -xdev. */ -static struct bfs_expr *parse_xdev(struct parser_state *state, int arg1, int arg2) { - state->ctx->flags |= BFTW_PRUNE_MOUNTS; - state->xdev_arg = state->argv; - return parse_nullary_option(state); +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_expr = expr; + return expr; } /** @@ -2874,7 +2677,8 @@ static CFILE *launch_pager(pid_t *pid, CFILE *cout) { NULL, }; - if (strcmp(xbasename(exe), "less") == 0) { + const char *cmd = exe + xbaseoff(exe); + if (strcmp(cmd, "less") == 0) { // We know less supports colors, other pagers may not ret->colors = cout->colors; argv[1] = "-FKRX"; @@ -2914,20 +2718,21 @@ fail: /** * "Parse" -help. */ -static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg2) { - CFILE *cout = state->ctx->cout; +static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2) { + CFILE *cout = parser->ctx->cout; pid_t pager = -1; - if (state->stdout_tty) { + if (parser->stdout_tty) { cout = launch_pager(&pager, cout); } cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n", - state->command); + parser->command); - cfprintf(cout, "${ex}bfs${rs} is compatible with ${ex}find${rs}, with some extensions. " - "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n" - "and ${blu}expressions${rs} may be freely mixed in any order.\n\n"); + cfprintf(cout, "${ex}%s${rs} is compatible with ${ex}find${rs}, with some extensions. " + "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n" + "and ${blu}expressions${rs} may be freely mixed in any order.\n\n", + BFS_COMMAND); cfprintf(cout, "${bld}Flags:${rs}\n\n"); @@ -2957,7 +2762,9 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n"); cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n"); cfprintf(cout, " Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n"); - cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n\n"); + cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n"); + cfprintf(cout, " ${cyn}-j${bld}N${rs}\n"); + cfprintf(cout, " Search with ${bld}N${rs} threads in parallel (default: number of CPUs, up to ${bld}8${rs})\n\n"); cfprintf(cout, "${bld}Operators:${rs}\n\n"); @@ -2996,14 +2803,16 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg cfprintf(cout, " Follow all symbolic links (same as ${cyn}-L${rs})\n"); cfprintf(cout, " ${blu}-ignore_readdir_race${rs}\n"); cfprintf(cout, " ${blu}-noignore_readdir_race${rs}\n"); - cfprintf(cout, " Whether to report an error if ${ex}bfs${rs} detects that the file tree is modified\n"); + cfprintf(cout, " Whether to report an error if ${ex}%s${rs} detects that the file tree is modified\n", + BFS_COMMAND); cfprintf(cout, " during the search (default: ${blu}-noignore_readdir_race${rs})\n"); cfprintf(cout, " ${blu}-maxdepth${rs} ${bld}N${rs}\n"); 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"); @@ -3039,6 +2848,10 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg 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"); @@ -3141,6 +2954,8 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg cfprintf(cout, " ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n"); cfprintf(cout, " Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n" " output\n"); + cfprintf(cout, " ${blu}-limit${rs} ${bld}N${rs}\n"); + cfprintf(cout, " Quit after this action is evaluated ${bld}N${rs} times\n"); cfprintf(cout, " ${blu}-ls${rs}\n"); cfprintf(cout, " List files like ${ex}ls${rs} ${bld}-dils${rs}\n"); cfprintf(cout, " ${blu}-print${rs}\n"); @@ -3167,160 +2982,207 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg if (pager > 0) { cfclose(cout); - waitpid(pager, NULL, 0); + xwaitpid(pager, NULL, 0); } - state->just_info = true; + parser->just_info = true; 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 parser_state *state, int arg1, int arg2) { - cfprintf(state->ctx->cout, "${ex}bfs${rs} ${bld}%s${rs}\n\n", BFS_VERSION); +static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) { + 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"); - state->just_info = true; + 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; } -typedef struct bfs_expr *parse_fn(struct parser_state *state, int arg1, int arg2); +/** Parser callback function type. */ +typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2); /** * An entry in the parse table for primary expressions. */ 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}, - {"-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}, }; @@ -3341,10 +3203,87 @@ 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; - int best_dist; + int best_dist = INT_MAX; for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { int dist = typo_distance(arg, entry->arg); @@ -3362,9 +3301,11 @@ static const struct table_entry *table_lookup_fuzzy(const char *arg) { * | TEST * | ACTION */ -static struct bfs_expr *parse_primary(struct parser_state *state) { +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 = state->argv[0]; + const char *arg = parser->argv[0]; if (arg[0] != '-') { goto unexpected; @@ -3379,15 +3320,19 @@ static struct bfs_expr *parse_primary(struct parser_state *state) { } } + if (is_flag_group(arg)) { + return parse_flag_group(parser); + } + match = table_lookup_fuzzy(arg); - CFILE *cerr = state->ctx->cerr; - parse_error(state, "Unknown argument; did you mean "); - switch (match->type) { - case T_FLAG: + CFILE *cerr = ctx->cerr; + parse_error(parser, "Unknown argument; did you mean "); + 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: @@ -3395,7 +3340,7 @@ static struct bfs_expr *parse_primary(struct parser_state *state) { break; } - if (!state->interactive || !match->parse) { + if (!ctx->interactive || !match->parse) { fprintf(stderr, "\n"); goto unmatched; } @@ -3406,16 +3351,16 @@ static struct bfs_expr *parse_primary(struct parser_state *state) { } fprintf(stderr, "\n"); - state->argv[0] = match->arg; + parser->argv[0] = match->arg; matched: - return match->parse(state, match->arg1, match->arg2); + return match->parse(parser, match->arg1, match->arg2); unmatched: return NULL; unexpected: - parse_error(state, "Expected a predicate.\n"); + parse_error(parser, "Expected a predicate.\n"); return NULL; } @@ -3425,71 +3370,66 @@ unexpected: * | "-exclude" FACTOR * | PRIMARY */ -static struct bfs_expr *parse_factor(struct parser_state *state) { - if (skip_paths(state) != 0) { +static struct bfs_expr *parse_factor(struct bfs_parser *parser) { + if (skip_paths(parser) != 0) { return NULL; } - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (!arg) { - parse_argv_error(state, state->last_arg, 1, "Expression terminated prematurely here.\n"); + parse_argv_error(parser, parser->last_arg, 1, "Expression terminated prematurely here.\n"); return NULL; } if (strcmp(arg, "(") == 0) { - parser_advance(state, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); - struct bfs_expr *expr = parse_expr(state); + struct bfs_expr *expr = parse_expr(parser); if (!expr) { return NULL; } - if (skip_paths(state) != 0) { - bfs_expr_free(expr); + if (skip_paths(parser) != 0) { return NULL; } - arg = state->argv[0]; + arg = parser->argv[0]; if (!arg || strcmp(arg, ")") != 0) { - parse_argv_error(state, state->last_arg, 1, "Expected a ${red})${rs}.\n"); - bfs_expr_free(expr); + parse_argv_error(parser, parser->last_arg, 1, "Expected a ${red})${rs}.\n"); return NULL; } - parser_advance(state, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); return expr; } else if (strcmp(arg, "-exclude") == 0) { - if (state->excluding) { - parse_error(state, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg); + if (parser->excluding) { + parse_error(parser, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg); return NULL; } - parser_advance(state, T_OPERATOR, 1); - state->excluding = true; + char **argv = parser_advance(parser, BFS_OPERATOR, 1); + parser->excluding = true; - struct bfs_expr *factor = parse_factor(state); + struct bfs_expr *factor = parse_factor(parser); if (!factor) { return NULL; } - state->excluding = false; + parser->excluding = false; - if (parse_exclude(state, factor) != 0) { - return NULL; - } - - return &bfs_true; + bfs_expr_append(parser->ctx->exclude, factor); + 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(state, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); - struct bfs_expr *factor = parse_factor(state); + struct bfs_expr *factor = parse_factor(parser); if (!factor) { return NULL; } - return new_unary_expr(eval_not, factor, argv); + return new_unary_expr(parser, eval_not, factor, argv); } else { - return parse_primary(state); + return parse_primary(parser); } } @@ -3499,16 +3439,15 @@ static struct bfs_expr *parse_factor(struct parser_state *state) { * | TERM "-a" FACTOR * | TERM "-and" FACTOR */ -static struct bfs_expr *parse_term(struct parser_state *state) { - struct bfs_expr *term = parse_factor(state); +static struct bfs_expr *parse_term(struct bfs_parser *parser) { + struct bfs_expr *term = parse_factor(parser); while (term) { - if (skip_paths(state) != 0) { - bfs_expr_free(term); + if (skip_paths(parser) != 0) { return NULL; } - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (!arg) { break; } @@ -3521,17 +3460,16 @@ static struct bfs_expr *parse_term(struct parser_state *state) { char **argv = &fake_and_arg; if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) { - argv = parser_advance(state, T_OPERATOR, 1); + argv = parser_advance(parser, BFS_OPERATOR, 1); } struct bfs_expr *lhs = term; - struct bfs_expr *rhs = parse_factor(state); + struct bfs_expr *rhs = parse_factor(parser); if (!rhs) { - bfs_expr_free(lhs); return NULL; } - term = new_binary_expr(eval_and, lhs, rhs, argv); + term = new_binary_expr(parser, eval_and, lhs, rhs, argv); } return term; @@ -3542,16 +3480,15 @@ static struct bfs_expr *parse_term(struct parser_state *state) { * | CLAUSE "-o" TERM * | CLAUSE "-or" TERM */ -static struct bfs_expr *parse_clause(struct parser_state *state) { - struct bfs_expr *clause = parse_term(state); +static struct bfs_expr *parse_clause(struct bfs_parser *parser) { + struct bfs_expr *clause = parse_term(parser); while (clause) { - if (skip_paths(state) != 0) { - bfs_expr_free(clause); + if (skip_paths(parser) != 0) { return NULL; } - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (!arg) { break; } @@ -3560,16 +3497,15 @@ static struct bfs_expr *parse_clause(struct parser_state *state) { break; } - char **argv = parser_advance(state, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = clause; - struct bfs_expr *rhs = parse_term(state); + struct bfs_expr *rhs = parse_term(parser); if (!rhs) { - bfs_expr_free(lhs); return NULL; } - clause = new_binary_expr(eval_or, lhs, rhs, argv); + clause = new_binary_expr(parser, eval_or, lhs, rhs, argv); } return clause; @@ -3579,16 +3515,15 @@ static struct bfs_expr *parse_clause(struct parser_state *state) { * EXPR : CLAUSE * | EXPR "," CLAUSE */ -static struct bfs_expr *parse_expr(struct parser_state *state) { - struct bfs_expr *expr = parse_clause(state); +static struct bfs_expr *parse_expr(struct bfs_parser *parser) { + struct bfs_expr *expr = parse_clause(parser); while (expr) { - if (skip_paths(state) != 0) { - bfs_expr_free(expr); + if (skip_paths(parser) != 0) { return NULL; } - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (!arg) { break; } @@ -3597,89 +3532,218 @@ static struct bfs_expr *parse_expr(struct parser_state *state) { break; } - char **argv = parser_advance(state, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = expr; - struct bfs_expr *rhs = parse_clause(state); + struct bfs_expr *rhs = parse_clause(parser); if (!rhs) { - bfs_expr_free(lhs); return NULL; } - expr = new_binary_expr(eval_comma, lhs, rhs, argv); + expr = new_binary_expr(parser, eval_comma, lhs, rhs, argv); } 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 parser_state *state) { - if (skip_paths(state) != 0) { +static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { + struct bfs_ctx *ctx = parser->ctx; + + if (skip_paths(parser) != 0) { return NULL; } - struct bfs_expr *expr = &bfs_true; - if (state->argv[0]) { - expr = parse_expr(state); - if (!expr) { + struct bfs_expr *expr; + if (parser->argv[0]) { + expr = parse_expr(parser); + } else { + expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg, BFS_TEST); + } + if (!expr) { + return NULL; + } + + if (parser->argv[0]) { + parse_error(parser, "Unexpected argument.\n"); + 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 (state->argv[0]) { - parse_error(state, "Unexpected argument.\n"); - goto fail; - } + if (parser->implicit_print) { + const struct bfs_expr *limit = parser->limit_expr; + if (limit) { + parse_expr_error(parser, limit, + "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n", + limit, limit); + return NULL; + } - if (state->implicit_print) { - struct bfs_expr *print = bfs_expr_new(eval_fprint, 1, &fake_print_arg); + struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg, BFS_ACTION); if (!print) { - goto fail; + return NULL; } - init_print_expr(state, print); - print->synthetic = true; + init_print_expr(parser, print); - expr = new_binary_expr(eval_and, expr, print, &fake_and_arg); + expr = new_binary_expr(parser, eval_and, expr, print, &fake_and_arg); if (!expr) { - goto fail; + return NULL; } } - if (state->mount_arg && state->xdev_arg) { - parse_conflict_warning(state, state->mount_arg, 1, state->xdev_arg, 1, - "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", - state->xdev_arg[0], state->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 (state->ctx->warn && state->depth_arg && state->prune_arg) { - parse_conflict_warning(state, state->depth_arg, 1, state->prune_arg, 1, - "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", - state->prune_arg[0], state->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 (state->interactive) { - bfs_warning(state->ctx, "Do you want to continue? "); - if (ynprompt() == 0) { - goto fail; + if (ctx->interactive) { + bfs_warning(ctx, "Do you want to continue? "); + if (ynprompt() <= 0) { + return NULL; } } fprintf(stderr, "\n"); } - if (state->ok_expr && state->files0_stdin_arg) { - parse_conflict_error(state, state->ok_expr->argv, state->ok_expr->argc, state->files0_stdin_arg, 2, - "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", - state->ok_expr->argv[0], state->files0_stdin_arg[0], state->files0_stdin_arg[1]); - goto fail; + return expr; +} + +static const char *bftw_strategy_name(enum bftw_strategy strategy) { + switch (strategy) { + case BFTW_BFS: + return "bfs"; + case BFTW_DFS: + return "dfs"; + case BFTW_IDS: + return "ids"; + case BFTW_EDS: + return "eds"; } - return expr; + bfs_bug("Invalid strategy"); + return "???"; +} -fail: - bfs_expr_free(expr); - return NULL; +static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag, const struct bfs_expr *expr, int indent, int rparens) { + bfs_debug_prefix(ctx, flag); + + for (int i = 0; i < indent; ++i) { + cfprintf(ctx->cerr, " "); + } + + bool close = true; + + if (bfs_expr_is_parent(expr)) { + if (SLIST_EMPTY(&expr->children)) { + cfprintf(ctx->cerr, "(${red}%s${rs}", expr->argv[0]); + ++rparens; + } else { + cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]); + for_expr (child, expr) { + int parens = child->next ? 0 : rparens + 1; + dump_expr_multiline(ctx, flag, child, indent + 1, parens); + } + close = false; + } + } else { + if (flag == DEBUG_RATES) { + cfprintf(ctx->cerr, "%pE", expr); + } else { + cfprintf(ctx->cerr, "%pe", expr); + } + } + + if (close) { + for (int i = 0; i < rparens; ++i) { + cfprintf(ctx->cerr, ")"); + } + cfprintf(ctx->cerr, "\n"); + } } void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) { @@ -3689,51 +3753,37 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) { CFILE *cerr = ctx->cerr; - cfprintf(cerr, "${ex}%s${rs} ", ctx->argv[0]); + cfprintf(cerr, "${ex}%s${rs}", ctx->argv[0]); if (ctx->flags & BFTW_FOLLOW_ALL) { - cfprintf(cerr, "${cyn}-L${rs} "); + cfprintf(cerr, " ${cyn}-L${rs}"); } else if (ctx->flags & BFTW_FOLLOW_ROOTS) { - cfprintf(cerr, "${cyn}-H${rs} "); + cfprintf(cerr, " ${cyn}-H${rs}"); } else { - cfprintf(cerr, "${cyn}-P${rs} "); + cfprintf(cerr, " ${cyn}-P${rs}"); } if (ctx->xargs_safe) { - cfprintf(cerr, "${cyn}-X${rs} "); + cfprintf(cerr, " ${cyn}-X${rs}"); } if (ctx->flags & BFTW_SORT) { - cfprintf(cerr, "${cyn}-s${rs} "); + cfprintf(cerr, " ${cyn}-s${rs}"); } + cfprintf(cerr, " ${cyn}-j${bld}%d${rs}", ctx->threads); + if (ctx->optlevel != 3) { - cfprintf(cerr, "${cyn}-O${bld}%d${rs} ", ctx->optlevel); + cfprintf(cerr, " ${cyn}-O${bld}%d${rs}", ctx->optlevel); } - const char *strategy = NULL; - switch (ctx->strategy) { - case BFTW_BFS: - strategy = "bfs"; - break; - case BFTW_DFS: - strategy = "dfs"; - break; - case BFTW_IDS: - strategy = "ids"; - break; - case BFTW_EDS: - strategy = "eds"; - break; - } - assert(strategy); - cfprintf(cerr, "${cyn}-S${rs} ${bld}%s${rs} ", strategy); + cfprintf(cerr, " ${cyn}-S${rs} ${bld}%s${rs}", bftw_strategy_name(ctx->strategy)); enum debug_flags debug = ctx->debug; if (debug == DEBUG_ALL) { - cfprintf(cerr, "${cyn}-D${rs} ${bld}all${rs} "); + cfprintf(cerr, " ${cyn}-D${rs} ${bld}all${rs}"); } else if (debug) { - cfprintf(cerr, "${cyn}-D${rs} "); + cfprintf(cerr, " ${cyn}-D${rs} "); for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) { if (debug & i) { cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i)); @@ -3743,61 +3793,53 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) { } } } - cfprintf(cerr, " "); } - for (size_t i = 0; i < darray_length(ctx->paths); ++i) { + for (size_t i = 0; i < ctx->npaths; ++i) { const char *path = ctx->paths[i]; char c = path[0]; if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') { - cfprintf(cerr, "${cyn}-f${rs} "); + cfprintf(cerr, " ${cyn}-f${rs}"); } - cfprintf(cerr, "${mag}%s${rs} ", path); + cfprintf(cerr, " ${mag}%pq${rs}", path); } if (ctx->cout->colors) { - cfprintf(cerr, "${blu}-color${rs} "); + cfprintf(cerr, " ${blu}-color${rs}"); } else { - cfprintf(cerr, "${blu}-nocolor${rs} "); + cfprintf(cerr, " ${blu}-nocolor${rs}"); } if (ctx->flags & BFTW_POST_ORDER) { - cfprintf(cerr, "${blu}-depth${rs} "); + cfprintf(cerr, " ${blu}-depth${rs}"); } if (ctx->ignore_races) { - cfprintf(cerr, "${blu}-ignore_readdir_race${rs} "); + cfprintf(cerr, " ${blu}-ignore_readdir_race${rs}"); } if (ctx->mindepth != 0) { - cfprintf(cerr, "${blu}-mindepth${rs} ${bld}%d${rs} ", ctx->mindepth); + cfprintf(cerr, " ${blu}-mindepth${rs} ${bld}%d${rs}", ctx->mindepth); } if (ctx->maxdepth != INT_MAX) { - cfprintf(cerr, "${blu}-maxdepth${rs} ${bld}%d${rs} ", ctx->maxdepth); + cfprintf(cerr, " ${blu}-maxdepth${rs} ${bld}%d${rs}", ctx->maxdepth); } if (ctx->flags & BFTW_SKIP_MOUNTS) { - cfprintf(cerr, "${blu}-mount${rs} "); + cfprintf(cerr, " ${blu}-mount${rs}"); } if (ctx->status) { - cfprintf(cerr, "${blu}-status${rs} "); + cfprintf(cerr, " ${blu}-status${rs}"); } if (ctx->unique) { - cfprintf(cerr, "${blu}-unique${rs} "); + cfprintf(cerr, " ${blu}-unique${rs}"); } if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) { - cfprintf(cerr, "${blu}-xdev${rs} "); - } - - if (flag == DEBUG_RATES) { - if (ctx->exclude != &bfs_false) { - cfprintf(cerr, "(${red}-exclude${rs} %pE) ", ctx->exclude); - } - cfprintf(cerr, "%pE", ctx->expr); - } else { - if (ctx->exclude != &bfs_false) { - cfprintf(cerr, "(${red}-exclude${rs} %pe) ", ctx->exclude); - } - cfprintf(cerr, "%pe", ctx->expr); + cfprintf(cerr, " ${blu}-xdev${rs}"); } fputs("\n", stderr); + + bfs_debug(ctx, flag, "(${red}-exclude${rs}\n"); + dump_expr_multiline(ctx, flag, ctx->exclude, 1, 1); + + dump_expr_multiline(ctx, flag, ctx->expr, 0, 0); } /** @@ -3806,57 +3848,38 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) { static void dump_costs(const struct bfs_ctx *ctx) { const struct bfs_expr *expr = ctx->expr; bfs_debug(ctx, DEBUG_COST, " Cost: ~${ylw}%g${rs}\n", expr->cost); - bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0*expr->probability); -} - -/** - * Get the current time. - */ -static int parse_gettime(const struct bfs_ctx *ctx, struct timespec *ts) { -#if _POSIX_TIMERS > 0 - int ret = clock_gettime(CLOCK_REALTIME, ts); - if (ret != 0) { - bfs_perror(ctx, "clock_gettime()"); - } - return ret; -#else - struct timeval tv; - int ret = gettimeofday(&tv, NULL); - if (ret == 0) { - ts->tv_sec = tv.tv_sec; - ts->tv_nsec = tv.tv_usec * 1000L; - } else { - bfs_perror(ctx, "gettimeofday()"); - } - return ret; -#endif + bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0 * expr->probability); } struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { struct bfs_ctx *ctx = bfs_ctx_new(); if (!ctx) { - perror("bfs_new_ctx()"); + perror("bfs_ctx_new()"); goto fail; } - static char* default_argv[] = {"bfs", NULL}; + static char *default_argv[] = {BFS_COMMAND, NULL}; if (argc < 1) { argc = 1; argv = default_argv; } ctx->argc = argc; - ctx->argv = malloc((argc + 1)*sizeof(*ctx->argv)); + ctx->argv = xmemdup(argv, sizeof_array(char *, argc + 1)); if (!ctx->argv) { - perror("malloc()"); + perror("xmemdup()"); goto fail; } - for (int i = 0; i <= argc; ++i) { - ctx->argv[i] = argv[i]; + + ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc); + if (!ctx->kinds) { + perror("zalloc()"); + goto fail; } enum use_color use_color = COLOR_AUTO; - if (getenv("NO_COLOR")) { + const char *no_color = getenv("NO_COLOR"); + if (no_color && *no_color) { // https://no-color.org/ use_color = COLOR_NEVER; } @@ -3892,59 +3915,50 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { } else { ctx->warn = stdin_tty; } + ctx->interactive = stdin_tty && stderr_tty; - struct parser_state state = { + struct bfs_parser parser = { .ctx = ctx, .argv = ctx->argv + 1, .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_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, }; - if (strcmp(xbasename(state.command), "find") == 0) { - // Operate depth-first when invoked as "find" - ctx->strategy = BFTW_DFS; - } - - if (parse_gettime(ctx, &state.now) != 0) { + ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg, BFS_OPERATOR); + if (!ctx->exclude) { goto fail; } - ctx->exclude = &bfs_false; - ctx->expr = parse_whole_expr(&state); + ctx->expr = parse_whole_expr(&parser); if (!ctx->expr) { - if (state.just_info) { + if (parser.just_info) { goto done; } else { goto fail; } } - if (bfs_optimize(ctx) != 0) { - goto fail; + if (parser.use_color == COLOR_AUTO && !ctx->colors) { + bfs_warning(ctx, "Error parsing $$LS_COLORS: %s.\n\n", xstrerror(ctx->colors_error)); } - if (darray_length(ctx->paths) == 0) { - if (!state.implicit_root) { - parse_argv_error(&state, state.files0_arg, 2, "No root paths specified.\n"); - goto fail; - } else if (parse_root(&state, ".") != 0) { - goto fail; + if (bfs_optimize(ctx) != 0) { + 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 7e29a03..fcc8234 100644 --- a/src/parse.h +++ b/src/parse.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * bfs command line parsing. @@ -24,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 8fdde41..30ec201 100644 --- a/src/printf.c +++ b/src/printf.c @@ -1,70 +1,70 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "printf.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "color.h" #include "ctx.h" -#include "darray.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 "util.h" -#include "xtime.h" -#include <assert.h> + #include <errno.h> #include <grp.h> #include <pwd.h> -#include <stdbool.h> +#include <stdarg.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> +struct bfs_fmt; + /** * A function implementing a printf directive. */ -typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf); +typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf); /** - * A single printf directive like %f or %#4m. The whole format string is stored - * as a darray of these. + * A single formatting directive like %f or %#4m. */ -struct bfs_printf { +struct bfs_fmt { /** The printing function to invoke. */ bfs_printf_fn *fn; /** String data associated with this directive. */ - char *str; + dchar *str; /** The stat field to print. */ enum bfs_stat_field stat_field; /** Character data associated with this directive. */ char c; /** Some data used by the directive. */ - const void *ptr; + void *ptr; +}; + +/** + * An entire format string. + */ +struct bfs_printf { + /** An array of formatting directives. */ + struct bfs_fmt *fmts; + /** The number of directives. */ + size_t nfmts; }; /** Print some text as-is. */ -static int bfs_printf_literal(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - size_t len = dstrlen(directive->str); - if (fwrite(directive->str, 1, len, cfile->file) == len) { +static int bfs_printf_literal(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + size_t len = dstrlen(fmt->str); + if (fwrite(fmt->str, 1, len, cfile->file) == len) { return 0; } else { return -1; @@ -72,26 +72,44 @@ static int bfs_printf_literal(CFILE *cfile, const struct bfs_printf *directive, } /** \c: flush */ -static int bfs_printf_flush(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_flush(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { return fflush(cfile->file); } /** Check if we can safely colorize this directive. */ -static bool should_color(CFILE *cfile, const struct bfs_printf *directive) { - return cfile->colors && strcmp(directive->str, "%s") == 0; +static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) { + return cfile->colors && strcmp(fmt->str, "%s") == 0; } /** * Print a value to a temporary buffer before formatting it. */ -#define BFS_PRINTF_BUF(buf, format, ...) \ - char buf[256]; \ - int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \ - assert(ret >= 0 && (size_t)ret < sizeof(buf)); \ +#define BFS_PRINTF_BUF(buf, format, ...) \ + char buf[256]; \ + int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \ + bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \ (void)ret +/** 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; +} + /** %a, %c, %t: ctime() */ -static int bfs_printf_ctime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { // Not using ctime() itself because GNU find adds nanoseconds static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; @@ -101,69 +119,69 @@ static int bfs_printf_ctime(CFILE *cfile, const struct bfs_printf *directive, co return -1; } - const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field); + const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field); if (!ts) { return -1; } struct tm tm; - if (xlocaltime(&ts->tv_sec, &tm) != 0) { + if (!localtime_r(&ts->tv_sec, &tm)) { return -1; } BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d", - days[tm.tm_wday], - months[tm.tm_mon], - tm.tm_mday, - tm.tm_hour, - tm.tm_min, - tm.tm_sec, - (long)ts->tv_nsec, - 1900 + tm.tm_year); + days[tm.tm_wday], + months[tm.tm_mon], + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec, + 1900 + tm.tm_year); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %A, %B/%W, %C, %T: strftime() */ -static int bfs_printf_strftime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field); + const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field); if (!ts) { return -1; } struct tm tm; - if (xlocaltime(&ts->tv_sec, &tm) != 0) { + if (!localtime_r(&ts->tv_sec, &tm)) { return -1; } int ret; char buf[256]; char format[] = "% "; - switch (directive->c) { + switch (fmt->c) { // Non-POSIX strftime() features case '@': ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec); break; case '+': ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0", - 1900 + tm.tm_year, - tm.tm_mon + 1, - tm.tm_mday, - tm.tm_hour, - tm.tm_min, - tm.tm_sec, - (long)ts->tv_nsec); + 1900 + tm.tm_year, + tm.tm_mon + 1, + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec); break; case 'k': ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour); break; case 'l': - ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11)%12 + 1); + ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11) % 12 + 1); break; case 's': ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec); @@ -173,102 +191,113 @@ static int bfs_printf_strftime(CFILE *cfile, const struct bfs_printf *directive, break; case 'T': ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0", - tm.tm_hour, - tm.tm_min, - tm.tm_sec, - (long)ts->tv_nsec); + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + (long)ts->tv_nsec); break; // POSIX strftime() features default: - format[1] = directive->c; + format[1] = fmt->c; +#if __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif ret = strftime(buf, sizeof(buf), format, &tm); +#if __GNUC__ +# pragma GCC diagnostic pop +#endif break; } - assert(ret >= 0 && (size_t)ret < sizeof(buf)); + bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); (void)ret; - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %b: blocks */ -static int bfs_printf_b(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 511)/512; + uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512; BFS_PRINTF_BUF(buf, "%ju", blocks); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %d: depth */ -static int bfs_printf_d(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - return fprintf(cfile->file, directive->str, (intmax_t)ftwbuf->depth); +static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth); } /** %D: device */ -static int bfs_printf_D(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %f: file name */ -static int bfs_printf_f(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - if (should_color(cfile, directive)) { +static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + if (should_color(cfile, fmt)) { return cfprintf(cfile, "%pF", ftwbuf); } else { - return fprintf(cfile->file, directive->str, ftwbuf->path + ftwbuf->nameoff); + return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff); } } /** %F: file system type */ -static int bfs_printf_F(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - const char *type = bfs_fstype(directive->ptr, statbuf); - return fprintf(cfile->file, directive->str, type); + const char *type = bfs_fstype(fmt->ptr, statbuf); + if (!type) { + return -1; + } + + return bfs_fprintf(cfile, fmt, "%s", type); } /** %G: gid */ -static int bfs_printf_G(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %g: group name */ -static int bfs_printf_g(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - const struct bfs_groups *groups = directive->ptr; - const struct group *grp = groups ? bfs_getgrgid(groups, statbuf->gid) : NULL; + struct bfs_groups *groups = fmt->ptr; + const struct group *grp = bfs_getgrgid(groups, statbuf->gid); if (!grp) { - return bfs_printf_G(cfile, directive, ftwbuf); + return bfs_printf_G(cfile, fmt, ftwbuf); } - return fprintf(cfile->file, directive->str, grp->gr_name); + return bfs_fprintf(cfile, fmt, "%s", grp->gr_name); } /** %h: leading directories */ -static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { char *copy = NULL; const char *buf; @@ -290,10 +319,10 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const } int ret; - if (should_color(cfile, directive)) { - ret = cfprintf(cfile, "${di}%s${rs}", buf); + if (should_color(cfile, fmt)) { + ret = cfprintf(cfile, "${di}%pQ${rs}", buf); } else { - ret = fprintf(cfile->file, directive->str, buf); + ret = bfs_fprintf(cfile, fmt, "%s", buf); } free(copy); @@ -301,48 +330,48 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const } /** %H: current root */ -static int bfs_printf_H(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - if (should_color(cfile, directive)) { +static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + if (should_color(cfile, fmt)) { if (ftwbuf->depth == 0) { return cfprintf(cfile, "%pP", ftwbuf); } else { - return cfprintf(cfile, "${di}%s${rs}", ftwbuf->root); + return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root); } } else { - return fprintf(cfile->file, directive->str, ftwbuf->root); + return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root); } } /** %i: inode */ -static int bfs_printf_i(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %k: 1K blocks */ -static int bfs_printf_k(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024; + uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024; BFS_PRINTF_BUF(buf, "%ju", blocks); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %l: link target */ -static int bfs_printf_l(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { char *buf = NULL; const char *target = ""; if (ftwbuf->type == BFS_LNK) { - if (should_color(cfile, directive)) { + if (should_color(cfile, fmt)) { return cfprintf(cfile, "%pL", ftwbuf); } @@ -355,23 +384,23 @@ static int bfs_printf_l(CFILE *cfile, const struct bfs_printf *directive, const } } - int ret = fprintf(cfile->file, directive->str, target); + int ret = bfs_fprintf(cfile, fmt, "%s", target); free(buf); return ret; } /** %m: mode */ -static int bfs_printf_m(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - return fprintf(cfile->file, directive->str, (unsigned int)(statbuf->mode & 07777)); + return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777)); } /** %M: symbolic mode */ -static int bfs_printf_M(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; @@ -379,37 +408,37 @@ static int bfs_printf_M(CFILE *cfile, const struct bfs_printf *directive, const char buf[11]; xstrmode(statbuf->mode, buf); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %n: link count */ -static int bfs_printf_n(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %p: full path */ -static int bfs_printf_p(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - if (should_color(cfile, directive)) { +static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + if (should_color(cfile, fmt)) { return cfprintf(cfile, "%pP", ftwbuf); } else { - return fprintf(cfile->file, directive->str, ftwbuf->path); + return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path); } } /** %P: path after root */ -static int bfs_printf_P(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { size_t offset = strlen(ftwbuf->root); if (ftwbuf->path[offset] == '/') { ++offset; } - if (should_color(cfile, directive)) { + if (should_color(cfile, fmt)) { if (ftwbuf->depth == 0) { return 0; } @@ -419,23 +448,23 @@ static int bfs_printf_P(CFILE *cfile, const struct bfs_printf *directive, const copybuf.nameoff -= offset; return cfprintf(cfile, "%pP", ©buf); } else { - return fprintf(cfile->file, directive->str, ftwbuf->path + offset); + return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset); } } /** %s: size */ -static int bfs_printf_s(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %S: sparseness */ -static int bfs_printf_S(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; @@ -445,97 +474,86 @@ static int bfs_printf_S(CFILE *cfile, const struct bfs_printf *directive, const if (statbuf->size == 0 && statbuf->blocks == 0) { sparsity = 1.0; } else { - sparsity = (double)BFS_STAT_BLKSIZE*statbuf->blocks/statbuf->size; + sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size; } - return fprintf(cfile->file, directive->str, sparsity); + return bfs_fprintf(cfile, fmt, "%g", sparsity); } /** %U: uid */ -static int bfs_printf_U(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid); - return fprintf(cfile->file, directive->str, buf); + return bfs_fprintf(cfile, fmt, "%s", buf); } /** %u: user name */ -static int bfs_printf_u(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { +static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags); if (!statbuf) { return -1; } - const struct bfs_users *users = directive->ptr; - const struct passwd *pwd = users ? bfs_getpwuid(users, statbuf->uid) : NULL; + struct bfs_users *users = fmt->ptr; + const struct passwd *pwd = bfs_getpwuid(users, statbuf->uid); if (!pwd) { - return bfs_printf_U(cfile, directive, ftwbuf); + return bfs_printf_U(cfile, fmt, ftwbuf); } - return fprintf(cfile->file, directive->str, 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_printf *directive, const struct BFTW *ftwbuf) { +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 fprintf(cfile->file, directive->str, type); + return bfs_fprintf(cfile, fmt, "%s", type); } /** %Y: target type */ -static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) { - int error = 0; +static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW); + const char *str; - if (ftwbuf->type != BFS_LNK) { - return bfs_printf_y(cfile, directive, 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 = fprintf(cfile->file, directive->str, type); + int ret = bfs_fprintf(cfile, fmt, "%s", str); if (error != 0) { ret = -1; errno = error; @@ -543,24 +561,36 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const 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. */ -static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal) { +static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal) { if (dstrlen(*literal) == 0) { return 0; } - struct bfs_printf directive = { - .fn = bfs_printf_literal, - .str = *literal, - }; - - if (DARRAY_PUSH(format, &directive) != 0) { - bfs_perror(ctx, "DARRAY_PUSH()"); + struct bfs_fmt *fmt = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts); + if (!fmt) { + bfs_perror(ctx, "RESERVE()"); return -1; } + fmt->fn = bfs_printf_literal; + fmt->str = *literal; + *literal = dstralloc(0); if (!*literal) { bfs_perror(ctx, "dstralloc()"); @@ -573,23 +603,29 @@ static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf **format, /** * Append a printf directive to the chain. */ -static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal, struct bfs_printf *directive) { +static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal, struct bfs_fmt *fmt) { if (append_literal(ctx, format, literal) != 0) { return -1; } - if (DARRAY_PUSH(format, directive) != 0) { - bfs_perror(ctx, "DARRAY_PUSH()"); + struct bfs_fmt *dest = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts); + if (!dest) { + bfs_perror(ctx, "RESERVE()"); return -1; } + *dest = *fmt; return 0; } int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format) { - expr->printf = NULL; + expr->printf = ZALLOC(struct bfs_printf); + if (!expr->printf) { + bfs_perror(ctx, "zalloc()"); + return -1; + } - char *literal = dstralloc(0); + dchar *literal = dstralloc(0); if (!literal) { bfs_perror(ctx, "dstralloc()"); goto error; @@ -623,10 +659,10 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha case 'c': { - struct bfs_printf directive = { + struct bfs_fmt fmt = { .fn = bfs_printf_flush, }; - if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) { + if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) { goto error; } goto done; @@ -648,15 +684,15 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha goto one_char; } - struct bfs_printf directive = { + struct bfs_fmt fmt = { .str = dstralloc(2), }; - if (!directive.str) { - goto directive_error; + if (!fmt.str) { + goto fmt_error; } - if (dstrapp(&directive.str, c) != 0) { + if (dstrapp(&fmt.str, c) != 0) { bfs_perror(ctx, "dstrapp()"); - goto directive_error; + goto fmt_error; } const char *specifier = "s"; @@ -670,18 +706,18 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha case '#': case '0': case '+': - must_be_numeric = true; - BFS_FALLTHROUGH; case ' ': + must_be_numeric = true; + _fallthrough; case '-': - if (strchr(directive.str, c)) { + if (strchr(fmt.str, c)) { bfs_expr_error(ctx, expr); bfs_error(ctx, "Duplicate flag '%c'.\n", c); - goto directive_error; + goto fmt_error; } - if (dstrapp(&directive.str, c) != 0) { + if (dstrapp(&fmt.str, c) != 0) { bfs_perror(ctx, "dstrapp()"); - goto directive_error; + goto fmt_error; } continue; } @@ -691,9 +727,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha // Parse the field width while (c >= '0' && c <= '9') { - if (dstrapp(&directive.str, c) != 0) { + if (dstrapp(&fmt.str, c) != 0) { bfs_perror(ctx, "dstrapp()"); - goto directive_error; + goto fmt_error; } c = *++i; } @@ -701,9 +737,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha // Parse the precision if (c == '.') { do { - if (dstrapp(&directive.str, c) != 0) { + if (dstrapp(&fmt.str, c) != 0) { bfs_perror(ctx, "dstrapp()"); - goto directive_error; + goto fmt_error; } c = *++i; } while (c >= '0' && c <= '9'); @@ -711,175 +747,172 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha switch (c) { case 'a': - directive.fn = bfs_printf_ctime; - directive.stat_field = BFS_STAT_ATIME; + fmt.fn = bfs_printf_ctime; + fmt.stat_field = BFS_STAT_ATIME; break; case 'b': - directive.fn = bfs_printf_b; + fmt.fn = bfs_printf_b; break; case 'c': - directive.fn = bfs_printf_ctime; - directive.stat_field = BFS_STAT_CTIME; + fmt.fn = bfs_printf_ctime; + fmt.stat_field = BFS_STAT_CTIME; break; case 'd': - directive.fn = bfs_printf_d; + fmt.fn = bfs_printf_d; specifier = "jd"; break; case 'D': - directive.fn = bfs_printf_D; + fmt.fn = bfs_printf_D; break; case 'f': - directive.fn = bfs_printf_f; + fmt.fn = bfs_printf_f; break; case 'F': - directive.ptr = bfs_ctx_mtab(ctx); - if (!directive.ptr) { + fmt.fn = bfs_printf_F; + fmt.ptr = (void *)bfs_ctx_mtab(ctx); + if (!fmt.ptr) { int error = errno; bfs_expr_error(ctx, expr); - bfs_error(ctx, "Couldn't parse the mount table: %s.\n", strerror(error)); - goto directive_error; + bfs_error(ctx, "Couldn't parse the mount table: %s.\n", xstrerror(error)); + goto fmt_error; } - directive.fn = bfs_printf_F; break; case 'g': - directive.ptr = bfs_ctx_groups(ctx); - if (!directive.ptr) { - int error = errno; - bfs_expr_error(ctx, expr); - bfs_error(ctx, "Couldn't parse the group table: %s.\n", strerror(error)); - goto directive_error; - } - directive.fn = bfs_printf_g; + fmt.fn = bfs_printf_g; + fmt.ptr = ctx->groups; break; case 'G': - directive.fn = bfs_printf_G; + fmt.fn = bfs_printf_G; break; case 'h': - directive.fn = bfs_printf_h; + fmt.fn = bfs_printf_h; break; case 'H': - directive.fn = bfs_printf_H; + fmt.fn = bfs_printf_H; break; case 'i': - directive.fn = bfs_printf_i; + fmt.fn = bfs_printf_i; break; case 'k': - directive.fn = bfs_printf_k; + fmt.fn = bfs_printf_k; break; case 'l': - directive.fn = bfs_printf_l; + fmt.fn = bfs_printf_l; break; case 'm': - directive.fn = bfs_printf_m; + fmt.fn = bfs_printf_m; specifier = "o"; break; case 'M': - directive.fn = bfs_printf_M; + fmt.fn = bfs_printf_M; break; case 'n': - directive.fn = bfs_printf_n; + fmt.fn = bfs_printf_n; break; case 'p': - directive.fn = bfs_printf_p; + fmt.fn = bfs_printf_p; break; case 'P': - directive.fn = bfs_printf_P; + fmt.fn = bfs_printf_P; break; case 's': - directive.fn = bfs_printf_s; + fmt.fn = bfs_printf_s; break; case 'S': - directive.fn = bfs_printf_S; + fmt.fn = bfs_printf_S; specifier = "g"; break; case 't': - directive.fn = bfs_printf_ctime; - directive.stat_field = BFS_STAT_MTIME; + fmt.fn = bfs_printf_ctime; + fmt.stat_field = BFS_STAT_MTIME; break; case 'u': - directive.ptr = bfs_ctx_users(ctx); - if (!directive.ptr) { - int error = errno; - bfs_expr_error(ctx, expr); - bfs_error(ctx, "Couldn't parse the user table: %s.\n", strerror(error)); - goto directive_error; - } - directive.fn = bfs_printf_u; + fmt.fn = bfs_printf_u; + fmt.ptr = ctx->users; break; case 'U': - directive.fn = bfs_printf_U; + fmt.fn = bfs_printf_U; break; case 'w': - directive.fn = bfs_printf_ctime; - directive.stat_field = BFS_STAT_BTIME; + fmt.fn = bfs_printf_ctime; + fmt.stat_field = BFS_STAT_BTIME; break; case 'y': - directive.fn = bfs_printf_y; + fmt.fn = bfs_printf_y; break; case 'Y': - directive.fn = bfs_printf_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': - directive.stat_field = BFS_STAT_ATIME; - goto directive_strftime; + fmt.stat_field = BFS_STAT_ATIME; + goto fmt_strftime; case 'B': case 'W': - directive.stat_field = BFS_STAT_BTIME; - goto directive_strftime; + fmt.stat_field = BFS_STAT_BTIME; + goto fmt_strftime; case 'C': - directive.stat_field = BFS_STAT_CTIME; - goto directive_strftime; + fmt.stat_field = BFS_STAT_CTIME; + goto fmt_strftime; case 'T': - directive.stat_field = BFS_STAT_MTIME; - goto directive_strftime; + fmt.stat_field = BFS_STAT_MTIME; + goto fmt_strftime; - directive_strftime: - directive.fn = bfs_printf_strftime; + fmt_strftime: + fmt.fn = bfs_printf_strftime; c = *++i; if (!c) { bfs_expr_error(ctx, expr); - bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", directive.str, i[-1]); - goto directive_error; + bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", fmt.str, i[-1]); + goto fmt_error; } else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) { - directive.c = c; + fmt.c = c; } else { bfs_expr_error(ctx, expr); bfs_error(ctx, "Unrecognized time specifier '%%%c%c'.\n", i[-1], c); - goto directive_error; + goto fmt_error; } break; case '\0': bfs_expr_error(ctx, expr); - bfs_error(ctx, "Incomplete format specifier '%s'.\n", directive.str); - goto directive_error; + bfs_error(ctx, "Incomplete format specifier '%s'.\n", fmt.str); + goto fmt_error; default: bfs_expr_error(ctx, expr); bfs_error(ctx, "Unrecognized format specifier '%%%c'.\n", c); - goto directive_error; + goto fmt_error; } if (must_be_numeric && strcmp(specifier, "s") == 0) { bfs_expr_error(ctx, expr); - bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", directive.str + 1, c); - goto directive_error; + bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", fmt.str + 1, c); + goto fmt_error; } - if (dstrcat(&directive.str, specifier) != 0) { + if (dstrcat(&fmt.str, specifier) != 0) { bfs_perror(ctx, "dstrcat()"); - goto directive_error; + goto fmt_error; } - if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) { - goto directive_error; + if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) { + goto fmt_error; } continue; - directive_error: - dstrfree(directive.str); + fmt_error: + dstrfree(fmt.str); goto error; } @@ -891,7 +924,7 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha } done: - if (append_literal(ctx, &expr->printf, &literal) != 0) { + if (append_literal(ctx, expr->printf, &literal) != 0) { goto error; } dstrfree(literal); @@ -907,9 +940,9 @@ error: int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf) { int ret = 0, error = 0; - for (size_t i = 0; i < darray_length(format); ++i) { - const struct bfs_printf *directive = &format[i]; - if (directive->fn(cfile, directive, ftwbuf) < 0) { + for (size_t i = 0; i < format->nfmts; ++i) { + const struct bfs_fmt *fmt = &format->fmts[i]; + if (fmt->fn(cfile, fmt, ftwbuf) < 0) { ret = -1; error = errno; } @@ -920,8 +953,13 @@ int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW } void bfs_printf_free(struct bfs_printf *format) { - for (size_t i = 0; i < darray_length(format); ++i) { - dstrfree(format[i].str); + if (!format) { + return; + } + + for (size_t i = 0; i < format->nfmts; ++i) { + dstrfree(format->fmts[i].str); } - darray_free(format); + free(format->fmts); + free(format); } diff --git a/src/printf.h b/src/printf.h index a8c5f2a..e8d862e 100644 --- a/src/printf.h +++ b/src/printf.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Implementation of -printf/-fprintf. @@ -35,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. @@ -49,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 91435bd..fa19dad 100644 --- a/src/pwcache.c +++ b/src/pwcache.c @@ -1,293 +1,219 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "pwcache.h" -#include "darray.h" + +#include "alloc.h" #include "trie.h" + #include <errno.h> #include <grp.h> #include <pwd.h> -#include <stdbool.h> #include <stdlib.h> -#include <string.h> + +/** Represents cache hits for negative results. */ +static void *MISSING = &MISSING; + +/** Callback type for bfs_getent(). */ +typedef void *bfs_getent_fn(const void *key, void *ptr, size_t bufsize); + +/** Shared scaffolding for get{pw,gr}{nam,?id}_r(). */ +static void *bfs_getent(bfs_getent_fn *fn, const void *key, struct trie_leaf *leaf, struct varena *varena) { + if (leaf->value) { + errno = 0; + return leaf->value == MISSING ? NULL : leaf->value; + } + + // _SC_GET{PW,GR}_R_SIZE_MAX tend to be fairly large (~1K). That's okay + // for temporary allocations, but for these long-lived ones, let's start + // with a smaller buffer. + size_t bufsize = 128; + void *ptr = varena_alloc(varena, bufsize); + if (!ptr) { + return NULL; + } + + while (true) { + void *ret = fn(key, ptr, bufsize); + if (ret) { + leaf->value = ret; + return ret; + } else if (errno == 0) { + leaf->value = MISSING; + break; + } else if (errno == ERANGE) { + void *next = varena_grow(varena, ptr, &bufsize); + if (!next) { + break; + } + ptr = next; + } else { + break; + } + } + + varena_free(varena, ptr, bufsize); + return NULL; +} + +/** + * An arena-allocated struct passwd. + */ +struct bfs_passwd { + struct passwd pwd; + char buf[]; +}; struct bfs_users { - /** The array of passwd entries. */ - struct passwd *entries; + /** bfs_passwd arena. */ + struct varena varena; /** A map from usernames to entries. */ struct trie by_name; /** A map from UIDs to entries. */ struct trie by_uid; }; -struct bfs_users *bfs_users_parse(void) { - int error; - - struct bfs_users *users = malloc(sizeof(*users)); +struct bfs_users *bfs_users_new(void) { + struct bfs_users *users = ALLOC(struct bfs_users); if (!users) { return NULL; } - users->entries = NULL; + VARENA_INIT(&users->varena, struct bfs_passwd, buf); trie_init(&users->by_name); trie_init(&users->by_uid); + return users; +} - setpwent(); - - while (true) { - errno = 0; - struct passwd *ent = getpwent(); - if (!ent) { - if (errno) { - error = errno; - goto fail_end; - } else { - break; - } - } +/** bfs_getent() callback for getpwnam_r(). */ +static void *bfs_getpwnam_impl(const void *key, void *ptr, size_t bufsize) { + struct bfs_passwd *storage = ptr; - if (DARRAY_PUSH(&users->entries, ent) != 0) { - error = errno; - goto fail_end; - } + struct passwd *ret = NULL; + errno = getpwnam_r(key, &storage->pwd, storage->buf, bufsize, &ret); + return ret; +} - ent = users->entries + darray_length(users->entries) - 1; - ent->pw_name = strdup(ent->pw_name); - ent->pw_dir = strdup(ent->pw_dir); - ent->pw_shell = strdup(ent->pw_shell); - if (!ent->pw_name || !ent->pw_dir || !ent->pw_shell) { - error = ENOMEM; - goto fail_end; - } +const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name) { + struct trie_leaf *leaf = trie_insert_str(&users->by_name, name); + if (!leaf) { + return NULL; } - endpwent(); - - for (size_t i = 0; i < darray_length(users->entries); ++i) { - struct passwd *entry = &users->entries[i]; - struct trie_leaf *leaf = trie_insert_str(&users->by_name, entry->pw_name); - if (leaf) { - if (!leaf->value) { - leaf->value = entry; - } - } else { - error = errno; - goto fail_free; - } - - leaf = trie_insert_mem(&users->by_uid, &entry->pw_uid, sizeof(entry->pw_uid)); - if (leaf) { - if (!leaf->value) { - leaf->value = entry; - } - } else { - error = errno; - goto fail_free; - } - } + return bfs_getent(bfs_getpwnam_impl, name, leaf, &users->varena); +} - return users; +/** bfs_getent() callback for getpwuid_r(). */ +static void *bfs_getpwuid_impl(const void *key, void *ptr, size_t bufsize) { + const uid_t *uid = key; + struct bfs_passwd *storage = ptr; -fail_end: - endpwent(); -fail_free: - bfs_users_free(users); - errno = error; - return NULL; + struct passwd *ret = NULL; + errno = getpwuid_r(*uid, &storage->pwd, storage->buf, bufsize, &ret); + return ret; } -const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name) { - const struct trie_leaf *leaf = trie_find_str(&users->by_name, name); - if (leaf) { - return leaf->value; - } else { +const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid) { + struct trie_leaf *leaf = trie_insert_mem(&users->by_uid, &uid, sizeof(uid)); + if (!leaf) { return NULL; } + + return bfs_getent(bfs_getpwuid_impl, &uid, leaf, &users->varena); } -const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid) { - const struct trie_leaf *leaf = trie_find_mem(&users->by_uid, &uid, sizeof(uid)); - if (leaf) { - return leaf->value; - } else { - return NULL; - } +void bfs_users_flush(struct bfs_users *users) { + trie_clear(&users->by_uid); + trie_clear(&users->by_name); + varena_clear(&users->varena); } void bfs_users_free(struct bfs_users *users) { if (users) { trie_destroy(&users->by_uid); trie_destroy(&users->by_name); - - for (size_t i = 0; i < darray_length(users->entries); ++i) { - struct passwd *entry = &users->entries[i]; - free(entry->pw_shell); - free(entry->pw_dir); - free(entry->pw_name); - } - darray_free(users->entries); - + varena_destroy(&users->varena); free(users); } } +/** + * An arena-allocated struct group. + */ +struct bfs_group { + struct group grp; + char buf[]; +}; + struct bfs_groups { - /** The array of group entries. */ - struct group *entries; + /** bfs_group arena. */ + struct varena varena; /** A map from group names to entries. */ struct trie by_name; /** A map from GIDs to entries. */ struct trie by_gid; }; -/** - * struct group::gr_mem isn't properly aligned on macOS, so do this to avoid - * ASAN warnings. - */ -static char *next_gr_mem(void **gr_mem) { - char *mem; - memcpy(&mem, *gr_mem, sizeof(mem)); - *gr_mem = (char *)*gr_mem + sizeof(mem); - return mem; -} - -struct bfs_groups *bfs_groups_parse(void) { - int error; - - struct bfs_groups *groups = malloc(sizeof(*groups)); +struct bfs_groups *bfs_groups_new(void) { + struct bfs_groups *groups = ALLOC(struct bfs_groups); if (!groups) { return NULL; } - groups->entries = NULL; + VARENA_INIT(&groups->varena, struct bfs_group, buf); trie_init(&groups->by_name); trie_init(&groups->by_gid); + return groups; +} - setgrent(); - - while (true) { - errno = 0; - struct group *ent = getgrent(); - if (!ent) { - if (errno) { - error = errno; - goto fail_end; - } else { - break; - } - } - - if (DARRAY_PUSH(&groups->entries, ent) != 0) { - error = errno; - goto fail_end; - } - ent = groups->entries + darray_length(groups->entries) - 1; - - void *members = ent->gr_mem; - ent->gr_mem = NULL; - - ent->gr_name = strdup(ent->gr_name); - if (!ent->gr_name) { - error = errno; - goto fail_end; - } +/** bfs_getent() callback for getgrnam_r(). */ +static void *bfs_getgrnam_impl(const void *key, void *ptr, size_t bufsize) { + struct bfs_group *storage = ptr; - for (char *mem = next_gr_mem(&members); mem; mem = next_gr_mem(&members)) { - char *dup = strdup(mem); - if (!dup) { - error = errno; - goto fail_end; - } + struct group *ret = NULL; + errno = getgrnam_r(key, &storage->grp, storage->buf, bufsize, &ret); + return ret; +} - if (DARRAY_PUSH(&ent->gr_mem, &dup) != 0) { - error = errno; - free(dup); - goto fail_end; - } - } +const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name) { + struct trie_leaf *leaf = trie_insert_str(&groups->by_name, name); + if (!leaf) { + return NULL; } - endgrent(); - - for (size_t i = 0; i < darray_length(groups->entries); ++i) { - struct group *entry = &groups->entries[i]; - struct trie_leaf *leaf = trie_insert_str(&groups->by_name, entry->gr_name); - if (leaf) { - if (!leaf->value) { - leaf->value = entry; - } - } else { - error = errno; - goto fail_free; - } - - leaf = trie_insert_mem(&groups->by_gid, &entry->gr_gid, sizeof(entry->gr_gid)); - if (leaf) { - if (!leaf->value) { - leaf->value = entry; - } - } else { - error = errno; - goto fail_free; - } - } + return bfs_getent(bfs_getgrnam_impl, name, leaf, &groups->varena); +} - return groups; +/** bfs_getent() callback for getgrgid_r(). */ +static void *bfs_getgrgid_impl(const void *key, void *ptr, size_t bufsize) { + const gid_t *gid = key; + struct bfs_group *storage = ptr; -fail_end: - endgrent(); -fail_free: - bfs_groups_free(groups); - errno = error; - return NULL; + struct group *ret = NULL; + errno = getgrgid_r(*gid, &storage->grp, storage->buf, bufsize, &ret); + return ret; } -const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name) { - const struct trie_leaf *leaf = trie_find_str(&groups->by_name, name); - if (leaf) { - return leaf->value; - } else { +const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid) { + struct trie_leaf *leaf = trie_insert_mem(&groups->by_gid, &gid, sizeof(gid)); + if (!leaf) { return NULL; } + + return bfs_getent(bfs_getgrgid_impl, &gid, leaf, &groups->varena); } -const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid) { - const struct trie_leaf *leaf = trie_find_mem(&groups->by_gid, &gid, sizeof(gid)); - if (leaf) { - return leaf->value; - } else { - return NULL; - } +void bfs_groups_flush(struct bfs_groups *groups) { + trie_clear(&groups->by_gid); + trie_clear(&groups->by_name); + varena_clear(&groups->varena); } void bfs_groups_free(struct bfs_groups *groups) { if (groups) { trie_destroy(&groups->by_gid); trie_destroy(&groups->by_name); - - for (size_t i = 0; i < darray_length(groups->entries); ++i) { - struct group *entry = &groups->entries[i]; - for (size_t j = 0; j < darray_length(entry->gr_mem); ++j) { - free(entry->gr_mem[j]); - } - darray_free(entry->gr_mem); - free(entry->gr_name); - } - darray_free(groups->entries); - + varena_destroy(&groups->varena); free(groups); } } diff --git a/src/pwcache.h b/src/pwcache.h index f1a1db3..d7c602d 100644 --- a/src/pwcache.h +++ b/src/pwcache.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A caching wrapper for /etc/{passwd,group}. @@ -25,92 +12,112 @@ #include <pwd.h> /** - * The user table. + * A user cache. */ struct bfs_users; /** - * Parse the user table. + * Create a user cache. * * @return - * The parsed user table, or NULL on failure. + * A new user cache, or NULL on failure. */ -struct bfs_users *bfs_users_parse(void); +struct bfs_users *bfs_users_new(void); /** * Get a user entry by name. * - * @param users - * The user table. - * @param name + * @users + * The user cache. + * @name * The username to look up. * @return - * The matching user, or NULL if not found. + * The matching user, or NULL if not found (errno == 0) or an error + * occurred (errno != 0). */ -const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name); +const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name); /** * Get a user entry by ID. * - * @param users - * The user table. - * @param uid + * @users + * The user cache. + * @uid * The ID to look up. * @return - * The matching user, or NULL if not found. + * The matching user, or NULL if not found (errno == 0) or an error + * occurred (errno != 0). */ -const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid); +const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid); /** - * Free a user table. + * Flush a user cache. * - * @param users - * The user table to free. + * @users + * The cache to flush. + */ +void bfs_users_flush(struct bfs_users *users); + +/** + * Free a user cache. + * + * @users + * The user cache to free. */ void bfs_users_free(struct bfs_users *users); /** - * The group table. + * A group cache. */ struct bfs_groups; /** - * Parse the group table. + * Create a group cache. * * @return - * The parsed group table, or NULL on failure. + * A new group cache, or NULL on failure. */ -struct bfs_groups *bfs_groups_parse(void); +struct bfs_groups *bfs_groups_new(void); /** * Get a group entry by name. * - * @param groups - * The group table. - * @param name + * @groups + * The group cache. + * @name * The group name to look up. * @return - * The matching group, or NULL if not found. + * The matching group, or NULL if not found (errno == 0) or an error + * occurred (errno != 0). */ -const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name); +const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name); /** * Get a group entry by ID. * - * @param groups - * The group table. - * @param uid + * @groups + * The group cache. + * @uid * The ID to look up. * @return - * The matching group, or NULL if not found. + * The matching group, or NULL if not found (errno == 0) or an error + * occurred (errno != 0). + */ +const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid); + +/** + * Flush a group cache. + * + * @groups + * The cache to flush. */ -const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid); +void bfs_groups_flush(struct bfs_groups *groups); /** - * Free a group table. + * Free a group cache. * - * @param groups - * The group table to free. + * @groups + * The group cache to free. */ void bfs_groups_free(struct bfs_groups *groups); diff --git a/src/sanity.h b/src/sanity.h new file mode 100644 index 0000000..be77eef --- /dev/null +++ b/src/sanity.h @@ -0,0 +1,94 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Sanitizer interface. + */ + +#ifndef BFS_SANITY_H +#define BFS_SANITY_H + +#include <stddef.h> + +// Call macro(ptr, size) or macro(ptr, sizeof(*ptr)) +#define SANITIZE_CALL(...) \ + SANITIZE_CALL_(__VA_ARGS__, ) + +#define SANITIZE_CALL_(macro, ptr, ...) \ + SANITIZE_CALL__(macro, ptr, __VA_ARGS__ sizeof(*(ptr)), ) + +#define SANITIZE_CALL__(macro, ptr, size, ...) \ + macro(ptr, size) + +#if __SANITIZE_ADDRESS__ +# include <sanitizer/asan_interface.h> + +/** + * sanitize_alloc(ptr, size = sizeof(*ptr)) + * + * Mark a memory region as allocated. + */ +#define sanitize_alloc(...) SANITIZE_CALL(__asan_unpoison_memory_region, __VA_ARGS__) + +/** + * sanitize_free(ptr, size = sizeof(*ptr)) + * + * Mark a memory region as free. + */ +#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(...) ((void)0) +# define sanitize_free(...) ((void)0) +# define sanitize_resize(ptr, old, new, cap) ((void)0) +#endif + +#if __SANITIZE_MEMORY__ +# include <sanitizer/msan_interface.h> + +/** + * sanitize_init(ptr, size = sizeof(*ptr)) + * + * Mark a memory region as initialized. + */ +#define sanitize_init(...) SANITIZE_CALL(__msan_unpoison, __VA_ARGS__) + +/** + * sanitize_uninit(ptr, size = sizeof(*ptr)) + * + * Mark a memory region as uninitialized. + */ +#define sanitize_uninit(...) SANITIZE_CALL(__msan_allocated_memory, __VA_ARGS__) + +#else +# define sanitize_init(...) ((void)0) +# define sanitize_uninit(...) ((void)0) +#endif + +/** + * Initialize a variable, unless sanitizers would detect uninitialized uses. + */ +#if __SANITIZE_MEMORY__ +# define uninit(value) +#else +# define uninit(value) = value +#endif + +#endif // BFS_SANITY_H 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 @@ -1,62 +1,34 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "stat.h" -#include "util.h" -#include <assert.h> + +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "sanity.h" + #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <string.h> -#include <sys/types.h> #include <sys/stat.h> +#include <sys/types.h> -#if BFS_HAS_SYS_PARAM -# include <sys/param.h> -#endif - -#ifdef STATX_BASIC_STATS -# define BFS_LIBC_STATX true -#elif __linux__ -# include <linux/stat.h> -# include <sys/syscall.h> -# include <unistd.h> -#endif - -#if BFS_LIBC_STATX || defined(__NR_statx) -# define BFS_STATX true -#endif - -#if __APPLE__ -# define st_atim st_atimespec -# define st_ctim st_ctimespec -# define st_mtim st_mtimespec -# define st_birthtim st_birthtimespec +#if BFS_USE_STATX && !BFS_HAS_STATX +# include <linux/stat.h> +# include <sys/syscall.h> +# include <unistd.h> #endif const char *bfs_stat_field_name(enum bfs_stat_field field) { switch (field) { + case BFS_STAT_MODE: + return "mode"; case BFS_STAT_DEV: return "device number"; case BFS_STAT_INO: return "inode nunmber"; - case BFS_STAT_TYPE: - return "type"; - case BFS_STAT_MODE: - return "mode"; case BFS_STAT_NLINK: return "link count"; case BFS_STAT_GID: @@ -79,62 +51,85 @@ 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"; } - assert(!"Unrecognized stat field"); + bfs_bug("Unrecognized stat field %d", (int)field); return "???"; } -/** - * Convert a struct stat to a struct bfs_stat. - */ -static void bfs_stat_convert(const struct stat *statbuf, struct bfs_stat *buf) { - buf->mask = 0; +int bfs_fstatat_flags(enum bfs_stat_flags flags) { + int ret = 0; + + if (flags & BFS_STAT_NOFOLLOW) { + ret |= AT_SYMLINK_NOFOLLOW; + } + +#ifdef AT_NO_AUTOMOUNT + ret |= AT_NO_AUTOMOUNT; +#endif + + return ret; +} - buf->dev = statbuf->st_dev; - buf->mask |= BFS_STAT_DEV; +void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) { + dest->mask = 0; - buf->ino = statbuf->st_ino; - buf->mask |= BFS_STAT_INO; + dest->mode = src->st_mode; + dest->mask |= BFS_STAT_MODE; - buf->mode = statbuf->st_mode; - buf->mask |= BFS_STAT_TYPE | BFS_STAT_MODE; + dest->dev = src->st_dev; + dest->mask |= BFS_STAT_DEV; - buf->nlink = statbuf->st_nlink; - buf->mask |= BFS_STAT_NLINK; + dest->ino = src->st_ino; + dest->mask |= BFS_STAT_INO; - buf->gid = statbuf->st_gid; - buf->mask |= BFS_STAT_GID; + dest->nlink = src->st_nlink; + dest->mask |= BFS_STAT_NLINK; - buf->uid = statbuf->st_uid; - buf->mask |= BFS_STAT_UID; + dest->gid = src->st_gid; + dest->mask |= BFS_STAT_GID; - buf->size = statbuf->st_size; - buf->mask |= BFS_STAT_SIZE; + dest->uid = src->st_uid; + dest->mask |= BFS_STAT_UID; - buf->blocks = statbuf->st_blocks; - buf->mask |= BFS_STAT_BLOCKS; + dest->size = src->st_size; + dest->mask |= BFS_STAT_SIZE; - buf->rdev = statbuf->st_rdev; - buf->mask |= BFS_STAT_RDEV; + dest->blocks = src->st_blocks; + dest->mask |= BFS_STAT_BLOCKS; -#if BSD - buf->attrs = statbuf->st_flags; - buf->mask |= BFS_STAT_ATTRS; + dest->rdev = src->st_rdev; + dest->mask |= BFS_STAT_RDEV; + + // 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 - buf->atime = statbuf->st_atim; - buf->mask |= BFS_STAT_ATIME; + dest->atime = ST_ATIM(*src); + dest->mask |= BFS_STAT_ATIME; - buf->ctime = statbuf->st_ctim; - buf->mask |= BFS_STAT_CTIME; + dest->ctime = ST_CTIM(*src); + dest->mask |= BFS_STAT_CTIME; - buf->mtime = statbuf->st_mtim; - buf->mask |= BFS_STAT_MTIME; + dest->mtime = ST_MTIM(*src); + dest->mask |= BFS_STAT_MTIME; -#if __APPLE__ || __FreeBSD__ || __NetBSD__ - buf->btime = statbuf->st_birthtim; - buf->mask |= BFS_STAT_BTIME; +#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 } @@ -145,143 +140,164 @@ static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, struct bf struct stat statbuf; int ret = fstatat(at_fd, at_path, &statbuf, at_flags); if (ret == 0) { - bfs_stat_convert(&statbuf, buf); + bfs_stat_convert(buf, &statbuf); } return ret; } -#if BFS_STATX +#if BFS_USE_STATX /** * 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_FEATURE(memory_sanitizer, false) - // -fsanitize=memory doesn't know about statx(), so tell it the memory - // got initialized - memset(buf, 0, sizeof(*buf)); -#endif - -#if BFS_LIBC_STATX - return statx(at_fd, at_path, at_flags, mask, buf); +#if BFS_HAS_STATX + int ret = statx(at_fd, at_path, at_flags, mask, buf); #else - return syscall(__NR_statx, at_fd, at_path, at_flags, mask, buf); + int ret = syscall(SYS_statx, at_fd, at_path, at_flags, mask, buf); #endif + + if (ret == 0) { + // -fsanitize=memory doesn't know about statx() + sanitize_init(buf); + } + + return ret; } -/** - * 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; - struct statx xbuf; - int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); - if (ret != 0) { - return ret; +int bfs_statx_flags(enum bfs_stat_flags flags) { + int ret = bfs_fstatat_flags(flags); + + if (flags & BFS_STAT_NOSYNC) { + ret |= AT_STATX_DONT_SYNC; } + 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); - if ((xbuf.stx_mask & guaranteed) != guaranteed) { + const unsigned int guaranteed = STATX_BASIC_STATS & ~(STATX_ATIME | STATX_CTIME | STATX_MTIME); + if ((src->stx_mask & guaranteed) != guaranteed) { errno = ENOTSUP; return -1; } - buf->mask = 0; + dest->mask = 0; - buf->dev = bfs_makedev(xbuf.stx_dev_major, xbuf.stx_dev_minor); - buf->mask |= BFS_STAT_DEV; + dest->mode = src->stx_mode; + dest->mask |= BFS_STAT_MODE; - if (xbuf.stx_mask & STATX_INO) { - buf->ino = xbuf.stx_ino; - buf->mask |= BFS_STAT_INO; - } + dest->dev = xmakedev(src->stx_dev_major, src->stx_dev_minor); + dest->mask |= BFS_STAT_DEV; - buf->mode = xbuf.stx_mode; - if (xbuf.stx_mask & STATX_TYPE) { - buf->mask |= BFS_STAT_TYPE; - } - if (xbuf.stx_mask & STATX_MODE) { - buf->mask |= BFS_STAT_MODE; - } + dest->ino = src->stx_ino; + dest->mask |= BFS_STAT_INO; - if (xbuf.stx_mask & STATX_NLINK) { - buf->nlink = xbuf.stx_nlink; - buf->mask |= BFS_STAT_NLINK; - } + dest->nlink = src->stx_nlink; + dest->mask |= BFS_STAT_NLINK; - if (xbuf.stx_mask & STATX_GID) { - buf->gid = xbuf.stx_gid; - buf->mask |= BFS_STAT_GID; - } + dest->gid = src->stx_gid; + dest->mask |= BFS_STAT_GID; - if (xbuf.stx_mask & STATX_UID) { - buf->uid = xbuf.stx_uid; - buf->mask |= BFS_STAT_UID; - } + dest->uid = src->stx_uid; + dest->mask |= BFS_STAT_UID; - if (xbuf.stx_mask & STATX_SIZE) { - buf->size = xbuf.stx_size; - buf->mask |= BFS_STAT_SIZE; - } + dest->size = src->stx_size; + dest->mask |= BFS_STAT_SIZE; - if (xbuf.stx_mask & STATX_BLOCKS) { - buf->blocks = xbuf.stx_blocks; - buf->mask |= BFS_STAT_BLOCKS; - } + dest->blocks = src->stx_blocks; + dest->mask |= BFS_STAT_BLOCKS; - buf->rdev = bfs_makedev(xbuf.stx_rdev_major, xbuf.stx_rdev_minor); - buf->mask |= BFS_STAT_RDEV; + dest->rdev = xmakedev(src->stx_rdev_major, src->stx_rdev_minor); + dest->mask |= BFS_STAT_RDEV; - buf->attrs = xbuf.stx_attributes; - buf->mask |= BFS_STAT_ATTRS; + dest->attrs = src->stx_attributes; + dest->mask |= BFS_STAT_ATTRS; - if (xbuf.stx_mask & STATX_ATIME) { - buf->atime.tv_sec = xbuf.stx_atime.tv_sec; - buf->atime.tv_nsec = xbuf.stx_atime.tv_nsec; - buf->mask |= BFS_STAT_ATIME; + 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 (xbuf.stx_mask & STATX_BTIME) { - buf->btime.tv_sec = xbuf.stx_btime.tv_sec; - buf->btime.tv_nsec = xbuf.stx_btime.tv_nsec; - buf->mask |= BFS_STAT_BTIME; + if (src->stx_mask & STATX_ATIME) { + dest->atime.tv_sec = src->stx_atime.tv_sec; + dest->atime.tv_nsec = src->stx_atime.tv_nsec; + dest->mask |= BFS_STAT_ATIME; } - if (xbuf.stx_mask & STATX_CTIME) { - buf->ctime.tv_sec = xbuf.stx_ctime.tv_sec; - buf->ctime.tv_nsec = xbuf.stx_ctime.tv_nsec; - buf->mask |= BFS_STAT_CTIME; + if (src->stx_mask & STATX_BTIME) { + dest->btime.tv_sec = src->stx_btime.tv_sec; + dest->btime.tv_nsec = src->stx_btime.tv_nsec; + dest->mask |= BFS_STAT_BTIME; } - if (xbuf.stx_mask & STATX_MTIME) { - buf->mtime.tv_sec = xbuf.stx_mtime.tv_sec; - buf->mtime.tv_nsec = xbuf.stx_mtime.tv_nsec; - buf->mask |= BFS_STAT_MTIME; + if (src->stx_mask & STATX_CTIME) { + dest->ctime.tv_sec = src->stx_ctime.tv_sec; + dest->ctime.tv_nsec = src->stx_ctime.tv_nsec; + dest->mask |= BFS_STAT_CTIME; } - return ret; + if (src->stx_mask & STATX_MTIME) { + dest->mtime.tv_sec = src->stx_mtime.tv_sec; + dest->mtime.tv_nsec = src->stx_mtime.tv_nsec; + dest->mask |= BFS_STAT_MTIME; + } + + return 0; +} + +/** + * 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 = bfs_statx_mask(); + struct statx xbuf; + int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); + if (ret != 0) { + return ret; + } + + return bfs_statx_convert(buf, &xbuf); } -#endif // BFS_STATX +#endif // BFS_USE_STATX /** * Calls the stat() implementation with explicit flags. */ -static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, int x_flags, struct bfs_stat *buf) { -#if BFS_STATX - static bool has_statx = true; - - if (has_statx) { - int ret = bfs_statx_impl(at_fd, at_path, at_flags | x_flags, buf); - // EPERM is commonly returned in a seccomp() sandbox that does - // not allow statx() - if (ret != 0 && (errno == ENOSYS || errno == EPERM)) { - has_statx = false; +static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) { +#if BFS_USE_STATX + static atomic bool has_statx = true; + + if (load(&has_statx, relaxed)) { + int ret = bfs_statx_impl(at_fd, at_path, at_flags, buf); + if (ret != 0 && errno_is_like(ENOSYS)) { + store(&has_statx, false, relaxed); } else { return ret; } } + + at_flags &= ~AT_STATX_DONT_SYNC; #endif return bfs_stat_impl(at_fd, at_path, at_flags, buf); @@ -290,62 +306,46 @@ static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, int x /** * Implements the BFS_STAT_TRYFOLLOW retry logic. */ -static int bfs_stat_tryfollow(int at_fd, const char *at_path, int at_flags, int x_flags, enum bfs_stat_flags bfs_flags, struct bfs_stat *buf) { - int ret = bfs_stat_explicit(at_fd, at_path, at_flags, x_flags, buf); +static int bfs_stat_tryfollow(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags bfs_flags, struct bfs_stat *buf) { + int ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf); if (ret != 0 && (bfs_flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW - && is_nonexistence_error(errno)) + && errno_is_like(ENOENT)) { at_flags |= AT_SYMLINK_NOFOLLOW; - ret = bfs_stat_explicit(at_fd, at_path, at_flags, x_flags, buf); + ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf); } return ret; } int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf) { - int at_flags = 0; - if (flags & BFS_STAT_NOFOLLOW) { - at_flags |= AT_SYMLINK_NOFOLLOW; - } - -#if defined(AT_NO_AUTOMOUNT) && (!__GNU__ || BFS_GLIBC_PREREQ(2, 35)) - at_flags |= AT_NO_AUTOMOUNT; -#endif - - int x_flags = 0; -#ifdef AT_STATX_DONT_SYNC - if (flags & BFS_STAT_NOSYNC) { - x_flags |= AT_STATX_DONT_SYNC; - } +#if BFS_USE_STATX + int at_flags = bfs_statx_flags(flags); +#else + int at_flags = bfs_fstatat_flags(flags); #endif if (at_path) { - return bfs_stat_tryfollow(at_fd, at_path, at_flags, x_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 bool has_at_ep = true; - if (has_at_ep) { - at_flags |= AT_EMPTY_PATH; - int ret = bfs_stat_explicit(at_fd, "", at_flags, x_flags, buf); - if (ret != 0 && errno == EINVAL) { - has_at_ep = false; - } else { - return ret; - } + return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf); } -#endif - struct stat statbuf; - if (fstat(at_fd, &statbuf) == 0) { - bfs_stat_convert(&statbuf, buf); - 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) { @@ -364,7 +364,7 @@ const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_f case BFS_STAT_MTIME: return &buf->mtime; default: - assert(!"Invalid stat field for time"); + bfs_bug("Invalid stat field for time"); errno = EINVAL; return NULL; } @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2018-2019 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A facade over the stat() API that unifies some details that diverge between @@ -25,33 +12,52 @@ #ifndef BFS_STAT_H #define BFS_STAT_H -#include "util.h" +#include "bfs.h" + +#include <stdint.h> +#include <sys/stat.h> #include <sys/types.h> #include <time.h> -#if BFS_HAS_SYS_PARAM -# include <sys/param.h> +#if !BFS_HAS_STATX && BFS_HAS_STATX_SYSCALL +# include <linux/stat.h> +#endif + +#ifndef BFS_USE_STATX +# define BFS_USE_STATX (BFS_HAS_STATX || BFS_HAS_STATX_SYSCALL) +#endif + +#if __has_include(<sys/param.h>) +# include <sys/param.h> +#endif + +#ifdef DEV_BSIZE +# define BFS_STAT_BLKSIZE DEV_BSIZE +#elif defined(S_BLKSIZE) +# define BFS_STAT_BLKSIZE S_BLKSIZE +#else +# define BFS_STAT_BLKSIZE 512 #endif /** * bfs_stat field bitmask. */ enum bfs_stat_field { - BFS_STAT_DEV = 1 << 0, - BFS_STAT_INO = 1 << 1, - BFS_STAT_TYPE = 1 << 2, - BFS_STAT_MODE = 1 << 3, - BFS_STAT_NLINK = 1 << 4, - BFS_STAT_GID = 1 << 5, - BFS_STAT_UID = 1 << 6, - BFS_STAT_SIZE = 1 << 7, - BFS_STAT_BLOCKS = 1 << 8, - BFS_STAT_RDEV = 1 << 9, - BFS_STAT_ATTRS = 1 << 10, - BFS_STAT_ATIME = 1 << 11, - BFS_STAT_BTIME = 1 << 12, - BFS_STAT_CTIME = 1 << 13, - BFS_STAT_MTIME = 1 << 14, + BFS_STAT_MODE = 1 << 0, + BFS_STAT_DEV = 1 << 1, + BFS_STAT_INO = 1 << 2, + BFS_STAT_NLINK = 1 << 3, + BFS_STAT_GID = 1 << 4, + BFS_STAT_UID = 1 << 5, + BFS_STAT_SIZE = 1 << 6, + BFS_STAT_BLOCKS = 1 << 7, + BFS_STAT_RDEV = 1 << 8, + BFS_STAT_ATTRS = 1 << 9, + BFS_STAT_ATIME = 1 << 10, + BFS_STAT_BTIME = 1 << 11, + BFS_STAT_CTIME = 1 << 12, + BFS_STAT_MTIME = 1 << 13, + BFS_STAT_MNT_ID = 1 << 14, }; /** @@ -73,14 +79,6 @@ enum bfs_stat_flags { BFS_STAT_NOSYNC = 1 << 2, }; -#ifdef DEV_BSIZE -# define BFS_STAT_BLKSIZE DEV_BSIZE -#elif defined(S_BLKSIZE) -# define BFS_STAT_BLKSIZE S_BLKSIZE -#else -# define BFS_STAT_BLKSIZE 512 -#endif - /** * Facade over struct stat. */ @@ -88,12 +86,12 @@ struct bfs_stat { /** Bitmask indicating filled fields. */ enum bfs_stat_field mask; + /** File type and access mode. */ + mode_t mode; /** Device ID containing the file. */ dev_t dev; /** Inode number. */ ino_t ino; - /** File type and access mode. */ - mode_t mode; /** Number of hard links. */ nlink_t nlink; /** Owner group ID. */ @@ -106,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; @@ -123,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. @@ -138,6 +138,33 @@ struct bfs_stat { int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf); /** + * Convert bfs_stat_flags to fstatat() flags. + */ +int bfs_fstatat_flags(enum bfs_stat_flags flags); + +/** + * Convert struct stat to struct bfs_stat. + */ +void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src); + +#if BFS_USE_STATX +/** + * Convert bfs_stat_flags to statx() flags. + */ +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); +#endif + +/** * Get a particular time field from a bfs_stat() buffer. */ const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field); diff --git a/src/thread.c b/src/thread.c new file mode 100644 index 0000000..b3604f8 --- /dev/null +++ b/src/thread.c @@ -0,0 +1,94 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "thread.h" + +#include "bfstd.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; \ + if (err == 0) { \ + return 0; \ + } else { \ + errno = err; \ + return -1; \ + } \ + } while (0) + +#define THREAD_INFALLIBLE(...) \ + THREAD_INFALLIBLE_(__VA_ARGS__, 0, ) + +#define THREAD_INFALLIBLE_(expr, allowed, ...) \ + int err = expr; \ + bfs_verify(err == 0 || err == allowed, "%s: %s", #expr, xstrerror(err)); \ + (void)0 + +int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg) { + 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)); +} + +int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) { + THREAD_FALLIBLE(pthread_mutex_init(mutex, attr)); +} + +void mutex_lock(pthread_mutex_t *mutex) { + THREAD_INFALLIBLE(pthread_mutex_lock(mutex)); +} + +bool mutex_trylock(pthread_mutex_t *mutex) { + THREAD_INFALLIBLE(pthread_mutex_trylock(mutex), EBUSY); + return err == 0; +} + +void mutex_unlock(pthread_mutex_t *mutex) { + THREAD_INFALLIBLE(pthread_mutex_unlock(mutex)); +} + +void mutex_destroy(pthread_mutex_t *mutex) { + THREAD_INFALLIBLE(pthread_mutex_destroy(mutex)); +} + +int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) { + THREAD_FALLIBLE(pthread_cond_init(cond, attr)); +} + +void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { + THREAD_INFALLIBLE(pthread_cond_wait(cond, mutex)); +} + +void cond_signal(pthread_cond_t *cond) { + THREAD_INFALLIBLE(pthread_cond_signal(cond)); +} + +void cond_broadcast(pthread_cond_t *cond) { + THREAD_INFALLIBLE(pthread_cond_broadcast(cond)); +} + +void cond_destroy(pthread_cond_t *cond) { + THREAD_INFALLIBLE(pthread_cond_destroy(cond)); +} + +void invoke_once(pthread_once_t *once, once_fn *fn) { + THREAD_INFALLIBLE(pthread_once(once, fn)); +} diff --git a/src/thread.h b/src/thread.h new file mode 100644 index 0000000..3dd8422 --- /dev/null +++ b/src/thread.h @@ -0,0 +1,95 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Wrappers for POSIX threading APIs. + */ + +#ifndef BFS_THREAD_H +#define BFS_THREAD_H + +#include <pthread.h> + +/** Thread entry point type. */ +typedef void *thread_fn(void *arg); + +/** + * Wrapper for pthread_create(). + * + * @return + * 0 on success, -1 on error. + */ +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); + +/** + * Wrapper for pthread_mutex_init(). + */ +int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); + +/** + * Wrapper for pthread_mutex_lock(). + */ +void mutex_lock(pthread_mutex_t *mutex); + +/** + * Wrapper for pthread_mutex_trylock(). + * + * @return + * Whether the mutex was locked. + */ +bool mutex_trylock(pthread_mutex_t *mutex); + +/** + * Wrapper for pthread_mutex_unlock(). + */ +void mutex_unlock(pthread_mutex_t *mutex); + +/** + * Wrapper for pthread_mutex_destroy(). + */ +void mutex_destroy(pthread_mutex_t *mutex); + +/** + * Wrapper for pthread_cond_init(). + */ +int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); + +/** + * Wrapper for pthread_cond_wait(). + */ +void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); + +/** + * Wrapper for pthread_cond_signal(). + */ +void cond_signal(pthread_cond_t *cond); + +/** + * Wrapper for pthread_cond_broadcast(). + */ +void cond_broadcast(pthread_cond_t *cond); + +/** + * Wrapper for pthread_cond_destroy(). + */ +void cond_destroy(pthread_cond_t *cond); + +/** pthread_once() callback type. */ +typedef void once_fn(void); + +/** + * Wrapper for pthread_once(). + */ +void invoke_once(pthread_once_t *once, once_fn *fn); + +#endif // BFS_THREAD_H @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * This is an implementation of a "qp trie," as documented at @@ -22,29 +9,29 @@ * look like * * A A A A - * *--->*--->*--->*--->$ - * | | | D D - * | | +--->*--->$ - * | | B C D - * | +--->*--->*--->$ - * | D D A A - * +--->*--->*--->*--->$ - * | D D - * +--->*--->$ + * ●───→●───→●───→●───→○ + * │ │ │ D D + * │ │ └───→●───→○ + * │ │ B C D + * │ └───→●───→●───→○ + * │ D D A A + * └───→●───→●───→●───→○ + * │ D D + * └───→●───→○ * * A compressed (PATRICIA) trie collapses internal nodes that have only a single * child, like this: * * A A AA - * *--->*--->*---->$ - * | | | DD - * | | +---->$ - * | | BCD - * | +----->$ - * | DD AA - * +---->*---->$ - * | DD - * +---->$ + * ●───→●───→●────→○ + * │ │ │ DD + * │ │ └────→○ + * │ │ BCD + * │ └─────→○ + * │ DD AA + * └────→●────→○ + * │ DD + * └────→○ * * The nodes can be compressed further by dropping the actual compressed * sequences from the nodes, storing it only in the leaves. This is the @@ -53,21 +40,39 @@ * branch on, need to be stored in each node. * * A A A - * 0--->1--->2--->AAAA - * | | | D - * | | +--->AADD - * | | B - * | +--->ABCD - * | D A - * +--->2--->DDAA - * | D - * +--->DDDD + * 0───→1───→2───→AAAA + * │ │ │ D + * │ │ └───→AADD + * │ │ B + * │ └───→ABCD + * │ D A + * └───→2───→DDAA + * │ D + * └───→DDDD * * Nodes are represented very compactly. Rather than a dense array of children, * a sparse array of only the non-NULL children directly follows the node in - * memory. A bitmap is used to track which children exist; the index of a child - * i is found by counting the number of bits below bit i that are set. A tag - * bit is used to tell pointers to internal nodes apart from pointers to leaves. + * memory. A bitmap is used to track which children exist. + * + * ┌────────────┐ + * │ [4] [3] [2][1][0] ←─ children + * │ ↓ ↓ ↓ ↓ ↓ + * │ 14 10 6 3 0 ←─ sparse index + * │ ↓ ↓ ↓ ↓ ↓ + * │ 0100010001001001 ←─ bitmap + * │ + * │ To convert a sparse index to a dense index, mask off the bits above it, and + * │ count the remaining bits. + * │ + * │ 10 ←─ sparse index + * │ ↓ + * │ 0000001111111111 ←─ mask + * │ & 0100010001001001 ←─ bitmap + * │ ──────────────── + * │ = 0000000001001001 + * │ └──┼──┘ + * │ [3] ←─ dense index + * └───────────────────┘ * * This implementation tests a whole nibble (half byte/hex digit) at every * branch, so the bitmap takes up 16 bits. The remainder of a machine word is @@ -77,24 +82,30 @@ */ #include "trie.h" -#include "util.h" -#include <assert.h> -#include <limits.h> -#include <stdbool.h> + +#include "alloc.h" +#include "bfs.h" +#include "bit.h" +#include "diag.h" +#include "list.h" + #include <stdint.h> -#include <stdlib.h> #include <string.h> -#if CHAR_BIT != 8 -# error "This trie implementation assumes 8-bit bytes." +static_assert(CHAR_WIDTH == 8, "This trie implementation assumes 8-bit bytes."); + +#if __i386__ || __x86_64__ +# define _trie_clones _target_clones("popcnt", "default") +#else +# define _trie_clones #endif /** Number of bits for the sparse array bitmap, aka the range of a nibble. */ -#define BITMAP_BITS 16 +#define BITMAP_WIDTH 16 /** The number of remaining bits in a word, to hold the offset. */ -#define OFFSET_BITS (sizeof(size_t)*CHAR_BIT - BITMAP_BITS) +#define OFFSET_WIDTH (SIZE_WIDTH - BITMAP_WIDTH) /** The highest representable offset (only 64k on a 32-bit architecture). */ -#define OFFSET_MAX (((size_t)1 << OFFSET_BITS) - 1) +#define OFFSET_MAX (((size_t)1 << OFFSET_WIDTH) - 1) /** * An internal node of the trie. @@ -105,88 +116,87 @@ struct trie_node { * Bit i will be set if a child exists at logical index i, and its index * into the array will be popcount(bitmap & ((1 << i) - 1)). */ - size_t bitmap : BITMAP_BITS; + size_t bitmap : BITMAP_WIDTH; /** * The offset into the key in nibbles. This is relative to the parent * node, to support offsets larger than OFFSET_MAX. */ - size_t offset : OFFSET_BITS; + size_t offset : OFFSET_WIDTH; /** * Flexible array of children. Each pointer uses the lowest bit as a * 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) { - 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; - 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) { - 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; - 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; } void trie_init(struct trie *trie) { trie->root = 0; -} - -/** Compute the popcount (Hamming weight) of a bitmap. */ -static unsigned int trie_popcount(unsigned int n) { -#if __POPCNT__ - // Use the x86 instruction if we have it. Otherwise, GCC generates a - // library call, so use the below implementation instead. - return __builtin_popcount(n); -#else - // See https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation - n -= (n >> 1) & 0x5555; - n = (n & 0x3333) + ((n >> 2) & 0x3333); - n = (n + (n >> 4)) & 0x0F0F; - n = (n + (n >> 8)) & 0xFF; - return n; -#endif + LIST_INIT(trie); + VARENA_INIT(&trie->nodes, struct trie_node, children); + VARENA_INIT(&trie->leaves, struct trie_leaf, key); } /** 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 @@ -194,24 +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 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; - if (node->bitmap & bit) { - index = trie_popcount(node->bitmap & (bit - 1)); - } + 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]; } @@ -219,15 +229,12 @@ static struct trie_leaf *trie_representative(const struct trie *trie, const void return trie_decode_leaf(ptr); } -struct trie_leaf *trie_first_leaf(const struct trie *trie) { - return trie_representative(trie, NULL, 0); -} - 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; @@ -236,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) { @@ -246,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. */ @@ -257,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); } } @@ -276,7 +302,8 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k } } -struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { +_trie_clones +static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) { uintptr_t ptr = trie->root; if (!ptr) { return NULL; @@ -286,24 +313,24 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { 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 = trie_popcount(node->bitmap & (bit - 1)); + unsigned int index = count_ones(node->bitmap & (bit - 1)); ptr = node->children[index]; } else { return best; @@ -318,55 +345,97 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { return best; } +struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { + return trie_find_prefix_impl(trie, key); +} + /** Create a new leaf, holding a copy of the given key. */ -static struct trie_leaf *new_trie_leaf(const void *key, size_t length) { - struct trie_leaf *leaf = malloc(BFS_FLEX_SIZEOF(struct trie_leaf, key, length)); - if (leaf) { - leaf->value = NULL; - leaf->length = length; - memcpy(leaf->key, key, length); +static struct trie_leaf *trie_leaf_alloc(struct trie *trie, const void *key, size_t length) { + struct trie_leaf *leaf = varena_alloc(&trie->leaves, length); + if (!leaf) { + return NULL; } + + LIST_ITEM_INIT(leaf); + LIST_APPEND(trie, leaf); + + leaf->value = NULL; + leaf->length = length; + memcpy(leaf->key, key, length); + return leaf; } -/** Compute the size of a trie node with a certain number of children. */ -static size_t trie_node_size(unsigned int size) { - // Empty nodes aren't supported - assert(size > 0); - // Node size must be a power of two - assert((size & (size - 1)) == 0); +/** Free a leaf. */ +static void trie_leaf_free(struct trie *trie, struct trie_leaf *leaf) { + LIST_REMOVE(trie, leaf); + varena_free(&trie->leaves, leaf, leaf->length); +} + +/** Create a new node. */ +static struct trie_node *trie_node_alloc(struct trie *trie, size_t size) { + bfs_assert(has_single_bit(size)); + return varena_alloc(&trie->nodes, size); +} + +/** Reallocate a trie node. */ +static struct trie_node *trie_node_realloc(struct trie *trie, struct trie_node *node, size_t old_size, size_t new_size) { + bfs_assert(has_single_bit(old_size)); + bfs_assert(has_single_bit(new_size)); + return varena_realloc(&trie->nodes, node, old_size, new_size); +} - return BFS_FLEX_SIZEOF(struct trie_node, children, size); +/** Free a node. */ +static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) { + bfs_assert(size == trie_node_size(node)); + varena_free(&trie->nodes, node, size); } /** Find the offset of the first nibble that differs between two keys. */ -static size_t trie_key_mismatch(const void *key1, const void *key2, size_t length) { - const unsigned char *bytes1 = key1; - const unsigned char *bytes2 = key2; - size_t i = 0; - size_t offset = 0; - const size_t chunk = sizeof(size_t); +static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t length) { + if (!rep) { + return 0; + } - for (; i + chunk <= length; i += chunk) { - if (memcmp(bytes1 + i, bytes2 + i, chunk) != 0) { - break; - } + if (rep->length < length) { + length = rep->length; } - for (; i < length; ++i) { - unsigned char b1 = bytes1[i], b2 = bytes2[i]; - if (b1 != b2) { - offset = (b1 & 0xF) == (b2 & 0xF); - break; - } + const char *rep_bytes = rep->key; + const char *key_bytes = key; + + 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); \ } - offset |= i << 1; - return offset; +#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 ret; } /** - * Insert a key into a node. The node must not have a child in that position + * Insert a leaf into a node. The node must not have a child in that position * already. Effectively takes a subtrie like this: * * ptr @@ -383,41 +452,36 @@ static size_t trie_key_mismatch(const void *key1, const void *key2, size_t lengt * v X * *--->... * | Y - * +--->key + * +--->leaf * | Z * +--->... */ -static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_t length, size_t offset) { +_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 = trie_popcount(node->bitmap); + unsigned int size = trie_node_size(node); // Double the capacity every power of two - if ((size & (size - 1)) == 0) { - node = realloc(node, trie_node_size(2*size)); + if (has_single_bit(size)) { + node = trie_node_realloc(trie, node, size, 2 * size); if (!node) { + trie_leaf_free(trie, leaf); return NULL; } *ptr = trie_encode_node(node); } - struct trie_leaf *leaf = new_trie_leaf(key, length); - if (!leaf) { - return NULL; - } - - unsigned char nibble = trie_key_nibble(key, offset); unsigned int bit = 1U << nibble; // The child must not already be present - assert(!(node->bitmap & bit)); + bfs_assert(!(node->bitmap & bit)); node->bitmap |= bit; - unsigned int index = trie_popcount(node->bitmap & (bit - 1)); - uintptr_t *child = &node->children[index]; - if (index < size) { - memmove(child + 1, child, (size - index)*sizeof(*child)); + unsigned int target = count_ones(node->bitmap & (bit - 1)); + for (size_t i = size; i > target; --i) { + node->children[i] = node->children[i - 1]; } - *child = trie_encode_leaf(leaf); + node->children[target] = trie_encode_leaf(leaf); return leaf; } @@ -446,12 +510,12 @@ static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_ * | Y * +--->key */ -static uintptr_t *trie_jump(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 - assert(trie_is_leaf(*ptr)); + struct trie_leaf *leaf = trie_decode_leaf(*ptr); - struct trie_node *node = malloc(trie_node_size(1)); + struct trie_node *node = trie_node_alloc(trie, 1); if (!node) { return NULL; } @@ -459,7 +523,7 @@ static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) { *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; @@ -482,28 +546,23 @@ static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) { * v X * *--->*...>--->rep * | Y - * +--->key + * +--->leaf */ -static struct trie_leaf *trie_split(uintptr_t *ptr, const void *key, size_t length, struct trie_leaf *rep, size_t offset, size_t mismatch) { - unsigned char key_nibble = trie_key_nibble(key, mismatch); - unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch); - assert(key_nibble != rep_nibble); +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_leaf_nibble(leaf, mismatch); + unsigned char rep_nibble = trie_leaf_nibble(rep, mismatch); + bfs_assert(key_nibble != rep_nibble); - struct trie_node *node = malloc(trie_node_size(2)); + struct trie_node *node = trie_node_alloc(trie, 2); if (!node) { - return NULL; - } - - struct trie_leaf *leaf = new_trie_leaf(key, length); - if (!leaf) { - free(node); + trie_leaf_free(trie, leaf); return NULL; } 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; } @@ -520,66 +579,99 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) { return trie_insert_mem(trie, key, strlen(key) + 1); } -struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) { +_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); - if (!rep) { - struct trie_leaf *leaf = new_trie_leaf(key, length); - if (leaf) { - trie->root = trie_encode_leaf(leaf); - } - return leaf; + size_t mismatch = trie_mismatch(rep, key, length); + 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; } - size_t limit = length < rep->length ? length : rep->length; - size_t mismatch = trie_key_mismatch(key, rep->key, limit); - if ((mismatch >> 1) >= length) { - return rep; + struct trie_leaf *leaf = trie_leaf_alloc(trie, key, length); + if (!leaf) { + return NULL; + } + + if (!rep) { + trie->root = trie_encode_leaf(leaf); + return leaf; } 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) { - assert(offset < mismatch); - unsigned int index = trie_popcount(node->bitmap & (bit - 1)); + bfs_assert(offset < mismatch); + unsigned int index = count_ones(node->bitmap & (bit - 1)); ptr = &node->children[index]; } else { - assert(offset == mismatch); - return trie_node_insert(ptr, key, length, offset); + bfs_assert(offset == mismatch); + return trie_node_insert(trie, ptr, leaf, nibble); } } while (mismatch - offset > OFFSET_MAX) { - ptr = trie_jump(ptr, key, &offset); + ptr = trie_jump(trie, ptr, &offset); if (!ptr) { + trie_leaf_free(trie, leaf); return NULL; } } - return trie_split(ptr, key, length, rep, offset, mismatch); + return trie_split(trie, ptr, leaf, rep, offset, mismatch); +} + +struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) { + 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(uintptr_t ptr) { - while (!trie_is_leaf(ptr)) { +static void trie_free_singletons(struct trie *trie, uintptr_t 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 - assert((node->bitmap & (node->bitmap - 1)) == 0); + bfs_assert(has_single_bit((size_t)node->bitmap)); ptr = node->children[0]; - free(node); + trie_node_free(trie, node, 1); } - free(trie_decode_leaf(ptr)); + trie_leaf_free(trie, trie_decode_leaf(ptr)); } /** @@ -599,9 +691,9 @@ static void trie_free_singletons(uintptr_t ptr) { * v * other */ -static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) { +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; @@ -611,28 +703,28 @@ static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, } *parent = other; - free(parent_node); + trie_node_free(trie, parent_node, 2); return 0; } -void trie_remove(struct trie *trie, struct trie_leaf *leaf) { +_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; - 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; - assert(bitmap & bit); - unsigned int index = trie_popcount(bitmap & (bit - 1)); + bfs_assert(bitmap & bit); + unsigned int index = count_ones(bitmap & (bit - 1)); // Advance the parent pointer, unless this node had only one child - if (bitmap & (bitmap - 1)) { + if (!has_single_bit(bitmap)) { parent = child; child_bit = bit; child_index = index; @@ -641,53 +733,50 @@ void trie_remove(struct trie *trie, struct trie_leaf *leaf) { child = &node->children[index]; } - assert(trie_decode_leaf(*child) == leaf); + bfs_assert(trie_decode_leaf(*child) == leaf); if (!parent) { - trie_free_singletons(trie->root); + trie_free_singletons(trie, trie->root); trie->root = 0; return; } struct trie_node *node = trie_decode_node(*parent); - child = node->children + child_index; - trie_free_singletons(*child); + trie_free_singletons(trie, node->children[child_index]); - node->bitmap ^= child_bit; - unsigned int parent_size = trie_popcount(node->bitmap); - assert(parent_size > 0); - if (parent_size == 1 && trie_collapse_node(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 ((parent_size & (parent_size - 1)) == 0) { - node = realloc(node, trie_node_size(parent_size)); + if (has_single_bit(parent_size)) { + node = trie_node_realloc(trie, node, 2 * parent_size, parent_size); if (node) { *parent = trie_encode_node(node); } } } -/** Free an encoded pointer to a node. */ -static void free_trie_ptr(uintptr_t ptr) { - if (trie_is_leaf(ptr)) { - free(trie_decode_leaf(ptr)); - } else { - struct trie_node *node = trie_decode_node(ptr); - size_t size = trie_popcount(node->bitmap); - for (size_t i = 0; i < size; ++i) { - free_trie_ptr(node->children[i]); - } - free(node); - } +void trie_remove(struct trie *trie, struct trie_leaf *leaf) { + trie_remove_impl(trie, leaf); +} + +void trie_clear(struct trie *trie) { + trie->root = 0; + LIST_INIT(trie); + + varena_clear(&trie->leaves); + varena_clear(&trie->nodes); } void trie_destroy(struct trie *trie) { - if (trie->root) { - free_trie_ptr(trie->root); - } + varena_destroy(&trie->leaves); + varena_destroy(&trie->nodes); } @@ -1,50 +1,41 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #ifndef BFS_TRIE_H #define BFS_TRIE_H +#include "alloc.h" +#include "list.h" + #include <stddef.h> #include <stdint.h> /** - * A trie that holds a set of fixed- or variable-length strings. - */ -struct trie { - uintptr_t root; -}; - -/** * A leaf of a trie. */ struct trie_leaf { - /** - * An arbitrary value associated with this leaf. - */ + /** Linked list of leaves, in insertion order. */ + struct trie_leaf *prev, *next; + /** An arbitrary value associated with this leaf. */ void *value; - - /** - * The length of the key in bytes. - */ + /** The length of the key in bytes. */ size_t length; + /** The key itself, stored inline. */ + char key[] _counted_by(length); +}; - /** - * The key itself, stored inline. - */ - char key[]; +/** + * A trie that holds a set of fixed- or variable-length strings. + */ +struct trie { + /** Pointer to the root node/leaf. */ + uintptr_t root; + /** Linked list of leaves. */ + struct trie_leaf *head, *tail; + /** Node allocator. */ + struct varena nodes; + /** Leaf allocator. */ + struct varena leaves; }; /** @@ -53,47 +44,63 @@ struct trie_leaf { void trie_init(struct trie *trie); /** - * Get the first (lexicographically earliest) leaf in the trie. + * Find the leaf for a string key. * - * @param trie + * @trie * The trie to search. + * @key + * The key to look up. * @return - * The first leaf, or NULL if the trie is empty. + * The found leaf, or NULL if the key is not present. */ -struct trie_leaf *trie_first_leaf(const struct trie *trie); +struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); /** - * Find the leaf for a string key. + * Find the leaf for a fixed-size key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. + * @length + * The length of the key in bytes. * @return * The found leaf, or NULL if the key is not present. */ -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); /** - * Find the leaf for a fixed-size key. + * Get the value associated with a string key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. - * @param length + * @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 leaf, or NULL if the key is not present. + * The found value, or NULL if the key is not present. */ -struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length); +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. @@ -103,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. @@ -115,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. @@ -127,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. @@ -139,18 +146,59 @@ 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); /** + * Remove all leaves from a trie. + */ +void trie_clear(struct trie *trie); + +/** * Destroy a trie and its contents. */ void trie_destroy(struct trie *trie); +/** + * Iterate over the leaves of a trie. + */ +#define for_trie(leaf, trie) \ + for_list (struct trie_leaf, leaf, trie) + #endif // BFS_TRIE_H @@ -1,26 +1,15 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "typo.h" + #include <limits.h> +#include <stdint.h> #include <stdlib.h> #include <string.h> // Assume QWERTY layout for now -static const int key_coords[UCHAR_MAX][3] = { +static const int8_t key_coords[UCHAR_MAX + 1][3] = { ['`'] = { 0, 0, 0}, ['~'] = { 0, 0, 1}, ['1'] = { 3, 0, 0}, @@ -125,7 +114,7 @@ static const int key_coords[UCHAR_MAX][3] = { }; static int char_distance(char a, char b) { - const int *ac = key_coords[(unsigned char)a], *bc = key_coords[(unsigned char)b]; + const int8_t *ac = key_coords[(unsigned char)a], *bc = key_coords[(unsigned char)b]; int ret = 0; for (int i = 0; i < 3; ++i) { ret += abs(ac[i] - bc[i]); @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #ifndef BFS_TYPO_H #define BFS_TYPO_H @@ -20,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/util.c b/src/util.c deleted file mode 100644 index a62e66c..0000000 --- a/src/util.c +++ /dev/null @@ -1,510 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -#include "util.h" -#include "dstring.h" -#include "xregex.h" -#include <assert.h> -#include <errno.h> -#include <fcntl.h> -#include <langinfo.h> -#include <nl_types.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> -#include <sys/types.h> -#include <unistd.h> -#include <wchar.h> - -#if BFS_HAS_SYS_PARAM -# include <sys/param.h> -#endif - -#if BFS_HAS_SYS_SYSMACROS -# include <sys/sysmacros.h> -#elif BFS_HAS_SYS_MKDEV -# include <sys/mkdev.h> -#endif - -#if BFS_HAS_UTIL -# include <util.h> -#endif - -char *xreadlinkat(int fd, const char *path, size_t size) { - ssize_t len; - char *name = NULL; - - if (size == 0) { - size = 64; - } else { - ++size; // NUL terminator - } - - while (true) { - char *new_name = realloc(name, size); - if (!new_name) { - goto error; - } - name = new_name; - - len = readlinkat(fd, path, name, size); - if (len < 0) { - goto error; - } else if ((size_t)len >= size) { - size *= 2; - } else { - break; - } - } - - name[len] = '\0'; - return name; - -error: - free(name); - return NULL; -} - -int dup_cloexec(int fd) { -#ifdef F_DUPFD_CLOEXEC - return fcntl(fd, F_DUPFD_CLOEXEC, 0); -#else - int ret = dup(fd); - if (ret < 0) { - return -1; - } - - if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) { - close_quietly(ret); - return -1; - } - - return ret; -#endif -} - -int pipe_cloexec(int pipefd[2]) { -#if __linux__ || (BSD && !__APPLE__) - return pipe2(pipefd, O_CLOEXEC); -#else - if (pipe(pipefd) != 0) { - return -1; - } - - if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) { - close_quietly(pipefd[1]); - close_quietly(pipefd[0]); - return -1; - } - - return 0; -#endif -} - -/** 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, "----------"); - - str[0] = type_char(mode); - - if (mode & 00400) { - str[1] = 'r'; - } - if (mode & 00200) { - str[2] = 'w'; - } - if ((mode & 04100) == 04000) { - str[3] = 'S'; - } else if (mode & 04000) { - str[3] = 's'; - } else if (mode & 00100) { - str[3] = 'x'; - } - - if (mode & 00040) { - str[4] = 'r'; - } - if (mode & 00020) { - str[5] = 'w'; - } - if ((mode & 02010) == 02000) { - str[6] = 'S'; - } else if (mode & 02000) { - str[6] = 's'; - } else if (mode & 00010) { - str[6] = 'x'; - } - - if (mode & 00004) { - str[7] = 'r'; - } - if (mode & 00002) { - str[8] = 'w'; - } - if ((mode & 01001) == 01000) { - str[9] = 'T'; - } else if (mode & 01000) { - str[9] = 't'; - } else if (mode & 00001) { - str[9] = 'x'; - } -} - -const char *xbasename(const char *path) { - const char *i; - - // Skip trailing slashes - for (i = path + strlen(path); i > path && i[-1] == '/'; --i); - - // Find the beginning of the name - for (; i > path && i[-1] != '/'; --i); - - // Skip leading slashes - for (; i[0] == '/' && i[1]; ++i); - - return i; -} - -int xfaccessat(int fd, const char *path, int amode) { - int ret = faccessat(fd, path, amode, 0); - -#ifdef AT_EACCESS - // Some platforms, like Hurd, only support AT_EACCESS. Other platforms, - // like Android, don't support AT_EACCESS at all. - if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) { - ret = faccessat(fd, path, amode, AT_EACCESS); - } -#endif - - return ret; -} - -int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) { -#if BSD && !__GNU__ - char *str_arg = (char *)*str; - unsigned long set_arg = 0; - unsigned long 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 - - *str = str_arg; - *set = set_arg; - *clear = clear_arg; - - if (ret != 0) { - errno = EINVAL; - } - return ret; -#else // !BSD - errno = ENOTSUP; - return -1; -#endif -} - -size_t xstrwidth(const char *str) { - size_t len = strlen(str); - size_t ret = 0; - - mbstate_t mb; - memset(&mb, 0, sizeof(mb)); - - while (len > 0) { - wchar_t wc; - size_t mblen = mbrtowc(&wc, str, len, &mb); - int cwidth; - if (mblen == (size_t)-1) { - // Invalid byte sequence, assume a single-width '?' - mblen = 1; - cwidth = 1; - memset(&mb, 0, sizeof(mb)); - } else if (mblen == (size_t)-2) { - // Incomplete byte sequence, assume a single-width '?' - mblen = len; - cwidth = 1; - } else { - cwidth = wcwidth(wc); - if (cwidth < 0) { - cwidth = 0; - } - } - - str += mblen; - len -= mblen; - ret += cwidth; - } - - return ret; -} - -bool is_nonexistence_error(int error) { - return error == ENOENT || errno == ENOTDIR; -} - -/** Compile and execute a regular expression for xrpmatch(). */ -static int xrpregex(nl_item item, const char *response) { - const char *pattern = nl_langinfo(item); - if (!pattern) { - return -1; - } - - struct bfs_regex *regex; - int ret = bfs_regcomp(®ex, pattern, BFS_REGEX_POSIX_EXTENDED, 0); - if (ret == 0) { - ret = bfs_regexec(regex, response, 0); - } - - bfs_regfree(regex); - return ret; -} - -/** Check if a response is affirmative or negative. */ -static int xrpmatch(const char *response) { - int ret = xrpregex(NOEXPR, response); - if (ret > 0) { - return 0; - } else if (ret < 0) { - return -1; - } - - ret = xrpregex(YESEXPR, response); - if (ret > 0) { - return 1; - } else if (ret < 0) { - return -1; - } - - // Failsafe: always handle y/n - char c = response[0]; - if (c == 'n' || c == 'N') { - return 0; - } else if (c == 'y' || c == 'Y') { - return 1; - } else { - return -1; - } -} - -int ynprompt(void) { - fflush(stderr); - - char *line = xgetdelim(stdin, '\n'); - int ret = line ? xrpmatch(line) : -1; - free(line); - return ret; -} - -dev_t bfs_makedev(int ma, int mi) { -#ifdef makedev - return makedev(ma, mi); -#else - return (ma << 8) | mi; -#endif -} - -int bfs_major(dev_t dev) { -#ifdef major - return major(dev); -#else - return dev >> 8; -#endif -} - -int bfs_minor(dev_t dev) { -#ifdef minor - return minor(dev); -#else - return dev & 0xFF; -#endif -} - -size_t xread(int fd, void *buf, size_t nbytes) { - size_t count = 0; - - while (count < nbytes) { - ssize_t ret = read(fd, (char *)buf + count, nbytes - count); - if (ret < 0) { - if (errno == EINTR) { - continue; - } else { - break; - } - } else if (ret == 0) { - // EOF - errno = 0; - break; - } else { - count += ret; - } - } - - return count; -} - -size_t xwrite(int fd, const void *buf, size_t nbytes) { - size_t count = 0; - - while (count < nbytes) { - ssize_t ret = write(fd, (const char *)buf + count, nbytes - count); - if (ret < 0) { - if (errno == EINTR) { - continue; - } else { - break; - } - } else if (ret == 0) { - // EOF? - errno = 0; - break; - } else { - count += ret; - } - } - - return count; -} - -char *xconfstr(int name) { - size_t len = confstr(name, NULL, 0); - if (len == 0) { - return NULL; - } - - char *str = malloc(len); - if (!str) { - return NULL; - } - - if (confstr(name, str, len) != len) { - free(str); - return NULL; - } - - return str; -} - -char *xgetdelim(FILE *file, char delim) { - char *chunk = NULL; - size_t n = 0; - ssize_t len = getdelim(&chunk, &n, delim, file); - if (len >= 0) { - if (chunk[len] == delim) { - chunk[len] = '\0'; - } - return chunk; - } else { - free(chunk); - if (!ferror(file)) { - errno = 0; - } - return NULL; - } -} - -FILE *xfopen(const char *path, int flags) { - char mode[4]; - - switch (flags & O_ACCMODE) { - case O_RDONLY: - strcpy(mode, "rb"); - break; - case O_WRONLY: - strcpy(mode, "wb"); - break; - case O_RDWR: - strcpy(mode, "r+b"); - break; - default: - assert(!"Invalid access mode"); - errno = EINVAL; - return NULL; - } - - if (flags & O_APPEND) { - mode[0] = 'a'; - } - - int fd; - if (flags & O_CREAT) { - fd = open(path, flags, 0666); - } else { - fd = open(path, flags); - } - - if (fd < 0) { - return NULL; - } - - FILE *ret = fdopen(fd, mode); - if (!ret) { - close_quietly(fd); - return NULL; - } - - return ret; -} - -int xclose(int fd) { - int ret = close(fd); - if (ret != 0) { - assert(errno != EBADF); - } - return ret; -} - -void close_quietly(int fd) { - int error = errno; - xclose(fd); - errno = error; -} diff --git a/src/util.h b/src/util.h deleted file mode 100644 index 2e89af6..0000000 --- a/src/util.h +++ /dev/null @@ -1,323 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -/** - * Assorted utilities that don't belong anywhere else. - */ - -#ifndef BFS_UTIL_H -#define BFS_UTIL_H - -#include <fcntl.h> -#include <fnmatch.h> -#include <stdbool.h> -#include <stddef.h> -#include <stdio.h> -#include <sys/types.h> - -// Some portability concerns - -#ifdef __has_feature -# define BFS_HAS_FEATURE(feature, fallback) __has_feature(feature) -#else -# define BFS_HAS_FEATURE(feature, fallback) fallback -#endif - -#ifdef __has_include -# define BFS_HAS_INCLUDE(header, fallback) __has_include(header) -#else -# define BFS_HAS_INCLUDE(header, fallback) fallback -#endif - -#ifdef __has_c_attribute -# define BFS_HAS_C_ATTRIBUTE(attr) __has_c_attribute(attr) -#else -# define BFS_HAS_C_ATTRIBUTE(attr) false -#endif - -#if __GNUC__ && defined(__has_attribute) -# define BFS_HAS_GNU_ATTRIBUTE(attr) __has_attribute(attr) -#else -# define BFS_HAS_GNU_ATTRIBUTE(attr) false -#endif - -#ifndef BFS_HAS_MNTENT -# define BFS_HAS_MNTENT BFS_HAS_INCLUDE(<mntent.h>, __GLIBC__) -#endif - -#ifndef BFS_HAS_SYS_ACL -# define BFS_HAS_SYS_ACL BFS_HAS_INCLUDE(<sys/acl.h>, true) -#endif - -#ifndef BFS_HAS_SYS_CAPABILITY -# define BFS_HAS_SYS_CAPABILITY BFS_HAS_INCLUDE(<sys/capability.h>, __linux__) -#endif - -#ifndef BFS_HAS_SYS_EXTATTR -# define BFS_HAS_SYS_EXTATTR BFS_HAS_INCLUDE(<sys/extattr.h>, __FreeBSD__) -#endif - -#ifndef BFS_HAS_SYS_MKDEV -# define BFS_HAS_SYS_MKDEV BFS_HAS_INCLUDE(<sys/mkdev.h>, false) -#endif - -#ifndef BFS_HAS_SYS_PARAM -# define BFS_HAS_SYS_PARAM BFS_HAS_INCLUDE(<sys/param.h>, true) -#endif - -#ifndef BFS_HAS_SYS_SYSMACROS -# define BFS_HAS_SYS_SYSMACROS BFS_HAS_INCLUDE(<sys/sysmacros.h>, __GLIBC__) -#endif - -#ifndef BFS_HAS_SYS_XATTR -# define BFS_HAS_SYS_XATTR BFS_HAS_INCLUDE(<sys/xattr.h>, __linux__) -#endif - -#ifndef BFS_HAS_UTIL -# define BFS_HAS_UTIL BFS_HAS_INCLUDE(<util.h>, __NetBSD__) -#endif - -#ifdef __GLIBC_PREREQ -# define BFS_GLIBC_PREREQ(maj, min) __GLIBC_PREREQ(maj, min) -#else -# define BFS_GLIBC_PREREQ(maj, min) false -#endif - -#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE) -# define FNM_CASEFOLD FNM_IGNORECASE -#endif - -#ifndef O_DIRECTORY -# define O_DIRECTORY 0 -#endif - -#if BFS_HAS_C_ATTRIBUTE(fallthrough) -# define BFS_FALLTHROUGH [[fallthrough]] -#elif BFS_HAS_GNU_ATTRIBUTE(fallthrough) -# define BFS_FALLTHROUGH __attribute__((fallthrough)) -#else -# define BFS_FALLTHROUGH ((void)0) -#endif - -/** - * Adds compiler warnings for bad printf()-style function calls, if supported. - */ -#if BFS_HAS_GNU_ATTRIBUTE(format) -# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args))) -#else -# define BFS_FORMATTER(fmt, args) -#endif - -// Lower bound on BFS_FLEX_SIZEOF() -#define BFS_FLEX_LB(type, member, length) (offsetof(type, member) + sizeof(((type *)NULL)->member[0]) * (length)) - -// Maximum macro for BFS_FLEX_SIZE() -#define BFS_FLEX_MAX(a, b) ((a) > (b) ? (a) : (b)) - -/** - * Computes the size of a struct containing a flexible array member of the given - * length. - * - * @param type - * The type of the struct containing the flexible array. - * @param member - * The name of the flexible array member. - * @param length - * The length of the flexible array. - */ -#define BFS_FLEX_SIZEOF(type, member, length) \ - (sizeof(type) <= BFS_FLEX_LB(type, member, 0) \ - ? BFS_FLEX_LB(type, member, length) \ - : BFS_FLEX_MAX(sizeof(type), BFS_FLEX_LB(type, member, length))) - -/** - * readlinkat() wrapper that dynamically allocates the result. - * - * @param fd - * The base directory descriptor. - * @param path - * The path to the link, relative to fd. - * @param 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. - */ -char *xreadlinkat(int fd, const char *path, size_t size); - -/** - * Like dup(), but set the FD_CLOEXEC flag. - * - * @param fd - * The file descriptor to duplicate. - * @return A duplicated file descriptor, or -1 on failure. - */ -int dup_cloexec(int fd); - -/** - * Like pipe(), but set the FD_CLOEXEC flag. - * - * @param pipefd - * The array to hold the two file descriptors. - * @return 0 on success, -1 on failure. - */ -int pipe_cloexec(int pipefd[2]); - -/** - * Format a mode like ls -l (e.g. -rw-r--r--). - * - * @param mode - * The mode to format. - * @param str - * The string to hold the formatted mode. - */ -void xstrmode(mode_t mode, char str[11]); - -/** - * basename() variant that doesn't modify the input. - * - * @param path - * The path in question. - * @return A pointer into path at the base name offset. - */ -const char *xbasename(const char *path); - -/** - * Wrapper for faccessat() that handles some portability issues. - */ -int xfaccessat(int fd, const char *path, int amode); - -/** - * Portability wrapper for strtofflags(). - * - * @param str - * The string to parse. The pointee will be advanced to the first - * invalid position on error. - * @param set - * The flags that are set in the string. - * @param 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); - -/** - * wcswidth() variant that works on narrow strings. - * - * @param str - * The string to measure. - * @return - * The likely width of that string in a terminal. - */ -size_t xstrwidth(const char *str); - -/** - * Return whether an error code is due to a path not existing. - */ -bool is_nonexistence_error(int error); - -/** - * Process a yes/no prompt. - * - * @return 1 for yes, 0 for no, and -1 for unknown. - */ -int ynprompt(void); - -/** - * Portable version of makedev(). - */ -dev_t bfs_makedev(int ma, int mi); - -/** - * Portable version of major(). - */ -int bfs_major(dev_t dev); - -/** - * Portable version of minor(). - */ -int bfs_minor(dev_t dev); - -/** - * A safe version of read() that handles interrupted system calls and partial - * reads. - * - * @return - * The number of bytes read. A value != nbytes indicates an error - * (errno != 0) or end of file (errno == 0). - */ -size_t xread(int fd, void *buf, size_t nbytes); - -/** - * A safe version of write() that handles interrupted system calls and partial - * writes. - * - * @return - The number of bytes written. A value != nbytes indicates an error. - */ -size_t xwrite(int fd, const void *buf, size_t nbytes); - -/** - * Wrapper for confstr() that allocates with malloc(). - * - * @param name - * The ID of the confstr to look up. - * @return - * The value of the confstr, or NULL on failure. - */ -char *xconfstr(int name); - -/** - * Convenience wrapper for getdelim(). - * - * @param file - * The file to read. - * @param delim - * The delimiter character to split on. - * @return - * The read chunk (without the delimiter), allocated with malloc(). - * NULL is returned on error (errno != 0) or end of file (errno == 0). - */ -char *xgetdelim(FILE *file, char delim); - -/** - * fopen() variant that takes open() style flags. - * - * @param path - * The path to open. - * @param flags - * Flags to pass to open(). - */ -FILE *xfopen(const char *path, int flags); - -/** - * close() wrapper that asserts the file descriptor is valid. - * - * @param fd - * The file descriptor to close. - * @return - * 0 on success, or -1 on error. - */ -int xclose(int fd); - -/** - * close() variant that preserves errno. - * - * @param fd - * The file descriptor to close. - */ -void close_quietly(int fd); - -#endif // BFS_UTIL_H 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 3c3cf35..796544e 100644 --- a/src/xregex.c +++ b/src/xregex.c @@ -1,31 +1,25 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "xregex.h" -#include "util.h" -#include <assert.h> + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "sanity.h" +#include "thread.h" + #include <errno.h> +#include <pthread.h> #include <stdlib.h> #include <string.h> #if BFS_WITH_ONIGURUMA -# include <langinfo.h> -# include <oniguruma.h> +# include <langinfo.h> +# include <oniguruma.h> #else -# include <regex.h> +# include <regex.h> #endif struct bfs_regex { @@ -41,35 +35,39 @@ struct bfs_regex { }; #if BFS_WITH_ONIGURUMA -/** Get (and initialize) the appropriate encoding for the current locale. */ -static int bfs_onig_encoding(OnigEncoding *penc) { - static OnigEncoding enc = NULL; - if (enc) { - *penc = enc; - return ONIG_NORMAL; - } +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 - enc = ONIG_ENCODING_ASCII; + bfs_onig_enc = ONIG_ENCODING_ASCII; // Oniguruma has no locale support, so try to guess the right encoding // from the current locale. const char *charmap = nl_langinfo(CODESET); if (charmap) { -#define BFS_MAP_ENCODING(name, value) \ - do { \ - if (strcmp(charmap, name) == 0) { \ - enc = value; \ - } \ +#define BFS_MAP_ENCODING(name, value) \ + do { \ + if (strcmp(charmap, name) == 0) { \ + bfs_onig_enc = value; \ + } \ } while (0) -#define BFS_MAP_ENCODING2(name1, name2, value) \ - do { \ - BFS_MAP_ENCODING(name1, value); \ - BFS_MAP_ENCODING(name2, value); \ +#define BFS_MAP_ENCODING2(name1, name2, value) \ + do { \ + BFS_MAP_ENCODING(name1, value); \ + BFS_MAP_ENCODING(name2, value); \ } while (0) // These names were found with locale -m on Linux and FreeBSD -#define BFS_MAP_ISO_8859(n) \ +#define BFS_MAP_ISO_8859(n) \ BFS_MAP_ENCODING2("ISO-8859-" #n, "ISO8859-" #n, ONIG_ENCODING_ISO_8859_ ## n) BFS_MAP_ISO_8859(1); @@ -91,7 +89,7 @@ static int bfs_onig_encoding(OnigEncoding *penc) { BFS_MAP_ENCODING("UTF-8", ONIG_ENCODING_UTF8); -#define BFS_MAP_EUC(name) \ +#define BFS_MAP_EUC(name) \ BFS_MAP_ENCODING2("EUC-" #name, "euc" #name, ONIG_ENCODING_EUC_ ## name) BFS_MAP_EUC(JP); @@ -109,17 +107,53 @@ static int bfs_onig_encoding(OnigEncoding *penc) { BFS_MAP_ENCODING("GB18030", ONIG_ENCODING_BIG5); } - int ret = onig_initialize(&enc, 1); - if (ret != ONIG_NORMAL) { - enc = NULL; + bfs_onig_status = onig_initialize(&bfs_onig_enc, 1); + if (bfs_onig_status != ONIG_NORMAL) { + bfs_onig_enc = NULL; } - *penc = enc; - return ret; + + // 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. */ +static int bfs_onig_initialize(OnigEncoding *enc) { + static pthread_once_t once = PTHREAD_ONCE_INIT; + invoke_once(&once, bfs_onig_once); + + *enc = bfs_onig_enc; + return bfs_onig_status; } #endif int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags) { - struct bfs_regex *regex = *preg = malloc(sizeof(*regex)); + struct bfs_regex *regex = *preg = ALLOC(struct bfs_regex); if (!regex) { return -1; } @@ -146,14 +180,26 @@ 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; } - assert(syntax); + bfs_assert(syntax, "Invalid regex type"); OnigOptionType options = syntax->options; if (flags & BFS_REGEX_ICASE) { @@ -161,7 +207,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ } OnigEncoding enc; - regex->err = bfs_onig_encoding(&enc); + regex->err = bfs_onig_initialize(&enc); if (regex->err != ONIG_NORMAL) { return -1; } @@ -188,13 +234,10 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ cflags |= REG_ICASE; } -#if BFS_HAS_FEATURE(memory_sanitizer, false) - // https://github.com/google/sanitizers/issues/1496 - memset(®ex->impl, 0, sizeof(regex->impl)); -#endif - regex->err = regcomp(®ex->impl, pattern, cflags); if (regex->err != 0) { + // https://github.com/google/sanitizers/issues/1496 + sanitize_init(®ex->impl); return -1; } #endif @@ -281,7 +324,7 @@ void bfs_regfree(struct bfs_regex *regex) { char *bfs_regerror(const struct bfs_regex *regex) { if (!regex) { - return strdup(strerror(ENOMEM)); + return strdup(xstrerror(ENOMEM)); } #if BFS_WITH_ONIGURUMA diff --git a/src/xregex.h b/src/xregex.h index b2f56a5..c4504ee 100644 --- a/src/xregex.h +++ b/src/xregex.h @@ -1,19 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2022 Tavian Barnes <tavianator@tavianator.com> and bfs * - * contributors * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> and the bfs contributors +// SPDX-License-Identifier: 0BSD #ifndef BFS_XREGEX_H #define BFS_XREGEX_H @@ -29,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, }; /** @@ -52,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. @@ -68,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. @@ -87,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 93c270a..ee62c05 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -1,33 +1,37 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "xspawn.h" -#include "util.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.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 <sys/wait.h> #include <unistd.h> +#if __has_include(<paths.h>) +# include <paths.h> +#endif + +#if BFS_POSIX_SPAWN >= 0 +# include <spawn.h> +#endif + /** * Types of spawn actions. */ enum bfs_spawn_op { + BFS_SPAWN_OPEN, BFS_SPAWN_CLOSE, BFS_SPAWN_DUP2, BFS_SPAWN_FCHDIR, @@ -38,120 +42,555 @@ enum bfs_spawn_op { * A spawn action. */ struct bfs_spawn_action { + /** The next action in the list. */ struct bfs_spawn_action *next; + /** This action's operation. */ enum bfs_spawn_op op; + /** The input fd (or -1). */ int in_fd; + /** The output fd (or -1). */ int out_fd; - int resource; - struct rlimit rlimit; + + /** Operation-specific args. */ + union { + /** BFS_SPAWN_OPEN args. */ + struct { + const char *path; + int flags; + mode_t mode; + }; + + /** BFS_SPAWN_SETRLIMIT args. */ + struct { + int resource; + struct rlimit rlimit; + }; + }; }; int bfs_spawn_init(struct bfs_spawn *ctx) { ctx->flags = 0; - ctx->actions = NULL; - ctx->tail = &ctx->actions; + SLIST_INIT(ctx); + +#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_spawnattr_init(&ctx->attr); + if (errno != 0) { + posix_spawn_file_actions_destroy(&ctx->actions); + return -1; + } + } +#endif + return 0; } +/** + * 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) { - struct bfs_spawn_action *action = ctx->actions; - while (action) { - struct bfs_spawn_action *next = action->next; + bfs_spawn_clear_posix(ctx); + + for_slist (struct bfs_spawn_action, action, ctx) { free(action); - action = next; } + return 0; } -int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) { - ctx->flags = flags; +#if BFS_POSIX_SPAWN >= 0 +/** Set some posix_spawnattr flags. */ +_maybe_unused +static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { + short prev; + errno = posix_spawnattr_getflags(&ctx->attr, &prev); + if (errno != 0) { + return -1; + } + + short next = prev | flags; + if (next != prev) { + errno = posix_spawnattr_setflags(&ctx->attr, next); + if (errno != 0) { + return -1; + } + } + return 0; } +#endif -/** Add a spawn action to the chain. */ -static struct bfs_spawn_action *bfs_spawn_add(struct bfs_spawn *ctx, enum bfs_spawn_op op) { - struct bfs_spawn_action *action = malloc(sizeof(*action)); - if (action) { - action->next = NULL; - action->op = op; - action->in_fd = -1; - action->out_fd = -1; - - *ctx->tail = action; - ctx->tail = &action->next; +/** Allocate a spawn action. */ +static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) { + struct bfs_spawn_action *action = ALLOC(struct bfs_spawn_action); + if (!action) { + return NULL; } + + SLIST_ITEM_INIT(action); + action->op = op; + action->in_fd = -1; + action->out_fd = -1; return action; } -int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { - if (fd < 0) { - errno = EBADF; +int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_OPEN); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE); - if (action) { - action->out_fd = fd; - return 0; - } else { +#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) { + free(action); + return -1; + } + } +#endif + + action->out_fd = fd; + action->path = path; + action->flags = flags; + action->mode = mode; + SLIST_APPEND(ctx, action); + return 0; +} + +int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_CLOSE); + if (!action) { return -1; } + +#if BFS_POSIX_SPAWN >= 0 + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = posix_spawn_file_actions_addclose(&ctx->actions, fd); + if (errno != 0) { + free(action); + return -1; + } + } +#endif + + action->out_fd = fd; + SLIST_APPEND(ctx, action); + return 0; } int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { - if (oldfd < 0 || newfd < 0) { - errno = EBADF; + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_DUP2); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_DUP2); - if (action) { - action->in_fd = oldfd; - action->out_fd = newfd; - return 0; - } else { - return -1; +#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) { + free(action); + return -1; + } } +#endif + + action->in_fd = oldfd; + action->out_fd = newfd; + SLIST_APPEND(ctx, action); + return 0; } +/** + * https://www.austingroupbugs.net/view.php?id=1208#c4830 says: + * + * ... a search of the directories passed as the environment variable + * PATH ..., using the working directory of the child process after all + * file_actions have been performed. + * + * 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__ || __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) { - if (fd < 0) { - errno = EBADF; + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR); - if (action) { - action->in_fd = fd; - return 0; +#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_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 BFS_POSIX_SPAWN >= 0 && defined(BFS_POSIX_SPAWN_ADDFCHDIR) + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd); + if (errno != 0) { + free(action); + return -1; + } + } +#else + bfs_spawn_clear_posix(ctx); +#endif + + action->in_fd = fd; + SLIST_APPEND(ctx, action); + return 0; +} + +int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_SETRLIMIT); + if (!action) { + goto fail; + } + +#ifdef POSIX_SPAWN_SETRLIMIT + if (bfs_spawn_addflags(ctx, POSIX_SPAWN_SETRLIMIT) != 0) { + goto fail; + } + + errno = posix_spawnattr_setrlimit(&ctx->attr, resource, rl); + if (errno != 0) { + goto fail; + } +#else + bfs_spawn_clear_posix(ctx); +#endif + + action->resource = resource; + action->rlimit = *rl; + SLIST_APPEND(ctx, action); + return 0; + +fail: + free(action); + return -1; +} + +/** + * Context for resolving executables in the $PATH. + */ +struct bfs_resolver { + /** The executable to spawn. */ + const char *exe; + /** The $PATH to resolve in. */ + char *path; + /** A buffer to hold the resolved path. */ + char *buf; + /** The size of the buffer. */ + size_t len; + /** Whether the executable is already resolved. */ + bool done; + /** Whether to free(path). */ + bool free; +}; + +/** Free a $PATH resolution context. */ +static void bfs_resolve_free(struct bfs_resolver *res) { + if (res->free) { + free(res->path); + } + free(res->buf); +} + +/** Get the next component in the $PATH. */ +static bool bfs_resolve_next(const char **path, const char **next, size_t *len) { + *path = *next; + if (!*path) { + return false; + } + + *next = strchr(*path, ':'); + if (*next) { + *len = *next - *path; + ++*next; } else { - return -1; + *len = strlen(*path); + } + + if (*len == 0) { + // POSIX 8.3: "A zero-length prefix is a legacy feature that + // indicates the current working directory." + *path = "."; + *len = 1; + } + + return true; +} + +/** Finish resolving an executable, potentially from the child process. */ +static int bfs_resolve_late(struct bfs_resolver *res) { + if (res->done) { + return 0; + } + + char *buf = res->buf; + char *end = buf + res->len; + + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + char *cur = xstpencpy(buf, end, path, len); + cur = xstpecpy(cur, end, "/"); + cur = xstpecpy(cur, end, res->exe); + if (cur == end) { + bfs_bug("PATH resolution buffer too small"); + errno = ENOMEM; + return -1; + } + + if (xfaccessat(AT_FDCWD, buf, X_OK) == 0) { + res->exe = buf; + res->done = true; + return 0; + } + } + + errno = ENOENT; + return -1; +} + +/** Check if we can skip path resolution entirely. */ +static bool bfs_can_skip_resolve(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (ctx && !(ctx->flags & BFS_SPAWN_USE_PATH)) { + return true; + } + + if (strchr(res->exe, '/')) { + return true; + } + + return false; +} + +/** Check if any $PATH components are relative. */ +static bool bfs_resolve_relative(const struct bfs_resolver *res) { + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + if (path[0] != '/') { + return true; + } + } + + 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 (bfs_spawn_will_chdir(ctx)) { + return false; } + + return true; } -int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) { - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_SETRLIMIT); - if (action) { - action->resource = resource; - action->rlimit = *rl; +/** Get the required path resolution buffer size. */ +static size_t bfs_resolve_capacity(const struct bfs_resolver *res) { + size_t max = 0; + + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + if (len > max) { + max = len; + } + } + + // path + "/" + exe + '\0' + return max + 1 + strlen(res->exe) + 1; +} + +/** Begin resolving an executable, from the parent process. */ +static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const struct bfs_spawn *ctx) { + *res = (struct bfs_resolver) { + .exe = exe, + }; + + 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; + } + + res->path = getenv("PATH"); + if (!res->path) { +#if defined(_CS_PATH) + res->path = xconfstr(_CS_PATH); + res->free = true; +#elif defined(_PATH_DEFPATH) + res->path = _PATH_DEFPATH; +#else + errno = ENOENT; +#endif + } + if (!res->path) { + goto fail; + } + + bool can_finish = bfs_can_resolve_early(res, ctx); + +#if BFS_POSIX_SPAWNP_AFTER_FCHDIR + bool use_posix = ctx && (ctx->flags & BFS_SPAWN_USE_POSIX); + if (!can_finish && use_posix) { + // posix_spawnp() will do the resolution, so don't bother + // allocating a buffer + return 0; + } +#endif + + res->len = bfs_resolve_capacity(res); + res->buf = malloc(res->len); + if (!res->buf) { + goto fail; + } + + if (can_finish && bfs_resolve_late(res) != 0) { + goto fail; + } + + return 0; + +fail: + bfs_resolve_free(res); + return -1; +} + +#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) { + pid_t ret; + + if (res->done) { + errno = posix_spawn(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp); } else { + errno = posix_spawnp(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp); + } + + if (errno != 0) { return -1; } + + return ret; } -/** Actually exec() the new process. */ -static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { - int error; - const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL; +/** Check if we can use posix_spawn(). */ +static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (!(ctx->flags & BFS_SPAWN_USE_POSIX)) { + return false; + } + +#if !BFS_POSIX_SPAWNP_AFTER_FCHDIR + if (!res->done) { + return false; + } +#endif + +#if !BFS_POSIX_SPAWN_AFTER_FCHDIR + if (res->exe[0] != '/' && bfs_spawn_will_chdir(ctx)) { + return false; + } +#endif + return true; +} + +#endif // BFS_POSIX_SPAWN >= 0 + +/** Actually exec() the new process. */ +_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 (const struct bfs_spawn_action *action = actions; action; action = action->next) { + for_slist (const struct bfs_spawn_action, action, ctx) { + int fd; + // Move the error-reporting pipe out of the way if necessary... if (action->out_fd == pipefd[1]) { - int fd = dup_cloexec(pipefd[1]); + fd = dup_cloexec(pipefd[1]); if (fd < 0) { goto fail; } @@ -166,6 +605,17 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char ** } switch (action->op) { + case BFS_SPAWN_OPEN: + fd = open(action->path, action->flags, action->mode); + if (fd < 0) { + goto fail; + } + if (fd != action->out_fd) { + if (dup2(fd, action->out_fd) < 0) { + goto fail; + } + } + break; case BFS_SPAWN_CLOSE: if (close(action->out_fd) != 0) { goto fail; @@ -189,130 +639,140 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char ** } } - execve(exe, argv, envp); + if (bfs_resolve_late(res) != 0) { + goto fail; + } -fail: - error = errno; + // 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:; + int error = errno; // In case of a write error, the parent will still see that we exited // unsuccessfully, but won't know why - (void) xwrite(pipefd[1], &error, sizeof(error)); + (void)xwrite(pipefd[1], &error, sizeof(error)); xclose(pipefd[1]); _Exit(127); } -pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { - extern char **environ; - if (!envp) { - envp = environ; - } - - enum bfs_spawn_flags flags = ctx ? ctx->flags : 0; - char *resolved = NULL; - if (flags & BFS_SPAWN_USEPATH) { - exe = resolved = bfs_spawn_resolve(exe); - if (!resolved) { - return -1; - } - } - +/** bfs_spawn() implementation using fork()/exec(). */ +static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { // Use a pipe to report errors from the child int pipefd[2]; if (pipe_cloexec(pipefd) != 0) { - free(resolved); 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]); - free(resolved); - return -1; - } else if (pid == 0) { +#endif + if (pid == 0) { // Child - bfs_spawn_exec(exe, 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]); - free(resolved); int error; ssize_t nbytes = xread(pipefd[0], &error, sizeof(error)); xclose(pipefd[0]); if (nbytes == sizeof(error)) { - int wstatus; - waitpid(pid, &wstatus, 0); + xwaitpid(pid, NULL, 0); errno = error; return -1; } return pid; + +fail: + close_quietly(pipefd[1]); + close_quietly(pipefd[0]); + return -1; } -char *bfs_spawn_resolve(const char *exe) { - if (strchr(exe, '/')) { - return strdup(exe); +/** 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 BFS_POSIX_SPAWN >= 0 + if (bfs_use_posix_spawn(res, ctx)) { + return bfs_posix_spawn(res, ctx, argv, envp); } +#endif - const char *path = getenv("PATH"); + return bfs_fork_spawn(res, ctx, argv, envp); +} - char *confpath = NULL; - if (!path) { - path = confpath = xconfstr(_CS_PATH); - if (!path) { - return NULL; - } +pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { + // execvp()/posix_spawnp() are typically implemented with repeated + // execv() calls for each $PATH component until one succeeds. It's + // faster to resolve the full path ahead of time. + struct bfs_resolver res; + if (bfs_resolve_early(&res, exe, ctx) != 0) { + return -1; } - size_t cap = 0; - char *ret = NULL; - while (true) { - const char *end = strchr(path, ':'); - size_t len = end ? (size_t)(end - path) : strlen(path); - - // POSIX 8.3: "A zero-length prefix is a legacy feature that - // indicates the current working directory." - if (len == 0) { - path = "."; - len = 1; - } - - size_t total = len + 1 + strlen(exe) + 1; - if (cap < total) { - char *grown = realloc(ret, total); - if (!grown) { - goto fail; - } - ret = grown; - cap = total; - } - - memcpy(ret, path, len); - if (ret[len - 1] != '/') { - ret[len++] = '/'; - } - strcpy(ret + len, exe); + extern char **environ; + if (!envp) { + envp = environ; + } - if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) { - break; - } + pid_t ret = bfs_spawn_impl(&res, ctx, argv, envp); + bfs_resolve_free(&res); + return ret; +} - if (!end) { - errno = ENOENT; - goto fail; - } +char *bfs_spawn_resolve(const char *exe) { + struct bfs_resolver res; + if (bfs_resolve_early(&res, exe, NULL) != 0) { + return NULL; + } + if (bfs_resolve_late(&res) != 0) { + bfs_resolve_free(&res); + return NULL; + } - path = end + 1; + char *ret; + if (res.exe == res.buf) { + ret = res.buf; + res.buf = NULL; + } else { + ret = strdup(res.exe); } - free(confpath); + bfs_resolve_free(&res); return ret; - -fail: - free(confpath); - free(ret); - return NULL; } diff --git a/src/xspawn.h b/src/xspawn.h index cd6a42e..3c74ccd 100644 --- a/src/xspawn.h +++ b/src/xspawn.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * A process-spawning library inspired by posix_spawn(). @@ -23,83 +10,112 @@ #include <sys/resource.h> #include <sys/types.h> +#include <unistd.h> + +#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 /** * bfs_spawn() flags. */ enum bfs_spawn_flags { /** Use the PATH variable to resolve the executable (like execvp()). */ - BFS_SPAWN_USEPATH = 1 << 0, + BFS_SPAWN_USE_PATH = 1 << 0, + /** Whether posix_spawn() can be used. */ + BFS_SPAWN_USE_POSIX = 1 << 1, }; /** * bfs_spawn() attributes, controlling the context of the new process. */ struct bfs_spawn { + /** Spawn flags. */ enum bfs_spawn_flags flags; - struct bfs_spawn_action *actions; + + /** Linked list of actions. */ + struct bfs_spawn_action *head; struct bfs_spawn_action **tail; + +#if BFS_POSIX_SPAWN >= 0 + /** posix_spawn() context, for when we can use it. */ + posix_spawn_file_actions_t actions; + posix_spawnattr_t attr; +#endif }; /** * Create a new bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ int bfs_spawn_init(struct bfs_spawn *ctx); /** * Destroy a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ int bfs_spawn_destroy(struct bfs_spawn *ctx); /** - * Set the flags for a bfs_spawn() context. + * Add an open() action to a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ -int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags); +int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode); /** * Add a close() action to a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd); /** * Add a dup2() action to a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd); /** * Add an fchdir() action to a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd); /** - * Add a setrlimit() action to a bfs_spawn() context. + * Apply setrlimit() to a bfs_spawn() context. * - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. */ -int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl); +int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl); /** * 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 @@ -108,10 +124,10 @@ int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rli pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp); /** - * Look up an executable in the current PATH, as BFS_SPAWN_USEPATH or execvp() + * 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 8ca963b..6b8a141 100644 --- a/src/xtime.c +++ b/src/xtime.c @@ -1,65 +1,55 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD #include "xtime.h" + +#include "alloc.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "sanity.h" + #include <errno.h> #include <limits.h> -#include <stdbool.h> -#include <stdlib.h> +#include <sys/time.h> #include <time.h> +#include <unistd.h> -/** Whether tzset() has been called. */ -static bool tz_is_set = false; +int xmktime(struct tm *tm, time_t *timep) { + time_t time = mktime(tm); -int xlocaltime(const time_t *timep, struct tm *result) { - // Should be called before localtime_r() according to POSIX.1-2004 - if (!tz_is_set) { - tzset(); - tz_is_set = true; - } + if (time == -1) { + int error = errno; - if (localtime_r(timep, result)) { - return 0; - } else { - return -1; - } -} + struct tm tmp; + if (!localtime_r(&time, &tmp)) { + bfs_ebug("localtime_r(-1)"); + return -1; + } -int xgmtime(const time_t *timep, struct tm *result) { - // Should be called before gmtime_r() according to POSIX.1-2004 - if (!tz_is_set) { - tzset(); - tz_is_set = true; + 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; + } } - if (gmtime_r(timep, result)) { - return 0; - } else { - return -1; - } + *timep = time; + return 0; } -int xmktime(struct tm *tm, time_t *timep) { - *timep = mktime(tm); +// 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 (*timep == -1) { + if (time == -1) { int error = errno; struct tm tmp; - if (xlocaltime(timep, &tmp) != 0) { + if (!gmtime_r(&time, &tmp)) { + bfs_ebug("gmtime_r(-1)"); return -1; } @@ -70,9 +60,12 @@ int xmktime(struct tm *tm, time_t *timep) { } } + *timep = time; return 0; } +#else + static int safe_add(int *value, int delta) { if (*value >= 0) { if (delta > INT_MAX - *value) { @@ -90,7 +83,7 @@ static int safe_add(int *value, int delta) { static int floor_div(int n, int d) { int a = n < 0; - return (n + a)/d - a; + return (n + a) / d - a; } static int wrap(int *value, int max, int *next) { @@ -102,80 +95,84 @@ static int wrap(int *value, int max, int *next) { static int month_length(int year, int month) { static const int month_lengths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int ret = month_lengths[month]; - if (month == 1 && year%4 == 0 && (year%100 != 0 || (year + 300)%400 == 0)) { + if (month == 1 && year % 4 == 0 && (year % 100 != 0 || (year + 300) % 400 == 0)) { ++ret; } return ret; } int xtimegm(struct tm *tm, time_t *timep) { - tm->tm_isdst = 0; + struct tm copy = *tm; + copy.tm_isdst = 0; - if (wrap(&tm->tm_sec, 60, &tm->tm_min) != 0) { + if (wrap(©.tm_sec, 60, ©.tm_min) != 0) { goto overflow; } - if (wrap(&tm->tm_min, 60, &tm->tm_hour) != 0) { + if (wrap(©.tm_min, 60, ©.tm_hour) != 0) { goto overflow; } - if (wrap(&tm->tm_hour, 24, &tm->tm_mday) != 0) { + if (wrap(©.tm_hour, 24, ©.tm_mday) != 0) { goto overflow; } // In order to wrap the days of the month, we first need to know what // month it is - if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + if (wrap(©.tm_mon, 12, ©.tm_year) != 0) { goto overflow; } - if (tm->tm_mday < 1) { + if (copy.tm_mday < 1) { do { - --tm->tm_mon; - if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + --copy.tm_mon; + if (wrap(©.tm_mon, 12, ©.tm_year) != 0) { goto overflow; } - tm->tm_mday += month_length(tm->tm_year, tm->tm_mon); - } while (tm->tm_mday < 1); + copy.tm_mday += month_length(copy.tm_year, copy.tm_mon); + } while (copy.tm_mday < 1); } else { while (true) { - int days = month_length(tm->tm_year, tm->tm_mon); - if (tm->tm_mday <= days) { + int days = month_length(copy.tm_year, copy.tm_mon); + if (copy.tm_mday <= days) { break; } - tm->tm_mday -= days; - ++tm->tm_mon; - if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) { + copy.tm_mday -= days; + ++copy.tm_mon; + if (wrap(©.tm_mon, 12, ©.tm_year) != 0) { goto overflow; } } } - tm->tm_yday = 0; - for (int i = 0; i < tm->tm_mon; ++i) { - tm->tm_yday += month_length(tm->tm_year, i); + copy.tm_yday = 0; + for (int i = 0; i < copy.tm_mon; ++i) { + copy.tm_yday += month_length(copy.tm_year, i); } - tm->tm_yday += tm->tm_mday - 1; + copy.tm_yday += copy.tm_mday - 1; int leap_days; // Compute floor((year - 69)/4) - floor((year - 1)/100) + floor((year + 299)/400) without overflows - if (tm->tm_year >= 0) { - leap_days = floor_div(tm->tm_year - 69, 4) - floor_div(tm->tm_year - 1, 100) + floor_div(tm->tm_year - 101, 400) + 1; + if (copy.tm_year >= 0) { + leap_days = floor_div(copy.tm_year - 69, 4) - floor_div(copy.tm_year - 1, 100) + floor_div(copy.tm_year - 101, 400) + 1; } else { - leap_days = floor_div(tm->tm_year + 3, 4) - floor_div(tm->tm_year + 99, 100) + floor_div(tm->tm_year + 299, 400) - 17; + leap_days = floor_div(copy.tm_year + 3, 4) - floor_div(copy.tm_year + 99, 100) + floor_div(copy.tm_year + 299, 400) - 17; } - long long epoch_days = 365LL*(tm->tm_year - 70) + leap_days + tm->tm_yday; - tm->tm_wday = (epoch_days + 4)%7; - if (tm->tm_wday < 0) { - tm->tm_wday += 7; + long long epoch_days = 365LL * (copy.tm_year - 70) + leap_days + copy.tm_yday; + copy.tm_wday = (epoch_days + 4) % 7; + if (copy.tm_wday < 0) { + copy.tm_wday += 7; } - long long epoch_time = tm->tm_sec + 60*(tm->tm_min + 60*(tm->tm_hour + 24*epoch_days)); - *timep = (time_t)epoch_time; - if ((long long)*timep != epoch_time) { + long long epoch_time = copy.tm_sec + 60 * (copy.tm_min + 60 * (copy.tm_hour + 24 * epoch_days)); + time_t time = (time_t)epoch_time; + if ((long long)time != epoch_time) { goto overflow; } + + *tm = copy; + *timep = time; return 0; overflow: @@ -183,23 +180,52 @@ overflow: return -1; } +#endif // !BFS_HAS_TIMEGM + +/** Parse a decimal digit. */ +static int xgetdigit(char c) { + int ret = c - '0'; + if (ret < 0 || ret > 9) { + return -1; + } else { + return ret; + } +} + /** Parse some digits from a timestamp. */ -static int parse_timestamp_part(const char **str, size_t n, int *result) { - char buf[n + 1]; +static int xgetpart(const char **str, size_t n, int *result) { + *result = 0; + for (size_t i = 0; i < n; ++i, ++*str) { - char c = **str; - if (c < '0' || c > '9') { + int dig = xgetdigit(**str); + if (dig < 0) { return -1; } - buf[i] = c; + *result *= 10; + *result += dig; } - buf[n] = '\0'; - *result = atoi(buf); return 0; } -int parse_timestamp(const char *str, struct timespec *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, }; @@ -210,7 +236,7 @@ int parse_timestamp(const char *str, struct timespec *result) { bool local = true; // YYYY - if (parse_timestamp_part(&str, 4, &tm.tm_year) != 0) { + if (xgetpart(&str, 4, &tm.tm_year) != 0) { goto invalid; } tm.tm_year -= 1900; @@ -219,7 +245,7 @@ int parse_timestamp(const char *str, struct timespec *result) { if (*str == '-') { ++str; } - if (parse_timestamp_part(&str, 2, &tm.tm_mon) != 0) { + if (xgetpart(&str, 2, &tm.tm_mon) != 0) { goto invalid; } tm.tm_mon -= 1; @@ -228,18 +254,18 @@ int parse_timestamp(const char *str, struct timespec *result) { if (*str == '-') { ++str; } - if (parse_timestamp_part(&str, 2, &tm.tm_mday) != 0) { + if (xgetpart(&str, 2, &tm.tm_mday) != 0) { goto invalid; } if (!*str) { goto end; - } else if (*str == 'T') { + } else if (*str == 'T' || *str == ' ') { ++str; } // hh - if (parse_timestamp_part(&str, 2, &tm.tm_hour) != 0) { + if (xgetpart(&str, 2, &tm.tm_hour) != 0) { goto invalid; } @@ -248,8 +274,10 @@ int parse_timestamp(const char *str, struct timespec *result) { goto end; } else if (*str == ':') { ++str; + } else if (xgetdigit(*str) < 0) { + goto zone; } - if (parse_timestamp_part(&str, 2, &tm.tm_min) != 0) { + if (xgetpart(&str, 2, &tm.tm_min) != 0) { goto invalid; } @@ -258,11 +286,14 @@ int parse_timestamp(const char *str, struct timespec *result) { goto end; } else if (*str == ':') { ++str; + } else if (xgetdigit(*str) < 0) { + goto zone; } - if (parse_timestamp_part(&str, 2, &tm.tm_sec) != 0) { + if (xgetpart(&str, 2, &tm.tm_sec) != 0) { goto invalid; } +zone: if (!*str) { goto end; } else if (*str == 'Z') { @@ -274,7 +305,7 @@ int parse_timestamp(const char *str, struct timespec *result) { ++str; // hh - if (parse_timestamp_part(&str, 2, &tz_hour) != 0) { + if (xgetpart(&str, 2, &tz_hour) != 0) { goto invalid; } @@ -284,7 +315,7 @@ int parse_timestamp(const char *str, struct timespec *result) { } else if (*str == ':') { ++str; } - if (parse_timestamp_part(&str, 2, &tz_min) != 0) { + if (xgetpart(&str, 2, &tz_min) != 0) { goto invalid; } } else { @@ -305,14 +336,15 @@ end: goto error; } - int offset = 60*tz_hour + tz_min; + int offset = (tz_hour * 60 + tz_min) * 60; if (tz_negative) { - result->tv_sec -= offset; - } else { result->tv_sec += offset; + } else { + result->tv_sec -= offset; } } +done: result->tv_nsec = 0; return 0; @@ -321,3 +353,151 @@ invalid: error: return -1; } + +/** 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 +# 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; + } + +#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 ceff48f..b76fef2 100644 --- a/src/xtime.h +++ b/src/xtime.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * Date/time handling. @@ -24,63 +11,98 @@ #include <time.h> /** - * localtime_r() wrapper that calls tzset() first. + * mktime() wrapper that reports errors more reliably. * - * @param[in] timep - * The time_t to convert. - * @param[out] result - * Buffer to hold the result. + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] + * A pointer to the result. * @return * 0 on success, -1 on failure. */ -int xlocaltime(const time_t *timep, struct tm *result); +int xmktime(struct tm *tm, time_t *timep); /** - * gmtime_r() wrapper that calls tzset() first. + * A portable timegm(), the inverse of gmtime(). * - * @param[in] timep - * The time_t to convert. - * @param[out] result - * Buffer to hold the result. + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] + * A pointer to the result. * @return * 0 on success, -1 on failure. */ -int xgmtime(const time_t *timep, struct tm *result); +int xtimegm(struct tm *tm, time_t *timep); /** - * mktime() wrapper that reports errors more reliably. + * Parse an ISO 8601-style timestamp. * - * @param[in,out] tm - * The struct tm to convert. - * @param[out] timep + * @str + * The string to parse. + * @result[out] * A pointer to the result. * @return * 0 on success, -1 on failure. */ -int xmktime(struct tm *tm, time_t *timep); +int xgetdate(const char *str, struct timespec *result); /** - * A portable timegm(), the inverse of gmtime(). + * 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[in,out] tm - * The struct tm to convert. - * @param[out] timep - * A pointer to the result. * @return - * 0 on success, -1 on failure. + * An integer with the sign of (*lhs - *rhs). */ -int xtimegm(struct tm *tm, time_t *timep); +int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs); /** - * Parse an ISO 8601-style timestamp. + * 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. * - * @param[in] str - * The string to parse. - * @param[out] result - * A pointer to the result. * @return - * 0 on success, -1 on failure. + * 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 parse_timestamp(const char *str, struct timespec *result); +void xtimer_stop(struct timer *timer); #endif // BFS_XTIME_H diff --git a/tests/alloc.c b/tests/alloc.c new file mode 100644 index 0000000..4aae515 --- /dev/null +++ b/tests/alloc.c @@ -0,0 +1,78 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "alloc.h" +#include "diag.h" + +#include <errno.h> +#include <stdlib.h> +#include <stdint.h> + +struct flexible { + alignas(64) int foo[8]; + int bar[]; +}; + +/** 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); + + 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; + } + } + + return flexy; +} + +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); + + // 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_everify(varena_alloc(&varena, i)); + struct arena *arena = &varena.arenas[varena.narenas - 1]; + 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); +} diff --git a/tests/test_D_all.out b/tests/bfs/D_all.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_D_all.out +++ b/tests/bfs/D_all.out diff --git a/tests/bfs/D_all.sh b/tests/bfs/D_all.sh new file mode 100644 index 0000000..170698a --- /dev/null +++ b/tests/bfs/D_all.sh @@ -0,0 +1 @@ +bfs_diff -D all basic diff --git a/tests/bfs/D_incomplete.sh b/tests/bfs/D_incomplete.sh new file mode 100644 index 0000000..30c522a --- /dev/null +++ b/tests/bfs/D_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs -D diff --git a/tests/test_D_multi.out b/tests/bfs/D_multi.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_D_multi.out +++ b/tests/bfs/D_multi.out diff --git a/tests/bfs/D_multi.sh b/tests/bfs/D_multi.sh new file mode 100644 index 0000000..08a8ca6 --- /dev/null +++ b/tests/bfs/D_multi.sh @@ -0,0 +1 @@ +bfs_diff -D opt,tree,unknown basic diff --git a/tests/test_type_f.out b/tests/bfs/D_opt.out index 6218a0c..6218a0c 100644 --- a/tests/test_type_f.out +++ b/tests/bfs/D_opt.out diff --git a/tests/bfs/D_opt.sh b/tests/bfs/D_opt.sh new file mode 100644 index 0000000..c14fe70 --- /dev/null +++ b/tests/bfs/D_opt.sh @@ -0,0 +1 @@ +bfs_diff -D opt -nohidden -not \( -type c -o -type d \) -links -5 -links -10 -not -hidden basic diff --git a/tests/test_O0.out b/tests/bfs/D_unknown.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_O0.out +++ b/tests/bfs/D_unknown.out diff --git a/tests/bfs/D_unknown.sh b/tests/bfs/D_unknown.sh new file mode 100644 index 0000000..cac9bd9 --- /dev/null +++ b/tests/bfs/D_unknown.sh @@ -0,0 +1,4 @@ +stderr=$(invoke_bfs -warn -D unknown basic 2>&1 >"$OUT") +[ -n "$stderr" ] +sort_output +diff_output diff --git a/tests/test_O1.out b/tests/bfs/Dmulti.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_O1.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/test_L.out b/tests/bfs/LD_stat.out index ec9e861..ec9e861 100644 --- a/tests/test_L.out +++ b/tests/bfs/LD_stat.out 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/test_L_depth.out b/tests/bfs/LDstat.out index ec9e861..ec9e861 100644 --- a/tests/test_L_depth.out +++ b/tests/bfs/LDstat.out 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_capable.out b/tests/bfs/L_capable.out new file mode 100644 index 0000000..0810d4a --- /dev/null +++ b/tests/bfs/L_capable.out @@ -0,0 +1,2 @@ +./capable +./link diff --git a/tests/bfs/L_capable.sh b/tests/bfs/L_capable.sh new file mode 100644 index 0000000..97c404f --- /dev/null +++ b/tests/bfs/L_capable.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 + +bfs_diff -L . -capable diff --git a/tests/test_L_loops_continue.out b/tests/bfs/L_noerror.out index a514555..a514555 100644 --- a/tests/test_L_loops_continue.out +++ b/tests/bfs/L_noerror.out 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/test_L_unique.out b/tests/bfs/L_unique.out index c94c48e..c94c48e 100644 --- a/tests/test_L_unique.out +++ b/tests/bfs/L_unique.out diff --git a/tests/bfs/L_unique.sh b/tests/bfs/L_unique.sh new file mode 100644 index 0000000..c804526 --- /dev/null +++ b/tests/bfs/L_unique.sh @@ -0,0 +1 @@ +bfs_diff -L links/{file,symlink,hardlink} -unique diff --git a/tests/test_L_unique_depth.out b/tests/bfs/L_unique_depth.out index dad0a98..dad0a98 100644 --- a/tests/test_L_unique_depth.out +++ b/tests/bfs/L_unique_depth.out diff --git a/tests/bfs/L_unique_depth.sh b/tests/bfs/L_unique_depth.sh new file mode 100644 index 0000000..fb9aca1 --- /dev/null +++ b/tests/bfs/L_unique_depth.sh @@ -0,0 +1 @@ +bfs_diff -L loops/deeply/nested -unique -depth diff --git a/tests/test_L_unique_loops.out b/tests/bfs/L_unique_loops.out index dad0a98..dad0a98 100644 --- a/tests/test_L_unique_loops.out +++ b/tests/bfs/L_unique_loops.out diff --git a/tests/bfs/L_unique_loops.sh b/tests/bfs/L_unique_loops.sh new file mode 100644 index 0000000..2bdd94e --- /dev/null +++ b/tests/bfs/L_unique_loops.sh @@ -0,0 +1 @@ +bfs_diff -L loops/deeply/nested -unique diff --git a/tests/test_O2.out b/tests/bfs/O0.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_O2.out +++ b/tests/bfs/O0.out diff --git a/tests/bfs/O0.sh b/tests/bfs/O0.sh new file mode 100644 index 0000000..0f92d71 --- /dev/null +++ b/tests/bfs/O0.sh @@ -0,0 +1 @@ +bfs_diff -O0 basic -not \( -type f -not -type f \) diff --git a/tests/test_O3.out b/tests/bfs/O1.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_O3.out +++ b/tests/bfs/O1.out diff --git a/tests/bfs/O1.sh b/tests/bfs/O1.sh new file mode 100644 index 0000000..924b410 --- /dev/null +++ b/tests/bfs/O1.sh @@ -0,0 +1 @@ +bfs_diff -O1 basic -not \( -type f -not -type f \) diff --git a/tests/test_Ofast.out b/tests/bfs/O2.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_Ofast.out +++ b/tests/bfs/O2.out diff --git a/tests/bfs/O2.sh b/tests/bfs/O2.sh new file mode 100644 index 0000000..9382456 --- /dev/null +++ b/tests/bfs/O2.sh @@ -0,0 +1 @@ +bfs_diff -O2 basic -not \( -type f -not -type f \) diff --git a/tests/test_S_dfs.out b/tests/bfs/O3.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_S_dfs.out +++ b/tests/bfs/O3.out diff --git a/tests/bfs/O3.sh b/tests/bfs/O3.sh new file mode 100644 index 0000000..5bdf2bc --- /dev/null +++ b/tests/bfs/O3.sh @@ -0,0 +1 @@ +bfs_diff -O3 basic -not \( -type f -not -type f \) diff --git a/tests/test_basic.out b/tests/bfs/O9.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_basic.out +++ b/tests/bfs/O9.out diff --git a/tests/bfs/O9.sh b/tests/bfs/O9.sh new file mode 100644 index 0000000..c12a7a3 --- /dev/null +++ b/tests/bfs/O9.sh @@ -0,0 +1,4 @@ +stderr=$(invoke_bfs -warn -O9 basic 2>&1 >"$OUT") +[ -n "$stderr" ] +sort_output +diff_output 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/test_closed_stdin.out b/tests/bfs/Ofast.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_closed_stdin.out +++ b/tests/bfs/Ofast.out diff --git a/tests/bfs/Ofast.sh b/tests/bfs/Ofast.sh new file mode 100644 index 0000000..87c1d8d --- /dev/null +++ b/tests/bfs/Ofast.sh @@ -0,0 +1 @@ +bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \) diff --git a/tests/test_S_bfs.out b/tests/bfs/S_bfs.out index bb3cd8d..bb3cd8d 100644 --- a/tests/test_S_bfs.out +++ b/tests/bfs/S_bfs.out diff --git a/tests/bfs/S_bfs.sh b/tests/bfs/S_bfs.sh new file mode 100644 index 0000000..76976de --- /dev/null +++ b/tests/bfs/S_bfs.sh @@ -0,0 +1,2 @@ +invoke_bfs -S bfs -s basic >"$OUT" +diff_output diff --git a/tests/test_d_path.out b/tests/bfs/S_dfs.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_d_path.out +++ b/tests/bfs/S_dfs.out diff --git a/tests/bfs/S_dfs.sh b/tests/bfs/S_dfs.sh new file mode 100644 index 0000000..7dd7a46 --- /dev/null +++ b/tests/bfs/S_dfs.sh @@ -0,0 +1,2 @@ +invoke_bfs -S dfs -s basic >"$OUT" +diff_output diff --git a/tests/test_S_ids.out b/tests/bfs/S_ids.out index bb3cd8d..bb3cd8d 100644 --- a/tests/test_S_ids.out +++ b/tests/bfs/S_ids.out diff --git a/tests/bfs/S_ids.sh b/tests/bfs/S_ids.sh new file mode 100644 index 0000000..3995cf0 --- /dev/null +++ b/tests/bfs/S_ids.sh @@ -0,0 +1,2 @@ +invoke_bfs -S ids -s basic >"$OUT" +diff_output 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/and_incomplete.sh b/tests/bfs/and_incomplete.sh new file mode 100644 index 0000000..05abc2d --- /dev/null +++ b/tests/bfs/and_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs -print -a diff --git a/tests/bfs/capable.out b/tests/bfs/capable.out new file mode 100644 index 0000000..ac7b5ce --- /dev/null +++ b/tests/bfs/capable.out @@ -0,0 +1 @@ +./capable diff --git a/tests/bfs/capable.sh b/tests/bfs/capable.sh new file mode 100644 index 0000000..35bb0b4 --- /dev/null +++ b/tests/bfs/capable.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 + +bfs_diff . -capable diff --git a/tests/bfs/closed_stderr.sh b/tests/bfs/closed_stderr.sh new file mode 100644 index 0000000..26abd85 --- /dev/null +++ b/tests/bfs/closed_stderr.sh @@ -0,0 +1,4 @@ +# Check if the platform automatically re-opens stderr before we can +(bash -c 'echo >&2' 2>&-) && skip + +! invoke_bfs basic >&- 2>&- diff --git a/tests/test_data_flow_group.out b/tests/bfs/closed_stdin.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_data_flow_group.out +++ b/tests/bfs/closed_stdin.out diff --git a/tests/bfs/closed_stdin.sh b/tests/bfs/closed_stdin.sh new file mode 100644 index 0000000..6932be8 --- /dev/null +++ b/tests/bfs/closed_stdin.sh @@ -0,0 +1 @@ +bfs_diff basic <&- diff --git a/tests/bfs/closed_stdout.sh b/tests/bfs/closed_stdout.sh new file mode 100644 index 0000000..5b6f7c3 --- /dev/null +++ b/tests/bfs/closed_stdout.sh @@ -0,0 +1,4 @@ +# Check if the platform automatically re-opens stdout before we can +(bash -c echo >&-) && skip + +! invoke_bfs basic >&- diff --git a/tests/test_color.out b/tests/bfs/color.out index 77fc8a8..a439814 100644 --- a/tests/test_color.out +++ b/tests/bfs/color.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color.sh b/tests/bfs/color.sh new file mode 100644 index 0000000..23f05a3 --- /dev/null +++ b/tests/bfs/color.sh @@ -0,0 +1 @@ +bfs_diff rainbow -color diff --git a/tests/test_color_L.out b/tests/bfs/color_L.out index b60dd4a..85923db 100644 --- a/tests/test_color_L.out +++ b/tests/bfs/color_L.out @@ -1,3 +1,5 @@ +[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;33mchardev_link[0m @@ -13,8 +15,13 @@ [01;34mrainbow/[0mfile.dat [01;34mrainbow/[0mfile.txt [01;34mrainbow/[0mlink.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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_L.sh b/tests/bfs/color_L.sh new file mode 100644 index 0000000..823db62 --- /dev/null +++ b/tests/bfs/color_L.sh @@ -0,0 +1 @@ +bfs_diff -L rainbow -color diff --git a/tests/test_color_ln_target.out b/tests/bfs/color_L_ln_target.out index cd4ec5e..23fe8d7 100644 --- a/tests/test_color_ln_target.out +++ b/tests/bfs/color_L_ln_target.out @@ -1,3 +1,5 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[01;31mbroken[0m [01;34mrainbow/[0m[01;32mexec.sh[0m @@ -13,8 +15,13 @@ [01;34mrainbow/[0mfile.dat [01;34mrainbow/[0mfile.txt [01;34mrainbow/[0mlink.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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_L_ln_target.sh b/tests/bfs/color_L_ln_target.sh new file mode 100644 index 0000000..cc5991d --- /dev/null +++ b/tests/bfs/color_L_ln_target.sh @@ -0,0 +1 @@ +LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color diff --git a/tests/test_color_L_no_stat.out b/tests/bfs/color_L_no_stat.out index c0bb1be..72e0319 100644 --- a/tests/test_color_L_no_stat.out +++ b/tests/bfs/color_L_no_stat.out @@ -1,8 +1,7 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[01;33mchardev_link[0m -[01;34mrainbow/[0m[01;34mow[0m -[01;34mrainbow/[0m[01;34msticky[0m -[01;34mrainbow/[0m[01;34msticky_ow[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01mfile.txt[0m @@ -10,11 +9,19 @@ [01;34mrainbow/[0m[33mpipe[0m [01;34mrainbow/[0mexec.sh [01;34mrainbow/[0mfile.dat +[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/[0msgid -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz [01;34mrainbow/[0msugid [01;34mrainbow/[0msuid +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/ow[0m +[01;34mrainbow/sticky[0m +[01;34mrainbow/sticky_ow[0m diff --git a/tests/bfs/color_L_no_stat.sh b/tests/bfs/color_L_no_stat.sh new file mode 100644 index 0000000..0a2caf0 --- /dev/null +++ b/tests/bfs/color_L_no_stat.sh @@ -0,0 +1 @@ +LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color diff --git a/tests/test_color_ext_override.out b/tests/bfs/color_auto.out index 1377b65..a439814 100644 --- a/tests/test_color_ext_override.out +++ b/tests/bfs/color_auto.out @@ -1,8 +1,7 @@ +[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;32mstar.tar[0m -[01;34mrainbow/[0m[01;33mstar.gz[0m -[01;34mrainbow/[0m[01;33mstar.tar.gz[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -16,5 +15,13 @@ [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_auto.sh b/tests/bfs/color_auto.sh new file mode 100644 index 0000000..7e875cc --- /dev/null +++ b/tests/bfs/color_auto.sh @@ -0,0 +1,4 @@ +unset NO_COLOR +bfs_pty rainbow >"$OUT" +sort_output +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/test_color_ext_underride.out b/tests/bfs/color_ca_incapable.out index 787248a..a439814 100644 --- a/tests/test_color_ext_underride.out +++ b/tests/bfs/color_ca_incapable.out @@ -1,8 +1,7 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m -[01;34mrainbow/[0m[01;31mstar.tar.gz[0m [01;34mrainbow/[0m[01;32mexec.sh[0m -[01;34mrainbow/[0m[01;32mstar.tar[0m -[01;34mrainbow/[0m[01;33mstar.gz[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -16,5 +15,13 @@ [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_cd0_no.out b/tests/bfs/color_cd0_no.out new file mode 100644 index 0000000..37b3fbc --- /dev/null +++ b/tests/bfs/color_cd0_no.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m[01;92m$'\e[0m'[0m +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;92mbroken[0m +[01;34mrainbow/[0m[01;92mfile.dat[0m +[01;34mrainbow/[0m[01;92mfile.txt[0m +[01;34mrainbow/[0m[01;92mlink.txt[0m +[01;34mrainbow/[0m[01;92mlower.gz[0m +[01;34mrainbow/[0m[01;92mlower.tar[0m +[01;34mrainbow/[0m[01;92mlower.tar.gz[0m +[01;34mrainbow/[0m[01;92mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;92mmh1[0m +[01;34mrainbow/[0m[01;92mmh2[0m +[01;34mrainbow/[0m[01;92mul.TAR.gz[0m +[01;34mrainbow/[0m[01;92mupper.GZ[0m +[01;34mrainbow/[0m[01;92mupper.TAR[0m +[01;34mrainbow/[0m[01;92mupper.TAR.GZ[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/[0mchardev_link diff --git a/tests/bfs/color_cd0_no.sh b/tests/bfs/color_cd0_no.sh new file mode 100644 index 0000000..325a782 --- /dev/null +++ b/tests/bfs/color_cd0_no.sh @@ -0,0 +1 @@ +LS_COLORS="ln=target:cd=0:no=01;92:" bfs_diff rainbow -color diff --git a/tests/bfs/color_deep.out b/tests/bfs/color_deep.out new file mode 100644 index 0000000..fb990d5 --- /dev/null +++ b/tests/bfs/color_deep.out @@ -0,0 +1,16 @@ +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m +[01m0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE[0m diff --git a/tests/bfs/color_deep.sh b/tests/bfs/color_deep.sh new file mode 100644 index 0000000..a83ee0e --- /dev/null +++ b/tests/bfs/color_deep.sh @@ -0,0 +1,7 @@ +name="0123456789ABCDEF" +name="${name}${name}${name}${name}" +name="${name}${name}${name}${name}" +name="${name:0:255}" +export LS_COLORS="*${name}=01:" + +bfs_diff deep -color -type f -printf '%f\n' diff --git a/tests/test_color_escapes.out b/tests/bfs/color_escapes.out index b71e138..0bf9fbb 100644 --- a/tests/test_color_escapes.out +++ b/tests/bfs/color_escapes.out @@ -1,3 +1,5 @@ +[01;34m:$'rainbow/\e[1m'[m +[01;34m:$'rainbow/\e[1m/'[m$'\e[0m' [01;34m:rainbow[m [01;34m:rainbow/[m[01;32m:exec.sh[m [01;34m:rainbow/[m[01;35m:socket[m @@ -13,8 +15,13 @@ [01;34m:rainbow/[m[37;44m:sticky[m [01;34m:rainbow/[mfile.dat [01;34m:rainbow/[mfile.txt +[01;34m:rainbow/[mlower.gz +[01;34m:rainbow/[mlower.tar +[01;34m:rainbow/[mlower.tar.gz +[01;34m:rainbow/[mlu.tar.GZ [01;34m:rainbow/[mmh1 [01;34m:rainbow/[mmh2 -[01;34m:rainbow/[mstar.gz -[01;34m:rainbow/[mstar.tar -[01;34m:rainbow/[mstar.tar.gz +[01;34m:rainbow/[mul.TAR.gz +[01;34m:rainbow/[mupper.GZ +[01;34m:rainbow/[mupper.TAR +[01;34m:rainbow/[mupper.TAR.GZ diff --git a/tests/bfs/color_escapes.sh b/tests/bfs/color_escapes.sh new file mode 100644 index 0000000..eb5817f --- /dev/null +++ b/tests/bfs/color_escapes.sh @@ -0,0 +1 @@ +LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color diff --git a/tests/test_color_missing_colon.out b/tests/bfs/color_ext.out index cf26e73..218100f 100644 --- a/tests/test_color_missing_colon.out +++ b/tests/bfs/color_ext.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [01;34mrainbow/[0m[37;41msuid[0m [01;34mrainbow/[0m[37;44msticky[0m [01;34mrainbow/[0mfile.dat +[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_ext.sh b/tests/bfs/color_ext.sh new file mode 100644 index 0000000..c9f6d46 --- /dev/null +++ b/tests/bfs/color_ext.sh @@ -0,0 +1 @@ +LS_COLORS="*.txt=01:" bfs_diff rainbow -color diff --git a/tests/test_color_ext0.out b/tests/bfs/color_ext0.out index e764a6b..d2a7fd5 100644 --- a/tests/test_color_ext0.out +++ b/tests/bfs/color_ext0.out @@ -1,3 +1,5 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[00mfile.txt[0m [01;34mrainbow/[0m[01;32mexec.sh[0m @@ -13,8 +15,13 @@ [01;34mrainbow/[0m[37;41msuid[0m [01;34mrainbow/[0m[37;44msticky[0m [01;34mrainbow/[0mfile.dat +[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_ext0.sh b/tests/bfs/color_ext0.sh new file mode 100644 index 0000000..371a9c5 --- /dev/null +++ b/tests/bfs/color_ext0.sh @@ -0,0 +1 @@ +LS_COLORS="*.txt=00:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case.out b/tests/bfs/color_ext_case.out new file mode 100644 index 0000000..93dc8f6 --- /dev/null +++ b/tests/bfs/color_ext_case.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;32mexec.sh[0m +[01;34mrainbow/[0m[01;32mupper.GZ[0m +[01;34mrainbow/[0m[01;32mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;33mlower.tar[0m +[01;34mrainbow/[0m[01;33mupper.TAR[0m +[01;34mrainbow/[0m[01;34mul.TAR.gz[0m +[01;34mrainbow/[0m[01;35mlu.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[36mfile.txt[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/[0mmh1 +[01;34mrainbow/[0mmh2 diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh new file mode 100644 index 0000000..4c14610 --- /dev/null +++ b/tests/bfs/color_ext_case.sh @@ -0,0 +1,6 @@ +# *.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.out b/tests/bfs/color_ext_override.out new file mode 100644 index 0000000..0acfcbc --- /dev/null +++ b/tests/bfs/color_ext_override.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;32mlower.tar[0m +[01;34mrainbow/[0m[01;32mupper.TAR[0m +[01;34mrainbow/[0m[01;33mlower.gz[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.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/[0mmh1 +[01;34mrainbow/[0mmh2 diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh new file mode 100644 index 0000000..9f818c9 --- /dev/null +++ b/tests/bfs/color_ext_override.sh @@ -0,0 +1 @@ +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_ext_underride.out b/tests/bfs/color_ext_underride.out new file mode 100644 index 0000000..5c98341 --- /dev/null +++ b/tests/bfs/color_ext_underride.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;31mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;31mul.TAR.gz[0m +[01;34mrainbow/[0m[01;31mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;32mlower.tar[0m +[01;34mrainbow/[0m[01;32mupper.TAR[0m +[01;34mrainbow/[0m[01;33mlower.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/[0mmh1 +[01;34mrainbow/[0mmh2 diff --git a/tests/bfs/color_ext_underride.sh b/tests/bfs/color_ext_underride.sh new file mode 100644 index 0000000..fb12e01 --- /dev/null +++ b/tests/bfs/color_ext_underride.sh @@ -0,0 +1 @@ +LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color diff --git a/tests/test_color_mh0.out b/tests/bfs/color_fi0_no.out index 77fc8a8..a439814 100644 --- a/tests/test_color_mh0.out +++ b/tests/bfs/color_fi0_no.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_fi0_no.sh b/tests/bfs/color_fi0_no.sh new file mode 100644 index 0000000..f947d64 --- /dev/null +++ b/tests/bfs/color_fi0_no.sh @@ -0,0 +1 @@ +LS_COLORS="fi=0:no=01;92:" bfs_diff rainbow -color diff --git a/tests/bfs/color_fi_no.out b/tests/bfs/color_fi_no.out new file mode 100644 index 0000000..1c1ad8e --- /dev/null +++ b/tests/bfs/color_fi_no.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m[01;91m$'\e[0m'[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[01;91mfile.dat[0m +[01;34mrainbow/[0m[01;91mfile.txt[0m +[01;34mrainbow/[0m[01;91mlower.gz[0m +[01;34mrainbow/[0m[01;91mlower.tar[0m +[01;34mrainbow/[0m[01;91mlower.tar.gz[0m +[01;34mrainbow/[0m[01;91mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;91mmh1[0m +[01;34mrainbow/[0m[01;91mmh2[0m +[01;34mrainbow/[0m[01;91mul.TAR.gz[0m +[01;34mrainbow/[0m[01;91mupper.GZ[0m +[01;34mrainbow/[0m[01;91mupper.TAR[0m +[01;34mrainbow/[0m[01;91mupper.TAR.GZ[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 diff --git a/tests/bfs/color_fi_no.sh b/tests/bfs/color_fi_no.sh new file mode 100644 index 0000000..c2b4ec7 --- /dev/null +++ b/tests/bfs/color_fi_no.sh @@ -0,0 +1 @@ +LS_COLORS="fi=01;91:no=01;92:" bfs_diff rainbow -color diff --git a/tests/test_color_L_ln_target.out b/tests/bfs/color_ln_target.out index cd4ec5e..23fe8d7 100644 --- a/tests/test_color_L_ln_target.out +++ b/tests/bfs/color_ln_target.out @@ -1,3 +1,5 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[01;31mbroken[0m [01;34mrainbow/[0m[01;32mexec.sh[0m @@ -13,8 +15,13 @@ [01;34mrainbow/[0mfile.dat [01;34mrainbow/[0mfile.txt [01;34mrainbow/[0mlink.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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_ln_target.sh b/tests/bfs/color_ln_target.sh new file mode 100644 index 0000000..707d25e --- /dev/null +++ b/tests/bfs/color_ln_target.sh @@ -0,0 +1 @@ +LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ls.out b/tests/bfs/color_ls.out new file mode 100644 index 0000000..cc64318 --- /dev/null +++ b/tests/bfs/color_ls.out @@ -0,0 +1,12 @@ +[01;31mscratch/foo/bar[0m +[01;31mscratch/foo/bar[0m +[01;34m/[0m[01;31m__bfs__/nowhere[0m +[01;34m/[0m[01;31m__bfs__/nowhere[0m +[01;34mfoo/bar/[0m[01;31mnowhere[0m +[01;34mfoo/bar/[0m[01;31mnowhere[0m +[01;34mfoo/bar/[0m[01;31mnowhere/nothing[0m +[01;34mfoo/bar/[0m[01;31mnowhere/nothing[0m +[01;34mfoo/bar/[0mbaz +[01;34mfoo/bar/[0mbaz +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m diff --git a/tests/bfs/color_ls.sh b/tests/bfs/color_ls.sh new file mode 100644 index 0000000..f1cc216 --- /dev/null +++ b/tests/bfs/color_ls.sh @@ -0,0 +1,15 @@ +cd "$TEST" +"$XTOUCH" -p scratch/foo/bar/baz +ln -s foo/bar/baz scratch/link +ln -s foo/bar/nowhere scratch/broken +ln -s foo/bar/nowhere/nothing scratch/nested +ln -s foo/bar/baz//qux scratch/notdir +ln -s scratch/foo/bar scratch/relative +mkdir scratch/__bfs__ +ln -s /__bfs__/nowhere scratch/absolute + +export LS_COLORS="or=01;31:" +invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \ + | sed 's/.* -> //' \ + | sort >"$OUT" +diff_output diff --git a/tests/test_color_mh.out b/tests/bfs/color_mh.out index 757a6a1..c658082 100644 --- a/tests/test_color_mh.out +++ b/tests/bfs/color_mh.out @@ -1,3 +1,5 @@ +[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 @@ -15,6 +17,11 @@ [01;34mrainbow/[0m[37;44msticky[0m [01;34mrainbow/[0mfile.dat [01;34mrainbow/[0mfile.txt -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mlower.gz +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mlower.tar.gz +[01;34mrainbow/[0mlu.tar.GZ +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_mh.sh b/tests/bfs/color_mh.sh new file mode 100644 index 0000000..aff1845 --- /dev/null +++ b/tests/bfs/color_mh.sh @@ -0,0 +1 @@ +LS_COLORS="mh=01:" bfs_diff rainbow -color diff --git a/tests/bfs/color_mh0.out b/tests/bfs/color_mh0.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_mh0.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_mh0.sh b/tests/bfs/color_mh0.sh new file mode 100644 index 0000000..7de880d --- /dev/null +++ b/tests/bfs/color_mh0.sh @@ -0,0 +1 @@ +LS_COLORS="mh=00:" bfs_diff rainbow -color diff --git a/tests/bfs/color_mi.out b/tests/bfs/color_mi.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_mi.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_mi.sh b/tests/bfs/color_mi.sh new file mode 100644 index 0000000..06dd8c6 --- /dev/null +++ b/tests/bfs/color_mi.sh @@ -0,0 +1 @@ +LS_COLORS="mi=01:" bfs_diff rainbow -color diff --git a/tests/test_color_ext.out b/tests/bfs/color_missing_colon.out index cf26e73..218100f 100644 --- a/tests/test_color_ext.out +++ b/tests/bfs/color_missing_colon.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [01;34mrainbow/[0m[37;41msuid[0m [01;34mrainbow/[0m[37;44msticky[0m [01;34mrainbow/[0mfile.dat +[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_missing_colon.sh b/tests/bfs/color_missing_colon.sh new file mode 100644 index 0000000..afa3763 --- /dev/null +++ b/tests/bfs/color_missing_colon.sh @@ -0,0 +1 @@ +LS_COLORS="*.txt=01" bfs_diff rainbow -color diff --git a/tests/bfs/color_no.out b/tests/bfs/color_no.out new file mode 100644 index 0000000..67e1eee --- /dev/null +++ b/tests/bfs/color_no.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m[01;92m$'\e[0m'[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[01;92mfile.dat[0m +[01;34mrainbow/[0m[01;92mfile.txt[0m +[01;34mrainbow/[0m[01;92mlower.gz[0m +[01;34mrainbow/[0m[01;92mlower.tar[0m +[01;34mrainbow/[0m[01;92mlower.tar.gz[0m +[01;34mrainbow/[0m[01;92mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;92mmh1[0m +[01;34mrainbow/[0m[01;92mmh2[0m +[01;34mrainbow/[0m[01;92mul.TAR.gz[0m +[01;34mrainbow/[0m[01;92mupper.GZ[0m +[01;34mrainbow/[0m[01;92mupper.TAR[0m +[01;34mrainbow/[0m[01;92mupper.TAR.GZ[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 diff --git a/tests/bfs/color_no.sh b/tests/bfs/color_no.sh new file mode 100644 index 0000000..b7527cb --- /dev/null +++ b/tests/bfs/color_no.sh @@ -0,0 +1 @@ +LS_COLORS="no=01;92:" bfs_diff rainbow -color diff --git a/tests/test_color_no_stat.out b/tests/bfs/color_no_stat.out index 1fc5324..e3031b2 100644 --- a/tests/test_color_no_stat.out +++ b/tests/bfs/color_no_stat.out @@ -1,7 +1,6 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m -[01;34mrainbow/[0m[01;34mow[0m -[01;34mrainbow/[0m[01;34msticky[0m -[01;34mrainbow/[0m[01;34msticky_ow[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -10,11 +9,19 @@ [01;34mrainbow/[0m[33mpipe[0m [01;34mrainbow/[0mexec.sh [01;34mrainbow/[0mfile.dat +[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/[0msgid -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz [01;34mrainbow/[0msugid [01;34mrainbow/[0msuid +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/ow[0m +[01;34mrainbow/sticky[0m +[01;34mrainbow/sticky_ow[0m diff --git a/tests/bfs/color_no_stat.sh b/tests/bfs/color_no_stat.sh new file mode 100644 index 0000000..0bc2520 --- /dev/null +++ b/tests/bfs/color_no_stat.sh @@ -0,0 +1 @@ +LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color diff --git a/tests/test_L_ilname.out b/tests/bfs/color_notdir_slash_error.out index e69de29..e69de29 100644 --- a/tests/test_L_ilname.out +++ 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/color_nul.out b/tests/bfs/color_nul.out new file mode 100644 index 0000000..8ccd9a7 --- /dev/null +++ b/tests/bfs/color_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_nul.sh b/tests/bfs/color_nul.sh new file mode 100644 index 0000000..cb662d6 --- /dev/null +++ b/tests/bfs/color_nul.sh @@ -0,0 +1,3 @@ +LS_COLORS="ec=\33[\0m:*.gz=\0\61;31:" invoke_bfs rainbow -color | tr '\0' '0' >"$OUT" +sort_output +diff_output diff --git a/tests/test_color_or.out b/tests/bfs/color_or.out index 9e1fe5c..0bd2570 100644 --- a/tests/test_color_or.out +++ b/tests/bfs/color_or.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_or.sh b/tests/bfs/color_or.sh new file mode 100644 index 0000000..bccb400 --- /dev/null +++ b/tests/bfs/color_or.sh @@ -0,0 +1 @@ +LS_COLORS="or=01:" bfs_diff rainbow -color diff --git a/tests/bfs/color_or0_mi.out b/tests/bfs/color_or0_mi.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_or0_mi.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_or0_mi.sh b/tests/bfs/color_or0_mi.sh new file mode 100644 index 0000000..a362cf1 --- /dev/null +++ b/tests/bfs/color_or0_mi.sh @@ -0,0 +1 @@ +LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_or0_mi0.out b/tests/bfs/color_or0_mi0.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_or0_mi0.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_or0_mi0.sh b/tests/bfs/color_or0_mi0.sh new file mode 100644 index 0000000..d7c00f6 --- /dev/null +++ b/tests/bfs/color_or0_mi0.sh @@ -0,0 +1 @@ +LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color diff --git a/tests/test_color_or_mi.out b/tests/bfs/color_or_mi.out index 5667f56..fb67e58 100644 --- a/tests/test_color_or_mi.out +++ b/tests/bfs/color_or_mi.out @@ -1,3 +1,5 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[01;31mbroken[0m [01;34mrainbow/[0m[01;32mexec.sh[0m @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_or_mi.sh b/tests/bfs/color_or_mi.sh new file mode 100644 index 0000000..467ce6b --- /dev/null +++ b/tests/bfs/color_or_mi.sh @@ -0,0 +1 @@ +LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color diff --git a/tests/test_color_or_mi0.out b/tests/bfs/color_or_mi0.out index 5667f56..fb67e58 100644 --- a/tests/test_color_or_mi0.out +++ b/tests/bfs/color_or_mi0.out @@ -1,3 +1,5 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' [01;34mrainbow[0m [01;34mrainbow/[0m[01;31mbroken[0m [01;34mrainbow/[0m[01;32mexec.sh[0m @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_or_mi0.sh b/tests/bfs/color_or_mi0.sh new file mode 100644 index 0000000..a9c36bf --- /dev/null +++ b/tests/bfs/color_or_mi0.sh @@ -0,0 +1 @@ +LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color diff --git a/tests/test_color_rs_lc_rc_ec.out b/tests/bfs/color_rs_lc_rc_ec.out index d39bbe7..077ef8d 100644 --- a/tests/test_color_rs_lc_rc_ec.out +++ b/tests/bfs/color_rs_lc_rc_ec.out @@ -1,3 +1,5 @@ +LC01;34RC$'rainbow/\e[1m'EC +LC01;34RC$'rainbow/\e[1m/'EC$'\e[0m' LC01;34RCrainbow/ECLC01;32RCexec.shEC LC01;34RCrainbow/ECLC01;35RCsocketEC LC01;34RCrainbow/ECLC01;36RCbrokenEC @@ -12,9 +14,14 @@ LC01;34RCrainbow/ECLC37;41RCsuidEC LC01;34RCrainbow/ECLC37;44RCstickyEC LC01;34RCrainbow/ECfile.dat LC01;34RCrainbow/ECfile.txt +LC01;34RCrainbow/EClower.gz +LC01;34RCrainbow/EClower.tar +LC01;34RCrainbow/EClower.tar.gz +LC01;34RCrainbow/EClu.tar.GZ LC01;34RCrainbow/ECmh1 LC01;34RCrainbow/ECmh2 -LC01;34RCrainbow/ECstar.gz -LC01;34RCrainbow/ECstar.tar -LC01;34RCrainbow/ECstar.tar.gz +LC01;34RCrainbow/ECul.TAR.gz +LC01;34RCrainbow/ECupper.GZ +LC01;34RCrainbow/ECupper.TAR +LC01;34RCrainbow/ECupper.TAR.GZ LC01;34RCrainbowEC diff --git a/tests/bfs/color_rs_lc_rc_ec.sh b/tests/bfs/color_rs_lc_rc_ec.sh new file mode 100644 index 0000000..467b2da --- /dev/null +++ b/tests/bfs/color_rs_lc_rc_ec.sh @@ -0,0 +1 @@ +LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color diff --git a/tests/test_color_st0_tw0_ow.out b/tests/bfs/color_st0_tw0_ow.out index 9a47ef2..a82762b 100644 --- a/tests/test_color_st0_tw0_ow.out +++ b/tests/bfs/color_st0_tw0_ow.out @@ -1,6 +1,7 @@ +[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;34msticky[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -13,8 +14,14 @@ [01;34mrainbow/[0m[37;41msuid[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/sticky[0m diff --git a/tests/bfs/color_st0_tw0_ow.sh b/tests/bfs/color_st0_tw0_ow.sh new file mode 100644 index 0000000..8e2b8e3 --- /dev/null +++ b/tests/bfs/color_st0_tw0_ow.sh @@ -0,0 +1 @@ +LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color diff --git a/tests/bfs/color_st0_tw0_ow0.out b/tests/bfs/color_st0_tw0_ow0.out new file mode 100644 index 0000000..041f1d4 --- /dev/null +++ b/tests/bfs/color_st0_tw0_ow0.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;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[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 +[01;34mrainbow/ow[0m +[01;34mrainbow/sticky[0m +[01;34mrainbow/sticky_ow[0m diff --git a/tests/bfs/color_st0_tw0_ow0.sh b/tests/bfs/color_st0_tw0_ow0.sh new file mode 100644 index 0000000..c5d5fe7 --- /dev/null +++ b/tests/bfs/color_st0_tw0_ow0.sh @@ -0,0 +1 @@ +LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color diff --git a/tests/test_color_st0_tw_ow.out b/tests/bfs/color_st0_tw_ow.out index 42549a1..4dcb2f2 100644 --- a/tests/test_color_st0_tw_ow.out +++ b/tests/bfs/color_st0_tw_ow.out @@ -1,6 +1,7 @@ +[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;34msticky[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -13,8 +14,14 @@ [01;34mrainbow/[0m[40;32msticky_ow[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/sticky[0m diff --git a/tests/bfs/color_st0_tw_ow.sh b/tests/bfs/color_st0_tw_ow.sh new file mode 100644 index 0000000..8fd9605 --- /dev/null +++ b/tests/bfs/color_st0_tw_ow.sh @@ -0,0 +1 @@ +LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color diff --git a/tests/test_color_st0_tw_ow0.out b/tests/bfs/color_st0_tw_ow0.out index 535b8ae..954ce9c 100644 --- a/tests/test_color_st0_tw_ow0.out +++ b/tests/bfs/color_st0_tw_ow0.out @@ -1,7 +1,7 @@ +[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;34mow[0m -[01;34mrainbow/[0m[01;34msticky[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -13,8 +13,15 @@ [01;34mrainbow/[0m[40;32msticky_ow[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/ow[0m +[01;34mrainbow/sticky[0m diff --git a/tests/bfs/color_st0_tw_ow0.sh b/tests/bfs/color_st0_tw_ow0.sh new file mode 100644 index 0000000..68c63dc --- /dev/null +++ b/tests/bfs/color_st0_tw_ow0.sh @@ -0,0 +1 @@ +LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color diff --git a/tests/test_color_st_tw0_ow.out b/tests/bfs/color_st_tw0_ow.out index c9a86f4..a6e9a16 100644 --- a/tests/test_color_st_tw0_ow.out +++ b/tests/bfs/color_st_tw0_ow.out @@ -1,3 +1,5 @@ +[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 @@ -13,8 +15,13 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_st_tw0_ow.sh b/tests/bfs/color_st_tw0_ow.sh new file mode 100644 index 0000000..be16251 --- /dev/null +++ b/tests/bfs/color_st_tw0_ow.sh @@ -0,0 +1 @@ +LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color diff --git a/tests/test_color_st_tw0_ow0.out b/tests/bfs/color_st_tw0_ow0.out index 2d94f3a..756dafb 100644 --- a/tests/test_color_st_tw0_ow0.out +++ b/tests/bfs/color_st_tw0_ow0.out @@ -1,6 +1,7 @@ +[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;34mow[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -13,8 +14,14 @@ [01;34mrainbow/[0m[37;44msticky_ow[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/ow[0m diff --git a/tests/bfs/color_st_tw0_ow0.sh b/tests/bfs/color_st_tw0_ow0.sh new file mode 100644 index 0000000..f869e7c --- /dev/null +++ b/tests/bfs/color_st_tw0_ow0.sh @@ -0,0 +1 @@ +LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color diff --git a/tests/test_color_st_tw_ow0.out b/tests/bfs/color_st_tw_ow0.out index 317ef90..6e4a260 100644 --- a/tests/test_color_st_tw_ow0.out +++ b/tests/bfs/color_st_tw_ow0.out @@ -1,6 +1,7 @@ +[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;34mow[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mchardev_link[0m @@ -13,8 +14,14 @@ [01;34mrainbow/[0m[40;32msticky_ow[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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ +[01;34mrainbow/ow[0m diff --git a/tests/bfs/color_st_tw_ow0.sh b/tests/bfs/color_st_tw_ow0.sh new file mode 100644 index 0000000..99a17a6 --- /dev/null +++ b/tests/bfs/color_st_tw_ow0.sh @@ -0,0 +1 @@ +LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color diff --git a/tests/bfs/color_star.sh b/tests/bfs/color_star.sh new file mode 100644 index 0000000..6d5312e --- /dev/null +++ b/tests/bfs/color_star.sh @@ -0,0 +1,2 @@ +# Regression test: don't segfault on LS_COLORS="*" +! LS_COLORS="*" invoke_bfs rainbow -color diff --git a/tests/test_color_su0_sg.out b/tests/bfs/color_su0_sg.out index 8b8c8b8..d13b6b6 100644 --- a/tests/test_color_su0_sg.out +++ b/tests/bfs/color_su0_sg.out @@ -1,3 +1,5 @@ +[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 @@ -12,9 +14,14 @@ [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/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz [01;34mrainbow/[0msuid +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_su0_sg.sh b/tests/bfs/color_su0_sg.sh new file mode 100644 index 0000000..f5f57b4 --- /dev/null +++ b/tests/bfs/color_su0_sg.sh @@ -0,0 +1 @@ +LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color diff --git a/tests/test_color_su0_sg0.out b/tests/bfs/color_su0_sg0.out index 0cd5f9a..77fba58 100644 --- a/tests/test_color_su0_sg0.out +++ b/tests/bfs/color_su0_sg0.out @@ -1,3 +1,5 @@ +[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 @@ -10,11 +12,16 @@ [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/[0msgid -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz [01;34mrainbow/[0msugid [01;34mrainbow/[0msuid +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_su0_sg0.sh b/tests/bfs/color_su0_sg0.sh new file mode 100644 index 0000000..0198383 --- /dev/null +++ b/tests/bfs/color_su0_sg0.sh @@ -0,0 +1 @@ +LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color diff --git a/tests/test_color_su_sg0.out b/tests/bfs/color_su_sg0.out index a9e8c5d..8fab046 100644 --- a/tests/test_color_su_sg0.out +++ b/tests/bfs/color_su_sg0.out @@ -1,3 +1,5 @@ +[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 @@ -12,9 +14,14 @@ [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/[0msgid -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_su_sg0.sh b/tests/bfs/color_su_sg0.sh new file mode 100644 index 0000000..8dc6984 --- /dev/null +++ b/tests/bfs/color_su_sg0.sh @@ -0,0 +1 @@ +LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color diff --git a/tests/bfs/comma_incomplete.sh b/tests/bfs/comma_incomplete.sh new file mode 100644 index 0000000..bd60168 --- /dev/null +++ b/tests/bfs/comma_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs -print , diff --git a/tests/test_data_flow_hidden.out b/tests/bfs/data_flow_hidden.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_data_flow_hidden.out +++ b/tests/bfs/data_flow_hidden.out diff --git a/tests/bfs/data_flow_hidden.sh b/tests/bfs/data_flow_hidden.sh new file mode 100644 index 0000000..6afaab2 --- /dev/null +++ b/tests/bfs/data_flow_hidden.sh @@ -0,0 +1 @@ +bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \) diff --git a/tests/test_deep.out b/tests/bfs/deep_strict.out index c385fce..c385fce 100644 --- a/tests/test_deep.out +++ b/tests/bfs/deep_strict.out diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh new file mode 100644 index 0000000..22453c0 --- /dev/null +++ b/tests/bfs/deep_strict.sh @@ -0,0 +1,3 @@ +# Not even enough fds to keep the root open +ulimit -n $((NOPENFD + 4)) +bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; diff --git a/tests/test_exclude_depth.out b/tests/bfs/exclude_depth.out index 59e3c42..59e3c42 100644 --- a/tests/test_exclude_depth.out +++ b/tests/bfs/exclude_depth.out diff --git a/tests/bfs/exclude_depth.sh b/tests/bfs/exclude_depth.sh new file mode 100644 index 0000000..437b4dd --- /dev/null +++ b/tests/bfs/exclude_depth.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -exclude -name foo diff --git a/tests/bfs/exclude_exclude.sh b/tests/bfs/exclude_exclude.sh new file mode 100644 index 0000000..739342f --- /dev/null +++ b/tests/bfs/exclude_exclude.sh @@ -0,0 +1 @@ +! invoke_bfs basic -exclude -exclude -name foo diff --git a/tests/test_L_lname.out b/tests/bfs/exclude_mindepth.out index e69de29..e69de29 100644 --- a/tests/test_L_lname.out +++ b/tests/bfs/exclude_mindepth.out diff --git a/tests/bfs/exclude_mindepth.sh b/tests/bfs/exclude_mindepth.sh new file mode 100644 index 0000000..c8f70f9 --- /dev/null +++ b/tests/bfs/exclude_mindepth.sh @@ -0,0 +1 @@ +bfs_diff basic -mindepth 3 -exclude -name foo diff --git a/tests/test_exclude_name.out b/tests/bfs/exclude_name.out index 59e3c42..59e3c42 100644 --- a/tests/test_exclude_name.out +++ b/tests/bfs/exclude_name.out diff --git a/tests/bfs/exclude_name.sh b/tests/bfs/exclude_name.sh new file mode 100644 index 0000000..7cf9f33 --- /dev/null +++ b/tests/bfs/exclude_name.sh @@ -0,0 +1 @@ +bfs_diff basic -exclude -name foo diff --git a/tests/bfs/exclude_print.sh b/tests/bfs/exclude_print.sh new file mode 100644 index 0000000..dc89e1d --- /dev/null +++ b/tests/bfs/exclude_print.sh @@ -0,0 +1 @@ +! invoke_bfs basic -exclude -print diff --git a/tests/test_exec_flush_fprint.out b/tests/bfs/exec_flush_fprint.out index 511198f..511198f 100644 --- a/tests/test_exec_flush_fprint.out +++ b/tests/bfs/exec_flush_fprint.out diff --git a/tests/bfs/exec_flush_fprint.sh b/tests/bfs/exec_flush_fprint.sh new file mode 100644 index 0000000..a862773 --- /dev/null +++ b/tests/bfs/exec_flush_fprint.sh @@ -0,0 +1,2 @@ +# Even non-stdstreams should be flushed +bfs_diff basic/a -fprint "$OUT.f" -exec cat "$OUT.f" \; diff --git a/tests/bfs/exec_flush_fprint_fail.sh b/tests/bfs/exec_flush_fprint_fail.sh new file mode 100644 index 0000000..cd38e41 --- /dev/null +++ b/tests/bfs/exec_flush_fprint_fail.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic/a -fprint /dev/full -exec true \; diff --git a/tests/test_execdir.out b/tests/bfs/execdir_path_relative_slash.out index 62b31f6..62b31f6 100644 --- a/tests/test_execdir.out +++ b/tests/bfs/execdir_path_relative_slash.out 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/test_execdir_plus.out b/tests/bfs/execdir_plus.out index 8866a8f..8866a8f 100644 --- a/tests/test_execdir_plus.out +++ b/tests/bfs/execdir_plus.out diff --git a/tests/bfs/execdir_plus.sh b/tests/bfs/execdir_plus.sh new file mode 100644 index 0000000..6f24bdc --- /dev/null +++ b/tests/bfs/execdir_plus.sh @@ -0,0 +1,4 @@ +tree=$(invoke_bfs -D tree 2>&1 -quit) +[[ "$tree" == *"-S dfs"* ]] && skip + +bfs_diff -j1 basic -execdir "$TESTS/sort-args.sh" {} + diff --git a/tests/test_data_flow_sparse.out b/tests/bfs/execdir_plus_nonexistent.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_data_flow_sparse.out +++ b/tests/bfs/execdir_plus_nonexistent.out diff --git a/tests/bfs/execdir_plus_nonexistent.sh b/tests/bfs/execdir_plus_nonexistent.sh new file mode 100644 index 0000000..ed7ed56 --- /dev/null +++ b/tests/bfs/execdir_plus_nonexistent.sh @@ -0,0 +1,2 @@ +bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/test_H_type_l.out b/tests/bfs/expr_flag_path.out index e67f10b..e67f10b 100644 --- a/tests/test_H_type_l.out +++ b/tests/bfs/expr_flag_path.out diff --git a/tests/bfs/expr_flag_path.sh b/tests/bfs/expr_flag_path.sh new file mode 100644 index 0000000..bb89d92 --- /dev/null +++ b/tests/bfs/expr_flag_path.sh @@ -0,0 +1 @@ +bfs_diff -type l -H links/skip diff --git a/tests/test_expr_flag_path.out b/tests/bfs/expr_path_flag.out index e67f10b..e67f10b 100644 --- a/tests/test_expr_flag_path.out +++ b/tests/bfs/expr_path_flag.out diff --git a/tests/bfs/expr_path_flag.sh b/tests/bfs/expr_path_flag.sh new file mode 100644 index 0000000..818e5d1 --- /dev/null +++ b/tests/bfs/expr_path_flag.sh @@ -0,0 +1 @@ +bfs_diff -type l links/skip -H 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/test_expr_path_flag.out b/tests/bfs/flag_expr_path.out index e67f10b..e67f10b 100644 --- a/tests/test_expr_path_flag.out +++ b/tests/bfs/flag_expr_path.out diff --git a/tests/bfs/flag_expr_path.sh b/tests/bfs/flag_expr_path.sh new file mode 100644 index 0000000..a414e10 --- /dev/null +++ b/tests/bfs/flag_expr_path.sh @@ -0,0 +1 @@ +bfs_diff -H -type l links/skip diff --git a/tests/test_fprint_append.out b/tests/bfs/fprint_duplicate_stdout.out index 6c21751..6c21751 100644 --- a/tests/test_fprint_append.out +++ b/tests/bfs/fprint_duplicate_stdout.out diff --git a/tests/bfs/fprint_duplicate_stdout.sh b/tests/bfs/fprint_duplicate_stdout.sh new file mode 100644 index 0000000..4e95e30 --- /dev/null +++ b/tests/bfs/fprint_duplicate_stdout.sh @@ -0,0 +1,3 @@ +invoke_bfs basic -fprint "$OUT" -print >"$OUT" +sort_output +diff_output diff --git a/tests/bfs/fprint_error_stderr.sh b/tests/bfs/fprint_error_stderr.sh new file mode 100644 index 0000000..2cc4037 --- /dev/null +++ b/tests/bfs/fprint_error_stderr.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full diff --git a/tests/bfs/fprint_error_stdout.sh b/tests/bfs/fprint_error_stdout.sh new file mode 100644 index 0000000..42a7b36 --- /dev/null +++ b/tests/bfs/fprint_error_stdout.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full diff --git a/tests/bfs/help.sh b/tests/bfs/help.sh new file mode 100644 index 0000000..5029c7e --- /dev/null +++ b/tests/bfs/help.sh @@ -0,0 +1,4 @@ +! invoke_bfs -help | grep -E '\{...?\}' || fail +! invoke_bfs -D help | grep -E '\{...?\}' || fail +! invoke_bfs -S help | grep -E '\{...?\}' || fail +! invoke_bfs -regextype help | grep -E '\{...?\}' || fail diff --git a/tests/test_hidden.out b/tests/bfs/hidden.out index e65ede9..e65ede9 100644 --- a/tests/test_hidden.out +++ b/tests/bfs/hidden.out diff --git a/tests/bfs/hidden.sh b/tests/bfs/hidden.sh new file mode 100644 index 0000000..b0413c5 --- /dev/null +++ b/tests/bfs/hidden.sh @@ -0,0 +1 @@ +bfs_diff weirdnames -hidden diff --git a/tests/test_hidden_root.out b/tests/bfs/hidden_root.out index 8c1371b..8c1371b 100644 --- a/tests/test_hidden_root.out +++ b/tests/bfs/hidden_root.out diff --git a/tests/bfs/hidden_root.sh b/tests/bfs/hidden_root.sh new file mode 100644 index 0000000..905c5b5 --- /dev/null +++ b/tests/bfs/hidden_root.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff . ./. ... ./... .../.. -hidden diff --git a/tests/bfs/high_byte.sh b/tests/bfs/high_byte.sh new file mode 100644 index 0000000..c76199f --- /dev/null +++ b/tests/bfs/high_byte.sh @@ -0,0 +1 @@ +! invoke_bfs -$'\xFF' diff --git a/tests/bfs/j0.sh b/tests/bfs/j0.sh new file mode 100644 index 0000000..97a7c5c --- /dev/null +++ b/tests/bfs/j0.sh @@ -0,0 +1 @@ +! invoke_bfs -j0 basic diff --git a/tests/test_data_flow_user.out b/tests/bfs/j1.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_data_flow_user.out +++ b/tests/bfs/j1.out diff --git a/tests/bfs/j1.sh b/tests/bfs/j1.sh new file mode 100644 index 0000000..972ac1b --- /dev/null +++ b/tests/bfs/j1.sh @@ -0,0 +1 @@ +bfs_diff -j1 basic diff --git a/tests/test_daystart.out b/tests/bfs/j64.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_daystart.out +++ b/tests/bfs/j64.out diff --git a/tests/bfs/j64.sh b/tests/bfs/j64.sh new file mode 100644 index 0000000..c56788f --- /dev/null +++ b/tests/bfs/j64.sh @@ -0,0 +1 @@ +bfs_diff -j64 basic diff --git a/tests/bfs/j_negative.sh b/tests/bfs/j_negative.sh new file mode 100644 index 0000000..809c98c --- /dev/null +++ b/tests/bfs/j_negative.sh @@ -0,0 +1 @@ +! invoke_bfs -j-1 basic diff --git a/tests/bfs/limit.out b/tests/bfs/limit.out new file mode 100644 index 0000000..ea94276 --- /dev/null +++ b/tests/bfs/limit.out @@ -0,0 +1,4 @@ +basic/a +basic/b +basic/c/d +basic/e/f diff --git a/tests/bfs/limit.sh b/tests/bfs/limit.sh new file mode 100644 index 0000000..84b605f --- /dev/null +++ b/tests/bfs/limit.sh @@ -0,0 +1 @@ +bfs_diff -s basic -type f -print -limit 4 diff --git a/tests/bfs/limit_0.sh b/tests/bfs/limit_0.sh new file mode 100644 index 0000000..3ce26de --- /dev/null +++ b/tests/bfs/limit_0.sh @@ -0,0 +1 @@ +! invoke_bfs basic -print -limit 0 diff --git a/tests/bfs/limit_implicit_print.sh b/tests/bfs/limit_implicit_print.sh new file mode 100644 index 0000000..cdb059d --- /dev/null +++ b/tests/bfs/limit_implicit_print.sh @@ -0,0 +1 @@ +! invoke_bfs basic -type f -limit 1 diff --git a/tests/bfs/limit_incomplete.sh b/tests/bfs/limit_incomplete.sh new file mode 100644 index 0000000..2d1e842 --- /dev/null +++ b/tests/bfs/limit_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs basic -print -limit diff --git a/tests/bfs/limit_one.sh b/tests/bfs/limit_one.sh new file mode 100644 index 0000000..3f8181c --- /dev/null +++ b/tests/bfs/limit_one.sh @@ -0,0 +1 @@ +! invoke_bfs basic -print -limit one diff --git a/tests/bfs/links_empty.sh b/tests/bfs/links_empty.sh new file mode 100644 index 0000000..42cf6e5 --- /dev/null +++ b/tests/bfs/links_empty.sh @@ -0,0 +1 @@ +! invoke_bfs links -links '' diff --git a/tests/bfs/links_invalid.sh b/tests/bfs/links_invalid.sh new file mode 100644 index 0000000..4d139c9 --- /dev/null +++ b/tests/bfs/links_invalid.sh @@ -0,0 +1 @@ +! invoke_bfs links -links ASDF diff --git a/tests/bfs/links_leading_space.sh b/tests/bfs/links_leading_space.sh new file mode 100644 index 0000000..15957af --- /dev/null +++ b/tests/bfs/links_leading_space.sh @@ -0,0 +1 @@ +! invoke_bfs links -links ' 1' diff --git a/tests/bfs/links_negative.sh b/tests/bfs/links_negative.sh new file mode 100644 index 0000000..e664b99 --- /dev/null +++ b/tests/bfs/links_negative.sh @@ -0,0 +1 @@ +! invoke_bfs links -links +-1 diff --git a/tests/bfs/links_noarg.sh b/tests/bfs/links_noarg.sh new file mode 100644 index 0000000..5c948dc --- /dev/null +++ b/tests/bfs/links_noarg.sh @@ -0,0 +1 @@ +! invoke_bfs links -links diff --git a/tests/bfs/newerma_nonexistent.sh b/tests/bfs/newerma_nonexistent.sh new file mode 100644 index 0000000..cdedb4a --- /dev/null +++ b/tests/bfs/newerma_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs times -newerma basic/nonexistent diff --git a/tests/bfs/newermq.sh b/tests/bfs/newermq.sh new file mode 100644 index 0000000..2f705dc --- /dev/null +++ b/tests/bfs/newermq.sh @@ -0,0 +1 @@ +! invoke_bfs times -newermq times/a diff --git a/tests/bfs/newermt_invalid.sh b/tests/bfs/newermt_invalid.sh new file mode 100644 index 0000000..98efece --- /dev/null +++ b/tests/bfs/newermt_invalid.sh @@ -0,0 +1 @@ +! invoke_bfs times -newermt not_a_date_time diff --git a/tests/bfs/newerqm.sh b/tests/bfs/newerqm.sh new file mode 100644 index 0000000..c0cff98 --- /dev/null +++ b/tests/bfs/newerqm.sh @@ -0,0 +1 @@ +! invoke_bfs times -newerqm times/a diff --git a/tests/bfs/nocolor.out b/tests/bfs/nocolor.out new file mode 100644 index 0000000..d51d24d --- /dev/null +++ b/tests/bfs/nocolor.out @@ -0,0 +1,27 @@ +rainbow +rainbow/[1m +rainbow/[1m/[0m +rainbow/broken +rainbow/chardev_link +rainbow/exec.sh +rainbow/file.dat +rainbow/file.txt +rainbow/link.txt +rainbow/lower.gz +rainbow/lower.tar +rainbow/lower.tar.gz +rainbow/lu.tar.GZ +rainbow/mh1 +rainbow/mh2 +rainbow/ow +rainbow/pipe +rainbow/sgid +rainbow/socket +rainbow/sticky +rainbow/sticky_ow +rainbow/sugid +rainbow/suid +rainbow/ul.TAR.gz +rainbow/upper.GZ +rainbow/upper.TAR +rainbow/upper.TAR.GZ diff --git a/tests/bfs/nocolor.sh b/tests/bfs/nocolor.sh new file mode 100644 index 0000000..8dace0b --- /dev/null +++ b/tests/bfs/nocolor.sh @@ -0,0 +1 @@ +bfs_diff rainbow -nocolor diff --git a/tests/bfs/nocolor_env.out b/tests/bfs/nocolor_env.out new file mode 100644 index 0000000..d51d24d --- /dev/null +++ b/tests/bfs/nocolor_env.out @@ -0,0 +1,27 @@ +rainbow +rainbow/[1m +rainbow/[1m/[0m +rainbow/broken +rainbow/chardev_link +rainbow/exec.sh +rainbow/file.dat +rainbow/file.txt +rainbow/link.txt +rainbow/lower.gz +rainbow/lower.tar +rainbow/lower.tar.gz +rainbow/lu.tar.GZ +rainbow/mh1 +rainbow/mh2 +rainbow/ow +rainbow/pipe +rainbow/sgid +rainbow/socket +rainbow/sticky +rainbow/sticky_ow +rainbow/sugid +rainbow/suid +rainbow/ul.TAR.gz +rainbow/upper.GZ +rainbow/upper.TAR +rainbow/upper.TAR.GZ diff --git a/tests/bfs/nocolor_env.sh b/tests/bfs/nocolor_env.sh new file mode 100644 index 0000000..d1c2afb --- /dev/null +++ b/tests/bfs/nocolor_env.sh @@ -0,0 +1,3 @@ +NO_COLOR=1 bfs_pty rainbow >"$OUT" +sort_output +diff_output diff --git a/tests/bfs/nocolor_env_empty.out b/tests/bfs/nocolor_env_empty.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/nocolor_env_empty.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/nocolor_env_empty.sh b/tests/bfs/nocolor_env_empty.sh new file mode 100644 index 0000000..1edfb1d --- /dev/null +++ b/tests/bfs/nocolor_env_empty.sh @@ -0,0 +1,3 @@ +NO_COLOR= bfs_pty rainbow >"$OUT" +sort_output +diff_output 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/test_nohidden.out b/tests/bfs/nohidden.out index d3ec901..84e6bd2 100644 --- a/tests/test_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.sh b/tests/bfs/nohidden.sh new file mode 100644 index 0000000..e3a3e4a --- /dev/null +++ b/tests/bfs/nohidden.sh @@ -0,0 +1 @@ +bfs_diff weirdnames -nohidden diff --git a/tests/test_nohidden_depth.out b/tests/bfs/nohidden_depth.out index d3ec901..84e6bd2 100644 --- a/tests/test_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/nohidden_depth.sh b/tests/bfs/nohidden_depth.sh new file mode 100644 index 0000000..9fd7017 --- /dev/null +++ b/tests/bfs/nohidden_depth.sh @@ -0,0 +1 @@ +bfs_diff weirdnames -depth -nohidden diff --git a/tests/bfs/nowarn.sh b/tests/bfs/nowarn.sh new file mode 100644 index 0000000..d9f9ab3 --- /dev/null +++ b/tests/bfs/nowarn.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null) +[ -z "$stderr" ] diff --git a/tests/test_ok_plus_semicolon.out b/tests/bfs/ok_plus_semicolon.out index 2a3e14f..2a3e14f 100644 --- a/tests/test_ok_plus_semicolon.out +++ b/tests/bfs/ok_plus_semicolon.out diff --git a/tests/bfs/ok_plus_semicolon.sh b/tests/bfs/ok_plus_semicolon.sh new file mode 100644 index 0000000..57d6103 --- /dev/null +++ b/tests/bfs/ok_plus_semicolon.sh @@ -0,0 +1,8 @@ +# The -ok primary shall be equivalent to -exec, except that the use of a +# <plus-sign> to punctuate the end of the primary expression need not be +# supported, ... +# +# bfs chooses not to support it, for compatibility with most other find +# implementations. + +yes | bfs_diff basic -ok echo {} + \; diff --git a/tests/test_okdir_plus_semicolon.out b/tests/bfs/okdir_plus_semicolon.out index 1909d27..1909d27 100644 --- a/tests/test_okdir_plus_semicolon.out +++ b/tests/bfs/okdir_plus_semicolon.out diff --git a/tests/bfs/okdir_plus_semicolon.sh b/tests/bfs/okdir_plus_semicolon.sh new file mode 100644 index 0000000..d316bd7 --- /dev/null +++ b/tests/bfs/okdir_plus_semicolon.sh @@ -0,0 +1 @@ +yes | bfs_diff basic -okdir echo {} + \; diff --git a/tests/bfs/or_incomplete.sh b/tests/bfs/or_incomplete.sh new file mode 100644 index 0000000..4af31b6 --- /dev/null +++ b/tests/bfs/or_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs -print -o diff --git a/tests/test_flag_expr_path.out b/tests/bfs/path_expr_flag.out index e67f10b..e67f10b 100644 --- a/tests/test_flag_expr_path.out +++ b/tests/bfs/path_expr_flag.out diff --git a/tests/bfs/path_expr_flag.sh b/tests/bfs/path_expr_flag.sh new file mode 100644 index 0000000..7cfa1cd --- /dev/null +++ b/tests/bfs/path_expr_flag.sh @@ -0,0 +1 @@ +bfs_diff links/skip -type l -H diff --git a/tests/test_path_expr_flag.out b/tests/bfs/path_flag_expr.out index e67f10b..e67f10b 100644 --- a/tests/test_path_expr_flag.out +++ b/tests/bfs/path_flag_expr.out diff --git a/tests/bfs/path_flag_expr.sh b/tests/bfs/path_flag_expr.sh new file mode 100644 index 0000000..ca00c8c --- /dev/null +++ b/tests/bfs/path_flag_expr.sh @@ -0,0 +1 @@ +bfs_diff links/skip -H -type l diff --git a/tests/bfs/perm_leading_plus_symbolic.out b/tests/bfs/perm_leading_plus_symbolic.out new file mode 100644 index 0000000..09bc88f --- /dev/null +++ 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/perm_leading_plus_symbolic.sh b/tests/bfs/perm_leading_plus_symbolic.sh new file mode 100644 index 0000000..4202ac1 --- /dev/null +++ b/tests/bfs/perm_leading_plus_symbolic.sh @@ -0,0 +1 @@ +bfs_diff perms -perm +rwx diff --git a/tests/bfs/perm_symbolic_double_comma.sh b/tests/bfs/perm_symbolic_double_comma.sh new file mode 100644 index 0000000..48f9d4b --- /dev/null +++ b/tests/bfs/perm_symbolic_double_comma.sh @@ -0,0 +1 @@ +! invoke_bfs perms -perm a+r,,u+w diff --git a/tests/bfs/perm_symbolic_missing_action.sh b/tests/bfs/perm_symbolic_missing_action.sh new file mode 100644 index 0000000..28446ab --- /dev/null +++ b/tests/bfs/perm_symbolic_missing_action.sh @@ -0,0 +1 @@ +! invoke_bfs perms -perm a diff --git a/tests/bfs/perm_symbolic_trailing_comma.sh b/tests/bfs/perm_symbolic_trailing_comma.sh new file mode 100644 index 0000000..01bbc16 --- /dev/null +++ b/tests/bfs/perm_symbolic_trailing_comma.sh @@ -0,0 +1 @@ +! invoke_bfs perms -perm a+r, diff --git a/tests/test_printf_color.out b/tests/bfs/printf_color.out index d9cd1a4..77d21c3 100644 --- a/tests/test_printf_color.out +++ b/tests/bfs/printf_color.out @@ -1,5 +1,8 @@ -[01;34m.[0m [01;34m.[0m [01;34mrainbow[0m [01;34m./[0m[01;34mrainbow[0m [01;34mrainbow[0m +[01;34m.[0m [01;34m$'./rainbow/\e[1m'[0m $'\e[0m' [01;34m$'./rainbow/\e[1m/'[0m$'\e[0m' [01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34m.[0m [01;34m.[0m [01;34m.[0m [01;34m.[0m +[01;34m.[0m [01;34m.[0m [01;34mrainbow[0m [01;34m./rainbow[0m [01;34mrainbow[0m [01;34m.[0m [01;34m./rainbow[0m [01;32mexec.sh[0m [01;34m./rainbow/[0m[01;32mexec.sh[0m [01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34m.[0m [01;34m./rainbow[0m [01;34m$'\e[1m'[0m [01;34m$'./rainbow/\e[1m'[0m [01;34m$'rainbow/\e[1m'[0m [01;34m.[0m [01;34m./rainbow[0m [01;35msocket[0m [01;34m./rainbow/[0m[01;35msocket[0m [01;34mrainbow/[0m[01;35msocket[0m [01;34m.[0m [01;34m./rainbow[0m [01;36mbroken[0m [01;34m./rainbow/[0m[01;36mbroken[0m [01;34mrainbow/[0m[01;36mbroken[0m nowhere [01;34m.[0m [01;34m./rainbow[0m [01;36mchardev_link[0m [01;34m./rainbow/[0m[01;36mchardev_link[0m [01;34mrainbow/[0m[01;36mchardev_link[0m [01;34m/dev/[0m[01;33mnull[0m @@ -13,8 +16,13 @@ [01;34m.[0m [01;34m./rainbow[0m [37;44msticky[0m [01;34m./rainbow/[0m[37;44msticky[0m [01;34mrainbow/[0m[37;44msticky[0m [01;34m.[0m [01;34m./rainbow[0m file.dat [01;34m./rainbow/[0mfile.dat [01;34mrainbow/[0mfile.dat [01;34m.[0m [01;34m./rainbow[0m file.txt [01;34m./rainbow/[0mfile.txt [01;34mrainbow/[0mfile.txt +[01;34m.[0m [01;34m./rainbow[0m lower.gz [01;34m./rainbow/[0mlower.gz [01;34mrainbow/[0mlower.gz +[01;34m.[0m [01;34m./rainbow[0m lower.tar [01;34m./rainbow/[0mlower.tar [01;34mrainbow/[0mlower.tar +[01;34m.[0m [01;34m./rainbow[0m lower.tar.gz [01;34m./rainbow/[0mlower.tar.gz [01;34mrainbow/[0mlower.tar.gz +[01;34m.[0m [01;34m./rainbow[0m lu.tar.GZ [01;34m./rainbow/[0mlu.tar.GZ [01;34mrainbow/[0mlu.tar.GZ [01;34m.[0m [01;34m./rainbow[0m mh1 [01;34m./rainbow/[0mmh1 [01;34mrainbow/[0mmh1 [01;34m.[0m [01;34m./rainbow[0m mh2 [01;34m./rainbow/[0mmh2 [01;34mrainbow/[0mmh2 -[01;34m.[0m [01;34m./rainbow[0m star.gz [01;34m./rainbow/[0mstar.gz [01;34mrainbow/[0mstar.gz -[01;34m.[0m [01;34m./rainbow[0m star.tar [01;34m./rainbow/[0mstar.tar [01;34mrainbow/[0mstar.tar -[01;34m.[0m [01;34m./rainbow[0m star.tar.gz [01;34m./rainbow/[0mstar.tar.gz [01;34mrainbow/[0mstar.tar.gz +[01;34m.[0m [01;34m./rainbow[0m ul.TAR.gz [01;34m./rainbow/[0mul.TAR.gz [01;34mrainbow/[0mul.TAR.gz +[01;34m.[0m [01;34m./rainbow[0m upper.GZ [01;34m./rainbow/[0mupper.GZ [01;34mrainbow/[0mupper.GZ +[01;34m.[0m [01;34m./rainbow[0m upper.TAR [01;34m./rainbow/[0mupper.TAR [01;34mrainbow/[0mupper.TAR +[01;34m.[0m [01;34m./rainbow[0m upper.TAR.GZ [01;34m./rainbow/[0mupper.TAR.GZ [01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/printf_color.sh b/tests/bfs/printf_color.sh new file mode 100644 index 0000000..3641ddb --- /dev/null +++ b/tests/bfs/printf_color.sh @@ -0,0 +1 @@ +bfs_diff -color -exclude \( -depth 1 -not -name rainbow \) -printf '%H %h %f %p %P %l\n' diff --git a/tests/bfs/printf_duplicate_flag.sh b/tests/bfs/printf_duplicate_flag.sh new file mode 100644 index 0000000..5ff29f1 --- /dev/null +++ b/tests/bfs/printf_duplicate_flag.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '%--p' diff --git a/tests/bfs/printf_everything.sh b/tests/bfs/printf_everything.sh new file mode 100644 index 0000000..07d574a --- /dev/null +++ b/tests/bfs/printf_everything.sh @@ -0,0 +1,15 @@ +everything=(%{a,b,c,d,D,f,g,G,h,H,i,k,l,m,M,n,p,P,s,S,t,u,U,y,Y}) + +# Check if we have fstypes +if invoke_bfs basic -printf '%F' -quit >/dev/null; then + everything+=(%F) +fi + +everything+=(%{A,C,T}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z}) + +# Check if we have birth times +if invoke_bfs basic -printf '%w' -quit >/dev/null; then + everything+=(%w %{B,W}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z}) +fi + +invoke_bfs rainbow -printf "${everything[*]}\n" >/dev/null diff --git a/tests/bfs/printf_incomplete_escape.sh b/tests/bfs/printf_incomplete_escape.sh new file mode 100644 index 0000000..f560d28 --- /dev/null +++ b/tests/bfs/printf_incomplete_escape.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '\' diff --git a/tests/bfs/printf_incomplete_format.sh b/tests/bfs/printf_incomplete_format.sh new file mode 100644 index 0000000..92c6afc --- /dev/null +++ b/tests/bfs/printf_incomplete_format.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '%' diff --git a/tests/bfs/printf_invalid_escape.sh b/tests/bfs/printf_invalid_escape.sh new file mode 100644 index 0000000..4338f9b --- /dev/null +++ b/tests/bfs/printf_invalid_escape.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '\!' 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/printf_invalid_format.sh b/tests/bfs/printf_invalid_format.sh new file mode 100644 index 0000000..59d63a7 --- /dev/null +++ b/tests/bfs/printf_invalid_format.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '%!' diff --git a/tests/bfs/printf_must_be_numeric.sh b/tests/bfs/printf_must_be_numeric.sh new file mode 100644 index 0000000..7c7c3fa --- /dev/null +++ b/tests/bfs/printf_must_be_numeric.sh @@ -0,0 +1 @@ +! invoke_bfs basic -printf '%+p' diff --git a/tests/test_and_purity.out b/tests/bfs/printf_w.out index e69de29..e69de29 100644 --- a/tests/test_and_purity.out +++ b/tests/bfs/printf_w.out diff --git a/tests/bfs/printf_w.sh b/tests/bfs/printf_w.sh new file mode 100644 index 0000000..3b27ee7 --- /dev/null +++ b/tests/bfs/printf_w.sh @@ -0,0 +1,2 @@ +# Birth times may not be supported, so just check that %w/%W/%B can be parsed +bfs_diff times -false -printf '%w %WY %BY\n' diff --git a/tests/bfs/status.sh b/tests/bfs/status.sh new file mode 100644 index 0000000..83e12d3 --- /dev/null +++ b/tests/bfs/status.sh @@ -0,0 +1 @@ +bfs_pty basic -status -print -depth 0 -exec stty cols 123 rows 14 \; >"$OUT" diff --git a/tests/bfs/stderr_fails_loudly.sh b/tests/bfs/stderr_fails_loudly.sh new file mode 100644 index 0000000..8572d5a --- /dev/null +++ b/tests/bfs/stderr_fails_loudly.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full diff --git a/tests/test_daystart_twice.out b/tests/bfs/stderr_fails_silently.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_daystart_twice.out +++ b/tests/bfs/stderr_fails_silently.out diff --git a/tests/bfs/stderr_fails_silently.sh b/tests/bfs/stderr_fails_silently.sh new file mode 100644 index 0000000..a37393d --- /dev/null +++ b/tests/bfs/stderr_fails_silently.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +bfs_diff -D all basic 2>/dev/full diff --git a/tests/test_type_multi.out b/tests/bfs/type_multi.out index 3cae08a..3cae08a 100644 --- a/tests/test_type_multi.out +++ b/tests/bfs/type_multi.out diff --git a/tests/bfs/type_multi.sh b/tests/bfs/type_multi.sh new file mode 100644 index 0000000..59992c7 --- /dev/null +++ b/tests/bfs/type_multi.sh @@ -0,0 +1 @@ +bfs_diff links -type f,d,c diff --git a/tests/bfs/typo.sh b/tests/bfs/typo.sh new file mode 100644 index 0000000..459e9fe --- /dev/null +++ b/tests/bfs/typo.sh @@ -0,0 +1 @@ +invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null diff --git a/tests/bfs/unexpected_operator.sh b/tests/bfs/unexpected_operator.sh new file mode 100644 index 0000000..2eb0e71 --- /dev/null +++ b/tests/bfs/unexpected_operator.sh @@ -0,0 +1 @@ +! invoke_bfs \! -o -print diff --git a/tests/test_unique.out b/tests/bfs/unique.out index 289cbde..289cbde 100644 --- a/tests/test_unique.out +++ b/tests/bfs/unique.out diff --git a/tests/bfs/unique.sh b/tests/bfs/unique.sh new file mode 100644 index 0000000..ea8adfd --- /dev/null +++ b/tests/bfs/unique.sh @@ -0,0 +1 @@ +bfs_diff links/{file,symlink,hardlink} -unique diff --git a/tests/test_depth.out b/tests/bfs/unique_depth.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_depth.out +++ b/tests/bfs/unique_depth.out diff --git a/tests/bfs/unique_depth.sh b/tests/bfs/unique_depth.sh new file mode 100644 index 0000000..c1d9716 --- /dev/null +++ b/tests/bfs/unique_depth.sh @@ -0,0 +1 @@ +bfs_diff basic -unique -depth diff --git a/tests/bfs/version.sh b/tests/bfs/version.sh new file mode 100644 index 0000000..e0417ca --- /dev/null +++ b/tests/bfs/version.sh @@ -0,0 +1 @@ +invoke_bfs -version >/dev/null diff --git a/tests/bfs/warn_O9.out b/tests/bfs/warn_O9.out new file mode 100644 index 0000000..336a6e8 --- /dev/null +++ b/tests/bfs/warn_O9.out @@ -0,0 +1,19 @@ +. +./a +./b +./c +./c/d +./e +./e/f +./g +./g/h +./i +./j +./j/foo +./k +./k/foo +./k/foo/bar +./l +./l/foo +./l/foo/bar +./l/foo/bar/baz diff --git a/tests/bfs/warn_O9.sh b/tests/bfs/warn_O9.sh new file mode 100644 index 0000000..821789f --- /dev/null +++ b/tests/bfs/warn_O9.sh @@ -0,0 +1,3 @@ +# Regression test: don't crash when warning if -O9 is the last argument +cd basic +bfs_diff -warn -O9 diff --git a/tests/bfs/warn_depth_prune.sh b/tests/bfs/warn_depth_prune.sh new file mode 100644 index 0000000..0f613c8 --- /dev/null +++ b/tests/bfs/warn_depth_prune.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null) +[ -n "$stderr" ] diff --git a/tests/bfs/warn_exclude_path.sh b/tests/bfs/warn_exclude_path.sh new file mode 100644 index 0000000..988544e --- /dev/null +++ b/tests/bfs/warn_exclude_path.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs -warn -exclude basic -name '*f*' 2>&1 >/dev/null) +[ -n "$stderr" ] 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/test_depth_overflow.out b/tests/bfs/warn_xdev_mount.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_depth_overflow.out +++ b/tests/bfs/warn_xdev_mount.out diff --git a/tests/bfs/warn_xdev_mount.sh b/tests/bfs/warn_xdev_mount.sh new file mode 100644 index 0000000..5d395f6 --- /dev/null +++ b/tests/bfs/warn_xdev_mount.sh @@ -0,0 +1,2 @@ +# Regression test: don't crash if -mount is the last option +bfs_diff basic -warn -xdev -mount diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh new file mode 100644 index 0000000..4683764 --- /dev/null +++ b/tests/bfs/xtype_depth.sh @@ -0,0 +1,2 @@ +# Make sure -xtype is considered side-effecting for facts_when_impure +! invoke_bfs inaccessible/link -xtype l -depth 100 diff --git a/tests/test_xtype_multi.out b/tests/bfs/xtype_multi.out index 558e89c..558e89c 100644 --- a/tests/test_xtype_multi.out +++ b/tests/bfs/xtype_multi.out diff --git a/tests/bfs/xtype_multi.sh b/tests/bfs/xtype_multi.sh new file mode 100644 index 0000000..ed20955 --- /dev/null +++ b/tests/bfs/xtype_multi.sh @@ -0,0 +1 @@ +bfs_diff links -xtype f,d,c diff --git a/tests/test_data_flow_type.out b/tests/bfs/xtype_reorder.out index e69de29..e69de29 100644 --- a/tests/test_data_flow_type.out +++ b/tests/bfs/xtype_reorder.out diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh new file mode 100644 index 0000000..c1d94f3 --- /dev/null +++ b/tests/bfs/xtype_reorder.sh @@ -0,0 +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 inaccessible/link -links 100 -xtype l diff --git a/tests/bfstd.c b/tests/bfstd.c new file mode 100644 index 0000000..6e15e2b --- /dev/null +++ b/tests/bfstd.c @@ -0,0 +1,212 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.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> + +/** 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_everify(xdir, "xdirname()"); + bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); + free(xdir); + + char *xbase = xbasename(path); + bfs_everify(xbase, "xbasename()"); + bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); + free(xbase); +} + +/** 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 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); + + if (bfs_check(esc != end)) { + bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + } +} + +/** 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) { + 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); + + 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); +} + +void check_bfstd(void) { + check_asciilen(); + check_basedirs(); + check_wordescs(); + check_strtox(); + check_strwidth(); +} diff --git a/tests/bit.c b/tests/bit.c new file mode 100644 index 0000000..09d470b --- /dev/null +++ b/tests/bit.c @@ -0,0 +1,160 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "bfs.h" +#include "bit.h" + +#include <limits.h> +#include <stdint.h> +#include <string.h> + +// 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) + +static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); +static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); +static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_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)); + +static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); +static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); +static_assert(INT_MAX == IWIDTH_MAX(INT_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)); + +static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); +static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); +static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_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)); + +static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); +static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); +static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_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)) + +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 + check_eq(word, 0x04030201); +#elif ENDIAN_NATIVE == ENDIAN_BIG + check_eq(word, 0x01020304); +#else +# warning "Skipping byte order tests on mixed/unknown-endian machine" +#endif + + 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 (unsigned int j = i; j < 16; ++j) { + uint16_t m = (uint16_t)1 << j; + uint16_t nm = n | 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) { + check_eq(bit_ceil(nm), m); + bfs_check(has_single_bit(nm)); + } else { + if (j < 15) { + check_eq(bit_ceil(nm), (m << 1)); + } + bfs_check(!has_single_bit(nm)); + } + } + } + + 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/test_E.out b/tests/bsd/E.out index 0f0971e..0f0971e 100644 --- a/tests/test_E.out +++ b/tests/bsd/E.out diff --git a/tests/bsd/E.sh b/tests/bsd/E.sh new file mode 100644 index 0000000..5d97178 --- /dev/null +++ b/tests/bsd/E.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -E . -regex '\./(\()' diff --git a/tests/test_H_mnewer.out b/tests/bsd/H_mnewer.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_H_mnewer.out +++ b/tests/bsd/H_mnewer.out diff --git a/tests/bsd/H_mnewer.sh b/tests/bsd/H_mnewer.sh new file mode 100644 index 0000000..94fe08b --- /dev/null +++ b/tests/bsd/H_mnewer.sh @@ -0,0 +1 @@ +bfs_diff -H times -mnewer times/l diff --git a/tests/test_H.out b/tests/bsd/Hf.out index ff635ff..ff635ff 100644 --- a/tests/test_H.out +++ b/tests/bsd/Hf.out 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/L_acl.out b/tests/bsd/L_acl.out new file mode 100644 index 0000000..dd89800 --- /dev/null +++ b/tests/bsd/L_acl.out @@ -0,0 +1,2 @@ +./acl +./link diff --git a/tests/bsd/L_acl.sh b/tests/bsd/L_acl.sh new file mode 100644 index 0000000..a3fcbc8 --- /dev/null +++ b/tests/bsd/L_acl.sh @@ -0,0 +1,9 @@ +cd "$TEST" + +invoke_bfs . -quit -acl || skip + +"$XTOUCH" normal acl +set_acl acl || skip +ln -s acl link + +bfs_diff -L . -acl diff --git a/tests/bsd/L_xattr.out b/tests/bsd/L_xattr.out new file mode 100644 index 0000000..21eb50f --- /dev/null +++ b/tests/bsd/L_xattr.out @@ -0,0 +1,3 @@ +./link +./xattr +./xattr_2 diff --git a/tests/bsd/L_xattr.sh b/tests/bsd/L_xattr.sh new file mode 100644 index 0000000..f8b56d8 --- /dev/null +++ b/tests/bsd/L_xattr.sh @@ -0,0 +1,3 @@ +invoke_bfs . -quit -xattr || skip +make_xattrs || skip +bfs_diff -L . -xattr diff --git a/tests/bsd/L_xattrname.out b/tests/bsd/L_xattrname.out new file mode 100644 index 0000000..9e4c172 --- /dev/null +++ b/tests/bsd/L_xattrname.out @@ -0,0 +1,2 @@ +./link +./xattr diff --git a/tests/bsd/L_xattrname.sh b/tests/bsd/L_xattrname.sh new file mode 100644 index 0000000..8108d57 --- /dev/null +++ b/tests/bsd/L_xattrname.sh @@ -0,0 +1,11 @@ +invoke_bfs . -quit -xattr || skip +make_xattrs || skip + +case "$UNAME" in + Darwin|FreeBSD) + bfs_diff -L . -xattrname bfs_test + ;; + *) + bfs_diff -L . -xattrname security.bfs_test + ;; +esac diff --git a/tests/test_X.out b/tests/bsd/X.out index afa84f7..dbe2408 100644 --- a/tests/test_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/X.sh b/tests/bsd/X.sh new file mode 100644 index 0000000..54000cf --- /dev/null +++ b/tests/bsd/X.sh @@ -0,0 +1 @@ +! bfs_diff -X weirdnames diff --git a/tests/bsd/acl.out b/tests/bsd/acl.out new file mode 100644 index 0000000..92e2f67 --- /dev/null +++ b/tests/bsd/acl.out @@ -0,0 +1 @@ +./acl diff --git a/tests/bsd/acl.sh b/tests/bsd/acl.sh new file mode 100644 index 0000000..a13c75f --- /dev/null +++ b/tests/bsd/acl.sh @@ -0,0 +1,9 @@ +cd "$TEST" + +invoke_bfs . -quit -acl || skip + +"$XTOUCH" normal acl +set_acl acl || skip +ln -s acl link + +bfs_diff . -acl diff --git a/tests/test_asince.out b/tests/bsd/asince.out index 650e550..650e550 100644 --- a/tests/test_asince.out +++ b/tests/bsd/asince.out diff --git a/tests/bsd/asince.sh b/tests/bsd/asince.sh new file mode 100644 index 0000000..32d5228 --- /dev/null +++ b/tests/bsd/asince.sh @@ -0,0 +1 @@ +bfs_diff times -asince 1991-12-14T00:01 diff --git a/tests/test_exec.out b/tests/bsd/d_path.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_exec.out +++ b/tests/bsd/d_path.out diff --git a/tests/bsd/d_path.sh b/tests/bsd/d_path.sh new file mode 100644 index 0000000..010f76f --- /dev/null +++ b/tests/bsd/d_path.sh @@ -0,0 +1 @@ +bfs_diff -d basic diff --git a/tests/test_data_flow_depth.out b/tests/bsd/data_flow_depth.out index ab127ec..ab127ec 100644 --- a/tests/test_data_flow_depth.out +++ b/tests/bsd/data_flow_depth.out diff --git a/tests/bsd/data_flow_depth.sh b/tests/bsd/data_flow_depth.sh new file mode 100644 index 0000000..cd5d6b2 --- /dev/null +++ b/tests/bsd/data_flow_depth.sh @@ -0,0 +1 @@ +bfs_diff basic -depth +1 -depth -4 diff --git a/tests/test_exec_nopath.out b/tests/bsd/data_flow_sparse.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_exec_nopath.out +++ b/tests/bsd/data_flow_sparse.out diff --git a/tests/bsd/data_flow_sparse.sh b/tests/bsd/data_flow_sparse.sh new file mode 100644 index 0000000..bd7e1f7 --- /dev/null +++ b/tests/bsd/data_flow_sparse.sh @@ -0,0 +1 @@ +bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \) diff --git a/tests/test_depth_depth_n.out b/tests/bsd/depth_depth_n.out index 3bfc1d3..3bfc1d3 100644 --- a/tests/test_depth_depth_n.out +++ b/tests/bsd/depth_depth_n.out diff --git a/tests/bsd/depth_depth_n.sh b/tests/bsd/depth_depth_n.sh new file mode 100644 index 0000000..5989b3c --- /dev/null +++ b/tests/bsd/depth_depth_n.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -depth 2 diff --git a/tests/test_depth_depth_n_minus.out b/tests/bsd/depth_depth_n_minus.out index 7575ae4..7575ae4 100644 --- a/tests/test_depth_depth_n_minus.out +++ b/tests/bsd/depth_depth_n_minus.out diff --git a/tests/bsd/depth_depth_n_minus.sh b/tests/bsd/depth_depth_n_minus.sh new file mode 100644 index 0000000..1d8ac79 --- /dev/null +++ b/tests/bsd/depth_depth_n_minus.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -depth -2 diff --git a/tests/test_depth_depth_n_plus.out b/tests/bsd/depth_depth_n_plus.out index 847995d..847995d 100644 --- a/tests/test_depth_depth_n_plus.out +++ b/tests/bsd/depth_depth_n_plus.out diff --git a/tests/bsd/depth_depth_n_plus.sh b/tests/bsd/depth_depth_n_plus.sh new file mode 100644 index 0000000..64e392b --- /dev/null +++ b/tests/bsd/depth_depth_n_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -depth +2 diff --git a/tests/test_depth_n.out b/tests/bsd/depth_n.out index 3bfc1d3..3bfc1d3 100644 --- a/tests/test_depth_n.out +++ b/tests/bsd/depth_n.out diff --git a/tests/bsd/depth_n.sh b/tests/bsd/depth_n.sh new file mode 100644 index 0000000..4852952 --- /dev/null +++ b/tests/bsd/depth_n.sh @@ -0,0 +1 @@ +bfs_diff basic -depth 2 diff --git a/tests/test_depth_maxdepth_1.out b/tests/bsd/depth_n_minus.out index 7575ae4..7575ae4 100644 --- a/tests/test_depth_maxdepth_1.out +++ b/tests/bsd/depth_n_minus.out diff --git a/tests/bsd/depth_n_minus.sh b/tests/bsd/depth_n_minus.sh new file mode 100644 index 0000000..192bf8a --- /dev/null +++ b/tests/bsd/depth_n_minus.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -2 diff --git a/tests/test_depth_n_plus.out b/tests/bsd/depth_n_plus.out index 847995d..847995d 100644 --- a/tests/test_depth_n_plus.out +++ b/tests/bsd/depth_n_plus.out diff --git a/tests/bsd/depth_n_plus.sh b/tests/bsd/depth_n_plus.sh new file mode 100644 index 0000000..858e1c4 --- /dev/null +++ b/tests/bsd/depth_n_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -depth +2 diff --git a/tests/test_exec_plus_status.out b/tests/bsd/depth_overflow.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_exec_plus_status.out +++ b/tests/bsd/depth_overflow.out diff --git a/tests/bsd/depth_overflow.sh b/tests/bsd/depth_overflow.sh new file mode 100644 index 0000000..4685d03 --- /dev/null +++ b/tests/bsd/depth_overflow.sh @@ -0,0 +1 @@ +bfs_diff basic -depth -4294967296 diff --git a/tests/test_exit.out b/tests/bsd/exit.out index cf4d5a9..cf4d5a9 100644 --- a/tests/test_exit.out +++ b/tests/bsd/exit.out diff --git a/tests/bsd/exit.sh b/tests/bsd/exit.sh new file mode 100644 index 0000000..248349c --- /dev/null +++ b/tests/bsd/exit.sh @@ -0,0 +1,5 @@ +check_exit 42 invoke_bfs basic -name foo -exit 42 + +check_exit 0 invoke_bfs basic -name qux -exit 42 + +bfs_diff basic/g -print -name g -exit diff --git a/tests/test_exclude_mindepth.out b/tests/bsd/exit_no_implicit_print.out index e69de29..e69de29 100644 --- a/tests/test_exclude_mindepth.out +++ b/tests/bsd/exit_no_implicit_print.out diff --git a/tests/bsd/exit_no_implicit_print.sh b/tests/bsd/exit_no_implicit_print.sh new file mode 100644 index 0000000..c48b43c --- /dev/null +++ b/tests/bsd/exit_no_implicit_print.sh @@ -0,0 +1 @@ +bfs_diff basic -not -name foo -o -exit diff --git a/tests/test_f.out b/tests/bsd/f.out index 77eac77..77eac77 100644 --- a/tests/test_f.out +++ b/tests/bsd/f.out diff --git a/tests/bsd/f.sh b/tests/bsd/f.sh new file mode 100644 index 0000000..42d2dfd --- /dev/null +++ b/tests/bsd/f.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -f '-' -f '(' diff --git a/tests/bsd/f_incomplete.sh b/tests/bsd/f_incomplete.sh new file mode 100644 index 0000000..0dfb19f --- /dev/null +++ b/tests/bsd/f_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs -f diff --git a/tests/bsd/flags.out b/tests/bsd/flags.out new file mode 100644 index 0000000..3216ff5 --- /dev/null +++ b/tests/bsd/flags.out @@ -0,0 +1 @@ +./bar diff --git a/tests/bsd/flags.sh b/tests/bsd/flags.sh new file mode 100644 index 0000000..eb9bc22 --- /dev/null +++ b/tests/bsd/flags.sh @@ -0,0 +1,8 @@ +invoke_bfs . -quit -flags offline || skip + +cd "$TEST" + +"$XTOUCH" foo bar +chflags offline bar || skip + +bfs_diff . -flags -offline,nohidden diff --git a/tests/test_fprint.out b/tests/bsd/gid_name.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_fprint.out +++ b/tests/bsd/gid_name.out diff --git a/tests/bsd/gid_name.sh b/tests/bsd/gid_name.sh new file mode 100644 index 0000000..c7e0e71 --- /dev/null +++ b/tests/bsd/gid_name.sh @@ -0,0 +1 @@ +bfs_diff basic -gid "$(id -gn)" diff --git a/tests/test_H_newer.out b/tests/bsd/mnewer.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_H_newer.out +++ b/tests/bsd/mnewer.out diff --git a/tests/bsd/mnewer.sh b/tests/bsd/mnewer.sh new file mode 100644 index 0000000..5d9f1a7 --- /dev/null +++ b/tests/bsd/mnewer.sh @@ -0,0 +1 @@ +bfs_diff times -mnewer times/a diff --git a/tests/test_msince.out b/tests/bsd/msince.out index 650e550..650e550 100644 --- a/tests/test_msince.out +++ b/tests/bsd/msince.out diff --git a/tests/bsd/msince.sh b/tests/bsd/msince.sh new file mode 100644 index 0000000..ec22f02 --- /dev/null +++ b/tests/bsd/msince.sh @@ -0,0 +1 @@ +bfs_diff times -msince 1991-12-14T00:01 diff --git a/tests/bsd/mtime_bad_unit.sh b/tests/bsd/mtime_bad_unit.sh new file mode 100644 index 0000000..6e2caf1 --- /dev/null +++ b/tests/bsd/mtime_bad_unit.sh @@ -0,0 +1 @@ +! invoke_bfs times -mtime +1q diff --git a/tests/bsd/mtime_missing_unit.sh b/tests/bsd/mtime_missing_unit.sh new file mode 100644 index 0000000..f6b1f93 --- /dev/null +++ b/tests/bsd/mtime_missing_unit.sh @@ -0,0 +1 @@ +! invoke_bfs times -mtime +1w2 diff --git a/tests/test_mtime_units.out b/tests/bsd/mtime_units.out index f7f63b0..f7f63b0 100644 --- a/tests/test_mtime_units.out +++ b/tests/bsd/mtime_units.out diff --git a/tests/bsd/mtime_units.sh b/tests/bsd/mtime_units.sh new file mode 100644 index 0000000..a1e587e --- /dev/null +++ b/tests/bsd/mtime_units.sh @@ -0,0 +1 @@ +bfs_diff times -mtime +500w400d300h200m100s diff --git a/tests/test_okdir_stdin.out b/tests/bsd/okdir_stdin.out index ef2a68b..ef2a68b 100644 --- a/tests/test_okdir_stdin.out +++ b/tests/bsd/okdir_stdin.out diff --git a/tests/bsd/okdir_stdin.sh b/tests/bsd/okdir_stdin.sh new file mode 100644 index 0000000..7908ac0 --- /dev/null +++ b/tests/bsd/okdir_stdin.sh @@ -0,0 +1,2 @@ +# -okdir should *not* close stdin +yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \; diff --git a/tests/bsd/perm_000_plus.out b/tests/bsd/perm_000_plus.out new file mode 100644 index 0000000..e279684 --- /dev/null +++ b/tests/bsd/perm_000_plus.out @@ -0,0 +1,29 @@ +perms +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_000_plus.sh b/tests/bsd/perm_000_plus.sh new file mode 100644 index 0000000..9ab3146 --- /dev/null +++ b/tests/bsd/perm_000_plus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm +000 diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out new file mode 100644 index 0000000..1b6d885 --- /dev/null +++ b/tests/bsd/perm_222_plus.out @@ -0,0 +1,20 @@ +perms +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_222_plus.sh b/tests/bsd/perm_222_plus.sh new file mode 100644 index 0000000..ac3c4eb --- /dev/null +++ b/tests/bsd/perm_222_plus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm +222 diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out new file mode 100644 index 0000000..eef88ca --- /dev/null +++ b/tests/bsd/perm_644_plus.out @@ -0,0 +1,26 @@ +perms +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/perm_644_plus.sh b/tests/bsd/perm_644_plus.sh new file mode 100644 index 0000000..b3f5bc6 --- /dev/null +++ b/tests/bsd/perm_644_plus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm +644 diff --git a/tests/test_printx.out b/tests/bsd/printx.out index 04bf9a9..034b2da 100644 --- a/tests/test_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/printx.sh b/tests/bsd/printx.sh new file mode 100644 index 0000000..cb24aab --- /dev/null +++ b/tests/bsd/printx.sh @@ -0,0 +1 @@ +bfs_diff weirdnames -printx diff --git a/tests/test_and_false_or_true.out b/tests/bsd/quit_implicit_print.out index 15a13db..15a13db 100644 --- a/tests/test_and_false_or_true.out +++ b/tests/bsd/quit_implicit_print.out diff --git a/tests/bsd/quit_implicit_print.sh b/tests/bsd/quit_implicit_print.sh new file mode 100644 index 0000000..ea8fd5d --- /dev/null +++ b/tests/bsd/quit_implicit_print.sh @@ -0,0 +1 @@ +bfs_diff basic -name basic -o -quit diff --git a/tests/bsd/rm.out b/tests/bsd/rm.out new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/tests/bsd/rm.out @@ -0,0 +1 @@ +. diff --git a/tests/bsd/rm.sh b/tests/bsd/rm.sh new file mode 100644 index 0000000..595d514 --- /dev/null +++ b/tests/bsd/rm.sh @@ -0,0 +1,4 @@ +cd "$TEST" +"$XTOUCH" -p foo/bar/baz +invoke_bfs . -rm +bfs_diff . diff --git a/tests/test_s.out b/tests/bsd/s.out index 6b790c2..5c85ac8 100644 --- a/tests/test_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/bsd/s.sh b/tests/bsd/s.sh new file mode 100644 index 0000000..52f8eb3 --- /dev/null +++ b/tests/bsd/s.sh @@ -0,0 +1,2 @@ +invoke_bfs -s weirdnames -maxdepth 1 >"$OUT" +diff_output diff --git a/tests/bsd/s_quit.out b/tests/bsd/s_quit.out new file mode 100644 index 0000000..5ea492b --- /dev/null +++ b/tests/bsd/s_quit.out @@ -0,0 +1 @@ +basic/j/foo diff --git a/tests/bsd/s_quit.sh b/tests/bsd/s_quit.sh new file mode 100644 index 0000000..6bd55ab --- /dev/null +++ b/tests/bsd/s_quit.sh @@ -0,0 +1,4 @@ +# Regression test: bfs -S ids -s -name foo -quit would not actually quit, +# ending up in a confused state and erroring/crashing + +bfs_diff -s basic -name foo -print -quit diff --git a/tests/test_size_T.out b/tests/bsd/size_T.out index 279f3f1..279f3f1 100644 --- a/tests/test_size_T.out +++ b/tests/bsd/size_T.out diff --git a/tests/bsd/size_T.sh b/tests/bsd/size_T.sh new file mode 100644 index 0000000..1023a10 --- /dev/null +++ b/tests/bsd/size_T.sh @@ -0,0 +1 @@ +bfs_diff basic -type f -size 1T diff --git a/tests/bsd/sparse.out b/tests/bsd/sparse.out new file mode 100644 index 0000000..52dcf49 --- /dev/null +++ b/tests/bsd/sparse.out @@ -0,0 +1 @@ +mnt/sparse diff --git a/tests/bsd/sparse.sh b/tests/bsd/sparse.sh new file mode 100644 index 0000000..7fcdeed --- /dev/null +++ b/tests/bsd/sparse.sh @@ -0,0 +1,12 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +mkdir mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +truncate -s 1M mnt/sparse +dd if=/dev/zero of=mnt/dense bs=1M count=1 + +bfs_diff mnt -type f -sparse diff --git a/tests/bsd/type_w.out b/tests/bsd/type_w.out new file mode 100644 index 0000000..a20a4f3 --- /dev/null +++ b/tests/bsd/type_w.out @@ -0,0 +1,34 @@ +1: -rw-r--r-- mnt/lower/bar +1: -rw-r--r-- mnt/lower/baz +1: -rw-r--r-- mnt/lower/foo +1: -rw-r--r-- mnt/upper/baz/qux +1: -rw-r--r-- mnt/upper/foo +1: drwxr-xr-x mnt/lower +1: drwxr-xr-x mnt/upper +1: drwxr-xr-x mnt/upper/baz +2: w--------- mnt/upper/bar +3: -rw-r--r-- mnt/lower/bar +3: -rw-r--r-- mnt/lower/baz +3: -rw-r--r-- mnt/lower/foo +3: -rw-r--r-- mnt/upper/baz/qux +3: -rw-r--r-- mnt/upper/foo +3: drwxr-xr-x mnt/lower +3: drwxr-xr-x mnt/upper +3: drwxr-xr-x mnt/upper/baz +3: w--------- mnt/upper/bar +4: -rw-r--r-- mnt/lower/bar +4: -rw-r--r-- mnt/lower/baz +4: -rw-r--r-- mnt/lower/foo +4: -rw-r--r-- mnt/upper/baz/qux +4: drwxr-xr-x mnt/lower +4: drwxr-xr-x mnt/upper +4: drwxr-xr-x mnt/upper/baz +5: w--------- mnt/upper/bar +6: -rw-r--r-- mnt/lower/bar +6: -rw-r--r-- mnt/lower/baz +6: -rw-r--r-- mnt/lower/foo +6: -rw-r--r-- mnt/upper/baz/qux +6: drwxr-xr-x mnt/lower +6: drwxr-xr-x mnt/upper +6: drwxr-xr-x mnt/upper/baz +6: w--------- mnt/upper/bar diff --git a/tests/bsd/type_w.sh b/tests/bsd/type_w.sh new file mode 100644 index 0000000..3aa50d5 --- /dev/null +++ b/tests/bsd/type_w.sh @@ -0,0 +1,56 @@ +# Only ffs supports whiteouts on FreeBSD +command -v mdconfig &>/dev/null || skip +command -v newfs &>/dev/null || skip + +cd "$TEST" + +# Create a ramdisk +if command -v truncate &>/dev/null; then + truncate -s1M img +else + dd if=/dev/zero of=img bs=1k count=1k +fi +md=$(bfs_sudo mdconfig img) || skip +defer bfs_sudo mdconfig -du "$md" + +# Make an ffs filesystem +bfs_sudo newfs -n "/dev/$md" >&2 || skip +mkdir mnt + +# Mount it +bfs_sudo mount "/dev/$md" mnt || skip +defer bfs_sudo umount mnt + +# Make it owned by us +bfs_sudo chown "$(id -u):$(id -g)" mnt +"$XTOUCH" -p mnt/{lower/{foo,bar,baz},upper/{bar,baz/qux}} + +# Mount a union filesystem within it +bfs_sudo mount -t unionfs -o below mnt/{lower,upper} +defer bfs_sudo umount mnt/upper + +# Create a whiteout +rm mnt/upper/bar + +# FreeBSD find doesn't have -printf, so munge -ls output +munge_ls() { + sed -En 's|.*([-drwx]{10}).*(mnt/.*)|'"$1"': \1 \2|p' +} + +# Do a few tests in one +{ + # Normally, we shouldn't see the whiteouts + invoke_bfs mnt -ls | munge_ls 1 + # -type w adds whiteouts to the output + invoke_bfs mnt -type w -ls | munge_ls 2 + # So this is not the same as test 1 + invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 3 + # Unmount the unionfs + pop_defer + # Now repeat the same tests + invoke_bfs mnt -ls | munge_ls 4 + invoke_bfs mnt -type w -ls | munge_ls 5 + invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 6 +} >"$OUT" +sort_output +diff_output diff --git a/tests/test_fstype.out b/tests/bsd/uid_name.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_fstype.out +++ b/tests/bsd/uid_name.out diff --git a/tests/bsd/uid_name.sh b/tests/bsd/uid_name.sh new file mode 100644 index 0000000..7d3ba82 --- /dev/null +++ b/tests/bsd/uid_name.sh @@ -0,0 +1 @@ +bfs_diff basic -uid "$(id -un)" diff --git a/tests/bsd/xattr.out b/tests/bsd/xattr.out new file mode 100644 index 0000000..0afed35 --- /dev/null +++ b/tests/bsd/xattr.out @@ -0,0 +1,3 @@ +./xattr +./xattr_2 +./xattr_link diff --git a/tests/bsd/xattr.sh b/tests/bsd/xattr.sh new file mode 100644 index 0000000..68f729a --- /dev/null +++ b/tests/bsd/xattr.sh @@ -0,0 +1,3 @@ +invoke_bfs . -quit -xattr || skip +make_xattrs || skip +bfs_diff . -xattr diff --git a/tests/bsd/xattrname.out b/tests/bsd/xattrname.out new file mode 100644 index 0000000..ef732bd --- /dev/null +++ b/tests/bsd/xattrname.out @@ -0,0 +1,2 @@ +./xattr +./xattr_link diff --git a/tests/bsd/xattrname.sh b/tests/bsd/xattrname.sh new file mode 100644 index 0000000..38b111a --- /dev/null +++ b/tests/bsd/xattrname.sh @@ -0,0 +1,11 @@ +invoke_bfs . -quit -xattr || skip +make_xattrs || skip + +case "$UNAME" in + Darwin|FreeBSD) + bfs_diff . -xattrname bfs_test + ;; + *) + bfs_diff . -xattrname security.bfs_test + ;; +esac diff --git a/tests/color.sh b/tests/color.sh new file mode 100644 index 0000000..9e2e0f6 --- /dev/null +++ b/tests/color.sh @@ -0,0 +1,151 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Colored output + +# Common escape sequences +BLD=$'\e[01m' +RED=$'\e[01;31m' +GRN=$'\e[01;32m' +YLW=$'\e[01;33m' +BLU=$'\e[01;34m' +MAG=$'\e[01;35m' +CYN=$'\e[01;36m' +RST=$'\e[0m' + +# Check if we should color output to the given fd +color_fd() { + [ -z "${NO_COLOR:-}" ] && [ -t "$1" ] +} + +# Cache the color status for std{out,err} +color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0 +color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0 + +# Save this in case the tests unset PATH +SED=$(command -v sed) + +# Filter out escape sequences if necessary +color() { + if color_fd 1; then + "$@" + else + "$@" | "$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/test_P.out b/tests/common/HLP.out index ff635ff..ff635ff 100644 --- a/tests/test_P.out +++ b/tests/common/HLP.out 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/test_anewer.out b/tests/common/H_newer.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_anewer.out +++ b/tests/common/H_newer.out diff --git a/tests/common/H_newer.sh b/tests/common/H_newer.sh new file mode 100644 index 0000000..c72bff7 --- /dev/null +++ b/tests/common/H_newer.sh @@ -0,0 +1 @@ +bfs_diff -H times -newer times/l diff --git a/tests/test_H_broken.out b/tests/common/H_samefile_broken.out index 21d6316..21d6316 100644 --- a/tests/test_H_broken.out +++ b/tests/common/H_samefile_broken.out diff --git a/tests/common/H_samefile_broken.sh b/tests/common/H_samefile_broken.sh new file mode 100644 index 0000000..7a3366d --- /dev/null +++ b/tests/common/H_samefile_broken.sh @@ -0,0 +1 @@ +bfs_diff -H links -samefile links/broken diff --git a/tests/test_H_notdir.out b/tests/common/H_samefile_notdir.out index 6e6658d..6e6658d 100644 --- a/tests/test_H_notdir.out +++ b/tests/common/H_samefile_notdir.out diff --git a/tests/common/H_samefile_notdir.sh b/tests/common/H_samefile_notdir.sh new file mode 100644 index 0000000..25ad88d --- /dev/null +++ b/tests/common/H_samefile_notdir.sh @@ -0,0 +1 @@ +bfs_diff -H links -samefile links/notdir diff --git a/tests/test_H_samefile_symlink.out b/tests/common/H_samefile_symlink.out index 996ffc8..996ffc8 100644 --- a/tests/test_H_samefile_symlink.out +++ b/tests/common/H_samefile_symlink.out diff --git a/tests/common/H_samefile_symlink.sh b/tests/common/H_samefile_symlink.sh new file mode 100644 index 0000000..c73ddb2 --- /dev/null +++ b/tests/common/H_samefile_symlink.sh @@ -0,0 +1 @@ +bfs_diff -H links -samefile links/symlink diff --git a/tests/test_exit_no_implicit_print.out b/tests/common/L_ilname.out index e69de29..e69de29 100644 --- a/tests/test_exit_no_implicit_print.out +++ b/tests/common/L_ilname.out diff --git a/tests/common/L_ilname.sh b/tests/common/L_ilname.sh new file mode 100644 index 0000000..e0495ed --- /dev/null +++ b/tests/common/L_ilname.sh @@ -0,0 +1,2 @@ +invoke_bfs -quit -ilname PATTERN || skip +bfs_diff -L links -ilname '[AQ]' diff --git a/tests/test_false.out b/tests/common/L_lname.out index e69de29..e69de29 100644 --- a/tests/test_false.out +++ b/tests/common/L_lname.out diff --git a/tests/common/L_lname.sh b/tests/common/L_lname.sh new file mode 100644 index 0000000..65b9da5 --- /dev/null +++ b/tests/common/L_lname.sh @@ -0,0 +1 @@ +bfs_diff -L links -lname '[aq]' diff --git a/tests/common/L_ls.sh b/tests/common/L_ls.sh new file mode 100644 index 0000000..7ee2b44 --- /dev/null +++ b/tests/common/L_ls.sh @@ -0,0 +1 @@ +invoke_bfs -L rainbow -ls >"$OUT" diff --git a/tests/test_H_samefile_broken.out b/tests/common/L_samefile_broken.out index 21d6316..21d6316 100644 --- a/tests/test_H_samefile_broken.out +++ b/tests/common/L_samefile_broken.out diff --git a/tests/common/L_samefile_broken.sh b/tests/common/L_samefile_broken.sh new file mode 100644 index 0000000..5f860cc --- /dev/null +++ b/tests/common/L_samefile_broken.sh @@ -0,0 +1 @@ +bfs_diff -L links -samefile links/broken diff --git a/tests/test_H_samefile_notdir.out b/tests/common/L_samefile_notdir.out index 6e6658d..6e6658d 100644 --- a/tests/test_H_samefile_notdir.out +++ b/tests/common/L_samefile_notdir.out diff --git a/tests/common/L_samefile_notdir.sh b/tests/common/L_samefile_notdir.sh new file mode 100644 index 0000000..9b63429 --- /dev/null +++ b/tests/common/L_samefile_notdir.sh @@ -0,0 +1 @@ +bfs_diff -L links -samefile links/notdir diff --git a/tests/test_L_samefile_symlink.out b/tests/common/L_samefile_symlink.out index 222ac78..222ac78 100644 --- a/tests/test_L_samefile_symlink.out +++ b/tests/common/L_samefile_symlink.out diff --git a/tests/common/L_samefile_symlink.sh b/tests/common/L_samefile_symlink.sh new file mode 100644 index 0000000..4a7a8dd --- /dev/null +++ b/tests/common/L_samefile_symlink.sh @@ -0,0 +1 @@ +bfs_diff -L links -samefile links/symlink diff --git a/tests/common/P.out b/tests/common/P.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/common/P.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/common/P.sh b/tests/common/P.sh new file mode 100644 index 0000000..a7a09d1 --- /dev/null +++ b/tests/common/P.sh @@ -0,0 +1 @@ +bfs_diff -P links/deeply/nested/dir diff --git a/tests/test_H_slash.out b/tests/common/P_slash.out index df7701b..df7701b 100644 --- a/tests/test_H_slash.out +++ b/tests/common/P_slash.out diff --git a/tests/common/P_slash.sh b/tests/common/P_slash.sh new file mode 100644 index 0000000..9b9ffa0 --- /dev/null +++ b/tests/common/P_slash.sh @@ -0,0 +1 @@ +bfs_diff -P links/deeply/nested/dir/ diff --git a/tests/common/amin.out b/tests/common/amin.out new file mode 100644 index 0000000..af57325 --- /dev/null +++ b/tests/common/amin.out @@ -0,0 +1,6 @@ +-amin 1: ./one_minute_ago +-amin +1: ./one_hour_ago +-amin +1: ./two_minutes_ago +-amin -1: ./in_one_hour +-amin -1: ./in_one_minute +-amin -1: ./thirty_seconds_ago diff --git a/tests/common/amin.sh b/tests/common/amin.sh new file mode 100644 index 0000000..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/test_mnewer.out b/tests/common/anewer.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_mnewer.out +++ b/tests/common/anewer.out diff --git a/tests/common/anewer.sh b/tests/common/anewer.sh new file mode 100644 index 0000000..0cdd759 --- /dev/null +++ b/tests/common/anewer.sh @@ -0,0 +1 @@ +bfs_diff times -anewer times/a diff --git a/tests/common/delete.out b/tests/common/delete.out new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/tests/common/delete.out @@ -0,0 +1 @@ +. diff --git a/tests/common/delete.sh b/tests/common/delete.sh new file mode 100644 index 0000000..638f307 --- /dev/null +++ b/tests/common/delete.sh @@ -0,0 +1,4 @@ +cd "$TEST" +"$XTOUCH" -p foo/bar/baz +invoke_bfs . -delete +bfs_diff . diff --git a/tests/common/delete_error.out b/tests/common/delete_error.out new file mode 100644 index 0000000..b6b6505 --- /dev/null +++ b/tests/common/delete_error.out @@ -0,0 +1,8 @@ +. +. +./baz +./baz +./baz/qux +./baz/qux +./foo +./foo/bar diff --git a/tests/common/delete_error.sh b/tests/common/delete_error.sh new file mode 100644 index 0000000..e6327f3 --- /dev/null +++ b/tests/common/delete_error.sh @@ -0,0 +1,9 @@ +cd "$TEST" + +"$XTOUCH" -p foo/bar baz/qux +chmod -w foo +defer chmod +w foo + +! invoke_bfs . -print -delete -print >"$OUT" || fail +sort_output +diff_output diff --git a/tests/common/delete_many.out b/tests/common/delete_many.out new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/tests/common/delete_many.out @@ -0,0 +1 @@ +. diff --git a/tests/common/delete_many.sh b/tests/common/delete_many.sh new file mode 100644 index 0000000..48fe4c2 --- /dev/null +++ b/tests/common/delete_many.sh @@ -0,0 +1,8 @@ +# Test for https://github.com/tavianator/bfs/issues/67 + +cd "$TEST" +mkdir foo +"$XTOUCH" foo/{1..256} + +invoke_bfs foo -delete +bfs_diff . diff --git a/tests/test_depth_n_minus.out b/tests/common/depth_maxdepth_1.out index 7575ae4..7575ae4 100644 --- a/tests/test_depth_n_minus.out +++ b/tests/common/depth_maxdepth_1.out diff --git a/tests/common/depth_maxdepth_1.sh b/tests/common/depth_maxdepth_1.sh new file mode 100644 index 0000000..4b7e538 --- /dev/null +++ b/tests/common/depth_maxdepth_1.sh @@ -0,0 +1 @@ +bfs_diff basic -maxdepth 1 -depth diff --git a/tests/test_depth_maxdepth_2.out b/tests/common/depth_maxdepth_2.out index c53041e..c53041e 100644 --- a/tests/test_depth_maxdepth_2.out +++ b/tests/common/depth_maxdepth_2.out diff --git a/tests/common/depth_maxdepth_2.sh b/tests/common/depth_maxdepth_2.sh new file mode 100644 index 0000000..2c49a65 --- /dev/null +++ b/tests/common/depth_maxdepth_2.sh @@ -0,0 +1 @@ +bfs_diff basic -maxdepth 2 -depth diff --git a/tests/test_depth_mindepth_1.out b/tests/common/depth_mindepth_1.out index 3b461cf..3b461cf 100644 --- a/tests/test_depth_mindepth_1.out +++ b/tests/common/depth_mindepth_1.out diff --git a/tests/common/depth_mindepth_1.sh b/tests/common/depth_mindepth_1.sh new file mode 100644 index 0000000..868d9e1 --- /dev/null +++ b/tests/common/depth_mindepth_1.sh @@ -0,0 +1 @@ +bfs_diff basic -mindepth 1 -depth diff --git a/tests/test_depth_mindepth_2.out b/tests/common/depth_mindepth_2.out index 6ccd80a..6ccd80a 100644 --- a/tests/test_depth_mindepth_2.out +++ b/tests/common/depth_mindepth_2.out diff --git a/tests/common/depth_mindepth_2.sh b/tests/common/depth_mindepth_2.sh new file mode 100644 index 0000000..2031b2c --- /dev/null +++ b/tests/common/depth_mindepth_2.sh @@ -0,0 +1 @@ +bfs_diff basic -mindepth 2 -depth diff --git a/tests/test_double_dash.out b/tests/common/double_dash.out index 774cc7c..774cc7c 100644 --- a/tests/test_double_dash.out +++ b/tests/common/double_dash.out diff --git a/tests/common/double_dash.sh b/tests/common/double_dash.sh new file mode 100644 index 0000000..1a7a118 --- /dev/null +++ b/tests/common/double_dash.sh @@ -0,0 +1,2 @@ +cd basic +bfs_diff -- . -type f diff --git a/tests/test_empty.out b/tests/common/empty.out index a0f4b76..a0f4b76 100644 --- a/tests/test_empty.out +++ b/tests/common/empty.out diff --git a/tests/common/empty.sh b/tests/common/empty.sh new file mode 100644 index 0000000..95ee988 --- /dev/null +++ b/tests/common/empty.sh @@ -0,0 +1 @@ +bfs_diff basic -empty 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/common/empty_special.out b/tests/common/empty_special.out new file mode 100644 index 0000000..fa35478 --- /dev/null +++ b/tests/common/empty_special.out @@ -0,0 +1,20 @@ +rainbow/[1m/[0m +rainbow/exec.sh +rainbow/file.dat +rainbow/file.txt +rainbow/lower.gz +rainbow/lower.tar +rainbow/lower.tar.gz +rainbow/lu.tar.GZ +rainbow/mh1 +rainbow/mh2 +rainbow/ow +rainbow/sgid +rainbow/sticky +rainbow/sticky_ow +rainbow/sugid +rainbow/suid +rainbow/ul.TAR.gz +rainbow/upper.GZ +rainbow/upper.TAR +rainbow/upper.TAR.GZ diff --git a/tests/common/empty_special.sh b/tests/common/empty_special.sh new file mode 100644 index 0000000..31e9d2e --- /dev/null +++ b/tests/common/empty_special.sh @@ -0,0 +1 @@ +bfs_diff rainbow -empty diff --git a/tests/test_exec_substring.out b/tests/common/exec_substring.out index 32a6353..32a6353 100644 --- a/tests/test_exec_substring.out +++ b/tests/common/exec_substring.out diff --git a/tests/common/exec_substring.sh b/tests/common/exec_substring.sh new file mode 100644 index 0000000..5cf8e85 --- /dev/null +++ b/tests/common/exec_substring.sh @@ -0,0 +1 @@ +bfs_diff basic -exec echo '-{}-' \; diff --git a/tests/test_gid.out b/tests/common/execdir_nonexistent.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid.out +++ b/tests/common/execdir_nonexistent.out diff --git a/tests/common/execdir_nonexistent.sh b/tests/common/execdir_nonexistent.sh new file mode 100644 index 0000000..0ec013c --- /dev/null +++ b/tests/common/execdir_nonexistent.sh @@ -0,0 +1,2 @@ +bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/test_execdir_pwd.out b/tests/common/execdir_pwd.out index a11cd25..a11cd25 100644 --- a/tests/test_execdir_pwd.out +++ b/tests/common/execdir_pwd.out diff --git a/tests/common/execdir_pwd.sh b/tests/common/execdir_pwd.sh new file mode 100644 index 0000000..1c0165a --- /dev/null +++ b/tests/common/execdir_pwd.sh @@ -0,0 +1,3 @@ +TMP_REAL=$(cd "$TMP" && pwd) +OFFSET=$((${#TMP_REAL} + 2)) +bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \; diff --git a/tests/test_execdir_slash.out b/tests/common/execdir_slash.out index b498fd4..b498fd4 100644 --- a/tests/test_execdir_slash.out +++ b/tests/common/execdir_slash.out diff --git a/tests/common/execdir_slash.sh b/tests/common/execdir_slash.sh new file mode 100644 index 0000000..965f679 --- /dev/null +++ b/tests/common/execdir_slash.sh @@ -0,0 +1,2 @@ +# Don't prepend ./ for absolute paths in -execdir +bfs_diff / -maxdepth 0 -execdir echo {} \; diff --git a/tests/test_execdir_slash_pwd.out b/tests/common/execdir_slash_pwd.out index b498fd4..b498fd4 100644 --- a/tests/test_execdir_slash_pwd.out +++ b/tests/common/execdir_slash_pwd.out diff --git a/tests/common/execdir_slash_pwd.sh b/tests/common/execdir_slash_pwd.sh new file mode 100644 index 0000000..9c82e09 --- /dev/null +++ b/tests/common/execdir_slash_pwd.sh @@ -0,0 +1 @@ +bfs_diff / -maxdepth 0 -execdir pwd \; diff --git a/tests/test_execdir_slashes.out b/tests/common/execdir_slashes.out index b498fd4..b498fd4 100644 --- a/tests/test_execdir_slashes.out +++ b/tests/common/execdir_slashes.out diff --git a/tests/common/execdir_slashes.sh b/tests/common/execdir_slashes.sh new file mode 100644 index 0000000..4e2b327 --- /dev/null +++ b/tests/common/execdir_slashes.sh @@ -0,0 +1 @@ +bfs_diff /// -maxdepth 0 -execdir echo {} \; diff --git a/tests/test_execdir_ulimit.out b/tests/common/execdir_ulimit.out index 7f53982..bf52c09 100644 --- a/tests/test_execdir_ulimit.out +++ b/tests/common/execdir_ulimit.out @@ -1,3 +1,4 @@ +./. ./0 ./1 ./2 @@ -30,7 +31,6 @@ ./q ./r ./s -./scratch ./t ./u ./v diff --git a/tests/common/execdir_ulimit.sh b/tests/common/execdir_ulimit.sh new file mode 100644 index 0000000..122c282 --- /dev/null +++ b/tests/common/execdir_ulimit.sh @@ -0,0 +1,6 @@ +cd "$TEST" +mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z +mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C + +ulimit -n $((NOPENFD + 10)) +bfs_diff . -execdir echo {} \; diff --git a/tests/test_flag_double_dash.out b/tests/common/flag_double_dash.out index 774cc7c..774cc7c 100644 --- a/tests/test_flag_double_dash.out +++ b/tests/common/flag_double_dash.out diff --git a/tests/common/flag_double_dash.sh b/tests/common/flag_double_dash.sh new file mode 100644 index 0000000..1075b06 --- /dev/null +++ b/tests/common/flag_double_dash.sh @@ -0,0 +1,2 @@ +cd basic +bfs_diff -L -- . -type f diff --git a/tests/test_follow.out b/tests/common/follow.out index ec9e861..ec9e861 100644 --- a/tests/test_follow.out +++ b/tests/common/follow.out diff --git a/tests/common/follow.sh b/tests/common/follow.sh new file mode 100644 index 0000000..b5a2ae1 --- /dev/null +++ b/tests/common/follow.sh @@ -0,0 +1 @@ +bfs_diff links -follow diff --git a/tests/test_gid_minus.out b/tests/common/gid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid_minus.out +++ b/tests/common/gid.out diff --git a/tests/common/gid.sh b/tests/common/gid.sh new file mode 100644 index 0000000..2707b4a --- /dev/null +++ b/tests/common/gid.sh @@ -0,0 +1 @@ +bfs_diff basic -gid "$(id -g)" 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/test_gid_minus_plus.out b/tests/common/gid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid_minus_plus.out +++ b/tests/common/gid_minus.out diff --git a/tests/common/gid_minus.sh b/tests/common/gid_minus.sh new file mode 100644 index 0000000..e3822f0 --- /dev/null +++ b/tests/common/gid_minus.sh @@ -0,0 +1 @@ +bfs_diff basic -gid "-$(($(id -g) + 1))" diff --git a/tests/test_gid_name.out b/tests/common/gid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid_name.out +++ b/tests/common/gid_minus_plus.out diff --git a/tests/common/gid_minus_plus.sh b/tests/common/gid_minus_plus.sh new file mode 100644 index 0000000..4ff0877 --- /dev/null +++ b/tests/common/gid_minus_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -gid "-+$(($(id -g) + 1))" diff --git a/tests/test_gid_plus.out b/tests/common/gid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid_plus.out +++ b/tests/common/gid_plus.out diff --git a/tests/common/gid_plus.sh b/tests/common/gid_plus.sh new file mode 100644 index 0000000..ccba0e6 --- /dev/null +++ b/tests/common/gid_plus.sh @@ -0,0 +1,2 @@ +test "$(id -g)" -eq 0 && skip +bfs_diff basic -gid +0 diff --git a/tests/test_gid_plus_plus.out b/tests/common/gid_plus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_gid_plus_plus.out +++ b/tests/common/gid_plus_plus.out diff --git a/tests/common/gid_plus_plus.sh b/tests/common/gid_plus_plus.sh new file mode 100644 index 0000000..ec7ae86 --- /dev/null +++ b/tests/common/gid_plus_plus.sh @@ -0,0 +1,2 @@ +test "$(id -g)" -eq 0 && skip +bfs_diff basic -gid ++0 diff --git a/tests/test_ilname.out b/tests/common/ilname.out index e69de29..e69de29 100644 --- a/tests/test_ilname.out +++ b/tests/common/ilname.out diff --git a/tests/common/ilname.sh b/tests/common/ilname.sh new file mode 100644 index 0000000..fc7e9e4 --- /dev/null +++ b/tests/common/ilname.sh @@ -0,0 +1,2 @@ +invoke_bfs -quit -ilname PATTERN || skip +bfs_diff links -ilname '[AQ]' diff --git a/tests/test_inum.out b/tests/common/inum.out index ebcaf79..ebcaf79 100644 --- a/tests/test_inum.out +++ b/tests/common/inum.out diff --git a/tests/common/inum.sh b/tests/common/inum.sh new file mode 100644 index 0000000..ca63bbb --- /dev/null +++ b/tests/common/inum.sh @@ -0,0 +1 @@ +bfs_diff basic -inum "$(inum basic/k/foo/bar)" diff --git a/tests/common/inum_bind_mount.out b/tests/common/inum_bind_mount.out new file mode 100644 index 0000000..ede8749 --- /dev/null +++ b/tests/common/inum_bind_mount.out @@ -0,0 +1,2 @@ +./bar +./foo diff --git a/tests/common/inum_bind_mount.sh b/tests/common/inum_bind_mount.sh new file mode 100644 index 0000000..892713e --- /dev/null +++ b/tests/common/inum_bind_mount.sh @@ -0,0 +1,9 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +"$XTOUCH" foo bar baz + +bfs_sudo mount --bind foo bar || skip +defer bfs_sudo umount bar + +bfs_diff . -inum "$(inum bar)" diff --git a/tests/common/inum_mount.out b/tests/common/inum_mount.out new file mode 100644 index 0000000..99fa01e --- /dev/null +++ b/tests/common/inum_mount.out @@ -0,0 +1 @@ +./mnt diff --git a/tests/common/inum_mount.sh b/tests/common/inum_mount.sh new file mode 100644 index 0000000..7facf57 --- /dev/null +++ b/tests/common/inum_mount.sh @@ -0,0 +1,9 @@ +test "$UNAME" = "Darwin" && skip + +cd "$TEST" +mkdir foo mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +bfs_diff . -inum "$(inum mnt)" diff --git a/tests/test_ipath.out b/tests/common/ipath.out index ae1ae21..ae1ae21 100644 --- a/tests/test_ipath.out +++ b/tests/common/ipath.out diff --git a/tests/common/ipath.sh b/tests/common/ipath.sh new file mode 100644 index 0000000..7d05f31 --- /dev/null +++ b/tests/common/ipath.sh @@ -0,0 +1,2 @@ +invoke_bfs -quit -ipath PATTERN || skip +bfs_diff basic -ipath 'basic/*F*' diff --git a/tests/test_iregex.out b/tests/common/iregex.out index cfc113b..cfc113b 100644 --- a/tests/test_iregex.out +++ b/tests/common/iregex.out diff --git a/tests/common/iregex.sh b/tests/common/iregex.sh new file mode 100644 index 0000000..fc782f5 --- /dev/null +++ b/tests/common/iregex.sh @@ -0,0 +1 @@ +bfs_diff basic -iregex 'basic/[A-Z]/[a-z]' diff --git a/tests/test_lname.out b/tests/common/lname.out index e69de29..e69de29 100644 --- a/tests/test_lname.out +++ b/tests/common/lname.out diff --git a/tests/common/lname.sh b/tests/common/lname.sh new file mode 100644 index 0000000..cf8a2a1 --- /dev/null +++ b/tests/common/lname.sh @@ -0,0 +1 @@ +bfs_diff links -lname '[aq]' diff --git a/tests/common/ls.sh b/tests/common/ls.sh new file mode 100644 index 0000000..bc50d90 --- /dev/null +++ b/tests/common/ls.sh @@ -0,0 +1 @@ +invoke_bfs rainbow -ls >"$OUT" diff --git a/tests/test_maxdepth.out b/tests/common/maxdepth.out index 7575ae4..7575ae4 100644 --- a/tests/test_maxdepth.out +++ b/tests/common/maxdepth.out diff --git a/tests/common/maxdepth.sh b/tests/common/maxdepth.sh new file mode 100644 index 0000000..bb47cc9 --- /dev/null +++ b/tests/common/maxdepth.sh @@ -0,0 +1 @@ +bfs_diff basic -maxdepth 1 diff --git a/tests/common/maxdepth_incomplete.sh b/tests/common/maxdepth_incomplete.sh new file mode 100644 index 0000000..0bcb461 --- /dev/null +++ b/tests/common/maxdepth_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs basic -maxdepth diff --git a/tests/test_mindepth.out b/tests/common/mindepth.out index 3b461cf..3b461cf 100644 --- a/tests/test_mindepth.out +++ b/tests/common/mindepth.out diff --git a/tests/common/mindepth.sh b/tests/common/mindepth.sh new file mode 100644 index 0000000..22d7770 --- /dev/null +++ b/tests/common/mindepth.sh @@ -0,0 +1 @@ +bfs_diff basic -mindepth 1 diff --git a/tests/common/mindepth_incomplete.sh b/tests/common/mindepth_incomplete.sh new file mode 100644 index 0000000..6f55a42 --- /dev/null +++ b/tests/common/mindepth_incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs basic -mindepth 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/test_newer.out b/tests/common/newerma.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_newer.out +++ b/tests/common/newerma.out diff --git a/tests/common/newerma.sh b/tests/common/newerma.sh new file mode 100644 index 0000000..b05af8d --- /dev/null +++ b/tests/common/newerma.sh @@ -0,0 +1 @@ +bfs_diff times -newerma times/a diff --git a/tests/test_newermt.out b/tests/common/newermt.out index 650e550..650e550 100644 --- a/tests/test_newermt.out +++ b/tests/common/newermt.out diff --git a/tests/common/newermt.sh b/tests/common/newermt.sh new file mode 100644 index 0000000..e816b29 --- /dev/null +++ b/tests/common/newermt.sh @@ -0,0 +1,3 @@ +bfs_diff times -newermt 1991-12-14T00:01 \ + -newermt "1991-12-14 01:01+01:00" \ + -newermt "19911213 20:31:00-0330" diff --git a/tests/test_newermt_epoch_minus_one.out b/tests/common/newermt_epoch_minus_one.out index f7f63b0..f7f63b0 100644 --- a/tests/test_newermt_epoch_minus_one.out +++ b/tests/common/newermt_epoch_minus_one.out diff --git a/tests/common/newermt_epoch_minus_one.sh b/tests/common/newermt_epoch_minus_one.sh new file mode 100644 index 0000000..568e2f3 --- /dev/null +++ b/tests/common/newermt_epoch_minus_one.sh @@ -0,0 +1 @@ +bfs_diff times -newermt 1969-12-31T23:59:59Z diff --git a/tests/test_name_backslash.out b/tests/common/ok_closed_stdin.out index e69de29..e69de29 100644 --- a/tests/test_name_backslash.out +++ b/tests/common/ok_closed_stdin.out diff --git a/tests/common/ok_closed_stdin.sh b/tests/common/ok_closed_stdin.sh new file mode 100644 index 0000000..687e998 --- /dev/null +++ b/tests/common/ok_closed_stdin.sh @@ -0,0 +1 @@ +bfs_diff basic -ok echo \; <&- diff --git a/tests/test_nogroup.out b/tests/common/okdir_closed_stdin.out index e69de29..e69de29 100644 --- a/tests/test_nogroup.out +++ b/tests/common/okdir_closed_stdin.out diff --git a/tests/common/okdir_closed_stdin.sh b/tests/common/okdir_closed_stdin.sh new file mode 100644 index 0000000..a515298 --- /dev/null +++ b/tests/common/okdir_closed_stdin.sh @@ -0,0 +1 @@ +bfs_diff basic -okdir echo {} \; <&- diff --git a/tests/test_name_root_depth.out b/tests/common/quit.out index cf4d5a9..cf4d5a9 100644 --- a/tests/test_name_root_depth.out +++ b/tests/common/quit.out diff --git a/tests/common/quit.sh b/tests/common/quit.sh new file mode 100644 index 0000000..46b60c5 --- /dev/null +++ b/tests/common/quit.sh @@ -0,0 +1 @@ +bfs_diff basic/g -print -name g -quit diff --git a/tests/test_comma_reachability.out b/tests/common/quit_after_print.out index 15a13db..15a13db 100644 --- a/tests/test_comma_reachability.out +++ b/tests/common/quit_after_print.out diff --git a/tests/common/quit_after_print.sh b/tests/common/quit_after_print.sh new file mode 100644 index 0000000..ee5653a --- /dev/null +++ b/tests/common/quit_after_print.sh @@ -0,0 +1 @@ +bfs_diff basic basic -print -quit diff --git a/tests/test_nogroup_ulimit.out b/tests/common/quit_before_print.out index e69de29..e69de29 100644 --- a/tests/test_nogroup_ulimit.out +++ b/tests/common/quit_before_print.out diff --git a/tests/common/quit_before_print.sh b/tests/common/quit_before_print.sh new file mode 100644 index 0000000..cda3a2c --- /dev/null +++ b/tests/common/quit_before_print.sh @@ -0,0 +1 @@ +bfs_diff basic basic -quit -print diff --git a/tests/test_quit_child.out b/tests/common/quit_child.out index fb683c7..fb683c7 100644 --- a/tests/test_quit_child.out +++ b/tests/common/quit_child.out diff --git a/tests/common/quit_child.sh b/tests/common/quit_child.sh new file mode 100644 index 0000000..bd27eff --- /dev/null +++ b/tests/common/quit_child.sh @@ -0,0 +1 @@ +bfs_diff basic/g -print -name h -quit diff --git a/tests/test_quit_depth.out b/tests/common/quit_depth.out index fb683c7..fb683c7 100644 --- a/tests/test_quit_depth.out +++ b/tests/common/quit_depth.out diff --git a/tests/common/quit_depth.sh b/tests/common/quit_depth.sh new file mode 100644 index 0000000..f5f82ba --- /dev/null +++ b/tests/common/quit_depth.sh @@ -0,0 +1 @@ +bfs_diff basic/g -depth -print -name g -quit diff --git a/tests/test_quit_depth_child.out b/tests/common/quit_depth_child.out index 72b086d..72b086d 100644 --- a/tests/test_quit_depth_child.out +++ b/tests/common/quit_depth_child.out diff --git a/tests/common/quit_depth_child.sh b/tests/common/quit_depth_child.sh new file mode 100644 index 0000000..dd09d5b --- /dev/null +++ b/tests/common/quit_depth_child.sh @@ -0,0 +1 @@ +bfs_diff basic/g -depth -print -name h -quit diff --git a/tests/test_regex.out b/tests/common/regex.out index cfc113b..cfc113b 100644 --- a/tests/test_regex.out +++ b/tests/common/regex.out diff --git a/tests/common/regex.sh b/tests/common/regex.sh new file mode 100644 index 0000000..a3bdae8 --- /dev/null +++ b/tests/common/regex.sh @@ -0,0 +1 @@ +bfs_diff basic -regex 'basic/./.' diff --git a/tests/test_regex_parens.out b/tests/common/regex_parens.out index 0f0971e..0f0971e 100644 --- a/tests/test_regex_parens.out +++ b/tests/common/regex_parens.out diff --git a/tests/common/regex_parens.sh b/tests/common/regex_parens.sh new file mode 100644 index 0000000..fe0abf6 --- /dev/null +++ b/tests/common/regex_parens.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff . -regex '\./\((\)' diff --git a/tests/test_links.out b/tests/common/samefile.out index 996ffc8..996ffc8 100644 --- a/tests/test_links.out +++ b/tests/common/samefile.out diff --git a/tests/common/samefile.sh b/tests/common/samefile.sh new file mode 100644 index 0000000..8e51966 --- /dev/null +++ b/tests/common/samefile.sh @@ -0,0 +1 @@ +bfs_diff links -samefile links/file diff --git a/tests/test_L_broken.out b/tests/common/samefile_broken.out index 21d6316..21d6316 100644 --- a/tests/test_L_broken.out +++ b/tests/common/samefile_broken.out diff --git a/tests/common/samefile_broken.sh b/tests/common/samefile_broken.sh new file mode 100644 index 0000000..1cb52db --- /dev/null +++ b/tests/common/samefile_broken.sh @@ -0,0 +1 @@ +bfs_diff links -samefile links/broken diff --git a/tests/test_L_notdir.out b/tests/common/samefile_notdir.out index 6e6658d..6e6658d 100644 --- a/tests/test_L_notdir.out +++ b/tests/common/samefile_notdir.out diff --git a/tests/common/samefile_notdir.sh b/tests/common/samefile_notdir.sh new file mode 100644 index 0000000..f274ef6 --- /dev/null +++ b/tests/common/samefile_notdir.sh @@ -0,0 +1 @@ +bfs_diff links -samefile links/notdir diff --git a/tests/test_samefile_symlink.out b/tests/common/samefile_symlink.out index 299a572..299a572 100644 --- a/tests/test_samefile_symlink.out +++ b/tests/common/samefile_symlink.out diff --git a/tests/common/samefile_symlink.sh b/tests/common/samefile_symlink.sh new file mode 100644 index 0000000..55ccf5c --- /dev/null +++ b/tests/common/samefile_symlink.sh @@ -0,0 +1 @@ +bfs_diff links -samefile links/symlink diff --git a/tests/common/samefile_wordesc.sh b/tests/common/samefile_wordesc.sh new file mode 100644 index 0000000..b5d158f --- /dev/null +++ b/tests/common/samefile_wordesc.sh @@ -0,0 +1,4 @@ +# Regression test: don't abort on incomplete UTF-8 sequences +export LC_ALL=$(locale -a | grep -Ei 'utf-?8$' | head -n1) +test -n "$LC_ALL" || skip +! invoke_bfs -samefile $'\xFA\xFA' diff --git a/tests/test_nouser.out b/tests/common/size_big.out index e69de29..e69de29 100644 --- a/tests/test_nouser.out +++ b/tests/common/size_big.out diff --git a/tests/common/size_big.sh b/tests/common/size_big.sh new file mode 100644 index 0000000..6c100eb --- /dev/null +++ b/tests/common/size_big.sh @@ -0,0 +1 @@ +bfs_diff basic -size 9223372036854775807 diff --git a/tests/test_group_id.out b/tests/common/uid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_group_id.out +++ b/tests/common/uid.out diff --git a/tests/common/uid.sh b/tests/common/uid.sh new file mode 100644 index 0000000..fb3cd93 --- /dev/null +++ b/tests/common/uid.sh @@ -0,0 +1 @@ +bfs_diff basic -uid "$(id -u)" 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/test_group_name.out b/tests/common/uid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_group_name.out +++ b/tests/common/uid_minus.out diff --git a/tests/common/uid_minus.sh b/tests/common/uid_minus.sh new file mode 100644 index 0000000..6d371f2 --- /dev/null +++ b/tests/common/uid_minus.sh @@ -0,0 +1 @@ +bfs_diff basic -uid "-$(($(id -u) + 1))" diff --git a/tests/test_group_nogroup.out b/tests/common/uid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_group_nogroup.out +++ b/tests/common/uid_minus_plus.out diff --git a/tests/common/uid_minus_plus.sh b/tests/common/uid_minus_plus.sh new file mode 100644 index 0000000..e7a0496 --- /dev/null +++ b/tests/common/uid_minus_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -uid "-+$(($(id -u) + 1))" diff --git a/tests/test_path_d.out b/tests/common/uid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_path_d.out +++ b/tests/common/uid_plus.out diff --git a/tests/common/uid_plus.sh b/tests/common/uid_plus.sh new file mode 100644 index 0000000..22b2c8e --- /dev/null +++ b/tests/common/uid_plus.sh @@ -0,0 +1,2 @@ +test "$(id -u)" -eq 0 && skip +bfs_diff basic -uid +0 diff --git a/tests/test_stderr_fails_silently.out b/tests/common/uid_plus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_stderr_fails_silently.out +++ b/tests/common/uid_plus_plus.out diff --git a/tests/common/uid_plus_plus.sh b/tests/common/uid_plus_plus.sh new file mode 100644 index 0000000..e021888 --- /dev/null +++ b/tests/common/uid_plus_plus.sh @@ -0,0 +1,2 @@ +test "$(id -u)" -eq 0 && skip +bfs_diff basic -uid ++0 diff --git a/tests/find-color.sh b/tests/find-color.sh index ecdd5af..47de2a2 100755 --- a/tests/find-color.sh +++ b/tests/find-color.sh @@ -1,20 +1,7 @@ #!/usr/bin/env bash -############################################################################ -# bfs # -# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> # -# # -# Permission to use, copy, modify, and/or distribute this software for any # -# purpose with or without fee is hereby granted. # -# # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -############################################################################ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD set -e diff --git a/tests/getopts.sh b/tests/getopts.sh new file mode 100644 index 0000000..a16511f --- /dev/null +++ b/tests/getopts.sh @@ -0,0 +1,174 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Argument parsing + +JOBS=$(_nproc) +MAKE= +PATTERNS=() +SUDO=() +STOP=0 +CLEAN=1 +UPDATE=0 +VERBOSE_COMMANDS=0 +VERBOSE_ERRORS=0 +VERBOSE_SKIPPED=0 +VERBOSE_TESTS=0 + +# Print usage information +usage() { + 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}]] + [${BLU}--stop${RST}] [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}] + [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]] + + ${BLU}-j${RST}${BLD}N${RST} + Run ${BLD}N${RST} tests in parallel (default: ${BLD}$JOBS${RST}) + + ${BLU}--make${RST}=${BLD}MAKE${RST} + Use the jobserver from ${BLD}MAKE${RST}, e.g. ${BLU}--make${RST}=${BLD}"make -j$JOBS"${RST} + + ${BLU}--bfs${RST}=${BLD}path/to/bfs${RST} + Set the path to the bfs executable to test (default: ${BLD}./bin/bfs${RST}) + + ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}] + Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST} + + ${BLU}--stop${RST} + Stop when the first error occurs + + ${BLU}--no-clean${RST} + Keep the test directories around after the run + + ${BLU}--update${RST} + Update the expected outputs for the test cases + + ${BLU}--verbose${RST}=${BLD}commands${RST} + Log the commands that get executed + ${BLU}--verbose${RST}=${BLD}errors${RST} + Don't redirect standard error + ${BLU}--verbose${RST}=${BLD}skipped${RST} + Log which tests get skipped + ${BLU}--verbose${RST}=${BLD}tests${RST} + Log all tests that get run + ${BLU}--verbose${RST} + Log everything + + ${BLU}--help${RST} + This message + + ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST} + Choose which test cases to run (default: ${BLU}--all${RST}) + + ${BLD}TEST${RST} + Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...) +EOF +} + +# Parse the command line +parse_args() { + for arg; do + case "$arg" in + -j?*) + JOBS="${arg#-j}" + ;; + --make=*) + MAKE="${arg#*=}" + ;; + --bfs=*) + BFS="${arg#*=}" + ;; + --posix) + PATTERNS+=("posix/*") + ;; + --bsd) + PATTERNS+=("posix/*" "common/*" "bsd/*") + ;; + --gnu) + PATTERNS+=("posix/*" "common/*" "gnu/*") + ;; + --all) + PATTERNS+=("*") + ;; + --sudo) + SUDO=(sudo) + ;; + --sudo=*) + read -a SUDO <<<"${arg#*=}" + ;; + --stop) + STOP=1 + ;; + --no-clean|--noclean) + CLEAN=0 + ;; + --update) + UPDATE=1 + ;; + --verbose=commands) + VERBOSE_COMMANDS=1 + ;; + --verbose=errors) + VERBOSE_ERRORS=1 + ;; + --verbose=skipped) + VERBOSE_SKIPPED=1 + ;; + --verbose=tests) + VERBOSE_TESTS=1 + ;; + --verbose) + VERBOSE_COMMANDS=1 + VERBOSE_ERRORS=1 + VERBOSE_SKIPPED=1 + VERBOSE_TESTS=1 + ;; + --help) + usage + exit 0 + ;; + -*) + color printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2 + usage >&2 + exit 1 + ;; + *) + PATTERNS+=("$arg") + ;; + esac + done + + read -a MAKE <<<"$MAKE" + + # Try to resolve the path to $BFS before we cd, while also supporting + # --bfs="./bin/bfs -S ids" + read -a BFS <<<"${BFS:-$BIN/bfs}" + BFS[0]=$(_realpath "$(command -v "${BFS[0]}")") + + if ((${#PATTERNS[@]} == 0)); then + PATTERNS=("*") + fi + + TEST_CASES=() + ALL_TESTS=($(cd "$TESTS" && quote {posix,common,bsd,gnu,bfs}/*.sh)) + for TEST in "${ALL_TESTS[@]}"; do + TEST="${TEST%.sh}" + for PATTERN in "${PATTERNS[@]}"; do + if [[ $TEST == $PATTERN ]]; then + TEST_CASES+=("$TEST") + break + fi + done + done + + if ((${#TEST_CASES[@]} == 0)); then + color printf "${RED}error:${RST} No tests matched" >&2 + color printf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2 + color printf ".\n\n" >&2 + usage >&2 + exit 1 + fi +} diff --git a/tests/gnu/L_delete.out b/tests/gnu/L_delete.out new file mode 100644 index 0000000..7ed5f0d --- /dev/null +++ b/tests/gnu/L_delete.out @@ -0,0 +1,2 @@ +. +./foo diff --git a/tests/gnu/L_delete.sh b/tests/gnu/L_delete.sh new file mode 100644 index 0000000..0559c49 --- /dev/null +++ b/tests/gnu/L_delete.sh @@ -0,0 +1,8 @@ +cd "$TEST" +mkdir foo bar +ln -s ../foo bar/baz + +# Don't try to rmdir() a symlink +invoke_bfs -L bar -delete + +bfs_diff . diff --git a/tests/gnu/L_loops_continue.out b/tests/gnu/L_loops_continue.out new file mode 100644 index 0000000..a514555 --- /dev/null +++ b/tests/gnu/L_loops_continue.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/L_loops_continue.sh b/tests/gnu/L_loops_continue.sh new file mode 100644 index 0000000..55aeb33 --- /dev/null +++ b/tests/gnu/L_loops_continue.sh @@ -0,0 +1 @@ +! bfs_diff -L loops 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/test_L_xtype_f.out b/tests/gnu/L_xtype_f.out index 8b95397..8b95397 100644 --- a/tests/test_L_xtype_f.out +++ b/tests/gnu/L_xtype_f.out diff --git a/tests/gnu/L_xtype_f.sh b/tests/gnu/L_xtype_f.sh new file mode 100644 index 0000000..47f7be7 --- /dev/null +++ b/tests/gnu/L_xtype_f.sh @@ -0,0 +1 @@ +bfs_diff -L links -xtype f diff --git a/tests/test_L_xtype_l.out b/tests/gnu/L_xtype_l.out index 973864f..973864f 100644 --- a/tests/test_L_xtype_l.out +++ b/tests/gnu/L_xtype_l.out diff --git a/tests/gnu/L_xtype_l.sh b/tests/gnu/L_xtype_l.sh new file mode 100644 index 0000000..afe52ef --- /dev/null +++ b/tests/gnu/L_xtype_l.sh @@ -0,0 +1 @@ +bfs_diff -L links -xtype l diff --git a/tests/test_a.out b/tests/gnu/and.out index 722962c..722962c 100644 --- a/tests/test_a.out +++ b/tests/gnu/and.out diff --git a/tests/gnu/and.sh b/tests/gnu/and.sh new file mode 100644 index 0000000..1606455 --- /dev/null +++ b/tests/gnu/and.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -and -type d diff --git a/tests/test_comma_redundant_false.out b/tests/gnu/and_false_or_true.out index 15a13db..15a13db 100644 --- a/tests/test_comma_redundant_false.out +++ b/tests/gnu/and_false_or_true.out diff --git a/tests/gnu/and_false_or_true.sh b/tests/gnu/and_false_or_true.sh new file mode 100644 index 0000000..e500722 --- /dev/null +++ b/tests/gnu/and_false_or_true.sh @@ -0,0 +1,3 @@ +# Test (-a lhs(always_true) -false) <==> (! lhs), +# (-o lhs(always_false) -true) <==> (! lhs) +bfs_diff basic -prune -false -o -true diff --git a/tests/test_nouser_ulimit.out b/tests/gnu/and_purity.out index e69de29..e69de29 100644 --- a/tests/test_nouser_ulimit.out +++ b/tests/gnu/and_purity.out diff --git a/tests/gnu/and_purity.sh b/tests/gnu/and_purity.sh new file mode 100644 index 0000000..55e2cfc --- /dev/null +++ b/tests/gnu/and_purity.sh @@ -0,0 +1,2 @@ +# Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure +bfs_diff basic -name nonexistent \( -print , -false \) diff --git a/tests/test_comma.out b/tests/gnu/comma.out index 740eefc..740eefc 100644 --- a/tests/test_comma.out +++ b/tests/gnu/comma.out diff --git a/tests/gnu/comma.sh b/tests/gnu/comma.sh new file mode 100644 index 0000000..cdcebf8 --- /dev/null +++ b/tests/gnu/comma.sh @@ -0,0 +1 @@ +bfs_diff basic -name '*f*' -print , -print diff --git a/tests/test_comma_redundant_true.out b/tests/gnu/comma_reachability.out index 15a13db..15a13db 100644 --- a/tests/test_comma_redundant_true.out +++ b/tests/gnu/comma_reachability.out diff --git a/tests/gnu/comma_reachability.sh b/tests/gnu/comma_reachability.sh new file mode 100644 index 0000000..60c26bc --- /dev/null +++ b/tests/gnu/comma_reachability.sh @@ -0,0 +1 @@ +bfs_diff basic -print -quit , -print diff --git a/tests/test_fprint_truncate.out b/tests/gnu/comma_redundant_false.out index 15a13db..15a13db 100644 --- a/tests/test_fprint_truncate.out +++ b/tests/gnu/comma_redundant_false.out diff --git a/tests/gnu/comma_redundant_false.sh b/tests/gnu/comma_redundant_false.sh new file mode 100644 index 0000000..f35d9b8 --- /dev/null +++ b/tests/gnu/comma_redundant_false.sh @@ -0,0 +1,2 @@ +# Test (, lhs(always_false) -false) <==> lhs +bfs_diff basic -print -not -prune , -false diff --git a/tests/test_not_reachability.out b/tests/gnu/comma_redundant_true.out index 15a13db..15a13db 100644 --- a/tests/test_not_reachability.out +++ b/tests/gnu/comma_redundant_true.out diff --git a/tests/gnu/comma_redundant_true.sh b/tests/gnu/comma_redundant_true.sh new file mode 100644 index 0000000..f9eef57 --- /dev/null +++ b/tests/gnu/comma_redundant_true.sh @@ -0,0 +1,2 @@ +# Test (, lhs(always_true) -true) <==> lhs +bfs_diff basic -prune , -true diff --git a/tests/test_true.out b/tests/gnu/daystart.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_true.out +++ b/tests/gnu/daystart.out diff --git a/tests/gnu/daystart.sh b/tests/gnu/daystart.sh new file mode 100644 index 0000000..9c3be1a --- /dev/null +++ b/tests/gnu/daystart.sh @@ -0,0 +1 @@ +TZ=WAT-1 bfs_diff basic -daystart -mtime 0 diff --git a/tests/test_uid.out b/tests/gnu/daystart_twice.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid.out +++ b/tests/gnu/daystart_twice.out diff --git a/tests/gnu/daystart_twice.sh b/tests/gnu/daystart_twice.sh new file mode 100644 index 0000000..edbf18d --- /dev/null +++ b/tests/gnu/daystart_twice.sh @@ -0,0 +1 @@ +TZ=WAT-1 bfs_diff basic -daystart -daystart -mtime 0 diff --git a/tests/gnu/exec_flush.out b/tests/gnu/exec_flush.out new file mode 100644 index 0000000..fdd7b16 --- /dev/null +++ b/tests/gnu/exec_flush.out @@ -0,0 +1,19 @@ +basic found +basic/a found +basic/b found +basic/c found +basic/c/d found +basic/e found +basic/e/f found +basic/g found +basic/g/h found +basic/i found +basic/j found +basic/j/foo found +basic/k found +basic/k/foo found +basic/k/foo/bar found +basic/l found +basic/l/foo found +basic/l/foo/bar found +basic/l/foo/bar/baz found diff --git a/tests/gnu/exec_flush.sh b/tests/gnu/exec_flush.sh new file mode 100644 index 0000000..ff6088e --- /dev/null +++ b/tests/gnu/exec_flush.sh @@ -0,0 +1,4 @@ +# I/O streams should be flushed before executing programs +invoke_bfs basic -print0 -exec echo found \; | tr '\0' ' ' >"$OUT" +sort_output +diff_output diff --git a/tests/gnu/exec_flush_fail.sh b/tests/gnu/exec_flush_fail.sh new file mode 100644 index 0000000..5505f7a --- /dev/null +++ b/tests/gnu/exec_flush_fail.sh @@ -0,0 +1,3 @@ +# Failure to flush streams before exec should be caught +test -e /dev/full || skip +! invoke_bfs basic -print0 -exec true \; >/dev/full diff --git a/tests/gnu/exec_nothing.sh b/tests/gnu/exec_nothing.sh new file mode 100644 index 0000000..443aa0d --- /dev/null +++ b/tests/gnu/exec_nothing.sh @@ -0,0 +1,2 @@ +# Regression test: don't segfault on missing command +! invoke_bfs basic -exec \; diff --git a/tests/test_exec_plus_flush.out b/tests/gnu/exec_plus_flush.out Binary files differindex 3e276be..3e276be 100644 --- a/tests/test_exec_plus_flush.out +++ b/tests/gnu/exec_plus_flush.out diff --git a/tests/gnu/exec_plus_flush.sh b/tests/gnu/exec_plus_flush.sh new file mode 100644 index 0000000..0c03837 --- /dev/null +++ b/tests/gnu/exec_plus_flush.sh @@ -0,0 +1,2 @@ +invoke_bfs basic/a -print0 -exec echo found {} + >"$OUT" +diff_output diff --git a/tests/gnu/exec_plus_flush_fail.sh b/tests/gnu/exec_plus_flush_fail.sh new file mode 100644 index 0000000..53a50e5 --- /dev/null +++ b/tests/gnu/exec_plus_flush_fail.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full diff --git a/tests/gnu/execdir.out b/tests/gnu/execdir.out new file mode 100644 index 0000000..62b31f6 --- /dev/null +++ b/tests/gnu/execdir.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/gnu/execdir.sh b/tests/gnu/execdir.sh new file mode 100644 index 0000000..5a3a95a --- /dev/null +++ b/tests/gnu/execdir.sh @@ -0,0 +1 @@ +bfs_diff basic -execdir echo {} \; diff --git a/tests/gnu/execdir_path_dot.sh b/tests/gnu/execdir_path_dot.sh new file mode 100644 index 0000000..632dbb4 --- /dev/null +++ b/tests/gnu/execdir_path_dot.sh @@ -0,0 +1 @@ +! PATH=".:$PATH" invoke_bfs basic -execdir echo {} + diff --git a/tests/gnu/execdir_path_empty.sh b/tests/gnu/execdir_path_empty.sh new file mode 100644 index 0000000..eda6b1c --- /dev/null +++ b/tests/gnu/execdir_path_empty.sh @@ -0,0 +1 @@ +! PATH=":$PATH" invoke_bfs basic -execdir echo {} + diff --git a/tests/gnu/execdir_path_relative.sh b/tests/gnu/execdir_path_relative.sh new file mode 100644 index 0000000..69899ad --- /dev/null +++ b/tests/gnu/execdir_path_relative.sh @@ -0,0 +1 @@ +! PATH="foo:$PATH" invoke_bfs basic -execdir echo {} + diff --git a/tests/test_execdir_plus_semicolon.out b/tests/gnu/execdir_plus_semicolon.out index e39f452..e39f452 100644 --- a/tests/test_execdir_plus_semicolon.out +++ b/tests/gnu/execdir_plus_semicolon.out diff --git a/tests/gnu/execdir_plus_semicolon.sh b/tests/gnu/execdir_plus_semicolon.sh new file mode 100644 index 0000000..c5cdafe --- /dev/null +++ b/tests/gnu/execdir_plus_semicolon.sh @@ -0,0 +1 @@ +bfs_diff basic -execdir echo foo {} bar + baz \; 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/test_execdir_substring.out b/tests/gnu/execdir_substring.out index f7a9ac0..f7a9ac0 100644 --- a/tests/test_execdir_substring.out +++ b/tests/gnu/execdir_substring.out diff --git a/tests/gnu/execdir_substring.sh b/tests/gnu/execdir_substring.sh new file mode 100644 index 0000000..feeabc4 --- /dev/null +++ b/tests/gnu/execdir_substring.sh @@ -0,0 +1 @@ +bfs_diff basic -execdir echo '-{}-' \; diff --git a/tests/gnu/execdir_ulimit.out b/tests/gnu/execdir_ulimit.out new file mode 100644 index 0000000..6749f7d --- /dev/null +++ b/tests/gnu/execdir_ulimit.out @@ -0,0 +1,16 @@ +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE diff --git a/tests/gnu/execdir_ulimit.sh b/tests/gnu/execdir_ulimit.sh new file mode 100644 index 0000000..e14e716 --- /dev/null +++ b/tests/gnu/execdir_ulimit.sh @@ -0,0 +1,2 @@ +ulimit -Sn 64 +bfs_diff deep -type f -execdir bash -c 'printf "%d %s\n" $(ulimit -Sn) "$1"' bash {} \; diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out new file mode 100644 index 0000000..e256554 --- /dev/null +++ b/tests/gnu/executable.out @@ -0,0 +1,19 @@ +perms +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/executable.sh b/tests/gnu/executable.sh new file mode 100644 index 0000000..f7f6633 --- /dev/null +++ b/tests/gnu/executable.sh @@ -0,0 +1 @@ +bfs_diff perms -executable diff --git a/tests/test_ok_closed_stdin.out b/tests/gnu/false.out index e69de29..e69de29 100644 --- a/tests/test_ok_closed_stdin.out +++ b/tests/gnu/false.out diff --git a/tests/gnu/false.sh b/tests/gnu/false.sh new file mode 100644 index 0000000..89d86c2 --- /dev/null +++ b/tests/gnu/false.sh @@ -0,0 +1 @@ +bfs_diff basic -false diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh new file mode 100644 index 0000000..7b42772 --- /dev/null +++ b/tests/gnu/files0_from_empty.sh @@ -0,0 +1 @@ +! printf '\0' | invoke_bfs -files0-from - diff --git a/tests/gnu/files0_from_error.sh b/tests/gnu/files0_from_error.sh new file mode 100644 index 0000000..1515d0b --- /dev/null +++ b/tests/gnu/files0_from_error.sh @@ -0,0 +1 @@ +! invoke_bfs -files0-from basic diff --git a/tests/test_files0_from_file.out b/tests/gnu/files0_from_file.out index 1d87e6b..0f6b00d 100644 --- a/tests/test_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.sh b/tests/gnu/files0_from_file.sh new file mode 100644 index 0000000..81435a0 --- /dev/null +++ b/tests/gnu/files0_from_file.sh @@ -0,0 +1,4 @@ +FILE="$TMP/$TEST.in" +cd weirdnames +invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0" +bfs_diff -files0-from "$FILE" 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/test_okdir_closed_stdin.out b/tests/gnu/files0_from_none.out index e69de29..e69de29 100644 --- a/tests/test_okdir_closed_stdin.out +++ b/tests/gnu/files0_from_none.out diff --git a/tests/gnu/files0_from_none.sh b/tests/gnu/files0_from_none.sh new file mode 100644 index 0000000..1633163 --- /dev/null +++ b/tests/gnu/files0_from_none.sh @@ -0,0 +1 @@ +printf "" | bfs_diff -files0-from - diff --git a/tests/gnu/files0_from_nothing.sh b/tests/gnu/files0_from_nothing.sh new file mode 100644 index 0000000..fee50a8 --- /dev/null +++ b/tests/gnu/files0_from_nothing.sh @@ -0,0 +1 @@ +! invoke_bfs -files0-from basic/nonexistent diff --git a/tests/gnu/files0_from_nowhere.sh b/tests/gnu/files0_from_nowhere.sh new file mode 100644 index 0000000..68eea4b --- /dev/null +++ b/tests/gnu/files0_from_nowhere.sh @@ -0,0 +1 @@ +! invoke_bfs -files0-from diff --git a/tests/test_files0_from_stdin.out b/tests/gnu/files0_from_stdin.out index 1d87e6b..0f6b00d 100644 --- a/tests/test_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.sh b/tests/gnu/files0_from_stdin.sh new file mode 100644 index 0000000..9df7736 --- /dev/null +++ b/tests/gnu/files0_from_stdin.sh @@ -0,0 +1,2 @@ +cd weirdnames +invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - 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.sh b/tests/gnu/fls.sh new file mode 100644 index 0000000..d2ff794 --- /dev/null +++ b/tests/gnu/fls.sh @@ -0,0 +1 @@ +invoke_bfs rainbow -fls "$OUT" diff --git a/tests/gnu/fls_nonexistent.sh b/tests/gnu/fls_nonexistent.sh new file mode 100644 index 0000000..2854569 --- /dev/null +++ b/tests/gnu/fls_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs rainbow -fls nonexistent/path 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/test_follow_comma.out b/tests/gnu/follow_comma.out index 920b3d3..5e4b806 100644 --- a/tests/test_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_comma.sh b/tests/gnu/follow_comma.sh new file mode 100644 index 0000000..f57b932 --- /dev/null +++ b/tests/gnu/follow_comma.sh @@ -0,0 +1,3 @@ +# , is an operator after a non-flag is seen +cd weirdnames +bfs_diff -follow ',' -print 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/test_uid_minus.out b/tests/gnu/fprint.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid_minus.out +++ b/tests/gnu/fprint.out diff --git a/tests/gnu/fprint.sh b/tests/gnu/fprint.sh new file mode 100644 index 0000000..04b50fa --- /dev/null +++ b/tests/gnu/fprint.sh @@ -0,0 +1,3 @@ +invoke_bfs basic -fprint "$OUT" +sort_output +diff_output diff --git a/tests/test_fprint0.out b/tests/gnu/fprint0.out Binary files differindex 1347444..1347444 100644 --- a/tests/test_fprint0.out +++ b/tests/gnu/fprint0.out diff --git a/tests/gnu/fprint0.sh b/tests/gnu/fprint0.sh new file mode 100644 index 0000000..dd10b5f --- /dev/null +++ b/tests/gnu/fprint0.sh @@ -0,0 +1,2 @@ +invoke_bfs basic/a basic/b -fprint0 "$OUT" +diff_output diff --git a/tests/gnu/fprint0_nonexistent.sh b/tests/gnu/fprint0_nonexistent.sh new file mode 100644 index 0000000..4906081 --- /dev/null +++ b/tests/gnu/fprint0_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprint0 nonexistent/path diff --git a/tests/test_fprint_duplicate.out b/tests/gnu/fprint_duplicate.out index 2575f35..2575f35 100644 --- a/tests/test_fprint_duplicate.out +++ b/tests/gnu/fprint_duplicate.out diff --git a/tests/gnu/fprint_duplicate.sh b/tests/gnu/fprint_duplicate.sh new file mode 100644 index 0000000..8533b05 --- /dev/null +++ b/tests/gnu/fprint_duplicate.sh @@ -0,0 +1,7 @@ +"$XTOUCH" -p "$TEST/foo.out" +ln "$TEST/foo.out" "$TEST/foo.hard" +ln -s foo.out "$TEST/foo.soft" + +invoke_bfs basic -fprint "$TEST/foo.out" -fprint "$TEST/foo.hard" -fprint "$TEST/foo.soft" +sort "$TEST/foo.out" >"$OUT" +diff_output diff --git a/tests/gnu/fprint_error.sh b/tests/gnu/fprint_error.sh new file mode 100644 index 0000000..7617034 --- /dev/null +++ b/tests/gnu/fprint_error.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic -maxdepth 0 -fprint /dev/full diff --git a/tests/gnu/fprint_noarg.sh b/tests/gnu/fprint_noarg.sh new file mode 100644 index 0000000..8511649 --- /dev/null +++ b/tests/gnu/fprint_noarg.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprint diff --git a/tests/gnu/fprint_nonexistent.sh b/tests/gnu/fprint_nonexistent.sh new file mode 100644 index 0000000..2a403a2 --- /dev/null +++ b/tests/gnu/fprint_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprint nonexistent/path diff --git a/tests/test_printf_leak.out b/tests/gnu/fprint_truncate.out index 15a13db..15a13db 100644 --- a/tests/test_printf_leak.out +++ b/tests/gnu/fprint_truncate.out diff --git a/tests/gnu/fprint_truncate.sh b/tests/gnu/fprint_truncate.sh new file mode 100644 index 0000000..db58a7a --- /dev/null +++ b/tests/gnu/fprint_truncate.sh @@ -0,0 +1,5 @@ +printf "basic\nbasic\n" >"$OUT" + +invoke_bfs basic -maxdepth 0 -fprint "$OUT" +sort_output +diff_output diff --git a/tests/gnu/fprint_unreached_error.sh b/tests/gnu/fprint_unreached_error.sh new file mode 100644 index 0000000..f13a62b --- /dev/null +++ b/tests/gnu/fprint_unreached_error.sh @@ -0,0 +1,3 @@ +# Regression test: /dev/full should not fail until actually written to +test -e /dev/full || skip +invoke_bfs basic -false -fprint /dev/full diff --git a/tests/test_fprintf.out b/tests/gnu/fprintf.out index 77ce17a..77ce17a 100644 --- a/tests/test_fprintf.out +++ b/tests/gnu/fprintf.out diff --git a/tests/gnu/fprintf.sh b/tests/gnu/fprintf.sh new file mode 100644 index 0000000..9c6355a --- /dev/null +++ b/tests/gnu/fprintf.sh @@ -0,0 +1,3 @@ +invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n' +sort_output +diff_output diff --git a/tests/gnu/fprintf_nofile.sh b/tests/gnu/fprintf_nofile.sh new file mode 100644 index 0000000..4e79002 --- /dev/null +++ b/tests/gnu/fprintf_nofile.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprintf diff --git a/tests/gnu/fprintf_noformat.sh b/tests/gnu/fprintf_noformat.sh new file mode 100644 index 0000000..fd97f4c --- /dev/null +++ b/tests/gnu/fprintf_noformat.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprintf /dev/null diff --git a/tests/gnu/fprintf_nonexistent.sh b/tests/gnu/fprintf_nonexistent.sh new file mode 100644 index 0000000..b1eea10 --- /dev/null +++ b/tests/gnu/fprintf_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs basic -fprintf nonexistent/path '%p\n' diff --git a/tests/test_uid_minus_plus.out b/tests/gnu/fstype.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid_minus_plus.out +++ b/tests/gnu/fstype.out diff --git a/tests/gnu/fstype.sh b/tests/gnu/fstype.sh new file mode 100644 index 0000000..05645c3 --- /dev/null +++ b/tests/gnu/fstype.sh @@ -0,0 +1,2 @@ +fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') || skip +bfs_diff basic -fstype "$fstype" 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/fstype_stacked.out b/tests/gnu/fstype_stacked.out new file mode 100644 index 0000000..c1e0e6c --- /dev/null +++ b/tests/gnu/fstype_stacked.out @@ -0,0 +1 @@ +mnt diff --git a/tests/gnu/fstype_stacked.sh b/tests/gnu/fstype_stacked.sh new file mode 100644 index 0000000..a9739bb --- /dev/null +++ b/tests/gnu/fstype_stacked.sh @@ -0,0 +1,12 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +mkdir mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +bfs_sudo mount -t ramfs ramfs mnt || skip +defer bfs_sudo umount mnt + +bfs_diff mnt -fstype ramfs -print -o -printf '%p: %F\n' diff --git a/tests/test_or_purity.out b/tests/gnu/fstype_umount.out index e69de29..e69de29 100644 --- a/tests/test_or_purity.out +++ b/tests/gnu/fstype_umount.out diff --git a/tests/gnu/fstype_umount.sh b/tests/gnu/fstype_umount.sh new file mode 100644 index 0000000..81c195f --- /dev/null +++ b/tests/gnu/fstype_umount.sh @@ -0,0 +1,12 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" + +mkdir tmp +bfs_sudo mount -t tmpfs tmpfs tmp || skip +defer bfs_sudo umount -R tmp + +mkdir tmp/ram +bfs_sudo mount -t ramfs ramfs tmp/ram || skip + +bfs_diff tmp -path tmp -exec "${SUDO[@]}" umount tmp/ram \; , -fstype ramfs -print diff --git a/tests/gnu/ignore_readdir_race.sh b/tests/gnu/ignore_readdir_race.sh new file mode 100644 index 0000000..75165f6 --- /dev/null +++ b/tests/gnu/ignore_readdir_race.sh @@ -0,0 +1,5 @@ +cd "$TEST" +"$XTOUCH" foo bar + +# -links 1 forces a stat() call, which will fail for the second file +invoke_bfs . -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \; 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_notdir.sh b/tests/gnu/ignore_readdir_race_notdir.sh new file mode 100644 index 0000000..12e9fe6 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_notdir.sh @@ -0,0 +1,7 @@ +# Check -ignore_readdir_race handling when a directory is replaced with a file +cd "$TEST" +mkdir foo + +invoke_bfs . -mindepth 1 -ignore_readdir_race \ + -type d -execdir rmdir {} \; \ + -execdir "$XTOUCH" {} \; 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/gnu/ignore_readdir_race_root.sh b/tests/gnu/ignore_readdir_race_root.sh new file mode 100644 index 0000000..dc41e7f --- /dev/null +++ b/tests/gnu/ignore_readdir_race_root.sh @@ -0,0 +1,2 @@ +# Make sure -ignore_readdir_race doesn't suppress ENOENT at the root +! invoke_bfs basic/nonexistent -ignore_readdir_race diff --git a/tests/gnu/inum_automount.out b/tests/gnu/inum_automount.out new file mode 100644 index 0000000..3378e2d --- /dev/null +++ b/tests/gnu/inum_automount.out @@ -0,0 +1 @@ +./automnt diff --git a/tests/gnu/inum_automount.sh b/tests/gnu/inum_automount.sh new file mode 100644 index 0000000..86b23e1 --- /dev/null +++ b/tests/gnu/inum_automount.sh @@ -0,0 +1,14 @@ +# bfs shouldn't trigger automounts unless it descends into them + +command -v systemd-mount &>/dev/null || skip + +cd "$TEST" +mkdir foo automnt + +bfs_sudo systemd-mount -A -o bind "$TMP/basic" automnt || skip +defer bfs_sudo systemd-umount automnt + +before=$(inum automnt) +bfs_diff . -inum "$before" -prune +after=$(inum automnt) +((before == after)) diff --git a/tests/test_iwholename.out b/tests/gnu/iwholename.out index ae1ae21..ae1ae21 100644 --- a/tests/test_iwholename.out +++ b/tests/gnu/iwholename.out diff --git a/tests/gnu/iwholename.sh b/tests/gnu/iwholename.sh new file mode 100644 index 0000000..0b2d038 --- /dev/null +++ b/tests/gnu/iwholename.sh @@ -0,0 +1,2 @@ +invoke_bfs -quit -iwholename PATTERN || skip +bfs_diff basic -iwholename 'basic/*F*' diff --git a/tests/test_newer_link.out b/tests/gnu/newer_link.out index d2dcdd1..d2dcdd1 100644 --- a/tests/test_newer_link.out +++ b/tests/gnu/newer_link.out diff --git a/tests/gnu/newer_link.sh b/tests/gnu/newer_link.sh new file mode 100644 index 0000000..685ac78 --- /dev/null +++ b/tests/gnu/newer_link.sh @@ -0,0 +1 @@ +bfs_diff times -newer times/l diff --git a/tests/test_uid_name.out b/tests/gnu/noleaf.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid_name.out +++ b/tests/gnu/noleaf.out diff --git a/tests/gnu/noleaf.sh b/tests/gnu/noleaf.sh new file mode 100644 index 0000000..b19438c --- /dev/null +++ b/tests/gnu/noleaf.sh @@ -0,0 +1 @@ +bfs_diff basic -noleaf diff --git a/tests/test_bang.out b/tests/gnu/not.out index b286454..b286454 100644 --- a/tests/test_bang.out +++ b/tests/gnu/not.out diff --git a/tests/gnu/not.sh b/tests/gnu/not.sh new file mode 100644 index 0000000..9fa9edc --- /dev/null +++ b/tests/gnu/not.sh @@ -0,0 +1 @@ +bfs_diff basic -not -name foo diff --git a/tests/test_fprint_duplicate_stdout.out b/tests/gnu/not_comma.out index 6c21751..b90468e 100644 --- a/tests/test_fprint_duplicate_stdout.out +++ b/tests/gnu/not_comma.out @@ -11,7 +11,6 @@ basic/c/d basic/e basic/e basic/e/f -basic/e/f basic/g basic/g basic/g/h @@ -21,17 +20,14 @@ basic/i basic/j basic/j basic/j/foo -basic/j/foo basic/k basic/k basic/k/foo -basic/k/foo basic/k/foo/bar basic/k/foo/bar basic/l basic/l basic/l/foo -basic/l/foo basic/l/foo/bar basic/l/foo/bar 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/test_quit_after_print.out b/tests/gnu/not_reachability.out index 15a13db..15a13db 100644 --- a/tests/test_quit_after_print.out +++ b/tests/gnu/not_reachability.out diff --git a/tests/gnu/not_reachability.sh b/tests/gnu/not_reachability.sh new file mode 100644 index 0000000..7fd3c74 --- /dev/null +++ b/tests/gnu/not_reachability.sh @@ -0,0 +1 @@ +bfs_diff basic -print \! -quit -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.out b/tests/gnu/ok_flush.out new file mode 100644 index 0000000..6731408 --- /dev/null +++ b/tests/gnu/ok_flush.out @@ -0,0 +1,19 @@ +basic ? found +basic/a ? found +basic/b ? found +basic/c ? found +basic/c/d ? found +basic/e ? found +basic/e/f ? found +basic/g ? found +basic/g/h ? found +basic/i ? found +basic/j ? found +basic/j/foo ? found +basic/k ? found +basic/k/foo ? found +basic/k/foo/bar ? found +basic/l ? found +basic/l/foo ? found +basic/l/foo/bar ? found +basic/l/foo/bar/baz ? found diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh new file mode 100644 index 0000000..a5dc0d0 --- /dev/null +++ b/tests/gnu/ok_flush.sh @@ -0,0 +1,4 @@ +# I/O streams should be flushed before -ok prompts +yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT" +sort_output +diff_output diff --git a/tests/gnu/ok_nothing.sh b/tests/gnu/ok_nothing.sh new file mode 100644 index 0000000..52c3547 --- /dev/null +++ b/tests/gnu/ok_nothing.sh @@ -0,0 +1,2 @@ +# Regression test: don't segfault on missing command +! invoke_bfs basic -ok \; diff --git a/tests/gnu/okdir_path_dot.sh b/tests/gnu/okdir_path_dot.sh new file mode 100644 index 0000000..5b40e27 --- /dev/null +++ b/tests/gnu/okdir_path_dot.sh @@ -0,0 +1 @@ +! PATH=".:$PATH" invoke_bfs basic -okdir echo {} \; diff --git a/tests/gnu/okdir_path_empty.sh b/tests/gnu/okdir_path_empty.sh new file mode 100644 index 0000000..2669ee8 --- /dev/null +++ b/tests/gnu/okdir_path_empty.sh @@ -0,0 +1 @@ +! PATH=":$PATH" invoke_bfs basic -okdir echo {} \; diff --git a/tests/gnu/okdir_path_relative.sh b/tests/gnu/okdir_path_relative.sh new file mode 100644 index 0000000..05100a1 --- /dev/null +++ b/tests/gnu/okdir_path_relative.sh @@ -0,0 +1 @@ +! PATH="foo:$PATH" invoke_bfs basic -okdir echo {} \; diff --git a/tests/test_o.out b/tests/gnu/or.out index 1650c4d..1650c4d 100644 --- a/tests/test_o.out +++ b/tests/gnu/or.out diff --git a/tests/gnu/or.sh b/tests/gnu/or.sh new file mode 100644 index 0000000..eb28030 --- /dev/null +++ b/tests/gnu/or.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -or -type d diff --git a/tests/test_uid_plus.out b/tests/gnu/path_d.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid_plus.out +++ b/tests/gnu/path_d.out diff --git a/tests/gnu/path_d.sh b/tests/gnu/path_d.sh new file mode 100644 index 0000000..e546ff3 --- /dev/null +++ b/tests/gnu/path_d.sh @@ -0,0 +1 @@ +bfs_diff basic -d diff --git a/tests/gnu/perm_000_slash.out b/tests/gnu/perm_000_slash.out new file mode 100644 index 0000000..e279684 --- /dev/null +++ b/tests/gnu/perm_000_slash.out @@ -0,0 +1,29 @@ +perms +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_000_slash.sh b/tests/gnu/perm_000_slash.sh new file mode 100644 index 0000000..f4b2665 --- /dev/null +++ b/tests/gnu/perm_000_slash.sh @@ -0,0 +1 @@ +bfs_diff perms -perm /000 diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out new file mode 100644 index 0000000..1b6d885 --- /dev/null +++ b/tests/gnu/perm_222_slash.out @@ -0,0 +1,20 @@ +perms +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_222_slash.sh b/tests/gnu/perm_222_slash.sh new file mode 100644 index 0000000..f4be665 --- /dev/null +++ b/tests/gnu/perm_222_slash.sh @@ -0,0 +1 @@ +bfs_diff perms -perm /222 diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out new file mode 100644 index 0000000..eef88ca --- /dev/null +++ b/tests/gnu/perm_644_slash.out @@ -0,0 +1,26 @@ +perms +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_644_slash.sh b/tests/gnu/perm_644_slash.sh new file mode 100644 index 0000000..e883f17 --- /dev/null +++ b/tests/gnu/perm_644_slash.sh @@ -0,0 +1 @@ +bfs_diff perms -perm /644 diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out new file mode 100644 index 0000000..fcbf49e --- /dev/null +++ b/tests/gnu/perm_leading_plus_symbolic_slash.out @@ -0,0 +1,28 @@ +perms +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_leading_plus_symbolic_slash.sh b/tests/gnu/perm_leading_plus_symbolic_slash.sh new file mode 100644 index 0000000..3db27bd --- /dev/null +++ b/tests/gnu/perm_leading_plus_symbolic_slash.sh @@ -0,0 +1 @@ +bfs_diff perms -perm /+rwx diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out new file mode 100644 index 0000000..5a21321 --- /dev/null +++ b/tests/gnu/perm_symbolic_slash.out @@ -0,0 +1,24 @@ +perms +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/perm_symbolic_slash.sh b/tests/gnu/perm_symbolic_slash.sh new file mode 100644 index 0000000..253b14e --- /dev/null +++ b/tests/gnu/perm_symbolic_slash.sh @@ -0,0 +1 @@ +bfs_diff perms -perm /a+r,u=wX,g+wX-w diff --git a/tests/test_precedence.out b/tests/gnu/precedence.out index 7f589f2..7f589f2 100644 --- a/tests/test_precedence.out +++ b/tests/gnu/precedence.out diff --git a/tests/gnu/precedence.sh b/tests/gnu/precedence.sh new file mode 100644 index 0000000..b35d160 --- /dev/null +++ b/tests/gnu/precedence.sh @@ -0,0 +1 @@ +bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print diff --git a/tests/gnu/print_error.sh b/tests/gnu/print_error.sh new file mode 100644 index 0000000..bc79637 --- /dev/null +++ b/tests/gnu/print_error.sh @@ -0,0 +1,2 @@ +test -e /dev/full || skip +! invoke_bfs basic -maxdepth 0 >/dev/full diff --git a/tests/test_printf.out b/tests/gnu/printf.out index 77ce17a..77ce17a 100644 --- a/tests/test_printf.out +++ b/tests/gnu/printf.out diff --git a/tests/gnu/printf.sh b/tests/gnu/printf.sh new file mode 100644 index 0000000..4dd48e8 --- /dev/null +++ b/tests/gnu/printf.sh @@ -0,0 +1 @@ +bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n' diff --git a/tests/test_printf_H.out b/tests/gnu/printf_H.out index 6b605ff..6b605ff 100644 --- a/tests/test_printf_H.out +++ b/tests/gnu/printf_H.out diff --git a/tests/gnu/printf_H.sh b/tests/gnu/printf_H.sh new file mode 100644 index 0000000..ddef7e2 --- /dev/null +++ b/tests/gnu/printf_H.sh @@ -0,0 +1 @@ +bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n' diff --git a/tests/gnu/printf_Y_error.out b/tests/gnu/printf_Y_error.out new file mode 100644 index 0000000..1dd554e --- /dev/null +++ b/tests/gnu/printf_Y_error.out @@ -0,0 +1,3 @@ +(.) () d d +(./bar) (foo/bar) l ? +(./foo) () d d diff --git a/tests/gnu/printf_Y_error.sh b/tests/gnu/printf_Y_error.sh new file mode 100644 index 0000000..d3130ce --- /dev/null +++ b/tests/gnu/printf_Y_error.sh @@ -0,0 +1,8 @@ +cd "$TEST" +mkdir foo +ln -s foo/bar bar + +chmod -x foo +defer chmod +x foo + +! bfs_diff . -printf '(%p) (%l) %y %Y\n' diff --git a/tests/test_perm_leading_plus_symbolic.out b/tests/gnu/printf_empty.out index e69de29..e69de29 100644 --- a/tests/test_perm_leading_plus_symbolic.out +++ b/tests/gnu/printf_empty.out diff --git a/tests/gnu/printf_empty.sh b/tests/gnu/printf_empty.sh new file mode 100644 index 0000000..ed5eb04 --- /dev/null +++ b/tests/gnu/printf_empty.sh @@ -0,0 +1 @@ +bfs_diff basic -printf '' diff --git a/tests/test_printf_escapes.out b/tests/gnu/printf_escapes.out index 20ea120..20ea120 100644 --- a/tests/test_printf_escapes.out +++ b/tests/gnu/printf_escapes.out diff --git a/tests/gnu/printf_escapes.sh b/tests/gnu/printf_escapes.sh new file mode 100644 index 0000000..ece7c0e --- /dev/null +++ b/tests/gnu/printf_escapes.sh @@ -0,0 +1 @@ +bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo' diff --git a/tests/test_printf_flags.out b/tests/gnu/printf_flags.out index c2c1f0a..c2c1f0a 100644 --- a/tests/test_printf_flags.out +++ b/tests/gnu/printf_flags.out diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh new file mode 100644 index 0000000..98e8faa --- /dev/null +++ b/tests/gnu/printf_flags.sh @@ -0,0 +1 @@ +bfs_diff basic -printf '|%-10.10p| %+03d % #4m\n' diff --git a/tests/test_printf_l_nonlink.out b/tests/gnu/printf_l_nonlink.out index 30df155..30df155 100644 --- a/tests/test_printf_l_nonlink.out +++ b/tests/gnu/printf_l_nonlink.out diff --git a/tests/gnu/printf_l_nonlink.sh b/tests/gnu/printf_l_nonlink.sh new file mode 100644 index 0000000..1c66442 --- /dev/null +++ b/tests/gnu/printf_l_nonlink.sh @@ -0,0 +1 @@ +bfs_diff links -printf '| %26p -> %-26l |\n' diff --git a/tests/test_quit_implicit_print.out b/tests/gnu/printf_leak.out index 15a13db..15a13db 100644 --- a/tests/test_quit_implicit_print.out +++ b/tests/gnu/printf_leak.out diff --git a/tests/gnu/printf_leak.sh b/tests/gnu/printf_leak.sh new file mode 100644 index 0000000..c4092c7 --- /dev/null +++ b/tests/gnu/printf_leak.sh @@ -0,0 +1,2 @@ +# Memory leak regression test +bfs_diff basic -maxdepth 0 -printf '%p' diff --git a/tests/gnu/printf_nul.out b/tests/gnu/printf_nul.out Binary files differnew file mode 100644 index 0000000..fdb6c6b --- /dev/null +++ b/tests/gnu/printf_nul.out diff --git a/tests/gnu/printf_nul.sh b/tests/gnu/printf_nul.sh new file mode 100644 index 0000000..0b8b928 --- /dev/null +++ b/tests/gnu/printf_nul.sh @@ -0,0 +1,3 @@ +# NUL byte regression test +invoke_bfs basic/a basic/b -maxdepth 0 -printf '%h\0%f\n' >"$OUT" +diff_output diff --git a/tests/test_printf_slash.out b/tests/gnu/printf_slash.out index 5571971..5571971 100644 --- a/tests/test_printf_slash.out +++ b/tests/gnu/printf_slash.out diff --git a/tests/gnu/printf_slash.sh b/tests/gnu/printf_slash.sh new file mode 100644 index 0000000..b64ff10 --- /dev/null +++ b/tests/gnu/printf_slash.sh @@ -0,0 +1 @@ +bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n' diff --git a/tests/test_printf_slashes.out b/tests/gnu/printf_slashes.out index 5571971..5571971 100644 --- a/tests/test_printf_slashes.out +++ b/tests/gnu/printf_slashes.out diff --git a/tests/gnu/printf_slashes.sh b/tests/gnu/printf_slashes.sh new file mode 100644 index 0000000..d56a287 --- /dev/null +++ b/tests/gnu/printf_slashes.sh @@ -0,0 +1 @@ +bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n' diff --git a/tests/test_printf_times.out b/tests/gnu/printf_times.out index 7e7d256..7e7d256 100644 --- a/tests/test_printf_times.out +++ b/tests/gnu/printf_times.out diff --git a/tests/gnu/printf_times.sh b/tests/gnu/printf_times.sh new file mode 100644 index 0000000..e4f5155 --- /dev/null +++ b/tests/gnu/printf_times.sh @@ -0,0 +1 @@ +bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %A@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n' diff --git a/tests/test_printf_trailing_slash.out b/tests/gnu/printf_trailing_slash.out index 017ac0d..017ac0d 100644 --- a/tests/test_printf_trailing_slash.out +++ b/tests/gnu/printf_trailing_slash.out diff --git a/tests/gnu/printf_trailing_slash.sh b/tests/gnu/printf_trailing_slash.sh new file mode 100644 index 0000000..2df818d --- /dev/null +++ b/tests/gnu/printf_trailing_slash.sh @@ -0,0 +1 @@ +bfs_diff basic/ -printf '(%h)/(%f)\n' diff --git a/tests/test_printf_trailing_slashes.out b/tests/gnu/printf_trailing_slashes.out index fd27101..fd27101 100644 --- a/tests/test_printf_trailing_slashes.out +++ b/tests/gnu/printf_trailing_slashes.out diff --git a/tests/gnu/printf_trailing_slashes.sh b/tests/gnu/printf_trailing_slashes.sh new file mode 100644 index 0000000..6dc532c --- /dev/null +++ b/tests/gnu/printf_trailing_slashes.sh @@ -0,0 +1 @@ +bfs_diff basic/// -printf '(%h)/(%f)\n' diff --git a/tests/test_printf_types.out b/tests/gnu/printf_types.out index 8144c7c..8144c7c 100644 --- a/tests/test_printf_types.out +++ b/tests/gnu/printf_types.out diff --git a/tests/gnu/printf_types.sh b/tests/gnu/printf_types.sh new file mode 100644 index 0000000..6ed1d75 --- /dev/null +++ b/tests/gnu/printf_types.sh @@ -0,0 +1 @@ +bfs_diff loops -printf '(%p) (%l) %y %Y\n' diff --git a/tests/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh new file mode 100644 index 0000000..c621b9b --- /dev/null +++ b/tests/gnu/printf_u_g_ulimit.sh @@ -0,0 +1,2 @@ +ulimit -n $((NOPENFD + 13)) +[ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ] diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out new file mode 100644 index 0000000..56d1f52 --- /dev/null +++ b/tests/gnu/readable.out @@ -0,0 +1,19 @@ +perms +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/readable.sh b/tests/gnu/readable.sh new file mode 100644 index 0000000..a496667 --- /dev/null +++ b/tests/gnu/readable.sh @@ -0,0 +1 @@ +bfs_diff perms -readable diff --git a/tests/gnu/regex_error.sh b/tests/gnu/regex_error.sh new file mode 100644 index 0000000..4af933f --- /dev/null +++ b/tests/gnu/regex_error.sh @@ -0,0 +1 @@ +! invoke_bfs basic -regex '[' diff --git a/tests/gnu/regex_invalid_utf8.out b/tests/gnu/regex_invalid_utf8.out new file mode 100644 index 0000000..a133b1a --- /dev/null +++ b/tests/gnu/regex_invalid_utf8.out @@ -0,0 +1 @@ +./ diff --git a/tests/gnu/regex_invalid_utf8.sh b/tests/gnu/regex_invalid_utf8.sh new file mode 100644 index 0000000..7006dcd --- /dev/null +++ b/tests/gnu/regex_invalid_utf8.sh @@ -0,0 +1,8 @@ +cd "$TEST" + +# Incomplete UTF-8 sequences +touch $'\xC3' || skip +touch $'\xE2\x84' || skip +touch $'\xF0\x9F\x92' || skip + +bfs_diff . -regex '\./..' 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/test_regextype_ed.out b/tests/gnu/regextype_ed.out index 0f0971e..0f0971e 100644 --- a/tests/test_regextype_ed.out +++ b/tests/gnu/regextype_ed.out diff --git a/tests/gnu/regextype_ed.sh b/tests/gnu/regextype_ed.sh new file mode 100644 index 0000000..0e92db3 --- /dev/null +++ b/tests/gnu/regextype_ed.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype ed -regex '\./\((\)' diff --git a/tests/test_perm_leading_plus_symbolic_minus.out b/tests/gnu/regextype_egrep.out index e69de29..e69de29 100644 --- a/tests/test_perm_leading_plus_symbolic_minus.out +++ 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/test_regextype_emacs.out b/tests/gnu/regextype_emacs.out index 95942b4..95942b4 100644 --- a/tests/test_regextype_emacs.out +++ b/tests/gnu/regextype_emacs.out diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh new file mode 100644 index 0000000..164d17a --- /dev/null +++ b/tests/gnu/regextype_emacs.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype emacs -quit || skip + +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/test_iname.out b/tests/gnu/regextype_grep.out index a9e5d42..a9e5d42 100644 --- a/tests/test_iname.out +++ b/tests/gnu/regextype_grep.out diff --git a/tests/gnu/regextype_grep.sh b/tests/gnu/regextype_grep.sh new file mode 100644 index 0000000..0830667 --- /dev/null +++ b/tests/gnu/regextype_grep.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype grep -quit || skip + +bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?' 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/test_regextype_posix_basic.out b/tests/gnu/regextype_posix_basic.out index 0f0971e..0f0971e 100644 --- a/tests/test_regextype_posix_basic.out +++ b/tests/gnu/regextype_posix_basic.out diff --git a/tests/gnu/regextype_posix_basic.sh b/tests/gnu/regextype_posix_basic.sh new file mode 100644 index 0000000..fa2254c --- /dev/null +++ b/tests/gnu/regextype_posix_basic.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype posix-basic -regex '\./\((\)' diff --git a/tests/test_regextype_posix_extended.out b/tests/gnu/regextype_posix_extended.out index 0f0971e..0f0971e 100644 --- a/tests/test_regextype_posix_extended.out +++ b/tests/gnu/regextype_posix_extended.out diff --git a/tests/gnu/regextype_posix_extended.sh b/tests/gnu/regextype_posix_extended.sh new file mode 100644 index 0000000..f82ed65 --- /dev/null +++ b/tests/gnu/regextype_posix_extended.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype posix-extended -regex '\./(\()' diff --git a/tests/test_regextype_sed.out b/tests/gnu/regextype_posix_minimal_basic.out index 0f0971e..0f0971e 100644 --- a/tests/test_regextype_sed.out +++ b/tests/gnu/regextype_posix_minimal_basic.out 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/regextype_sed.out b/tests/gnu/regextype_sed.out new file mode 100644 index 0000000..0f0971e --- /dev/null +++ b/tests/gnu/regextype_sed.out @@ -0,0 +1 @@ +./( diff --git a/tests/gnu/regextype_sed.sh b/tests/gnu/regextype_sed.sh new file mode 100644 index 0000000..9ce6f4e --- /dev/null +++ b/tests/gnu/regextype_sed.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype sed -regex '\./\((\)' diff --git a/tests/test_uid_plus_plus.out b/tests/gnu/true.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_uid_plus_plus.out +++ b/tests/gnu/true.out diff --git a/tests/gnu/true.sh b/tests/gnu/true.sh new file mode 100644 index 0000000..65f3254 --- /dev/null +++ b/tests/gnu/true.sh @@ -0,0 +1 @@ +bfs_diff basic -true diff --git a/tests/gnu/used.out b/tests/gnu/used.out new file mode 100644 index 0000000..647621b --- /dev/null +++ b/tests/gnu/used.out @@ -0,0 +1,4 @@ +-used +7: ./nextyear +-used 1: ./tomorrow +-used 2: ./dayafter +-used 7: ./nextweek diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh new file mode 100644 index 0000000..fe0a778 --- /dev/null +++ b/tests/gnu/used.sh @@ -0,0 +1,21 @@ +cd "$TEST" + +now=$(epoch_time) + +# -used is always false if atime < ctime +"$XTOUCH" -at "@$((now - 60 * 60 * 24))" yesterday + +# -used rounds up +"$XTOUCH" -at "@$((now + 60 * 60))" tomorrow + +"$XTOUCH" -at "@$((now + 60 * 60 * 25))" dayafter + +"$XTOUCH" -at "@$((now + 60 * 60 * (24 * 6 + 1)))" nextweek + +"$XTOUCH" -at "@$((now + 60 * 60 * 24 * 365))" nextyear + +bfs_diff -mindepth 1 \ + -a -used 1 -printf '-used 1: %p\n' \ + -o -used 2 -printf '-used 2: %p\n' \ + -o -used 7 -printf '-used 7: %p\n' \ + -o -used +7 -printf '-used +7: %p\n' diff --git a/tests/test_path.out b/tests/gnu/wholename.out index ae1ae21..ae1ae21 100644 --- a/tests/test_path.out +++ b/tests/gnu/wholename.out diff --git a/tests/gnu/wholename.sh b/tests/gnu/wholename.sh new file mode 100644 index 0000000..4c641b8 --- /dev/null +++ b/tests/gnu/wholename.sh @@ -0,0 +1 @@ +bfs_diff basic -wholename 'basic/*f*' diff --git a/tests/gnu/writable.out b/tests/gnu/writable.out new file mode 100644 index 0000000..1b6d885 --- /dev/null +++ b/tests/gnu/writable.out @@ -0,0 +1,20 @@ +perms +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/writable.sh b/tests/gnu/writable.sh new file mode 100644 index 0000000..93c666f --- /dev/null +++ b/tests/gnu/writable.sh @@ -0,0 +1 @@ +bfs_diff perms -writable diff --git a/tests/gnu/xtype_bind_mount.out b/tests/gnu/xtype_bind_mount.out new file mode 100644 index 0000000..d18d706 --- /dev/null +++ b/tests/gnu/xtype_bind_mount.out @@ -0,0 +1,2 @@ +./link +./null diff --git a/tests/gnu/xtype_bind_mount.sh b/tests/gnu/xtype_bind_mount.sh new file mode 100644 index 0000000..35fb3f5 --- /dev/null +++ b/tests/gnu/xtype_bind_mount.sh @@ -0,0 +1,10 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +"$XTOUCH" file null +ln -s /dev/null link + +bfs_sudo mount --bind /dev/null null || skip +defer bfs_sudo umount null + +bfs_diff . -xtype c diff --git a/tests/test_xtype_f.out b/tests/gnu/xtype_f.out index e6ba322..e6ba322 100644 --- a/tests/test_xtype_f.out +++ b/tests/gnu/xtype_f.out diff --git a/tests/gnu/xtype_f.sh b/tests/gnu/xtype_f.sh new file mode 100644 index 0000000..0ea27bb --- /dev/null +++ b/tests/gnu/xtype_f.sh @@ -0,0 +1 @@ +bfs_diff links -xtype f diff --git a/tests/test_xtype_l.out b/tests/gnu/xtype_l.out index f29c978..f29c978 100644 --- a/tests/test_xtype_l.out +++ b/tests/gnu/xtype_l.out diff --git a/tests/gnu/xtype_l.sh b/tests/gnu/xtype_l.sh new file mode 100644 index 0000000..39c8ea4 --- /dev/null +++ b/tests/gnu/xtype_l.sh @@ -0,0 +1 @@ +bfs_diff links -xtype l 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 new file mode 100644 index 0000000..1a0da97 --- /dev/null +++ b/tests/ioq.c @@ -0,0 +1,76 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "diag.h" +#include "dir.h" +#include "ioq.h" + +#include <fcntl.h> +#include <stdlib.h> + +/** + * Test for blocking within ioq_slot_push(). + * + * struct ioqq only supports non-blocking reads; if a write encounters a full + * slot, it must block until someone pops from that slot: + * + * Reader Writer + * ────────────────────────── ───────────────────────── + * tail: 0 → 1 + * slots[0]: empty → full + * tail: 1 → 0 + * slots[1]: empty → full + * tail: 0 → 1 + * slots[0]: full → full* (IOQ_BLOCKED) + * ioq_slot_wait() ... + * head: 0 → 1 + * slots[0]: full* → empty + * ioq_slot_wake() + * ... + * slots[0]: empty → full + * + * To reproduce this unlikely scenario, we must fill up the ready queue, then + * call ioq_cancel() which pushes an additional sentinel IOQ_STOP operation. + */ +static void check_ioq_push_block(void) { + // Must be a power of two to fill the entire queue + const size_t depth = 2; + + struct ioq *ioq = ioq_create(depth, 1); + 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_everify(dir, "bfs_allocdir()"); + + int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL); + 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 + ioq_cancel(ioq); + + // Drain the queue + for (size_t i = 0; i < depth; ++i) { + struct ioq_ent *ent = ioq_pop(ioq, true); + bfs_verify(ent && ent->op == IOQ_OPENDIR); + + if (ent->result >= 0) { + bfs_closedir(ent->opendir.dir); + } + free(ent->opendir.dir); + ioq_free(ioq, ent); + } + bfs_verify(!ioq_pop(ioq, true)); + + ioq_destroy(ioq); +} + +void check_ioq(void) { + check_ioq_push_block(); +} 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/ls-color.sh b/tests/ls-color.sh index c82a58d..b9a0402 100755 --- a/tests/ls-color.sh +++ b/tests/ls-color.sh @@ -1,36 +1,50 @@ #!/usr/bin/env bash -############################################################################ -# bfs # -# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> # -# # -# Permission to use, copy, modify, and/or distribute this software for any # -# purpose with or without fee is hereby granted. # -# # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -############################################################################ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD # Prints the "ground truth" coloring of a path using ls set -e +parse_ls_colors() { + for key; do + local -n var="$key" + if [[ "$LS_COLORS" =~ (^|:)$key=(([^:]|\\:)*) ]]; then + var="${BASH_REMATCH[2]}" + # Interpret escapes + var=$(printf "$var" | sed $'s/\^\[/\033/g; s/\\\\:/:/g') + fi + done +} + +re_escape() { + # https://stackoverflow.com/a/29613573/502399 + sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1" +} + +rs=0 +lc=$'\033[' +rc=m +ec= +no= + +parse_ls_colors rs lc rc ec no +: "${ec:=$lc$rs$rc}" + +strip="(($(re_escape "$lc$no$rc"))?($(re_escape "$ec")|$(re_escape "$lc$rc")))+" + +ls_color() { + # Strip the leading reset sequence from the ls output + ls -1d --color "$@" | sed -E "s/^$strip([a-z].*)$strip/\4/; s/^$strip//" +} + L= if [ "$1" = "-L" ]; then L="$1" shift fi -function ls_color() { - # Strip the leading reset sequence from the ls output - ls -1d --color "$@" | sed $'s/^\033\\[0m//' -} - DIR="${1%/*}" if [ "$DIR" = "$1" ]; then ls_color "$1" diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..9240e1c --- /dev/null +++ b/tests/main.c @@ -0,0 +1,271 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Entry point for unit tests. + */ + +#include "tests.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; +}; + +/** + * Global test context. + */ +struct test_ctx { + /** Number of command line arguments. */ + int argc; + /** 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. */ + CFILE *cout; + + /** Eventual exit status. */ + int ret; +}; + +/** Initialize the test context. */ +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) { + ctx->ret = EXIT_FAILURE; + return -1; + } + + ctx->ret = EXIT_SUCCESS; + return 0; +} + +/** 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 == 0) { + return true; + } + + // With args, run only specified tests + for (int i = 0; i < ctx->argc; ++i) { + if (strcmp(test, ctx->argv[i]) == 0) { + return true; + } + } + + return false; +} + +/** 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} (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); +} + +/** 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[]) { + // Try to set a UTF-8 locale + if (!setlocale(LC_ALL, "C.UTF-8")) { + setlocale(LC_ALL, ""); + } + + // Run tests in UTC + if (setenv("TZ", "UTC0", true) != 0) { + perror("setenv()"); + return EXIT_FAILURE; + } + 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, jobs, argc - optind, argv + optind) != 0) { + goto done; + } + + run_test(&ctx, "alloc", check_alloc); + 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); + +done: + return test_fini(&ctx); +} diff --git a/tests/mksock.c b/tests/mksock.c index d1776b3..f46df96 100644 --- a/tests/mksock.c +++ b/tests/mksock.c @@ -1,29 +1,17 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD /** * There's no standard Unix utility that creates a socket file, so this small * program does the job. */ +#include "bfstd.h" + #include <errno.h> -#include <libgen.h> #include <stdio.h> -#include <string.h> #include <stdlib.h> +#include <string.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> @@ -32,7 +20,7 @@ * Print an error message. */ static void errmsg(const char *cmd, const char *path) { - fprintf(stderr, "%s: '%s': %s.\n", cmd, path, strerror(errno)); + fprintf(stderr, "%s: '%s': %s.\n", cmd, path, xstrerror(errno)); } /** @@ -41,18 +29,13 @@ static void errmsg(const char *cmd, const char *path) { * file name is not. */ static int chdir_parent(const char *path) { - char *copy = strdup(path); - if (!copy) { + char *dir = xdirname(path); + if (!dir) { return -1; } - const char *dir = dirname(copy); int ret = chdir(dir); - - int error = errno; - free(copy); - errno = error; - + free(dir); return ret; } @@ -66,22 +49,21 @@ static int init_sun(struct sockaddr_un *sock, const char *path) { return -1; } - char *copy = strdup(path); - if (!copy) { + char *base = xbasename(path); + if (!base) { return -1; } - const char *base = basename(copy); len = strlen(base); if (len >= sizeof(sock->sun_path)) { - free(copy); + free(base); errno = ENAMETOOLONG; return -1; } sock->sun_family = AF_UNIX; memcpy(sock->sun_path, base, len + 1); - free(copy); + free(base); return 0; } @@ -119,7 +101,7 @@ int main(int argc, char *argv[]) { ret = EXIT_FAILURE; } - if (close(fd) != 0) { + if (xclose(fd) != 0) { errmsg(cmd, path); ret = EXIT_FAILURE; } diff --git a/tests/posix/H.out b/tests/posix/H.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/posix/H.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/posix/H.sh b/tests/posix/H.sh new file mode 100644 index 0000000..5bae1be --- /dev/null +++ b/tests/posix/H.sh @@ -0,0 +1 @@ +bfs_diff -H links/deeply/nested/dir 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/test_L_samefile_broken.out b/tests/posix/H_broken.out index 21d6316..21d6316 100644 --- a/tests/test_L_samefile_broken.out +++ b/tests/posix/H_broken.out diff --git a/tests/posix/H_broken.sh b/tests/posix/H_broken.sh new file mode 100644 index 0000000..9ff761c --- /dev/null +++ b/tests/posix/H_broken.sh @@ -0,0 +1 @@ +bfs_diff -H links/broken diff --git a/tests/test_H_loops.out b/tests/posix/H_loops.out index 1fc8f8f..1fc8f8f 100644 --- a/tests/test_H_loops.out +++ b/tests/posix/H_loops.out diff --git a/tests/posix/H_loops.sh b/tests/posix/H_loops.sh new file mode 100644 index 0000000..90383b8 --- /dev/null +++ b/tests/posix/H_loops.sh @@ -0,0 +1 @@ +bfs_diff -H loops/deeply/nested/loop diff --git a/tests/test_L_samefile_notdir.out b/tests/posix/H_notdir.out index 6e6658d..6e6658d 100644 --- a/tests/test_L_samefile_notdir.out +++ b/tests/posix/H_notdir.out diff --git a/tests/posix/H_notdir.sh b/tests/posix/H_notdir.sh new file mode 100644 index 0000000..68d7be7 --- /dev/null +++ b/tests/posix/H_notdir.sh @@ -0,0 +1 @@ +bfs_diff -H links/notdir diff --git a/tests/test_P_slash.out b/tests/posix/H_slash.out index df7701b..df7701b 100644 --- a/tests/test_P_slash.out +++ b/tests/posix/H_slash.out diff --git a/tests/posix/H_slash.sh b/tests/posix/H_slash.sh new file mode 100644 index 0000000..b44d756 --- /dev/null +++ b/tests/posix/H_slash.sh @@ -0,0 +1 @@ +bfs_diff -H links/deeply/nested/dir/ diff --git a/tests/test_path_flag_expr.out b/tests/posix/H_type_l.out index e67f10b..e67f10b 100644 --- a/tests/test_path_flag_expr.out +++ b/tests/posix/H_type_l.out diff --git a/tests/posix/H_type_l.sh b/tests/posix/H_type_l.sh new file mode 100644 index 0000000..416a53e --- /dev/null +++ b/tests/posix/H_type_l.sh @@ -0,0 +1 @@ +bfs_diff -H links/skip -type l diff --git a/tests/posix/L.out b/tests/posix/L.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/posix/L.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/L.sh b/tests/posix/L.sh new file mode 100644 index 0000000..d8aebe6 --- /dev/null +++ b/tests/posix/L.sh @@ -0,0 +1 @@ +bfs_diff -L 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/test_samefile_broken.out b/tests/posix/L_broken.out index 21d6316..21d6316 100644 --- a/tests/test_samefile_broken.out +++ b/tests/posix/L_broken.out diff --git a/tests/posix/L_broken.sh b/tests/posix/L_broken.sh new file mode 100644 index 0000000..9ff761c --- /dev/null +++ b/tests/posix/L_broken.sh @@ -0,0 +1 @@ +bfs_diff -H links/broken diff --git a/tests/posix/L_depth.out b/tests/posix/L_depth.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/posix/L_depth.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/L_depth.sh b/tests/posix/L_depth.sh new file mode 100644 index 0000000..59d7ee9 --- /dev/null +++ b/tests/posix/L_depth.sh @@ -0,0 +1 @@ +bfs_diff -L links -depth diff --git a/tests/posix/L_loops.sh b/tests/posix/L_loops.sh new file mode 100644 index 0000000..01b7efc --- /dev/null +++ b/tests/posix/L_loops.sh @@ -0,0 +1,4 @@ +# POSIX says it's okay to either stop or keep going on seeing a filesystem +# loop, as long as a diagnostic is printed +invoke_bfs -L loops >/dev/null 2>"$OUT" && fail +test -s "$OUT" 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/posix/L_mount.sh b/tests/posix/L_mount.sh new file mode 100644 index 0000000..fd8042a --- /dev/null +++ b/tests/posix/L_mount.sh @@ -0,0 +1,13 @@ +test "$UNAME" = "Darwin" && skip + +cd "$TEST" +mkdir foo mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +ln -s ../mnt foo/bar +"$XTOUCH" mnt/baz +ln -s ../mnt/baz foo/qux + +bfs_diff -L . -mount diff --git a/tests/test_samefile_notdir.out b/tests/posix/L_notdir.out index 6e6658d..6e6658d 100644 --- a/tests/test_samefile_notdir.out +++ b/tests/posix/L_notdir.out diff --git a/tests/posix/L_notdir.sh b/tests/posix/L_notdir.sh new file mode 100644 index 0000000..68d7be7 --- /dev/null +++ b/tests/posix/L_notdir.sh @@ -0,0 +1 @@ +bfs_diff -H links/notdir diff --git a/tests/test_L_type_l.out b/tests/posix/L_type_l.out index 725d398..725d398 100644 --- a/tests/test_L_type_l.out +++ b/tests/posix/L_type_l.out diff --git a/tests/posix/L_type_l.sh b/tests/posix/L_type_l.sh new file mode 100644 index 0000000..ee9e563 --- /dev/null +++ b/tests/posix/L_type_l.sh @@ -0,0 +1 @@ +bfs_diff -L links/skip -type l diff --git a/tests/posix/L_xdev.out b/tests/posix/L_xdev.out new file mode 100644 index 0000000..788579d --- /dev/null +++ b/tests/posix/L_xdev.out @@ -0,0 +1,5 @@ +. +./foo +./foo/bar +./foo/qux +./mnt diff --git a/tests/posix/L_xdev.sh b/tests/posix/L_xdev.sh new file mode 100644 index 0000000..82d8605 --- /dev/null +++ b/tests/posix/L_xdev.sh @@ -0,0 +1,13 @@ +test "$UNAME" = "Darwin" && skip + +cd "$TEST" +mkdir foo mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +ln -s ../mnt foo/bar +"$XTOUCH" mnt/baz +ln -s ../mnt/baz foo/qux + +bfs_diff -L . -xdev diff --git a/tests/test_and.out b/tests/posix/a.out index 722962c..722962c 100644 --- a/tests/test_and.out +++ b/tests/posix/a.out diff --git a/tests/posix/a.sh b/tests/posix/a.sh new file mode 100644 index 0000000..7d82d88 --- /dev/null +++ b/tests/posix/a.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -a -type d 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/test_not.out b/tests/posix/bang.out index b286454..b286454 100644 --- a/tests/test_not.out +++ b/tests/posix/bang.out diff --git a/tests/posix/bang.sh b/tests/posix/bang.sh new file mode 100644 index 0000000..27840cd --- /dev/null +++ b/tests/posix/bang.sh @@ -0,0 +1 @@ +bfs_diff basic \! -name foo diff --git a/tests/test_unique_depth.out b/tests/posix/basic.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_unique_depth.out +++ b/tests/posix/basic.out diff --git a/tests/posix/basic.sh b/tests/posix/basic.sh new file mode 100644 index 0000000..3d43529 --- /dev/null +++ b/tests/posix/basic.sh @@ -0,0 +1 @@ +bfs_diff basic diff --git a/tests/test_data_flow_and_swap.out b/tests/posix/data_flow_and_swap.out index e604709..e604709 100644 --- a/tests/test_data_flow_and_swap.out +++ b/tests/posix/data_flow_and_swap.out diff --git a/tests/posix/data_flow_and_swap.sh b/tests/posix/data_flow_and_swap.sh new file mode 100644 index 0000000..9a141af --- /dev/null +++ b/tests/posix/data_flow_and_swap.sh @@ -0,0 +1 @@ +bfs_diff basic \! -type f -a -type d diff --git a/tests/test_user_id.out b/tests/posix/data_flow_group.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_user_id.out +++ b/tests/posix/data_flow_group.out diff --git a/tests/posix/data_flow_group.sh b/tests/posix/data_flow_group.sh new file mode 100644 index 0000000..453dc3e --- /dev/null +++ b/tests/posix/data_flow_group.sh @@ -0,0 +1 @@ +bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \) diff --git a/tests/test_data_flow_or_swap.out b/tests/posix/data_flow_or_swap.out index e604709..e604709 100644 --- a/tests/test_data_flow_or_swap.out +++ b/tests/posix/data_flow_or_swap.out diff --git a/tests/posix/data_flow_or_swap.sh b/tests/posix/data_flow_or_swap.sh new file mode 100644 index 0000000..e8f504b --- /dev/null +++ b/tests/posix/data_flow_or_swap.sh @@ -0,0 +1 @@ +bfs_diff basic \! \( -type f -o \! -type d \) diff --git a/tests/test_perm_symbolic.out b/tests/posix/data_flow_type.out index e69de29..e69de29 100644 --- a/tests/test_perm_symbolic.out +++ b/tests/posix/data_flow_type.out diff --git a/tests/posix/data_flow_type.sh b/tests/posix/data_flow_type.sh new file mode 100644 index 0000000..33339df --- /dev/null +++ b/tests/posix/data_flow_type.sh @@ -0,0 +1 @@ +bfs_diff basic \! \( -type f -o \! -type f \) diff --git a/tests/test_user_name.out b/tests/posix/data_flow_user.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_user_name.out +++ b/tests/posix/data_flow_user.out diff --git a/tests/posix/data_flow_user.sh b/tests/posix/data_flow_user.sh new file mode 100644 index 0000000..44b6e1f --- /dev/null +++ b/tests/posix/data_flow_user.sh @@ -0,0 +1 @@ +bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \) diff --git a/tests/test_de_morgan_and.out b/tests/posix/de_morgan_and.out index 7b7afd2..7b7afd2 100644 --- a/tests/test_de_morgan_and.out +++ b/tests/posix/de_morgan_and.out diff --git a/tests/posix/de_morgan_and.sh b/tests/posix/de_morgan_and.sh new file mode 100644 index 0000000..d52975e --- /dev/null +++ b/tests/posix/de_morgan_and.sh @@ -0,0 +1 @@ +bfs_diff basic \( \! -name 'foo' -a \! -type f \) diff --git a/tests/test_de_morgan_not.out b/tests/posix/de_morgan_not.out index 5916da3..5916da3 100644 --- a/tests/test_de_morgan_not.out +++ b/tests/posix/de_morgan_not.out diff --git a/tests/posix/de_morgan_not.sh b/tests/posix/de_morgan_not.sh new file mode 100644 index 0000000..7393ce0 --- /dev/null +++ b/tests/posix/de_morgan_not.sh @@ -0,0 +1 @@ +bfs_diff basic \! \( -name 'foo' -o \! -type f \) diff --git a/tests/test_de_morgan_or.out b/tests/posix/de_morgan_or.out index 2a57066..2a57066 100644 --- a/tests/test_de_morgan_or.out +++ b/tests/posix/de_morgan_or.out diff --git a/tests/posix/de_morgan_or.sh b/tests/posix/de_morgan_or.sh new file mode 100644 index 0000000..378aab2 --- /dev/null +++ b/tests/posix/de_morgan_or.sh @@ -0,0 +1 @@ +bfs_diff basic \( \! -name 'foo' -o \! -type f \) diff --git a/tests/test_deep_strict.out b/tests/posix/deep.out index c385fce..c385fce 100644 --- a/tests/test_deep_strict.out +++ b/tests/posix/deep.out diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh new file mode 100644 index 0000000..36a88c0 --- /dev/null +++ b/tests/posix/deep.sh @@ -0,0 +1,2 @@ +ulimit -n $((NOPENFD + 13)) +bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; diff --git a/tests/test_user_nouser.out b/tests/posix/depth.out index a7ccfe4..a7ccfe4 100644 --- a/tests/test_user_nouser.out +++ b/tests/posix/depth.out diff --git a/tests/posix/depth.sh b/tests/posix/depth.sh new file mode 100644 index 0000000..444eba5 --- /dev/null +++ b/tests/posix/depth.sh @@ -0,0 +1 @@ +bfs_diff basic -depth diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out new file mode 100644 index 0000000..c4f8ce4 --- /dev/null +++ b/tests/posix/depth_error.out @@ -0,0 +1,4 @@ +inaccessible +inaccessible/dir +inaccessible/file +inaccessible/link diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh new file mode 100644 index 0000000..9b29385 --- /dev/null +++ b/tests/posix/depth_error.sh @@ -0,0 +1 @@ +! bfs_diff inaccessible -depth diff --git a/tests/test_depth_slash.out b/tests/posix/depth_slash.out index 77526d5..77526d5 100644 --- a/tests/test_depth_slash.out +++ b/tests/posix/depth_slash.out diff --git a/tests/posix/depth_slash.sh b/tests/posix/depth_slash.sh new file mode 100644 index 0000000..f73e9f1 --- /dev/null +++ b/tests/posix/depth_slash.sh @@ -0,0 +1 @@ +bfs_diff basic/ -depth diff --git a/tests/test_double_negation.out b/tests/posix/double_negation.out index e9d47b1..e9d47b1 100644 --- a/tests/test_double_negation.out +++ b/tests/posix/double_negation.out diff --git a/tests/posix/double_negation.sh b/tests/posix/double_negation.sh new file mode 100644 index 0000000..eefe464 --- /dev/null +++ b/tests/posix/double_negation.sh @@ -0,0 +1 @@ +bfs_diff basic \! \! -name 'foo' diff --git a/tests/posix/exec.out b/tests/posix/exec.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec.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.sh b/tests/posix/exec.sh new file mode 100644 index 0000000..96c897b --- /dev/null +++ b/tests/posix/exec.sh @@ -0,0 +1 @@ +bfs_diff basic -exec echo {} \; diff --git a/tests/posix/exec_nonexistent.out b/tests/posix/exec_nonexistent.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_nonexistent.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_nonexistent.sh b/tests/posix/exec_nonexistent.sh new file mode 100644 index 0000000..a9ff052 --- /dev/null +++ b/tests/posix/exec_nonexistent.sh @@ -0,0 +1,4 @@ +# Failure to execute the command should lead to an error message and +# non-zero exit status. See https://unix.stackexchange.com/q/704522/56202 +bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/posix/exec_nopath.out b/tests/posix/exec_nopath.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_nopath.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_nopath.sh b/tests/posix/exec_nopath.sh new file mode 100644 index 0000000..6e05d2e --- /dev/null +++ b/tests/posix/exec_nopath.sh @@ -0,0 +1,7 @@ +( + unset PATH + invoke_bfs basic -exec echo {} \; >"$OUT" +) + +sort_output +diff_output diff --git a/tests/test_exec_plus.out b/tests/posix/exec_plus.out index f6b423b..f6b423b 100644 --- a/tests/test_exec_plus.out +++ b/tests/posix/exec_plus.out diff --git a/tests/posix/exec_plus.sh b/tests/posix/exec_plus.sh new file mode 100644 index 0000000..56a93f1 --- /dev/null +++ b/tests/posix/exec_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -exec "$TESTS/sort-args.sh" {} + diff --git a/tests/posix/exec_plus_nonexistent.out b/tests/posix/exec_plus_nonexistent.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_plus_nonexistent.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_plus_nonexistent.sh b/tests/posix/exec_plus_nonexistent.sh new file mode 100644 index 0000000..24582a3 --- /dev/null +++ b/tests/posix/exec_plus_nonexistent.sh @@ -0,0 +1,2 @@ +bfs_diff basic -exec "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/posix/exec_plus_nothing.sh b/tests/posix/exec_plus_nothing.sh new file mode 100644 index 0000000..347722d --- /dev/null +++ b/tests/posix/exec_plus_nothing.sh @@ -0,0 +1,2 @@ +# Regression test: don't look OOB for {} + +! invoke_bfs basic -exec + diff --git a/tests/test_exec_plus_semicolon.out b/tests/posix/exec_plus_semicolon.out index f33c48f..f33c48f 100644 --- a/tests/test_exec_plus_semicolon.out +++ b/tests/posix/exec_plus_semicolon.out diff --git a/tests/posix/exec_plus_semicolon.sh b/tests/posix/exec_plus_semicolon.sh new file mode 100644 index 0000000..449a3f9 --- /dev/null +++ b/tests/posix/exec_plus_semicolon.sh @@ -0,0 +1,5 @@ +# POSIX says: +# 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. +bfs_diff basic -exec echo foo {} bar + baz \; diff --git a/tests/posix/exec_plus_status.out b/tests/posix/exec_plus_status.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_plus_status.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_plus_status.sh b/tests/posix/exec_plus_status.sh new file mode 100644 index 0000000..a814c4e --- /dev/null +++ b/tests/posix/exec_plus_status.sh @@ -0,0 +1,3 @@ +# -exec ... {} + should always return true, but if the command fails, bfs +# should exit with a non-zero status +! bfs_diff basic -exec false {} + -print 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/exec_ulimit.out b/tests/posix/exec_ulimit.out new file mode 100644 index 0000000..144169e --- /dev/null +++ b/tests/posix/exec_ulimit.out @@ -0,0 +1,16 @@ +64 deep/0/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/1/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/2/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/3/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/4/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/5/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/6/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/7/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/8/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/9/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/A/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/B/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/C/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/D/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/E/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE +64 deep/F/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE diff --git a/tests/posix/exec_ulimit.sh b/tests/posix/exec_ulimit.sh new file mode 100644 index 0000000..655fbec --- /dev/null +++ b/tests/posix/exec_ulimit.sh @@ -0,0 +1,2 @@ +ulimit -Sn 64 +bfs_diff deep -type f -exec bash -c 'printf "%d %s\n" $(ulimit -Sn) "${1:0:6}/.../${1##*/}"' bash {} \; diff --git a/tests/posix/extra_paren.sh b/tests/posix/extra_paren.sh new file mode 100644 index 0000000..d15022f --- /dev/null +++ b/tests/posix/extra_paren.sh @@ -0,0 +1 @@ +! invoke_bfs basic -print \) diff --git a/tests/test_flag_comma.out b/tests/posix/flag_comma.out index 3574388..3574388 100644 --- a/tests/test_flag_comma.out +++ b/tests/posix/flag_comma.out diff --git a/tests/posix/flag_comma.sh b/tests/posix/flag_comma.sh new file mode 100644 index 0000000..cec87e7 --- /dev/null +++ b/tests/posix/flag_comma.sh @@ -0,0 +1,3 @@ +# , is a filename until a non-flag is seen +cd weirdnames +bfs_diff -L ',' -print diff --git a/tests/test_flag_weird_names.out b/tests/posix/flag_weird_names.out index c395659..c395659 100644 --- a/tests/test_flag_weird_names.out +++ b/tests/posix/flag_weird_names.out diff --git a/tests/posix/flag_weird_names.sh b/tests/posix/flag_weird_names.sh new file mode 100644 index 0000000..f6596e9 --- /dev/null +++ b/tests/posix/flag_weird_names.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \) diff --git a/tests/posix/group_id.out b/tests/posix/group_id.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/group_id.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_id.sh b/tests/posix/group_id.sh new file mode 100644 index 0000000..2ff7bb3 --- /dev/null +++ b/tests/posix/group_id.sh @@ -0,0 +1 @@ +bfs_diff basic -group "$(id -g)" 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_name.out b/tests/posix/group_name.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/group_name.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_name.sh b/tests/posix/group_name.sh new file mode 100644 index 0000000..36799d9 --- /dev/null +++ b/tests/posix/group_name.sh @@ -0,0 +1 @@ +bfs_diff basic -group "$(id -gn)" diff --git a/tests/posix/group_nogroup.out b/tests/posix/group_nogroup.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/group_nogroup.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_nogroup.sh b/tests/posix/group_nogroup.sh new file mode 100644 index 0000000..cbd1ffc --- /dev/null +++ b/tests/posix/group_nogroup.sh @@ -0,0 +1,2 @@ +# Regression test: this was wrongly optimized to -false +bfs_diff basic -group "$(id -g)" \! -nogroup 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/test_implicit_and.out b/tests/posix/implicit_and.out index 722962c..722962c 100644 --- a/tests/test_implicit_and.out +++ b/tests/posix/implicit_and.out diff --git a/tests/posix/implicit_and.sh b/tests/posix/implicit_and.sh new file mode 100644 index 0000000..161ab0b --- /dev/null +++ b/tests/posix/implicit_and.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -type d diff --git a/tests/test_name.out b/tests/posix/iname.out index a9e5d42..a9e5d42 100644 --- a/tests/test_name.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/posix/incomplete.sh b/tests/posix/incomplete.sh new file mode 100644 index 0000000..bca5a13 --- /dev/null +++ b/tests/posix/incomplete.sh @@ -0,0 +1 @@ +! invoke_bfs basic \( diff --git a/tests/test_links_plus.out b/tests/posix/links.out index 996ffc8..996ffc8 100644 --- a/tests/test_links_plus.out +++ b/tests/posix/links.out diff --git a/tests/posix/links.sh b/tests/posix/links.sh new file mode 100644 index 0000000..3d8ad80 --- /dev/null +++ b/tests/posix/links.sh @@ -0,0 +1 @@ +bfs_diff links -type f -links 2 diff --git a/tests/test_links_minus.out b/tests/posix/links_minus.out index eda26f1..eda26f1 100644 --- a/tests/test_links_minus.out +++ b/tests/posix/links_minus.out diff --git a/tests/posix/links_minus.sh b/tests/posix/links_minus.sh new file mode 100644 index 0000000..3ee0803 --- /dev/null +++ b/tests/posix/links_minus.sh @@ -0,0 +1 @@ +bfs_diff links -type f -links -2 diff --git a/tests/test_samefile.out b/tests/posix/links_plus.out index 996ffc8..996ffc8 100644 --- a/tests/test_samefile.out +++ b/tests/posix/links_plus.out diff --git a/tests/posix/links_plus.sh b/tests/posix/links_plus.sh new file mode 100644 index 0000000..375834b --- /dev/null +++ b/tests/posix/links_plus.sh @@ -0,0 +1 @@ +bfs_diff links -type f -links +1 diff --git a/tests/posix/missing_paren.sh b/tests/posix/missing_paren.sh new file mode 100644 index 0000000..d906fbe --- /dev/null +++ b/tests/posix/missing_paren.sh @@ -0,0 +1 @@ +! invoke_bfs basic \( -print diff --git a/tests/posix/mount.out b/tests/posix/mount.out new file mode 100644 index 0000000..b0ad937 --- /dev/null +++ b/tests/posix/mount.out @@ -0,0 +1,3 @@ +. +./foo +./foo/bar diff --git a/tests/posix/mount.sh b/tests/posix/mount.sh new file mode 100644 index 0000000..c9abde5 --- /dev/null +++ b/tests/posix/mount.sh @@ -0,0 +1,11 @@ +test "$UNAME" = "Darwin" && skip + +cd "$TEST" +mkdir foo mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +"$XTOUCH" foo/bar mnt/baz + +bfs_diff . -mount 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/test_name_star_star.out b/tests/posix/name.out index a9e5d42..a9e5d42 100644 --- a/tests/test_name_star_star.out +++ b/tests/posix/name.out diff --git a/tests/posix/name.sh b/tests/posix/name.sh new file mode 100644 index 0000000..a673ad0 --- /dev/null +++ b/tests/posix/name.sh @@ -0,0 +1 @@ +bfs_diff basic -name '*f*' diff --git a/tests/test_printf_empty.out b/tests/posix/name_backslash.out index e69de29..e69de29 100644 --- a/tests/test_printf_empty.out +++ b/tests/posix/name_backslash.out diff --git a/tests/posix/name_backslash.sh b/tests/posix/name_backslash.sh new file mode 100644 index 0000000..ff9b539 --- /dev/null +++ b/tests/posix/name_backslash.sh @@ -0,0 +1,2 @@ +# An unescaped \ doesn't match +bfs_diff weirdnames -name '\' diff --git a/tests/test_name_bracket.out b/tests/posix/name_bracket.out index 5ff3c0c..5ff3c0c 100644 --- a/tests/test_name_bracket.out +++ b/tests/posix/name_bracket.out diff --git a/tests/posix/name_bracket.sh b/tests/posix/name_bracket.sh new file mode 100644 index 0000000..e2f943d --- /dev/null +++ b/tests/posix/name_bracket.sh @@ -0,0 +1,9 @@ +# fnmatch() is broken on some platforms +case "$UNAME" in + Darwin|NetBSD) + skip + ;; +esac + +# An unclosed [ should be matched literally +bfs_diff weirdnames -name '[' diff --git a/tests/test_name_character_class.out b/tests/posix/name_character_class.out index e9d47b1..e9d47b1 100644 --- a/tests/test_name_character_class.out +++ b/tests/posix/name_character_class.out diff --git a/tests/posix/name_character_class.sh b/tests/posix/name_character_class.sh new file mode 100644 index 0000000..ecda190 --- /dev/null +++ b/tests/posix/name_character_class.sh @@ -0,0 +1 @@ +bfs_diff basic -name '[e-g][!a-n][!p-z]' diff --git a/tests/test_name_double_backslash.out b/tests/posix/name_double_backslash.out index 45ceda0..45ceda0 100644 --- a/tests/test_name_double_backslash.out +++ b/tests/posix/name_double_backslash.out diff --git a/tests/posix/name_double_backslash.sh b/tests/posix/name_double_backslash.sh new file mode 100644 index 0000000..009553a --- /dev/null +++ b/tests/posix/name_double_backslash.sh @@ -0,0 +1,2 @@ +# An escaped \\ matches +bfs_diff weirdnames -name '\\' diff --git a/tests/test_name_root.out b/tests/posix/name_root.out index 511198f..511198f 100644 --- a/tests/test_name_root.out +++ b/tests/posix/name_root.out diff --git a/tests/posix/name_root.sh b/tests/posix/name_root.sh new file mode 100644 index 0000000..785861e --- /dev/null +++ b/tests/posix/name_root.sh @@ -0,0 +1 @@ +bfs_diff basic/a -name a diff --git a/tests/test_quit.out b/tests/posix/name_root_depth.out index cf4d5a9..cf4d5a9 100644 --- a/tests/test_quit.out +++ b/tests/posix/name_root_depth.out diff --git a/tests/posix/name_root_depth.sh b/tests/posix/name_root_depth.sh new file mode 100644 index 0000000..dc3b8bb --- /dev/null +++ b/tests/posix/name_root_depth.sh @@ -0,0 +1 @@ +bfs_diff basic/g -depth -name g diff --git a/tests/test_name_slash.out b/tests/posix/name_slash.out index b498fd4..b498fd4 100644 --- a/tests/test_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/test_name_slashes.out b/tests/posix/name_slashes.out index 187b81f..187b81f 100644 --- a/tests/test_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/test_parens.out b/tests/posix/name_star_star.out index a9e5d42..a9e5d42 100644 --- a/tests/test_parens.out +++ b/tests/posix/name_star_star.out diff --git a/tests/posix/name_star_star.sh b/tests/posix/name_star_star.sh new file mode 100644 index 0000000..035f635 --- /dev/null +++ b/tests/posix/name_star_star.sh @@ -0,0 +1 @@ +bfs_diff basic -name '**f**' diff --git a/tests/test_name_trailing_slash.out b/tests/posix/name_trailing_slash.out index daff2f5..daff2f5 100644 --- a/tests/test_name_trailing_slash.out +++ b/tests/posix/name_trailing_slash.out diff --git a/tests/posix/name_trailing_slash.sh b/tests/posix/name_trailing_slash.sh new file mode 100644 index 0000000..ab058d1 --- /dev/null +++ b/tests/posix/name_trailing_slash.sh @@ -0,0 +1 @@ +bfs_diff basic/g/ -name g diff --git a/tests/test_newerma.out b/tests/posix/newer.out index 7f6c0dd..7f6c0dd 100644 --- a/tests/test_newerma.out +++ b/tests/posix/newer.out diff --git a/tests/posix/newer.sh b/tests/posix/newer.sh new file mode 100644 index 0000000..860623a --- /dev/null +++ b/tests/posix/newer.sh @@ -0,0 +1 @@ +bfs_diff times -newer times/a 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/newer_nonexistent.sh b/tests/posix/newer_nonexistent.sh new file mode 100644 index 0000000..5f2da4b --- /dev/null +++ b/tests/posix/newer_nonexistent.sh @@ -0,0 +1 @@ +! invoke_bfs times -newer times/nonexistent diff --git a/tests/test_printf_w.out b/tests/posix/nogroup.out index e69de29..e69de29 100644 --- a/tests/test_printf_w.out +++ b/tests/posix/nogroup.out diff --git a/tests/posix/nogroup.sh b/tests/posix/nogroup.sh new file mode 100644 index 0000000..60ffd68 --- /dev/null +++ b/tests/posix/nogroup.sh @@ -0,0 +1 @@ +bfs_diff basic -nogroup diff --git a/tests/test_quit_before_print.out b/tests/posix/nogroup_ulimit.out index e69de29..e69de29 100644 --- a/tests/test_quit_before_print.out +++ b/tests/posix/nogroup_ulimit.out diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh new file mode 100644 index 0000000..a39dd1f --- /dev/null +++ b/tests/posix/nogroup_ulimit.sh @@ -0,0 +1,2 @@ +ulimit -n $((NOPENFD + 13)) +bfs_diff deep -type f -nogroup diff --git a/tests/test_not_prune.out b/tests/posix/not_prune.out index 59e3c42..59e3c42 100644 --- a/tests/test_not_prune.out +++ b/tests/posix/not_prune.out diff --git a/tests/posix/not_prune.sh b/tests/posix/not_prune.sh new file mode 100644 index 0000000..6d7b092 --- /dev/null +++ b/tests/posix/not_prune.sh @@ -0,0 +1 @@ +bfs_diff basic \! \( -name foo -prune \) diff --git a/tests/test_size_big.out b/tests/posix/nouser.out index e69de29..e69de29 100644 --- a/tests/test_size_big.out +++ b/tests/posix/nouser.out diff --git a/tests/posix/nouser.sh b/tests/posix/nouser.sh new file mode 100644 index 0000000..e7c48c0 --- /dev/null +++ b/tests/posix/nouser.sh @@ -0,0 +1 @@ +bfs_diff basic -nouser diff --git a/tests/test_xtype_reorder.out b/tests/posix/nouser_ulimit.out index e69de29..e69de29 100644 --- a/tests/test_xtype_reorder.out +++ b/tests/posix/nouser_ulimit.out diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh new file mode 100644 index 0000000..a94b8c5 --- /dev/null +++ b/tests/posix/nouser_ulimit.sh @@ -0,0 +1,2 @@ +ulimit -n $((NOPENFD + 13)) +bfs_diff deep -type f -nouser diff --git a/tests/test_or.out b/tests/posix/o.out index 1650c4d..1650c4d 100644 --- a/tests/test_or.out +++ b/tests/posix/o.out diff --git a/tests/posix/o.sh b/tests/posix/o.sh new file mode 100644 index 0000000..6dcd442 --- /dev/null +++ b/tests/posix/o.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -o -type d diff --git a/tests/posix/ok_plus_nothing.sh b/tests/posix/ok_plus_nothing.sh new file mode 100644 index 0000000..77c7644 --- /dev/null +++ b/tests/posix/ok_plus_nothing.sh @@ -0,0 +1,2 @@ +# Regression test: don't look OOB for {} + +! invoke_bfs basic -ok + diff --git a/tests/test_ok_stdin.out b/tests/posix/ok_stdin.out index 7acf711..7acf711 100644 --- a/tests/test_ok_stdin.out +++ b/tests/posix/ok_stdin.out diff --git a/tests/posix/ok_stdin.sh b/tests/posix/ok_stdin.sh new file mode 100644 index 0000000..a190d81 --- /dev/null +++ b/tests/posix/ok_stdin.sh @@ -0,0 +1,3 @@ +# -ok should *not* close stdin +# See https://savannah.gnu.org/bugs/?24561 +yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \; diff --git a/tests/posix/or_purity.out b/tests/posix/or_purity.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/posix/or_purity.out diff --git a/tests/posix/or_purity.sh b/tests/posix/or_purity.sh new file mode 100644 index 0000000..277b18b --- /dev/null +++ b/tests/posix/or_purity.sh @@ -0,0 +1,2 @@ +# Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure +bfs_diff basic -name '*' -o -print diff --git a/tests/posix/overlayfs.out b/tests/posix/overlayfs.out new file mode 100644 index 0000000..b472b56 --- /dev/null +++ b/tests/posix/overlayfs.out @@ -0,0 +1,5 @@ +merged +merged/bar +merged/baz +merged/baz/qux +merged/foo diff --git a/tests/posix/overlayfs.sh b/tests/posix/overlayfs.sh new file mode 100644 index 0000000..21ef22f --- /dev/null +++ b/tests/posix/overlayfs.sh @@ -0,0 +1,11 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux} + +mkdir -p work merged +bfs_sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged || skip +defer bfs_sudo rm -rf work +defer bfs_sudo umount merged + +bfs_diff merged diff --git a/tests/test_regextype_grep.out b/tests/posix/parens.out index a9e5d42..a9e5d42 100644 --- a/tests/test_regextype_grep.out +++ b/tests/posix/parens.out diff --git a/tests/posix/parens.sh b/tests/posix/parens.sh new file mode 100644 index 0000000..abbb20f --- /dev/null +++ b/tests/posix/parens.sh @@ -0,0 +1 @@ +bfs_diff basic \( -name '*f*' \) diff --git a/tests/test_wholename.out b/tests/posix/path.out index ae1ae21..ae1ae21 100644 --- a/tests/test_wholename.out +++ b/tests/posix/path.out diff --git a/tests/posix/path.sh b/tests/posix/path.sh new file mode 100644 index 0000000..04606eb --- /dev/null +++ b/tests/posix/path.sh @@ -0,0 +1 @@ +bfs_diff basic -path 'basic/*f*' diff --git a/tests/posix/perm_000.out b/tests/posix/perm_000.out new file mode 100644 index 0000000..9df7f46 --- /dev/null +++ b/tests/posix/perm_000.out @@ -0,0 +1 @@ +perms/f--------- diff --git a/tests/posix/perm_000.sh b/tests/posix/perm_000.sh new file mode 100644 index 0000000..ee25f23 --- /dev/null +++ b/tests/posix/perm_000.sh @@ -0,0 +1 @@ +bfs_diff perms -perm 000 diff --git a/tests/posix/perm_000_minus.out b/tests/posix/perm_000_minus.out new file mode 100644 index 0000000..e279684 --- /dev/null +++ b/tests/posix/perm_000_minus.out @@ -0,0 +1,29 @@ +perms +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_000_minus.sh b/tests/posix/perm_000_minus.sh new file mode 100644 index 0000000..5027b91 --- /dev/null +++ b/tests/posix/perm_000_minus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm -000 diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out new file mode 100644 index 0000000..bdc5590 --- /dev/null +++ b/tests/posix/perm_222.out @@ -0,0 +1 @@ +perms/f-w--w--w- diff --git a/tests/posix/perm_222.sh b/tests/posix/perm_222.sh new file mode 100644 index 0000000..40f5804 --- /dev/null +++ b/tests/posix/perm_222.sh @@ -0,0 +1 @@ +bfs_diff perms -perm 222 diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out new file mode 100644 index 0000000..342b285 --- /dev/null +++ b/tests/posix/perm_222_minus.out @@ -0,0 +1,5 @@ +perms/drwxrwxrwx +perms/f-w--w--w- +perms/f-wx-wx-wx +perms/frw-rw-rw- +perms/frwxrwxrwx diff --git a/tests/posix/perm_222_minus.sh b/tests/posix/perm_222_minus.sh new file mode 100644 index 0000000..4e7ad5a --- /dev/null +++ b/tests/posix/perm_222_minus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm -222 diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out new file mode 100644 index 0000000..9f77ce6 --- /dev/null +++ b/tests/posix/perm_644.out @@ -0,0 +1 @@ +perms/frw-r--r-- diff --git a/tests/posix/perm_644.sh b/tests/posix/perm_644.sh new file mode 100644 index 0000000..9a4f41d --- /dev/null +++ b/tests/posix/perm_644.sh @@ -0,0 +1 @@ +bfs_diff perms -perm 644 diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out new file mode 100644 index 0000000..84f69f5 --- /dev/null +++ b/tests/posix/perm_644_minus.out @@ -0,0 +1,10 @@ +perms +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_644_minus.sh b/tests/posix/perm_644_minus.sh new file mode 100644 index 0000000..6464f84 --- /dev/null +++ b/tests/posix/perm_644_minus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm -644 diff --git a/tests/posix/perm_leading_plus_symbolic_minus.out b/tests/posix/perm_leading_plus_symbolic_minus.out new file mode 100644 index 0000000..38d0e1c --- /dev/null +++ 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_symbolic_minus.sh b/tests/posix/perm_leading_plus_symbolic_minus.sh new file mode 100644 index 0000000..60389c0 --- /dev/null +++ b/tests/posix/perm_leading_plus_symbolic_minus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm -+rwx 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/test_perm_setid.out b/tests/posix/perm_setid.out index 865a74e..865a74e 100644 --- a/tests/test_perm_setid.out +++ b/tests/posix/perm_setid.out diff --git a/tests/posix/perm_setid.sh b/tests/posix/perm_setid.sh new file mode 100644 index 0000000..3b98647 --- /dev/null +++ b/tests/posix/perm_setid.sh @@ -0,0 +1 @@ +bfs_diff rainbow -perm -u+s -o -perm -g+s diff --git a/tests/test_perm_sticky.out b/tests/posix/perm_sticky.out index c07eb61..c07eb61 100644 --- a/tests/test_perm_sticky.out +++ b/tests/posix/perm_sticky.out diff --git a/tests/posix/perm_sticky.sh b/tests/posix/perm_sticky.sh new file mode 100644 index 0000000..6bdf8e9 --- /dev/null +++ b/tests/posix/perm_sticky.sh @@ -0,0 +1 @@ +bfs_diff rainbow -perm -a+t diff --git a/tests/posix/perm_symbolic.out b/tests/posix/perm_symbolic.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/posix/perm_symbolic.out diff --git a/tests/posix/perm_symbolic.sh b/tests/posix/perm_symbolic.sh new file mode 100644 index 0000000..5cfddb6 --- /dev/null +++ b/tests/posix/perm_symbolic.sh @@ -0,0 +1 @@ +bfs_diff perms -perm a+r,u=wX,g+wX-w diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out new file mode 100644 index 0000000..84f69f5 --- /dev/null +++ b/tests/posix/perm_symbolic_minus.out @@ -0,0 +1,10 @@ +perms +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_symbolic_minus.sh b/tests/posix/perm_symbolic_minus.sh new file mode 100644 index 0000000..b6ba3a5 --- /dev/null +++ b/tests/posix/perm_symbolic_minus.sh @@ -0,0 +1 @@ +bfs_diff perms -perm -a+r,u=wX,g+wX-w diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out new file mode 100644 index 0000000..9f77ce6 --- /dev/null +++ b/tests/posix/permcopy.out @@ -0,0 +1 @@ +perms/frw-r--r-- diff --git a/tests/posix/permcopy.sh b/tests/posix/permcopy.sh new file mode 100644 index 0000000..3c85cce --- /dev/null +++ b/tests/posix/permcopy.sh @@ -0,0 +1 @@ +bfs_diff perms -perm u+rw,g+u-w,o=g diff --git a/tests/posix/print0.out b/tests/posix/print0.out Binary files differnew file mode 100644 index 0000000..1347444 --- /dev/null +++ b/tests/posix/print0.out diff --git a/tests/posix/print0.sh b/tests/posix/print0.sh new file mode 100644 index 0000000..b916172 --- /dev/null +++ b/tests/posix/print0.sh @@ -0,0 +1,2 @@ +invoke_bfs basic/a basic/b -print0 >"$OUT" +diff_output diff --git a/tests/test_prune.out b/tests/posix/prune.out index e9d47b1..e9d47b1 100644 --- a/tests/test_prune.out +++ b/tests/posix/prune.out diff --git a/tests/posix/prune.sh b/tests/posix/prune.sh new file mode 100644 index 0000000..b48ab48 --- /dev/null +++ b/tests/posix/prune.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -prune 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/test_prune_file.out b/tests/posix/prune_file.out index 7575ae4..7575ae4 100644 --- a/tests/test_prune_file.out +++ b/tests/posix/prune_file.out diff --git a/tests/posix/prune_file.sh b/tests/posix/prune_file.sh new file mode 100644 index 0000000..29a3a33 --- /dev/null +++ b/tests/posix/prune_file.sh @@ -0,0 +1 @@ +bfs_diff basic -print -name '?' -prune diff --git a/tests/test_prune_or_print.out b/tests/posix/prune_or_print.out index 59e3c42..59e3c42 100644 --- a/tests/test_prune_or_print.out +++ b/tests/posix/prune_or_print.out diff --git a/tests/posix/prune_or_print.sh b/tests/posix/prune_or_print.sh new file mode 100644 index 0000000..85b97fd --- /dev/null +++ b/tests/posix/prune_or_print.sh @@ -0,0 +1 @@ +bfs_diff basic -name foo -prune -o -print diff --git a/tests/posix/readdir_error.sh b/tests/posix/readdir_error.sh new file mode 100644 index 0000000..82fcd17 --- /dev/null +++ b/tests/posix/readdir_error.sh @@ -0,0 +1,37 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +mkfifo hang pid wait running + +( + # Create a zombie process + cat hang >/dev/null & + # Write the PID to pid + echo $! >pid + # Don't wait on the zombie process + exec cat wait hang >running +) & + +# Kill the parent cat on exit +defer kill -9 %1 + +# Read the child PID +read -r pid <pid + +# Make sure the parent cat is running before we kill the child, because bash +# will wait() on its children +echo >wait & +read -r _ <running + +# Turn the child into a zombie +kill -9 "$pid" + +# Wait until it's really a zombie +state=R +while [ "$state" != "Z" ]; do + read -r _ _ state _ <"/proc/$pid/stat" +done + +# On Linux, open(/proc/$pid/net) will succeed but readdir() will fail +test -r "/proc/$pid/net" || skip +! invoke_bfs "/proc/$pid/net" >/dev/null 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/test_size.out b/tests/posix/size.out index eeabbd7..eeabbd7 100644 --- a/tests/test_size.out +++ b/tests/posix/size.out diff --git a/tests/posix/size.sh b/tests/posix/size.sh new file mode 100644 index 0000000..1e7528a --- /dev/null +++ b/tests/posix/size.sh @@ -0,0 +1 @@ +bfs_diff basic -type f -size 0 diff --git a/tests/test_size_bytes.out b/tests/posix/size_bytes.out index 279f3f1..279f3f1 100644 --- a/tests/test_size_bytes.out +++ b/tests/posix/size_bytes.out diff --git a/tests/posix/size_bytes.sh b/tests/posix/size_bytes.sh new file mode 100644 index 0000000..6a68321 --- /dev/null +++ b/tests/posix/size_bytes.sh @@ -0,0 +1 @@ +bfs_diff basic -type f -size +0c diff --git a/tests/test_size_plus.out b/tests/posix/size_plus.out index 279f3f1..279f3f1 100644 --- a/tests/test_size_plus.out +++ b/tests/posix/size_plus.out diff --git a/tests/posix/size_plus.sh b/tests/posix/size_plus.sh new file mode 100644 index 0000000..01853d5 --- /dev/null +++ b/tests/posix/size_plus.sh @@ -0,0 +1 @@ +bfs_diff basic -type f -size +0 diff --git a/tests/posix/type_bind_mount.out b/tests/posix/type_bind_mount.out new file mode 100644 index 0000000..2f06c47 --- /dev/null +++ b/tests/posix/type_bind_mount.out @@ -0,0 +1 @@ +./null diff --git a/tests/posix/type_bind_mount.sh b/tests/posix/type_bind_mount.sh new file mode 100644 index 0000000..97b7305 --- /dev/null +++ b/tests/posix/type_bind_mount.sh @@ -0,0 +1,9 @@ +test "$UNAME" = "Linux" || skip + +cd "$TEST" +"$XTOUCH" file null + +bfs_sudo mount --bind /dev/null null || skip +defer bfs_sudo umount null + +bfs_diff . -type c diff --git a/tests/test_type_d.out b/tests/posix/type_d.out index e604709..e604709 100644 --- a/tests/test_type_d.out +++ b/tests/posix/type_d.out diff --git a/tests/posix/type_d.sh b/tests/posix/type_d.sh new file mode 100644 index 0000000..8d06b73 --- /dev/null +++ b/tests/posix/type_d.sh @@ -0,0 +1 @@ +bfs_diff basic -type d diff --git a/tests/posix/type_f.out b/tests/posix/type_f.out new file mode 100644 index 0000000..6218a0c --- /dev/null +++ b/tests/posix/type_f.out @@ -0,0 +1,7 @@ +basic/a +basic/b +basic/c/d +basic/e/f +basic/j/foo +basic/k/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/type_f.sh b/tests/posix/type_f.sh new file mode 100644 index 0000000..1fd0c8c --- /dev/null +++ b/tests/posix/type_f.sh @@ -0,0 +1 @@ +bfs_diff basic -type f diff --git a/tests/test_type_l.out b/tests/posix/type_l.out index f2c8b19..f2c8b19 100644 --- a/tests/test_type_l.out +++ b/tests/posix/type_l.out diff --git a/tests/posix/type_l.sh b/tests/posix/type_l.sh new file mode 100644 index 0000000..457f74d --- /dev/null +++ b/tests/posix/type_l.sh @@ -0,0 +1 @@ +bfs_diff links/skip -type l diff --git a/tests/posix/unionfs.out b/tests/posix/unionfs.out new file mode 100644 index 0000000..28c4ec1 --- /dev/null +++ b/tests/posix/unionfs.out @@ -0,0 +1,10 @@ +. +./lower +./lower/bar +./lower/baz +./lower/foo +./upper +./upper/bar +./upper/baz +./upper/baz/qux +./upper/foo diff --git a/tests/posix/unionfs.sh b/tests/posix/unionfs.sh new file mode 100644 index 0000000..94d3929 --- /dev/null +++ b/tests/posix/unionfs.sh @@ -0,0 +1,9 @@ +[[ "$UNAME" == *BSD* ]] || skip + +cd "$TEST" +"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux} + +bfs_sudo mount -t unionfs -o below lower upper || skip +defer bfs_sudo umount upper + +bfs_diff . diff --git a/tests/posix/user_id.out b/tests/posix/user_id.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/user_id.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_id.sh b/tests/posix/user_id.sh new file mode 100644 index 0000000..c3e4b31 --- /dev/null +++ b/tests/posix/user_id.sh @@ -0,0 +1 @@ +bfs_diff basic -user "$(id -u)" 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_name.out b/tests/posix/user_name.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/user_name.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_name.sh b/tests/posix/user_name.sh new file mode 100644 index 0000000..8599249 --- /dev/null +++ b/tests/posix/user_name.sh @@ -0,0 +1 @@ +bfs_diff basic -user "$(id -un)" diff --git a/tests/posix/user_nouser.out b/tests/posix/user_nouser.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/user_nouser.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_nouser.sh b/tests/posix/user_nouser.sh new file mode 100644 index 0000000..e72bd45 --- /dev/null +++ b/tests/posix/user_nouser.sh @@ -0,0 +1,2 @@ +# Regression test: this was wrongly optimized to -false +bfs_diff basic -user "$(id -u)" \! -nouser 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/test_weird_names.out b/tests/posix/weird_names.out index c395659..c395659 100644 --- a/tests/test_weird_names.out +++ b/tests/posix/weird_names.out diff --git a/tests/posix/weird_names.sh b/tests/posix/weird_names.sh new file mode 100644 index 0000000..8a9a8cd --- /dev/null +++ b/tests/posix/weird_names.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \) diff --git a/tests/posix/xdev.out b/tests/posix/xdev.out new file mode 100644 index 0000000..6253434 --- /dev/null +++ b/tests/posix/xdev.out @@ -0,0 +1,4 @@ +. +./foo +./foo/bar +./mnt diff --git a/tests/posix/xdev.sh b/tests/posix/xdev.sh new file mode 100644 index 0000000..c59c5c8 --- /dev/null +++ b/tests/posix/xdev.sh @@ -0,0 +1,11 @@ +test "$UNAME" = "Darwin" && skip + +cd "$TEST" +mkdir foo mnt + +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt + +"$XTOUCH" foo/bar mnt/baz + +bfs_diff . -xdev 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 new file mode 100644 index 0000000..3ed2a9c --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,453 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Running test cases + +# ERR trap for tests +debug_err() { + local ret=$? line func file + callers | while read -r line func file; do + if [ "$func" = source ]; then + debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR + break + fi + done +} + +# Source a test +source_test() ( + set -eE + trap debug_err ERR + + if ((${#MAKE[@]})); then + # Close the jobserver pipes + exec {READY_PIPE}<&- {DONE_PIPE}>&- + fi + + cd "$TMP" + source "$@" +) + +# Run a test +run_test() { + if ((VERBOSE_ERRORS)); then + source_test "$1" + else + source_test "$1" 2>"$TMP/$TEST.err" + fi + ret=$? + + if ((${#MAKE[@]})); then + # Write one byte to the done pipe + printf . >&$DONE_PIPE + fi + + case $ret in + 0) + if ((VERBOSE_TESTS)); then + color printf "${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST" + fi + ;; + $EX_SKIP) + if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then + color printf "${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST" + fi + ;; + *) + if ((!VERBOSE_ERRORS)); then + cat "$TMP/$TEST.err" >&2 + fi + color printf "${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST" + ;; + esac + + return $ret +} + +# Count the tests running in the background +BG=0 + +# Run a test in the background +bg_test() { + run_test "$1" & + ((++BG)) +} + +# Reap a finished background test +reap_test() { + ((BG--)) + + case "$1" in + 0) + ((++passed)) + ;; + $EX_SKIP) + ((++skipped)) + ;; + *) + ((++failed)) + ;; + esac +} + +# Wait for a background test to finish +wait_test() { + 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 +} + +# Wait until we're ready to run another test +wait_ready() { + if ((${#MAKE[@]})); then + # 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" + + local job status ret rest + while read -r job status ret rest; do + case "$status" in + Done) + reap_test 0 + ;; + Exit) + reap_test $ret + ;; + esac + done <"$TMP/jobs" + + # Read one byte from the ready pipe + read -r -N1 -u$READY_PIPE + elif ((BG >= JOBS)); then + wait_test + fi +} + +# Run make as a co-process to use its job control +comake() { + coproc { + # We can't just use std{in,out}, due to + # https://www.gnu.org/software/make/manual/html_node/Parallel-Input.html + exec {DONE_PIPE}<&0 {READY_PIPE}>&1 + exec "${MAKE[@]}" -s \ + -f "$TESTS/tests.mk" \ + DONE=$DONE_PIPE \ + READY=$READY_PIPE \ + "${!TEST_CASES[@]}" \ + </dev/null >/dev/null + } + + # coproc pipes aren't inherited by subshells, so dup them + exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]} +} + +# 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[@]} + + TEST_FMT="${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" + + if ((${#MAKE[@]})); then + comake + fi + + # 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 + break + fi + + percent=$((100 * ran / total)) + progress "${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" $percent "$TEST" + + mkdir -p "$TMP/$TEST" + OUT="$TMP/$TEST.out" + + bg_test "$TESTS/$TEST.sh" + ((++ran)) + done + + while ((BG > 0)); do + wait_test + done + + if [ "${BAR:-}" ]; then + hide_bar + fi + + if ((passed > 0)); then + color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total + fi + if ((skipped > 0)); then + color printf "${CYN}[SKIP]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $skipped $total + fi + if ((failed > 0)); then + color printf "${RED}[FAIL]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $failed $total + exit 1 + fi +} + +## Utilities for the tests themselves + +# Default return value for failed tests +EX_FAIL=1 + +# Fail the current test +fail() { + exit $EX_FAIL +} + +# Return value when a test is skipped +EX_SKIP=77 + +# Skip the current test +skip() { + if ((VERBOSE_SKIPPED)); then + caller | { + read -r line file + debug "$file" $line "" >&$DUPOUT + } + fi + + exit $EX_SKIP +} + +# Run a command and check its exit status +check_exit() { + local expected="$1" + local actual=0 + shift + "$@" || actual=$? + ((actual == expected)) +} + +# Run a command with sudo +bfs_sudo() { + if ((${#SUDO[@]})); then + "${SUDO[@]}" "$@" + else + return 1 + fi +} + +# Get the inode number of a file +inum() { + ls -id "$@" | awk '{ print $1 }' +} + +# Set an ACL on a file +set_acl() { + case "$UNAME" in + Darwin) + chmod +a "$(id -un) allow read,write" "$1" + ;; + FreeBSD) + if (($(getconf ACL_NFS4 "$1") > 0)); then + setfacl -m "u:$(id -un):rw::allow" "$1" + else + setfacl -m "u:$(id -un):rw" "$1" + fi + ;; + *) + setfacl -m "u:$(id -un):rw" "$1" + ;; + esac +} + +# Print a bfs invocation for --verbose=commands +bfs_verbose() { + if ((VERBOSE_COMMANDS)); then + ( + # Close some fds to make room for the pipe, + # even with extremely low ulimit -n + exec >&- {DUPERR}>&- + exec >&$DUPOUT {DUPOUT}>&- + color bfs_verbose_impl "$@" + ) + fi +} + +bfs_verbose_impl() { + printf "${GRN}%q${RST}" "${BFS[0]}" + if ((${#BFS[@]} > 1)); then + printf " ${GRN}%q${RST}" "${BFS[@]:1}" + fi + + local expr_started=0 color + for arg; do + case "$arg" in + -[A-Z]*|-[dsxf]|-j*) + color="${CYN}" + ;; + \(|!|-[ao]|-and|-or|-not|-exclude) + expr_started=1 + color="${RED}" + ;; + \)|,) + if ((expr_started)); then + color="${RED}" + else + color="${MAG}" + fi + ;; + -?*) + expr_started=1 + color="${BLU}" + ;; + *) + if ((expr_started)); then + color="${BLD}" + else + color="${MAG}" + fi + ;; + esac + printf " ${color}%q${RST}" "$arg" + done + + printf '\n' +} + +# Run the bfs we're testing +invoke_bfs() { + bfs_verbose "$@" + + local ret=0 + # Close the logging fds + "${BFS[@]}" "$@" {DUPOUT}>&- {DUPERR}>&- || ret=$? + + # Allow bfs to fail, but not crash + if ((ret > 125)); then + exit $ret + else + return $ret + fi +} + +# Run bfs with a pseudo-terminal attached +bfs_pty() { + bfs_verbose "$@" + + local ret=0 + "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$? + + if ((ret > 125)); then + exit $ret + else + return $ret + fi +} + +# Create a directory tree with xattrs in scratch +make_xattrs() { + cd "$TEST" + + "$XTOUCH" normal xattr xattr_2 + ln -s xattr link + ln -s normal xattr_link + + case "$UNAME" in + Darwin) + xattr -w bfs_test true xattr \ + && xattr -w bfs_test_2 true xattr_2 \ + && xattr -s -w bfs_test true xattr_link + ;; + FreeBSD) + setextattr user bfs_test true xattr \ + && setextattr user bfs_test_2 true xattr_2 \ + && setextattr -h user bfs_test true xattr_link + ;; + *) + # Linux tmpfs doesn't support the user.* namespace, so we use the security.* + # namespace, which is writable by root and readable by others + bfs_sudo setfattr -n security.bfs_test xattr \ + && bfs_sudo setfattr -n security.bfs_test_2 xattr_2 \ + && bfs_sudo setfattr -h -n security.bfs_test xattr_link + ;; + 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 ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then + DIFF="diff --color=always" +else + DIFF="diff" +fi + +# Sort the output file +sort_output() { + sort -o "$OUT" "$OUT" +} + +# Diff against the expected output +diff_output() { + local GOLD="$TESTS/$TEST.out" + + if ((UPDATE)); then + cp "$OUT" "$GOLD" + elif ! cmp -s "$GOLD" "$OUT"; then + $DIFF -u "$GOLD" "$OUT" >&$DUPERR + fi +} + +# Run bfs, and diff it against the expected output +bfs_diff() { + local ret=0 + invoke_bfs "$@" >"$OUT" || ret=$? + + sort_output + diff_output || exit $EX_DIFF + + return $ret +} 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 new file mode 100644 index 0000000..1569fee --- /dev/null +++ b/tests/stddirs.sh @@ -0,0 +1,181 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Standard directory trees for tests + +# Creates a simple file+directory structure for tests +make_basic() { + "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/} + "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz} + echo baz >"$1/l/foo/bar/baz" +} + +# Creates a file+directory structure with various permissions for tests +make_perms() { + "$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 +make_links() { + "$XTOUCH" -p "$1/file" + ln -s file "$1/symlink" + ln "$1/file" "$1/hardlink" + ln -s nowhere "$1/broken" + ln -s symlink/file "$1/notdir" + "$XTOUCH" -p "$1/deeply/nested"/{dir/,file} + ln -s file "$1/deeply/nested/link" + ln -s nowhere "$1/deeply/nested/broken" + ln -s deeply/nested "$1/skip" +} + +# Creates a file+directory structure with symbolic link loops +make_loops() { + "$XTOUCH" -p "$1/file" + ln -s file "$1/symlink" + ln -s nowhere "$1/broken" + ln -s symlink/file "$1/notdir" + ln -s loop "$1/loop" + mkdir -p "$1/deeply/nested/dir" + ln -s ../../deeply "$1/deeply/nested/loop" + 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" + "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b" + "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c" + ln -s a "$1/l" + "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l" + "$XTOUCH" -p -t "1991-12-14 00:04" "$1" +} + +# Creates a file+directory structure with various weird file/directory names +make_weirdnames() { + "$XTOUCH" -p "$1/-/a" + "$XTOUCH" -p "$1/(/b" + "$XTOUCH" -p "$1/(-/c" + "$XTOUCH" -p "$1/!/d" + "$XTOUCH" -p "$1/!-/e" + "$XTOUCH" -p "$1/,/f" + "$XTOUCH" -p "$1/)/g" + "$XTOUCH" -p "$1/.../h" + "$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 +make_deep() { + mkdir -p "$1" + + # $name will be 255 characters, aka _XOPEN_NAME_MAX + local name="0123456789ABCDEF" + name="$name$name$name$name" + name="$name$name$name$name" + name="${name:0:255}" + + # 4 * 4 * 256 == 4096 >= PATH_MAX + local path="$name/$name/$name/$name" + path="$path/$path/$path/$path" + + "$XTOUCH" -p "$1"/{{0..9},A,B,C,D,E,F}/"$path/$name" +} + +# Creates a directory structure with many different types, and therefore colors +make_rainbow() { + "$XTOUCH" -p "$1/file.txt" + "$XTOUCH" -p "$1/file.dat" + "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz} + "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ} + "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz" + ln -s file.txt "$1/link.txt" + "$XTOUCH" -p "$1/mh1" + ln "$1/mh1" "$1/mh2" + mkfifo "$1/pipe" + # TODO: block + ln -s /dev/null "$1/chardev_link" + ln -s nowhere "$1/broken" + "$MKSOCK" "$1/socket" + "$XTOUCH" -p "$1"/s{u,g,ug}id + chmod 06644 "$1"/sugid + chmod 04644 "$1"/suid + chmod 02644 "$1"/sgid + mkdir "$1/ow" "$1"/sticky{,_ow} + chmod o+w "$1"/*ow + chmod +t "$1"/sticky* + "$XTOUCH" -p "$1"/exec.sh + chmod +x "$1"/exec.sh + "$XTOUCH" -p "$1/"$'\e[1m/\e[0m' +} + +# Create all standard directory trees +make_stddirs() { + TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX) + + if ((CLEAN)); then + defer clean_stddirs + else + color printf "Test files saved to ${BLD}%s${RST}\n" "$TMP" + fi + + chown "$(id -u):$(id -g)" "$TMP" + + make_basic "$TMP/basic" + 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" + make_rainbow "$TMP/rainbow" +} + +# Clean up temporary directories on exit +clean_stddirs() { + # Don't force rm to deal with long paths + for dir in "$TMP"/deep/*/*; do + if [ -d "$dir" ]; then + (cd "$dir" && rm -rf *) + fi + done + + chmod -R +rwX "$TMP" + rm -rf "$TMP" +} diff --git a/tests/test_L_acl.out b/tests/test_L_acl.out deleted file mode 100644 index 1dae00a..0000000 --- a/tests/test_L_acl.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/acl -scratch/link diff --git a/tests/test_L_capable.out b/tests/test_L_capable.out deleted file mode 100644 index e5ba3c7..0000000 --- a/tests/test_L_capable.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/capable -scratch/link diff --git a/tests/test_L_delete.out b/tests/test_L_delete.out deleted file mode 100644 index ed0e9a1..0000000 --- a/tests/test_L_delete.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch -scratch/foo diff --git a/tests/test_L_mount.out b/tests/test_L_mount.out deleted file mode 100644 index 2e80082..0000000 --- a/tests/test_L_mount.out +++ /dev/null @@ -1,5 +0,0 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/foo/qux -scratch/mnt diff --git a/tests/test_L_xattr.out b/tests/test_L_xattr.out deleted file mode 100644 index 12fac95..0000000 --- a/tests/test_L_xattr.out +++ /dev/null @@ -1,3 +0,0 @@ -scratch/link -scratch/xattr -scratch/xattr_2 diff --git a/tests/test_L_xattrname.out b/tests/test_L_xattrname.out deleted file mode 100644 index 4dc4836..0000000 --- a/tests/test_L_xattrname.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/link -scratch/xattr diff --git a/tests/test_L_xdev.out b/tests/test_L_xdev.out deleted file mode 100644 index 2e80082..0000000 --- a/tests/test_L_xdev.out +++ /dev/null @@ -1,5 +0,0 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/foo/qux -scratch/mnt diff --git a/tests/test_acl.out b/tests/test_acl.out deleted file mode 100644 index ddf8446..0000000 --- a/tests/test_acl.out +++ /dev/null @@ -1 +0,0 @@ -scratch/acl diff --git a/tests/test_capable.out b/tests/test_capable.out deleted file mode 100644 index 78b5bd9..0000000 --- a/tests/test_capable.out +++ /dev/null @@ -1 +0,0 @@ -scratch/capable diff --git a/tests/test_color_ls.out b/tests/test_color_ls.out deleted file mode 100644 index b08d894..0000000 --- a/tests/test_color_ls.out +++ /dev/null @@ -1,12 +0,0 @@ -[01;31mscratch/foo/[0m[01;31mbar[0m -[01;31mscratch/foo/[0m[01;31mbar[0m -[01;34m/[0m[01;31m__bfs__/[0m[01;31mnowhere[0m -[01;34m/[0m[01;31m__bfs__/[0m[01;31mnowhere[0m -[01;34mfoo/bar/[0m[01;31mbaz/[0m[01;31mqux[0m -[01;34mfoo/bar/[0m[01;31mbaz/[0m[01;31mqux[0m -[01;34mfoo/bar/[0m[01;31mnowhere[0m -[01;34mfoo/bar/[0m[01;31mnowhere[0m -[01;34mfoo/bar/[0m[01;31mnowhere/[0m[01;31mnothing[0m -[01;34mfoo/bar/[0m[01;31mnowhere/[0m[01;31mnothing[0m -[01;34mfoo/bar/[0mbaz -[01;34mfoo/bar/[0mbaz diff --git a/tests/test_color_mi.out b/tests/test_color_mi.out deleted file mode 100644 index 77fc8a8..0000000 --- a/tests/test_color_mi.out +++ /dev/null @@ -1,20 +0,0 @@ -[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/[0mmh1 -[01;34mrainbow/[0mmh2 -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz diff --git a/tests/test_color_nul.out b/tests/test_color_nul.out Binary files differdeleted file mode 100644 index c328f82..0000000 --- a/tests/test_color_nul.out +++ /dev/null diff --git a/tests/test_color_or0_mi.out b/tests/test_color_or0_mi.out deleted file mode 100644 index 77fc8a8..0000000 --- a/tests/test_color_or0_mi.out +++ /dev/null @@ -1,20 +0,0 @@ -[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/[0mmh1 -[01;34mrainbow/[0mmh2 -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz diff --git a/tests/test_color_or0_mi0.out b/tests/test_color_or0_mi0.out deleted file mode 100644 index 77fc8a8..0000000 --- a/tests/test_color_or0_mi0.out +++ /dev/null @@ -1,20 +0,0 @@ -[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/[0mmh1 -[01;34mrainbow/[0mmh2 -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz diff --git a/tests/test_color_st0_tw0_ow0.out b/tests/test_color_st0_tw0_ow0.out deleted file mode 100644 index 2b86fe4..0000000 --- a/tests/test_color_st0_tw0_ow0.out +++ /dev/null @@ -1,20 +0,0 @@ -[01;34mrainbow[0m -[01;34mrainbow/[0m[01;32mexec.sh[0m -[01;34mrainbow/[0m[01;34mow[0m -[01;34mrainbow/[0m[01;34msticky[0m -[01;34mrainbow/[0m[01;34msticky_ow[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;43msgid[0m -[01;34mrainbow/[0m[33mpipe[0m -[01;34mrainbow/[0m[37;41msugid[0m -[01;34mrainbow/[0m[37;41msuid[0m -[01;34mrainbow/[0mfile.dat -[01;34mrainbow/[0mfile.txt -[01;34mrainbow/[0mmh1 -[01;34mrainbow/[0mmh2 -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz diff --git a/tests/test_color_star.out b/tests/test_color_star.out deleted file mode 100644 index 77fc8a8..0000000 --- a/tests/test_color_star.out +++ /dev/null @@ -1,20 +0,0 @@ -[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/[0mmh1 -[01;34mrainbow/[0mmh2 -[01;34mrainbow/[0mstar.gz -[01;34mrainbow/[0mstar.tar -[01;34mrainbow/[0mstar.tar.gz diff --git a/tests/test_delete.out b/tests/test_delete.out deleted file mode 100644 index fb188b9..0000000 --- a/tests/test_delete.out +++ /dev/null @@ -1 +0,0 @@ -scratch diff --git a/tests/test_delete_many.out b/tests/test_delete_many.out deleted file mode 100644 index fb188b9..0000000 --- a/tests/test_delete_many.out +++ /dev/null @@ -1 +0,0 @@ -scratch diff --git a/tests/test_depth_error.out b/tests/test_depth_error.out deleted file mode 100644 index ed0e9a1..0000000 --- a/tests/test_depth_error.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch -scratch/foo diff --git a/tests/test_empty_special.out b/tests/test_empty_special.out deleted file mode 100644 index 3927f2b..0000000 --- a/tests/test_empty_special.out +++ /dev/null @@ -1,14 +0,0 @@ -rainbow/exec.sh -rainbow/file.dat -rainbow/file.txt -rainbow/mh1 -rainbow/mh2 -rainbow/ow -rainbow/sgid -rainbow/star.gz -rainbow/star.tar -rainbow/star.tar.gz -rainbow/sticky -rainbow/sticky_ow -rainbow/sugid -rainbow/suid diff --git a/tests/test_exec_flush.out b/tests/test_exec_flush.out Binary files differdeleted file mode 100644 index 7e93fdf..0000000 --- a/tests/test_exec_flush.out +++ /dev/null diff --git a/tests/test_executable.out b/tests/test_executable.out deleted file mode 100644 index 49c1b21..0000000 --- a/tests/test_executable.out +++ /dev/null @@ -1,4 +0,0 @@ -perms -perms/rwx -perms/rx -perms/wx diff --git a/tests/test_flags.out b/tests/test_flags.out deleted file mode 100644 index 11998ed..0000000 --- a/tests/test_flags.out +++ /dev/null @@ -1 +0,0 @@ -scratch/bar diff --git a/tests/test_inum_automount.out b/tests/test_inum_automount.out deleted file mode 100644 index 99c7511..0000000 --- a/tests/test_inum_automount.out +++ /dev/null @@ -1 +0,0 @@ -scratch/mnt diff --git a/tests/test_inum_bind_mount.out b/tests/test_inum_bind_mount.out deleted file mode 100644 index a520de3..0000000 --- a/tests/test_inum_bind_mount.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/bar -scratch/foo diff --git a/tests/test_inum_mount.out b/tests/test_inum_mount.out deleted file mode 100644 index 99c7511..0000000 --- a/tests/test_inum_mount.out +++ /dev/null @@ -1 +0,0 @@ -scratch/mnt diff --git a/tests/test_mount.out b/tests/test_mount.out deleted file mode 100644 index f7839fb..0000000 --- a/tests/test_mount.out +++ /dev/null @@ -1,4 +0,0 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/mnt diff --git a/tests/test_perm_000.out b/tests/test_perm_000.out deleted file mode 100644 index 5fd30bc..0000000 --- a/tests/test_perm_000.out +++ /dev/null @@ -1 +0,0 @@ -perms/0 diff --git a/tests/test_perm_000_minus.out b/tests/test_perm_000_minus.out deleted file mode 100644 index d7494b8..0000000 --- a/tests/test_perm_000_minus.out +++ /dev/null @@ -1,8 +0,0 @@ -perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_000_plus.out b/tests/test_perm_000_plus.out deleted file mode 100644 index d7494b8..0000000 --- a/tests/test_perm_000_plus.out +++ /dev/null @@ -1,8 +0,0 @@ -perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_000_slash.out b/tests/test_perm_000_slash.out deleted file mode 100644 index d7494b8..0000000 --- a/tests/test_perm_000_slash.out +++ /dev/null @@ -1,8 +0,0 @@ -perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_222.out b/tests/test_perm_222.out deleted file mode 100644 index 1690e43..0000000 --- a/tests/test_perm_222.out +++ /dev/null @@ -1 +0,0 @@ -perms/w diff --git a/tests/test_perm_222_minus.out b/tests/test_perm_222_minus.out deleted file mode 100644 index 1690e43..0000000 --- a/tests/test_perm_222_minus.out +++ /dev/null @@ -1 +0,0 @@ -perms/w diff --git a/tests/test_perm_222_plus.out b/tests/test_perm_222_plus.out deleted file mode 100644 index 9a5b95a..0000000 --- a/tests/test_perm_222_plus.out +++ /dev/null @@ -1,5 +0,0 @@ -perms -perms/rw -perms/rwx -perms/w -perms/wx diff --git a/tests/test_perm_222_slash.out b/tests/test_perm_222_slash.out deleted file mode 100644 index 9a5b95a..0000000 --- a/tests/test_perm_222_slash.out +++ /dev/null @@ -1,5 +0,0 @@ -perms -perms/rw -perms/rwx -perms/w -perms/wx diff --git a/tests/test_perm_644.out b/tests/test_perm_644.out deleted file mode 100644 index 4e64e49..0000000 --- a/tests/test_perm_644.out +++ /dev/null @@ -1 +0,0 @@ -perms/rw diff --git a/tests/test_perm_644_minus.out b/tests/test_perm_644_minus.out deleted file mode 100644 index 2e2576b..0000000 --- a/tests/test_perm_644_minus.out +++ /dev/null @@ -1,3 +0,0 @@ -perms -perms/rw -perms/rwx diff --git a/tests/test_perm_644_plus.out b/tests/test_perm_644_plus.out deleted file mode 100644 index 7e5ae98..0000000 --- a/tests/test_perm_644_plus.out +++ /dev/null @@ -1,7 +0,0 @@ -perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_644_slash.out b/tests/test_perm_644_slash.out deleted file mode 100644 index 7e5ae98..0000000 --- a/tests/test_perm_644_slash.out +++ /dev/null @@ -1,7 +0,0 @@ -perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_leading_plus_symbolic_slash.out b/tests/test_perm_leading_plus_symbolic_slash.out deleted file mode 100644 index 7e5ae98..0000000 --- a/tests/test_perm_leading_plus_symbolic_slash.out +++ /dev/null @@ -1,7 +0,0 @@ -perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_perm_symbolic_minus.out b/tests/test_perm_symbolic_minus.out deleted file mode 100644 index 2e2576b..0000000 --- a/tests/test_perm_symbolic_minus.out +++ /dev/null @@ -1,3 +0,0 @@ -perms -perms/rw -perms/rwx diff --git a/tests/test_perm_symbolic_slash.out b/tests/test_perm_symbolic_slash.out deleted file mode 100644 index 7e5ae98..0000000 --- a/tests/test_perm_symbolic_slash.out +++ /dev/null @@ -1,7 +0,0 @@ -perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx diff --git a/tests/test_permcopy.out b/tests/test_permcopy.out deleted file mode 100644 index 4e64e49..0000000 --- a/tests/test_permcopy.out +++ /dev/null @@ -1 +0,0 @@ -perms/rw diff --git a/tests/test_print0.out b/tests/test_print0.out Binary files differdeleted file mode 100644 index ed2b7e8..0000000 --- a/tests/test_print0.out +++ /dev/null diff --git a/tests/test_printf_Y_error.out b/tests/test_printf_Y_error.out deleted file mode 100644 index 410a9b5..0000000 --- a/tests/test_printf_Y_error.out +++ /dev/null @@ -1,3 +0,0 @@ -(scratch) () d d -(scratch/bar) (foo/bar) l ? -(scratch/foo) () d d diff --git a/tests/test_printf_nul.out b/tests/test_printf_nul.out Binary files differdeleted file mode 100644 index 6833fdd..0000000 --- a/tests/test_printf_nul.out +++ /dev/null diff --git a/tests/test_readable.out b/tests/test_readable.out deleted file mode 100644 index 386feba..0000000 --- a/tests/test_readable.out +++ /dev/null @@ -1,5 +0,0 @@ -perms -perms/r -perms/rw -perms/rwx -perms/rx diff --git a/tests/test_regex_invalid_utf8.out b/tests/test_regex_invalid_utf8.out deleted file mode 100644 index 03f3f58..0000000 --- a/tests/test_regex_invalid_utf8.out +++ /dev/null @@ -1 +0,0 @@ -scratch/ diff --git a/tests/test_rm.out b/tests/test_rm.out deleted file mode 100644 index fb188b9..0000000 --- a/tests/test_rm.out +++ /dev/null @@ -1 +0,0 @@ -scratch diff --git a/tests/test_type_bind_mount.out b/tests/test_type_bind_mount.out deleted file mode 100644 index 6435159..0000000 --- a/tests/test_type_bind_mount.out +++ /dev/null @@ -1 +0,0 @@ -scratch/null diff --git a/tests/test_writable.out b/tests/test_writable.out deleted file mode 100644 index 9a5b95a..0000000 --- a/tests/test_writable.out +++ /dev/null @@ -1,5 +0,0 @@ -perms -perms/rw -perms/rwx -perms/w -perms/wx diff --git a/tests/test_xattr.out b/tests/test_xattr.out deleted file mode 100644 index 109e7c9..0000000 --- a/tests/test_xattr.out +++ /dev/null @@ -1,3 +0,0 @@ -scratch/xattr -scratch/xattr_2 -scratch/xattr_link diff --git a/tests/test_xattrname.out b/tests/test_xattrname.out deleted file mode 100644 index 0285ac1..0000000 --- a/tests/test_xattrname.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/xattr -scratch/xattr_link diff --git a/tests/test_xdev.out b/tests/test_xdev.out deleted file mode 100644 index f7839fb..0000000 --- a/tests/test_xdev.out +++ /dev/null @@ -1,4 +0,0 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/mnt diff --git a/tests/test_xtype_bind_mount.out b/tests/test_xtype_bind_mount.out deleted file mode 100644 index 16804ea..0000000 --- a/tests/test_xtype_bind_mount.out +++ /dev/null @@ -1,2 +0,0 @@ -scratch/link -scratch/null diff --git a/tests/tests.h b/tests/tests.h new file mode 100644 index 0000000..d395c7c --- /dev/null +++ b/tests/tests.h @@ -0,0 +1,74 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Unit tests. + */ + +#ifndef BFS_TESTS_H +#define BFS_TESTS_H + +#include "bfstd.h" +#include "diag.h" + +/** Memory allocation tests. */ +void check_alloc(void); + +/** Standard library wrapper tests. */ +void check_bfstd(void); + +/** Bit manipulation tests. */ +void check_bit(void); + +/** I/O queue tests. */ +void check_ioq(void); + +/** Linked list tests. */ +void check_list(void); + +/** Signal hook tests. */ +void check_sighook(void); + +/** Trie tests. */ +void check_trie(void); + +/** Process spawning tests. */ +void check_xspawn(void); + +/** Time tests. */ +void check_xtime(void); + +/** 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_(#__VA_ARGS__, __VA_ARGS__, "", ) + +#define bfs_check_(str, cond, format, ...) \ + bfs_check_impl((cond) || (bfs_check__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false)) + +#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_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 new file mode 100644 index 0000000..035ca79 --- /dev/null +++ b/tests/tests.mk @@ -0,0 +1,13 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that exposes make's job control to tests.sh + +# 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/tests.sh b/tests/tests.sh index dce421a..3890243 100755 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -1,3460 +1,20 @@ #!/usr/bin/env bash -############################################################################ -# bfs # -# Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> # -# # -# Permission to use, copy, modify, and/or distribute this software for any # -# purpose with or without fee is hereby granted. # -# # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -############################################################################ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD set -euP umask 022 -export LC_ALL=C -export TZ=UTC0 - -export ASAN_OPTIONS="abort_on_error=1" -export LSAN_OPTIONS="abort_on_error=1" -export MSAN_OPTIONS="abort_on_error=1" -export TSAN_OPTIONS="abort_on_error=1" -export UBSAN_OPTIONS="abort_on_error=1" - -export LS_COLORS="" -unset BFS_COLORS - -if [ -t 1 ]; then - BLD=$'\033[01m' - RED=$'\033[01;31m' - GRN=$'\033[01;32m' - YLW=$'\033[01;33m' - BLU=$'\033[01;34m' - MAG=$'\033[01;35m' - CYN=$'\033[01;36m' - RST=$'\033[0m' -else - BLD= - RED= - GRN= - YLW= - BLU= - MAG= - CYN= - RST= -fi - -UNAME=$(uname) - -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 - if [ -n "$BFS_TRIED_DROP" ]; then - cat >&2 <<EOF -${RED}error:${RST} Failed to drop capabilities. -EOF - - exit 1 - fi - - cat >&2 <<EOF -${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and -${BLD}cap_dac_read_search${RST}. - -EOF - - BFS_TRIED_DROP=y exec capsh \ - --drop=cap_dac_override,cap_dac_read_search \ - --caps=cap_dac_override,cap_dac_read_search-eip \ - -- "$0" "$@" - fi -elif [ "$EUID" -eq 0 ]; then - UNLESS= - if [ "$UNAME" = "Linux" ]; then - UNLESS=" unless ${GRN}capsh${RST} is installed" - fi - - cat >&2 <<EOF -${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore -will not work when run as ${BLD}$(id -un)${RST}${UNLESS}. -EOF - exit 1 -fi - -function usage() { - local pad=$(printf "%*s" ${#0} "") - cat <<EOF -Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLU}--sudo${RST}] - $pad [${BLU}--stop${RST}] [${BLU}--noclean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}] - $pad [${BLD}test_*${RST} [${BLD}test_*${RST} ...]] - - ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST} - Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST}) - - ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST} - Choose which test cases to run (default: ${BLU}--all${RST}) - - ${BLU}--sudo${RST} - Run tests that require root - - ${BLU}--stop${RST} - Stop when the first error occurs - - ${BLU}--noclean${RST} - Keep the test directories around after the run - - ${BLU}--update${RST} - Update the expected outputs for the test cases - - ${BLU}--verbose${RST}=${BLD}commands${RST} - Log the commands that get executed - ${BLU}--verbose${RST}=${BLD}errors${RST} - Don't redirect standard error - ${BLU}--verbose${RST}=${BLD}skipped${RST} - Log which tests get skipped - ${BLU}--verbose${RST}=${BLD}tests${RST} - Log all tests that get run - ${BLU}--verbose${RST} - Log everything - - ${BLU}--help${RST} - This message - - ${BLD}test_*${RST} - Select individual test cases to run -EOF -} - -DEFAULT=yes -POSIX= -BSD= -GNU= -ALL= -SUDO= -STOP= -CLEAN=yes -UPDATE= -VERBOSE_COMMANDS= -VERBOSE_ERRORS= -VERBOSE_SKIPPED= -VERBOSE_TESTS= -EXPLICIT= - -enabled_tests=() - -for arg; do - case "$arg" in - --bfs=*) - BFS="${arg#*=}" - ;; - --posix) - DEFAULT= - POSIX=yes - ;; - --bsd) - DEFAULT= - POSIX=yes - BSD=yes - ;; - --gnu) - DEFAULT= - POSIX=yes - GNU=yes - ;; - --all) - DEFAULT= - POSIX=yes - BSD=yes - GNU=yes - ALL=yes - ;; - --sudo) - SUDO=yes - ;; - --stop) - STOP=yes - ;; - --noclean) - CLEAN= - ;; - --update) - UPDATE=yes - ;; - --verbose=commands) - VERBOSE_COMMANDS=yes - ;; - --verbose=errors) - VERBOSE_ERRORS=yes - ;; - --verbose=skipped) - VERBOSE_SKIPPED=yes - ;; - --verbose=tests) - VERBOSE_SKIPPED=yes - VERBOSE_TESTS=yes - ;; - --verbose) - VERBOSE_COMMANDS=yes - VERBOSE_ERRORS=yes - VERBOSE_SKIPPED=yes - VERBOSE_TESTS=yes - ;; - --help) - usage - exit 0 - ;; - test_*) - EXPLICIT=yes - SUDO=yes - enabled_tests+=("$arg") - ;; - *) - printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2 - usage >&2 - exit 1 - ;; - esac -done - -posix_tests=( - # General parsing - test_basic - - test_parens - test_bang - test_implicit_and - test_a - test_o - - test_weird_names - - test_incomplete - test_missing_paren - test_extra_paren - - # Flags - - test_H - test_H_slash - test_H_broken - test_H_notdir - test_H_loops - - test_L - test_L_broken - test_L_notdir - test_L_loops - - test_flag_weird_names - test_flag_comma - - # Primaries - - test_depth - test_depth_slash - test_depth_error - test_L_depth - - test_exec - test_exec_nopath - test_exec_plus - test_exec_plus_status - test_exec_plus_semicolon - - test_group_name - test_group_id - test_group_nogroup - - test_links - test_links_plus - test_links_minus - - test_name - test_name_root - test_name_root_depth - test_name_trailing_slash - test_name_star_star - test_name_character_class - test_name_bracket - test_name_backslash - test_name_double_backslash - - test_newer - test_newer_link - - test_nogroup - test_nogroup_ulimit - - test_nouser - test_nouser_ulimit - - test_ok_stdin - test_ok_plus_semicolon - - test_path - - test_perm_000 - test_perm_000_minus - test_perm_222 - test_perm_222_minus - test_perm_644 - test_perm_644_minus - test_perm_symbolic - test_perm_symbolic_minus - test_perm_leading_plus_symbolic_minus - test_permcopy - test_perm_setid - test_perm_sticky - - test_prune - test_prune_file - test_prune_or_print - test_not_prune - - test_size - test_size_plus - test_size_bytes - - test_type_d - test_type_f - test_type_l - test_H_type_l - test_L_type_l - test_type_bind_mount - - test_user_name - test_user_id - test_user_nouser - - test_xdev - test_L_xdev - - # Closed file descriptors - test_closed_stdin - test_closed_stdout - test_closed_stderr - - # PATH_MAX handling - test_deep - - # Optimizer tests - test_or_purity - test_double_negation - test_de_morgan_not - test_de_morgan_and - test_de_morgan_or - test_data_flow_group - test_data_flow_user - test_data_flow_type - test_data_flow_and_swap - test_data_flow_or_swap -) - -bsd_tests=( - # Flags - - test_E - - test_P - test_P_slash - - test_X - - test_d_path - - test_f - - test_s - - test_double_dash - test_flag_double_dash - - # Primaries - - test_acl - test_L_acl - - test_anewer - test_asince - - test_delete - test_delete_many - - test_depth_maxdepth_1 - test_depth_maxdepth_2 - test_depth_mindepth_1 - test_depth_mindepth_2 - - test_depth_n - test_depth_n_plus - test_depth_n_minus - test_depth_depth_n - test_depth_depth_n_plus - test_depth_depth_n_minus - test_depth_overflow - test_data_flow_depth - - test_exec_substring - - test_execdir_pwd - test_execdir_slash - test_execdir_slash_pwd - test_execdir_slashes - test_execdir_ulimit - - test_exit - test_exit_no_implicit_print - - test_flags - - test_follow - - test_gid_name - - test_ilname - test_L_ilname - - test_iname - - test_inum - test_inum_mount - test_inum_bind_mount - - test_ipath - - test_iregex - - test_lname - test_L_lname - - test_ls - test_L_ls - - test_maxdepth - - test_mindepth - - test_mnewer - test_H_mnewer - - test_mount - test_L_mount - - test_msince - - test_mtime_units - - test_name_slash - test_name_slashes - - test_H_newer - - test_newerma - test_newermt - test_newermt_epoch_minus_one - - test_ok_stdin - test_ok_closed_stdin - - test_okdir_stdin - test_okdir_closed_stdin - - test_perm_000_plus - test_perm_222_plus - test_perm_644_plus - - test_printx - - test_quit - test_quit_child - test_quit_depth - test_quit_depth_child - test_quit_after_print - test_quit_before_print - test_quit_implicit_print - - test_rm - - test_regex - test_regex_parens - - test_samefile - test_samefile_symlink - test_H_samefile_symlink - test_L_samefile_symlink - test_samefile_broken - test_H_samefile_broken - test_L_samefile_broken - test_samefile_notdir - test_H_samefile_notdir - test_L_samefile_notdir - - test_size_T - test_size_big - - test_uid_name - - test_xattr - test_L_xattr - - test_xattrname - test_L_xattrname - - # Optimizer tests - test_data_flow_sparse -) - -gnu_tests=( - # General parsing - - test_not - test_and - test_or - test_comma - test_precedence - - test_follow_comma - - # Flags - - test_P - test_P_slash - - test_L_loops_continue - - test_double_dash - test_flag_double_dash - - # Primaries - - test_anewer - - test_path_d - - test_daystart - test_daystart_twice - - test_delete - test_delete_many - test_L_delete - - test_depth_mindepth_1 - test_depth_mindepth_2 - test_depth_maxdepth_1 - test_depth_maxdepth_2 - - test_empty - test_empty_special - - test_exec_nothing - test_exec_substring - test_exec_flush - test_exec_flush_fail - test_exec_plus_flush - test_exec_plus_flush_fail - - test_execdir - test_execdir_substring - test_execdir_plus_semicolon - test_execdir_pwd - test_execdir_slash - test_execdir_slash_pwd - test_execdir_slashes - test_execdir_ulimit - - test_executable - - test_false - - test_files0_from_file - test_files0_from_stdin - test_files0_from_none - test_files0_from_empty - test_files0_from_nowhere - test_files0_from_nothing - test_files0_from_ok - - test_fls - - test_follow - - test_fprint - test_fprint_duplicate - test_fprint_error - test_fprint_noerror - test_fprint_noarg - test_fprint_nonexistent - test_fprint_truncate - - test_fprint0 - - test_fprintf - test_fprintf_nofile - test_fprintf_noformat - - test_fstype - - test_gid - test_gid_plus - test_gid_plus_plus - test_gid_minus - test_gid_minus_plus - - test_ignore_readdir_race - test_ignore_readdir_race_root - test_ignore_readdir_race_notdir - - test_ilname - test_L_ilname - - test_iname - - test_inum - test_inum_mount - test_inum_bind_mount - test_inum_automount - - test_ipath - - test_iregex - - test_iwholename - - test_lname - test_L_lname - - test_ls - test_L_ls - - test_maxdepth - - test_mindepth - - test_mount - test_L_mount - - test_name_slash - test_name_slashes - - test_H_newer - - test_newerma - test_newermt - test_newermt_epoch_minus_one - - test_ok_closed_stdin - test_ok_nothing - - test_okdir_closed_stdin - test_okdir_plus_semicolon - - test_perm_000_slash - test_perm_222_slash - test_perm_644_slash - test_perm_symbolic_slash - test_perm_leading_plus_symbolic_slash - - test_print_error - - test_print0 - - test_printf - test_printf_empty - test_printf_slash - test_printf_slashes - test_printf_trailing_slash - test_printf_trailing_slashes - test_printf_flags - test_printf_types - test_printf_escapes - test_printf_times - test_printf_leak - test_printf_nul - test_printf_Y_error - test_printf_H - test_printf_u_g_ulimit - test_printf_l_nonlink - - test_quit - test_quit_child - test_quit_depth - test_quit_depth_child - test_quit_after_print - test_quit_before_print - - test_readable - - test_regex - test_regex_parens - test_regex_error - test_regex_invalid_utf8 - - test_regextype_posix_basic - test_regextype_posix_extended - test_regextype_ed - test_regextype_emacs - test_regextype_grep - test_regextype_sed - - test_samefile - test_samefile_symlink - test_H_samefile_symlink - test_L_samefile_symlink - test_samefile_broken - test_H_samefile_broken - test_L_samefile_broken - test_samefile_notdir - test_H_samefile_notdir - test_L_samefile_notdir - - test_size_big - - test_true - - test_uid - test_uid_plus - test_uid_plus_plus - test_uid_minus - test_uid_minus_plus - - test_wholename - - test_writable - - test_xtype_l - test_xtype_f - test_L_xtype_l - test_L_xtype_f - test_xtype_bind_mount - - # Optimizer tests - test_and_purity - test_not_reachability - test_comma_reachability - test_and_false_or_true - test_comma_redundant_true - test_comma_redundant_false -) - -bfs_tests=( - # General parsing - test_path_flag_expr - test_path_expr_flag - test_flag_expr_path - test_expr_flag_path - test_expr_path_flag - - test_unexpected_operator - test_and_incomplete - test_or_incomplete - test_comma_incomplete - - test_typo - - # Flags - - test_D_multi - test_D_all - - test_O0 - test_O1 - test_O2 - test_O3 - test_Ofast - - test_S_bfs - test_S_dfs - test_S_ids - - # Special forms - - test_exclude_name - test_exclude_depth - test_exclude_mindepth - test_exclude_print - test_exclude_exclude - - # Primaries - - test_capable - test_L_capable - - test_color - test_color_L - test_color_rs_lc_rc_ec - test_color_escapes - test_color_nul - test_color_ln_target - test_color_L_ln_target - test_color_mh - test_color_mh0 - test_color_or - test_color_mi - test_color_or_mi - test_color_or_mi0 - test_color_or0_mi - test_color_or0_mi0 - test_color_su_sg0 - test_color_su0_sg - test_color_su0_sg0 - test_color_st_tw_ow0 - test_color_st_tw0_ow - test_color_st_tw0_ow0 - test_color_st0_tw_ow - test_color_st0_tw_ow0 - test_color_st0_tw0_ow - test_color_st0_tw0_ow0 - test_color_ext - test_color_ext0 - test_color_ext_override - test_color_ext_underride - test_color_missing_colon - test_color_no_stat - test_color_L_no_stat - test_color_star - test_color_ls - - test_exec_flush_fprint - test_exec_flush_fprint_fail - - test_execdir_plus - - test_fprint_duplicate_stdout - test_fprint_error_stdout - test_fprint_error_stderr - - test_help - - test_hidden - test_hidden_root - - test_links_noarg - test_links_empty - test_links_negative - test_links_invalid - - test_newerma_nonexistent - test_newermt_invalid - test_newermq - test_newerqm - - test_nohidden - test_nohidden_depth - - test_perm_symbolic_trailing_comma - test_perm_symbolic_double_comma - test_perm_symbolic_missing_action - test_perm_leading_plus_symbolic - - test_printf_w - test_printf_incomplete_escape - test_printf_invalid_escape - test_printf_incomplete_format - test_printf_invalid_format - test_printf_duplicate_flag - test_printf_must_be_numeric - test_printf_color - test_printf_everything - - test_type_multi - - test_unique - test_unique_depth - test_L_unique - test_L_unique_loops - test_L_unique_depth - - test_version - - test_warn - test_nowarn - - test_xtype_multi - - # Optimizer tests - test_data_flow_hidden - test_xtype_reorder - test_xtype_depth - - # PATH_MAX handling - test_deep_strict - - # Error handling - test_stderr_fails_silently - test_stderr_fails_loudly -) - -if [ "$DEFAULT" ]; then - POSIX=yes - BSD=yes - GNU=yes - ALL=yes -fi - -if [ ! "$EXPLICIT" ]; then - [ "$POSIX" ] && enabled_tests+=("${posix_tests[@]}") - [ "$BSD" ] && enabled_tests+=("${bsd_tests[@]}") - [ "$GNU" ] && enabled_tests+=("${gnu_tests[@]}") - [ "$ALL" ] && enabled_tests+=("${bfs_tests[@]}") -fi - -eval "enabled_tests=($(printf '%q\n' "${enabled_tests[@]}" | sort -u))" - -function _realpath() { - ( - cd "$(dirname -- "$1")" - echo "$PWD/$(basename -- "$1")" - ) -} - -TESTS=$(_realpath "$(dirname -- "${BASH_SOURCE[0]}")") - -if [ "${BUILDDIR-}" ]; then - BIN=$(_realpath "$BUILDDIR/bin") -else - BIN=$(_realpath "$TESTS/../bin") -fi - -# Try to resolve the path to $BFS before we cd, while also supporting -# --bfs="./bin/bfs -S ids" -read -a BFS <<<"${BFS:-$BIN/bfs}" -BFS[0]=$(_realpath "$(command -v "${BFS[0]}")") - -# The temporary directory that will hold our test data -TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX) -chown "$(id -u):$(id -g)" "$TMP" - -# Clean up temporary directories on exit -function cleanup() { - # Don't force rm to deal with long paths - for dir in "$TMP"/deep/*/*; do - if [ -d "$dir" ]; then - (cd "$dir" && rm -rf *) - fi - done - - # In case a test left anything weird in scratch/ - if [ -e "$TMP"/scratch ]; then - chmod -R +rX "$TMP"/scratch - fi - - rm -rf "$TMP" -} - -if [ "$CLEAN" ]; then - trap cleanup EXIT -else - echo "Test files saved to $TMP" -fi - -# Install a file, creating any parent directories -function installp() { - local target="${@: -1}" - mkdir -p "${target%/*}" - install "$@" -} - -# Prefer GNU touch to work around https://apple.stackexchange.com/a/425730/397839 -if command -v gtouch &>/dev/null; then - TOUCH=gtouch -else - TOUCH=touch -fi - -# Like a mythical touch -p -function touchp() { - for arg; do - installp -m644 /dev/null "$arg" - done -} - -# Creates a simple file+directory structure for tests -function make_basic() { - touchp "$1/a" - touchp "$1/b" - touchp "$1/c/d" - touchp "$1/e/f" - mkdir -p "$1/g/h" - mkdir -p "$1/i" - touchp "$1/j/foo" - touchp "$1/k/foo/bar" - touchp "$1/l/foo/bar/baz" - echo baz >"$1/l/foo/bar/baz" -} -make_basic "$TMP/basic" - -# Creates a file+directory structure with various permissions for tests -function make_perms() { - installp -m000 /dev/null "$1/0" - installp -m444 /dev/null "$1/r" - installp -m222 /dev/null "$1/w" - installp -m644 /dev/null "$1/rw" - installp -m555 /dev/null "$1/rx" - installp -m311 /dev/null "$1/wx" - installp -m755 /dev/null "$1/rwx" -} -make_perms "$TMP/perms" - -# Creates a file+directory structure with various symbolic and hard links -function make_links() { - touchp "$1/file" - ln -s file "$1/symlink" - ln "$1/file" "$1/hardlink" - ln -s nowhere "$1/broken" - ln -s symlink/file "$1/notdir" - mkdir -p "$1/deeply/nested/dir" - touchp "$1/deeply/nested/file" - ln -s file "$1/deeply/nested/link" - ln -s nowhere "$1/deeply/nested/broken" - ln -s deeply/nested "$1/skip" -} -make_links "$TMP/links" - -# Creates a file+directory structure with symbolic link loops -function make_loops() { - touchp "$1/file" - ln -s file "$1/symlink" - ln -s nowhere "$1/broken" - ln -s symlink/file "$1/notdir" - ln -s loop "$1/loop" - mkdir -p "$1/deeply/nested/dir" - ln -s ../../deeply "$1/deeply/nested/loop" - ln -s deeply/nested/loop/nested "$1/skip" -} -make_loops "$TMP/loops" - -# Creates a file+directory structure with varying timestamps -function make_times() { - mkdir -p "$1" - $TOUCH -t 199112140000 "$1/a" - $TOUCH -t 199112140001 "$1/b" - $TOUCH -t 199112140002 "$1/c" - ln -s a "$1/l" - $TOUCH -h -t 199112140003 "$1/l" - $TOUCH -t 199112140004 "$1" -} -make_times "$TMP/times" - -# Creates a file+directory structure with various weird file/directory names -function make_weirdnames() { - touchp "$1/-/a" - touchp "$1/(/b" - touchp "$1/(-/c" - touchp "$1/!/d" - touchp "$1/!-/e" - touchp "$1/,/f" - touchp "$1/)/g" - touchp "$1/.../h" - touchp "$1/\\/i" - touchp "$1/ /j" - touchp "$1/[/k" -} -make_weirdnames "$TMP/weirdnames" - -# Creates a very deep directory structure for testing PATH_MAX handling -function make_deep() { - mkdir -p "$1" - - # $name will be 255 characters, aka _XOPEN_NAME_MAX - local name="0123456789ABCDEF" - name="${name}${name}${name}${name}" - name="${name}${name}${name}${name}" - name="${name:0:255}" - - # 4 * 256 - 1 == 1023 - local names="$name/$name/$name/$name" - - for i in {0..9} A B C D E F; do - ( - mkdir "$1/$i" - cd "$1/$i" - - # 4 * 1024 == 4096 == PATH_MAX - for _ in {1..4}; do - mkdir -p "$names" - cd "$names" - done - - $TOUCH "$name" - ) - done -} -make_deep "$TMP/deep" - -# Creates a directory structure with many different types, and therefore colors -function make_rainbow() { - touchp "$1/file.txt" - touchp "$1/file.dat" - touchp "$1/star".{gz,tar,tar.gz} - ln -s file.txt "$1/link.txt" - touchp "$1/mh1" - ln "$1/mh1" "$1/mh2" - mkfifo "$1/pipe" - # TODO: block - ln -s /dev/null "$1/chardev_link" - ln -s nowhere "$1/broken" - "$BIN/tests/mksock" "$1/socket" - touchp "$1"/s{u,g,ug}id - chmod u+s "$1"/su{,g}id - chmod g+s "$1"/s{u,}gid - mkdir "$1/ow" "$1"/sticky{,_ow} - chmod o+w "$1"/*ow - chmod +t "$1"/sticky* - touchp "$1"/exec.sh - chmod +x "$1"/exec.sh -} -make_rainbow "$TMP/rainbow" - -# Creates a scratch directory that tests can modify -function make_scratch() { - mkdir -p "$1" -} -make_scratch "$TMP/scratch" - -# Close stdin so bfs doesn't think we're interactive -exec </dev/null - -if [ "$VERBOSE_COMMANDS" ]; then - # dup stdout for verbose logging even when redirected - exec 3>&1 -fi - -function bfs_verbose() { - if [ "$VERBOSE_COMMANDS" ]; then - if [ -t 3 ]; then - printf "${GRN}%q${RST} " "${BFS[@]}" >&3 - - local expr_started= - for arg; do - if [[ $arg == -[A-Z]* ]]; then - printf "${CYN}%q${RST} " "$arg" >&3 - elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then - expr_started=yes - printf "${RED}%q${RST} " "$arg" >&3 - elif [[ $expr_started && $arg == [\),] ]]; then - printf "${RED}%q${RST} " "$arg" >&3 - elif [[ $arg == -?* ]]; then - expr_started=yes - printf "${BLU}%q${RST} " "$arg" >&3 - elif [ "$expr_started" ]; then - printf "${BLD}%q${RST} " "$arg" >&3 - else - printf "${MAG}%q${RST} " "$arg" >&3 - fi - done - else - printf '%q ' "${BFS[@]}" "$@" >&3 - fi - printf '\n' >&3 - fi -} - -function invoke_bfs() { - bfs_verbose "$@" - "${BFS[@]}" "$@" -} - -# Expect a command to fail, but not crash -function fail() { - "$@" - local STATUS="$?" - - if ((STATUS > 125)); then - exit "$STATUS" - elif ((STATUS > 0)); then - return 0 - else - return 1 - fi -} - -# Detect colored diff support -if [ -t 2 ] && diff --color=always /dev/null /dev/null 2>/dev/null; then - DIFF="diff --color=always" -else - DIFF="diff" -fi - -# Return value when bfs fails -EX_BFS=10 -# Return value when a difference is detected -EX_DIFF=20 -# Return value when a test is skipped -EX_SKIP=77 - -function sort_output() { - sort -o "$OUT" "$OUT" -} - -function diff_output() { - local GOLD="$TESTS/$TEST.out" - - if [ "$UPDATE" ]; then - cp "$OUT" "$GOLD" - else - $DIFF -u "$GOLD" "$OUT" >&2 - fi -} - -function bfs_diff() ( - bfs_verbose "$@" - - # Close the dup()'d stdout to make sure we have enough fd's for the process - # substitution, even with low ulimit -n - exec 3>&- - - "${BFS[@]}" "$@" | sort >"$OUT" - local STATUS="${PIPESTATUS[0]}" - - diff_output || return $EX_DIFF - - if [ "$STATUS" -eq 0 ]; then - return 0 - else - return $EX_BFS - fi -) - -function skip() { - exit $EX_SKIP -} - -function skip_if() { - if "$@"; then - skip - fi -} - -function skip_unless() { - skip_if fail "$@" -} - -function closefrom() { - if [ -d /proc/self/fd ]; then - local fds=/proc/self/fd - else - local fds=/dev/fd - fi - - for fd in "$fds"/*; do - if [ ! -e "$fd" ]; then - continue - fi - - local fd="${fd##*/}" - if [ "$fd" -ge "$1" ]; then - eval "exec ${fd}<&-" - fi - done -} - -function inum() { - ls -id "$@" | awk '{ print $1 }' -} - - -cd "$TMP" -set +e - -# Test cases - -function test_basic() { - bfs_diff basic -} - -function test_type_d() { - bfs_diff basic -type d -} - -function test_type_f() { - bfs_diff basic -type f -} - -function test_type_l() { - bfs_diff links/skip -type l -} - -function test_H_type_l() { - bfs_diff -H links/skip -type l -} - -function test_L_type_l() { - bfs_diff -L links/skip -type l -} - -function test_type_multi() { - bfs_diff links -type f,d,c -} - -function test_mindepth() { - bfs_diff basic -mindepth 1 -} - -function test_maxdepth() { - bfs_diff basic -maxdepth 1 -} - -function test_depth() { - bfs_diff basic -depth -} - -function test_depth_slash() { - bfs_diff basic/ -depth -} - -function test_depth_mindepth_1() { - bfs_diff basic -mindepth 1 -depth -} - -function test_depth_mindepth_2() { - bfs_diff basic -mindepth 2 -depth -} - -function test_depth_maxdepth_1() { - bfs_diff basic -maxdepth 1 -depth -} - -function test_depth_maxdepth_2() { - bfs_diff basic -maxdepth 2 -depth -} - -function test_depth_error() { - rm -rf scratch/* - touchp scratch/foo/bar - chmod a-r scratch/foo - - bfs_diff scratch -depth - local ret=$? - - chmod +r scratch/foo - rm -rf scratch/* - - [ $ret -eq $EX_BFS ] -} - -function test_name() { - bfs_diff basic -name '*f*' -} - -function test_name_root() { - bfs_diff basic/a -name a -} - -function test_name_root_depth() { - bfs_diff basic/g -depth -name g -} - -function test_name_trailing_slash() { - bfs_diff basic/g/ -name g -} - -function test_name_slash() { - bfs_diff / -maxdepth 0 -name / -} - -function test_name_slashes() { - bfs_diff /// -maxdepth 0 -name / -} - -function test_name_star_star() { - bfs_diff basic -name '**f**' -} - -function test_name_character_class() { - bfs_diff basic -name '[e-g][!a-n][!p-z]' -} - -function test_name_bracket() { - # fnmatch() is broken on macOS - skip_if test "$UNAME" = "Darwin" - - # An unclosed [ should be matched literally - bfs_diff weirdnames -name '[' -} - -function test_name_backslash() { - # An unescaped \ doesn't match - bfs_diff weirdnames -name '\' -} - -function test_name_double_backslash() { - # An escaped \\ matches - bfs_diff weirdnames -name '\\' -} - -function test_path() { - bfs_diff basic -path 'basic/*f*' -} - -function test_wholename() { - bfs_diff basic -wholename 'basic/*f*' -} - -function test_true() { - bfs_diff basic -true -} - -function test_false() { - bfs_diff basic -false -} - -function test_executable() { - bfs_diff perms -executable -} - -function test_readable() { - bfs_diff perms -readable -} - -function test_writable() { - bfs_diff perms -writable -} - -function test_empty() { - bfs_diff basic -empty -} - -function test_empty_special() { - bfs_diff rainbow -empty -} - -function test_gid() { - bfs_diff basic -gid "$(id -g)" -} - -function test_gid_plus() { - skip_if test "$(id -g)" -eq 0 - bfs_diff basic -gid +0 -} - -function test_gid_plus_plus() { - skip_if test "$(id -g)" -eq 0 - bfs_diff basic -gid ++0 -} - -function test_gid_minus() { - bfs_diff basic -gid "-$(($(id -g) + 1))" -} - -function test_gid_minus_plus() { - bfs_diff basic -gid "-+$(($(id -g) + 1))" -} - -function test_uid() { - bfs_diff basic -uid "$(id -u)" -} - -function test_uid_plus() { - skip_if test "$(id -u)" -eq 0 - bfs_diff basic -uid +0 -} - -function test_uid_plus_plus() { - skip_if test "$(id -u)" -eq 0 - bfs_diff basic -uid ++0 -} - -function test_uid_minus() { - bfs_diff basic -uid "-$(($(id -u) + 1))" -} - -function test_uid_minus_plus() { - bfs_diff basic -uid "-+$(($(id -u) + 1))" -} - -function test_newer() { - bfs_diff times -newer times/a -} - -function test_newer_link() { - bfs_diff times -newer times/l -} - -function test_anewer() { - bfs_diff times -anewer times/a -} - -function test_asince() { - bfs_diff times -asince 1991-12-14T00:01 -} - -function test_links() { - bfs_diff links -type f -links 2 -} - -function test_links_plus() { - bfs_diff links -type f -links +1 -} - -function test_links_minus() { - bfs_diff links -type f -links -2 -} - -function test_links_noarg() { - fail invoke_bfs links -links -} - -function test_links_empty() { - fail invoke_bfs links -links '' -} - -function test_links_negative() { - fail invoke_bfs links -links +-1 -} - -function test_links_invalid() { - fail invoke_bfs links -links ASDF -} - -function test_P() { - bfs_diff -P links/deeply/nested/dir -} - -function test_P_slash() { - bfs_diff -P links/deeply/nested/dir/ -} - -function test_H() { - bfs_diff -H links/deeply/nested/dir -} - -function test_H_slash() { - bfs_diff -H links/deeply/nested/dir/ -} - -function test_H_broken() { - bfs_diff -H links/broken -} - -function test_H_notdir() { - bfs_diff -H links/notdir -} - -function test_H_newer() { - bfs_diff -H times -newer times/l -} - -function test_H_loops() { - bfs_diff -H loops/deeply/nested/loop -} - -function test_L() { - bfs_diff -L links -} - -function test_L_broken() { - bfs_diff -H links/broken -} - -function test_L_notdir() { - bfs_diff -H links/notdir -} - -function test_L_loops() { - # POSIX says it's okay to either stop or keep going on seeing a filesystem - # loop, as long as a diagnostic is printed - local errors=$(invoke_bfs -L loops 2>&1 >/dev/null) - [ -n "$errors" ] -} - -function test_L_loops_continue() { - bfs_diff -L loops - [ $? -eq $EX_BFS ] -} - -function test_X() { - bfs_diff -X weirdnames - [ $? -eq $EX_BFS ] -} - -function test_follow() { - bfs_diff links -follow -} - -function test_L_depth() { - bfs_diff -L links -depth -} - -function test_samefile() { - bfs_diff links -samefile links/file -} - -function test_samefile_symlink() { - bfs_diff links -samefile links/symlink -} - -function test_H_samefile_symlink() { - bfs_diff -H links -samefile links/symlink -} - -function test_L_samefile_symlink() { - bfs_diff -L links -samefile links/symlink -} - -function test_samefile_broken() { - bfs_diff links -samefile links/broken -} - -function test_H_samefile_broken() { - bfs_diff -H links -samefile links/broken -} - -function test_L_samefile_broken() { - bfs_diff -L links -samefile links/broken -} - -function test_samefile_notdir() { - bfs_diff links -samefile links/notdir -} - -function test_H_samefile_notdir() { - bfs_diff -H links -samefile links/notdir -} - -function test_L_samefile_notdir() { - bfs_diff -L links -samefile links/notdir -} - -function test_xtype_l() { - bfs_diff links -xtype l -} - -function test_xtype_f() { - bfs_diff links -xtype f -} - -function test_L_xtype_l() { - bfs_diff -L links -xtype l -} - -function test_L_xtype_f() { - bfs_diff -L links -xtype f -} - -function test_xtype_multi() { - bfs_diff links -xtype f,d,c -} - -function test_xtype_reorder() { - # 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 -} - -function test_xtype_depth() { - # Make sure -xtype is considered side-effecting for facts_when_impure - fail invoke_bfs loops -xtype l -depth 100 -} - -function test_iname() { - skip_unless invoke_bfs -quit -iname PATTERN - bfs_diff basic -iname '*F*' -} - -function test_ipath() { - skip_unless invoke_bfs -quit -ipath PATTERN - bfs_diff basic -ipath 'basic/*F*' -} - -function test_iwholename() { - skip_unless invoke_bfs -quit -iwholename PATTERN - bfs_diff basic -iwholename 'basic/*F*' -} - -function test_lname() { - bfs_diff links -lname '[aq]' -} - -function test_ilname() { - skip_unless invoke_bfs -quit -ilname PATTERN - bfs_diff links -ilname '[AQ]' -} - -function test_L_lname() { - bfs_diff -L links -lname '[aq]' -} - -function test_L_ilname() { - skip_unless invoke_bfs -quit -ilname PATTERN - bfs_diff -L links -ilname '[AQ]' -} - -function test_user_name() { - bfs_diff basic -user "$(id -un)" -} - -function test_user_id() { - bfs_diff basic -user "$(id -u)" -} - -function test_user_nouser() { - # Regression test: this was wrongly optimized to -false - bfs_diff basic -user "$(id -u)" \! -nouser -} - -function test_group_name() { - bfs_diff basic -group "$(id -gn)" -} - -function test_group_id() { - bfs_diff basic -group "$(id -g)" -} - -function test_group_nogroup() { - # Regression test: this was wrongly optimized to -false - bfs_diff basic -group "$(id -g)" \! -nogroup -} - -function test_daystart() { - bfs_diff basic -daystart -mtime 0 -} - -function test_daystart_twice() { - bfs_diff basic -daystart -daystart -mtime 0 -} - -function test_newerma() { - bfs_diff times -newerma times/a -} - -function test_newermt() { - bfs_diff times -newermt 1991-12-14T00:01 -} - -function test_newermt_epoch_minus_one() { - bfs_diff times -newermt 1969-12-31T23:59:59Z -} - -function test_newermt_invalid() { - fail invoke_bfs times -newermt not_a_date_time -} - -function test_newerma_nonexistent() { - fail invoke_bfs times -newerma basic/nonexistent -} - -function test_newermq() { - fail invoke_bfs times -newermq times/a -} - -function test_newerqm() { - fail invoke_bfs times -newerqm times/a -} - -function test_size() { - bfs_diff basic -type f -size 0 -} - -function test_size_plus() { - bfs_diff basic -type f -size +0 -} - -function test_size_bytes() { - bfs_diff basic -type f -size +0c -} - -function test_size_big() { - bfs_diff basic -size 9223372036854775807 -} - -function test_exec() { - bfs_diff basic -exec echo {} \; -} - -function test_exec_nopath() { - ( - unset PATH - invoke_bfs basic -exec echo {} \; >"$OUT" - ) - - sort_output - diff_output -} - -function test_exec_nothing() { - # Regression test: don't segfault on missing command - fail invoke_bfs basic -exec \; -} - -function test_exec_plus() { - bfs_diff basic -exec "$TESTS/sort-args.sh" {} + -} - -function test_exec_plus_status() { - # -exec ... {} + should always return true, but if the command fails, bfs - # should exit with a non-zero status - bfs_diff basic -exec false {} + -print - (($? == EX_BFS)) -} - -function test_exec_plus_semicolon() { - # POSIX says: - # 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. - bfs_diff basic -exec echo foo {} bar + baz \; -} - -function test_exec_substring() { - bfs_diff basic -exec echo '-{}-' \; -} - -function test_exec_flush() { - # IO streams should be flushed before executing programs - bfs_diff basic -print0 -exec echo found \; -} - -function test_exec_flush_fail() { - # Failure to flush streams before exec should be caught - skip_unless test -e /dev/full - fail invoke_bfs basic -print0 -exec true \; >/dev/full -} - -function test_exec_flush_fprint() { - # Even non-stdstreams should be flushed - bfs_diff basic/a -fprint scratch/foo -exec cat scratch/foo \; -} - -function test_exec_flush_fprint_fail() { - skip_unless test -e /dev/full - fail invoke_bfs basic/a -fprint /dev/full -exec true \; -} - -function test_exec_plus_flush() { - bfs_diff basic/a -print0 -exec echo found {} + -} - -function test_exec_plus_flush_fail() { - skip_unless test -e /dev/full - fail invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full -} - -function test_execdir() { - bfs_diff basic -execdir echo {} \; -} - -function test_execdir_plus() { - local tree=$(invoke_bfs -D tree 2>&1 -quit) - - if [[ "$tree" == *"-S dfs"* ]]; then - skip - fi - - bfs_diff basic -execdir "$TESTS/sort-args.sh" {} + -} - -function test_execdir_substring() { - bfs_diff basic -execdir echo '-{}-' \; -} - -function test_execdir_plus_semicolon() { - bfs_diff basic -execdir echo foo {} bar + baz \; -} - -function test_execdir_pwd() { - local TMP_REAL=$(cd "$TMP" && pwd) - local OFFSET=$((${#TMP_REAL} + 2)) - bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \; -} - -function test_execdir_slash() { - # Don't prepend ./ for absolute paths in -execdir - bfs_diff / -maxdepth 0 -execdir echo {} \; -} - -function test_execdir_slash_pwd() { - bfs_diff / -maxdepth 0 -execdir pwd \; -} - -function test_execdir_slashes() { - bfs_diff /// -maxdepth 0 -execdir echo {} \; -} - -function test_execdir_ulimit() { - rm -rf scratch/* - mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z - mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C - - closefrom 4 - ulimit -n 13 - bfs_diff scratch -execdir echo {} \; -} - -function test_weird_names() { - cd weirdnames - bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \) -} - -function test_flag_weird_names() { - cd weirdnames - bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \) -} - -function test_flag_comma() { - # , is a filename until a non-flag is seen - cd weirdnames - bfs_diff -L ',' -print -} - -function test_follow_comma() { - # , is an operator after a non-flag is seen - cd weirdnames - bfs_diff -follow ',' -print -} - -function test_fprint() { - invoke_bfs basic -fprint "$OUT" - sort_output - diff_output -} - -function test_fprint_duplicate() { - touchp scratch/$TEST.out - ln scratch/$TEST.out scratch/$TEST.hard - ln -s $TEST.out scratch/$TEST.soft - - invoke_bfs basic -fprint scratch/$TEST.out -fprint scratch/$TEST.hard -fprint scratch/$TEST.soft - sort scratch/$TEST.out >"$OUT" - diff_output -} - -function test_fprint_duplicate_stdout() { - invoke_bfs basic -fprint "$OUT" -print >"$OUT" - sort_output - diff_output -} - -function test_fprint_noarg() { - fail invoke_bfs basic -fprint -} - -function test_fprint_nonexistent() { - fail invoke_bfs basic -fprint scratch/nonexistent/path -} - -function test_fprint_truncate() { - printf "basic\nbasic\n" >"$OUT" - - invoke_bfs basic -maxdepth 0 -fprint "$OUT" - sort_output - diff_output -} - -function test_double_dash() { - cd basic - bfs_diff -- . -type f -} - -function test_flag_double_dash() { - cd basic - bfs_diff -L -- . -type f -} - -function test_ignore_readdir_race() { - rm -rf scratch/* - $TOUCH scratch/{foo,bar} - - # -links 1 forces a stat() call, which will fail for the second file - invoke_bfs scratch -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \; -} - -function test_ignore_readdir_race_root() { - # Make sure -ignore_readdir_race doesn't suppress ENOENT at the root - fail invoke_bfs basic/nonexistent -ignore_readdir_race -} - -function test_ignore_readdir_race_notdir() { - # Check -ignore_readdir_race handling when a directory is replaced with a file - rm -rf scratch/* - touchp scratch/foo/bar - - invoke_bfs scratch -mindepth 1 -ignore_readdir_race -execdir rm -r {} \; -execdir $TOUCH {} \; -} - -function test_perm_000() { - bfs_diff perms -perm 000 -} - -function test_perm_000_minus() { - bfs_diff perms -perm -000 -} - -function test_perm_000_slash() { - bfs_diff perms -perm /000 -} - -function test_perm_000_plus() { - bfs_diff perms -perm +000 -} - -function test_perm_222() { - bfs_diff perms -perm 222 -} - -function test_perm_222_minus() { - bfs_diff perms -perm -222 -} - -function test_perm_222_slash() { - bfs_diff perms -perm /222 -} - -function test_perm_222_plus() { - bfs_diff perms -perm +222 -} - -function test_perm_644() { - bfs_diff perms -perm 644 -} - -function test_perm_644_minus() { - bfs_diff perms -perm -644 -} - -function test_perm_644_slash() { - bfs_diff perms -perm /644 -} - -function test_perm_644_plus() { - bfs_diff perms -perm +644 -} - -function test_perm_symbolic() { - bfs_diff perms -perm a+r,u=wX,g+wX-w -} - -function test_perm_symbolic_minus() { - bfs_diff perms -perm -a+r,u=wX,g+wX-w -} - -function test_perm_symbolic_slash() { - bfs_diff perms -perm /a+r,u=wX,g+wX-w -} - -function test_perm_symbolic_trailing_comma() { - fail invoke_bfs perms -perm a+r, -} - -function test_perm_symbolic_double_comma() { - fail invoke_bfs perms -perm a+r,,u+w -} - -function test_perm_symbolic_missing_action() { - fail invoke_bfs perms -perm a -} - -function test_perm_leading_plus_symbolic() { - bfs_diff perms -perm +rwx -} - -function test_perm_leading_plus_symbolic_minus() { - bfs_diff perms -perm -+rwx -} - -function test_perm_leading_plus_symbolic_slash() { - bfs_diff perms -perm /+rwx -} - -function test_permcopy() { - bfs_diff perms -perm u+rw,g+u-w,o=g -} - -function test_perm_setid() { - bfs_diff rainbow -perm -u+s -o -perm -g+s -} - -function test_perm_sticky() { - bfs_diff rainbow -perm -a+t -} - -function test_prune() { - bfs_diff basic -name foo -prune -} - -function test_prune_file() { - bfs_diff basic -print -name '?' -prune -} - -function test_prune_or_print() { - bfs_diff basic -name foo -prune -o -print -} - -function test_not_prune() { - bfs_diff basic \! \( -name foo -prune \) -} - -function test_ok_nothing() { - # Regression test: don't segfault on missing command - fail invoke_bfs basic -ok \; -} - -function test_ok_stdin() { - # -ok should *not* close stdin - # See https://savannah.gnu.org/bugs/?24561 - yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \; -} - -function test_okdir_stdin() { - # -okdir should *not* close stdin - yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \; -} - -function test_ok_plus_semicolon() { - yes | bfs_diff basic -ok echo {} + \; -} - -function test_okdir_plus_semicolon() { - yes | bfs_diff basic -okdir echo {} + \; -} - -function test_delete() { - rm -rf scratch/* - touchp scratch/foo/bar/baz - - # Don't try to delete '.' - (cd scratch && invoke_bfs . -delete) - - bfs_diff scratch -} - -function test_delete_many() { - # Test for https://github.com/tavianator/bfs/issues/67 - - rm -rf scratch/* - mkdir scratch/foo - $TOUCH scratch/foo/{1..256} - - invoke_bfs scratch/foo -delete - bfs_diff scratch -} - -function test_L_delete() { - rm -rf scratch/* - mkdir scratch/foo - mkdir scratch/bar - ln -s ../foo scratch/bar/baz - - # Don't try to rmdir() a symlink - invoke_bfs -L scratch/bar -delete || return 1 - - bfs_diff scratch -} - -function test_rm() { - rm -rf scratch/* - touchp scratch/foo/bar/baz - - (cd scratch && invoke_bfs . -rm) - - bfs_diff scratch -} - -function test_regex() { - bfs_diff basic -regex 'basic/./.' -} - -function test_iregex() { - bfs_diff basic -iregex 'basic/[A-Z]/[a-z]' -} - -function test_regex_parens() { - cd weirdnames - bfs_diff . -regex '\./\((\)' -} - -function test_regex_error() { - fail invoke_bfs basic -regex '[' -} - -function test_regex_invalid_utf8() { - rm -rf scratch/* - - # Incomplete UTF-8 sequences - skip_unless touch scratch/$'\xC3' - skip_unless touch scratch/$'\xE2\x84' - skip_unless touch scratch/$'\xF0\x9F\x92' - - bfs_diff scratch -regex 'scratch/..' -} - -function test_E() { - cd weirdnames - bfs_diff -E . -regex '\./(\()' -} - -function test_regextype_posix_basic() { - cd weirdnames - bfs_diff -regextype posix-basic -regex '\./\((\)' -} - -function test_regextype_posix_extended() { - cd weirdnames - bfs_diff -regextype posix-extended -regex '\./(\()' -} - -function test_regextype_ed() { - cd weirdnames - bfs_diff -regextype ed -regex '\./\((\)' -} - -function test_regextype_emacs() { - skip_unless invoke_bfs -regextype emacs -quit - - bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)' -} - -function test_regextype_grep() { - skip_unless invoke_bfs -regextype grep -quit - - bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?' -} - -function test_regextype_sed() { - cd weirdnames - bfs_diff -regextype sed -regex '\./\((\)' -} - -function test_d_path() { - bfs_diff -d basic -} - -function test_path_d() { - bfs_diff basic -d -} - -function test_f() { - cd weirdnames - bfs_diff -f '-' -f '(' -} - -function test_s() { - invoke_bfs -s weirdnames -maxdepth 1 >"$OUT" - diff_output -} - -function test_hidden() { - bfs_diff weirdnames -hidden -} - -function test_hidden_root() { - cd weirdnames - bfs_diff . ./. ... ./... .../.. -hidden -} - -function test_nohidden() { - bfs_diff weirdnames -nohidden -} - -function test_nohidden_depth() { - bfs_diff weirdnames -depth -nohidden -} - -function test_depth_n() { - bfs_diff basic -depth 2 -} - -function test_depth_n_plus() { - bfs_diff basic -depth +2 -} - -function test_depth_n_minus() { - bfs_diff basic -depth -2 -} - -function test_depth_depth_n() { - bfs_diff basic -depth -depth 2 -} - -function test_depth_depth_n_plus() { - bfs_diff basic -depth -depth +2 -} - -function test_depth_depth_n_minus() { - bfs_diff basic -depth -depth -2 -} - -function test_depth_overflow() { - bfs_diff basic -depth -4294967296 -} - -function test_gid_name() { - bfs_diff basic -gid "$(id -gn)" -} - -function test_uid_name() { - bfs_diff basic -uid "$(id -un)" -} - -function test_mnewer() { - bfs_diff times -mnewer times/a -} - -function test_H_mnewer() { - bfs_diff -H times -mnewer times/l -} - -function test_msince() { - bfs_diff times -msince 1991-12-14T00:01 -} - -function test_mtime_units() { - bfs_diff times -mtime +500w400d300h200m100s -} - -function test_size_T() { - bfs_diff basic -type f -size 1T -} - -function test_quit() { - bfs_diff basic/g -print -name g -quit -} - -function test_quit_child() { - bfs_diff basic/g -print -name h -quit -} - -function test_quit_depth() { - bfs_diff basic/g -depth -print -name g -quit -} - -function test_quit_depth_child() { - bfs_diff basic/g -depth -print -name h -quit -} - -function test_quit_after_print() { - bfs_diff basic basic -print -quit -} - -function test_quit_before_print() { - bfs_diff basic basic -quit -print -} - -function test_quit_implicit_print() { - bfs_diff basic -name basic -o -quit -} - -function test_inum() { - bfs_diff basic -inum "$(inum basic/k/foo/bar)" -} - -function test_nogroup() { - bfs_diff basic -nogroup -} - -function test_nogroup_ulimit() { - closefrom 4 - ulimit -n 16 - bfs_diff deep -nogroup -} - -function test_nouser() { - bfs_diff basic -nouser -} - -function test_nouser_ulimit() { - closefrom 4 - ulimit -n 16 - bfs_diff deep -nouser -} - -function test_ls() { - invoke_bfs rainbow -ls >scratch/test_ls.out -} - -function test_L_ls() { - invoke_bfs -L rainbow -ls >scratch/test_L_ls.out -} - -function test_fls() { - invoke_bfs rainbow -fls scratch/test_fls.out -} - -function test_printf() { - bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n' -} - -function test_printf_empty() { - bfs_diff basic -printf '' -} - -function test_printf_slash() { - bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n' -} - -function test_printf_slashes() { - bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n' -} - -function test_printf_trailing_slash() { - bfs_diff basic/ -printf '(%h)/(%f)\n' -} - -function test_printf_trailing_slashes() { - bfs_diff basic/// -printf '(%h)/(%f)\n' -} - -function test_printf_flags() { - bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n' -} - -function test_printf_types() { - bfs_diff loops -printf '(%p) (%l) %y %Y\n' -} - -function test_printf_escapes() { - bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo' -} - -function test_printf_times() { - bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %T@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n' -} - -function test_printf_leak() { - # Memory leak regression test - bfs_diff basic -maxdepth 0 -printf '%p' -} - -function test_printf_nul() { - # NUL byte regression test - bfs_diff basic -maxdepth 0 -printf '%h\0%f\n' -} - -function test_printf_w() { - # Birth times may not be supported, so just check that %w/%W/%B can be parsed - bfs_diff times -false -printf '%w %WY %BY\n' -} - -function test_printf_Y_error() { - rm -rf scratch/* - mkdir scratch/foo - chmod -x scratch/foo - ln -s foo/bar scratch/bar - - bfs_diff scratch -printf '(%p) (%l) %y %Y\n' - local ret=$? - - chmod +x scratch/foo - rm -rf scratch/* - - [ $ret -eq $EX_BFS ] -} - -function test_printf_H() { - bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n' -} - -function test_printf_u_g_ulimit() { - closefrom 4 - ulimit -n 16 - [ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ] -} - -function test_printf_l_nonlink() { - bfs_diff links -printf '| %26p -> %-26l |\n' -} - -function test_printf_incomplete_escape() { - fail invoke_bfs basic -printf '\' -} - -function test_printf_invalid_escape() { - fail invoke_bfs basic -printf '\!' -} - -function test_printf_incomplete_format() { - fail invoke_bfs basic -printf '%' -} - -function test_printf_invalid_format() { - fail invoke_bfs basic -printf '%!' -} - -function test_printf_duplicate_flag() { - fail invoke_bfs basic -printf '%--p' -} - -function test_printf_must_be_numeric() { - fail invoke_bfs basic -printf '%+p' -} - -function test_printf_color() { - bfs_diff -color -path './rainbow*' -printf '%H %h %f %p %P %l\n' -} - -function test_printf_everything() { - local everything=(%{a,b,c,d,D,f,F,g,G,h,H,i,k,l,m,M,n,p,P,s,S,t,u,U,y,Y}) - everything+=(%{A,C,T}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z}) - - # Check if we have birth times - if ! fail invoke_bfs basic -printf '%w' -quit >/dev/null; then - everything+=(%w %{B,W}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z}) - fi - - invoke_bfs rainbow -printf "${everything[*]}\n" >/dev/null -} - -function test_fprintf() { - invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n' - sort_output - diff_output -} - -function test_fprintf_nofile() { - fail invoke_bfs basic -fprintf -} - -function test_fprintf_noformat() { - fail invoke_bfs basic -fprintf /dev/null -} - -function test_fstype() { - fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') - bfs_diff basic -fstype "$fstype" -} - -function test_path_flag_expr() { - bfs_diff links/skip -H -type l -} - -function test_path_expr_flag() { - bfs_diff links/skip -type l -H -} - -function test_flag_expr_path() { - bfs_diff -H -type l links/skip -} - -function test_expr_flag_path() { - bfs_diff -type l -H links/skip -} - -function test_expr_path_flag() { - bfs_diff -type l links/skip -H -} - -function test_parens() { - bfs_diff basic \( -name '*f*' \) -} - -function test_bang() { - bfs_diff basic \! -name foo -} - -function test_not() { - bfs_diff basic -not -name foo -} - -function test_implicit_and() { - bfs_diff basic -name foo -type d -} - -function test_a() { - bfs_diff basic -name foo -a -type d -} - -function test_and() { - bfs_diff basic -name foo -and -type d -} - -function test_o() { - bfs_diff basic -name foo -o -type d -} - -function test_or() { - bfs_diff basic -name foo -or -type d -} - -function test_comma() { - bfs_diff basic -name '*f*' -print , -print -} - -function test_precedence() { - bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print -} - -function test_incomplete() { - fail invoke_bfs basic \( -} - -function test_missing_paren() { - fail invoke_bfs basic \( -print -} - -function test_extra_paren() { - fail invoke_bfs basic -print \) -} - -function test_color() { - bfs_diff rainbow -color -} - -function test_color_L() { - bfs_diff -L rainbow -color -} - -function test_color_rs_lc_rc_ec() { - LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color -} - -function test_color_escapes() { - LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color -} - -function test_color_nul() { - LS_COLORS="ec=\33[m\0:" bfs_diff rainbow -color -maxdepth 0 -} - -function test_color_ln_target() { - LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color -} - -function test_color_L_ln_target() { - LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color -} - -function test_color_mh() { - LS_COLORS="mh=01:" bfs_diff rainbow -color -} - -function test_color_mh0() { - LS_COLORS="mh=00:" bfs_diff rainbow -color -} - -function test_color_or() { - LS_COLORS="or=01:" bfs_diff rainbow -color -} - -function test_color_mi() { - LS_COLORS="mi=01:" bfs_diff rainbow -color -} - -function test_color_or_mi() { - LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color -} - -function test_color_or_mi0() { - LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color -} - -function test_color_or0_mi() { - LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color -} - -function test_color_or0_mi0() { - LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color -} - -function test_color_su_sg0() { - LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color -} - -function test_color_su0_sg() { - LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color -} - -function test_color_su0_sg0() { - LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color -} - -function test_color_st_tw_ow0() { - LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color -} - -function test_color_st_tw0_ow() { - LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color -} - -function test_color_st_tw0_ow0() { - LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color -} - -function test_color_st0_tw_ow() { - LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color -} - -function test_color_st0_tw_ow0() { - LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color -} - -function test_color_st0_tw0_ow() { - LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color -} - -function test_color_st0_tw0_ow0() { - LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color -} - -function test_color_ext() { - LS_COLORS="*.txt=01:" bfs_diff rainbow -color -} - -function test_color_ext0() { - LS_COLORS="*.txt=00:" bfs_diff rainbow -color -} - -function test_color_ext_override() { - LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color -} - -function test_color_ext_underride() { - LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color -} - -function test_color_missing_colon() { - LS_COLORS="*.txt=01" bfs_diff rainbow -color -} - -function test_color_no_stat() { - LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color -} - -function test_color_L_no_stat() { - LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color -} - -function test_color_star() { - # Regression test: don't segfault on LS_COLORS="*" - LS_COLORS="*" bfs_diff rainbow -color -} - -function test_color_ls() { - rm -rf scratch/* - touchp scratch/foo/bar/baz - ln -s foo/bar/baz scratch/link - ln -s foo/bar/nowhere scratch/broken - ln -s foo/bar/nowhere/nothing scratch/nested - ln -s foo/bar/baz/qux scratch/notdir - ln -s scratch/foo/bar scratch/relative - mkdir scratch/__bfs__ - ln -s /__bfs__/nowhere scratch/absolute - - LS_COLORS="or=01;31:" invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \ - | sed 's/.* -> //' \ - | sort >"$OUT" - - diff_output -} - -function test_deep() { - closefrom 4 - - ulimit -n 16 - bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; -} - -function test_deep_strict() { - closefrom 4 - - # Not even enough fds to keep the root open - ulimit -n 7 - bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; -} - -function test_exit() { - invoke_bfs basic -name foo -exit 42 - if [ $? -ne 42 ]; then - return 1 - fi - - invoke_bfs basic -name qux -exit 42 - if [ $? -ne 0 ]; then - return 1 - fi - - bfs_diff basic/g -print -name g -exit -} - -function test_exit_no_implicit_print() { - bfs_diff basic -not -name foo -o -exit -} - -function test_printx() { - bfs_diff weirdnames -printx -} - -function test_and_purity() { - # Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure - bfs_diff basic -name nonexistent \( -print , -false \) -} - -function test_or_purity() { - # Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure - bfs_diff basic -name '*' -o -print -} - -function test_double_negation() { - bfs_diff basic \! \! -name 'foo' -} - -function test_not_reachability() { - bfs_diff basic -print \! -quit -print -} - -function test_comma_reachability() { - bfs_diff basic -print -quit , -print -} - -function test_de_morgan_not() { - bfs_diff basic \! \( -name 'foo' -o \! -type f \) -} - -function test_de_morgan_and() { - bfs_diff basic \( \! -name 'foo' -a \! -type f \) -} - -function test_de_morgan_or() { - bfs_diff basic \( \! -name 'foo' -o \! -type f \) -} - -function test_and_false_or_true() { - # Test (-a lhs(always_true) -false) <==> (! lhs), - # (-o lhs(always_false) -true) <==> (! lhs) - bfs_diff basic -prune -false -o -true -} - -function test_comma_redundant_true() { - # Test (, lhs(always_true) -true) <==> lhs - bfs_diff basic -prune , -true -} - -function test_comma_redundant_false() { - # Test (, lhs(always_false) -false) <==> lhs - bfs_diff basic -print -not -prune , -false -} - -function test_data_flow_depth() { - bfs_diff basic -depth +1 -depth -4 -} - -function test_data_flow_group() { - bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \) -} - -function test_data_flow_user() { - bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \) -} - -function test_data_flow_hidden() { - bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \) -} - -function test_data_flow_sparse() { - bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \) -} - -function test_data_flow_type() { - bfs_diff basic \! \( -type f -o \! -type f \) -} - -function test_data_flow_and_swap() { - bfs_diff basic \! -type f -a -type d -} - -function test_data_flow_or_swap() { - bfs_diff basic \! \( -type f -o \! -type d \) -} - -function test_print_error() { - skip_unless test -e /dev/full - fail invoke_bfs basic -maxdepth 0 >/dev/full -} - -function test_fprint_error() { - skip_unless test -e /dev/full - fail invoke_bfs basic -maxdepth 0 -fprint /dev/full -} - -function test_fprint_noerror() { - # Regression test: /dev/full should not fail until actually written to - skip_unless test -e /dev/full - invoke_bfs basic -false -fprint /dev/full -} - -function test_fprint_error_stdout() { - skip_unless test -e /dev/full - fail invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full -} - -function test_fprint_error_stderr() { - skip_unless test -e /dev/full - fail invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full -} - -function test_print0() { - bfs_diff basic/a basic/b -print0 -} - -function test_fprint0() { - invoke_bfs basic/a basic/b -fprint0 "$OUT" - diff_output -} - -function test_closed_stdin() { - bfs_diff basic <&- -} - -function test_ok_closed_stdin() { - bfs_diff basic -ok echo \; <&- -} - -function test_okdir_closed_stdin() { - bfs_diff basic -okdir echo {} \; <&- -} - -function test_closed_stdout() { - fail invoke_bfs basic >&- -} - -function test_closed_stderr() { - fail invoke_bfs basic >&- 2>&- -} - -function test_unique() { - bfs_diff links/{file,symlink,hardlink} -unique -} - -function test_unique_depth() { - bfs_diff basic -unique -depth -} - -function test_L_unique() { - bfs_diff -L links/{file,symlink,hardlink} -unique -} - -function test_L_unique_loops() { - bfs_diff -L loops/deeply/nested -unique -} - -function test_L_unique_depth() { - bfs_diff -L loops/deeply/nested -unique -depth -} - -function test_mount() { - skip_unless test "$SUDO" - skip_if test "$UNAME" = "Darwin" - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - sudo mount -t tmpfs tmpfs scratch/mnt - $TOUCH scratch/foo/bar scratch/mnt/baz - - bfs_diff scratch -mount - local ret=$? - - sudo umount scratch/mnt - return $ret -} - -function test_L_mount() { - skip_unless test "$SUDO" - skip_if test "$UNAME" = "Darwin" - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - sudo mount -t tmpfs tmpfs scratch/mnt - ln -s ../mnt scratch/foo/bar - $TOUCH scratch/mnt/baz - ln -s ../mnt/baz scratch/foo/qux - - bfs_diff -L scratch -mount - local ret=$? - - sudo umount scratch/mnt - return $ret -} - -function test_xdev() { - skip_unless test "$SUDO" - skip_if test "$UNAME" = "Darwin" - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - sudo mount -t tmpfs tmpfs scratch/mnt - $TOUCH scratch/foo/bar scratch/mnt/baz - - bfs_diff scratch -xdev - local ret=$? - - sudo umount scratch/mnt - return $ret -} - -function test_L_xdev() { - skip_unless test "$SUDO" - skip_if test "$UNAME" = "Darwin" - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - sudo mount -t tmpfs tmpfs scratch/mnt - ln -s ../mnt scratch/foo/bar - $TOUCH scratch/mnt/baz - ln -s ../mnt/baz scratch/foo/qux - - bfs_diff -L scratch -xdev - local ret=$? - - sudo umount scratch/mnt - return $ret -} - -function test_inum_mount() { - skip_unless test "$SUDO" - skip_if test "$UNAME" = "Darwin" - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - sudo mount -t tmpfs tmpfs scratch/mnt - - bfs_diff scratch -inum "$(inum scratch/mnt)" - local ret=$? - - sudo umount scratch/mnt - return $ret -} - -function test_inum_bind_mount() { - skip_unless test "$SUDO" - skip_unless test "$UNAME" = "Linux" - - rm -rf scratch/* - $TOUCH scratch/{foo,bar} - sudo mount --bind scratch/{foo,bar} - - bfs_diff scratch -inum "$(inum scratch/bar)" - local ret=$? - - sudo umount scratch/bar - return $ret -} - -function test_type_bind_mount() { - skip_unless test "$SUDO" - skip_unless test "$UNAME" = "Linux" - - rm -rf scratch/* - $TOUCH scratch/{file,null} - sudo mount --bind /dev/null scratch/null - - bfs_diff scratch -type c - local ret=$? - - sudo umount scratch/null - return $ret -} - -function test_xtype_bind_mount() { - skip_unless test "$SUDO" - skip_unless test "$UNAME" = "Linux" - - rm -rf scratch/* - $TOUCH scratch/{file,null} - sudo mount --bind /dev/null scratch/null - ln -s /dev/null scratch/link - - bfs_diff -L scratch -type c - local ret=$? - - sudo umount scratch/null - return $ret -} - -function test_inum_automount() { - # bfs shouldn't trigger automounts unless it descends into them - - skip_unless test "$SUDO" - skip_unless command -v systemd-mount &>/dev/null - - rm -rf scratch/* - mkdir scratch/{foo,mnt} - skip_unless sudo systemd-mount -A -o bind basic scratch/mnt - - local before=$(inum scratch/mnt) - bfs_diff scratch -inum "$before" -prune - local ret=$? - local after=$(inum scratch/mnt) - - sudo systemd-umount scratch/mnt - - ((ret == 0 && before == after)) -} - -function set_acl() { - case "$UNAME" in - Darwin) - chmod +a "$(id -un) allow read,write" "$1" - ;; - FreeBSD) - if [ "$(getconf ACL_NFS4 "$1")" -gt 0 ]; then - setfacl -m "u:$(id -un):rw::allow" "$1" - else - setfacl -m "u:$(id -un):rw" "$1" - fi - ;; - *) - setfacl -m "u:$(id -un):rw" "$1" - ;; - esac -} - -function test_acl() { - rm -rf scratch/* - - skip_unless invoke_bfs scratch -quit -acl - - $TOUCH scratch/{normal,acl} - skip_unless set_acl scratch/acl - ln -s acl scratch/link - - bfs_diff scratch -acl -} - -function test_L_acl() { - rm -rf scratch/* - - skip_unless invoke_bfs scratch -quit -acl - - $TOUCH scratch/{normal,acl} - skip_unless set_acl scratch/acl - ln -s acl scratch/link - - bfs_diff -L scratch -acl -} - -function test_capable() { - skip_unless test "$SUDO" - skip_unless test "$UNAME" = "Linux" - - rm -rf scratch/* - - skip_unless invoke_bfs scratch -quit -capable - - $TOUCH scratch/{normal,capable} - sudo setcap all+ep scratch/capable - ln -s capable scratch/link - - bfs_diff scratch -capable -} - -function test_L_capable() { - skip_unless test "$SUDO" - skip_unless test "$UNAME" = "Linux" - - rm -rf scratch/* - - skip_unless invoke_bfs scratch -quit -capable - - $TOUCH scratch/{normal,capable} - sudo setcap all+ep scratch/capable - ln -s capable scratch/link - - bfs_diff -L scratch -capable -} - -function make_xattrs() { - rm -rf scratch/* - - $TOUCH scratch/{normal,xattr,xattr_2} - ln -s xattr scratch/link - ln -s normal scratch/xattr_link - - case "$UNAME" in - Darwin) - xattr -w bfs_test true scratch/xattr \ - && xattr -w bfs_test_2 true scratch/xattr_2 \ - && xattr -s -w bfs_test true scratch/xattr_link - ;; - FreeBSD) - setextattr user bfs_test true scratch/xattr \ - && setextattr user bfs_test_2 true scratch/xattr_2 \ - && setextattr -h user bfs_test true scratch/xattr_link - ;; - *) - # Linux tmpfs doesn't support the user.* namespace, so we use the security.* - # namespace, which is writable by root and readable by others - [ "$SUDO" ] \ - && sudo setfattr -n security.bfs_test scratch/xattr \ - && sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \ - && sudo setfattr -h -n security.bfs_test scratch/xattr_link - ;; - esac -} - -function test_xattr() { - skip_unless invoke_bfs scratch -quit -xattr - skip_unless make_xattrs - bfs_diff scratch -xattr -} - -function test_L_xattr() { - skip_unless invoke_bfs scratch -quit -xattr - skip_unless make_xattrs - bfs_diff -L scratch -xattr -} - -function test_xattrname() { - skip_unless invoke_bfs scratch -quit -xattr - skip_unless make_xattrs - - case "$UNAME" in - Darwin|FreeBSD) - bfs_diff scratch -xattrname bfs_test - ;; - *) - bfs_diff scratch -xattrname security.bfs_test - ;; - esac -} - -function test_L_xattrname() { - skip_unless invoke_bfs scratch -quit -xattr - skip_unless make_xattrs - - case "$UNAME" in - Darwin|FreeBSD) - bfs_diff -L scratch -xattrname bfs_test - ;; - *) - bfs_diff -L scratch -xattrname security.bfs_test - ;; - esac -} - -function test_help() { - invoke_bfs -help | grep -E '\{...?\}' && return 1 - invoke_bfs -D help | grep -E '\{...?\}' && return 1 - invoke_bfs -S help | grep -E '\{...?\}' && return 1 - invoke_bfs -regextype help | grep -E '\{...?\}' && return 1 - - return 0 -} - -function test_version() { - invoke_bfs -version >/dev/null -} - -function test_warn() { - local stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null) - [ -n "$stderr" ] -} - -function test_nowarn() { - local stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null) - [ -z "$stderr" ] -} - -function test_typo() { - invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null -} - -function test_D_multi() { - bfs_diff -D opt,tree,unknown basic -} - -function test_D_all() { - bfs_diff -D all basic -} - -function test_O0() { - bfs_diff -O0 basic -not \( -type f -not -type f \) -} - -function test_O1() { - bfs_diff -O1 basic -not \( -type f -not -type f \) -} - -function test_O2() { - bfs_diff -O2 basic -not \( -type f -not -type f \) -} - -function test_O3() { - bfs_diff -O3 basic -not \( -type f -not -type f \) -} - -function test_Ofast() { - bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \) -} - -function test_S() { - invoke_bfs -S "$1" -s basic >"$OUT" - diff_output -} - -function test_S_bfs() { - test_S bfs -} - -function test_S_dfs() { - test_S dfs -} - -function test_S_ids() { - test_S ids -} - -function test_exclude_name() { - bfs_diff basic -exclude -name foo -} - -function test_exclude_depth() { - bfs_diff basic -depth -exclude -name foo -} - -function test_exclude_mindepth() { - bfs_diff basic -mindepth 3 -exclude -name foo -} - -function test_exclude_print() { - fail invoke_bfs basic -exclude -print -} - -function test_exclude_exclude() { - fail invoke_bfs basic -exclude -exclude -name foo -} - -function test_flags() { - skip_unless invoke_bfs scratch -quit -flags offline - - rm -rf scratch/* - - $TOUCH scratch/{foo,bar} - skip_unless chflags offline scratch/bar - - bfs_diff scratch -flags -offline,nohidden -} - -function test_files0_from_file() { - cd weirdnames - invoke_bfs -mindepth 1 -fprintf ../scratch/files0.in "%P\0" - bfs_diff -files0-from ../scratch/files0.in -} - -function test_files0_from_stdin() { - cd weirdnames - invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -} - -function test_files0_from_none() { - printf "" | fail invoke_bfs -files0-from - -} - -function test_files0_from_empty() { - printf "\0" | fail invoke_bfs -files0-from - -} - -function test_files0_from_nowhere() { - fail invoke_bfs -files0-from -} - -function test_files0_from_nothing() { - fail invoke_bfs -files0-from basic/nonexistent -} - -function test_files0_from_ok() { - printf "basic\0" | fail invoke_bfs -files0-from - -ok echo {} \; -} - -function test_stderr_fails_silently() { - skip_unless test -e /dev/full - bfs_diff -D all basic 2>/dev/full -} - -function test_stderr_fails_loudly() { - skip_unless test -e /dev/full - fail invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full -} - -function test_unexpected_operator() { - fail invoke_bfs \! -o -print -} - -function test_and_incomplete() { - fail invoke_bfs -print -a -} - -function test_or_incomplete() { - fail invoke_bfs -print -o -} - -function test_comma_incomplete() { - fail invoke_bfs -print , -} - -BOL='\n' -EOL='\n' - -function update_eol() { - # Put the cursor at the last column, then write a space so the next - # character will wrap - EOL="\\033[${COLUMNS}G " -} - - -if [ "$VERBOSE_TESTS" ]; then - BOL='' -elif [ -t 1 ]; then - BOL='\r\033[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 -fi - -passed=0 -failed=0 -skipped=0 - -for TEST in "${enabled_tests[@]}"; do - if [[ -t 1 || "$VERBOSE_TESTS" ]]; then - printf "${BOL}${YLW}%s${RST}${EOL}" "$TEST" - else - printf "." - fi - - OUT="$TMP/$TEST.out" - - if [ "$VERBOSE_ERRORS" ]; then - ("$TEST") - else - ("$TEST") 2>"$TMP/stderr" - fi - status=$? - - if ((status == 0)); then - ((++passed)) - elif ((status == EX_SKIP)); then - ((++skipped)) - if [ "$VERBOSE_SKIPPED" ]; then - printf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST" - fi - else - ((++failed)) - [ "$VERBOSE_ERRORS" ] || cat "$TMP/stderr" >&2 - printf "${BOL}${RED}%s failed!${RST}\n" "$TEST" - [ "$STOP" ] && break - fi -done - -printf "${BOL}" - -if ((passed > 0)); then - printf "${GRN}tests passed: %d${RST}\n" "$passed" -fi -if ((skipped > 0)); then - printf "${CYN}tests skipped: %s${RST}\n" "$skipped" -fi -if ((failed > 0)); then - printf "${RED}tests failed: %s${RST}\n" "$failed" - exit 1 -fi +TESTS="$(dirname -- "${BASH_SOURCE[0]}")" +. "$TESTS/util.sh" +. "$TESTS/color.sh" +. "$TESTS/stddirs.sh" +. "$TESTS/getopts.sh" +. "$TESTS/run.sh" + +stdenv +drop_root "$@" +parse_args "$@" +make_stddirs +run_tests diff --git a/tests/trie.c b/tests/trie.c index 0158fd8..59bde40 100644 --- a/tests/trie.c +++ b/tests/trie.c @@ -1,27 +1,16 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -#undef NDEBUG - -#include "../src/trie.h" -#include <assert.h> +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.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", @@ -31,9 +20,11 @@ const char *keys[] = { "quuuux", "pre", - "pref", "prefi", + "pref", "prefix", + "p", + "pRefix", "AAAA", "AADD", @@ -48,16 +39,20 @@ const char *keys[] = { ">>>>>>", ">>><<<", ">>>", + + "AAAAAAA", + "AAAAAAAB", + "AAAAAAAa", }; -const size_t nkeys = sizeof(keys) / sizeof(keys[0]); +static const size_t nkeys = countof(keys); -int main(void) { +void check_trie(void) { struct trie trie; trie_init(&trie); for (size_t i = 0; i < nkeys; ++i) { - assert(!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) { @@ -70,27 +65,39 @@ int main(void) { struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]); if (prefix) { - assert(leaf); - assert(strcmp(prefix, leaf->key) == 0); + bfs_verify(leaf); + bfs_check(strcmp(prefix, leaf->key) == 0); } else { - assert(!leaf); + bfs_check(!leaf); } leaf = trie_insert_str(&trie, keys[i]); - assert(leaf); - assert(strcmp(keys[i], leaf->key) == 0); - assert(leaf->length == strlen(keys[i]) + 1); + bfs_verify(leaf); + bfs_check(strcmp(keys[i], leaf->key) == 0); + bfs_check(leaf->length == strlen(keys[i]) + 1); + } + + { + size_t i = 0; + for_trie (leaf, &trie) { + 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; + } + bfs_check(i == nkeys); } for (size_t i = 0; i < nkeys; ++i) { struct trie_leaf *leaf = trie_find_str(&trie, keys[i]); - assert(leaf); - assert(strcmp(keys[i], leaf->key) == 0); - assert(leaf->length == strlen(keys[i]) + 1); + bfs_verify(leaf); + 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]); - assert(!leaf); + bfs_check(!leaf); const char *postfix = NULL; for (size_t j = i + 1; j < nkeys; ++j) { @@ -103,31 +110,34 @@ int main(void) { leaf = trie_find_postfix(&trie, keys[i]); if (postfix) { - assert(leaf); - assert(strcmp(postfix, leaf->key) == 0); + bfs_verify(leaf); + bfs_check(strcmp(postfix, leaf->key) == 0); } else { - assert(!leaf); + bfs_check(!leaf); } } + for_trie (leaf, &trie) { + bfs_check(false, "trie should be empty"); + } + // This tests the "jump" node handling on 32-bit platforms size_t longsize = 1 << 20; char *longstr = malloc(longsize); - assert(longstr); + bfs_verify(longstr); memset(longstr, 0xAC, longsize); - assert(!trie_find_mem(&trie, longstr, longsize)); - assert(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); - assert(!trie_find_mem(&trie, longstr, longsize)); - assert(trie_insert_mem(&trie, longstr, longsize)); + memset(longstr + longsize / 2, 0xAB, longsize / 2); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); - memset(longstr, 0xAA, longsize/2); - assert(!trie_find_mem(&trie, longstr, longsize)); - assert(trie_insert_mem(&trie, longstr, longsize)); + memset(longstr, 0xAA, longsize / 2); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); free(longstr); trie_destroy(&trie); - return EXIT_SUCCESS; } diff --git a/tests/util.sh b/tests/util.sh new file mode 100644 index 0000000..1718a1a --- /dev/null +++ b/tests/util.sh @@ -0,0 +1,217 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Utility functions + +# Portable realpath(1) +_realpath() ( + cd "$(dirname -- "$1")" + echo "$PWD/$(basename -- "$1")" +) + +# Globals +ROOT=$(_realpath "$(dirname -- "$TESTS")") +TESTS="$ROOT/tests" +BIN="$ROOT/bin" +MKSOCK="$BIN/tests/mksock" +PTYX="$BIN/tests/ptyx" +XTOUCH="$BIN/tests/xtouch" +UNAME=$(uname) + +# Standardize the environment +stdenv() { + export LC_ALL=C + export TZ=UTC0 + + local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0" + export ASAN_OPTIONS="$SAN_OPTIONS" + export LSAN_OPTIONS="$SAN_OPTIONS" + export MSAN_OPTIONS="$SAN_OPTIONS" + export TSAN_OPTIONS="$SAN_OPTIONS" + export UBSAN_OPTIONS="$SAN_OPTIONS" + + export LS_COLORS="" + unset BFS_COLORS + + if [ "$UNAME" = Darwin ]; then + # ASan on macOS likes to report + # + # malloc: nano zone abandoned due to inability to preallocate reserved vm space. + # + # to syslog, which as a side effect opens a socket which might take the + # place of one of the standard streams if the process is launched with + # it closed. This environment variable avoids the message. + export MallocNanoZone=0 + fi + + # Count the inherited FDs + if [ -d /proc/self/fd ]; then + local fds=/proc/self/fd + else + local fds=/dev/fd + fi + # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting + # internal bash fds that are not exposed to spawned processes + NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l) + NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3)) + + # 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 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 + if [ -n "${BFS_TRIED_DROP:-}" ]; then + color cat >&2 <<EOF +${RED}error:${RST} Failed to drop capabilities. +EOF + + exit 1 + fi + + color cat >&2 <<EOF +${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and +${BLD}cap_dac_read_search${RST}. + +EOF + + BFS_TRIED_DROP=y exec capsh \ + --drop=cap_dac_override,cap_dac_read_search \ + --caps=cap_dac_override,cap_dac_read_search-eip \ + -- "$0" "$@" + fi + elif ((EUID == 0)); then + UNLESS= + if [ "$UNAME" = "Linux" ]; then + UNLESS=" unless ${GRN}capsh${RST} is installed" + fi + + color cat >&2 <<EOF +${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore +will not work when run as ${BLD}$(id -un)${RST}${UNLESS}. +EOF + exit 1 + fi +} + +## Debugging + +# Get the bash call stack +callers() { + local frame=0 + while caller $frame; do + ((++frame)) + done +} + +# Print a message including path, line number, and command +debug() { + local file="$1" + local line="$2" + local msg="$3" + local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || : + file="${file/#*\/tests\//tests/}" + + color printf "${BLD}%s:%d:${RST} %s\n %s\n" "$file" "$line" "$msg" "$cmd" +} + +## Deferred cleanup + +# Quote a command safely for eval +quote() { + printf '%q' "$1" + shift + if (($# > 0)); then + printf ' %q' "$@" + fi +} + +DEFER_LEVEL=-1 + +# Run a command when this (sub)shell exits +defer() { + # Check if the EXIT trap is already set + if ((DEFER_LEVEL != BASH_SUBSHELL)); then + DEFER_LEVEL=$BASH_SUBSHELL + DEFER_CMDS=() + DEFER_LINES=() + DEFER_FILES=() + trap pop_defers EXIT + fi + + DEFER_CMDS+=("$(quote "$@")") + + local line file + read -r line file < <(caller) + DEFER_LINES+=("$line") + DEFER_FILES+=("$file") +} + +# Pop a single command from the defer stack and run it +pop_defer() { + local cmd="${DEFER_CMDS[-1]}" + local file="${DEFER_FILES[-1]}" + local line="${DEFER_LINES[-1]}" + unset "DEFER_CMDS[-1]" + unset "DEFER_FILES[-1]" + unset "DEFER_LINES[-1]" + + local ret=0 + eval "$cmd" || ret=$? + + if ((ret != 0)); then + debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR + fi + + return $ret +} + +# Run all deferred commands +pop_defers() { + local ret=0 + + while ((${#DEFER_CMDS[@]} > 0)); do + pop_defer || ret=$? + done + + 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 new file mode 100644 index 0000000..6864192 --- /dev/null +++ b/tests/xspawn.c @@ -0,0 +1,220 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "alloc.h" +#include "bfstd.h" +#include "dstring.h" +#include "xspawn.h" + +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> + +/** Duplicate the current environment. */ +static char **envdup(void) { + extern char **environ; + + char **envp = NULL; + size_t envc = 0; + + for (char **var = environ; ; ++var) { + char *copy = NULL; + if (*var) { + copy = strdup(*var); + if (!copy) { + goto fail; + } + } + + char **dest = RESERVE(char *, &envp, &envc); + if (!dest) { + free(copy); + goto fail; + } + *dest = copy; + + if (!*var) { + break; + } + } + + return envp; + +fail: + for (size_t i = 0; i < envc; ++i) { + free(envp[i]); + } + free(envp); + return NULL; +} + +/** Add an entry to $PATH. */ +static int add_path(const char *entry, char **old_path) { + int ret = -1; + const char *new_path = NULL; + + *old_path = getenv("PATH"); + if (*old_path) { + *old_path = strdup(*old_path); + if (!*old_path) { + goto done; + } + + new_path = dstrprintf("%s:%s", entry, *old_path); + if (!new_path) { + goto done; + } + } else { + new_path = entry; + } + + ret = setenv("PATH", new_path, true); + +done: + if (new_path && new_path != entry) { + dstrfree((dchar *)new_path); + } + + if (ret != 0) { + free(*old_path); + *old_path = NULL; + } + + return ret; +} + +/** Undo add_path(). */ +static int reset_path(char *old_path) { + int ret; + + if (old_path) { + ret = setenv("PATH", old_path, true); + free(old_path); + } else { + ret = unsetenv("PATH"); + } + + return ret; +} + +/** 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; + bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid) + && bfs_check(WIFEXITED(wstatus)); + if (exited) { + int wexit = WEXITSTATUS(wstatus); + bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); + } +} + +/** 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; + } + + 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); +destroy: + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); +} + +/** Check path resolution of non-existent executables. */ +static void check_enoent(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; + } + + char *argv[] = {"eW6f5RM9Qi", NULL}; + pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL); + bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()"); + + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); +} + +static void check_resolve(void) { + char *exe; + + exe = bfs_spawn_resolve("sh"); + bfs_echeck(exe, "bfs_spawn_resolve('sh')"); + free(exe); + + exe = bfs_spawn_resolve("/bin/sh"); + bfs_echeck(exe && strcmp(exe, "/bin/sh") == 0); + free(exe); + + exe = bfs_spawn_resolve("bin/tests/xspawnee"); + bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); + free(exe); + + bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); + + bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT); + + 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); + } +} + +void check_xspawn(void) { + check_use_path(true); + check_use_path(false); + + check_enoent(true); + check_enoent(false); + + check_resolve(); +} diff --git a/tests/xspawnee.c b/tests/xspawnee.c new file mode 100644 index 0000000..b0a76ca --- /dev/null +++ b/tests/xspawnee.c @@ -0,0 +1,17 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdlib.h> +#include <string.h> + +/** Child binary for bfs_spawn() tests. */ +int main(int argc, char *argv[]) { + if (argc >= 2) { + const char *path = getenv("PATH"); + if (!path || strcmp(path, argv[1]) != 0) { + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} diff --git a/tests/xtime.c b/tests/xtime.c new file mode 100644 index 0000000..c890a1e --- /dev/null +++ b/tests/xtime.c @@ -0,0 +1,187 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "bfs.h" +#include "diag.h" +#include "xtime.h" + +#include <errno.h> +#include <limits.h> +#include <stdint.h> +#include <time.h> + +static bool tm_equal(const struct tm *tma, const struct tm *tmb) { + return tma->tm_year == tmb->tm_year + && tma->tm_mon == tmb->tm_mon + && tma->tm_mday == tmb->tm_mday + && tma->tm_hour == tmb->tm_hour + && tma->tm_min == tmb->tm_min + && tma->tm_sec == tmb->tm_sec + && tma->tm_wday == tmb->tm_wday + && tma->tm_yday == tmb->tm_yday + && tma->tm_isdst == tmb->tm_isdst; +} + +/** Check one xgetdate() result. */ +static bool check_one_xgetdate(const char *str, int error, time_t expected) { + struct timespec ts; + int ret = xgetdate(str, &ts); + + if (error) { + return bfs_echeck(ret == -1 && errno == error, "xgetdate('%s')", str); + } else { + 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); + } +} + +/** xgetdate() tests. */ +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)" + +#define TM_PRINTF(tm) \ + (1900 + (tm).tm_year), (tm).tm_mon, (tm).tm_mday, \ + (tm).tm_hour, (tm).tm_min, (tm).tm_sec, \ + ((tm).tm_wday + 1), ((tm).tm_yday + 1), \ + ((tm).tm_isdst ? ((tm).tm_isdst < 0 ? ", DST?" : ", DST") : "") + +/** Check one xmktime() result. */ +static bool check_one_xmktime(time_t expected) { + struct tm tm; + if (!localtime_r(&expected, &tm)) { + bfs_ediag("localtime_r(%jd)", (intmax_t)expected); + return false; + } + + time_t actual; + 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 void check_xmktime(void) { + for (time_t time = -10; time <= 10; ++time) { + check_one_xmktime(time); + } + + // Attempt to trigger overflow (but don't test for it, since it's not mandatory) + struct tm tm = { + .tm_year = INT_MAX, + .tm_mon = INT_MAX, + .tm_mday = INT_MAX, + .tm_hour = INT_MAX, + .tm_min = INT_MAX, + .tm_sec = INT_MAX, + .tm_isdst = -1, + }; + time_t time; + xmktime(&tm, &time); +} + +/** Check one xtimegm() result. */ +static void check_one_xtimegm(const struct tm *tm) { + struct tm tma = *tm, tmb = *tm; + time_t ta, tb; + ta = mktime(&tma); + if (xtimegm(&tmb, &tb) != 0) { + tb = -1; + } + + 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 (!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)); + } +} + +#if !BFS_HAS_TIMEGM +/** Check an overflowing xtimegm() call. */ +static void check_xtimegm_overflow(const struct tm *tm) { + struct tm copy = *tm; + time_t 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 (!pass) { + bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy)); + bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); + } +} +#endif + +/** xtimegm() tests. */ +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) + for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4) + 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) { + check_one_xtimegm(&tm); + } + + // Check integer overflow cases + check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX }); +#endif // !BFS_HAS_TIMEGM +} + +void check_xtime(void) { + check_xgetdate(); + check_xmktime(); + check_xtimegm(); +} diff --git a/tests/xtimegm.c b/tests/xtimegm.c deleted file mode 100644 index d774b9e..0000000 --- a/tests/xtimegm.c +++ /dev/null @@ -1,107 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -#include "../src/xtime.h" -#include <stdbool.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <time.h> - -static bool tm_equal(const struct tm *tma, const struct tm *tmb) { - if (tma->tm_year != tmb->tm_year) { - return false; - } - if (tma->tm_mon != tmb->tm_mon) { - return false; - } - if (tma->tm_mday != tmb->tm_mday) { - return false; - } - if (tma->tm_hour != tmb->tm_hour) { - return false; - } - if (tma->tm_min != tmb->tm_min) { - return false; - } - if (tma->tm_sec != tmb->tm_sec) { - return false; - } - if (tma->tm_wday != tmb->tm_wday) { - return false; - } - if (tma->tm_yday != tmb->tm_yday) { - return false; - } - if (tma->tm_isdst != tmb->tm_isdst) { - return false; - } - - return true; -} - -static void tm_print(FILE *file, const struct tm *tm) { - fprintf(file, "Y%d M%d D%d h%d m%d s%d wd%d yd%d%s\n", - tm->tm_year, tm->tm_mon, tm->tm_mday, - tm->tm_hour, tm->tm_min, tm->tm_sec, - tm->tm_wday, tm->tm_yday, - tm->tm_isdst ? (tm->tm_isdst < 0 ? " (DST?)" : " (DST)") : ""); -} - -int main(void) { - if (setenv("TZ", "UTC0", true) != 0) { - perror("setenv()"); - return EXIT_FAILURE; - } - - struct tm tm = { - .tm_isdst = -1, - }; - - 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) - for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4) - 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) { - struct tm tma = tm, tmb = tm; - time_t ta, tb; - ta = mktime(&tma); - if (xtimegm(&tmb, &tb) != 0) { - tb = -1; - } - - bool fail = false; - if (ta != tb) { - printf("Mismatch: %jd != %jd\n", (intmax_t)ta, (intmax_t)tb); - fail = true; - } - if (ta != -1 && !tm_equal(&tma, &tmb)) { - printf("mktime(): "); - tm_print(stdout, &tma); - printf("xtimegm(): "); - tm_print(stdout, &tmb); - fail = true; - } - if (fail) { - printf("Input: "); - tm_print(stdout, &tm); - return EXIT_FAILURE; - } - } - - return EXIT_SUCCESS; -} diff --git a/tests/xtouch.c b/tests/xtouch.c new file mode 100644 index 0000000..f33c573 --- /dev/null +++ b/tests/xtouch.c @@ -0,0 +1,279 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "bfstd.h" +#include "sanity.h" +#include "xtime.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +/** Parsed xtouch arguments. */ +struct args { + /** Simple flags. */ + enum { + /** Don't create nonexistent files (-c). */ + NO_CREATE = 1 << 0, + /** Don't follow symlinks (-h). */ + NO_FOLLOW = 1 << 1, + /** Create any missing parent directories (-p). */ + CREATE_PARENTS = 1 << 2, + } flags; + + /** Timestamps (-r|-t|-d). */ + struct timespec times[2]; + + /** File creation mode (-M; default 0666 & ~umask). */ + mode_t fmode; + /** Directory creation mode (-M; default 0777 & ~umask). */ + mode_t dmode; + /** Parent directory creation mode (0777 & ~umask). */ + mode_t pmode; +}; + +/** Open (and maybe create) a single directory. */ +static int open_dir(const struct args *args, int dfd, const char *path) { + int ret = openat(dfd, path, O_SEARCH | O_DIRECTORY); + + if (ret < 0 && errno == ENOENT && (args->flags & CREATE_PARENTS)) { + if (mkdirat(dfd, path, args->pmode) == 0 || errno == EEXIST) { + ret = openat(dfd, path, O_SEARCH | O_DIRECTORY); + } + } + + return ret; +} + +/** Open (and maybe create) the parent directory of the path. */ +static int open_parent(const struct args *args, const char **path) { + size_t max = xbaseoff(*path); + if (max == 0) { + return AT_FDCWD; + } + + char *dir = strndup(*path, max); + if (!dir) { + return -1; + } + + // Optimistically try the whole path first + int dfd = open_dir(args, AT_FDCWD, dir); + if (dfd >= 0) { + goto done; + } + + if (errno == ENOENT) { + if (!(args->flags & CREATE_PARENTS)) { + goto err; + } + } else if (!errno_is_like(ENAMETOOLONG)) { + goto err; + } + + // Open the parents one-at-a-time + dfd = AT_FDCWD; + char *cur = dir; + while (*cur) { + char *next = cur; + next += strcspn(next, "/"); + next += strspn(next, "/"); + + char c = *next; + *next = '\0'; + + int parent = dfd; + dfd = open_dir(args, parent, cur); + if (parent >= 0) { + close_quietly(parent); + } + if (dfd < 0) { + goto err; + } + + *next = c; + cur = next; + } + +done: + *path += max; +err: + free(dir); + return dfd; +} + +/** Compute flags for fstatat()/utimensat(). */ +static int at_flags(const struct args *args) { + if (args->flags & NO_FOLLOW) { + return AT_SYMLINK_NOFOLLOW; + } else { + return 0; + } +} + +/** Touch one path. */ +static int xtouch(const struct args *args, const char *path) { + int dfd = open_parent(args, &path); + if (dfd < 0 && dfd != (int)AT_FDCWD) { + return -1; + } + + int ret = utimensat(dfd, path, args->times, at_flags(args)); + if (ret == 0 || errno != ENOENT) { + goto done; + } + + if (args->flags & NO_CREATE) { + ret = 0; + goto done; + } + + size_t len = strlen(path); + if (len > 0 && path[len - 1] == '/') { + if (mkdirat(dfd, path, args->dmode) == 0) { + ret = utimensat(dfd, path, args->times, at_flags(args)); + } + } else { + int fd = openat(dfd, path, O_WRONLY | O_CREAT, args->fmode); + if (fd >= 0) { + if (futimens(fd, args->times) == 0) { + ret = xclose(fd); + } else { + close_quietly(fd); + } + } + } + +done: + if (dfd >= 0) { + close_quietly(dfd); + } + return ret; +} + +int main(int argc, char *argv[]) { + tzset(); + + mode_t mask = umask(0); + + struct args args = { + .flags = 0, + .times = { + { .tv_nsec = UTIME_OMIT }, + { .tv_nsec = UTIME_OMIT }, + }, + .fmode = 0666 & ~mask, + .dmode = 0777 & ~mask, + .pmode = 0777 & ~mask, + }; + + bool atime = false, mtime = false; + const char *darg = NULL; + const char *marg = NULL; + const char *rarg = NULL; + + const char *cmd = argc > 0 ? argv[0] : "xtouch"; + int c; + while (c = getopt(argc, argv, ":M:acd:hmpr:t:"), c != -1) { + switch (c) { + case 'M': + marg = optarg; + break; + case 'a': + atime = true; + break; + case 'c': + args.flags |= NO_CREATE; + break; + case 'd': + case 't': + darg = optarg; + break; + case 'h': + args.flags |= NO_FOLLOW; + break; + case 'm': + mtime = true; + break; + case 'p': + args.flags |= CREATE_PARENTS; + break; + case 'r': + rarg = optarg; + 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 (marg) { + 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); + return EXIT_FAILURE; + } + } + + struct timespec times[2]; + + if (rarg) { + struct stat buf; + if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) { + fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno)); + return EXIT_FAILURE; + } + 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)); + return EXIT_FAILURE; + } + times[1] = times[0]; + } else { + // Don't use UTIME_NOW, so that multiple paths all get the same timestamp + if (clock_gettime(CLOCK_REALTIME, ×[0]) != 0) { + perror("clock_gettime()"); + return EXIT_FAILURE; + } + times[1] = times[0]; + } + + if (!atime && !mtime) { + atime = true; + mtime = true; + } + if (atime) { + args.times[0] = times[0]; + } + if (mtime) { + args.times[1] = times[1]; + } + + if (optind >= argc) { + fprintf(stderr, "%s: No files to touch\n", cmd); + return EXIT_FAILURE; + } + + int ret = EXIT_SUCCESS; + for (; optind < argc; ++optind) { + const char *path = argv[optind]; + if (xtouch(&args, path) != 0) { + fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno)); + ret = EXIT_FAILURE; + } + } + return ret; +} |