diff options
author | Tavian Barnes <tavianator@tavianator.com> | 2024-05-07 15:42:46 -0400 |
---|---|---|
committer | Tavian Barnes <tavianator@tavianator.com> | 2024-05-07 15:42:46 -0400 |
commit | 452d6697e0f92326ab139eed4eadd9c2fd8b55ca (patch) | |
tree | 0feeb3722dcf6debb6c33c5175342bf1d70a1dba | |
parent | a4299f9bc1d3e60a7e628561e8d650c2a241e1c2 (diff) | |
parent | c5cf2cf90834f2f56b2940d2a499a1a614ebfd21 (diff) | |
download | bfs-find2fd.tar.xz |
Merge branch 'main' into find2fdfind2fd
448 files changed, 20127 insertions, 8239 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml new file mode 100644 index 0000000..6ff8337 --- /dev/null +++ b/.github/codeql.yml @@ -0,0 +1,9 @@ +query-filters: + - exclude: + id: cpp/commented-out-code + - exclude: + id: cpp/long-switch + - exclude: + id: cpp/loop-variable-changed + - exclude: + id: cpp/poorly-documented-function diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9db363d..1d4196e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,82 +6,222 @@ jobs: linux: name: Linux - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 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 \ + expect \ 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 + # Work around https://github.com/actions/runner-images/issues/9491 + sudo sysctl vm.mmap_rnd_bits=28 - name: Run tests run: | make -j$(nproc) distcheck + - uses: actions/upload-artifact@v4 + with: + name: linux-config.log + path: distcheck-*/gen/config.log + macos: name: macOS - runs-on: macos-latest + runs-on: macos-14 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install coreutils + brew install \ + bash \ + expect - name: Run tests run: | - make -j$(sysctl -n hw.ncpu) distcheck + jobs=$(sysctl -n hw.ncpu) + make -j$jobs distcheck freebsd: name: FreeBSD - if: ${{ github.repository_owner == 'tavianator' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + runs-on: ubuntu-22.04 - runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - concurrency: spurion + - name: Run tests + uses: cross-platform-actions/action@v0.24.0 + with: + operating_system: freebsd + version: "14.0" + + run: | + sudo pkg install -y \ + bash \ + expect \ + oniguruma \ + pkgconf \ + tcl-wrapper + sudo mount -t fdescfs none /dev/fd + make -j$(nproc) distcheck + + - uses: actions/upload-artifact@v4 + with: + name: freebsd-config.log + path: distcheck-*/gen/config.log + + openbsd: + name: OpenBSD + + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: tailscale/github-action@main + - name: Run tests + uses: cross-platform-actions/action@v0.24.0 with: - authkey: ${{ secrets.TAILSCALE_KEY }} + operating_system: openbsd + version: "7.5" + + run: | + sudo pkg_add \ + bash \ + expect \ + gmake \ + oniguruma + jobs=$(sysctl -n hw.ncpu) + ./configure MAKE=gmake + gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped" + + - uses: actions/upload-artifact@v4 + with: + name: openbsd-config.log + path: gen/config.log - - 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-22.04 + + 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.24.0 + with: + operating_system: netbsd + version: "10.0" + + run: | + PATH="/sbin:/usr/sbin:$PATH" + sudo pkgin -y install \ + bash \ + oniguruma \ + pkgconf \ + tcl-expect + jobs=$(sysctl -n hw.ncpu) + ./configure + make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped" + + - uses: actions/upload-artifact@v4 + with: + name: netbsd-config.log + path: gen/config.log + + dragonflybsd: + name: DragonFly BSD + + runs-on: ubuntu-22.04 + + 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 \ + expect \ + oniguruma \ + pkgconf \ + sudo \ + tcl-wrapper + pw useradd -n action -m -G wheel -s /usr/local/bin/bash + echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers + + run: | + chown -R action:action . + jobs=$(sysctl -n hw.ncpu) + sudo -u action ./configure + sudo -u action make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped" + + - uses: actions/upload-artifact@v4 + with: + name: dragonfly-config.log + path: gen/config.log + + omnios: + name: OmniOS + + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Run tests + uses: vmactions/omnios-vm@v1 + with: + release: "r151048" + usesh: true + + prepare: | + pkg install \ + bash \ + build-essential \ + expect \ + 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 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 783cc43..6aaace6 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -4,30 +4,32 @@ on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y \ + expect \ gcc \ acl \ libacl1-dev \ attr \ - libattr1-dev \ libcap2-bin \ libcap-dev \ - libonig-dev + libonig-dev \ + liburing-dev - name: Generate coverage run: | - make -j$(nproc) gcov check TEST_FLAGS="--sudo" + ./configure --enable-gcov + make -j$(nproc) check TEST_FLAGS="--sudo" gcov -abcfpu obj/*/*.o - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a0b8fe3 --- /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-22.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-2023 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,319 +1,231 @@ -############################################################################ -# 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.2 -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_USE_SYS_ACL_H=0 -endif - -ifdef WITH_ATTR -LOCAL_LDLIBS += -lattr -else -LOCAL_CPPFLAGS += -DBFS_USE_SYS_XATTR_H=0 -endif - -ifdef WITH_LIBCAP -LOCAL_LDLIBS += -lcap -else -LOCAL_CPPFLAGS += -DBFS_USE_SYS_CAPABILITY_H=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)/find2fd +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# To build bfs, run +# +# $ ./configure +# $ make + +# Utilities and GNU/BSD portability +include build/prelude.mk + +# The default build target +default: bfs +.PHONY: default + +# Include the generated build config, if it exists +-include gen/config.mk + +## Configuration phase (`./configure`) + +# bfs used to have flag-like targets (`make release`, `make asan ubsan`, etc.). +# Direct users to the new configuration system. +asan lsan msan tsan ubsan gcov lint release:: + @printf 'error: `%s %s` is no longer supported. Use `./configure --enable-%s` instead.\n' \ + "${MAKE}" $@ $@ >&2 + @false + +# Print an error if `make` is run before `./configure` +gen/config.mk:: + @if ! [ -e $@ ]; then \ + printf 'error: You must run `./configure` before `%s`.\n' "${MAKE}" >&2; \ + false; \ + fi + +## Build phase (`make`) + +# The main binary +bfs: bin/find2fd .PHONY: bfs -all: \ - $(BIN)/find2fd \ - $(BIN)/tests/mksock \ - $(BIN)/tests/trie \ - $(BIN)/tests/xtimegm \ - $(BIN)/tests/xtouch -.PHONY: all +# All binaries +BINS := \ + bin/find2fd \ + bin/tests/mksock \ + bin/tests/units \ + bin/tests/xspawnee \ + bin/tests/xtouch -$(BIN)/find2fd: \ - $(OBJ)/src/bar.o \ - $(OBJ)/src/bfstd.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/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)/tests/xtouch: $(OBJ)/src/xtime.o $(OBJ)/tests/xtouch.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 $(BIN)/tests/xtouch - ./tests/tests.sh --bfs="$(BIN)/bfs -S $*" $(TEST_FLAGS) - -check-trie check-xtimegm: check-%: $(BIN)/tests/% - $< +all: ${BINS} +.PHONY: all +# The main binary +bin/find2fd: ${LIBBFS} obj/src/main.o + +${BINS}: + @${MKDIR} ${@D} + +${MSG} "[ LD ] $@" ${CC} ${CFLAGS} ${LDFLAGS} ${.ALLSRC} ${LDLIBS} -o $@ + ${POSTLINK} + +# Get the .c file for a .o file +CSRC = ${@:obj/%.o=%.c} + +# Rebuild when the configuration changes +${OBJS}: gen/config.mk + @${MKDIR} ${@D} + ${MSG} "[ CC ] ${CSRC}" ${CC} ${CPPFLAGS} ${CFLAGS} -c ${CSRC} -o $@ + +# Save the version number to this file, but only update version.c if it changes +gen/version.c.new:: + @${MKDIR} ${@D} + @printf 'const char bfs_version[] = "' >$@ + @if [ "$$VERSION" ]; then \ + printf '%s' "$$VERSION"; \ + elif test -e src/../.git && command -v git >/dev/null 2>&1; then \ + git -C src/.. describe --always --dirty; \ + else \ + echo "3.2"; \ + fi | tr -d '\n' >>$@ + @printf '";\n' >>$@ + +gen/version.c: gen/version.c.new + @test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@ + +obj/gen/version.o: gen/version.c + +## Test phase (`make check`) + +# Unit test binaries +UTEST_BINS := \ + bin/tests/units \ + bin/tests/xspawnee + +# Integration test binaries +ITEST_BINS := \ + bin/tests/mksock \ + bin/tests/xtouch + +# Build (but don't run) test binaries +tests: ${UTEST_BINS} ${ITEST_BINS} +.PHONY: tests + +# Run all the tests +check: unit-tests integration-tests +.PHONY: check + +# Run the unit tests +unit-tests: ${UTEST_BINS} + ${MSG} "[TEST] tests/units" bin/tests/units +.PHONY: unit-tests + +bin/tests/units: \ + ${UNIT_OBJS} \ + ${LIBBFS} + +bin/tests/xspawnee: \ + obj/tests/xspawnee.o + +# The different flag combinations we check +INTEGRATIONS := default dfs ids eds j1 j2 j3 s +INTEGRATION_TESTS := ${INTEGRATIONS:%=check-%} + +# Check just `bfs` +check-default: bin/find2fd ${ITEST_BINS} + +${MSG} "[TEST] bfs" \ + ./tests/tests.sh --make="${MAKE}" --bfs="bin/find2fd" ${TEST_FLAGS} + +# Check the different search strategies +check-dfs check-ids check-eds: bin/find2fd ${ITEST_BINS} + +${MSG} "[TEST] bfs -S ${@:check-%=%}" \ + ./tests/tests.sh --make="${MAKE}" --bfs="bin/find2fd -S ${@:check-%=%}" ${TEST_FLAGS} + +# Check various flags +check-j1 check-j2 check-j3 check-s: bin/find2fd ${ITEST_BINS} + +${MSG} "[TEST] bfs -${@:check-%=%}" \ + ./tests/tests.sh --make="${MAKE}" --bfs="bin/find2fd -${@:check-%=%}" ${TEST_FLAGS} + +# Run the integration tests +integration-tests: ${INTEGRATION_TESTS} +.PHONY: integration-tests + +bin/tests/mksock: \ + obj/tests/mksock.o \ + ${LIBBFS} + +bin/tests/xtouch: \ + obj/tests/xtouch.o \ + ${LIBBFS} + +# `make distcheck` configurations +DISTCHECKS := \ + distcheck-asan \ + distcheck-msan \ + distcheck-tsan \ + distcheck-m32 \ + distcheck-release + +# Test multiple configurations distcheck: - +$(MAKE) -B asan ubsan check $(DISTCHECK_FLAGS) -ifneq ($(OS),Darwin) - +$(MAKE) -B msan ubsan 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 + @+${MAKE} distcheck-tsan + @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32 + @+${MAKE} distcheck-release .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} $@ + @+cd $@ \ + && ../configure ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \ + && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped" + +## Packaging (`make install`) + +DEST_PREFIX := ${DESTDIR}${PREFIX} +DEST_MANDIR := ${DESTDIR}${MANDIR} + +install:: + ${Q}${MKDIR} ${DEST_PREFIX}/bin + ${MSG} "[INST] bin/bfs" \ + ${INSTALL} -m755 bin/bfs ${DEST_PREFIX}/bin/bfs + ${Q}${MKDIR} ${DEST_MANDIR}/man1 + ${MSG} "[INST] man/man1/bfs.1" \ + ${INSTALL} -m644 docs/bfs.1 ${DEST_MANDIR}/man1/bfs.1 + ${Q}${MKDIR} ${DEST_PREFIX}/share/bash-completion/completions + ${MSG} "[INST] completions/bfs.bash" \ + ${INSTALL} -m644 completions/bfs.bash ${DEST_PREFIX}/share/bash-completion/completions/bfs + ${Q}${MKDIR} ${DEST_PREFIX}/share/zsh/site-functions + ${MSG} "[INST] completions/bfs.zsh" \ + ${INSTALL} -m644 completions/bfs.zsh ${DEST_PREFIX}/share/zsh/site-functions/_bfs + ${Q}${MKDIR} ${DEST_PREFIX}/share/fish/vendor_completions.d + ${MSG} "[INST] completions/bfs.fish" \ + ${INSTALL} -m644 completions/bfs.fish ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish + +uninstall:: + ${MSG} "[ RM ] completions/bfs.bash" \ + ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs + ${MSG} "[ RM ] completions/bfs.zsh" \ + ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs + ${MSG} "[ RM ] completions/bfs.fish" \ + ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish + ${MSG} "[ RM ] man/man1/bfs.1" \ + ${RM} ${DEST_MANDIR}/man1/bfs.1 + ${MSG} "[ RM ] bin/bfs" \ + ${RM} ${DEST_PREFIX}/bin/bfs + +# Check that `make install` works and `make uninstall` removes everything +check-install:: + +${MAKE} install DESTDIR=pkg + +${MAKE} uninstall DESTDIR=pkg + bin/bfs pkg -not -type d -print -exit 1 + ${RM} -r pkg + +## Cleanup (`make clean`) + +# Clean all build products +clean:: + ${MSG} "[ RM ] bin obj" \ + ${RM} -r bin obj + +# Clean everything, including generated files +distclean: clean + ${MSG} "[ RM ] gen" \ + ${RM} -r gen ${DISTCHECKS} +.PHONY: distclean @@ -1,22 +1,21 @@ <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> +</h1> -***Breadth-first search for your files.*** - -**[Features] • [Installation] • [Usage] • [Building] • [Hacking] • [Changelog]** +**[Features] • [Installation] • [Usage] • [Building] • [Contributing] • [Changelog]** [Features]: #features [Installation]: #installation [Usage]: /docs/USAGE.md [Building]: /docs/BUILDING.md -[Hacking]: /docs/HACKING.md +[Contributing]: /docs/CONTRIBUTING.md [Changelog]: /docs/CHANGELOG.md <picture> @@ -71,34 +70,44 @@ 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> @@ -154,21 +163,22 @@ haystack/needle 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> @@ -204,12 +214,19 @@ Installation </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 @@ -217,21 +234,48 @@ Installation <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"> -<strong><a href="https://github.com/tavianator/homebrew-tap/blob/master/Formula/bfs.rb">Homebrew</a></strong> -$ brew install tavianator/tap/bfs +<pre> +<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong> +# pkg install bfs + +<strong><a href="https://openports.pl/path/sysutils/bfs">OpenBSD</a></strong> +# pkg_add bfs </pre> + +</td> +</tr> +</tbody> +</table> </details> <details> @@ -249,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. @@ -289,6 +333,7 @@ Once you have the dependencies, you can build <code>bfs</code>. Download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs). Then run + $ ./configure $ make This will build the `./bin/bfs` binary. @@ -298,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..cf1ae49 --- /dev/null +++ b/bench/bench.sh @@ -0,0 +1,715 @@ +#!/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) + +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 ' --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=() + + 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#*=}" + ;; + --default) + COMPLETE=("${COMPLETE_DEFAULT[@]}") + EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}") + STAT=("${STAT_DEFAULT[@]}") + PRINT=("${PRINT_DEFAULT[@]}") + STRATEGIES=("${STRATEGIES_DEFAULT[@]}") + JOBS=("${JOBS_DEFAULT[@]}") + EXEC=("${EXEC_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[@]}"; 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 + + # $SETUP_DIR contains `:` so it won't work in $PATH + # Work around this with a symlink + tmp=$(as-user mktemp) + as-user ln -sf "$bin" "$tmp" + defer rm "$tmp" + export PATH="$tmp:$PATH" + 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 + + 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 quiting 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 +} + +# 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 + + bench-complete "${COMPLETE[@]}" + bench-early-quit "${EARLY_QUIT[@]}" + bench-stat "${STAT[@]}" + bench-print "${PRINT[@]}" + bench-strategies "${STRATEGIES[@]}" + bench-jobs "${JOBS[@]}" + bench-exec "${EXEC[@]}" + 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/build/cc.sh b/build/cc.sh new file mode 100755 index 0000000..45d51ca --- /dev/null +++ b/build/cc.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Run the compiler and check if it succeeded + +set -eu + +TMP=$(mktemp) +trap 'rm -f "$TMP"' EXIT + +( + set -x + $XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS -o "$TMP" +) diff --git a/build/config.mk b/build/config.mk new file mode 100644 index 0000000..24873ec --- /dev/null +++ b/build/config.mk @@ -0,0 +1,62 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that implements `./configure` + +include build/prelude.mk +include build/exports.mk + +# All configuration steps +config: gen/config.mk gen/config.h +.PHONY: config + +# Makefile fragments generated by `./configure` +MKS := \ + gen/vars.mk \ + gen/flags.mk \ + gen/deps.mk \ + gen/pkgs.mk + +# The main configuration file, which includes the others +gen/config.mk: ${MKS} + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @printf 'include %s\n' ${.ALLSRC} >>$@ + ${VCAT} gen/config.mk +.PHONY: gen/config.mk + +# Saves the configurable variables +gen/vars.mk:: + @${MKDIR} ${@D} + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @printf 'PREFIX := %s\n' "$$XPREFIX" >>$@ + @printf 'MANDIR := %s\n' "$$XMANDIR" >>$@ + @printf 'OS := %s\n' "$${OS:-$$(uname)}" >>$@ + @printf 'CC := %s\n' "$$XCC" >>$@ + @printf 'INSTALL := %s\n' "$$XINSTALL" >>$@ + @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@ + @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@ + @printf 'RM := %s\n' "$$XRM" >>$@ + ${VCAT} $@ + +# Sets the build flags. This depends on vars.mk and uses a recursive make so +# that the default flags can depend on variables like ${OS}. +gen/flags.mk: gen/vars.mk + @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/flags.mk $@ +.PHONY: gen/flags.mk + +# Check for dependency generation support +gen/deps.mk: gen/flags.mk + @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/deps.mk $@ +.PHONY: gen/deps.mk + +# Auto-detect dependencies and their build flags +gen/pkgs.mk: gen/flags.mk + @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/pkgs.mk $@ +.PHONY: gen/pkgs.mk + +# Compile-time feature detection +gen/config.h: gen/config.mk + @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@ +.PHONY: gen/config.h diff --git a/build/define-if.sh b/build/define-if.sh new file mode 100755 index 0000000..295ead8 --- /dev/null +++ b/build/define-if.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Output a C preprocessor definition based on whether a command succeeds + +set -eu + +SLUG="${1#build/}" +SLUG="${SLUG%.c}" +MACRO="BFS_$(printf '%s' "$SLUG" | tr '/a-z-' '_A-Z_')" +shift + +if "$@"; then + printf '#define %s true\n' "$MACRO" +else + printf '#define %s false\n' "$MACRO" + exit 1 +fi diff --git a/build/deps.mk b/build/deps.mk new file mode 100644 index 0000000..3db62b6 --- /dev/null +++ b/build/deps.mk @@ -0,0 +1,18 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that generates gen/deps.mk + +include build/prelude.mk +include gen/vars.mk +include gen/flags.mk +include build/exports.mk + +gen/deps.mk:: + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @if build/cc.sh -MD -MP -MF /dev/null build/empty.c; then \ + printf 'CPPFLAGS += -MD -MP\n'; \ + fi >>$@ 2>$@.log + ${VCAT} $@ + @printf -- '-include %s\n' ${OBJS:.o=.d} >>$@ diff --git a/build/empty.c b/build/empty.c new file mode 100644 index 0000000..4fa9a5b --- /dev/null +++ b/build/empty.c @@ -0,0 +1,6 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +int main(void) { + return 0; +} diff --git a/build/exports.mk b/build/exports.mk new file mode 100644 index 0000000..ed19134 --- /dev/null +++ b/build/exports.mk @@ -0,0 +1,20 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile fragment that exports variables used by configuration scripts + +export XPREFIX=${PREFIX} +export XMANDIR=${MANDIR} + +export XCC=${CC} +export XINSTALL=${INSTALL} +export XMKDIR=${MKDIR} +export XPKG_CONFIG=${PKG_CONFIG} +export XRM=${RM} + +export XCPPFLAGS=${CPPFLAGS} +export XCFLAGS=${CFLAGS} +export XLDFLAGS=${LDFLAGS} +export XLDLIBS=${LDLIBS} + +export XNOLIBS=${NOLIBS} diff --git a/build/flags.mk b/build/flags.mk new file mode 100644 index 0000000..c911b22 --- /dev/null +++ b/build/flags.mk @@ -0,0 +1,136 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that generates gen/flags.mk + +include build/prelude.mk +include gen/vars.mk + +# Configurable flags +CPPFLAGS ?= +CFLAGS ?= \ + -g \ + -Wall \ + -Wformat=2 \ + -Werror=implicit \ + -Wimplicit-fallthrough \ + -Wmissing-declarations \ + -Wshadow \ + -Wsign-compare \ + -Wstrict-prototypes +LDFLAGS ?= +LDLIBS ?= + +export XCPPFLAGS=${CPPFLAGS} +export XCFLAGS=${CFLAGS} +export XLDFLAGS=${LDFLAGS} +export XLDLIBS=${LDLIBS} + +# Immutable flags +export BFS_CPPFLAGS= \ + -Isrc \ + -Igen \ + -D__EXTENSIONS__ \ + -D_ATFILE_SOURCE \ + -D_BSD_SOURCE \ + -D_DARWIN_C_SOURCE \ + -D_DEFAULT_SOURCE \ + -D_GNU_SOURCE \ + -D_POSIX_PTHREAD_SEMANTICS \ + -D_FILE_OFFSET_BITS=64 \ + -D_TIME_BITS=64 +export BFS_CFLAGS= -std=c17 -pthread + +# Platform-specific system libraries +LDLIBS,DragonFly := -lposix1e +LDLIBS,Linux := -lrt +LDLIBS,NetBSD := -lutil +LDLIBS,SunOS := -lsec -lsocket -lnsl +export BFS_LDLIBS=${LDLIBS,${OS}} + +# Build profiles +_ASAN := ${TRUTHY,${ASAN}} +_LSAN := ${TRUTHY,${LSAN}} +_MSAN := ${TRUTHY,${MSAN}} +_TSAN := ${TRUTHY,${TSAN}} +_UBSAN := ${TRUTHY,${UBSAN}} +_GCOV := ${TRUTHY,${GCOV}} +_LINT := ${TRUTHY,${LINT}} +_RELEASE := ${TRUTHY,${RELEASE}} + +# https://github.com/google/sanitizers/issues/342 +TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0 +export TSAN_CPPFLAGS=${TSAN_CPPFLAGS,${_TSAN}} + +ASAN_CFLAGS,y := -fsanitize=address +LSAN_CFLAGS,y := -fsanitize=leak +MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins +TSAN_CFLAGS,y := -fsanitize=thread +UBSAN_CFLAGS.y := -fsanitize=undefined + +export ASAN_CFLAGS=${ASAN_CFLAGS,${_ASAN}} +export LSAN_CFLAGS=${LSAN_CFLAGS,${_LSAN}} +export MSAN_CFLAGS=${MSAN_CFLAGS,${_MSAN}} +export TSAN_CFLAGS=${TSAN_CFLAGS,${_TSAN}} +export UBSAN_CFLAGS=${UBSAN_CFLAGS,${_UBSAN}} + +SAN_CFLAGS,y := -fno-sanitize-recover=all +INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}} +SAN := ${NOT,${INSANE}} +export SAN_CFLAGS=${SAN_CFLAGS,${SAN}} + +# MSAN and TSAN both need all code to be instrumented +YESLIBS := ${NOT,${_MSAN}${_TSAN}} +NOLIBS ?= ${NOT,${YESLIBS}} +export XNOLIBS=${NOLIBS} + +# gcov only intercepts fork()/exec() with -std=gnu* +GCOV_CFLAGS,y := -std=gnu17 --coverage +export GCOV_CFLAGS=${GCOV_CFLAGS,${_GCOV}} + +LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT +LINT_CFLAGS,y := -Werror -O2 + +export LINT_CPPFLAGS=${LINT_CPPFLAGS,${_LINT}} +export LINT_CFLAGS=${LINT_CFLAGS,${_LINT}} + +RELEASE_CPPFLAGS,y := -DNDEBUG +RELEASE_CFLAGS,y := -O3 -flto=auto + +export RELEASE_CPPFLAGS=${RELEASE_CPPFLAGS,${_RELEASE}} +export RELEASE_CFLAGS=${RELEASE_CFLAGS,${_RELEASE}} + +# Set a variable +SETVAR = @printf '%s := %s\n' >>$@ + +# Append to a variable, if non-empty +APPEND = @append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append + +gen/flags.mk:: + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + ${SETVAR} CPPFLAGS "$$BFS_CPPFLAGS" + ${APPEND} CPPFLAGS "$$TSAN_CPPFLAGS" + ${APPEND} CPPFLAGS "$$LINT_CPPFLAGS" + ${APPEND} CPPFLAGS "$$RELEASE_CPPFLAGS" + ${APPEND} CPPFLAGS "$$XCPPFLAGS" + ${APPEND} CPPFLAGS "$$EXTRA_CPPFLAGS" + ${SETVAR} CFLAGS "$$BFS_CFLAGS" + ${APPEND} CFLAGS "$$ASAN_CFLAGS" + ${APPEND} CFLAGS "$$LSAN_CFLAGS" + ${APPEND} CFLAGS "$$MSAN_CFLAGS" + ${APPEND} CFLAGS "$$TSAN_CFLAGS" + ${APPEND} CFLAGS "$$UBSAN_CFLAGS" + ${APPEND} CFLAGS "$$SAN_CFLAGS" + ${APPEND} CFLAGS "$$GCOV_CFLAGS" + ${APPEND} CFLAGS "$$LINT_CFLAGS" + ${APPEND} CFLAGS "$$RELEASE_CFLAGS" + ${APPEND} CFLAGS "$$XCFLAGS" + ${APPEND} CFLAGS "$$EXTRA_CFLAGS" + ${SETVAR} LDFLAGS "$$XLDFLAGS" + ${SETVAR} LDLIBS "$$XLDLIBS" + ${APPEND} LDLIBS "$$EXTRA_LDLIBS" + ${APPEND} LDLIBS "$$BFS_LDLIBS" + ${SETVAR} NOLIBS "$$XNOLIBS" + @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@ + ${VCAT} $@ diff --git a/build/has/acl-get-entry.c b/build/has/acl-get-entry.c new file mode 100644 index 0000000..3cce771 --- /dev/null +++ b/build/has/acl-get-entry.c @@ -0,0 +1,8 @@ +#include <sys/types.h> +#include <sys/acl.h> + +int main(void) { + acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT); + acl_entry_t entry; + return acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); +} diff --git a/build/has/acl-get-file.c b/build/has/acl-get-file.c new file mode 100644 index 0000000..89fbf23 --- /dev/null +++ b/build/has/acl-get-file.c @@ -0,0 +1,8 @@ +#include <stddef.h> +#include <sys/types.h> +#include <sys/acl.h> + +int main(void) { + acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT); + return acl == (acl_t)NULL; +} diff --git a/build/has/acl-get-tag-type.c b/build/has/acl-get-tag-type.c new file mode 100644 index 0000000..2901956 --- /dev/null +++ b/build/has/acl-get-tag-type.c @@ -0,0 +1,10 @@ +#include <string.h> +#include <sys/types.h> +#include <sys/acl.h> + +int main(void) { + acl_entry_t entry; + memset(&entry, 0, sizeof(entry)); + acl_tag_t tag; + return acl_get_tag_type(entry, &tag); +} diff --git a/build/has/acl-is-trivial-np.c b/build/has/acl-is-trivial-np.c new file mode 100644 index 0000000..9ca9fc7 --- /dev/null +++ b/build/has/acl-is-trivial-np.c @@ -0,0 +1,12 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/types.h> +#include <sys/acl.h> + +int main(void) { + acl_t acl = acl_get_fd(3); + int trivial; + acl_is_trivial_np(acl, &trivial); + return 0; +} diff --git a/build/has/acl-trivial.c b/build/has/acl-trivial.c new file mode 100644 index 0000000..7efc838 --- /dev/null +++ b/build/has/acl-trivial.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/acl.h> + +int main(void) { + return acl_trivial("."); +} diff --git a/build/has/aligned-alloc.c b/build/has/aligned-alloc.c new file mode 100644 index 0000000..4460038 --- /dev/null +++ b/build/has/aligned-alloc.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdlib.h> + +int main(void) { + return !aligned_alloc(_Alignof(void *), sizeof(void *)); +} diff --git a/build/has/confstr.c b/build/has/confstr.c new file mode 100644 index 0000000..58280b4 --- /dev/null +++ b/build/has/confstr.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <unistd.h> + +int main(void) { + confstr(_CS_PATH, NULL, 0); + return 0; +} diff --git a/build/has/extattr-get-file.c b/build/has/extattr-get-file.c new file mode 100644 index 0000000..ac9cf96 --- /dev/null +++ b/build/has/extattr-get-file.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <sys/types.h> +#include <sys/extattr.h> + +int main(void) { + return extattr_get_file("file", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0); +} diff --git a/build/has/extattr-get-link.c b/build/has/extattr-get-link.c new file mode 100644 index 0000000..c35be5b --- /dev/null +++ b/build/has/extattr-get-link.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <sys/types.h> +#include <sys/extattr.h> + +int main(void) { + return extattr_get_link("link", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0); +} diff --git a/build/has/extattr-list-file.c b/build/has/extattr-list-file.c new file mode 100644 index 0000000..e68a8bb --- /dev/null +++ b/build/has/extattr-list-file.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <sys/types.h> +#include <sys/extattr.h> + +int main(void) { + return extattr_list_file("file", EXTATTR_NAMESPACE_USER, NULL, 0); +} diff --git a/build/has/extattr-list-link.c b/build/has/extattr-list-link.c new file mode 100644 index 0000000..49f0ec2 --- /dev/null +++ b/build/has/extattr-list-link.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <sys/types.h> +#include <sys/extattr.h> + +int main(void) { + return extattr_list_link("link", EXTATTR_NAMESPACE_USER, NULL, 0); +} diff --git a/build/has/fdclosedir.c b/build/has/fdclosedir.c new file mode 100644 index 0000000..f4ad1f5 --- /dev/null +++ b/build/has/fdclosedir.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <dirent.h> + +int main(void) { + return fdclosedir(opendir(".")); +} diff --git a/build/has/getdents.c b/build/has/getdents.c new file mode 100644 index 0000000..579898f --- /dev/null +++ b/build/has/getdents.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <dirent.h> + +int main(void) { + char buf[1024]; + return getdents(3, (void *)buf, sizeof(buf)); +} diff --git a/build/has/getdents64-syscall.c b/build/has/getdents64-syscall.c new file mode 100644 index 0000000..7642d93 --- /dev/null +++ b/build/has/getdents64-syscall.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <dirent.h> +#include <sys/syscall.h> +#include <unistd.h> + +int main(void) { + char buf[1024]; + return syscall(SYS_getdents64, 3, (void *)buf, sizeof(buf)); +} diff --git a/build/has/getdents64.c b/build/has/getdents64.c new file mode 100644 index 0000000..d8e8062 --- /dev/null +++ b/build/has/getdents64.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <dirent.h> + +int main(void) { + char buf[1024]; + return getdents64(3, (void *)buf, sizeof(buf)); +} diff --git a/build/has/getmntent-1.c b/build/has/getmntent-1.c new file mode 100644 index 0000000..9854dcd --- /dev/null +++ b/build/has/getmntent-1.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <mntent.h> +#include <stdio.h> + +int main(void) { + return !getmntent(stdin); +} diff --git a/build/has/getmntent-2.c b/build/has/getmntent-2.c new file mode 100644 index 0000000..71f0220 --- /dev/null +++ b/build/has/getmntent-2.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdio.h> +#include <sys/mnttab.h> + +int main(void) { + struct mnttab mnt; + return getmntent(stdin, &mnt); +} diff --git a/build/has/getmntinfo.c b/build/has/getmntinfo.c new file mode 100644 index 0000000..90ef5fb --- /dev/null +++ b/build/has/getmntinfo.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <sys/types.h> +#include <sys/mount.h> + +int main(void) { + return getmntinfo(NULL, MNT_WAIT); +} diff --git a/build/has/getprogname-gnu.c b/build/has/getprogname-gnu.c new file mode 100644 index 0000000..6b97c5e --- /dev/null +++ b/build/has/getprogname-gnu.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <errno.h> + +int main(void) { + const char *str = program_invocation_short_name; + return str[0]; +} diff --git a/build/has/getprogname.c b/build/has/getprogname.c new file mode 100644 index 0000000..83dc8e8 --- /dev/null +++ b/build/has/getprogname.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdlib.h> + +int main(void) { + const char *str = getprogname(); + return str[0]; +} diff --git a/build/has/max-align-t.c b/build/has/max-align-t.c new file mode 100644 index 0000000..96165ce --- /dev/null +++ b/build/has/max-align-t.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> + +int main(void) { + return _Alignof(max_align_t); +} diff --git a/build/has/pipe2.c b/build/has/pipe2.c new file mode 100644 index 0000000..4cb43b5 --- /dev/null +++ b/build/has/pipe2.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <fcntl.h> +#include <unistd.h> + +int main(void) { + int fds[2]; + return pipe2(fds, O_CLOEXEC); +} diff --git a/build/has/posix-spawn-addfchdir-np.c b/build/has/posix-spawn-addfchdir-np.c new file mode 100644 index 0000000..b870a53 --- /dev/null +++ b/build/has/posix-spawn-addfchdir-np.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <spawn.h> + +int main(void) { + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addfchdir_np(&actions, 3); + return 0; +} diff --git a/build/has/posix-spawn-addfchdir.c b/build/has/posix-spawn-addfchdir.c new file mode 100644 index 0000000..c52ff81 --- /dev/null +++ b/build/has/posix-spawn-addfchdir.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <spawn.h> + +int main(void) { + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addfchdir(&actions, 3); + return 0; +} diff --git a/build/has/st-acmtim.c b/build/has/st-acmtim.c new file mode 100644 index 0000000..d687ab0 --- /dev/null +++ b/build/has/st-acmtim.c @@ -0,0 +1,12 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/stat.h> + +int main(void) { + struct stat sb = {0}; + unsigned int a = sb.st_atim.tv_sec; + unsigned int c = sb.st_ctim.tv_sec; + unsigned int m = sb.st_mtim.tv_sec; + return a + c + m; +} diff --git a/build/has/st-acmtimespec.c b/build/has/st-acmtimespec.c new file mode 100644 index 0000000..f747bc0 --- /dev/null +++ b/build/has/st-acmtimespec.c @@ -0,0 +1,12 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/stat.h> + +int main(void) { + struct stat sb = {0}; + unsigned int a = sb.st_atimespec.tv_sec; + unsigned int c = sb.st_ctimespec.tv_sec; + unsigned int m = sb.st_mtimespec.tv_sec; + return a + c + m; +} diff --git a/build/has/st-birthtim.c b/build/has/st-birthtim.c new file mode 100644 index 0000000..4964571 --- /dev/null +++ b/build/has/st-birthtim.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/stat.h> + +int main(void) { + struct stat sb = {0}; + return sb.st_birthtim.tv_sec; +} diff --git a/build/has/st-birthtimespec.c b/build/has/st-birthtimespec.c new file mode 100644 index 0000000..91a613f --- /dev/null +++ b/build/has/st-birthtimespec.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/stat.h> + +int main(void) { + struct stat sb = {0}; + return sb.st_birthtimespec.tv_sec; +} diff --git a/build/has/st-flags.c b/build/has/st-flags.c new file mode 100644 index 0000000..b1d0c32 --- /dev/null +++ b/build/has/st-flags.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/stat.h> + +int main(void) { + struct stat sb = {0}; + return sb.st_flags; +} diff --git a/build/has/statx-syscall.c b/build/has/statx-syscall.c new file mode 100644 index 0000000..87ec869 --- /dev/null +++ b/build/has/statx-syscall.c @@ -0,0 +1,13 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <fcntl.h> +#include <linux/stat.h> +#include <sys/syscall.h> +#include <unistd.h> + +int main(void) { + struct statx sb; + syscall(SYS_statx, AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb); + return 0; +} diff --git a/build/has/statx.c b/build/has/statx.c new file mode 100644 index 0000000..65f1674 --- /dev/null +++ b/build/has/statx.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <fcntl.h> +#include <sys/stat.h> + +int main(void) { + struct statx sb; + statx(AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb); + return 0; +} diff --git a/build/has/strerror-l.c b/build/has/strerror-l.c new file mode 100644 index 0000000..3dcc4d7 --- /dev/null +++ b/build/has/strerror-l.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <errno.h> +#include <locale.h> +#include <string.h> + +int main(void) { + locale_t locale = duplocale(LC_GLOBAL_LOCALE); + return !strerror_l(ENOMEM, locale); +} diff --git a/build/has/strerror-r-gnu.c b/build/has/strerror-r-gnu.c new file mode 100644 index 0000000..26ca0ee --- /dev/null +++ b/build/has/strerror-r-gnu.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <errno.h> +#include <string.h> + +int main(void) { + char buf[256]; + // Check that strerror_r() returns a pointer + return *strerror_r(ENOMEM, buf, sizeof(buf)); +} diff --git a/build/has/strerror-r-posix.c b/build/has/strerror-r-posix.c new file mode 100644 index 0000000..41b2d30 --- /dev/null +++ b/build/has/strerror-r-posix.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <errno.h> +#include <string.h> + +int main(void) { + char buf[256]; + // Check that strerror_r() returns an integer + return 2 * strerror_r(ENOMEM, buf, sizeof(buf)); +} diff --git a/build/has/string-to-flags.c b/build/has/string-to-flags.c new file mode 100644 index 0000000..027d72c --- /dev/null +++ b/build/has/string-to-flags.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <util.h> + +int main(void) { + return string_to_flags(NULL, NULL, NULL); +} diff --git a/build/has/strtofflags.c b/build/has/strtofflags.c new file mode 100644 index 0000000..73ecbcb --- /dev/null +++ b/build/has/strtofflags.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stddef.h> +#include <unistd.h> + +int main(void) { + return strtofflags(NULL, NULL, NULL); +} diff --git a/build/has/timegm.c b/build/has/timegm.c new file mode 100644 index 0000000..6e2d155 --- /dev/null +++ b/build/has/timegm.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <time.h> + +int main(void) { + struct tm tm = {0}; + return (int)timegm(&tm); +} diff --git a/build/has/tm-gmtoff.c b/build/has/tm-gmtoff.c new file mode 100644 index 0000000..543df48 --- /dev/null +++ b/build/has/tm-gmtoff.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <time.h> + +int main(void) { + struct tm tm = {0}; + return tm.tm_gmtoff; +} diff --git a/build/has/uselocale.c b/build/has/uselocale.c new file mode 100644 index 0000000..a712ff8 --- /dev/null +++ b/build/has/uselocale.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <locale.h> + +int main(void) { + locale_t locale = uselocale((locale_t)0); + return locale == LC_GLOBAL_LOCALE; +} diff --git a/build/header.mk b/build/header.mk new file mode 100644 index 0000000..da6de68 --- /dev/null +++ b/build/header.mk @@ -0,0 +1,72 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that generates gen/config.h + +include build/prelude.mk +include gen/config.mk +include build/exports.mk + +# All header fragments we generate +HEADERS := \ + gen/has/acl-get-entry.h \ + gen/has/acl-get-file.h \ + gen/has/acl-get-tag-type.h \ + gen/has/acl-is-trivial-np.h \ + gen/has/acl-trivial.h \ + gen/has/aligned-alloc.h \ + gen/has/confstr.h \ + gen/has/extattr-get-file.h \ + gen/has/extattr-get-link.h \ + gen/has/extattr-list-file.h \ + gen/has/extattr-list-link.h \ + gen/has/fdclosedir.h \ + gen/has/getdents.h \ + gen/has/getdents64.h \ + gen/has/getdents64-syscall.h \ + gen/has/getmntent-1.h \ + gen/has/getmntent-2.h \ + gen/has/getmntinfo.h \ + gen/has/getprogname.h \ + gen/has/getprogname-gnu.h \ + gen/has/max-align-t.h \ + gen/has/pipe2.h \ + gen/has/posix-spawn-addfchdir.h \ + gen/has/posix-spawn-addfchdir-np.h \ + gen/has/st-acmtim.h \ + gen/has/st-acmtimespec.h \ + gen/has/st-birthtim.h \ + gen/has/st-birthtimespec.h \ + gen/has/st-flags.h \ + gen/has/statx.h \ + gen/has/statx-syscall.h \ + gen/has/strerror-l.h \ + gen/has/strerror-r-gnu.h \ + gen/has/strerror-r-posix.h \ + gen/has/string-to-flags.h \ + gen/has/strtofflags.h \ + gen/has/timegm.h \ + gen/has/tm-gmtoff.h \ + gen/has/uselocale.h + +# Previously generated by pkgs.mk +PKG_HEADERS := ${ALL_PKGS:%=gen/use/%.h} + +gen/config.h: ${PKG_HEADERS} ${HEADERS} + ${MSG} "[ GEN] $@" + @printf '// %s\n' "$@" >$@ + @printf '#ifndef BFS_CONFIG_H\n' >>$@ + @printf '#define BFS_CONFIG_H\n' >>$@ + @cat ${.ALLSRC} >>$@ + @printf '#endif // BFS_CONFIG_H\n' >>$@ + @cat ${.ALLSRC:%=%.log} >gen/config.log + ${VCAT} $@ +.PHONY: gen/config.h + +# The short name of the config test +SLUG = ${@:gen/%.h=%} + +${HEADERS}:: + @${MKDIR} ${@D} + @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0 diff --git a/build/msg-if.sh b/build/msg-if.sh new file mode 100755 index 0000000..8112aea --- /dev/null +++ b/build/msg-if.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Print a success/failure indicator from a makefile: +# +# $ ./configure +# [ CC ] use/liburing.c ✘ +# [ CC ] use/oniguruma.c ✔ + +set -eu + +MSG="$1" +shift + +if "$@"; then + build/msg.sh "$(printf '%-37s ✔' "$MSG")" +else + build/msg.sh "$(printf '%-37s ✘' "$MSG")" +fi diff --git a/build/msg.sh b/build/msg.sh new file mode 100755 index 0000000..a7da31b --- /dev/null +++ b/build/msg.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Print a message from a makefile: +# +# $ make -s +# $ make +# [ CC ] src/main.c +# $ make V=1 +# cc -Isrc -Igen -D... + +set -eu + +# Get the $MAKEFLAGS from the top-level make invocation +MFLAGS="${XMAKEFLAGS-${MAKEFLAGS-}}" + +# Check if make should be quiet (make -s) +is_quiet() { + # GNU make puts single-letter flags in the first word of $MAKEFLAGS, + # without a leading dash + case "${MFLAGS%% *}" in + -*) : ;; + *s*) return 0 ;; + esac + + # BSD make puts each flag separately like -r -s -j 48 + for flag in $MFLAGS; do + case "$flag" in + # Ignore things like --jobserver-auth + --*) continue ;; + # Skip variable assignments + *=*) break ;; + -*s*) return 0 ;; + esac + done + + return 1 +} + +# Check if make should be loud (make V=1) +is_loud() { + test "$XV" +} + +MSG="$1" +shift + +if ! is_quiet && ! is_loud; then + printf '%s\n' "$MSG" +fi + +if [ $# -eq 0 ]; then + exit +fi + +if is_loud; then + printf '%s\n' "$*" +fi + +"$@" diff --git a/build/pkgconf.sh b/build/pkgconf.sh new file mode 100755 index 0000000..96e4bf1 --- /dev/null +++ b/build/pkgconf.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# pkg-config wrapper with hardcoded fallbacks + +set -eu + +MODE= +case "${1:-}" in + --*) + MODE="$1" + shift +esac + +if [ $# -lt 1 ]; then + exit +fi + +case "$XNOLIBS" in + y|1) + exit 1 +esac + +if [ -z "$MODE" ]; then + # Check whether the libraries exist at all + for LIB; do + # Check ${USE_$LIB} + USE_LIB="USE_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')" + eval "USE=\"\${$USE_LIB:-}\"" + case "$USE" in + y|1) + continue + ;; + n|0) + exit 1 + ;; + esac + + CFLAGS=$("$0" --cflags "$LIB") || exit 1 + LDFLAGS=$("$0" --ldflags "$LIB") || exit 1 + LDLIBS=$("$0" --ldlibs "$LIB") || exit 1 + build/cc.sh $CFLAGS $LDFLAGS build/use/$LIB.c $LDLIBS || exit 1 + done +fi + +# Defer to pkg-config if possible +if command -v "${XPKG_CONFIG:-}" >/dev/null 2>&1; then + case "$MODE" in + --cflags) + "$XPKG_CONFIG" --cflags "$@" + ;; + --ldflags) + "$XPKG_CONFIG" --libs-only-L --libs-only-other "$@" + ;; + --ldlibs) + "$XPKG_CONFIG" --libs-only-l "$@" + ;; + esac + + exit +fi + +# pkg-config unavailable, emulate it ourselves +CFLAGS="" +LDFLAGS="" +LDLIBS="" + +for LIB; do + case "$LIB" in + libacl) + LDLIB=-lacl + ;; + libcap) + LDLIB=-lcap + ;; + libselinux) + LDLIB=-lselinux + ;; + liburing) + LDLIB=-luring + ;; + oniguruma) + LDLIB=-lonig + ;; + *) + printf 'error: Unknown package %s\n' "$LIB" >&2 + exit 1 + ;; + esac + + LDLIBS="$LDLIBS$LDLIB " +done + +case "$MODE" in + --ldlibs) + printf '%s\n' "$LDLIBS" + ;; +esac diff --git a/build/pkgs.mk b/build/pkgs.mk new file mode 100644 index 0000000..39b550d --- /dev/null +++ b/build/pkgs.mk @@ -0,0 +1,33 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Makefile that generates gen/pkgs.mk + +include build/prelude.mk +include gen/vars.mk +include gen/flags.mk +include build/exports.mk + +HEADERS := ${ALL_PKGS:%=gen/use/%.h} + +gen/pkgs.mk: ${HEADERS} + ${MSG} "[ GEN] $@" + @printf '# %s\n' "$@" >$@ + @gen() { \ + printf 'PKGS := %s\n' "$$*"; \ + printf 'CFLAGS += %s\n' "$$(build/pkgconf.sh --cflags "$$@")"; \ + printf 'LDFLAGS += %s\n' "$$(build/pkgconf.sh --ldflags "$$@")"; \ + printf 'LDLIBS := %s $${LDLIBS}\n' "$$(build/pkgconf.sh --ldlibs "$$@")"; \ + }; \ + gen $$(grep -l ' true$$' ${.ALLSRC} | sed 's|.*/\(.*\)\.h|\1|') >>$@ + ${VCAT} $@ + +.PHONY: gen/pkgs.mk + +# Convert gen/use/foo.h to foo +PKG = ${@:gen/use/%.h=%} + +${HEADERS}:: + @${MKDIR} ${@D} + @build/define-if.sh use/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] use/${PKG}.c" test $$? -eq 0; diff --git a/build/prelude.mk b/build/prelude.mk new file mode 100644 index 0000000..5be26cb --- /dev/null +++ b/build/prelude.mk @@ -0,0 +1,122 @@ +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Common makefile utilities. Compatible with both GNU make and most BSD makes. + +# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to +.OBJDIR: . + +# We don't use any suffix rules +.SUFFIXES: + +# GNU make has $^ for the full list of targets, while BSD make has $> and the +# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would +# break if one of them implemented support for the other. So instead, bring +# BSD's ${.ALLSRC} to GNU. +.ALLSRC ?= $^ + +# Installation paths +DESTDIR ?= +PREFIX ?= /usr +MANDIR ?= ${PREFIX}/share/man + +# Configurable executables +CC ?= cc +INSTALL ?= install +MKDIR ?= mkdir -p +PKG_CONFIG ?= pkg-config +RM ?= rm -f + +# GNU and BSD make have incompatible syntax for conditionals, but we can do a +# lot with just nested variable expansion. We use "y" as the canonical +# truthy value, and "" (the empty string) as the canonical falsey value. +# +# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this: +# +# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y +# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y +# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty] +# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty] +# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [emtpy] +# +# Inspired by https://github.com/wahern/autoguess +TRUTHY,y := y +TRUTHY,1 := y + +# Boolean operators are also implemented with nested expansion +NOT, := y + +# Normalize ${V} to either "y" or "" +export XV=${TRUTHY,${V}} + +# Suppress output unless V=1 +Q, := @ +Q := ${Q,${XV}} + +# Show full commands with `make V=1`, otherwise short summaries +MSG = @build/msg.sh + +# cat a file if V=1 +VCAT,y := @cat +VCAT, := @: +VCAT := ${VCAT,${XV}} + +# All external dependencies +ALL_PKGS := \ + libacl \ + libcap \ + libselinux \ + liburing \ + oniguruma + +# List all object files here, as they're needed by both `./configure` and `make` + +# All object files except the entry point +LIBBFS := \ + obj/src/alloc.o \ + obj/src/bar.o \ + obj/src/bfstd.o \ + obj/src/bftw.o \ + obj/src/color.o \ + obj/src/ctx.o \ + obj/src/diag.o \ + obj/src/dir.o \ + obj/src/dstring.o \ + obj/src/eval.o \ + obj/src/exec.o \ + obj/src/expr.o \ + obj/src/fsade.o \ + obj/src/ioq.o \ + obj/src/mtab.o \ + obj/src/opt.o \ + obj/src/parse.o \ + obj/src/printf.o \ + obj/src/pwcache.o \ + obj/src/stat.o \ + obj/src/thread.o \ + obj/src/trie.o \ + obj/src/typo.o \ + obj/src/xregex.o \ + obj/src/xspawn.o \ + obj/src/xtime.o \ + obj/gen/version.o + +# Unit test objects +UNIT_OBJS := \ + obj/tests/alloc.o \ + obj/tests/bfstd.o \ + obj/tests/bit.o \ + obj/tests/ioq.o \ + obj/tests/main.o \ + obj/tests/trie.o \ + obj/tests/xspawn.o \ + obj/tests/xtime.o + +# All object files +OBJS := \ + obj/src/main.o \ + obj/tests/mksock.o \ + obj/tests/xspawnee.o \ + obj/tests/xtouch.o \ + ${LIBBFS} \ + ${UNIT_OBJS} diff --git a/build/use/libacl.c b/build/use/libacl.c new file mode 100644 index 0000000..de1fe50 --- /dev/null +++ b/build/use/libacl.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/acl.h> + +int main(void) { + acl_free(0); + return 0; +} diff --git a/build/use/libcap.c b/build/use/libcap.c new file mode 100644 index 0000000..58e832c --- /dev/null +++ b/build/use/libcap.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sys/capability.h> + +int main(void) { + cap_free(0); + return 0; +} diff --git a/build/use/libselinux.c b/build/use/libselinux.c new file mode 100644 index 0000000..bca409d --- /dev/null +++ b/build/use/libselinux.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <selinux/selinux.h> + +int main(void) { + freecon(0); + return 0; +} diff --git a/build/use/liburing.c b/build/use/liburing.c new file mode 100644 index 0000000..bea499a --- /dev/null +++ b/build/use/liburing.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <liburing.h> + +int main(void) { + io_uring_free_probe(0); + return 0; +} diff --git a/build/use/oniguruma.c b/build/use/oniguruma.c new file mode 100644 index 0000000..cb17596 --- /dev/null +++ b/build/use/oniguruma.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <oniguruma.h> + +int main(void) { + onig_free(0); + return 0; +} diff --git a/completions/bfs.bash b/completions/bfs.bash index f734ab1..6fd82c8 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,8 +97,6 @@ _bfs() { -depth -follow -ignore_readdir_race - -maxdepth - -mindepth -mount -nocolor -noignore_readdir_race diff --git a/completions/bfs.fish b/completions/bfs.fish index 3f399e7..24b0ad9 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 @@ -70,6 +71,7 @@ complete -c bfs -o Btime -d "Find files birthed specified number of days ago" -x complete -c bfs -o ctime -d "Find files changed specified number of days ago" -x complete -c bfs -o mtime -d "Find files modified specified number of days ago" -x complete -c bfs -o capable -d "Find files with capabilities set" +complete -c bfs -o context -d "Find files by SELinux context" -x complete -c bfs -o depth -d "Find files with specified number of depth" -x complete -c bfs -o empty -d "Find empty files/directories" complete -c bfs -o executable -d "Find files the current user can execute" @@ -133,6 +135,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..432ab8c 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' @@ -73,6 +74,7 @@ args=( '*-mtime[find files modified N days ago]:modification time (days):->times' '*-capable[find files with POSIX.1e capabilities set]' + '*-context[find files by SELinux context]:pattern' # -depth without parameters exist above. I don't know how to handle this gracefully '*-empty[find empty files/directories]' '*-executable[find files the current user can execute]' @@ -81,7 +83,7 @@ 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' @@ -117,7 +119,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 +128,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..d42dec3 --- /dev/null +++ b/configure @@ -0,0 +1,137 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# bfs build configuration script + +set -eu + +# Default to `make` +MAKE="${MAKE:-make}" + +# Pass -j$(nproc) unless MAKEFLAGS is set +if [ "${MAKEFLAGS+y}" ]; then + j="" +else + j="-j$({ nproc || sysctl -n hw.ncpu || getconf _NPROCESSORS_ONLN || echo 1; } 2>/dev/null)" +fi + +for arg; do + case "$arg" in + -h|--help) + cat <<EOF +Usage: + + \$ $0 [--enable-*|--disable-*] [CC=...] [CFLAGS=...] [...] + \$ $MAKE $j + +Variables set in the environment or on the command line will be picked up: + + MAKE + The make implementation to use + CC + The C compiler to use + + CPPFLAGS="-I... -D..." + CFLAGS="-W... -f..." + LDFLAGS="-L... -Wl,..." + Preprocessor/compiler/linker flags + + LDLIBS="-l... -l..." + Dynamic libraries to link + + EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS} + Adds to the default flags, instead of replacing them + +The default flags result in a plain debug build. Other build profiles include: + + --enable-release + Enable optimizations, disable assertions + --enable-{asan,lsan,msan,tsan,ubsan} + Enable sanitizers + --enable-gcov + Enable code coverage instrumentation + +External dependencies are auto-detected by default, but you can --enable or +--disable them manually: + + --enable-libacl --disable-libacl + --enable-libcap --disable-libcap + --enable-libselinux --disable-libselinux + --enable-liburing --disable-liburing + --enable-oniguruma --disable-oniguruma + +Packaging: + + --prefix=/path + Set the installation prefix (default: /usr) + +This script is a thin wrapper around a makefile-based configuration system. +Any other arguments will be passed directly to the $MAKE invocation, e.g. + + \$ $0 $j V=1 +EOF + exit 0 + ;; + + --enable-*|--disable-*) + case "$arg" in + --enable-*) yn=y ;; + --disable-*) yn=n ;; + esac + + name="${arg#--*able-}" + NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_') + case "$name" in + libacl|libcap|libselinux|liburing|oniguruma) + shift + set -- "$@" "USE_$NAME=$yn" + ;; + release|asan|lsan|msan|tsan|ubsan|lint|gcov) + shift + set -- "$@" "$NAME=$yn" + ;; + *) + printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2 + printf 'Run %s --help for more information.\n' "$0" >&2 + exit 1 + ;; + esac + ;; + + --prefix=*) + shift + set -- "$@" "PREFIX=${arg#*=}" + ;; + + MAKE=*) + MAKE="${arg#*=}" + shift + ;; + + # make flag (-j2) or variable (CC=clang) + -*|*=*) + continue + ;; + + *) + printf 'error: Unrecognized option "%s"\n\n' "$arg" >&2 + printf 'Run %s --help for more information.\n' "$0" >&2 + exit 1 + ;; + esac +done + +# Get the relative path to the source tree based on how the script was run +DIR=$(dirname -- "$0") + +# Set up symbolic links for out-of-tree builds +for f in Makefile build completions docs src tests; do + test -e "$f" || ln -s "$DIR/$f" "$f" +done + +# Set MAKEFLAGS to -j$(nproc) if it's unset +export MAKEFLAGS="${MAKEFLAGS-$j}" + +$MAKE -rf build/config.mk "$@" diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 5219160..cb33c51 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-*] [CC=...] [CFLAGS=...] [...] + $ make + +... +``` + +### Variables + +Variables set in the environment or on the command line will be picked up: +These variables specify binaries to run during the configuration and build process: + +<pre> +<b>MAKE</b>=<i>make</i> + <a href="https://en.wikipedia.org/wiki/Make_(software)">make</a> implementation +<b>CC</b>=<i>cc</i> + C compiler +<b>INSTALL</b>=<i>install</i> + Copy files during <i>make install</i> +<b>MKDIR</b>="<i>mkdir -p</i>" + Create directories +<b>PKG_CONFIG</b>=<i>pkg-config</i> + Detect external libraries and required build flags +<b>RM</b>="<i>rm -f</i>" + Delete files +</pre> + +These flags will be used by the build process: + +<pre> +<b>CPPFLAGS</b>="<i>-I... -D...</i>" +<b>CFLAGS</b>="<i>-W... -f...</i>" +<b>LDFLAGS</b>="<i>-L... -Wl,...</i>" + Preprocessor/compiler/linker flags + +<b>LDLIBS</b>="<i>-l... -l...</i>" + Dynamic libraries to link + +<b>EXTRA_</b>{<b>CPPFLAGS</b>,<b>CFLAGS</b>,<b>LDFLAGS</b>,<b>LDLIBS</b>}="<i>...</i>" + Adds to the default flags, instead of replacing them +</pre> + +### Build profiles + +The default flags result in a plain debug build. +Other build profiles can be enabled: + +<pre> +--enable-release + Enable optimizations, disable assertions + +--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizer">asan</a> +--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode">lsan</a> +--enable-<a href="https://github.com/google/sanitizers/wiki/MemorySanitizer">msan</a> +--enable-<a href="https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual">tsan</a> +--enable-<a href="https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html">ubsan</a> + Enable sanitizers + +--enable-<a href="https://gcc.gnu.org/onlinedocs/gcc/gcov/introduction-to-gcov.html">gcov</a> + Enable code coverage instrumentation +</pre> + +You can combine multiple profiles (e.g. `./configure --enable-asan --enable-ubsan`), but not all of them will work together. ### Dependencies `bfs` depends on some system libraries for some of its features. -These dependencies are optional, and can be turned off at build time if necessary by setting the appropriate variable to the empty string (e.g. `make WITH_ONIGURUMA=`). +External dependencies are auto-detected by default, but you can `--enable` or `--disable` them manually: -| Dependency | Platforms | `make` flag | -|-------------|------------|------------------| -| [acl] | Linux only | `WITH_ACL` | -| [attr] | Linux only | `WITH_ATTR` | -| [libcap] | Linux only | `WITH_LIBCAP` | -| [Oniguruma] | All | `WITH_ONIGURUMA` | +<pre> +--enable-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --disable-libacl +--enable-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --disable-libcap +--enable-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --disable-libselinux +--enable-<a href="https://github.com/axboe/liburing">liburing</a> --disable-liburing +--enable-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --disable-oniguruma +</pre> -[acl]: https://savannah.nongnu.org/projects/acl -[attr]: https://savannah.nongnu.org/projects/attr -[libcap]: https://sites.google.com/site/fullycapable/ -[Oniguruma]: https://github.com/kkos/oniguruma +[`pkg-config`] is used, if available, to detect these libraries and any additional build flags they may require. +If this is undesireable, disable it by setting `PKG_CONFIG` to the empty string (`./configure PKG_CONFIG=""`). -### Dependency tracking +[`pkg-config`]: https://www.freedesktop.org/wiki/Software/pkg-config/ -The build system automatically tracks header dependencies with the `-M` family of compiler options (see `DEPFLAGS` in the [`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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ff1c1f7..62b6480 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,274 @@ +3.* +=== + +3.2 +--- + +**May 2, 2024** + +### New features + +- New `-limit N` action that quits immediately after `N` results + +- Implemented `-context` (from GNU find) for matching SELinux contexts ([#27](https://github.com/tavianator/bfs/issues/27)) + +- Implemented `-printf %Z` for printing SELinux contexts + +### Changes + +- The build system has been rewritten, and there is now a configure step: + + $ ./configure + $ make + + See `./configure --help` or [docs/BUILDING.md](/docs/BUILDING.md) for more details. + +- Improved platform support + - Implemented `-acl` on Solaris/Illumos + - Implemented `-xattr` on DragonFly BSD + +### Bug fixes + +- Fixed some rarely-used code paths that clean up after allocation failures + +3.1.3 +----- + +**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} plaforms, 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 ----- diff --git a/docs/HACKING.md b/docs/CONTRIBUTING.md index c9bbe14..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: diff --git a/docs/RELATED.md b/docs/RELATED.md new file mode 100644 index 0000000..cf52b70 --- /dev/null +++ b/docs/RELATED.md @@ -0,0 +1,42 @@ +# 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](http://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c)) +- uutils `find` ([source](https://github.com/uutils/findutils)) + +## `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/USAGE.md b/docs/USAGE.md index 0e45b34..70f8475 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -18,7 +18,7 @@ $ bfs ./completions/bfs.zsh ./docs/BUILDING.md ./docs/CHANGELOG.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ./docs/USAGE.md ./docs/bfs.1 ... @@ -53,7 +53,7 @@ $ bfs -name '*.md' ./README.md ./docs/BUILDING.md ./docs/CHANGELOG.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ./docs/USAGE.md ``` @@ -64,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*. @@ -84,7 +84,7 @@ $ bfs -name '*.md' -or -name 'bfs.*' ./completions/bfs.zsh ./docs/BUILDING.md ./docs/CHANGELOG.md -./docs/HACKING.md +./docs/CONTRIBUTING.md ./docs/USAGE.md ./docs/bfs.1 ``` @@ -130,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). @@ -90,7 +90,7 @@ 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 \fB\-regextype \fIposix-extended\fR). .TP .B \-X Filter out files with @@ -171,12 +171,18 @@ 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 +\fB\-j\fIN\fR +Search with +.I N +threads in parallel (default: number of CPUs, up to +.IR 8 ). .SH OPERATORS .TP \fB( \fIexpression \fB)\fR @@ -245,6 +251,20 @@ 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. +.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 .PP .B \-color @@ -309,11 +329,40 @@ Ignored; for compatibility with GNU find. \fB\-regextype \fITYPE\fR 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 resular 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. @@ -403,6 +452,10 @@ Find files with POSIX.1e .BR capabilities (7) set. .TP +\fB\-context \fIGLOB\fR +Find files whose SELinux context matches the +.IR GLOB . +.TP \fB\-depth\fR [\fI\-+\fR]\fIN\fR Find files with depth .IR N . @@ -426,9 +479,9 @@ Find files the current user can execute/read/write. Always false/true. .RE .TP -.B \-fstype TYPE +\fB\-fstype \fITYPE\fR Find files on file systems with the given -.BR TYPE . +.IR TYPE . .PP \fB\-gid\fR [\fI\-+\fR]\fIN\fR .br @@ -506,13 +559,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 @@ -549,35 +601,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 @@ -626,7 +710,9 @@ 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 .br @@ -643,6 +729,11 @@ but write to instead of standard output. .RE .TP +\fB\-limit \fIN\fR +Quit once this action is evaluated +.I N +times. +.TP .B \-ls List files like .B ls @@ -689,15 +780,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 +840,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 @@ -795,7 +918,7 @@ 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..ebaff38 --- /dev/null +++ b/src/alloc.c @@ -0,0 +1,384 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "alloc.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 + +/** Portable aligned_alloc()/posix_memalign(). */ +static void *xmemalign(size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + bfs_assert(align >= sizeof(void *)); + bfs_assert(is_aligned(align, size)); + +#if BFS_HAS_ALIGNED_ALLOC + return aligned_alloc(align, size); +#else + void *ptr = NULL; + errno = posix_memalign(&ptr, align, size); + return ptr; +#endif +} + +void *alloc(size_t align, size_t size) { + bfs_assert(has_single_bit(align)); + bfs_assert(is_aligned(align, size)); + + if (size > ALLOC_MAX) { + errno = EOVERFLOW; + 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)); + bfs_assert(is_aligned(align, size)); + + 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)); + bfs_assert(is_aligned(align, old_size)); + bfs_assert(is_aligned(align, new_size)); + + 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, re-use ptr. + if (count & (count - 1)) { + // Tell sanitizers about the new array element + sanitize_alloc((char *)ptr + old_size, 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_free((char *)ret + old_size + size, new_size - old_size - 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. */ +attr(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_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 min, 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, min, 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, 0, 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_free(ret, arena->size); + sanitize_alloc(ret, varena_exact_size(varena, count)); + + 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 new_exact_size = varena_exact_size(varena, new_count); + size_t old_exact_size = varena_exact_size(varena, old_count); + + if (new_arena == old_arena) { + if (new_count < old_count) { + sanitize_free((char *)ptr + new_exact_size, old_exact_size - new_exact_size); + } else if (new_count > old_count) { + sanitize_alloc((char *)ptr + old_exact_size, new_exact_size - old_exact_size); + } + return ptr; + } + + void *ret = arena_alloc(new_arena); + if (!ret) { + return NULL; + } + + size_t old_size = old_arena->size; + sanitize_alloc((char *)ptr + old_exact_size, old_size - old_exact_size); + + size_t new_size = new_arena->size; + size_t min_size = new_size < old_size ? new_size : old_size; + memcpy(ret, ptr, min_size); + + arena_free(old_arena, ptr); + sanitize_free((char *)ret + new_exact_size, new_size - new_exact_size); + + 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..095134a --- /dev/null +++ b/src/alloc.h @@ -0,0 +1,383 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Memory allocation. + */ + +#ifndef BFS_ALLOC_H +#define BFS_ALLOC_H + +#include "prelude.h" +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> + +/** Check if a size is properly aligned. */ +static inline bool is_aligned(size_t align, size_t size) { + return (size & (align - 1)) == 0; +} + +/** Round down to a multiple of an alignment. */ +static inline size_t align_floor(size_t align, size_t size) { + return size & ~(align - 1); +} + +/** Round up to a multiple of an alignment. */ +static inline size_t align_ceil(size_t align, size_t size) { + return align_floor(align, size + align - 1); +} + +/** + * Saturating array size. + * + * @param align + * Array element alignment. + * @param size + * Array element size. + * @param count + * Array element count. + * @return + * size * count, saturating to the maximum aligned value on overflow. + */ +static inline size_t array_size(size_t align, size_t size, size_t count) { + size_t ret = size * count; + return ret / size == count ? ret : ~(align - 1); +} + +/** Saturating array sizeof. */ +#define sizeof_array(type, count) \ + array_size(alignof(type), sizeof(type), count) + +/** Size of a struct/union field. */ +#define sizeof_member(type, member) \ + sizeof(((type *)NULL)->member) + +/** + * Saturating flexible struct size. + * + * @param align + * Struct alignment. + * @param min + * Minimum struct size. + * @param offset + * Flexible array member offset. + * @param size + * Flexible array element size. + * @param count + * Flexible array element count. + * @return + * The size of the struct with count flexible array elements. Saturates + * to the maximum aligned value on overflow. + */ +static inline size_t flex_size(size_t align, size_t min, size_t offset, size_t size, size_t count) { + size_t ret = size * count; + size_t overflow = ret / size != count; + + size_t extra = offset + align - 1; + ret += extra; + overflow |= ret < extra; + ret |= -overflow; + ret = align_floor(align, ret); + + // Make sure flex_sizeof(type, member, 0) >= sizeof(type), even if the + // type has more padding than necessary for alignment + if (min > align_ceil(align, offset)) { + ret = ret < min ? min : ret; + } + + return ret; +} + +/** + * Computes the size of a flexible struct. + * + * @param type + * The type of the struct containing the flexible array. + * @param member + * The name of the flexible array member. + * @param count + * The length of the flexible array. + * @return + * The size of the struct with count flexible array elements. Saturates + * to the maximum aligned value on overflow. + */ +#define sizeof_flex(type, member, count) \ + flex_size(alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0]), count) + +/** + * General memory allocator. + * + * @param align + * The required alignment. + * @param size + * The size of the allocation. + * @return + * The allocated memory, or NULL on failure. + */ +attr(malloc(free, 1), aligned_alloc(1, 2)) +void *alloc(size_t align, size_t size); + +/** + * Zero-initialized memory allocator. + * + * @param align + * The required alignment. + * @param size + * The size of the allocation. + * @return + * The allocated memory, or NULL on failure. + */ +attr(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(). + * + * @param ptr + * The pointer to reallocate. + * @param align + * The required alignment. + * @param old_size + * The previous allocation size. + * @param new_size + * The new allocation size. + * @return + * The reallocated memory, or NULL on failure. + */ +attr(nodiscard, aligned_alloc(2, 4)) +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. + * + * @param ptr + * The pointer to reallocate. + * @param align + * The required alignment. + * @param 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. + */ +attr(nodiscard) +void *reserve(void *ptr, size_t align, size_t size, size_t count); + +/** + * Convenience macro to grow a dynamic array. + * + * @param type + * The array element type. + * @param type **ptr + * A pointer to the array. + * @param 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. + */ +attr(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. + * + * @param varena + * The varena to initialize. + * @param align + * alignof(type) + * @param min + * sizeof(type) + * @param offset + * offsetof(type, flexible_array) + * @param size + * sizeof(flexible_array[i]) + */ +void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size); + +/** + * Initialize a varena for the given type and flexible array. + * + * @param varena + * The varena to initialize. + * @param type + * A struct type containing a flexible array. + * @param member + * The name of the flexible array member. + */ +#define VARENA_INIT(varena, type, member) \ + varena_init(varena, alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0])) + +/** + * Free an arena-allocated flexible struct. + * + * @param varena + * The that allocated the object. + * @param ptr + * The object to free. + * @param count + * The length of the flexible array. + */ +void varena_free(struct varena *varena, void *ptr, size_t count); + +/** + * Arena-allocate a flexible struct. + * + * @param varena + * The varena to allocate from. + * @param count + * The length of the flexible array. + * @return + * The allocated struct, or NULL on failure. + */ +attr(malloc(varena_free, 2)) +void *varena_alloc(struct varena *varena, size_t count); + +/** + * Resize a flexible struct. + * + * @param varena + * The varena to allocate from. + * @param ptr + * The object to resize. + * @param old_count + * The old array lenth. + * @param new_count + * The new array length. + * @return + * The resized struct, or NULL on failure. + */ +attr(nodiscard) +void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count); + +/** + * Grow a flexible struct by an arbitrary amount. + * + * @param varena + * The varena to allocate from. + * @param ptr + * The object to resize. + * @param count + * Pointer to the flexible array length. + * @return + * The resized struct, or NULL on failure. + */ +attr(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..f1a6bea --- /dev/null +++ b/src/atomic.h @@ -0,0 +1,85 @@ +// 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 <stdatomic.h> + +/** + * Prettier spelling of _Atomic. + */ +#define atomic _Atomic + +/** + * Shorthand for atomic_load_explicit(). + * + * @param obj + * A pointer to the atomic object. + * @param 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) + +#endif // BFS_ATOMIC_H @@ -1,26 +1,14 @@ -/**************************************************************************** - * 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 "prelude.h" #include "bar.h" +#include "atomic.h" #include "bfstd.h" -#include "config.h" +#include "bit.h" #include "dstring.h" #include <errno.h> #include <fcntl.h> -#include <limits.h> #include <signal.h> #include <stdarg.h> #include <stdio.h> @@ -29,8 +17,8 @@ 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. */ @@ -46,8 +34,8 @@ static int bfs_bar_getsize(struct bfs_bar *bar) { 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; @@ -62,7 +50,7 @@ static int ass_puts(int fd, const char *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) { @@ -87,8 +75,9 @@ static int bfs_bar_resize(struct bfs_bar *bar) { "\033[;"; // DECSTBM: Set scrollable region // DECSTBM takes the height as the second argument + unsigned int height = load(&bar->height, relaxed); char *ptr = esc_seq + strlen(esc_seq); - ptr = ass_itoa(ptr, bar->height - 1); + ptr = ass_itoa(ptr, height - 1); strcpy(ptr, "r" // DECSTBM @@ -138,11 +127,11 @@ static void reset_before_death_by(int sig) { } /** printf() to the status bar with a single write(). */ -BFS_FORMATTER(2, 3) +attr(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) { @@ -191,13 +180,14 @@ struct bfs_bar *bfs_bar_show(void) { sigaction(SIGWINCH, &sa, NULL); #endif + unsigned int height = load(&the_bar.height, relaxed); bfs_bar_printf(&the_bar, "\n" // Make space for the bar "\0337" // DECSC: Save cursor "\033[;%ur" // DECSTBM: Set scrollable region "\0338" // DECRC: Restore cursor "\033[1A", // CUU: Move cursor up 1 row - (unsigned int)(the_bar.height - 1) + height - 1 ); return &the_bar; @@ -210,10 +200,11 @@ fail: } 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); return bfs_bar_printf(bar, "\0337" // DECSC: Save cursor "\033[%u;0f" // HVP: Move cursor to row, column @@ -222,7 +213,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 ); } @@ -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. diff --git a/src/bfstd.c b/src/bfstd.c index 1561796..0ac3a72 100644 --- a/src/bfstd.c +++ b/src/bfstd.c @@ -1,77 +1,126 @@ -/**************************************************************************** - * 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 "prelude.h" #include "bfstd.h" -#include "config.h" +#include "bit.h" +#include "diag.h" +#include "sanity.h" +#include "thread.h" #include "xregex.h" -#include <assert.h> #include <errno.h> #include <fcntl.h> #include <langinfo.h> +#include <limits.h> +#include <locale.h> #include <nl_types.h> -#include <stdbool.h> +#include <pthread.h> +#include <stddef.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/resource.h> #include <sys/stat.h> #include <sys/types.h> +#include <sys/wait.h> #include <unistd.h> #include <wchar.h> #if BFS_USE_SYS_SYSMACROS_H -# include <sys/sysmacros.h> +# include <sys/sysmacros.h> #elif BFS_USE_SYS_MKDEV_H -# include <sys/mkdev.h> +# include <sys/mkdev.h> #endif #if BFS_USE_UTIL_H -# include <util.h> +# include <util.h> #endif -bool is_nonexistence_error(int error) { - return error == ENOENT || errno == ENOTDIR; +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; } -const char *xbasename(const char *path) { - const char *i; +bool errno_is_like(int category) { + return error_is_like(errno, category); +} - // Skip trailing slashes - for (i = path + strlen(path); i > path && i[-1] == '/'; --i); +int try(int ret) { + if (ret >= 0) { + return ret; + } else { + bfs_assert(errno > 0, "errno should be positive, was %d\n", errno); + return -errno; + } +} - // Find the beginning of the name - for (; i > path && i[-1] != '/'; --i); +char *xdirname(const char *path) { + size_t i = xbaseoff(path); - // Skip leading slashes - for (; i[0] == '/' && i[1]; ++i); + // Skip trailing slashes + while (i > 0 && path[i - 1] == '/') { + --i; + } - return i; + if (i > 0) { + return strndup(path, i); + } else if (path[i] == '/') { + return strdup("/"); + } else { + return strdup("."); + } } -void close_quietly(int fd) { - int error = errno; - xclose(fd); - errno = error; +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("."); + } } -int xclose(int fd) { - int ret = close(fd); - if (ret != 0) { - assert(errno != EBADF); +size_t xbaseoff(const char *path) { + size_t i = strlen(path); + + // Skip trailing slashes + while (i > 0 && path[i - 1] == '/') { + --i; } - return ret; + + // 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) { @@ -88,7 +137,7 @@ FILE *xfopen(const char *path, int flags) { strcpy(mode, "r+b"); break; default: - assert(!"Invalid access mode"); + bfs_bug("Invalid access mode"); errno = EINVAL; return NULL; } @@ -135,50 +184,19 @@ char *xgetdelim(FILE *file, char delim) { } } -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; +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 - 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; - } + if (!cmd) { + cmd = BFS_COMMAND; } - return count; + return cmd; } /** Compile and execute a regular expression for xrpmatch(). */ @@ -234,6 +252,76 @@ int ynprompt(void) { 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; +} + /** Get the single character describing the given file type. */ static char type_char(mode_t mode) { switch (mode & S_IFMT) { @@ -316,6 +404,38 @@ void xstrmode(mode_t mode, char str[11]) { } } +/** 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) { #ifdef makedev return makedev(ma, mi); @@ -340,6 +460,14 @@ int xminor(dev_t dev) { #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 dup_cloexec(int fd) { #ifdef F_DUPFD_CLOEXEC return fcntl(fd, F_DUPFD_CLOEXEC, 0); @@ -359,7 +487,7 @@ int dup_cloexec(int fd) { } int pipe_cloexec(int pipefd[2]) { -#if __linux__ || (BSD && !__APPLE__) +#if BFS_HAS_PIPE2 return pipe2(pipefd, O_CLOEXEC); #else if (pipe(pipefd) != 0) { @@ -376,28 +504,64 @@ int pipe_cloexec(int pipefd[2]) { #endif } -char *xconfstr(int name) { -#if __ANDROID__ - errno = ENOTSUP; - return NULL; -#else - size_t len = confstr(name, NULL, 0); - if (len == 0) { - return NULL; - } +size_t xread(int fd, void *buf, size_t nbytes) { + size_t count = 0; - char *str = malloc(len); - if (!str) { - return NULL; + 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; + } } - if (confstr(name, str, len) != len) { - free(str); - return NULL; + 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 str; -#endif // !__ANDROID__ + 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) { @@ -414,6 +578,30 @@ int xfaccessat(int fd, const char *path, int amode) { 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; @@ -449,17 +637,25 @@ error: return NULL; } +#if BFS_HAS_STRTOFFLAGS +# define BFS_STRTOFFLAGS strtofflags +#elif BFS_HAS_STRING_TO_FLAGS +# define BFS_STRTOFFLAGS string_to_flags +#endif + int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) { -#if BSD && !__GNU__ +#ifdef BFS_STRTOFFLAGS char *str_arg = (char *)*str; - unsigned long set_arg = 0; - unsigned long clear_arg = 0; -#if __NetBSD__ - int ret = string_to_flags(&str_arg, &set_arg, &clear_arg); +#if __OpenBSD__ + typedef uint32_t bfs_fflags_t; #else - int ret = strtofflags(&str_arg, &set_arg, &clear_arg); + 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; @@ -469,43 +665,327 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long * errno = EINVAL; } return ret; -#else // !BSD +#else // !BFS_STRTOFFLAGS errno = ENOTSUP; return -1; #endif } +size_t asciilen(const char *str) { + return asciinlen(str, strlen(str)); +} + +size_t asciinlen(const char *str, size_t n) { + size_t i = 0; + +#if SIZE_WIDTH % 8 == 0 + // Word-at-a-time isascii() + for (size_t word; i + sizeof(word) <= n; i += sizeof(word)) { + memcpy(&word, str + i, sizeof(word)); + + const size_t mask = (SIZE_MAX / 0xFF) << 7; // 0x808080... + word &= mask; + if (!word) { + continue; + } + +#if ENDIAN_NATIVE == ENDIAN_BIG + word = bswap(word); +#elif ENDIAN_NATIVE != ENDIAN_LITTLE + break; +#endif + + size_t first = trailing_zeros(word) / 8; + return i + first; + } +#endif + + for (; i < n; ++i) { + if (!xisascii(str[i])) { + break; + } + } + + 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; - mbstate_t mb; - memset(&mb, 0, sizeof(mb)); + 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; + } + } - 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; + 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 { - cwidth = wcwidth(wc); - if (cwidth < 0) { - cwidth = 0; + 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/9699919799/utilities/V3_chap02.html#tag_18_02 + size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}"); + return ret < len ? ret : len; +} + +/** How much of this string is safe to double-quote? */ +static size_t quotable_len(const char *str, size_t len) { + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03 + 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, "\""); +} - str += mblen; - len -= mblen; - ret += cwidth; +/** '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; + } } - return ret; + 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 index 6bf6ec8..f91e380 100644 --- a/src/bfstd.h +++ b/src/bfstd.h @@ -1,18 +1,5 @@ -/**************************************************************************** - * 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 /** * Standard library wrappers and polyfills. @@ -21,57 +8,132 @@ #ifndef BFS_BFSTD_H #define BFS_BFSTD_H -#include <stdbool.h> +#include "prelude.h" +#include "sanity.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> /** - * Return whether an error code is due to a path not existing. + * Check if an error code is "like" another one. For example, ENOTDIR is + * like ENOENT because they can both be triggered by non-existent paths. + * + * @param error + * The error code to check. + * @param 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 is_nonexistence_error(int error); +bool errno_is_like(int category); + +/** + * Apply the "negative errno" convention. + * + * @param 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 +# define O_DIRECTORY 0 #endif #include <fnmatch.h> #if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE) -# define FNM_CASEFOLD FNM_IGNORECASE +# define FNM_CASEFOLD FNM_IGNORECASE #endif // #include <libgen.h> /** - * basename() variant that doesn't modify the input. + * Re-entrant dirname() variant that always allocates a copy. * * @param path * The path in question. - * @return A pointer into path at the base name offset. + * @return + * The parent directory of the path. */ -const char *xbasename(const char *path); - -#include <stdio.h> +char *xdirname(const char *path); /** - * close() variant that preserves errno. + * Re-entrant basename() variant that always allocates a copy. * - * @param fd - * The file descriptor to close. + * @param path + * The path in question. + * @return + * The final component of the path. */ -void close_quietly(int fd); +char *xbasename(const char *path); /** - * close() wrapper that asserts the file descriptor is valid. + * Find the offset of the final component of a path. * - * @param fd - * The file descriptor to close. + * @param path + * The path in question. * @return - * 0 on success, or -1 on error. + * The offset of the basename. */ -int xclose(int fd); +size_t xbaseoff(const char *path); + +#include <stdio.h> /** * fopen() variant that takes open() style flags. @@ -96,35 +158,94 @@ FILE *xfopen(const char *path, int flags); */ char *xgetdelim(FILE *file, char delim); +// #include <stdlib.h> + /** - * A safe version of read() that handles interrupted system calls and partial - * reads. + * Wrapper for getprogname() or equivalent functionality. * * @return - * The number of bytes read. A value != nbytes indicates an error - * (errno != 0) or end of file (errno == 0). + * The basename of the currently running program. */ -size_t xread(int fd, void *buf, size_t nbytes); +const char *xgetprogname(void); /** - * A safe version of write() that handles interrupted system calls and partial - * writes. + * 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. + * + * @param str + * The string to check. + * @param n + * The maximum prefix length. + */ +size_t asciinlen(const char *str, size_t n); + +/** + * Allocate a copy of a region of memory. * + * @param src + * The memory region to copy. + * @param size + * The size of the memory region. * @return - The number of bytes written. A value != nbytes indicates an error. + * A copy of the region, allocated with malloc(), or NULL on failure. */ -size_t xwrite(int fd, const void *buf, size_t nbytes); +void *xmemdup(const void *src, size_t size); -// #include <stdlib.h> +/** + * A nice string copying function. + * + * @param dest + * The NUL terminator of the destination string, or `end` if it is + * already truncated. + * @param end + * The end of the destination buffer. + * @param 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); /** - * Process a yes/no prompt. + * A nice string copying function. * - * @return 1 for yes, 0 for no, and -1 for unknown. + * @param dest + * The NUL terminator of the destination string, or `end` if it is + * already truncated. + * @param end + * The end of the destination buffer. + * @param src + * The string to copy from. + * @param n + * The maximum number of characters to copy. + * @return + * The new NUL terminator of the destination, or `end` on truncation. */ -int ynprompt(void); +char *xstpencpy(char *dest, char *end, const char *src, size_t n); -// #include <string.h> +/** + * Thread-safe strerror(). + * + * @param 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); /** * Format a mode like ls -l (e.g. -rw-r--r--). @@ -136,6 +257,13 @@ int ynprompt(void); */ 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> /** @@ -155,13 +283,30 @@ int xminor(dev_t dev); // #include <sys/stat.h> -#if __APPLE__ -# define st_atim st_atimespec -# define st_ctim st_ctimespec -# define st_mtim st_mtimespec -# define st_birthtim st_birthtimespec +/** + * Get the access/change/modification time from a struct stat. + */ +#if BFS_HAS_ST_ACMTIM +# define ST_ATIM(sb) (sb).st_atim +# define ST_CTIM(sb) (sb).st_ctim +# define ST_MTIM(sb) (sb).st_mtim +#elif BFS_HAS_ST_ACMTIMESPEC +# define ST_ATIM(sb) (sb).st_atimespec +# define ST_CTIM(sb) (sb).st_ctimespec +# define ST_MTIM(sb) (sb).st_mtimespec +#else +# define ST_ATIM(sb) ((struct timespec) { .tv_sec = (sb).st_atime }) +# define ST_CTIM(sb) ((struct timespec) { .tv_sec = (sb).st_ctime }) +# define ST_MTIM(sb) ((struct timespec) { .tv_sec = (sb).st_mtime }) #endif +// #include <sys/wait.h> + +/** + * waitpid() wrapper that handles EINTR. + */ +pid_t xwaitpid(pid_t pid, int *status, int flags); + // #include <unistd.h> /** @@ -185,14 +330,41 @@ int dup_cloexec(int fd); int pipe_cloexec(int pipefd[2]); /** - * Wrapper for confstr() that allocates with malloc(). + * A safe version of read() that handles interrupted system calls and partial + * reads. * - * @param name - * The ID of the confstr to look up. * @return - * The value of the confstr, or NULL on failure. + * The number of bytes read. A value != nbytes indicates an error + * (errno != 0) or end of file (errno == 0). */ -char *xconfstr(int name); +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. + * + * @param fd + * The file descriptor to close. + */ +void close_quietly(int fd); + +/** + * 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); /** * Wrapper for faccessat() that handles some portability issues. @@ -214,6 +386,16 @@ int xfaccessat(int fd, const char *path, int amode); char *xreadlinkat(int fd, const char *path, size_t size); /** + * 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); + +/** * Portability wrapper for strtofflags(). * * @param str @@ -228,7 +410,24 @@ char *xreadlinkat(int fd, const char *path, size_t size); */ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear); -// #include <wchar.h> +#include <wchar.h> + +/** + * Error-recovering mbrtowc() wrapper. + * + * @param str + * The string to convert. + * @param i + * The current index. + * @param len + * The length of the string. + * @param 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. @@ -240,4 +439,80 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long * */ 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. + * + * @param dest + * The destination string to fill. + * @param end + * The end of the destination buffer. + * @param src + * The string to escape. + * @param 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. + * + * @param dest + * The destination string to fill. + * @param end + * The end of the destination buffer. + * @param src + * The string to escape. + * @param n + * The maximum length of the string. + * @param 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,31 +7,186 @@ * - 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 "prelude.h" #include "bftw.h" +#include "alloc.h" #include "bfstd.h" -#include "config.h" +#include "diag.h" #include "dir.h" #include "dstring.h" +#include "ioq.h" +#include "list.h" #include "mtab.h" #include "stat.h" #include "trie.h" -#include <assert.h> #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/stat.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. @@ -54,21 +196,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; @@ -77,6 +243,9 @@ 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. */ @@ -86,145 +255,445 @@ struct bftw_file { }; /** + * 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; + } +} + +/** Detatch 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; - } - - 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; + cache->target = 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; @@ -234,12 +703,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; } @@ -257,63 +734,403 @@ 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_USE_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; + } + + 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; + } + + 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); } @@ -321,19 +1138,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 { @@ -345,335 +1151,424 @@ 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; + for (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; + while ((cur = SLIST_POP(&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; - file->next = *queue->target; - *queue->target = file; - queue->target = &file->next; +#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->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) { + bfs_static_assert(AT_FDCWD != -1); + return -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 != 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; + } + + if (bftw_ioq_pop(state, block) < 0) { + break; + } + } - return bftw_sort_merge(head, mid, tail); + 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; + } + fallthru; + + 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 != 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) { + // To avoid surprising users too much, process the roots in order + if (file->depth == 0) { + return false; } - return cache->buf; +#ifdef S_IFWHT + // ioq_stat() does not do whiteout emulation like bftw_stat_impl() + if (file->type == BFS_WHT) { + return false; + } +#endif + + 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; - } +/** Build the path to the current file. */ +static int bftw_build_path(struct bftw_state *state, const char *name) { + const struct bftw_file *file = state->file; + + size_t pathlen = file ? file->nameoff + file->namelen : 0; + if (dstresize(&state->path, pathlen) != 0) { + state->error = errno; + return -1; } - const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags); - if (statbuf) { - return bfs_mode_to_type(statbuf->mode); - } else { - return BFS_ERROR; + // 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; } -} -/** - * 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; + // Build the path backwards + while (file && file != ancestor) { + if (file->nameoff > 0) { + state->path[file->nameoff - 1] = '/'; + } + memcpy(state->path + file->nameoff, file->name, file->namelen); - assert(dstrlen(state->path) >= length); - dstresize(&state->path, length); + if (ancestor && ancestor->depth == file->depth) { + ancestor = ancestor->parent; + } + file = file->parent; + } + + state->previous = state->file; if (name) { - if (length > 0 && state->path[length - 1] != '/') { + if (pathlen > 0 && state->path[pathlen - 1] != '/') { if (dstrapp(&state->path, '/') != 0) { + state->error = errno; return -1; } } if (dstrcat(&state->path, name) != 0) { + state->error = errno; return -1; } } @@ -681,58 +1576,79 @@ static int bftw_update_path(struct bftw_state *state, const char *name) { 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) { @@ -741,16 +1657,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; @@ -764,9 +1678,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { ftwbuf->error = state->direrror; 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) { @@ -779,11 +1691,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 { @@ -793,25 +1706,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); @@ -850,24 +1756,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; } @@ -880,246 +1780,278 @@ 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; + while ((file = SLIST_POP(&state->to_close, ready))) { + bftw_unwrapdir(state, file); } - // 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); -/** - * Open the current directory. - */ -static void bftw_opendir(struct bftw_state *state) { - assert(!state->dir); - assert(!state->de); - - state->direrror = 0; + // Merge + while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) { + struct bftw_file *lf = left.head; + struct bftw_file *rf = right.head; - state->dir = bftw_file_opendir(&state->cache, state->file, state->path); - if (!state->dir) { - state->direrror = errno; + 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); } -/** - * Read an entry from the current directory. - */ -static int bftw_readdir(struct bftw_state *state) { - if (!state->dir) { - return -1; +/** 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); - 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; + bftw_queue_flush(&state->dirq); + bftw_ioq_opendirs(state); +} + +/** Close the current directory. */ +static int bftw_closedir(struct bftw_state *state) { + if (bftw_gc(state, BFTW_VISIT_ALL) != 0) { + return -1; } - 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); } } @@ -1132,10 +2064,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); @@ -1143,152 +2083,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); - - while (bftw_pop(&state) > 0) { - bftw_opendir(&state); + bftw_flush(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. */ @@ -1303,12 +2154,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. */ @@ -1352,17 +2199,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; } @@ -1370,7 +2217,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; @@ -1378,31 +2225,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); } /** @@ -1410,15 +2245,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; @@ -1429,18 +2264,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); } /** @@ -1448,40 +2283,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(). @@ -39,12 +26,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,7 +54,7 @@ struct BFTW { /** The file type. */ enum bfs_type type; - /** The errno that occurred, if type == BFTW_ERROR. */ + /** The errno that occurred, if type == BFS_ERROR. */ int error; /** A parent file descriptor for the *at() family of calls. */ @@ -75,10 +64,8 @@ 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; }; /** @@ -117,7 +104,7 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat * @param flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return - * The type of the file, or BFTW_ERROR if an error occurred. + * The type of the file, or BFS_ERROR if an error occurred. */ enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags); @@ -169,6 +156,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 +182,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; }; diff --git a/src/bit.h b/src/bit.h new file mode 100644 index 0000000..17cfbcf --- /dev/null +++ b/src/bit.h @@ -0,0 +1,401 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Bits & bytes. + */ + +#ifndef BFS_BIT_H +#define BFS_BIT_H + +#include "prelude.h" +#include <limits.h> +#include <stdint.h> + +#if __STDC_VERSION__ >= C23 +# 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 + +// C23 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 __STDC_VERSION__ >= C23 +# define bswap_u16 stdc_memreverse8u16 +# define bswap_u32 stdc_memreverse8u32 +# define bswap_u64 stdc_memreverse8u64 +#elif __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; +} + +/** + * Reverse the byte order of an integer. + */ +#define bswap(n) \ + _Generic((n), \ + uint8_t: bswap_u8, \ + uint16_t: bswap_u16, \ + uint32_t: bswap_u32, \ + uint64_t: bswap_u64)(n) + +// 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), \ + char: name##_uc, \ + signed char: name##_uc, \ + unsigned char: name##_uc, \ + signed short: name##_us, \ + unsigned short: name##_us, \ + signed int: name##_ui, \ + unsigned int: name##_ui, \ + signed long: name##_ul, \ + unsigned long: name##_ul, \ + signed long long: name##_ull, \ + unsigned long long: name##_ull) + +// C23 polyfill: bit utilities + +#if __STDC_VERSION__ >= C23 +# define count_ones stdc_count_ones +# define count_zeros stdc_count_zeros +# define rotate_left stdc_rotate_left +# define rotate_right stdc_rotate_right +# define leading_zeros stdc_leading_zeros +# define leading_ones stdc_leading_ones +# define trailing_zeros stdc_trailing_zeros +# 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 int count_ones##suffix(type n) { \ + return UINT_BUILTIN(popcount, suffix)(n); \ + } + +#define LEADING_ZEROS(type, suffix, width) \ + static inline int leading_zeros##suffix(type n) { \ + return n \ + ? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \ + : width; \ + } + +#define TRAILING_ZEROS(type, suffix, width) \ + static inline int trailing_zeros##suffix(type n) { \ + return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \ + } + +#define FIRST_TRAILING_ONE(type, suffix, width) \ + static inline int first_trailing_one##suffix(type n) { \ + return UINT_BUILTIN(ffs, suffix)(n); \ + } + +#else // !__GNUC__ + +#define COUNT_ONES(type, suffix, width) \ + static inline 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 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 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 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 ROTATE_LEFT(type, suffix, width) \ + static inline type rotate_left##suffix(type n, int c) { \ + return (n << c) | (n >> ((width - c) % width)); \ + } + +#define ROTATE_RIGHT(type, suffix, width) \ + static inline type rotate_right##suffix(type n, int c) { \ + return (n >> c) | (n << ((width - c) % width)); \ + } + +#define FIRST_LEADING_ONE(type, suffix, width) \ + static inline int first_leading_one##suffix(type n) { \ + return width - leading_zeros##suffix(n); \ + } + +#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)); \ + } + +UINT_OVERLOADS(ROTATE_LEFT) +UINT_OVERLOADS(ROTATE_RIGHT) +UINT_OVERLOADS(FIRST_LEADING_ONE) +UINT_OVERLOADS(HAS_SINGLE_BIT) + +#define count_ones(n) UINT_SELECT(n, count_ones)(n) +#define count_zeros(n) UINT_SELECT(n, count_ones)(~(n)) + +#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c) +#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c) + +#define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n) +#define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n)) + +#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_FLOOR(type, suffix, width) \ + static inline type bit_floor##suffix(type n) { \ + return n ? (type)1 << (first_leading_one##suffix(n) - 1) : 0; \ + } + +#define BIT_CEIL(type, suffix, width) \ + static inline type bit_ceil##suffix(type n) { \ + return (type)1 << first_leading_one##suffix(n - !!n); \ + } + +UINT_OVERLOADS(BIT_FLOOR) +UINT_OVERLOADS(BIT_CEIL) + +#define bit_width(n) first_leading_one(n) +#define bit_floor(n) UINT_SELECT(n, bit_floor)(n) +#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n) + +#endif // __STDC_VERSION__ < C23 + +#endif // BFS_BIT_H diff --git a/src/color.c b/src/color.c index 98fe9f2..f004bf2 100644 --- a/src/color.c +++ b/src/color.c @@ -1,205 +1,399 @@ -/**************************************************************************** - * 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 "prelude.h" #include "color.h" +#include "alloc.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 <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 iteself, without a terminating NUL. */ + char seq[]; +}; + +/** + * 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[]; +}; + 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; } + *field = esc; + struct trie_leaf *leaf = trie_insert_str(&colors->names, name); - if (leaf) { - leaf->value = field; - return 0; - } else { + if (!leaf) { return -1; } + + leaf->value = field; + return 0; } -/** Get a color from the table. */ -static char **get_color(const struct colors *colors, const char *name) { +/** 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) { const struct trie_leaf *leaf = trie_find_str(&colors->names, name); - if (leaf) { - return (char **)leaf->value; - } else { - return NULL; - } + return leaf ? leaf->value : NULL; } -/** 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; +/** Set a named escape sequence. */ +static int set_esc(struct colors *colors, const char *name, char *value) { + struct esc_seq **field = get_esc(colors, name); + if (!field) { return 0; - } else { - return -1; } + + if (*field) { + free_esc(colors, *field); + *field = NULL; + } + + if (value) { + *field = new_esc(colors, value, dstrlen(value)); + if (!*field) { + return -1; + } + } + + return 0; } -/** - * Transform a file extension for fast lookups, by reversing and lowercasing it. - */ -static void extxfrm(char *ext, size_t len) { - for (size_t i = 0; i < len - i; ++i) { - char a = ext[i]; - char b = ext[len - i - 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; + } +} + +/** 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; } } -/** Maximum supported extension length. */ -#define EXT_MAX 255 - /** - * Set the color for an extension. + * The "smart case" algorithm. + * + * @param ext + * The current extension being added. + * @param prev + * The previous case-sensitive match, if any, for the same extension. + * @param iprev + * The previous case-insensitive match, if any, for the same extension. + * @return + * Whether this extension should become case-sensitive. */ -static int set_ext_color(struct colors *colors, char *key, char *value) { +static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *prev, struct ext_color *iprev) { + // This is the first case-insensitive occurrence of this extension, e.g. + // + // *.gz=01;31:*.tar.gz=01;33 + if (!iprev) { + bfs_assert(!prev); + return false; + } + + // If the last version of this extension is already case-sensitive, + // this one should be too, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 + if (iprev->case_sensitive) { + return true; + } + + // The case matches the last occurrence exactly, e.g. + // + // *.tar.gz=01;31:*.tar.gz=01;33 + if (iprev == prev) { + return false; + } + + // Different case, but same value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;31 + if (esc_eq(iprev->esc, ext->esc->seq, ext->esc->len)) { + return false; + } + + // Different case, different value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;33 + return true; +} + +/** Set the color for an extension. */ +static int set_ext(struct colors *colors, char *key, char *value) { size_t len = dstrlen(key); - if (len > EXT_MAX) { + struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1); + if (!ext) { return -1; } - extxfrm(key, len); + 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; + } + + key = memcpy(ext->ext, key, len + 1); + + // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix() + ext_reverse(key, len); + + // Find any pre-existing exact match + struct ext_color *prev = NULL; + struct trie_leaf *leaf = trie_find_str(&colors->ext_trie, key); + if (leaf) { + prev = leaf->value; + trie_remove(&colors->ext_trie, leaf); + } // A later *.x should override any earlier *.x, *.y.x, etc. - struct trie_leaf *match; - while ((match = trie_find_postfix(&colors->ext_colors, key))) { - dstrfree(match->value); - trie_remove(&colors->ext_colors, match); + while ((leaf = trie_find_postfix(&colors->ext_trie, key))) { + trie_remove(&colors->ext_trie, leaf); } - struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key); - if (leaf) { - leaf->value = value; - return 0; - } else { - return -1; + // Insert the extension into the case-sensitive trie + leaf = trie_insert_str(&colors->ext_trie, key); + if (!leaf) { + goto fail; + } + leaf->value = ext; + + // "Smart case": if the same extension is given with two different + // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive + ext_tolower(key, len); + leaf = trie_insert_str(&colors->iext_trie, key); + if (!leaf) { + goto fail; } + + struct ext_color *iprev = leaf->value; + if (ext_case_sensitive(ext, prev, iprev)) { + iprev->case_sensitive = true; + ext->case_sensitive = true; + } + leaf->value = ext; + + return 0; + +fail: + if (ext->esc) { + free_esc(colors, ext->esc); + } + varena_free(&colors->ext_arena, ext, len + 1); + return -1; +} + +/** Rebuild the case-insensitive trie after all extensions have been parsed. */ +static int build_iext_trie(struct colors *colors) { + trie_clear(&colors->iext_trie); + + for_trie (leaf, &colors->ext_trie) { + size_t len = leaf->length - 1; + if (colors->ext_len < len) { + colors->ext_len = len; + } + + struct ext_color *ext = leaf->value; + if (ext->case_sensitive) { + continue; + } + + // set_ext() already reversed and lowercased the extension + struct trie_leaf *ileaf; + while ((ileaf = trie_find_postfix(&colors->iext_trie, ext->ext))) { + trie_remove(&colors->iext_trie, ileaf); + } + + ileaf = trie_insert_str(&colors->iext_trie, ext->ext); + if (!ileaf) { + return -1; + } + ileaf->value = ext; + } + + return 0; } /** * Find a color by an extension. */ -static const char *get_ext_color(const struct colors *colors, const char *filename) { +static const struct esc_seq *get_ext(const struct colors *colors, const char *filename) { + size_t ext_len = colors->ext_len; size_t name_len = strlen(filename); - size_t ext_len = name_len < EXT_MAX ? name_len : EXT_MAX; - const char *ext = filename + name_len - ext_len; - - char xfrm[ext_len + 1]; - memcpy(xfrm, ext, sizeof(xfrm)); - extxfrm(xfrm, ext_len); + if (name_len < ext_len) { + ext_len = name_len; + } + const char *suffix = filename + name_len - ext_len; - const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm); - if (leaf) { - return leaf->value; + char buf[256]; + char *copy; + if (ext_len < sizeof(buf)) { + copy = memcpy(buf, suffix, ext_len + 1); } 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; } /** @@ -223,6 +417,8 @@ static const char *get_ext_color(const struct colors *colors, const char *filena * * See man dir_colors. * + * @param str + * A dstring to fill with the unescaped chunk. * @param value * The value to parse. * @param end @@ -230,16 +426,18 @@ static const char *get_ext_color(const struct colors *colors, const char *filena * @param[out] next * 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; @@ -316,7 +514,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; @@ -330,7 +529,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; @@ -343,174 +543,179 @@ 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 (dstrncpy(&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 + char *esc = value; if (strspn(value, "0") == strlen(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(void) { - struct colors *colors = malloc(sizeof(struct colors)); + 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; } return colors; + +fail: + free_colors(colors); + return NULL; } void free_colors(struct colors *colors) { - if (colors) { - TRIE_FOR_EACH(&colors->ext_colors, leaf) { - dstrfree(leaf->value); - } - trie_destroy(&colors->ext_colors); + if (!colors) { + return; + } - TRIE_FOR_EACH(&colors->names, leaf) { - char **field = leaf->value; - dstrfree(*field); - } - 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; } @@ -522,6 +727,7 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) { } cfile->file = file; + cfile->need_reset = false; cfile->close = close; if (isatty(fileno(file))) { @@ -558,15 +764,20 @@ static bool is_link_broken(const struct BFTW *ftwbuf) { } } +bool colors_need_stat(const struct colors *colors) { + return colors->setuid || colors->setgid || colors->executable || colors->multi_hard + || colors->sticky_other_writable || colors->other_writable || colors->sticky; +} + /** Get the color for a file. */ -static const char *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { +static const struct esc_seq *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { enum bfs_type type = bftw_type(ftwbuf, flags); 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: @@ -590,7 +801,7 @@ static const char *file_color(const struct colors *colors, const char *filename, } if (!color) { - color = get_ext_color(colors, filename); + color = get_ext(colors, filename); } if (!color) { @@ -647,7 +858,7 @@ static const char *file_color(const struct colors *colors, const char *filename, break; } - if (!color) { + if (color && color->len == 0) { color = colors->normal; } @@ -661,17 +872,29 @@ error: } } +/** Print an escape sequence chunk. */ +static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) { + return dstrxcat(&cfile->buffer, esc->seq, esc->len); +} + /** 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; } @@ -680,29 +903,37 @@ 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 (!cfile->need_reset) { + return 0; + } + cfile->need_reset = false; + const struct colors *colors = cfile->colors; if (colors->endcode) { - return dstrdcat(&cfile->buffer, colors->endcode); + return print_esc_chunk(cfile, colors->endcode); } else { return print_esc(cfile, colors->reset); } } +/** 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 (print_esc(cfile, esc) != 0) { + return -1; } - if (dstrncat(&cfile->buffer, str, len) != 0) { + + // Don't let the string itself interfere with the colors + if (print_wordesc(cfile, str, len, WESC_TTY) != 0) { return -1; } - if (esc) { - if (print_reset(cfile) != 0) { - return -1; - } + + if (print_reset(cfile) != 0) { + return -1; } return 0; @@ -711,13 +942,13 @@ static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t /** 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); + bfs_assert(ret >= 0); if (bftw_type(ftwbuf, flags) != BFS_ERROR) { goto out; } - char *at_path; + dchar *at_path; int at_fd; if (path == ftwbuf->path) { if (ftwbuf->depth == 0) { @@ -755,8 +986,10 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, while (ret && at_path[len - 1] == '/') { --len, --ret; } - while (ret && at_path[len - 1] != '/') { - --len, --ret; + if (errno != ENOTDIR) { + while (ret && at_path[len - 1] != '/') { + --len, --ret; + } } dstresize(&at_path, len); @@ -768,27 +1001,48 @@ 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; +/** Print a path with colors. */ +static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + size_t nameoff; + if (path == ftwbuf->path) { + nameoff = ftwbuf->nameoff; + } else { + nameoff = xbaseoff(path); + } + + const char *name = path + nameoff; + size_t pathlen = nameoff + strlen(name); ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); if (broken < 0) { return -1; } + size_t split = broken; - if (broken > 0) { - if (print_colored(cfile, colors->directory, path, broken) != 0) { - return -1; + const struct colors *colors = cfile->colors; + const struct esc_seq *dirs_color = colors->directory; + const struct esc_seq *name_color; + + if (split < nameoff) { + name_color = colors->missing; + if (!name_color) { + name_color = colors->orphan; + } + } else { + name_color = file_color(cfile->colors, path + nameoff, ftwbuf, flags); + if (name_color == dirs_color) { + split = pathlen; } } - if ((size_t)broken < nameoff) { - const char *color = colors->missing; - if (!color) { - color = colors->orphan; + if (split > 0) { + if (print_colored(cfile, dirs_color, path, split) != 0) { + return -1; } - if (print_colored(cfile, color, path + broken, nameoff - broken) != 0) { + } + + if (split < pathlen) { + if (print_colored(cfile, name_color, path + split, pathlen - split) != 0) { return -1; } } @@ -798,24 +1052,8 @@ 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); + const struct esc_seq *esc = file_color(cfile->colors, name, ftwbuf, flags); + return print_colored(cfile, esc, name, strlen(name)); } /** Print the name of a file with the appropriate colors. */ @@ -872,33 +1110,35 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { } /** Format some colored output to the buffer. */ -BFS_FORMATTER(2, 3) +attr(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; +static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) { + if (depth >= 2) { + return dstrcat(&cfile->buffer, "(...)"); } - const struct bfs_expr *lhs = NULL; - const struct bfs_expr *rhs = NULL; + if (!expr) { + return dstrcat(&cfile->buffer, "(null)"); + } - if (bfs_expr_has_children(expr)) { - lhs = expr->lhs; - rhs = expr->rhs; + if (dstrcat(&cfile->buffer, "(") != 0) { + return -1; + } - if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) { + if (bfs_expr_is_parent(expr)) { + if (cbuff(cfile, "${red}%pq${rs}", expr->argv[0]) < 0) { return -1; } } else { - if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) { + if (cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]) < 0) { return -1; } } for (size_t i = 1; i < expr->argc; ++i) { - if (cbuff(cfile, " ${bld}%s${rs}", expr->argv[i]) < 0) { + if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { return -1; } } @@ -906,30 +1146,29 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) { 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)) { + expr->successes, expr->evaluations, rate, time)) { return -1; } } - if (lhs) { + int count = 0; + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { if (dstrcat(&cfile->buffer, " ") != 0) { return -1; } - if (print_expr(cfile, lhs, verbose) != 0) { - return -1; - } - } - - if (rhs) { - 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; + } } } @@ -940,17 +1179,24 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) { return 0; } +attr(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) { return -1; } - i += verbatim; + switch (*i) { case '%': switch (*++i) { @@ -995,13 +1241,24 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { break; case 'm': - if (dstrcat(&cfile->buffer, strerror(error)) != 0) { + if (dstrcat(&cfile->buffer, xstrerror(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; @@ -1021,12 +1278,12 @@ 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; @@ -1050,9 +1307,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; } @@ -1061,16 +1318,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; } @@ -1078,7 +1341,6 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { i = end; break; - } default: goto invalid; @@ -1093,7 +1355,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; } @@ -1107,7 +1369,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) { diff --git a/src/color.h b/src/color.h index 1b0cadb..3278cd6 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,8 @@ #ifndef BFS_COLOR_H #define BFS_COLOR_H -#include "config.h" -#include <stdarg.h> -#include <stdbool.h> +#include "prelude.h" +#include "dstring.h" #include <stdio.h> /** @@ -32,17 +18,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 +41,9 @@ typedef struct CFILE { /** The color table to use, if any. */ const struct colors *colors; /** A buffer for colored formatting. */ - char *buffer; + dchar *buffer; + /** Whether the next ${rs} is actually necessary. */ + bool need_reset; /** Whether to close the underlying stream. */ bool close; } CFILE; @@ -98,6 +86,8 @@ int cfclose(CFILE *cfile); * %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 @@ -109,12 +99,13 @@ int cfclose(CFILE *cfile); * @return * 0 on success, -1 on failure. */ -BFS_FORMATTER(2, 3) +attr(printf(2, 3)) int cfprintf(CFILE *cfile, const char *format, ...); /** * cfprintf() variant that takes a va_list. */ +attr(printf(2, 0)) int cvfprintf(CFILE *cfile, const char *format, va_list args); #endif // BFS_COLOR_H diff --git a/src/config.h b/src/config.h deleted file mode 100644 index 5bd49de..0000000 --- a/src/config.h +++ /dev/null @@ -1,189 +0,0 @@ -/**************************************************************************** - * 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. * - ****************************************************************************/ - -/** - * Configuration and feature/platform detection. - */ - -#ifndef BFS_CONFIG_H -#define BFS_CONFIG_H - -#include <stdbool.h> -#include <stddef.h> - -// bfs packaging configuration - -#ifndef BFS_COMMAND -# define BFS_COMMAND "bfs" -#endif -#ifndef BFS_VERSION -# define BFS_VERSION "2.6.2" -#endif -#ifndef BFS_HOMEPAGE -# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html" -#endif - -// Check for system headers - -#ifdef __has_include - -#if __has_include(<mntent.h>) -# define BFS_HAS_MNTENT_H true -#endif -#if __has_include(<paths.h>) -# define BFS_HAS_PATHS_H true -#endif -#if __has_include(<sys/acl.h>) -# define BFS_HAS_SYS_ACL_H true -#endif -#if __has_include(<sys/capability.h>) -# define BFS_HAS_SYS_CAPABILITY_H true -#endif -#if __has_include(<sys/extattr.h>) -# define BFS_HAS_SYS_EXTATTR_H true -#endif -#if __has_include(<sys/mkdev.h>) -# define BFS_HAS_SYS_MKDEV_H true -#endif -#if __has_include(<sys/param.h>) -# define BFS_HAS_SYS_PARAM_H true -#endif -#if __has_include(<sys/sysmacros.h>) -# define BFS_HAS_SYS_SYSMACROS_H true -#endif -#if __has_include(<sys/xattr.h>) -# define BFS_HAS_SYS_XATTR_H true -#endif -#if __has_include(<util.h>) -# define BFS_HAS_UTIL_H true -#endif - -#else // !__has_include - -#define BFS_HAS_MNTENT_H __GLIBC__ -#define BFS_HAS_PATHS_H true -#define BFS_HAS_SYS_ACL_H true -#define BFS_HAS_SYS_CAPABILITY_H __linux__ -#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__ -#define BFS_HAS_SYS_MKDEV_H false -#define BFS_HAS_SYS_PARAM_H true -#define BFS_HAS_SYS_SYSMACROS_H __GLIBC__ -#define BFS_HAS_SYS_XATTR_H __linux__ -#define BFS_HAS_UTIL_H __NetBSD__ - -#endif // !__has_include - -#ifndef BFS_USE_MNTENT_H -# define BFS_USE_MNTENT_H BFS_HAS_MNTENT_H -#endif -#ifndef BFS_USE_PATHS_H -# define BFS_USE_PATHS_H BFS_HAS_PATHS_H -#endif -#ifndef BFS_USE_SYS_ACL_H -# define BFS_USE_SYS_ACL_H BFS_HAS_SYS_ACL_H -#endif -#ifndef BFS_USE_SYS_CAPABILITY_H -# define BFS_USE_SYS_CAPABILITY_H BFS_HAS_SYS_CAPABILITY_H -#endif -#ifndef BFS_USE_SYS_EXTATTR_H -# define BFS_USE_SYS_EXTATTR_H BFS_HAS_SYS_EXTATTR_H -#endif -#ifndef BFS_USE_SYS_MKDEV_H -# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H -#endif -#ifndef BFS_USE_SYS_PARAM_H -# define BFS_USE_SYS_PARAM_H BFS_HAS_SYS_PARAM_H -#endif -#ifndef BFS_USE_SYS_SYSMACROS_H -# define BFS_USE_SYS_SYSMACROS_H BFS_HAS_SYS_SYSMACROS_H -#endif -#ifndef BFS_USE_SYS_XATTR_H -# define BFS_USE_SYS_XATTR_H BFS_HAS_SYS_XATTR_H -#endif -#ifndef BFS_USE_UTIL_H -# define BFS_USE_UTIL_H BFS_HAS_UTIL_H -#endif - -// Stub out feature detection on old/incompatible compilers - -#ifndef __has_feature -# define __has_feature(feat) false -#endif - -#ifndef __has_c_attribute -# define __has_c_attribute(attr) false -#endif - -#ifndef __has_attribute -# define __has_attribute(attr) false -#endif - -// Platform detection - -// Get the definition of BSD if available -#if BFS_USE_SYS_PARAM_H -# include <sys/param.h> -#endif - -#ifndef __GLIBC_PREREQ -# define __GLIBC_PREREQ(maj, min) false -#endif - -// Wrappers for fundamental language features/extensions - -/** - * Silence compiler warnings about switch/case fall-throughs. - */ -#if __has_c_attribute(fallthrough) -# define BFS_FALLTHROUGH [[fallthrough]] -#elif __has_attribute(fallthrough) -# define BFS_FALLTHROUGH __attribute__((fallthrough)) -#else -# define BFS_FALLTHROUGH ((void)0) -#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))) - -/** - * Adds compiler warnings for bad printf()-style function calls, if supported. - */ -#if __has_attribute(format) -# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args))) -#else -# define BFS_FORMATTER(fmt, args) -#endif - -#endif // BFS_CONFIG_H @@ -1,103 +1,58 @@ -/**************************************************************************** - * 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 "color.h" -#include "darray.h" #include "diag.h" #include "expr.h" +#include "list.h" #include "mtab.h" #include "pwcache.h" #include "stat.h" #include "trie.h" -#include <assert.h> +#include "xtime.h" #include <errno.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> +#include <unistd.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; +/** Get the initial value for ctx->threads (-j). */ +static int bfs_nproc(void) { + long nproc = sysconf(_SC_NPROCESSORS_ONLN); + + if (nproc < 1) { + nproc = 1; + } else if (nproc > 8) { + // Not much speedup after 8 threads + nproc = 8; } - assert(!"Unrecognized debug flag"); - return "???"; + return nproc; } 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->threads = bfs_nproc(); 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->groups = NULL; - - ctx->mtab = NULL; - ctx->mtab_error = 0; trie_init(&ctx->files); - ctx->nfiles = 0; - struct rlimit rl; - if (getrlimit(RLIMIT_NOFILE, &rl) != 0) { + if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) { goto fail; } - ctx->nofile_soft = rl.rlim_cur; - ctx->nofile_hard = rl.rlim_max; + ctx->cur_nofile = ctx->orig_nofile; ctx->users = bfs_users_new(); if (!ctx->users) { @@ -109,6 +64,10 @@ struct bfs_ctx *bfs_ctx_new(void) { goto fail; } + if (xgettime(&ctx->now) != 0) { + goto fail; + } + return ctx; fail: @@ -163,7 +122,7 @@ 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; @@ -185,7 +144,7 @@ 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 - TRIE_FOR_EACH(&ctx->files, leaf) { + for_trie (leaf, &ctx->files) { struct bfs_ctx_file *ctx_file = leaf->value; CFILE *cfile = ctx_file->cfile; if (fflush(cfile->file) == 0) { @@ -201,7 +160,6 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx) { } else if (cfile == ctx->cout) { bfs_error(ctx, "(standard output): %m.\n"); } - } // Flush the user/group caches, in case the executed command edits the @@ -264,15 +222,12 @@ 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); - TRIE_FOR_EACH(&ctx->files, leaf) { + for_trie (leaf, &ctx->files) { struct bfs_ctx_file *ctx_file = leaf->value; if (ctx_file->error) { @@ -282,7 +237,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { if (bfs_ctx_fclose(ctx, ctx_file) != 0) { if (cerr) { - bfs_error(ctx, "'%s': %m.\n", ctx_file->path); + bfs_error(ctx, "%pq: %m.\n", ctx_file->path); } ret = -1; } @@ -303,10 +258,15 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { 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->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,38 +8,17 @@ #ifndef BFS_CTX_H #define BFS_CTX_H +#include "prelude.h" +#include "alloc.h" #include "bftw.h" +#include "diag.h" +#include "expr.h" #include "trie.h" -#include <stdbool.h> #include <stddef.h> #include <sys/resource.h> +#include <time.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, -}; - -/** - * Convert a debug flag to a string. - */ -const char *debug_flag_name(enum debug_flags flag); +struct CFILE; /** * The execution context for bfs. @@ -65,10 +31,17 @@ struct bfs_ctx { /** 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; @@ -80,6 +53,8 @@ struct bfs_ctx { /** bftw() search strategy. */ enum bftw_strategy strategy; + /** Threads (-j). */ + int threads; /** Optimization level (-O). */ int optlevel; /** Debugging flags (-D). */ @@ -123,10 +98,13 @@ 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 initial RLIMIT_NOFILE limits. */ + struct rlimit orig_nofile; + /** The current RLIMIT_NOFILE limits. */ + struct rlimit cur_nofile; + + /** The current time. */ + struct timespec now; }; /** 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,41 +1,80 @@ -/**************************************************************************** - * 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 "prelude.h" #include "diag.h" +#include "alloc.h" #include "bfstd.h" -#include "ctx.h" #include "color.h" +#include "ctx.h" +#include "dstring.h" #include "expr.h" -#include <assert.h> #include <errno.h> #include <stdarg.h> -#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +/** bfs_diagf() implementation. */ +attr(printf(2, 0)) +static void bfs_vdiagf(const struct bfs_loc *loc, const char *format, va_list args) { + fprintf(stderr, "%s: %s@%s:%d: ", xgetprogname(), loc->func, loc->file, loc->line); + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); +} + +void bfs_diagf(const struct bfs_loc *loc, const char *format, ...) { + va_list args; + va_start(args, format); + bfs_vdiagf(loc, format, args); + va_end(args); +} + +noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...) { + va_list args; + va_start(args, format); + bfs_vdiagf(loc, 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); } -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); @@ -43,7 +82,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); @@ -84,13 +123,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; @@ -99,7 +143,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; @@ -107,32 +151,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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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; } @@ -141,13 +186,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) { @@ -156,9 +212,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]); } } @@ -187,7 +243,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, "~"); @@ -202,9 +258,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); } @@ -215,7 +277,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,141 @@ -/**************************************************************************** - * 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 "config.h" +#include "prelude.h" #include <stdarg.h> -#include <stdbool.h> +/** + * static_assert() with an optional second argument. + */ +#if __STDC_VERSION__ >= C23 +# define bfs_static_assert static_assert +#else +# define bfs_static_assert(...) bfs_static_assert_(__VA_ARGS__, #__VA_ARGS__, ) +# define bfs_static_assert_(expr, msg, ...) _Static_assert(expr, msg) +#endif + +/** + * A source code location. + */ +struct bfs_loc { + const char *file; + int line; + const char *func; +}; + +#define BFS_LOC_INIT { .file = __FILE__, .line = __LINE__, .func = __func__ } + +/** + * Get the current source code location. + */ +#if __STDC_VERSION__ >= C23 +# define bfs_location() (&(static const struct bfs_loc)BFS_LOC_INIT) +#else +# define bfs_location() (&(const struct bfs_loc)BFS_LOC_INIT) +#endif + +/** + * Print a low-level diagnostic message to standard error, formatted like + * + * bfs: func@src/file.c:0: Message + */ +attr(printf(2, 3)) +void bfs_diagf(const struct bfs_loc *loc, const char *format, ...); + +/** + * Unconditional diagnostic message. + */ +#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__) + +/** + * Print a message to standard error and abort. + */ +attr(cold, printf(2, 3)) +noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...); + +/** + * Unconditional abort with a message. + */ +#define bfs_abort(...) bfs_abortf(bfs_location(), __VA_ARGS__) + +/** + * Abort in debug builds; no-op in release builds. + */ +#ifdef NDEBUG +# define bfs_bug(...) ((void)0) +#else +# define bfs_bug bfs_abort +#endif + +/** + * Unconditional assert. + */ +#define bfs_verify(...) \ + bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", "") + +#define bfs_verify_(str, cond, format, ...) \ + ((cond) ? (void)0 : bfs_abort( \ + sizeof(format) > 1 \ + ? "%.0s" format "%s%s" \ + : "Assertion failed: `%s`%s", \ + str, __VA_ARGS__)) + +/** + * Assert in debug builds; no-op in release builds. + */ +#ifdef NDEBUG +# define bfs_assert(...) ((void)0) +#else +# define bfs_assert bfs_verify +#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(). */ +attr(cold) void bfs_perror(const struct bfs_ctx *ctx, const char *str); /** * Shorthand for printing error messages. */ -BFS_FORMATTER(2, 3) +attr(cold, printf(2, 3)) void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -44,7 +143,7 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a warning was printed. */ -BFS_FORMATTER(2, 3) +attr(cold, printf(2, 3)) bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -52,57 +151,67 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a debug message was printed. */ -BFS_FORMATTER(3, 4) +attr(cold, printf(3, 4)) bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...); /** * bfs_error() variant that takes a va_list. */ +attr(cold, printf(2, 0)) void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_warning() variant that takes a va_list. */ +attr(cold, printf(2, 0)) bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_debug() variant that takes a va_list. */ +attr(cold, printf(3, 0)) bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args); /** * Print the error message prefix. */ +attr(cold) void bfs_error_prefix(const struct bfs_ctx *ctx); /** * Print the warning message prefix. */ +attr(cold) bool bfs_warning_prefix(const struct bfs_ctx *ctx); /** * Print the debug message prefix. */ +attr(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); +attr(cold) +void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in an error message. */ +attr(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); +attr(cold) +bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in a warning message. */ +attr(cold) bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr); #endif // BFS_DIAG_H @@ -1,33 +1,54 @@ -/**************************************************************************** - * 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 "prelude.h" #include "dir.h" +#include "alloc.h" #include "bfstd.h" -#include "config.h" +#include "diag.h" +#include "sanity.h" +#include "trie.h" #include <dirent.h> #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <unistd.h> -#if __linux__ -# include <sys/syscall.h> +#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_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 + +#if BFS_USE_GETDENTS && !BFS_HAS_GETDENTS +/** Directory entry type for bfs_getdents() */ +typedef struct dirent64 sys_dirent; +#else +typedef struct dirent sys_dirent; #endif enum bfs_type bfs_mode_to_type(mode_t mode) { @@ -78,227 +99,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 __has_feature(memory_sanitizer) - // 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,18 @@ #ifndef BFS_DIR_H #define BFS_DIR_H +#include "prelude.h" #include <sys/types.h> /** + * Whether the implementation uses the getdents() syscall directly, rather than + * libc's readdir(). + */ +#if !defined(BFS_USE_GETDENTS) && (__linux__ || __FreeBSD__) +# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL) +#endif + +/** * A directory. */ struct bfs_dir; @@ -74,17 +70,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. + * + * @param 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 dir + * The allocated directory. * @param at_fd * The base directory for path resolution. * @param at_path * The path of the directory to open, relative to at_fd. Pass NULL to * open at_fd itself. + * @param 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,6 +120,16 @@ 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. + * + * @param 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 @@ -112,13 +150,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. + * 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..b5bf3d3 100644 --- a/src/dstring.c +++ b/src/dstring.c @@ -1,147 +1,184 @@ -/**************************************************************************** - * 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 "prelude.h" #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[]; }; -/** 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); } -/** 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); +/** + * 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; } /** 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; + header->len = len; - memcpy(header->data, data, length); - header->data[length] = '\0'; - return header->data; + char *ret = dstrdata(header); + memcpy(ret, str, len); + ret[len] = '\0'; + 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'; - + header->len = len; + header->str[len] = '\0'; return 0; } -/** Common implementation of dstr{cat,ncat,app}. */ -static int dstrcat_impl(char **dest, const char *src, size_t srclen) { +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 dstrncat(char **dest, const char *src, size_t n) { - return dstrcat_impl(dest, src, strnlen(src, n)); +int dstrcpy(dchar **dest, const char *src) { + return dstrxcpy(dest, src, strlen(src)); } -int dstrdcat(char **dest, const char *src) { - return dstrcat_impl(dest, src, dstrlen(src)); +int dstrncpy(dchar **dest, const char *src, size_t n) { + return dstrxcpy(dest, src, strnlen(src, n)); } -int dstrapp(char **str, char c) { - return dstrcat_impl(str, &c, 1); +int dstrdcpy(dchar **dest, const dchar *src) { + return dstrxcpy(dest, src, dstrlen(src)); +} + +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, ...) { va_list args; va_start(args, format); - char *str = dstrvprintf(format, args); + dchar *str = dstrvprintf(format, args); va_end(args); return str; @@ -149,7 +186,7 @@ char *dstrprintf(const char *format, ...) { char *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 +199,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,48 +209,70 @@ 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)); } diff --git a/src/dstring.h b/src/dstring.h index 51f1b2f..9ea7eb9 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,17 +8,41 @@ #ifndef BFS_DSTRING_H #define BFS_DSTRING_H -#include "config.h" +#include "prelude.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. + * + * @param dstr + * The string to free. + */ +void dstrfree(dchar *dstr); + /** * Allocate a dynamic string. * - * @param capacity + * @param cap * The initial capacity of the string. */ -char *dstralloc(size_t capacity); +attr(malloc(dstrfree, 1)) +dchar *dstralloc(size_t cap); /** * Create a dynamic copy of a string. @@ -39,7 +50,8 @@ char *dstralloc(size_t capacity); * @param str * The NUL-terminated string to copy. */ -char *dstrdup(const char *str); +attr(malloc(dstrfree, 1)) +dchar *dstrdup(const char *str); /** * Create a length-limited dynamic copy of a string. @@ -49,38 +61,62 @@ char *dstrdup(const char *str); * @param n * The maximum number of characters to copy from str. */ -char *dstrndup(const char *str, size_t n); +attr(malloc(dstrfree, 1)) +dchar *dstrndup(const char *str, size_t n); + +/** + * Create a dynamic copy of a dynamic string. + * + * @param dstr + * The dynamic string to copy. + */ +attr(malloc(dstrfree, 1)) +dchar *dstrddup(const dchar *dstr); + +/** + * Create an exact-sized dynamic copy of a string. + * + * @param str + * The string to copy. + * @param len + * The length of the string, which may include internal NUL bytes. + */ +attr(malloc(dstrfree, 1)) +dchar *dstrxdup(const char *str, size_t len); /** * Get a dynamic string's length. * * @param 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 * The dynamic string to preallocate. - * @param capacity + * @param 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 * The dynamic string to resize. - * @param length + * @param 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); +int dstresize(dchar **dstr, size_t len); /** * Append to a dynamic string. @@ -91,7 +127,7 @@ int dstresize(char **dstr, size_t length); * The string to append. * @return 0 on success, -1 on failure. */ -int dstrcat(char **dest, const char *src); +int dstrcat(dchar **dest, const char *src); /** * Append to a dynamic string. @@ -102,9 +138,10 @@ int dstrcat(char **dest, const char *src); * The string to append. * @param 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); +int dstrncat(dchar **dest, const char *src, size_t n); /** * Append a dynamic string to another dynamic string. @@ -116,7 +153,21 @@ int dstrncat(char **dest, const char *src, size_t n); * @return * 0 on success, -1 on failure. */ -int dstrdcat(char **dest, const char *src); +int dstrdcat(dchar **dest, const dchar *src); + +/** + * Append to a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The string to append. + * @param len + * The exact number of characters to take from src. + * @return + * 0 on success, -1 on failure. + */ +int dstrxcat(dchar **dest, const char *src, size_t len); /** * Append a single character to a dynamic string. @@ -125,9 +176,62 @@ int dstrdcat(char **dest, const char *src); * The string to append to. * @param c * The character to append. - * @return 0 on success, -1 on failure. + * @return + * 0 on success, -1 on failure. + */ +int dstrapp(dchar **str, char c); + +/** + * Copy a string into a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The string to copy. + * @returns + * 0 on success, -1 on failure. + */ +int dstrcpy(dchar **dest, const char *str); + +/** + * Copy a dynamic string into another one. + * + * @param dest + * The destination dynamic string. + * @param src + * The dynamic string to copy. + * @returns + * 0 on success, -1 on failure. + */ +int dstrdcpy(dchar **dest, const dchar *str); + +/** + * Copy a string into a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The dynamic string to copy. + * @param n + * The maximum number of characters to take from src. + * @returns + * 0 on success, -1 on failure. + */ +int dstrncpy(dchar **dest, const char *str, size_t n); + +/** + * Copy a string into a dynamic string. + * + * @param dest + * The destination dynamic string. + * @param src + * The dynamic string to copy. + * @param len + * The exact number of characters to take from src. + * @returns + * 0 on success, -1 on failure. */ -int dstrapp(char **str, char c); +int dstrxcpy(dchar **dest, const char *str, size_t len); /** * Create a dynamic string from a format string. @@ -139,7 +243,7 @@ int dstrapp(char **str, char c); * @return * The created string, or NULL on failure. */ -BFS_FORMATTER(1, 2) +attr(printf(1, 2)) char *dstrprintf(const char *format, ...); /** @@ -152,6 +256,7 @@ char *dstrprintf(const char *format, ...); * @return * The created string, or NULL on failure. */ +attr(printf(1, 0)) char *dstrvprintf(const char *format, va_list args); /** @@ -166,8 +271,8 @@ char *dstrvprintf(const char *format, va_list args); * @return * 0 on success, -1 on failure. */ -BFS_FORMATTER(2, 3) -int dstrcatf(char **str, const char *format, ...); +attr(printf(2, 3)) +int dstrcatf(dchar **str, const char *format, ...); /** * Format some text from a va_list onto the end of a dynamic string. @@ -181,14 +286,37 @@ int dstrcatf(char **str, const char *format, ...); * @return * 0 on success, -1 on failure. */ -int dstrvcatf(char **str, const char *format, va_list args); +attr(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. + * @param dest + * The destination dynamic string. + * @param str + * The string to escape. + * @param flags + * Flags for wordesc(). + * @return + * 0 on success, -1 on failure. + */ +int dstrescat(dchar **dest, const char *str, enum wesc_flags flags); + +/** + * Concatenate while shell-escaping. + * + * @param dest + * The destination dynamic string. + * @param str + * The string to escape. + * @param n + * The maximum length of the string. + * @param flags + * Flags for wordesc(). + * @return + * 0 on success, -1 on failure. */ -void dstrfree(char *dstr); +int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags); #endif // BFS_DSTRING_H @@ -1,31 +1,17 @@ -/**************************************************************************** - * 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 "prelude.h" #include "eval.h" #include "bar.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.h" #include "ctx.h" -#include "darray.h" #include "diag.h" #include "dir.h" #include "dstring.h" @@ -35,11 +21,10 @@ #include "mtab.h" #include "printf.h" #include "pwcache.h" +#include "sanity.h" #include "stat.h" #include "trie.h" #include "xregex.h" -#include "xtime.h" -#include <assert.h> #include <errno.h> #include <fcntl.h> #include <fnmatch.h> @@ -50,8 +35,9 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <strings.h> #include <sys/resource.h> -#include <sys/stat.h> +#include <sys/types.h> #include <time.h> #include <unistd.h> #include <wchar.h> @@ -72,7 +58,7 @@ struct bfs_eval { /** * Print an error message. */ -BFS_FORMATTER(2, 3) +attr(printf(2, 3)) static void eval_error(struct bfs_eval *state, const char *format, ...) { // By POSIX, any errors should be accompanied by a non-zero exit status *state->ret = EXIT_FAILURE; @@ -95,7 +81,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; } @@ -155,10 +141,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. */ @@ -208,6 +208,21 @@ bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) { } /** + * -context test. + */ +bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) { + char *con = bfs_getfilecon(state->ftwbuf); + if (!con) { + eval_report_error(state); + return false; + } + + bool ret = eval_fnmatch(expr, con); + bfs_freecon(con); + return ret; +} + +/** * Get the given timespec field out of a stat buffer. */ static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) { @@ -253,11 +268,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; + fallthru; case BFS_MINUTES: diff /= 60; - BFS_FALLTHROUGH; + fallthru; case BFS_SECONDS: break; } @@ -285,7 +300,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); } @@ -387,11 +402,10 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c } 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + if (eval_exec_finish(child, ctx) != 0) { ret = -1; } } @@ -431,33 +445,42 @@ bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) { * -empty test. */ bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) { - bool ret = false; const struct BFTW *ftwbuf = state->ftwbuf; + const struct bfs_stat *statbuf; + struct bfs_dir *dir; + + switch (ftwbuf->type) { + case BFS_REG: + statbuf = eval_stat(state); + return statbuf && statbuf->size == 0; - if (ftwbuf->type == BFS_DIR) { - struct bfs_dir *dir = bfs_opendir(ftwbuf->at_fd, ftwbuf->at_path); + 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; + } } /** @@ -489,7 +512,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; } @@ -509,6 +532,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; } @@ -572,7 +600,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); @@ -583,6 +611,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; @@ -590,18 +619,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; } @@ -610,8 +637,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); } /** @@ -642,10 +668,28 @@ 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; + } +} + /** * -f?ls action. */ @@ -659,9 +703,14 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { 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 = ctx->posixly_correct ? 512 : 1024; - uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + block_size - 1)/block_size; + 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 ? '+' : ' '; @@ -670,28 +719,16 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { goto error; } - uintmax_t uid = statbuf->uid; - const struct passwd *pwd = bfs_getpwuid(ctx->users, uid); - 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 = bfs_getgrgid(ctx->groups, gid); - 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) { @@ -708,23 +745,25 @@ 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; + time_t now = ctx->now.tv_sec; + time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60; + time_t tomorrow = now + 24 * 60 * 60; struct tm tm; - if (xlocaltime(&time, &tm) != 0) { + if (!localtime_r(&time, &tm)) { goto error; } char time_str[256]; - const char *time_format = "%b %e %H:%M"; + size_t time_ret; if (time <= six_months_ago || time >= tomorrow) { - time_format = "%b %e %Y"; + 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 (!strftime(time_str, sizeof(time_str), time_format, &tm)) { + if (time_ret == 0) { errno = EOVERFLOW; goto error; } - if (fprintf(file, " %s", time_str) < 0) { + if (cfprintf(cfile, " %s${rs}", time_str) < 0) { goto error; } @@ -809,7 +848,6 @@ bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) { ++path; } - if (fputc('\n', file) == EOF) { goto error; } @@ -822,6 +860,19 @@ error: } /** + * -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; +} + +/** * -prune action. */ bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state) { @@ -891,7 +942,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); } @@ -904,7 +955,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; } @@ -957,9 +1008,9 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { } #if _POSIX_MONOTONIC_CLOCK > 0 -# define BFS_CLOCK CLOCK_MONOTONIC +# define BFS_CLOCK CLOCK_MONOTONIC #elif _POSIX_TIMERS > 0 -# define BFS_CLOCK CLOCK_REALTIME +# define BFS_CLOCK CLOCK_REALTIME #endif /** @@ -1004,7 +1055,7 @@ 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); @@ -1020,10 +1071,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; @@ -1033,50 +1084,49 @@ 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + ret = eval_expr(child, state); + if (state->quit) { + break; + } } - return eval_expr(expr->rhs, state); + return ret; } /** Update the status bar. */ @@ -1101,20 +1151,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); rhslen = 0; } - char *status = dstralloc(0); + status = dstralloc(0); if (!status) { - goto out_rhs; + goto out; } const char *path = ftwbuf->path; @@ -1123,26 +1174,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; } @@ -1151,35 +1201,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; } + dstresize(&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); } @@ -1208,21 +1252,21 @@ 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("); @@ -1242,10 +1286,10 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, con DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW); DEBUG_FLAG(flags, BFS_STAT_NOSYNC); - fprintf(stderr, ") == %d", cache->buf ? 0 : -1); + fprintf(stderr, ") == %d", err ? 0 : -1); - if (cache->error) { - fprintf(stderr, " [%d]", cache->error); + if (err) { + fprintf(stderr, " [%d]", err); } fprintf(stderr, "\n"); @@ -1259,14 +1303,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); } } @@ -1363,7 +1407,7 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) { if (ftwbuf->type == BFS_ERROR) { if (!eval_should_ignore(&state, ftwbuf->error)) { - eval_error(&state, "%s.\n", strerror(ftwbuf->error)); + eval_error(&state, "%s.\n", xstrerror(ftwbuf->error)); } state.action = BFTW_PRUNE; goto done; @@ -1420,59 +1464,48 @@ 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; -#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(). */ @@ -1482,20 +1515,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; @@ -1524,8 +1562,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); } /** @@ -1557,12 +1596,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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + if (eval_must_buffer(child)) { return true; } } @@ -1571,7 +1606,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; } @@ -1595,14 +1630,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), @@ -1622,6 +1662,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)); @@ -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,7 +9,7 @@ #ifndef BFS_EVAL_H #define BFS_EVAL_H -#include <stdbool.h> +#include "prelude.h" struct bfs_ctx; struct bfs_expr; @@ -52,7 +39,7 @@ typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state); * @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 +49,7 @@ bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state); +bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state); bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state); @@ -101,6 +89,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,33 +1,19 @@ -/**************************************************************************** - * 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 "prelude.h" #include "exec.h" +#include "alloc.h" #include "bfstd.h" #include "bftw.h" -#include "ctx.h" #include "color.h" -#include "config.h" +#include "ctx.h" #include "diag.h" #include "dstring.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> @@ -36,7 +22,7 @@ #include <unistd.h> /** Print some debugging info. */ -BFS_FORMATTER(2, 3) +attr(printf(2, 3)) static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) { const struct bfs_ctx *ctx = execbuf->ctx; @@ -139,26 +125,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]; @@ -191,9 +167,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; } @@ -239,9 +215,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; } @@ -252,7 +227,7 @@ 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; } @@ -284,18 +259,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) { // 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)) { @@ -352,8 +327,10 @@ 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(execbuf->ctx); + bfs_ctx_flush(ctx); if (execbuf->flags & BFS_EXEC_CONFIRM) { for (size_t i = 0; i < execbuf->argc; ++i) { @@ -373,49 +350,46 @@ static int bfs_exec_spawn(const struct bfs_exec *execbuf) { 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; } @@ -434,9 +408,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; @@ -496,7 +470,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. */ @@ -511,7 +485,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); } } @@ -527,7 +501,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; @@ -540,7 +514,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; } @@ -614,7 +588,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; } @@ -626,8 +600,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. diff --git a/src/expr.c b/src/expr.c new file mode 100644 index 0000000..5784220 --- /dev/null +++ b/src/expr.c @@ -0,0 +1,85 @@ +// 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) { + 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->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) { + while (!SLIST_EMPTY(children)) { + struct bfs_expr *child = SLIST_POP(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. @@ -21,11 +8,10 @@ #ifndef BFS_EXPR_H #define BFS_EXPR_H +#include "prelude.h" #include "color.h" #include "eval.h" #include "stat.h" -#include <stdbool.h> -#include <stddef.h> #include <sys/types.h> #include <time.h> @@ -88,9 +74,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; @@ -110,8 +109,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; @@ -127,12 +126,7 @@ struct bfs_expr { /** Auxilliary 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,21 +135,25 @@ 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. */ @@ -204,20 +202,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); + +/** + * @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. @@ -230,8 +240,8 @@ 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_free(struct bfs_expr *expr); +void bfs_expr_clear(struct bfs_expr *expr); #endif // BFS_EXPR_H diff --git a/src/fsade.c b/src/fsade.c index a609b97..d56fb07 100644 --- a/src/fsade.c +++ b/src/fsade.c @@ -1,56 +1,47 @@ -/**************************************************************************** - * 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 "prelude.h" #include "fsade.h" -#include "config.h" +#include "atomic.h" #include "bfstd.h" #include "bftw.h" #include "dir.h" #include "dstring.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_USE_SYS_EXTATTR_H -# include <sys/extattr.h> +# include <sys/extattr.h> #elif BFS_USE_SYS_XATTR_H -# include <sys/xattr.h> +# include <sys/xattr.h> #endif -#if BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS - /** * Many of the APIs used here don't have *at() variants, but we can try to * emulate something similar if /proc/self/fd is available. */ +attr(maybe_unused) 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 == AT_FDCWD || load(&proc_works, relaxed) == 0) { goto fail; } @@ -59,11 +50,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); } } @@ -78,15 +70,17 @@ fail: return ftwbuf->path; } +attr(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. */ +attr(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 @@ -125,28 +119,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(). */ +attr(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; } @@ -170,52 +209,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 __has_feature(memory_sanitizer) - // 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) { @@ -237,6 +280,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) { error = errno; acl_free(acl); } +#endif free_fake_at(ftwbuf, path); errno = error; @@ -300,17 +344,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) { #if BFS_CAN_CHECK_XATTRS +#if BFS_USE_SYS_EXTATTR_H + +/** Wrapper for extattr_list_{file,link}. */ +static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) { + if (type == BFS_LNK) { +#if BFS_HAS_EXTATTR_LIST_LINK + return extattr_list_link(path, namespace, NULL, 0); +#elif BFS_HAS_EXTATTR_GET_LINK + return extattr_get_link(path, namespace, "", NULL, 0); +#else + return 0; +#endif + } + +#if BFS_HAS_EXTATTR_LIST_FILE + return extattr_list_file(path, namespace, NULL, 0); +#elif BFS_HAS_EXTATTR_GET_FILE + // From man extattr(2): + // + // In earlier versions of this API, passing an empty string for the + // attribute name to extattr_get_file() would return the list of attributes + // defined for the target object. This interface has been deprecated in + // preference to using the explicit list API, and should not be used. + return extattr_get_file(path, namespace, "", NULL, 0); +#else + return 0; +#endif +} + +/** Wrapper for extattr_get_{file,link}. */ +static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) { + if (type == BFS_LNK) { +#if BFS_HAS_EXTATTR_GET_LINK + return extattr_get_link(path, namespace, name, NULL, 0); +#else + return 0; +#endif + } + +#if BFS_HAS_EXTATTR_GET_FILE + return extattr_get_file(path, namespace, name, NULL, 0); +#else + return 0; +#endif +} + +#endif // BFS_USE_SYS_EXTATTR_H + int bfs_check_xattrs(const struct BFTW *ftwbuf) { const char *path = fake_at(ftwbuf); ssize_t len; #if BFS_USE_SYS_EXTATTR_H - ssize_t (*extattr_list)(const char *, int, void*, size_t) = - ftwbuf->type == BFS_LNK ? extattr_list_link : extattr_list_file; - - len = extattr_list(path, EXTATTR_NAMESPACE_SYSTEM, NULL, 0); + len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM); if (len <= 0) { - len = extattr_list(path, EXTATTR_NAMESPACE_USER, NULL, 0); + len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER); } #elif __APPLE__ int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; @@ -344,12 +433,9 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { ssize_t len; #if BFS_USE_SYS_EXTATTR_H - ssize_t (*extattr_get)(const char *, int, const char *, void*, size_t) = - ftwbuf->type == BFS_LNK ? extattr_get_link : extattr_get_file; - - len = extattr_get(path, EXTATTR_NAMESPACE_SYSTEM, name, NULL, 0); + len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name); if (len < 0) { - len = extattr_get(path, EXTATTR_NAMESPACE_USER, name, NULL, 0); + len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name); } #elif __APPLE__ int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0; @@ -391,3 +477,32 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { } #endif + +char *bfs_getfilecon(const struct BFTW *ftwbuf) { +#if BFS_CAN_CHECK_CONTEXT + const char *path = fake_at(ftwbuf); + + char *con; + int ret; + if (ftwbuf->type == BFS_LNK) { + ret = lgetfilecon(path, &con); + } else { + ret = getfilecon(path, &con); + } + + if (ret >= 0) { + return con; + } else { + return NULL; + } +#else + errno = ENOTSUP; + return NULL; +#endif +} + +void bfs_freecon(char *con) { +#if BFS_CAN_CHECK_CONTEXT + freecon(con); +#endif +} diff --git a/src/fsade.h b/src/fsade.h index f45c6fd..eefef9f 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,17 +9,13 @@ #ifndef BFS_FSADE_H #define BFS_FSADE_H -#include "config.h" -#include <stdbool.h> +#include "prelude.h" -#define BFS_CAN_CHECK_ACL BFS_USE_SYS_ACL_H +#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL) -#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_USE_SYS_CAPABILITY_H && !__FreeBSD__ -# include <sys/capability.h> -# ifdef CAP_CHOWN -# define BFS_CAN_CHECK_CAPABILITIES true -# endif -#endif +#define BFS_CAN_CHECK_CAPABILITIES BFS_USE_LIBCAP + +#define BFS_CAN_CHECK_CONTEXT BFS_USE_LIBSELINUX #define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H) @@ -80,4 +63,19 @@ int bfs_check_xattrs(const struct BFTW *ftwbuf); */ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name); +/** + * Get a file's SELinux context + * + * @param ftwbuf + * The file to check. + * @return + * The file's SELinux context, or NULL on failure. + */ +char *bfs_getfilecon(const struct BFTW *ftwbuf); + +/** + * Free a bfs_getfilecon() result. + */ +void bfs_freecon(char *con); + #endif // BFS_FSADE_H diff --git a/src/ioq.c b/src/ioq.c new file mode 100644 index 0000000..43a1b35 --- /dev/null +++ b/src/ioq.c @@ -0,0 +1,1100 @@ +// 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 "prelude.h" +#include "ioq.h" +#include "alloc.h" +#include "atomic.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> + +#if BFS_USE_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) + +// Need room for two flag bits +bfs_static_assert(alignof(struct ioq_ent) >= (1 << 2)); + +/** + * 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[]; +}; + +/** 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) { + size_t i = slot - ioqq->slots; + return &ioqq->monitors[i & ioqq->monitor_mask]; +} + +/** Atomically wait for a slot to change. */ +attr(noinline) +static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) { + struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); + mutex_lock(&monitor->mutex); + + uintptr_t 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. */ +attr(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) ? ~IOQ_BLOCKED : 0 */ +static uintptr_t ioq_skip_mask(uintptr_t slot) { + return -(slot >> IOQ_SKIP_BIT) << 1; +} + +/** 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) { + size_t skip_mask = ioq_skip_mask(prev); + size_t full_mask = ~skip_mask & ~IOQ_BLOCKED; + if (prev & full_mask) { + // full(ptr) → wait + prev = ioq_slot_wait(ioqq, slot, prev); + continue; + } + + // empty → full(ptr) + uintptr_t next = ((uintptr_t)ent >> 1) & full_mask; + // skip(1) → empty + // skip(n) → skip(n - 1) + next |= (prev - IOQ_SKIP_ONE) & skip_mask; + + 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) { + // empty → skip(1) + // skip(n) → skip(n + 1) + // full(ptr) → full(ptr - 1) + uintptr_t next = prev + IOQ_SKIP_ONE; + // skip(n) → ~IOQ_BLOCKED + // full(ptr) → 0 + next &= ioq_skip_mask(next); + + 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_skip_mask(~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 an entry from the queue. */ +static struct ioq_ent *ioqq_pop(struct ioqq *ioqq, bool block) { + size_t i = fetch_add(&ioqq->tail, 1, relaxed); + ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask]; + return ioq_slot_pop(ioqq, slot, block); +} + +/** Pop a batch of entries from the queue. */ +static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size, bool block) { + size_t mask = ioqq->slot_mask; + 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 entries to send all at once. + */ +struct ioq_batch { + /** The current batch size. */ + size_t size; + /** The array of entries. */ + struct ioq_ent *entries[IOQ_BATCH]; +}; + +/** Send the batch to a queue. */ +static void ioq_batch_flush(struct ioqq *ioqq, struct ioq_batch *batch) { + if (batch->size > 0) { + ioqq_push_batch(ioqq, batch->entries, batch->size); + batch->size = 0; + } +} + +/** An an entry to a batch, flushing if necessary. */ +static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct ioq_ent *ent) { + if (batch->size >= IOQ_BATCH) { + ioq_batch_flush(ioqq, batch); + } + + batch->entries[batch->size++] = ent; +} + +/** Sentinel stop command. */ +static struct ioq_ent IOQ_STOP; + +#if BFS_USE_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_USE_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_USE_LIBURING && BFS_USE_STATX + /** struct statx arena. */ + struct arena xbufs; +#endif + + /** Pending I/O requests. */ + struct ioqq *pending; + /** Ready I/O responses. */ + struct ioqq *ready; + + /** The number of background threads. */ + size_t nthreads; + /** The background threads themselves. */ + struct ioq_thread threads[]; +}; + +/** 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_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_USE_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; +}; + +/** Dispatch a single request asynchronously. */ +static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, struct ioq_ent *ent) { + struct io_uring *ring = state->ring; + enum ioq_ring_ops ops = state->ops; + struct io_uring_sqe *sqe = NULL; + + switch (ent->op) { + case IOQ_CLOSE: + if (ops & IOQ_RING_CLOSE) { + sqe = io_uring_get_sqe(ring); + io_uring_prep_close(sqe, ent->close.fd); + } + return sqe; + + case IOQ_OPENDIR: + if (ops & IOQ_RING_OPENAT) { + sqe = io_uring_get_sqe(ring); + 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 = io_uring_get_sqe(ring); + 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 = io_uring_get_sqe(ring); + struct ioq_stat *args = &ent->stat; + int flags = bfs_statx_flags(args->flags); + unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + 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 && !state->ready.size; +} + +/** 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 io_uring *ring = state->ring; + struct ioq_ent *pending[IOQ_BATCH]; + + while (io_uring_sq_space_left(ring) >= IOQ_BATCH) { + bool block = ioq_ring_empty(state); + ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, block); + + bool any = false; + for (size_t i = 0; i < IOQ_BATCH; ++i) { + struct ioq_ent *ent = pending[i]; + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, &IOQ_STOP); + state->stop = true; + goto done; + } else if (ent) { + ioq_prep_sqe(state, ent); + any = true; + } + } + + if (!any) { + break; + } + } + +done: + return !ioq_ring_empty(state); +} + +/** Reap a single CQE. */ +static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) { + struct ioq *ioq = state->ioq; + struct io_uring *ring = state->ring; + + struct ioq_ent *ent = io_uring_cqe_get_data(cqe); + ent->result = cqe->res; + io_uring_cqe_seen(ring, cqe); + --state->submitted; + + if (ent->result < 0) { + goto push; + } + + switch (ent->op) { + case IOQ_OPENDIR: { + int fd = ent->result; + if (ioq_check_cancel(ioq, ent)) { + xclose(fd); + goto push; + } + + struct ioq_opendir *args = &ent->opendir; + ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags)); + if (ent->result >= 0) { + // TODO: io_uring_prep_getdents() + bfs_polldir(args->dir); + } else { + xclose(fd); + } + + 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); +} + +/** Reap a batch of CQEs. */ +static void ioq_ring_reap(struct ioq_ring_state *state) { + struct ioq *ioq = state->ioq; + struct io_uring *ring = state->ring; + + while (state->prepped) { + int ret = io_uring_submit_and_wait(ring, 1); + if (ret > 0) { + state->prepped -= ret; + state->submitted += ret; + } + } + + while (state->submitted) { + struct io_uring_cqe *cqe; + if (io_uring_wait_cqe(ring, &cqe) < 0) { + continue; + } + + ioq_reap_cqe(state, cqe); + } + + ioq_batch_flush(ioq->ready, &state->ready); +} + +/** io_uring worker loop. */ +static void ioq_ring_work(struct ioq_thread *thread) { + struct ioq_ring_state state = { + .ioq = thread->parent, + .ring = &thread->ring, + .ops = thread->ring_ops, + }; + + while (ioq_ring_prep(&state)) { + ioq_ring_reap(&state); + } +} + +#endif // BFS_USE_LIBURING + +/** Synchronous syscall loop. */ +static void ioq_sync_work(struct ioq_thread *thread) { + struct ioq *ioq = thread->parent; + + bool stop = false; + while (!stop) { + struct ioq_ent *pending[IOQ_BATCH]; + ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, true); + + struct ioq_batch ready; + ready.size = 0; + + for (size_t i = 0; i < IOQ_BATCH; ++i) { + struct ioq_ent *ent = pending[i]; + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, &IOQ_STOP); + stop = true; + break; + } else if (ent) { + if (!ioq_check_cancel(ioq, ent)) { + ioq_dispatch_sync(ioq, ent); + } + ioq_batch_push(ioq->ready, &ready, ent); + } + } + + ioq_batch_flush(ioq->ready, &ready); + } +} + +/** Background thread entry point. */ +static void *ioq_work(void *ptr) { + struct ioq_thread *thread = ptr; + +#if BFS_USE_LIBURING + if (thread->ring_err == 0) { + ioq_ring_work(thread); + return NULL; + } +#endif + + ioq_sync_work(thread); + return NULL; +} + +/** Initialize io_uring thread state. */ +static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { +#if BFS_USE_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; + } + + // Share io-wq workers between rings + struct io_uring_params params = {0}; + if (prev) { + params.flags |= IORING_SETUP_ATTACH_WQ; + params.wq_fd = prev->ring.ring_fd; + } + + // 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; + } + + // 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 + + return 0; +} + +/** Destroy an io_uring. */ +static void ioq_ring_exit(struct ioq_thread *thread) { +#if BFS_USE_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, struct ioq_thread *thread) { + thread->parent = ioq; + + ioq_ring_init(ioq, thread); + + if (thread_create(&thread->id, NULL, ioq_work, thread) != 0) { + ioq_ring_exit(thread); + return -1; + } + + 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_USE_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, &ioq->threads[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_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; + + ioqq_push(ioq->pending, 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; + + ioqq_push(ioq->pending, 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; + + ioqq_push(ioq->pending, 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_USE_LIBURING && BFS_USE_STATX + args->xbuf = arena_alloc(&ioq->xbufs); + if (!args->xbuf) { + ioq_free(ioq, ent); + return -1; + } +#endif + + ioqq_push(ioq->pending, ent); + return 0; +} + +struct ioq_ent *ioq_pop(struct ioq *ioq, bool block) { + if (ioq->size == 0) { + return NULL; + } + + return ioqq_pop(ioq->ready, block); +} + +void ioq_free(struct ioq *ioq, struct ioq_ent *ent) { + bfs_assert(ioq->size > 0); + --ioq->size; + +#if BFS_USE_LIBURING && BFS_USE_STATX + if (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)) { + ioqq_push(ioq->pending, &IOQ_STOP); + } +} + +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_USE_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..d8e1573 --- /dev/null +++ b/src/ioq.h @@ -0,0 +1,198 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Asynchronous I/O queues. + */ + +#ifndef BFS_IOQ_H +#define BFS_IOQ_H + +#include "prelude.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_close(). */ + IOQ_CLOSE, + /** ioq_opendir(). */ + IOQ_OPENDIR, + /** ioq_closedir(). */ + IOQ_CLOSEDIR, + /** ioq_stat(). */ + IOQ_STAT, +}; + +/** + * The I/O queue implementation needs two tag bits in each pointer to a struct + * ioq_ent, so we need to ensure at least 4-byte alignment. The natural + * alignment is enough on most architectures, but not m68k, so over-align it. + */ +#define IOQ_ENT_ALIGN alignas(4) + +/** + * An I/O queue entry. + */ +struct ioq_ent { + /** The I/O operation. */ + IOQ_ENT_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_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. + * + * @param depth + * The maximum depth of the queue. + * @param 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); + +/** + * Asynchronous close(). + * + * @param ioq + * The I/O queue. + * @param fd + * The fd to close. + * @param 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(). + * + * @param ioq + * The I/O queue. + * @param dir + * The allocated directory. + * @param dfd + * The base file descriptor. + * @param path + * The path to open, relative to dfd. + * @param flags + * Flags that control which directory entries are listed. + * @param 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(). + * + * @param ioq + * The I/O queue. + * @param dir + * The directory to close. + * @param 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(). + * + * @param ioq + * The I/O queue. + * @param dfd + * The base file descriptor. + * @param path + * The path to stat, relative to dfd. + * @param flags + * Flags that affect the lookup. + * @param buf + * A place to store the stat buffer, if successful. + * @param 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); + +/** + * Pop a response from the queue. + * + * @param 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. + * + * @param ioq + * The I/O queue. + * @param 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..61d0e5b --- /dev/null +++ b/src/list.h @@ -0,0 +1,581 @@ +// 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. + * + * @param 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. + * + * @param item + * The item to initialize. + * @param 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. + * + * @param 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. + * + * @param list + * The list in question. + * @param 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. + * + * @param list + * The list to check. + * @param item + * The item to check. + * @param 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. + * + * @param list + * The list to modify. + * @param cursor + * A pointer to the item to insert after, e.g. &list->head or list->tail. + * @param item + * The item to insert. + * @param 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. + * + * @param list + * The list to modify. + * @param item + * The item to append. + * @param 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. + * + * @param list + * The list to modify. + * @param item + * The item to prepend. + * @param 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__)) + +/** + * Add an entire singly-linked list to the tail of another. + * + * @param dest + * The destination list. + * @param src + * The source list. + */ +#define SLIST_EXTEND(dest, src) \ + SLIST_EXTEND_((dest), (src)) + +#define SLIST_EXTEND_(dest, src) \ + (src->head ? (*dest->tail = src->head, dest->tail = src->tail, SLIST_INIT(src)) : (void)0) + +/** + * Remove an item from a singly-linked list. + * + * @param list + * The list to modify. + * @param cursor + * A pointer to the item to remove, either &list->head or &prev->next. + * @param 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_impl(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) + +// Helper for SLIST_REMOVE() +static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_t size) { + // 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. + * + * @param list + * The list to modify. + * @param 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. + * + * @param type + * The list item type. + * @param item + * The induction variable name. + * @param list + * The list to iterate. + * @param 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) + +/** + * Initialize a doubly-linked list. + * + * @param 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. + * + * @param item + * The item to initialize. + * @param 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. + * + * @param list + * The list to modify. + * @param item + * The item to append. + * @param 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. + * + * @param list + * The list to modify. + * @param item + * The item to prepend. + * @param 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. + * + * @param list + * The list to check. + * @param item + * The item to check. + * @param 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. + * + * @param list + * The list to modify. + * @param cursor + * Insert after this element. + * @param item + * The item to insert. + * @param 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. + * + * @param list + * The list to modify. + * @param item + * The item to remove. + * @param 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. + * + * @param type + * The list item type. + * @param item + * The induction variable name. + * @param list + * The list to iterate. + * @param 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,18 +20,24 @@ * - bftw.[ch] (an extended version of nftw(3)) * * - Utilities: + * - alloc.[ch] (memory allocation) + * - atomic.h (atomic operations) * - bar.[ch] (a terminal status bar) + * - bit.h (bit manipulation) * - bfstd.[ch] (standard library wrappers/polyfills) * - color.[ch] (for pretty terminal colors) - * - config.h (configuration and feature/platform detection) - * - darray.[ch] (a dynamic array library) + * - prelude.h (configuration and feature/platform detection) * - 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) * - 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) * - xregex.[ch] (regular expression support) @@ -52,11 +45,10 @@ * - xtime.[ch] (date/time handling utilities) */ +#include "prelude.h" #include "bfstd.h" #include "ctx.h" -#include "darray.h" #include "diag.h" -#include "dstring.h" #include "eval.h" #include "exec.h" #include "expr.h" @@ -65,10 +57,8 @@ #include <fcntl.h> #include <limits.h> #include <locale.h> -#include <stdbool.h> #include <stdio.h> #include <stdlib.h> -#include <string.h> #include <unistd.h> /** @@ -120,30 +110,19 @@ static int open_std_streams(void) { return 0; } -static int find2fd_flatten(struct bfs_expr ***chain, struct bfs_expr *expr) { - if (!expr) { - return 0; - } else if (expr->eval_fn == eval_and) { - if (find2fd_flatten(chain, expr->lhs) != 0) { - return -1; - } - return find2fd_flatten(chain, expr->rhs); +static void find2fd_extract(struct bfs_exprs *exprs, struct bfs_expr *expr) { + SLIST_INIT(exprs); + + if (expr->eval_fn == eval_and) { + SLIST_EXTEND(exprs, &expr->children); } else { - return DARRAY_PUSH(chain, &expr); + SLIST_APPEND(exprs, expr); } } -static void shellesc(char **cmdline, const char *str) { - // https://stackoverflow.com/a/3669819/502399 - dstrcat(cmdline, " '"); - for (const char *c = str; *c; ++c) { - if (*c == '\'') { - dstrcat(cmdline, "'\\''"); - } else { - dstrapp(cmdline, *c); - } - } - dstrcat(cmdline, "'"); +static void shellesc(dchar **cmdline, const char *str) { + dstrcat(cmdline, " "); + dstrescat(cmdline, str, WESC_SHELL | WESC_TTY); } /** @@ -156,36 +135,43 @@ 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; } - bool hidden = true; - if (ctx->exclude != &bfs_false) { - if (ctx->exclude->eval_fn == eval_hidden) { - hidden = false; - } else { - bfs_expr_error(ctx, ctx->exclude); - bfs_error(ctx, "${ex}fd${rs} does not support ${red}-exclude${rs}.\n"); - 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)); } - struct bfs_expr **exprs = NULL; - if (find2fd_flatten(&exprs, ctx->expr) != 0) { + bool hidden = true; + if (ctx->exclude->eval_fn == eval_hidden) { + hidden = false; + } else if (ctx->exclude->eval_fn != eval_false) { + bfs_expr_error(ctx, ctx->exclude); + bfs_error(ctx, "${ex}fd${rs} does not support ${red}-exclude${rs}.\n"); return EXIT_FAILURE; } + struct bfs_exprs exprs; + find2fd_extract(&exprs, ctx->expr); + struct bfs_expr *pattern = NULL; struct bfs_expr *type = NULL; struct bfs_expr *executable = NULL; struct bfs_expr *empty = NULL; struct bfs_expr *action = NULL; - for (size_t i = 0; i < darray_length(exprs); ++i) { - struct bfs_expr *expr = exprs[i]; + for_slist (struct bfs_expr, expr, &exprs) { struct bfs_expr **target = NULL; if (expr->eval_fn == eval_name || expr->eval_fn == eval_path @@ -209,7 +195,7 @@ int main(int argc, char *argv[]) { if (!target) { bfs_expr_error(ctx, expr); - if (bfs_expr_has_children(expr)) { + if (bfs_expr_is_parent(expr)) { bfs_error(ctx, "Too complicated to convert to ${ex}fd${rs}.\n"); } else { bfs_error(ctx, "No equivalent ${ex}fd${rs} option.\n"); @@ -241,7 +227,7 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - char *cmdline = dstralloc(0); + dchar *cmdline = dstralloc(0); dstrcat(&cmdline, "fd --no-ignore"); @@ -333,7 +319,7 @@ int main(int argc, char *argv[]) { shellesc(&cmdline, pattern->argv[1]); } - 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]; if (!pattern || path[0] == '-') { dstrcat(&cmdline, " --search-path"); @@ -1,65 +1,58 @@ -/**************************************************************************** - * 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 "prelude.h" #include "mtab.h" +#include "alloc.h" #include "bfstd.h" -#include "config.h" -#include "darray.h" #include "stat.h" #include "trie.h" #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> -#if BFS_USE_MNTENT_H -# define BFS_MNTENT 1 -#elif BSD -# define BFS_MNTINFO 1 -#elif __SVR4 -# define BFS_MNTTAB 1 +#if !defined(BFS_USE_MNTENT) && BFS_HAS_GETMNTENT_1 +# define BFS_USE_MNTENT true +#elif !defined(BFS_USE_MNTINFO) && BFS_HAS_GETMNTINFO +# define BFS_USE_MNTINFO true +#elif !defined(BFS_USE_MNTTAB) && BFS_HAS_GETMNTENT_2 +# define BFS_USE_MNTTAB true #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; @@ -72,47 +65,56 @@ struct bfs_mtab { /** * Add an entry to the mount table. */ +attr(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) { @@ -135,7 +137,7 @@ struct bfs_mtab *bfs_mtab_parse(void) { endmntent(file); -#elif BFS_MNTINFO +#elif BFS_USE_MNTINFO #if __NetBSD__ typedef struct statvfs bfs_statfs; @@ -145,7 +147,7 @@ struct bfs_mtab *bfs_mtab_parse(void) { bfs_statfs *mntbuf; int size = getmntinfo(&mntbuf, MNT_WAIT); - if (size < 0) { + if (size <= 0) { error = errno; goto fail; } @@ -157,7 +159,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) { @@ -191,27 +193,89 @@ 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; + } + + 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; } struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev)); if (leaf) { - leaf->value = entry->type; + leaf->value = mount->type; + } else { + 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)); @@ -222,8 +286,7 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb } } -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); } @@ -232,11 +295,8 @@ void bfs_mtab_free(struct bfs_mtab *mtab) { trie_destroy(&mtab->types); trie_destroy(&mtab->names); - for (size_t i = 0; i < 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,7 +8,7 @@ #ifndef BFS_MTAB_H #define BFS_MTAB_H -#include <stdbool.h> +#include "prelude.h" struct bfs_stat; @@ -56,12 +43,12 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb * * @param mtab * The current mount table. - * @param path - * The path to check. + * @param 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,40 +21,154 @@ * -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 "prelude.h" #include "opt.h" +#include "bftw.h" +#include "bit.h" #include "color.h" -#include "config.h" #include "ctx.h" #include "diag.h" +#include "dir.h" #include "eval.h" +#include "exec.h" #include "expr.h" +#include "list.h" #include "pwcache.h" -#include <assert.h> #include <errno.h> #include <limits.h> #include <stdarg.h> -#include <stdbool.h> #include <stdio.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; +} + +/** + * Types of predicates we track. + */ +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, +}; + +/** Get the name of a predicate type. */ +static const char *pred_type_name(enum pred_type type) { + switch (type) { + case READABLE_PRED: + return "-readable"; + case WRITABLE_PRED: + return "-writable"; + case EXECUTABLE_PRED: + return "-executable"; + case ACL_PRED: + return "-acl"; + case CAPABLE_PRED: + return "-capable"; + case EMPTY_PRED: + return "-empty"; + case HIDDEN_PRED: + return "-hidden"; + case NOGROUP_PRED: + return "-nogroup"; + case NOUSER_PRED: + return "-nouser"; + case SPARSE_PRED: + return "-sparse"; + case XATTR_PRED: + return "-xattr"; + + case PRED_TYPES: + break; + } + + bfs_bug("Unknown predicate %d", (int)type); + return "???"; +} /** * A contrained integer range. */ -struct 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) { @@ -88,17 +188,17 @@ 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) { +static void constrain_max(struct df_range *range, long long value) { range->max = min_value(range->max, 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; @@ -117,20 +217,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); } /** @@ -153,179 +242,171 @@ 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, -}; - -/** 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; +/** Get the name of a range type. */ +static const char *range_type_name(enum range_type type) { + switch (type) { + case DEPTH_RANGE: + return "-depth"; + case GID_RANGE: + return "-gid"; + case INUM_RANGE: + return "-inum"; + case LINKS_RANGE: + return "-links"; + case SIZE_RANGE: + return "-size"; + case UID_RANGE: + return "-uid"; + + case RANGE_TYPES: + break; } + + bfs_bug("Unknown range %d", (int)type); + return "???"; } /** - * 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]; + struct df_range ranges[RANGE_TYPES]; - /** The predicates we track. */ - enum known_pred preds[PRED_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) { + 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; + } + + 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) { - facts->preds[i] = PRED_IMPOSSIBLE; + pred_join(&dest->preds[i], src->preds[i]); } - facts->types = 0; - facts->xtypes = 0; + 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); +attr(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 { @@ -333,757 +414,1886 @@ 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. */ +attr(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. */ +attr(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. */ +attr(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); } - if (argv == &fake_not_arg) { - expr->synthetic = true; + opt->depth = depth; + return debug; +} + +/** Log the deletion of an expression. */ +attr(printf(2, 3)) +static bool opt_delete(struct bfs_opt *opt, const char *format, ...) { + int depth = opt->depth; + + if (depth > 0) { + --opt->depth; } - expr->lhs = NULL; - expr->rhs = rhs; - return expr; + 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; } -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_type_name(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; } +} - bool has_parent = true; - if (parent->eval_fn != eval_not) { - expr = parent; - has_parent = false; +/** Print a df_range. */ +static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) { + dump(opt, "${blu}%s${rs}: ", range_type_name(type)); + + 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); + } } +} - 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; +/** 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 { - expr->eval_fn = eval_and; - expr->argv = &fake_and_arg; + fprintf(file, "~0x%X\n", ~types); } - 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; +/** 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; } - if (debug) { - cfprintf(state->ctx->cerr, "<==> %pe\n", parent); + for (int i = 0; i < RANGE_TYPES; ++i) { + lines += !range_is_top(&value->ranges[i]); } - if (expr->lhs->eval_fn == eval_not) { - expr->lhs = optimize_not_expr(state, expr->lhs); + 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 { + return opt_debug; } - if (expr->rhs->eval_fn == eval_not) { - expr->rhs = optimize_not_expr(state, expr->rhs); +} + +/** 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 (!expr->lhs || !expr->rhs) { - bfs_expr_free(parent); - return NULL; + + if (!opt_debug(opt, "%s:\n", str)) { + return; } - if (expr->eval_fn == eval_and) { - expr = optimize_and_expr(state, expr); - } else { - expr = optimize_or_expr(state, expr); + 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 (has_parent) { - parent->rhs = expr; - } else { - parent = expr; + + 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) { - bfs_expr_free(parent); + + if (value->types != ~0U) { + types_dump(df_dump_line(lines, &line), opt, "-type", value->types); + } + + if (value->xtypes != ~0U) { + types_dump(df_dump_line(lines, &line), opt, "-xtype", value->xtypes); + } +} + +/** 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. */ +attr(printf(3, 4)) +static void opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { + if (!opt->warn) { + return; + } + + if (bfs_expr_is_parent(expr) || is_const(expr)) { + return; + } + + if (bfs_expr_warning(opt->ctx, expr)) { + va_list args; + va_start(args, format); + bfs_vwarning(opt->ctx, format, args); + va_end(args); + } +} + +/** 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; + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&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; + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&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; + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&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 expr; +} - return optimize_not_expr(state, expr); +/** 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; +} -fail: - bfs_expr_free(expr); - return NULL; +/** 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; } -/** 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); +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; + } + entered = true; + } + + expr = recursive(opt, expr, visitor); + if (!expr) { + return NULL; + } + } + + 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; } - 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 = specific(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; + 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; +} - 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; +/** 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; + } + estimate_type_probability(expr); 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 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; +} + +/** 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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; - 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; + return expr; +} - return optimize_or_expr(state, expr); +/** 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; -fail: - bfs_expr_free(expr); - return NULL; + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + expr->pure &= child->pure; + expr->always_true = child->always_true; + expr->always_false = child->always_false; + expr->cost += child->cost; + expr->probability = child->probability; + } + + 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; } + } - 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; + /** 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}, + }; + + 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); - - struct bfs_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; - - int optlevel = state->ctx->optlevel; - if (optlevel >= 1) { - lhs = expr->lhs = ignore_result(state, lhs); - - 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); +/** + * 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]); + 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 *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv); + if (!ret) { + return NULL; + } + + bfs_expr_append(ret, expr); + return visit_shallow(opt, ret, &annotate); +} + +/** 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); + + struct bfs_expr *child; + while ((child = SLIST_POP(&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) { + bfs_assert(expr->eval_fn == eval_comma); - return expr; + opt_enter(opt, "%pe\n", expr); + + char **argv = expr->argv; + expr = only_child(expr); + + struct bfs_exprs children; + foster_children(expr, &children); + + struct bfs_expr *child; + while ((child = SLIST_POP(&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"); + rhs = only_child(expr); + return only_child(rhs); + } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) { + return sink_not_andor(opt, expr); + } 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; } +} - 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; +/** 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); + + struct bfs_expr *child; + while ((child = SLIST_POP(&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); + } + } + + 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); + + struct bfs_expr *child; + while ((child = SLIST_POP(&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); } -/** 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]; +/** + * 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); +} + +/** 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_min(true_range, value); + constrain_max(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) { + if (expr->num & R_OK) { + data_flow_pred(opt, READABLE_PRED, true); + } + if (expr->num & W_OK) { + data_flow_pred(opt, WRITABLE_PRED, true); + } + if (expr->num & X_OK) { + data_flow_pred(opt, EXECUTABLE_PRED, true); + } + + return expr; +} - struct range *range = &state->facts_when_true.ranges[GID_RANGE]; +/** 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(state->ctx->groups, gid); + bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid); if (errno == 0) { - constrain_pred(&state->facts_when_true.preds[NOGROUP_PRED], nogroup); + data_flow_pred(opt, NOGROUP_PRED, nogroup); } } + + return expr; +} + +/** 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; + } + + 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 -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; + } - struct range *range = &state->facts_when_true.ranges[UID_RANGE]; + return expr; +} + +/** Transfer function for -samefile. */ +static struct bfs_expr *data_flow_samefile(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + struct df_range *true_range = &opt->after_true.ranges[INUM_RANGE]; + constrain_min(true_range, expr->ino); + constrain_max(true_range, expr->ino); + + struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE]; + range_remove(false_range, expr->ino); + + return expr; +} + +/** 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; +} + +/** 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; +} + +/** 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(state->ctx->users, uid); + bool nouser = !bfs_getpwuid(opt->ctx->users, uid); if (errno == 0) { - constrain_pred(&state->facts_when_true.preds[NOUSER_PRED], nouser); + data_flow_pred(opt, NOUSER_PRED, nouser); } } + + 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); -} - -/** 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; -} - -/** 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; -} - -static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr) { - int optlevel = state->ctx->optlevel; - - state->facts_when_true = state->facts; - state->facts_when_false = state->facts; - - 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; - } - - 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); - } +/** 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; +} - if (!expr) { - goto done; - } +/** 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); - 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; - } - if (lhs) { - expr->persistent_fds += lhs->persistent_fds; - if (lhs->ephemeral_fds > expr->ephemeral_fds) { - expr->ephemeral_fds = lhs->ephemeral_fds; - } - } + 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); } + 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) { - set_facts_impossible(&state->facts_when_false); + expr->probability = 1.0; + df_init_bottom(&opt->after_false); } + if (expr->always_false) { - set_facts_impossible(&state->facts_when_true); + expr->probability = 0.0; + df_init_bottom(&opt->after_true); } - if (optlevel < 2 || expr == &bfs_true || expr == &bfs_false) { - goto done; + 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 = 0.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 (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 { + if (df_is_bottom(&opt->after_true)) { + if (!expr->pure) { 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; + } 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; + } } } -done: - return expr; + return visit_leave(opt, expr, visitor); } -/** 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->pure) { + opt_debug(opt, "ignored result\n"); + opt_warning(opt, expr, "The result of this expression is ignored.\n\n"); + expr = opt_const(opt, false); + if (!expr) { + return NULL; } - expr->cost = swapped_cost; - return true; - } else { - return false; } + + 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; + } + } + + 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_gid, data_flow_gid}, + {eval_inum, data_flow_inum}, + {eval_links, data_flow_links}, + {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); + } + + return expr; +} + +/** 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + if (child->eval_fn == eval_not) { + ++removed; + } else { + ++added; + } + } + if (added >= removed) { + return visit_shallow(opt, expr, &annotate); + } + + 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_expr *lhs = expr->lhs; - struct bfs_expr *rhs = expr->rhs; + struct bfs_exprs children; + foster_children(expr, &children); - bool ret = false; - if (lhs) { - ret |= reorder_expr_recursive(state, lhs); + struct bfs_expr *child; + while ((child = SLIST_POP(&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); } - if (rhs) { - ret |= reorder_expr_recursive(state, rhs); + + expr = visit_shallow(opt, expr, &annotate); + 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; } - 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); + struct bfs_expr *ret = NULL; + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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); + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&children); + + if (child == ignorable) { + ignore = true; + } + + if (ignore) { + opt_delete(opt, "%pe [ignored result]\n", child); + opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + 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) { + while ((child = SLIST_POP(&children))) { + opt_delete(opt, "%pe [short-circuit]\n", child); + } + } + } + + 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); + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&children); + + if (child == ignorable) { + ignore = true; + } + + if (ignore) { + opt_delete(opt, "%pe [ignored result]\n", child); + opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + 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) { + while ((child = SLIST_POP(&children))) { + opt_delete(opt, "%pe [short-circuit]\n", child); + } + } + } + + 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); + + while (!SLIST_EMPTY(&children)) { + struct bfs_expr *child = SLIST_POP(&children); + + if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) { + opt_delete(opt, "%pe [ignored result]\n", child); + opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + 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; + + 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; + } + + // Skip reordering the first time through the passes, to + // make warnings more understandable + if (passes[j].visitor == &reorder) { + if (i == 0) { + continue; + } else { + nested.warn = false; + } + } + + expr = visit(&nested, expr, passes[j].visitor); + if (!expr) { + return NULL; + } + } + + opt_leave(&nested, NULL); + + if (!bfs_expr_is_parent(expr)) { + break; } } + *opt->impure = impure; return expr; } +/** Estimate the odds of an expression calling stat(). */ +static float expr_stat_odds(struct bfs_expr *expr) { + if (expr->calls_stat) { + return 1.0; + } + + float nostat_odds = 1.0; + float reached_odds = 1.0; + for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + float child_odds = expr_stat_odds(child); + nostat_odds *= 1.0 - reached_odds * child_odds; + + 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 - nostat_odds; +} + +/** 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 - expr_stat_odds(ctx->exclude); + + float reached_odds = 1.0 - ctx->exclude->probability; + float expr_odds = expr_stat_odds(ctx->expr); + nostat_odds *= 1.0 - reached_odds * expr_odds; + + return 1.0 - nostat_odds; +} + 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); + } + + } + + 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. @@ -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 fc30cd2..a1155c0 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 @@ -21,28 +8,29 @@ * flags like always-true options, and skipping over paths wherever they appear. */ +#include "prelude.h" #include "parse.h" +#include "alloc.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.h" #include "ctx.h" -#include "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 "xregex.h" #include "xspawn.h" #include "xtime.h" -#include <assert.h> #include <errno.h> #include <fcntl.h> #include <fnmatch.h> @@ -50,183 +38,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; - - // Prevent bfs_expr_free() from freeing uninitialized pointers on error paths - if (bfs_expr_has_children(expr)) { - expr->lhs = NULL; - expr->rhs = NULL; - } else if (eval_fn == eval_exec) { - expr->exec = NULL; - } else if (eval_fn == eval_fprintf) { - expr->printf = NULL; - } else if (eval_fn == eval_regex) { - expr->regex = NULL; - } - - 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. */ @@ -237,9 +63,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. */ @@ -271,20 +97,20 @@ struct parser_state { char **last_arg; /** A "-depth"-type argument, if any. */ char **depth_arg; + /** A "-limit" argument, if any. */ + char **limit_arg; /** A "-prune" argument, if any. */ char **prune_arg; /** A "-mount" argument, if any. */ char **mount_arg; /** An "-xdev" argument, if any. */ char **xdev_arg; - /** A "-files0-from" argument, if any. */ - char **files0_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. */ + /** The current time (maybe modified by -daystart). */ struct timespec now; }; @@ -309,8 +135,8 @@ enum token_type { /** * 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. */ @@ -324,7 +150,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; } } @@ -332,30 +158,30 @@ 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, ...) { +attr(printf(2, 3)) +static void parse_error(const struct bfs_parser *parser, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + 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, ...) { +attr(printf(4, 5)) +static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); @@ -372,10 +198,10 @@ 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, ...) { +attr(printf(6, 7)) +static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); @@ -393,10 +219,10 @@ 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, ...) { +attr(printf(3, 4)) +static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + const struct bfs_ctx *ctx = parser->ctx; bfs_expr_error(ctx, expr); @@ -410,14 +236,14 @@ 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, ...) { +attr(printf(2, 3)) +static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + 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; } @@ -425,7 +251,7 @@ static bool parse_warning(const struct parser_state *state, const char *format, 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; } @@ -433,10 +259,10 @@ 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, ...) { +attr(printf(6, 7)) +static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); @@ -457,10 +283,10 @@ 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, ...) { +attr(printf(3, 4)) +static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { int error = errno; - const struct bfs_ctx *ctx = state->ctx; + const struct bfs_ctx *ctx = parser->ctx; if (!bfs_expr_warning(ctx, expr)) { return false; @@ -475,20 +301,58 @@ static bool parse_expr_warning(const struct parser_state *state, const struct bf } /** + * Allocate a new expression. + */ +static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv) { + struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv); + 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); + 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); + 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; @@ -498,7 +362,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; } @@ -517,7 +381,7 @@ static int expr_open(struct parser_state *state, struct bfs_expr *expr, const ch return 0; fail: - parse_expr_error(state, expr, "%m.\n"); + parse_expr_error(parser, expr, "%m.\n"); if (cfile) { cfclose(cfile); } else if (file) { @@ -529,15 +393,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, "%m.\n"); } return ret; } @@ -545,52 +409,53 @@ 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) { +static char **parser_advance(struct bfs_parser *parser, enum token_type type, size_t argc) { if (type != T_FLAG && type != T_PATH) { - state->expr_started = true; + parser->expr_started = true; } if (type != T_PATH) { - state->last_arg = state->argv; + parser->last_arg = parser->argv; } - char **argv = state->argv; - state->argv += argc; + 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; + parser->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; } @@ -600,7 +465,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, T_FLAG, 1); continue; } if (strcmp(arg, "-") != 0) { @@ -615,7 +480,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) { @@ -623,16 +488,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, T_PATH, 1); } } @@ -651,14 +516,18 @@ 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) { + // strtoll() skips leading spaces, but we want to reject them + if (xisspace(str[0])) { + goto bad; + } int base = flags & IF_BASE_MASK; if (base == 0) { base = 10; } + char *endptr; errno = 0; long long value = strtoll(str, &endptr, base); if (errno != 0) { @@ -669,6 +538,9 @@ static const char *parse_int(const struct parser_state *state, char **arg, const } } + // https://github.com/llvm/llvm-project/issues/64946 + sanitize_init(&endptr); + if (endptr == str) { goto bad; } @@ -701,7 +573,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; } @@ -709,19 +581,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; } @@ -729,7 +601,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]) { @@ -746,7 +618,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); } /** @@ -768,136 +640,139 @@ 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, T_FLAG, argc); + return parse_new_expr(parser, eval_true, argc, argv); } /** * 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]; + const char *value = parser->argv[1]; + if (!value) { + parse_error(parser, "${cyn}%s${rs} needs a value.\n", arg); + return NULL; + } + + return parse_flag(parser, 2); } /** * 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, T_OPTION, argc); + return parse_new_expr(parser, eval_true, argc, argv); } /** * 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, T_TEST, argc); + return parse_new_expr(parser, eval_fn, argc, argv); } /** * 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, T_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); } /** * 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; } @@ -933,22 +808,19 @@ 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); + struct bfs_expr *expr = parse_unary_flag(parser); + 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) { + for (const char *flag = expr->argv[1], *next; flag; flag = next) { size_t len = strcspn(flag, ","); if (flag[len]) { next = flag + len + 1; @@ -958,7 +830,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; @@ -976,7 +848,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; @@ -989,91 +861,74 @@ 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) { + struct bfs_expr *expr = parse_nullary_flag(parser); + if (!expr) { + return NULL; + } - if (strcmp(state->argv[0], "-Ofast") == 0) { + int *optlevel = &parser->ctx->optlevel; + + if (strcmp(expr->argv[0], "-Ofast") == 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, expr->argv[0] + 2, 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", expr->argv[0] + 2); } - 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 } @@ -1081,38 +936,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; @@ -1121,19 +970,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) { @@ -1148,21 +996,21 @@ static struct bfs_expr *parse_time(struct parser_state *state, int field, int ar switch (*tail) { case 'w': time *= 7; - BFS_FALLTHROUGH; + fallthru; case 'd': time *= 24; - BFS_FALLTHROUGH; + fallthru; case 'h': time *= 60; - BFS_FALLTHROUGH; + fallthru; case 'm': time *= 60; - BFS_FALLTHROUGH; + fallthru; 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; @@ -1171,37 +1019,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 } @@ -1209,47 +1048,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/9699919799/utilities/V3_chap02.html#tag_18_13_01 + expr->literal = strcspn(expr->pattern, "?*\\[") == len; + + return expr; +} + +/** + * Parse -context. + */ +static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) { +#if BFS_CAN_CHECK_CONTEXT + struct bfs_expr *expr = parse_unary_test(parser, eval_context); + return parse_fnmatch(parser, expr, false); +#else + parse_error(parser, "Missing platform support.\n"); + return NULL; +#endif } /** * Parse -{false,true}. */ -static struct bfs_expr *parse_const(struct 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; @@ -1258,100 +1162,87 @@ 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) { + parser->ctx->flags |= BFTW_POST_ORDER; + parser->depth_arg = parser->argv; + return parse_nullary_action(parser, eval_delete); } /** * 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 arg1, int arg2) { + parser->ctx->flags |= BFTW_POST_ORDER; + parser->depth_arg = parser->argv; + return parse_nullary_flag(parser); } /** * 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) { - return NULL; - } - - expr->cost = 2000.0; - expr->probability = 0.01; - - 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; +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; } - - expr->ephemeral_fds = 1; - return expr; } /** * 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_exec *execbuf = bfs_exec_parse(parser->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; @@ -1359,23 +1250,42 @@ 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; + if (execbuf->flags & BFS_EXEC_CHDIR) { + // Check for relative paths in $PATH + const char *path = getenv("PATH"); + while (path) { + if (*path != '/') { + size_t len = strcspn(path, ":"); + char *comp = strndup(path, len); + if (comp) { + parse_expr_error(parser, expr, + "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp); + free(comp); + } else { + parse_perror(parser, "strndup()"); + } + return NULL; + } + + path = strchr(path, ':'); + if (path) { + ++path; + } + } + + // 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; + parser->ok_expr = expr; } return expr; @@ -1384,18 +1294,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; @@ -1404,34 +1313,29 @@ 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_expr *expr = parse_unary_flag(parser); + if (!expr) { return NULL; } - if (parse_root(state, path) != 0) { + 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); +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; } - state->files0_arg = parser_advance(state, T_OPTION, 1); + const char *from = expr->argv[1]; FILE *file; if (strcmp(from, "-") == 0) { @@ -1440,46 +1344,48 @@ static struct bfs_expr *parse_files0_from(struct parser_state *state, int arg1, file = xfopen(from, O_RDONLY | O_CLOEXEC); } if (!file) { - parse_error(state, "%m.\n"); + parse_expr_error(parser, expr, "%m.\n"); 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; + goto fail; + } else { + break; } - break; } - int ret = parse_root(state, path); + int ret = parse_root(parser, path); free(path); if (ret != 0) { - expr = NULL; - break; + goto fail; } } if (file == stdin) { - state->files0_stdin_arg = state->files0_arg; + parser->files0_stdin_arg = expr->argv; } else { fclose(file); } - state->implicit_root = false; - parser_advance(state, T_OPTION, 1); + parser->implicit_root = false; return expr; + +fail: + if (file != stdin) { + fclose(file); + } + return NULL; } /** * 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; } @@ -1501,11 +1407,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; } @@ -1515,351 +1420,300 @@ 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; 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: %m.\n"); 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 group *grp = bfs_getgrnam(state->ctx->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(state, expr, "%m.\n"); - goto fail; + parse_expr_error(parser, expr, "%m.\n"); + 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 passwd *pwd = bfs_getpwnam(state->ctx->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(state, expr, "%m.\n"); - goto fail; + parse_expr_error(parser, expr, "%m.\n"); + 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) { + struct bfs_expr *expr = parse_nullary_flag(parser); + if (!expr) { + return NULL; } + + unsigned int n; + if (!parse_int(parser, expr->argv, expr->argv[0] + 2, &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; + } + + if (expr->num <= 0) { + parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]); + return NULL; + } + + parser->limit_arg = expr->argv; 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; - } + init_print_expr(parser, expr); + return expr; +} - // 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; +/** + * 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; } - expr->cost = 400.0; - - if (strchr(pattern, '*')) { - expr->probability = 0.5; - } else { - expr->probability = 0.1; - } + parse_expr_warning(parser, expr, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", expr->argv[0]); + bfs_warning(parser->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n"); + parser->ctx->flags |= BFTW_PRUNE_MOUNTS; + parser->mount_arg = expr->argv; return expr; } /** * 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. */ @@ -1879,20 +1733,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) { +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, "%m.\n"); 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; } @@ -1901,18 +1755,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; } @@ -1926,138 +1780,109 @@ 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 -nogroup. */ -static struct bfs_expr *parse_nogroup(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_nogroup); - if (!expr) { - return NULL; +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) { + // Who knows how many FDs getgrgid_r() needs? + expr->ephemeral_fds = 3; } - - expr->cost = STAT_COST; - expr->probability = 0.01; - - // Who knows how many FDs getgrgid_r() needs? Probably at least one for - // /etc/group - expr->ephemeral_fds = 1; - return expr; } /** * 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); 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}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", - BFS_COMMAND, state->argv[0]); - return parse_nullary_option(state); +static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) { + parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", + BFS_COMMAND, parser->argv[0]); + return parse_nullary_option(parser); } /** * Parse -nouser. */ -static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_test(state, eval_nouser); - if (!expr) { - return NULL; +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) { + // Who knows how many FDs getpwuid_r() needs? + expr->ephemeral_fds = 3; } - - expr->cost = STAT_COST; - expr->probability = 0.01; - - // Who knows how many FDs getpwuid_r() needs? Probably at least one for - // /etc/passwd - expr->ephemeral_fds = 1; - return expr; } /** * 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) { @@ -2089,7 +1914,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct // // PERMCOPY : "u" | "g" | "o" - // State machine state + // Parser machine parser enum { MODE_CLAUSE, MODE_WHO, @@ -2097,25 +1922,25 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct MODE_ACTION_APPLY, MODE_OP, MODE_PERM, - } mstate = MODE_CLAUSE; + } mparser = 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 file_change uninit(0); + mode_t dir_change uninit(0); const char *i = mode; while (true) { - switch (mstate) { + switch (mparser) { case MODE_CLAUSE: who = 0; - mstate = MODE_WHO; - BFS_FALLTHROUGH; + mparser = MODE_WHO; + fallthru; case MODE_WHO: switch (*i) { @@ -2132,7 +1957,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct who |= 0777; break; default: - mstate = MODE_ACTION; + mparser = MODE_ACTION; continue; } break; @@ -2142,7 +1967,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; + fallthru; case MODE_PLUS: expr->file_mode |= file_change; expr->dir_mode |= dir_change; @@ -2152,7 +1977,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct expr->dir_mode &= ~dir_change; break; } - BFS_FALLTHROUGH; + fallthru; case MODE_ACTION: if (who == 0) { @@ -2162,27 +1987,27 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct switch (*i) { case '+': op = MODE_PLUS; - mstate = MODE_OP; + mparser = MODE_OP; break; case '-': op = MODE_MINUS; - mstate = MODE_OP; + mparser = MODE_OP; break; case '=': op = MODE_EQUALS; - mstate = MODE_OP; + mparser = MODE_OP; break; case ',': - if (mstate == MODE_ACTION_APPLY) { - mstate = MODE_CLAUSE; + if (mparser == MODE_ACTION_APPLY) { + mparser = MODE_CLAUSE; } else { goto fail; } break; case '\0': - if (mstate == MODE_ACTION_APPLY) { + if (mparser == MODE_ACTION_APPLY) { goto done; } else { goto fail; @@ -2211,7 +2036,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct default: file_change = 0; dir_change = 0; - mstate = MODE_PERM; + mparser = MODE_PERM; continue; } @@ -2219,7 +2044,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct file_change &= who; dir_change |= (dir_change << 6) | (dir_change << 3); dir_change &= who; - mstate = MODE_ACTION_APPLY; + mparser = MODE_ACTION_APPLY; break; case MODE_PERM: @@ -2234,7 +2059,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct break; case 'x': file_change |= who & 0111; - BFS_FALLTHROUGH; + fallthru; case 'X': dir_change |= who & 0111; break; @@ -2255,7 +2080,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct } break; default: - mstate = MODE_ACTION_APPLY; + mparser = MODE_ACTION_APPLY; continue; } break; @@ -2268,15 +2093,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 +2122,26 @@ static struct bfs_expr *parse_perm(struct parser_state *state, int field, int ar ++mode; break; } - BFS_FALLTHROUGH; + fallthru; 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 +2149,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 +2160,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 +2178,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,115 +2189,98 @@ 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); - } - return expr; +static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) { + parser->prune_arg = parser->argv; + return parse_nullary_action(parser, eval_prune); } /** * 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, "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; -#if BFS_WITH_ONIGURUMA + parser->regex_type = BFS_REGEX_POSIX_EXTENDED; +#if BFS_USE_ONIGURUMA } 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; #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"); -#if BFS_WITH_ONIGURUMA +#if BFS_USE_ONIGURUMA cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n"); cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); #endif @@ -2490,51 +2291,44 @@ list_types: /** * 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); + struct bfs_expr *expr = parse_unary_flag(parser); + if (!expr) { + cfprintf(cfile, "\n"); goto list_strategies; } - parser_advance(state, T_FLAG, 1); - + const char *arg = expr->argv[1]; if (strcmp(arg, "bfs") == 0) { ctx->strategy = BFTW_BFS; } else if (strcmp(arg, "dfs") == 0) { @@ -2544,16 +2338,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 +2360,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 +2420,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 +2505,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 +2536,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 +2548,10 @@ 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) { + parser->ctx->flags |= BFTW_PRUNE_MOUNTS; + parser->xdev_arg = parser->argv; + return parse_nullary_option(parser); } /** @@ -2874,7 +2611,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,21 +2652,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}%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); + "${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"); @@ -2958,7 +2696,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"); @@ -2998,7 +2738,7 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg 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}%s${rs} detects that the file tree is modified\n", - BFS_COMMAND); + 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"); @@ -3041,6 +2781,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"); @@ -3143,6 +2887,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"); @@ -3169,26 +2915,26 @@ 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; } /** * "Parse" -version. */ -static struct bfs_expr *parse_version(struct parser_state *state, int arg1, int arg2) { - cfprintf(state->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, BFS_VERSION); +static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) { + cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version); printf("%s\n", BFS_HOMEPAGE); - state->just_info = true; + parser->just_info = true; return NULL; } -typedef struct bfs_expr *parse_fn(struct parser_state *state, int arg1, int arg2); +typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2); /** * An entry in the parse table for primary expressions. @@ -3232,6 +2978,7 @@ static const struct table_entry parse_table[] = { {"-cmin", T_TEST, parse_min, BFS_STAT_CTIME}, {"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME}, {"-color", T_OPTION, parse_color, true}, + {"-context", T_TEST, parse_context, true}, {"-csince", T_TEST, parse_since, BFS_STAT_CTIME}, {"-ctime", T_TEST, parse_time, BFS_STAT_CTIME}, {"-d", T_FLAG, parse_depth}, @@ -3265,6 +3012,8 @@ static const struct table_entry parse_table[] = { {"-ipath", T_TEST, parse_path, true}, {"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE}, {"-iwholename", T_TEST, parse_path, true}, + {"-j", T_FLAG, parse_jobs, 0, 0, true}, + {"-limit", T_ACTION, parse_limit}, {"-links", T_TEST, parse_links}, {"-lname", T_TEST, parse_lname, false}, {"-ls", T_ACTION, parse_ls}, @@ -3364,9 +3113,9 @@ 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) { // Paths are already skipped at this point - const char *arg = state->argv[0]; + const char *arg = parser->argv[0]; if (arg[0] != '-') { goto unexpected; @@ -3383,8 +3132,8 @@ static struct bfs_expr *parse_primary(struct parser_state *state) { match = table_lookup_fuzzy(arg); - CFILE *cerr = state->ctx->cerr; - parse_error(state, "Unknown argument; did you mean "); + CFILE *cerr = parser->ctx->cerr; + parse_error(parser, "Unknown argument; did you mean "); switch (match->type) { case T_FLAG: cfprintf(cerr, "${cyn}%s${rs}?", match->arg); @@ -3397,7 +3146,7 @@ static struct bfs_expr *parse_primary(struct parser_state *state) { break; } - if (!state->interactive || !match->parse) { + if (!parser->interactive || !match->parse) { fprintf(stderr, "\n"); goto unmatched; } @@ -3408,16 +3157,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; } @@ -3427,71 +3176,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, T_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, T_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, T_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; - - if (parse_exclude(state, factor) != 0) { - return NULL; - } + parser->excluding = false; - return &bfs_true; + bfs_expr_append(parser->ctx->exclude, factor); + return parse_new_expr(parser, eval_true, parser->argv - argv, argv); } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) { - char **argv = parser_advance(state, T_OPERATOR, 1); + char **argv = parser_advance(parser, T_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); } } @@ -3501,16 +3245,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; } @@ -3523,17 +3266,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, T_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; @@ -3544,16 +3286,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; } @@ -3562,16 +3303,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, T_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; @@ -3581,16 +3321,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; } @@ -3599,16 +3338,15 @@ static struct bfs_expr *parse_expr(struct parser_state *state) { break; } - char **argv = parser_advance(state, T_OPERATOR, 1); + char **argv = parser_advance(parser, T_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; @@ -3617,71 +3355,129 @@ static struct bfs_expr *parse_expr(struct parser_state *state) { /** * 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) { + if (skip_paths(parser) != 0) { return NULL; } - struct bfs_expr *expr = &bfs_true; - if (state->argv[0]) { - expr = parse_expr(state); - if (!expr) { - return NULL; - } + struct bfs_expr *expr; + if (parser->argv[0]) { + expr = parse_expr(parser); + } else { + expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg); + } + if (!expr) { + return NULL; } - if (state->argv[0]) { - parse_error(state, "Unexpected argument.\n"); - goto fail; + if (parser->argv[0]) { + parse_error(parser, "Unexpected argument.\n"); + return NULL; } - if (state->implicit_print) { - struct bfs_expr *print = bfs_expr_new(eval_fprint, 1, &fake_print_arg); + if (parser->implicit_print) { + char **limit = parser->limit_arg; + if (limit) { + parse_argv_error(parser, parser->limit_arg, 2, + "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n", + limit[0], limit[0], limit[1]); + return NULL; + } + + struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg); 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_arg && parser->xdev_arg) { + parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1, + "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", + parser->xdev_arg[0], parser->mount_arg[0]); } - if (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 (parser->ctx->warn && parser->depth_arg && parser->prune_arg) { + parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1, + "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", + parser->prune_arg[0], parser->depth_arg[0]); - if (state->interactive) { - bfs_warning(state->ctx, "Do you want to continue? "); + if (parser->interactive) { + bfs_warning(parser->ctx, "Do you want to continue? "); if (ynprompt() == 0) { - goto fail; + 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; + if (parser->ok_expr && parser->files0_stdin_arg) { + parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2, + "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", + parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]); + return NULL; } return expr; +} -fail: - bfs_expr_free(expr); - return NULL; +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"; + } + + bfs_bug("Invalid strategy"); + return "???"; +} + +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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) { + 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) { @@ -3691,51 +3487,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)); @@ -3745,61 +3527,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); } /** @@ -3808,34 +3582,32 @@ 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); + 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_COMMAND, 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]; - } 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; } @@ -3872,7 +3644,7 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { ctx->warn = stdin_tty; } - struct parser_state state = { + struct bfs_parser parser = { .ctx = ctx, .argv = ctx->argv + 1, .command = ctx->argv[0], @@ -3889,40 +3661,36 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { .prune_arg = NULL, .mount_arg = NULL, .xdev_arg = NULL, - .files0_arg = NULL, .files0_stdin_arg = NULL, .ok_expr = NULL, + .now = ctx->now, }; - if (strcmp(xbasename(state.command), "find") == 0) { - // Operate depth-first when invoked as "find" - ctx->strategy = BFTW_DFS; - } - - if (xgettime(&state.now) != 0) { - parse_perror(&state, "xgettime()"); + ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg); + 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 (parser.use_color == COLOR_AUTO && !ctx->colors) { + bfs_warning(ctx, "Error parsing $$LS_COLORS: %s.\n\n", xstrerror(ctx->colors_error)); + } + if (bfs_optimize(ctx) != 0) { + bfs_perror(ctx, "bfs_optimize()"); goto fail; } - 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) { + if (ctx->npaths == 0 && parser.implicit_root) { + if (parse_root(&parser, ".") != 0) { goto fail; } } diff --git a/src/parse.h b/src/parse.h index 7e29a03..6895c9f 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. diff --git a/src/prelude.h b/src/prelude.h new file mode 100644 index 0000000..72f88b0 --- /dev/null +++ b/src/prelude.h @@ -0,0 +1,370 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Configuration and feature/platform detection. + */ + +#ifndef BFS_PRELUDE_H +#define BFS_PRELUDE_H + +// Possible __STDC_VERSION__ values + +#define C95 199409L +#define C99 199901L +#define C11 201112L +#define C17 201710L +#define C23 202311L + +#include <stddef.h> + +#if __STDC_VERSION__ < C23 +# include <stdalign.h> +# include <stdbool.h> +# include <stdnoreturn.h> +#endif + +// bfs packaging configuration + +#include "config.h" + +#ifndef BFS_COMMAND +# define BFS_COMMAND "bfs" +#endif +#ifndef BFS_HOMEPAGE +# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html" +#endif + +// This is a symbol instead of a literal so we don't have to rebuild everything +// when the version number changes +extern const char bfs_version[]; + +// Check for system headers + +#ifdef __has_include + +#if __has_include(<mntent.h>) +# define BFS_HAS_MNTENT_H true +#endif +#if __has_include(<paths.h>) +# define BFS_HAS_PATHS_H true +#endif +#if __has_include(<sys/extattr.h>) +# define BFS_HAS_SYS_EXTATTR_H true +#endif +#if __has_include(<sys/mkdev.h>) +# define BFS_HAS_SYS_MKDEV_H true +#endif +#if __has_include(<sys/param.h>) +# define BFS_HAS_SYS_PARAM_H true +#endif +#if __has_include(<sys/sysmacros.h>) +# define BFS_HAS_SYS_SYSMACROS_H true +#endif +#if __has_include(<sys/xattr.h>) +# define BFS_HAS_SYS_XATTR_H true +#endif +#if __has_include(<threads.h>) +# define BFS_HAS_THREADS_H true +#endif +#if __has_include(<util.h>) +# define BFS_HAS_UTIL_H true +#endif + +#else // !__has_include + +#define BFS_HAS_MNTENT_H __GLIBC__ +#define BFS_HAS_PATHS_H true +#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__ +#define BFS_HAS_SYS_MKDEV_H false +#define BFS_HAS_SYS_PARAM_H true +#define BFS_HAS_SYS_SYSMACROS_H __GLIBC__ +#define BFS_HAS_SYS_XATTR_H __linux__ +#define BFS_HAS_THREADS_H (!__STDC_NO_THREADS__) +#define BFS_HAS_UTIL_H __NetBSD__ + +#endif // !__has_include + +#ifndef BFS_USE_MNTENT_H +# define BFS_USE_MNTENT_H BFS_HAS_MNTENT_H +#endif +#ifndef BFS_USE_PATHS_H +# define BFS_USE_PATHS_H BFS_HAS_PATHS_H +#endif +#ifndef BFS_USE_SYS_EXTATTR_H +# define BFS_USE_SYS_EXTATTR_H BFS_HAS_SYS_EXTATTR_H +#endif +#ifndef BFS_USE_SYS_MKDEV_H +# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H +#endif +#ifndef BFS_USE_SYS_PARAM_H +# define BFS_USE_SYS_PARAM_H BFS_HAS_SYS_PARAM_H +#endif +#ifndef BFS_USE_SYS_SYSMACROS_H +# define BFS_USE_SYS_SYSMACROS_H BFS_HAS_SYS_SYSMACROS_H +#endif +#ifndef BFS_USE_SYS_XATTR_H +# define BFS_USE_SYS_XATTR_H BFS_HAS_SYS_XATTR_H +#endif +#ifndef BFS_USE_THREADS_H +# define BFS_USE_THREADS_H BFS_HAS_THREADS_H +#endif +#ifndef BFS_USE_UTIL_H +# define BFS_USE_UTIL_H BFS_HAS_UTIL_H +#endif + +// Stub out feature detection on old/incompatible compilers + +#ifndef __has_feature +# define __has_feature(feat) false +#endif + +#ifndef __has_c_attribute +# define __has_c_attribute(attr) false +#endif + +#ifndef __has_attribute +# define __has_attribute(attr) false +#endif + +// Fundamental utilities + +/** + * Get the length of an array. + */ +#define countof(array) (sizeof(array) / sizeof(0[array])) + +/** + * False sharing/destructive interference/largest cache line size. + */ +#ifdef __GCC_DESTRUCTIVE_SIZE +# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE +#else +# define FALSE_SHARING_SIZE 64 +#endif + +/** + * True sharing/constructive interference/smallest cache line size. + */ +#ifdef __GCC_CONSTRUCTIVE_SIZE +# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE +#else +# define TRUE_SHARING_SIZE 64 +#endif + +/** + * Polyfill max_align_t if we don't already have it. + */ +#if !BFS_HAS_MAX_ALIGN_T +typedef union { +# ifdef __BIGGEST_ALIGNMENT__ + alignas(__BIGGEST_ALIGNMENT__) char c; +# else + long double ld; + long long ll; + void *ptr; +# endif +} max_align_t; +#endif + +/** + * Alignment specifier that avoids false sharing. + */ +#define cache_align alignas(FALSE_SHARING_SIZE) + +// Wrappers for attributes + +/** + * Silence warnings about switch/case fall-throughs. + */ +#if __has_attribute(fallthrough) +# define fallthru __attribute__((fallthrough)) +#else +# define fallthru ((void)0) +#endif + +/** + * Silence warnings about unused declarations. + */ +#if __has_attribute(unused) +# define attr_maybe_unused __attribute__((unused)) +#else +# define attr_maybe_unused +#endif + +/** + * Warn if a value is unused. + */ +#if __has_attribute(warn_unused_result) +# define attr_nodiscard __attribute__((warn_unused_result)) +#else +# define attr_nodiscard +#endif + +/** + * Hint to avoid inlining a function. + */ +#if __has_attribute(noinline) +# define attr_noinline __attribute__((noinline)) +#else +# define attr_noinline +#endif + +/** + * Hint that a function is unlikely to be called. + */ +#if __has_attribute(cold) +# define attr_cold attr_noinline __attribute__((cold)) +#else +# define attr_cold attr_noinline +#endif + +/** + * Adds compiler warnings for bad printf()-style function calls, if supported. + */ +#if __has_attribute(format) +# define attr_printf(fmt, args) __attribute__((format(printf, fmt, args))) +#else +# define attr_printf(fmt, args) +#endif + +/** + * Annotates functions that potentially modify and return format strings. + */ +#if __has_attribute(format_arg) +# define attr_format_arg(arg) __attribute__((format_arg(arg))) +#else +# define attr_format_arg(args) +#endif + +/** + * Annotates allocator-like functions. + */ +#if __has_attribute(malloc) +# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC +# define attr_malloc(...) attr_nodiscard __attribute__((malloc(__VA_ARGS__))) +# else +# define attr_malloc(...) attr_nodiscard __attribute__((malloc)) +# endif +#else +# define attr_malloc(...) attr_nodiscard +#endif + +/** + * Specifies that a function returns allocations with a given alignment. + */ +#if __has_attribute(alloc_align) +# define attr_alloc_align(param) __attribute__((alloc_align(param))) +#else +# define attr_alloc_align(param) +#endif + +/** + * Specifies that a function returns allocations with a given size. + */ +#if __has_attribute(alloc_size) +# define attr_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__))) +#else +# define attr_alloc_size(...) +#endif + +/** + * Shorthand for attr_alloc_align() and attr_alloc_size(). + */ +#define attr_aligned_alloc(align, ...) \ + attr_alloc_align(align) \ + attr_alloc_size(__VA_ARGS__) + +/** + * Check if function multiversioning via GNU indirect functions (ifunc) is supported. + */ +#ifndef BFS_USE_TARGET_CLONES +# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) +# define BFS_USE_TARGET_CLONES true +# endif +#endif + +/** + * Apply the target_clones attribute, if available. + */ +#if BFS_USE_TARGET_CLONES +# define attr_target_clones(...) __attribute__((target_clones(__VA_ARGS__))) +#else +# define attr_target_clones(...) +#endif + +/** + * Shorthand for multiple attributes at once. attr(a, b(c), d) is equivalent to + * + * attr_a + * attr_b(c) + * attr_d + */ +#define attr(...) \ + attr__(attr_##__VA_ARGS__, none, none, none, none, none, none, none, none, none, ) + +/** + * attr() helper. For exposition, pretend we support only 2 args, instead of 9. + * There are a few cases: + * + * attr() + * => attr__(attr_, none, none) + * => attr_ => + * attr_none => + * attr_too_many_none() => + * + * attr(a) + * => attr__(attr_a, none, none) + * => attr_a => __attribute__((a)) + * attr_none => + * attr_too_many_none() => + * + * attr(a, b(c)) + * => attr__(attr_a, b(c), none, none) + * => attr_a => __attribute__((a)) + * attr_b(c) => __attribute__((b(c))) + * attr_too_many_none(none) => + * + * attr(a, b(c), d) + * => attr__(attr_a, b(c), d, none, none) + * => attr_a => __attribute__((a)) + * attr_b(c) => __attribute__((b(c))) + * attr_too_many_d(none, none) => error + * + * Some attribute names are the same as standard library functions, e.g. printf. + * Standard libraries are permitted to define these functions as macros, like + * + * #define printf(...) __builtin_printf(__VA_ARGS__) + * + * The token paste in + * + * #define attr(...) attr__(attr_##__VA_ARGS__, none, none) + * + * is necessary to prevent macro expansion before evaluating attr__(). + * Otherwise, we could get + * + * attr(printf(1, 2)) + * => attr__(__builtin_printf(1, 2), none, none) + * => attr____builtin_printf(1, 2) + * => error + */ +#define attr__(a1, a2, a3, a4, a5, a6, a7, a8, a9, none, ...) \ + a1 \ + attr_##a2 \ + attr_##a3 \ + attr_##a4 \ + attr_##a5 \ + attr_##a6 \ + attr_##a7 \ + attr_##a8 \ + attr_##a9 \ + attr_too_many_##none(__VA_ARGS__) + +// Ignore `attr_none` from expanding 1-9 argument attr(a1, a2, ...) +#define attr_none +// Ignore `attr_` from expanding 0-argument attr() +#define attr_ +// Only trigger an error on more than 9 arguments +#define attr_too_many_none(...) + +#endif // BFS_PRELUDE_H diff --git a/src/printf.c b/src/printf.c index 6a7b9a9..f8428f7 100644 --- a/src/printf.c +++ b/src/printf.c @@ -1,59 +1,46 @@ -/**************************************************************************** - * 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 "prelude.h" #include "printf.h" +#include "alloc.h" #include "bfstd.h" #include "bftw.h" #include "color.h" -#include "config.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 "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. */ @@ -62,10 +49,20 @@ struct bfs_printf { 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; @@ -73,26 +70,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. */ +attr(format_arg(2)) +static const char *dyn_fmt(const char *str, const char *fake) { + bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0, + "Mismatched format specifiers: '%s' vs. '%s'", str, fake); + return str; +} + +/** Wrapper for fprintf(). */ +attr(printf(3, 4)) +static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) { + va_list args; + va_start(args, fake); + int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args); + va_end(args); + return ret; +} + /** %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"}; @@ -102,69 +117,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); @@ -174,102 +189,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; } - struct bfs_groups *groups = directive->ptr; + 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; @@ -291,10 +317,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); @@ -302,48 +328,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); } @@ -356,23 +382,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; @@ -380,37 +406,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; } @@ -420,23 +446,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; @@ -446,36 +472,36 @@ 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; } - struct bfs_users *users = directive->ptr; + 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) { @@ -492,51 +518,45 @@ static const char *bfs_printf_type(enum bfs_type type) { return "p"; case BFS_LNK: return "l"; + case BFS_PORT: + return "P"; case BFS_REG: return "f"; case BFS_SOCK: return "s"; + case BFS_WHT: + return "w"; default: return "U"; } } /** %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_is_like(ELOOP)) { + str = "L"; + } else if (errno_is_like(ENOENT)) { + str = "N"; + } else { + str = "?"; error = errno; - break; } + } else { + str = bfs_printf_type(type); } - int ret = fprintf(cfile->file, directive->str, type); + int ret = bfs_fprintf(cfile, fmt, "%s", str); if (error != 0) { ret = -1; errno = error; @@ -544,24 +564,36 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const return ret; } +/** %Z: SELinux context */ +attr(maybe_unused) +static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) { + char *con = bfs_getfilecon(ftwbuf); + if (!con) { + return -1; + } + + int ret = bfs_fprintf(cfile, fmt, "%s", con); + bfs_freecon(con); + return ret; +} + /** * Append a literal string to the chain. */ -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()"); @@ -574,23 +606,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; @@ -624,10 +662,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; @@ -649,15 +687,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"; @@ -672,17 +710,17 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha case '0': case '+': must_be_numeric = true; - BFS_FALLTHROUGH; + fallthru; case ' ': 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; } @@ -692,9 +730,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; } @@ -702,9 +740,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'); @@ -712,163 +750,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.fn = bfs_printf_F; - directive.ptr = (void *)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; } break; case 'g': - directive.fn = bfs_printf_g; - directive.ptr = ctx->groups; + 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.fn = bfs_printf_u; - directive.ptr = ctx->users; + 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; } @@ -880,7 +927,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); @@ -896,9 +943,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; } @@ -909,8 +956,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..2bff087 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. diff --git a/src/pwcache.c b/src/pwcache.c index 868ec8f..af8c237 100644 --- a/src/pwcache.c +++ b/src/pwcache.c @@ -1,26 +1,13 @@ -/**************************************************************************** - * 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 "prelude.h" #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> #include <unistd.h> @@ -29,54 +16,58 @@ static void *MISSING = &MISSING; /** Callback type for bfs_getent(). */ -typedef void *bfs_getent_fn(const void *key, void *ent, void *buf, size_t bufsize); +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(struct trie_leaf *leaf, bfs_getent_fn *fn, const void *key, size_t entsize, size_t bufsize) { +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; } - void *buf = NULL; - while (true) { - void *result = buf; - buf = realloc(buf, entsize + bufsize); - if (!buf) { - free(result); - return NULL; - } + // _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; + } - result = fn(key, buf, (char *)buf + entsize, bufsize); - if (result) { - leaf->value = result; - return result; + while (true) { + void *ret = fn(key, ptr, bufsize); + if (ret) { + leaf->value = ret; + return ret; } else if (errno == 0) { - free(buf); leaf->value = MISSING; - return NULL; + break; } else if (errno == ERANGE) { - bufsize *= 2; + void *next = varena_grow(varena, ptr, &bufsize); + if (!next) { + break; + } + ptr = next; } else { - free(buf); - return NULL; + break; } } -} -/** Flush a single cache. */ -static void bfs_pwcache_flush(struct trie *trie) { - TRIE_FOR_EACH(trie, leaf) { - if (leaf->value != MISSING) { - free(leaf->value); - } - trie_remove(trie, leaf); - } + varena_free(varena, ptr, bufsize); + return NULL; } +/** + * An arena-allocated struct passwd. + */ +struct bfs_passwd { + struct passwd pwd; + char buf[]; +}; + struct bfs_users { - /** Initial buffer size for getpw*_r(). */ - size_t bufsize; + /** bfs_passwd arena. */ + struct varena varena; /** A map from usernames to entries. */ struct trie by_name; /** A map from UIDs to entries. */ @@ -84,28 +75,24 @@ struct bfs_users { }; struct bfs_users *bfs_users_new(void) { - struct bfs_users *users = malloc(sizeof(*users)); + struct bfs_users *users = ALLOC(struct bfs_users); if (!users) { return NULL; } - long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); - if (bufsize > 0) { - users->bufsize = bufsize; - } else { - users->bufsize = 1024; - } - + VARENA_INIT(&users->varena, struct bfs_passwd, buf); trie_init(&users->by_name); trie_init(&users->by_uid); return users; } /** bfs_getent() callback for getpwnam_r(). */ -static void *bfs_getpwnam_impl(const void *key, void *ent, void *buf, size_t bufsize) { - struct passwd *result; - errno = getpwnam_r(key, ent, buf, bufsize, &result); - return result; +static void *bfs_getpwnam_impl(const void *key, void *ptr, size_t bufsize) { + struct bfs_passwd *storage = ptr; + + struct passwd *ret = NULL; + errno = getpwnam_r(key, &storage->pwd, storage->buf, bufsize, &ret); + return ret; } const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name) { @@ -114,14 +101,17 @@ const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name) { return NULL; } - return bfs_getent(leaf, bfs_getpwnam_impl, name, sizeof(struct passwd), users->bufsize); + return bfs_getent(bfs_getpwnam_impl, name, leaf, &users->varena); } /** bfs_getent() callback for getpwuid_r(). */ -static void *bfs_getpwuid_impl(const void *key, void *ent, void *buf, size_t bufsize) { - struct passwd *result; - errno = getpwuid_r(*(const uid_t *)key, ent, buf, bufsize, &result); - return result; +static void *bfs_getpwuid_impl(const void *key, void *ptr, size_t bufsize) { + const uid_t *uid = key; + struct bfs_passwd *storage = ptr; + + struct passwd *ret = NULL; + errno = getpwuid_r(*uid, &storage->pwd, storage->buf, bufsize, &ret); + return ret; } const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid) { @@ -130,26 +120,35 @@ const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid) { return NULL; } - return bfs_getent(leaf, bfs_getpwuid_impl, &uid, sizeof(struct passwd), users->bufsize); + return bfs_getent(bfs_getpwuid_impl, &uid, leaf, &users->varena); } void bfs_users_flush(struct bfs_users *users) { - bfs_pwcache_flush(&users->by_name); - bfs_pwcache_flush(&users->by_uid); + trie_clear(&users->by_uid); + trie_clear(&users->by_name); + varena_clear(&users->varena); } void bfs_users_free(struct bfs_users *users) { if (users) { - bfs_users_flush(users); trie_destroy(&users->by_uid); trie_destroy(&users->by_name); + varena_destroy(&users->varena); free(users); } } +/** + * An arena-allocated struct group. + */ +struct bfs_group { + struct group grp; + char buf[]; +}; + struct bfs_groups { - /** Initial buffer size for getgr*_r(). */ - size_t bufsize; + /** bfs_group arena. */ + struct varena varena; /** A map from group names to entries. */ struct trie by_name; /** A map from GIDs to entries. */ @@ -157,28 +156,24 @@ struct bfs_groups { }; struct bfs_groups *bfs_groups_new(void) { - struct bfs_groups *groups = malloc(sizeof(*groups)); + struct bfs_groups *groups = ALLOC(struct bfs_groups); if (!groups) { return NULL; } - long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX); - if (bufsize > 0) { - groups->bufsize = bufsize; - } else { - groups->bufsize = 1024; - } - + VARENA_INIT(&groups->varena, struct bfs_group, buf); trie_init(&groups->by_name); trie_init(&groups->by_gid); return groups; } /** bfs_getent() callback for getgrnam_r(). */ -static void *bfs_getgrnam_impl(const void *key, void *ent, void *buf, size_t bufsize) { - struct group *result; - errno = getgrnam_r(key, ent, buf, bufsize, &result); - return result; +static void *bfs_getgrnam_impl(const void *key, void *ptr, size_t bufsize) { + struct bfs_group *storage = ptr; + + struct group *ret = NULL; + errno = getgrnam_r(key, &storage->grp, storage->buf, bufsize, &ret); + return ret; } const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name) { @@ -187,14 +182,17 @@ const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name) { return NULL; } - return bfs_getent(leaf, bfs_getgrnam_impl, name, sizeof(struct group), groups->bufsize); + return bfs_getent(bfs_getgrnam_impl, name, leaf, &groups->varena); } /** bfs_getent() callback for getgrgid_r(). */ -static void *bfs_getgrgid_impl(const void *key, void *ent, void *buf, size_t bufsize) { - struct group *result; - errno = getgrgid_r(*(const gid_t *)key, ent, buf, bufsize, &result); - return result; +static void *bfs_getgrgid_impl(const void *key, void *ptr, size_t bufsize) { + const gid_t *gid = key; + struct bfs_group *storage = ptr; + + struct group *ret = NULL; + errno = getgrgid_r(*gid, &storage->grp, storage->buf, bufsize, &ret); + return ret; } const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid) { @@ -203,19 +201,20 @@ const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid) { return NULL; } - return bfs_getent(leaf, bfs_getgrgid_impl, &gid, sizeof(struct group), groups->bufsize); + return bfs_getent(bfs_getgrgid_impl, &gid, leaf, &groups->varena); } void bfs_groups_flush(struct bfs_groups *groups) { - bfs_pwcache_flush(&groups->by_name); - bfs_pwcache_flush(&groups->by_gid); + trie_clear(&groups->by_gid); + trie_clear(&groups->by_name); + varena_clear(&groups->varena); } void bfs_groups_free(struct bfs_groups *groups) { if (groups) { - bfs_groups_flush(groups); trie_destroy(&groups->by_gid); trie_destroy(&groups->by_name); + varena_destroy(&groups->varena); free(groups); } } diff --git a/src/pwcache.h b/src/pwcache.h index f1ca0bf..b6c0b67 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}. diff --git a/src/sanity.h b/src/sanity.h new file mode 100644 index 0000000..e168b8f --- /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 "prelude.h" +#include <stddef.h> + +#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) +# define SANITIZE_ADDRESS true +#endif + +#if __has_feature(memory_sanitizer) || defined(__SANITIZE_MEMORY__) +# define SANITIZE_MEMORY true +#endif + +#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__) +# define SANITIZE_THREAD true +#endif + +// Call macro(ptr, size) or macro(ptr, sizeof(*ptr)) +#define SANITIZE_CALL(...) \ + SANITIZE_CALL_(__VA_ARGS__, ) + +#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__) + +#else +# define sanitize_alloc sanitize_uninit +# define sanitize_free sanitize_uninit +#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(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) +# define sanitize_uninit(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) +#endif + +/** + * Squelch unused variable warnings when not sanitizing. + */ +#define sanitize_ignore(ptr, size) ((void)(ptr), (void)(size)) + +/** + * Initialize a variable, unless sanitizers would detect uninitialized uses. + */ +#if SANITIZE_MEMORY +# define uninit(value) +#else +# define uninit(value) = value +#endif + +#endif // BFS_SANITY_H @@ -1,52 +1,32 @@ -/**************************************************************************** - * 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 "prelude.h" #include "stat.h" +#include "atomic.h" #include "bfstd.h" -#include "config.h" -#include <assert.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 defined(STATX_BASIC_STATS) && (!__ANDROID__ || __ANDROID_API__ >= 30) -# 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 +#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: @@ -71,60 +51,74 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) { return "modification time"; } - assert(!"Unrecognized stat field"); + bfs_bug("Unrecognized stat 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; - buf->dev = statbuf->st_dev; - buf->mask |= BFS_STAT_DEV; + if (flags & BFS_STAT_NOFOLLOW) { + ret |= AT_SYMLINK_NOFOLLOW; + } + +#ifdef AT_NO_AUTOMOUNT + ret |= AT_NO_AUTOMOUNT; +#endif + + return ret; +} + +void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) { + dest->mask = 0; + + dest->mode = src->st_mode; + dest->mask |= BFS_STAT_MODE; - buf->ino = statbuf->st_ino; - buf->mask |= BFS_STAT_INO; + dest->dev = src->st_dev; + dest->mask |= BFS_STAT_DEV; - buf->mode = statbuf->st_mode; - buf->mask |= BFS_STAT_TYPE | BFS_STAT_MODE; + dest->ino = src->st_ino; + dest->mask |= BFS_STAT_INO; - buf->nlink = statbuf->st_nlink; - buf->mask |= BFS_STAT_NLINK; + dest->nlink = src->st_nlink; + dest->mask |= BFS_STAT_NLINK; - buf->gid = statbuf->st_gid; - buf->mask |= BFS_STAT_GID; + dest->gid = src->st_gid; + dest->mask |= BFS_STAT_GID; - buf->uid = statbuf->st_uid; - buf->mask |= BFS_STAT_UID; + dest->uid = src->st_uid; + dest->mask |= BFS_STAT_UID; - buf->size = statbuf->st_size; - buf->mask |= BFS_STAT_SIZE; + dest->size = src->st_size; + dest->mask |= BFS_STAT_SIZE; - buf->blocks = statbuf->st_blocks; - buf->mask |= BFS_STAT_BLOCKS; + dest->blocks = src->st_blocks; + dest->mask |= BFS_STAT_BLOCKS; - buf->rdev = statbuf->st_rdev; - buf->mask |= BFS_STAT_RDEV; + dest->rdev = src->st_rdev; + dest->mask |= BFS_STAT_RDEV; -#if BSD - buf->attrs = statbuf->st_flags; - buf->mask |= BFS_STAT_ATTRS; +#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_BIRTHTIMESPEC + dest->btime = src->st_birthtimespec; + dest->mask |= BFS_STAT_BTIME; #endif } @@ -135,143 +129,141 @@ 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 __has_feature(memory_sanitizer) - // -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; +} + +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 = xmakedev(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 = xmakedev(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; + 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_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_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_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_CTIME) { + dest->ctime.tv_sec = src->stx_ctime.tv_sec; + dest->ctime.tv_nsec = src->stx_ctime.tv_nsec; + dest->mask |= BFS_STAT_CTIME; } - 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_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 ret; + 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 = 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; + } + + 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); @@ -280,62 +272,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__ || __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); + return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf); } - // Check __GNU__ to work around https://lists.gnu.org/archive/html/bug-hurd/2021-12/msg00001.html -#if defined(AT_EMPTY_PATH) && !__GNU__ - static 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; - } - } -#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) { @@ -354,7 +330,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,49 @@ #ifndef BFS_STAT_H #define BFS_STAT_H -#include "config.h" +#include "prelude.h" +#include <sys/stat.h> #include <sys/types.h> #include <time.h> +#if !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 BFS_USE_SYS_PARAM_H -# 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, }; /** @@ -73,14 +76,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 +83,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. */ @@ -138,6 +133,28 @@ 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); + +/** + * 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..3793896 --- /dev/null +++ b/src/thread.c @@ -0,0 +1,81 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "thread.h" +#include "bfstd.h" +#include "diag.h" +#include <errno.h> +#include <pthread.h> + +#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_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..db11bd8 --- /dev/null +++ b/src/thread.h @@ -0,0 +1,99 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Wrappers for POSIX threading APIs. + */ + +#ifndef BFS_THREAD_H +#define BFS_THREAD_H + +#include "prelude.h" +#include <pthread.h> + +#if __STDC_VERSION__ < C23 && !defined(thread_local) +# if BFS_USE_THREADS_H +# include <threads.h> +# else +# define thread_local _Thread_local +# endif +#endif + +/** Thread entry point type. */ +typedef void *thread_fn(void *arg); + +/** + * 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); + +/** + * 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-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 /** * This is an implementation of a "qp trie," as documented at @@ -94,31 +81,29 @@ * and insert intermediate singleton "jump" nodes when necessary. */ +#include "prelude.h" #include "trie.h" -#include "config.h" -#include <assert.h> -#include <limits.h> -#include <stdbool.h> +#include "alloc.h" +#include "bit.h" +#include "diag.h" +#include "list.h" #include <stdint.h> -#include <stdlib.h> #include <string.h> -#if __has_attribute(target_clones) && (__i386__ || __x86_64__) -# define TARGET_CLONES_POPCNT __attribute__((target_clones("popcnt", "default"))) -#else -# define TARGET_CLONES_POPCNT -#endif +bfs_static_assert(CHAR_WIDTH == 8); -#if CHAR_BIT != 8 -# error "This trie implementation assumes 8-bit bytes." +#if __i386__ || __x86_64__ +# define trie_clones attr(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. @@ -129,13 +114,13 @@ 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 @@ -152,53 +137,35 @@ static bool trie_is_leaf(uintptr_t ptr) { /** Decode a pointer to a leaf. */ static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { - assert(trie_is_leaf(ptr)); + bfs_assert(trie_is_leaf(ptr)); return (struct trie_leaf *)(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)); + bfs_assert(trie_is_leaf(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)); + bfs_assert(!trie_is_leaf(ptr)); return (struct trie_node *)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)); + bfs_assert(!trie_is_leaf(ptr)); return ptr; } void trie_init(struct trie *trie) { trie->root = 0; - trie->head = NULL; - trie->tail = NULL; -} - -/** Check if a number is a power of two. */ -static bool is_power_of_two(size_t n) { - return (n & (n - 1)) == 0; -} - -/** Compute the popcount (Hamming weight) of a bitmap. */ -static unsigned int trie_popcount(unsigned int n) { -#if __GNUC__ - 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. */ @@ -223,7 +190,7 @@ 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. */ -TARGET_CLONES_POPCNT +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) { @@ -239,9 +206,10 @@ static struct trie_leaf *trie_representative(const struct trie *trie, const void if ((offset >> 1) < length) { unsigned char nibble = trie_key_nibble(key, offset); unsigned int bit = 1U << nibble; - if (node->bitmap & bit) { - index = trie_popcount(node->bitmap & (bit - 1)); - } + // bits = bitmap & bit ? bitmap & (bit - 1) : 0 + unsigned int mask = -!!(node->bitmap & bit); + unsigned int bits = node->bitmap & (bit - 1) & mask; + index = count_ones(bits); } ptr = node->children[index]; } @@ -302,7 +270,7 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k } } -TARGET_CLONES_POPCNT +trie_clones static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) { uintptr_t ptr = trie->root; if (!ptr) { @@ -330,7 +298,7 @@ static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const ch unsigned char nibble = trie_key_nibble(key, 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; @@ -351,21 +319,13 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) { /** Create a new leaf, holding a copy of the given key. */ static struct trie_leaf *trie_leaf_alloc(struct trie *trie, const void *key, size_t length) { - struct trie_leaf *leaf = malloc(BFS_FLEX_SIZEOF(struct trie_leaf, key, length)); + struct trie_leaf *leaf = varena_alloc(&trie->leaves, length); if (!leaf) { return NULL; } - leaf->prev = trie->tail; - leaf->next = NULL; - - if (leaf->prev) { - leaf->prev->next = leaf; - } else { - trie->head = leaf; - } - - trie->tail = leaf; + LIST_ITEM_INIT(leaf); + LIST_APPEND(trie, leaf); leaf->value = NULL; leaf->length = length; @@ -376,49 +336,33 @@ static struct trie_leaf *trie_leaf_alloc(struct trie *trie, const void *key, siz /** Free a leaf. */ static void trie_leaf_free(struct trie *trie, struct trie_leaf *leaf) { - if (leaf->prev) { - leaf->prev->next = leaf->next; - } else { - trie->head = leaf->next; - } - - if (leaf->next) { - leaf->next->prev = leaf->prev; - } else { - trie->tail = leaf->prev; - } - - free(leaf); + LIST_REMOVE(trie, leaf); + varena_free(&trie->leaves, leaf, leaf->length); } -/** 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(is_power_of_two(size)); +/** 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); +} - return BFS_FLEX_SIZEOF(struct trie_node, children, 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); } -#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -# define TRIE_BSWAP(n) (n) -#elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -# if __SIZEOF_SIZE_T__ == 8 -# define TRIE_BSWAP(n) __builtin_bswap64(n) -# elif __SIZEOF_SIZE_T__ == 4 -# define TRIE_BSWAP(n) __builtin_bswap32(n) -# endif -#endif +/** Free a node. */ +static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) { + bfs_assert(size == (size_t)count_ones(node->bitmap)); + varena_free(&trie->nodes, node, size); +} -#ifdef TRIE_BSWAP -# if __SIZEOF_SIZE_T__ == __SIZEOF_LONG_LONG__ -# define TRIE_CTZ(n) __builtin_ctzll(n) -# elif __SIZEOF_SIZE_T__ == __SIZEOF_LONG__ -# define TRIE_CTZ(n) __builtin_ctzl(n) -# elif __SIZEOF_SIZE_T__ == __SIZEOF_INT__ -# define TRIE_CTZ(n) __builtin_ctz(n) -# endif +#if ENDIAN_NATIVE == ENDIAN_LITTLE +# define TRIE_BSWAP(n) (n) +#elif ENDIAN_NATIVE == ENDIAN_BIG +# define TRIE_BSWAP(n) bswap(n) #endif /** Find the offset of the first nibble that differs between two keys. */ @@ -441,10 +385,10 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t memcpy(&key_chunk, key_bytes + i, sizeof(key_chunk)); if (rep_chunk != key_chunk) { -#ifdef TRIE_CTZ +#ifdef TRIE_BSWAP size_t diff = TRIE_BSWAP(rep_chunk ^ key_chunk); i *= 2; - i += TRIE_CTZ(diff) / 4; + i += trailing_zeros(diff) / 4; return i; #else break; @@ -484,14 +428,14 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t * | Z * +--->... */ -TARGET_CLONES_POPCNT +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 = count_ones(node->bitmap); // Double the capacity every power of two - if (is_power_of_two(size)) { - 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; @@ -502,10 +446,10 @@ static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, str 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 target = trie_popcount(node->bitmap & (bit - 1)); + unsigned int target = count_ones(node->bitmap & (bit - 1)); for (size_t i = size; i > target; --i) { node->children[i] = node->children[i - 1]; } @@ -538,12 +482,12 @@ static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, str * | 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, const char *key, 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)); + bfs_assert(trie_is_leaf(*ptr)); - struct trie_node *node = malloc(trie_node_size(1)); + struct trie_node *node = trie_node_alloc(trie, 1); if (!node) { return NULL; } @@ -579,9 +523,9 @@ static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) { static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, struct trie_leaf *rep, size_t offset, size_t mismatch) { unsigned char key_nibble = trie_key_nibble(leaf->key, mismatch); unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch); - assert(key_nibble != rep_nibble); + 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) { trie_leaf_free(trie, leaf); return NULL; @@ -607,7 +551,7 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) { return trie_insert_mem(trie, key, strlen(key) + 1); } -TARGET_CLONES_POPCNT +trie_clones static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) { struct trie_leaf *rep = trie_representative(trie, key, length); size_t mismatch = trie_mismatch(rep, key, length); @@ -637,17 +581,17 @@ static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key unsigned char nibble = trie_key_nibble(key, 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); + 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, key, &offset); if (!ptr) { trie_leaf_free(trie, leaf); return NULL; @@ -667,10 +611,10 @@ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { struct trie_node *node = trie_decode_node(ptr); // Make sure the bitmap is a power of two, i.e. it has just one child - assert(is_power_of_two(node->bitmap)); + bfs_assert(has_single_bit(node->bitmap)); ptr = node->children[0]; - free(node); + trie_node_free(trie, node, 1); } trie_leaf_free(trie, trie_decode_leaf(ptr)); @@ -693,7 +637,7 @@ static void trie_free_singletons(struct trie *trie, 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)) { struct trie_node *other_node = trie_decode_node(other); @@ -705,11 +649,11 @@ static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, } *parent = other; - free(parent_node); + trie_node_free(trie, parent_node, 1); return 0; } -TARGET_CLONES_POPCNT +trie_clones static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { uintptr_t *child = &trie->root; uintptr_t *parent = NULL; @@ -718,16 +662,16 @@ static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { while (!trie_is_leaf(*child)) { struct trie_node *node = trie_decode_node(*child); offset += node->offset; - assert((offset >> 1) < leaf->length); + bfs_assert((offset >> 1) < leaf->length); unsigned char nibble = trie_key_nibble(leaf->key, 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 (!is_power_of_two(bitmap)) { + if (!has_single_bit(bitmap)) { parent = child; child_bit = bit; child_index = index; @@ -736,7 +680,7 @@ static void trie_remove_impl(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, trie->root); @@ -749,18 +693,18 @@ static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { trie_free_singletons(trie, *child); 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 = count_ones(node->bitmap); + bfs_assert(parent_size > 0); + if (parent_size == 1 && 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)); + memmove(child, child + 1, (parent_size - child_index) * sizeof(*child)); } - if (is_power_of_two(parent_size)) { - 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); } @@ -771,23 +715,15 @@ void trie_remove(struct trie *trie, struct trie_leaf *leaf) { trie_remove_impl(trie, leaf); } -/** Free an encoded pointer to a node. */ -TARGET_CLONES_POPCNT -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_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,23 +1,11 @@ -/**************************************************************************** - * 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 #ifndef BFS_TRIE_H #define BFS_TRIE_H -#include <stdbool.h> +#include "alloc.h" +#include "list.h" #include <stddef.h> #include <stdint.h> @@ -25,24 +13,13 @@ * A leaf of a trie. */ struct trie_leaf { - /** - * Linked list of leaves, in insertion order. - */ + /** Linked list of leaves, in insertion order. */ struct trie_leaf *prev, *next; - - /** - * An arbitrary value associated with this leaf. - */ + /** 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. - */ + /** The key itself, stored inline. */ char key[]; }; @@ -54,6 +31,10 @@ struct trie { uintptr_t root; /** Linked list of leaves. */ struct trie_leaf *head, *tail; + /** Node allocator. */ + struct varena nodes; + /** Leaf allocator. */ + struct varena leaves; }; /** @@ -148,6 +129,11 @@ struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t len 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); @@ -155,9 +141,7 @@ void trie_destroy(struct trie *trie); /** * Iterate over the leaves of a trie. */ -#define TRIE_FOR_EACH(trie, leaf) \ - for (struct trie_leaf *leaf = (trie)->head, *_next; \ - leaf && (_next = leaf->next, true); \ - leaf = _next) +#define for_trie(leaf, trie) \ + for_list (struct trie_leaf, leaf, trie) #endif // BFS_TRIE_H @@ -1,26 +1,14 @@ -/**************************************************************************** - * 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 + 1][3] = { +static const int8_t key_coords[UCHAR_MAX + 1][3] = { ['`'] = { 0, 0, 0}, ['~'] = { 0, 0, 1}, ['1'] = { 3, 0, 0}, @@ -125,7 +113,7 @@ static const int key_coords[UCHAR_MAX + 1][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 diff --git a/src/xregex.c b/src/xregex.c index 6f0e5a1..c2711bc 100644 --- a/src/xregex.c +++ b/src/xregex.c @@ -1,35 +1,27 @@ -/**************************************************************************** - * 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 "prelude.h" #include "xregex.h" -#include "config.h" -#include <assert.h> +#include "alloc.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> +#if BFS_USE_ONIGURUMA +# include <langinfo.h> +# include <oniguruma.h> #else -# include <regex.h> +# include <regex.h> #endif struct bfs_regex { -#if BFS_WITH_ONIGURUMA +#if BFS_USE_ONIGURUMA unsigned char *pattern; OnigRegex impl; int err; @@ -40,36 +32,34 @@ struct bfs_regex { #endif }; -#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; - } +#if BFS_USE_ONIGURUMA + +static int bfs_onig_status; +static OnigEncoding bfs_onig_enc; +/** 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 +81,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,22 +99,29 @@ 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; +} + +/** 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; } -#if BFS_WITH_ONIGURUMA +#if BFS_USE_ONIGURUMA // onig_error_code_to_str() says // // don't call this after the pattern argument of onig_new() is freed @@ -153,7 +150,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ syntax = ONIG_SYNTAX_GREP; break; } - assert(syntax); + bfs_assert(syntax, "Invalid regex type"); OnigOptionType options = syntax->options; if (flags & BFS_REGEX_ICASE) { @@ -161,7 +158,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 +185,10 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ cflags |= REG_ICASE; } -#if __has_feature(memory_sanitizer) - // 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 @@ -210,7 +204,7 @@ fail: int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) { size_t len = strlen(str); -#if BFS_WITH_ONIGURUMA +#if BFS_USE_ONIGURUMA const unsigned char *ustr = (const unsigned char *)str; const unsigned char *end = ustr + len; @@ -269,7 +263,7 @@ int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags void bfs_regfree(struct bfs_regex *regex) { if (regex) { -#if BFS_WITH_ONIGURUMA +#if BFS_USE_ONIGURUMA onig_free(regex->impl); free(regex->pattern); #else @@ -281,10 +275,10 @@ 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 +#if BFS_USE_ONIGURUMA unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN); if (str) { onig_error_code_to_str(str, regex->err, ®ex->einfo); diff --git a/src/xregex.h b/src/xregex.h index b2f56a5..998a2b0 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 diff --git a/src/xspawn.c b/src/xspawn.c index f76267b..0b0cea4 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -1,40 +1,32 @@ -/**************************************************************************** - * 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 "prelude.h" #include "xspawn.h" +#include "alloc.h" #include "bfstd.h" -#include "config.h" +#include "list.h" #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/resource.h> #include <sys/types.h> -#include <sys/wait.h> #include <unistd.h> #if BFS_USE_PATHS_H -# include <paths.h> +# include <paths.h> +#endif + +#if _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, @@ -45,120 +37,486 @@ 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 _POSIX_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; } int bfs_spawn_destroy(struct bfs_spawn *ctx) { - struct bfs_spawn_action *action = ctx->actions; - while (action) { - struct bfs_spawn_action *next = action->next; +#if _POSIX_SPAWN > 0 + posix_spawnattr_destroy(&ctx->attr); + posix_spawn_file_actions_destroy(&ctx->actions); +#endif + + 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 _POSIX_SPAWN > 0 +/** Set some posix_spawnattr flags. */ +attr(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 // _POSIX_SPAWN > 0 -/** 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 _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 _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 _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__) + 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 BFS_HAS_POSIX_SPAWN_ADDFCHDIR +# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir +#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP +# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np +#endif + +#if _POSIX_SPAWN > 0 && defined(BFS_POSIX_SPAWN_FCHDIR) + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd); + if (errno != 0) { + free(action); + return -1; + } + } +#else + ctx->flags &= ~BFS_SPAWN_USE_POSIX; +#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 + ctx->flags &= ~BFS_SPAWN_USE_POSIX; +#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; } -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; +/** 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 we can resolve the executable before file actions. */ +static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (!bfs_resolve_relative(res)) { + return true; + } + + if (ctx) { + for_slist (const struct bfs_spawn_action, action, ctx) { + if (action->op == BFS_SPAWN_FCHDIR) { + return false; + } + } + } + + return true; +} + +/** 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)) { + 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 _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 + + return true; +} + +#endif // _POSIX_SPAWN > 0 +/** Actually exec() the new process. */ +static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { 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; } @@ -173,6 +531,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; @@ -196,38 +565,28 @@ 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; + 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; } @@ -235,23 +594,21 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char if (pid < 0) { close_quietly(pipefd[1]); close_quietly(pipefd[0]); - free(resolved); return -1; } else if (pid == 0) { // Child - bfs_spawn_exec(exe, ctx, argv, envp, pipefd); + bfs_spawn_exec(res, ctx, argv, envp, pipefd); } // 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, &wstatus, 0); errno = error; return -1; } @@ -259,73 +616,54 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char return pid; } -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 _POSIX_SPAWN > 0 + if (bfs_use_posix_spawn(res, ctx)) { + return bfs_posix_spawn(res, ctx, argv, envp); } - - const char *path = getenv("PATH"); - - char *confpath = NULL; - if (!path) { -#if defined(_CS_PATH) - path = confpath = xconfstr(_CS_PATH); -#elif defined(_PATH_DEFPATH) - path = _PATH_DEFPATH; -#else - errno = ENOENT; #endif - } - if (!path) { - return NULL; - } - - 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; - } + return bfs_fork_spawn(res, ctx, argv, envp); +} - size_t total = len + 1 + strlen(exe) + 1; - if (cap < total) { - char *grown = realloc(ret, total); - if (!grown) { - goto fail; - } - ret = grown; - cap = total; - } +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; + } - 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..6a8f54a 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(). @@ -21,74 +8,98 @@ #ifndef BFS_XSPAWN_H #define BFS_XSPAWN_H +#include "prelude.h" #include <sys/resource.h> #include <sys/types.h> +#include <unistd.h> + +#if _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 _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. @@ -108,7 +119,7 @@ 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 diff --git a/src/xtime.c b/src/xtime.c index 079d42a..eb11afa 100644 --- a/src/xtime.c +++ b/src/xtime.c @@ -1,67 +1,52 @@ -/**************************************************************************** - * 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 "prelude.h" #include "xtime.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_bug("localtime_r(-1): %s", xstrerror(errno)); + 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) - if (*timep == -1) { +int xtimegm(struct tm *tm, time_t *timep) { + time_t time = timegm(tm); + + if (time == -1) { int error = errno; struct tm tmp; - if (xlocaltime(timep, &tmp) != 0) { + if (!gmtime_r(&time, &tmp)) { + bfs_bug("gmtime_r(-1): %s", xstrerror(errno)); return -1; } @@ -72,9 +57,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) { @@ -92,7 +80,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) { @@ -104,80 +92,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: @@ -185,19 +177,31 @@ 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 xgetpart(const char **str, size_t n, int *result) { - char buf[n + 1]; + *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; } @@ -250,6 +254,8 @@ int xgetdate(const char *str, struct timespec *result) { goto end; } else if (*str == ':') { ++str; + } else if (xgetdigit(*str) < 0) { + goto zone; } if (xgetpart(&str, 2, &tm.tm_min) != 0) { goto invalid; @@ -260,11 +266,14 @@ int xgetdate(const char *str, struct timespec *result) { goto end; } else if (*str == ':') { ++str; + } else if (xgetdigit(*str) < 0) { + goto zone; } if (xgetpart(&str, 2, &tm.tm_sec) != 0) { goto invalid; } +zone: if (!*str) { goto end; } else if (*str == 'Z') { @@ -307,11 +316,11 @@ 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; } } diff --git a/src/xtime.h b/src/xtime.h index b49cd04..fb60ae4 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,30 +11,6 @@ #include <time.h> /** - * localtime_r() wrapper that calls tzset() first. - * - * @param[in] timep - * The time_t to convert. - * @param[out] result - * Buffer to hold the result. - * @return - * 0 on success, -1 on failure. - */ -int xlocaltime(const time_t *timep, struct tm *result); - -/** - * gmtime_r() wrapper that calls tzset() first. - * - * @param[in] timep - * The time_t to convert. - * @param[out] result - * Buffer to hold the result. - * @return - * 0 on success, -1 on failure. - */ -int xgmtime(const time_t *timep, struct tm *result); - -/** * mktime() wrapper that reports errors more reliably. * * @param[in,out] tm diff --git a/tests/alloc.c b/tests/alloc.c new file mode 100644 index 0000000..6c0defd --- /dev/null +++ b/tests/alloc.c @@ -0,0 +1,52 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "alloc.h" +#include "diag.h" +#include <errno.h> +#include <stdlib.h> +#include <stdint.h> + +bool check_alloc(void) { + bool ret = true; + + // Check sizeof_flex() + struct flexible { + alignas(64) int foo[8]; + int bar[]; + }; + ret &= bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible)); + ret &= bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0); + + size_t too_many = SIZE_MAX / sizeof(int) + 1; + ret &= bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX)); + + // Corner case: sizeof(type) > align_ceil(alignof(type), offsetof(type, member)) + // Doesn't happen in typical ABIs + ret &= bfs_check(flex_size(8, 16, 4, 4, 1) == 16); + + // Make sure we detect allocation size overflows +#if __GNUC__ && !__clang__ +# pragma GCC diagnostic ignored "-Walloc-size-larger-than=" +#endif + + ret &= bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + ret &= bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + ret &= bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + ret &= bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + + // varena tests + struct varena varena; + VARENA_INIT(&varena, struct flexible, bar); + + for (size_t i = 0; i < 256; ++i) { + bfs_verify(varena_alloc(&varena, i)); + struct arena *arena = &varena.arenas[varena.narenas - 1]; + ret &= bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i)); + } + + varena_destroy(&varena); + return ret; +} diff --git a/tests/bfs/D_incomplete.sh b/tests/bfs/D_incomplete.sh index 396d365..30c522a 100644 --- a/tests/bfs/D_incomplete.sh +++ b/tests/bfs/D_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs -D +! invoke_bfs -D diff --git a/tests/bfs/D_opt.out b/tests/bfs/D_opt.out new file mode 100644 index 0000000..6218a0c --- /dev/null +++ b/tests/bfs/D_opt.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/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/bfs/D_unknown.sh b/tests/bfs/D_unknown.sh index e3614ba..cac9bd9 100644 --- a/tests/bfs/D_unknown.sh +++ b/tests/bfs/D_unknown.sh @@ -1,4 +1,4 @@ stderr=$(invoke_bfs -warn -D unknown basic 2>&1 >"$OUT") -[ -n "$stderr" ] || return 1 +[ -n "$stderr" ] sort_output diff_output diff --git a/tests/bfs/L_capable.out b/tests/bfs/L_capable.out index e5ba3c7..0810d4a 100644 --- a/tests/bfs/L_capable.out +++ b/tests/bfs/L_capable.out @@ -1,2 +1,2 @@ -scratch/capable -scratch/link +./capable +./link diff --git a/tests/bfs/L_capable.sh b/tests/bfs/L_capable.sh index 533ac2f..97c404f 100644 --- a/tests/bfs/L_capable.sh +++ b/tests/bfs/L_capable.sh @@ -1,12 +1,10 @@ -skip_unless test "$SUDO" -skip_unless test "$UNAME" = "Linux" +test "$UNAME" = "Linux" || skip +invoke_bfs . -quit -capable || skip -clean_scratch +cd "$TEST" -skip_unless invoke_bfs scratch -quit -capable +"$XTOUCH" normal capable +bfs_sudo setcap all+ep capable || skip +ln -s capable link -"$XTOUCH" scratch/{normal,capable} -sudo setcap all+ep scratch/capable -ln -s capable scratch/link - -bfs_diff -L scratch -capable +bfs_diff -L . -capable diff --git a/tests/bfs/O9.sh b/tests/bfs/O9.sh index 12f6c2d..c12a7a3 100644 --- a/tests/bfs/O9.sh +++ b/tests/bfs/O9.sh @@ -1,4 +1,4 @@ stderr=$(invoke_bfs -warn -O9 basic 2>&1 >"$OUT") -[ -n "$stderr" ] || return 1 +[ -n "$stderr" ] sort_output diff_output diff --git a/tests/bfs/and_incomplete.sh b/tests/bfs/and_incomplete.sh index f7bc2c3..05abc2d 100644 --- a/tests/bfs/and_incomplete.sh +++ b/tests/bfs/and_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs -print -a +! invoke_bfs -print -a diff --git a/tests/bfs/capable.out b/tests/bfs/capable.out index 78b5bd9..ac7b5ce 100644 --- a/tests/bfs/capable.out +++ b/tests/bfs/capable.out @@ -1 +1 @@ -scratch/capable +./capable diff --git a/tests/bfs/capable.sh b/tests/bfs/capable.sh index 256b9bc..35bb0b4 100644 --- a/tests/bfs/capable.sh +++ b/tests/bfs/capable.sh @@ -1,12 +1,10 @@ -skip_unless test "$SUDO" -skip_unless test "$UNAME" = "Linux" +test "$UNAME" = "Linux" || skip +invoke_bfs . -quit -capable || skip -clean_scratch +cd "$TEST" -skip_unless invoke_bfs scratch -quit -capable +"$XTOUCH" normal capable +bfs_sudo setcap all+ep capable || skip +ln -s capable link -"$XTOUCH" scratch/{normal,capable} -sudo setcap all+ep scratch/capable -ln -s capable scratch/link - -bfs_diff scratch -capable +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/posix/closed_stdin.out b/tests/bfs/closed_stdin.out index a7ccfe4..a7ccfe4 100644 --- a/tests/posix/closed_stdin.out +++ b/tests/bfs/closed_stdin.out diff --git a/tests/posix/closed_stdin.sh b/tests/bfs/closed_stdin.sh index 6932be8..6932be8 100644 --- a/tests/posix/closed_stdin.sh +++ b/tests/bfs/closed_stdin.sh 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/bfs/color.out b/tests/bfs/color.out index 77fc8a8..a439814 100644 --- a/tests/bfs/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_L.out b/tests/bfs/color_L.out index b60dd4a..85923db 100644 --- a/tests/bfs/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_ln_target.out b/tests/bfs/color_L_ln_target.out index cd4ec5e..23fe8d7 100644 --- a/tests/bfs/color_L_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_no_stat.out b/tests/bfs/color_L_no_stat.out index c0bb1be..72e0319 100644 --- a/tests/bfs/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_star.out b/tests/bfs/color_auto.out index 77fc8a8..a439814 100644 --- a/tests/bfs/color_star.out +++ b/tests/bfs/color_auto.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_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_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/bfs/color_escapes.out b/tests/bfs/color_escapes.out index b71e138..0bf9fbb 100644 --- a/tests/bfs/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_ext.out b/tests/bfs/color_ext.out index cf26e73..218100f 100644 --- a/tests/bfs/color_ext.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_ext0.out b/tests/bfs/color_ext0.out index e764a6b..d2a7fd5 100644 --- a/tests/bfs/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_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..4adba69 --- /dev/null +++ b/tests/bfs/color_ext_case.sh @@ -0,0 +1,6 @@ +# *.gz=01;31:*.GZ=01;32 -- case sensitive +# *.tAr=01;33:*.TaR=01;33 -- case-insensitive +# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive +# *.txt=35:*TXT=36 -- case-insensitive +export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36" +bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_override.out b/tests/bfs/color_ext_override.out index 1377b65..0acfcbc 100644 --- a/tests/bfs/color_ext_override.out +++ b/tests/bfs/color_ext_override.out @@ -1,8 +1,15 @@ +[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;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 diff --git a/tests/bfs/color_ext_underride.out b/tests/bfs/color_ext_underride.out index 787248a..5c98341 100644 --- a/tests/bfs/color_ext_underride.out +++ b/tests/bfs/color_ext_underride.out @@ -1,8 +1,15 @@ +[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;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;32mstar.tar[0m -[01;34mrainbow/[0m[01;33mstar.gz[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 diff --git a/tests/bfs/color_fi0_no.out b/tests/bfs/color_fi0_no.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_fi0_no.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_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/bfs/color_ln_target.out b/tests/bfs/color_ln_target.out index cd4ec5e..23fe8d7 100644 --- a/tests/bfs/color_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_ls.out b/tests/bfs/color_ls.out index b08d894..f69eb9c 100644 --- a/tests/bfs/color_ls.out +++ b/tests/bfs/color_ls.out @@ -1,12 +1,12 @@ -[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;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/[0m[01;31mnothing[0m -[01;34mfoo/bar/[0m[01;31mnowhere/[0m[01;31mnothing[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/baz[0m[01;31m//qux[0m +[01;34mfoo/bar/baz[0m[01;31m//qux[0m diff --git a/tests/bfs/color_ls.sh b/tests/bfs/color_ls.sh index 37d088f..f1cc216 100644 --- a/tests/bfs/color_ls.sh +++ b/tests/bfs/color_ls.sh @@ -1,15 +1,15 @@ -clean_scratch +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 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 \ +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/bfs/color_mh.out b/tests/bfs/color_mh.out index 757a6a1..c658082 100644 --- a/tests/bfs/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_mh0.out b/tests/bfs/color_mh0.out index 77fc8a8..a439814 100644 --- a/tests/bfs/color_mh0.out +++ b/tests/bfs/color_mh0.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_mi.out b/tests/bfs/color_mi.out index 77fc8a8..a439814 100644 --- a/tests/bfs/color_mi.out +++ b/tests/bfs/color_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;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_missing_colon.out b/tests/bfs/color_missing_colon.out index cf26e73..218100f 100644 --- a/tests/bfs/color_missing_colon.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_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/bfs/color_no_stat.out b/tests/bfs/color_no_stat.out index 1fc5324..e3031b2 100644 --- a/tests/bfs/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_nul.out b/tests/bfs/color_nul.out Binary files differindex c328f82..8ccd9a7 100644 --- a/tests/bfs/color_nul.out +++ b/tests/bfs/color_nul.out diff --git a/tests/bfs/color_nul.sh b/tests/bfs/color_nul.sh index 4979569..cb662d6 100644 --- a/tests/bfs/color_nul.sh +++ b/tests/bfs/color_nul.sh @@ -1,2 +1,3 @@ -LS_COLORS="ec=\33[m\0:" invoke_bfs rainbow -color -maxdepth 0 >"$OUT" +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/bfs/color_or.out b/tests/bfs/color_or.out index 9e1fe5c..0bd2570 100644 --- a/tests/bfs/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_or0_mi.out b/tests/bfs/color_or0_mi.out index 77fc8a8..a439814 100644 --- a/tests/bfs/color_or0_mi.out +++ b/tests/bfs/color_or0_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;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_or0_mi0.out b/tests/bfs/color_or0_mi0.out index 77fc8a8..a439814 100644 --- a/tests/bfs/color_or0_mi0.out +++ b/tests/bfs/color_or0_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;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_mi.out b/tests/bfs/color_or_mi.out index 5667f56..fb67e58 100644 --- a/tests/bfs/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_mi0.out b/tests/bfs/color_or_mi0.out index 5667f56..fb67e58 100644 --- a/tests/bfs/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_rs_lc_rc_ec.out b/tests/bfs/color_rs_lc_rc_ec.out index d39bbe7..077ef8d 100644 --- a/tests/bfs/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_st0_tw0_ow.out b/tests/bfs/color_st0_tw0_ow.out index 9a47ef2..a82762b 100644 --- a/tests/bfs/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_ow0.out b/tests/bfs/color_st0_tw0_ow0.out index 2b86fe4..041f1d4 100644 --- a/tests/bfs/color_st0_tw0_ow0.out +++ b/tests/bfs/color_st0_tw0_ow0.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;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 @@ -13,8 +12,16 @@ [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/ow[0m +[01;34mrainbow/sticky[0m +[01;34mrainbow/sticky_ow[0m diff --git a/tests/bfs/color_st0_tw_ow.out b/tests/bfs/color_st0_tw_ow.out index 42549a1..4dcb2f2 100644 --- a/tests/bfs/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_ow0.out b/tests/bfs/color_st0_tw_ow0.out index 535b8ae..954ce9c 100644 --- a/tests/bfs/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_st_tw0_ow.out b/tests/bfs/color_st_tw0_ow.out index c9a86f4..a6e9a16 100644 --- a/tests/bfs/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_ow0.out b/tests/bfs/color_st_tw0_ow0.out index 2d94f3a..756dafb 100644 --- a/tests/bfs/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_tw_ow0.out b/tests/bfs/color_st_tw_ow0.out index 317ef90..6e4a260 100644 --- a/tests/bfs/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_star.sh b/tests/bfs/color_star.sh index 3ada4fd..6d5312e 100644 --- a/tests/bfs/color_star.sh +++ b/tests/bfs/color_star.sh @@ -1,2 +1,2 @@ # Regression test: don't segfault on LS_COLORS="*" -LS_COLORS="*" bfs_diff rainbow -color +! LS_COLORS="*" invoke_bfs rainbow -color diff --git a/tests/bfs/color_su0_sg.out b/tests/bfs/color_su0_sg.out index 8b8c8b8..d13b6b6 100644 --- a/tests/bfs/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_sg0.out b/tests/bfs/color_su0_sg0.out index 0cd5f9a..77fba58 100644 --- a/tests/bfs/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_su_sg0.out b/tests/bfs/color_su_sg0.out index a9e8c5d..8fab046 100644 --- a/tests/bfs/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/comma_incomplete.sh b/tests/bfs/comma_incomplete.sh index 07cf505..bd60168 100644 --- a/tests/bfs/comma_incomplete.sh +++ b/tests/bfs/comma_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs -print , +! invoke_bfs -print , diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh index e057310..22453c0 100644 --- a/tests/bfs/deep_strict.sh +++ b/tests/bfs/deep_strict.sh @@ -1,5 +1,3 @@ -closefrom 4 - # Not even enough fds to keep the root open -ulimit -n 7 +ulimit -n $((NOPENFD + 4)) bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; diff --git a/tests/bfs/exclude_exclude.sh b/tests/bfs/exclude_exclude.sh index c687623..739342f 100644 --- a/tests/bfs/exclude_exclude.sh +++ b/tests/bfs/exclude_exclude.sh @@ -1 +1 @@ -fail invoke_bfs basic -exclude -exclude -name foo +! invoke_bfs basic -exclude -exclude -name foo diff --git a/tests/bfs/exclude_print.sh b/tests/bfs/exclude_print.sh index 52ff0fd..dc89e1d 100644 --- a/tests/bfs/exclude_print.sh +++ b/tests/bfs/exclude_print.sh @@ -1 +1 @@ -fail invoke_bfs basic -exclude -print +! invoke_bfs basic -exclude -print diff --git a/tests/bfs/exec_flush_fprint.sh b/tests/bfs/exec_flush_fprint.sh index bf6b62f..a862773 100644 --- a/tests/bfs/exec_flush_fprint.sh +++ b/tests/bfs/exec_flush_fprint.sh @@ -1,3 +1,2 @@ # Even non-stdstreams should be flushed -clean_scratch -bfs_diff basic/a -fprint scratch/foo -exec cat scratch/foo \; +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 index 5da944a..cd38e41 100644 --- a/tests/bfs/exec_flush_fprint_fail.sh +++ b/tests/bfs/exec_flush_fprint_fail.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic/a -fprint /dev/full -exec true \; +test -e /dev/full || skip +! invoke_bfs basic/a -fprint /dev/full -exec true \; diff --git a/tests/bfs/execdir_plus.sh b/tests/bfs/execdir_plus.sh index 9ae7764..6f24bdc 100644 --- a/tests/bfs/execdir_plus.sh +++ b/tests/bfs/execdir_plus.sh @@ -1,7 +1,4 @@ tree=$(invoke_bfs -D tree 2>&1 -quit) +[[ "$tree" == *"-S dfs"* ]] && skip -if [[ "$tree" == *"-S dfs"* ]]; then - skip -fi - -bfs_diff basic -execdir "$TESTS/sort-args.sh" {} + +bfs_diff -j1 basic -execdir "$TESTS/sort-args.sh" {} + diff --git a/tests/bfs/execdir_plus_nonexistent.sh b/tests/bfs/execdir_plus_nonexistent.sh index 8436953..ed7ed56 100644 --- a/tests/bfs/execdir_plus_nonexistent.sh +++ b/tests/bfs/execdir_plus_nonexistent.sh @@ -1,5 +1,2 @@ -stderr=$(invoke_bfs basic -execdir "$TESTS/nonexistent" {} + 2>&1 >/dev/null) -[ -n "$stderr" ] || return 1 - -bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print -(($? == EX_BFS)) +bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/bfs/fprint_error_stderr.sh b/tests/bfs/fprint_error_stderr.sh index 427808f..2cc4037 100644 --- a/tests/bfs/fprint_error_stderr.sh +++ b/tests/bfs/fprint_error_stderr.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full +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 index fbdc1d0..42a7b36 100644 --- a/tests/bfs/fprint_error_stdout.sh +++ b/tests/bfs/fprint_error_stdout.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full +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 index 5e5c684..5029c7e 100644 --- a/tests/bfs/help.sh +++ b/tests/bfs/help.sh @@ -1,6 +1,4 @@ -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 +! 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/bfs/high_byte.sh b/tests/bfs/high_byte.sh index 222f24b..c76199f 100644 --- a/tests/bfs/high_byte.sh +++ b/tests/bfs/high_byte.sh @@ -1 +1 @@ -fail invoke_bfs -$'\xFF' +! 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/bfs/j1.out b/tests/bfs/j1.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/bfs/j1.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/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/bfs/j64.out b/tests/bfs/j64.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/bfs/j64.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/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 index 34c7c25..42cf6e5 100644 --- a/tests/bfs/links_empty.sh +++ b/tests/bfs/links_empty.sh @@ -1 +1 @@ -fail invoke_bfs links -links '' +! invoke_bfs links -links '' diff --git a/tests/bfs/links_invalid.sh b/tests/bfs/links_invalid.sh index ff69fa6..4d139c9 100644 --- a/tests/bfs/links_invalid.sh +++ b/tests/bfs/links_invalid.sh @@ -1 +1 @@ -fail invoke_bfs links -links ASDF +! 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 index b5d9c58..e664b99 100644 --- a/tests/bfs/links_negative.sh +++ b/tests/bfs/links_negative.sh @@ -1 +1 @@ -fail invoke_bfs links -links +-1 +! invoke_bfs links -links +-1 diff --git a/tests/bfs/links_noarg.sh b/tests/bfs/links_noarg.sh index 5dede5f..5c948dc 100644 --- a/tests/bfs/links_noarg.sh +++ b/tests/bfs/links_noarg.sh @@ -1 +1 @@ -fail invoke_bfs links -links +! invoke_bfs links -links diff --git a/tests/bfs/newerma_nonexistent.sh b/tests/bfs/newerma_nonexistent.sh index 7f3695f..cdedb4a 100644 --- a/tests/bfs/newerma_nonexistent.sh +++ b/tests/bfs/newerma_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs times -newerma basic/nonexistent +! invoke_bfs times -newerma basic/nonexistent diff --git a/tests/bfs/newermq.sh b/tests/bfs/newermq.sh index 2a22586..2f705dc 100644 --- a/tests/bfs/newermq.sh +++ b/tests/bfs/newermq.sh @@ -1 +1 @@ -fail invoke_bfs times -newermq times/a +! invoke_bfs times -newermq times/a diff --git a/tests/bfs/newermt_invalid.sh b/tests/bfs/newermt_invalid.sh index 61d2485..98efece 100644 --- a/tests/bfs/newermt_invalid.sh +++ b/tests/bfs/newermt_invalid.sh @@ -1 +1 @@ -fail invoke_bfs times -newermt not_a_date_time +! invoke_bfs times -newermt not_a_date_time diff --git a/tests/bfs/newerqm.sh b/tests/bfs/newerqm.sh index da84350..c0cff98 100644 --- a/tests/bfs/newerqm.sh +++ b/tests/bfs/newerqm.sh @@ -1 +1 @@ -fail invoke_bfs times -newerqm times/a +! invoke_bfs times -newerqm times/a diff --git a/tests/bfs/nocolor.out b/tests/bfs/nocolor.out index b53fe03..d51d24d 100644 --- a/tests/bfs/nocolor.out +++ b/tests/bfs/nocolor.out @@ -1,20 +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/star.gz -rainbow/star.tar -rainbow/star.tar.gz 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.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/or_incomplete.sh b/tests/bfs/or_incomplete.sh index c941b95..4af31b6 100644 --- a/tests/bfs/or_incomplete.sh +++ b/tests/bfs/or_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs -print -o +! invoke_bfs -print -o diff --git a/tests/bfs/perm_symbolic_double_comma.sh b/tests/bfs/perm_symbolic_double_comma.sh index 66db0ac..48f9d4b 100644 --- a/tests/bfs/perm_symbolic_double_comma.sh +++ b/tests/bfs/perm_symbolic_double_comma.sh @@ -1 +1 @@ -fail invoke_bfs perms -perm a+r,,u+w +! 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 index 3b18721..28446ab 100644 --- a/tests/bfs/perm_symbolic_missing_action.sh +++ b/tests/bfs/perm_symbolic_missing_action.sh @@ -1 +1 @@ -fail invoke_bfs perms -perm a +! invoke_bfs perms -perm a diff --git a/tests/bfs/perm_symbolic_trailing_comma.sh b/tests/bfs/perm_symbolic_trailing_comma.sh index c52ebe6..01bbc16 100644 --- a/tests/bfs/perm_symbolic_trailing_comma.sh +++ b/tests/bfs/perm_symbolic_trailing_comma.sh @@ -1 +1 @@ -fail invoke_bfs perms -perm a+r, +! invoke_bfs perms -perm a+r, diff --git a/tests/bfs/printf_color.out b/tests/bfs/printf_color.out index d9cd1a4..77d21c3 100644 --- a/tests/bfs/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 index 7bb38c2..3641ddb 100644 --- a/tests/bfs/printf_color.sh +++ b/tests/bfs/printf_color.sh @@ -1 +1 @@ -bfs_diff -color -path './rainbow*' -printf '%H %h %f %p %P %l\n' +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 index 77650d0..5ff29f1 100644 --- a/tests/bfs/printf_duplicate_flag.sh +++ b/tests/bfs/printf_duplicate_flag.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '%--p' +! invoke_bfs basic -printf '%--p' diff --git a/tests/bfs/printf_everything.sh b/tests/bfs/printf_everything.sh index 5f20718..07d574a 100644 --- a/tests/bfs/printf_everything.sh +++ b/tests/bfs/printf_everything.sh @@ -1,14 +1,14 @@ 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 ! fail invoke_bfs basic -printf '%F' -quit >/dev/null; then +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 ! fail invoke_bfs basic -printf '%w' -quit >/dev/null; then +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 diff --git a/tests/bfs/printf_incomplete_escape.sh b/tests/bfs/printf_incomplete_escape.sh index 144add5..f560d28 100644 --- a/tests/bfs/printf_incomplete_escape.sh +++ b/tests/bfs/printf_incomplete_escape.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '\' +! invoke_bfs basic -printf '\' diff --git a/tests/bfs/printf_incomplete_format.sh b/tests/bfs/printf_incomplete_format.sh index 347a0f4..92c6afc 100644 --- a/tests/bfs/printf_incomplete_format.sh +++ b/tests/bfs/printf_incomplete_format.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '%' +! invoke_bfs basic -printf '%' diff --git a/tests/bfs/printf_invalid_escape.sh b/tests/bfs/printf_invalid_escape.sh index ce12233..4338f9b 100644 --- a/tests/bfs/printf_invalid_escape.sh +++ b/tests/bfs/printf_invalid_escape.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '\!' +! invoke_bfs basic -printf '\!' diff --git a/tests/bfs/printf_invalid_format.sh b/tests/bfs/printf_invalid_format.sh index 1717615..59d63a7 100644 --- a/tests/bfs/printf_invalid_format.sh +++ b/tests/bfs/printf_invalid_format.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '%!' +! invoke_bfs basic -printf '%!' diff --git a/tests/bfs/printf_must_be_numeric.sh b/tests/bfs/printf_must_be_numeric.sh index eabb3d6..7c7c3fa 100644 --- a/tests/bfs/printf_must_be_numeric.sh +++ b/tests/bfs/printf_must_be_numeric.sh @@ -1 +1 @@ -fail invoke_bfs basic -printf '%+p' +! invoke_bfs basic -printf '%+p' 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 index d8b3861..8572d5a 100644 --- a/tests/bfs/stderr_fails_loudly.sh +++ b/tests/bfs/stderr_fails_loudly.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full +test -e /dev/full || skip +! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full diff --git a/tests/bfs/stderr_fails_silently.sh b/tests/bfs/stderr_fails_silently.sh index 731cb02..a37393d 100644 --- a/tests/bfs/stderr_fails_silently.sh +++ b/tests/bfs/stderr_fails_silently.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full +test -e /dev/full || skip bfs_diff -D all basic 2>/dev/full diff --git a/tests/bfs/unexpected_operator.sh b/tests/bfs/unexpected_operator.sh index b3658f6..2eb0e71 100644 --- a/tests/bfs/unexpected_operator.sh +++ b/tests/bfs/unexpected_operator.sh @@ -1 +1 @@ -fail invoke_bfs \! -o -print +! invoke_bfs \! -o -print 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_xdev_mount.out b/tests/bfs/warn_xdev_mount.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/bfs/warn_xdev_mount.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/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 index cd478af..02c8173 100644 --- a/tests/bfs/xtype_depth.sh +++ b/tests/bfs/xtype_depth.sh @@ -1,2 +1,2 @@ # Make sure -xtype is considered side-effecting for facts_when_impure -fail invoke_bfs loops -xtype l -depth 100 +! invoke_bfs loops -xtype l -depth 100 diff --git a/tests/bfstd.c b/tests/bfstd.c new file mode 100644 index 0000000..07b68b0 --- /dev/null +++ b/tests/bfstd.c @@ -0,0 +1,93 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "bfstd.h" +#include "diag.h" +#include <errno.h> +#include <langinfo.h> +#include <stdlib.h> +#include <string.h> + +/** Check the result of xdirname()/xbasename(). */ +static bool check_base_dir(const char *path, const char *dir, const char *base) { + bool ret = true; + + char *xdir = xdirname(path); + bfs_verify(xdir, "xdirname(): %s", xstrerror(errno)); + ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); + free(xdir); + + char *xbase = xbasename(path); + bfs_verify(xbase, "xbasename(): %s", xstrerror(errno)); + ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); + free(xbase); + + return ret; +} + +/** Check the result of wordesc(). */ +static bool check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { + char buf[256]; + char *end = buf + sizeof(buf); + char *esc = wordesc(buf, end, str, flags); + + return bfs_check(esc != end) + && bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); +} + +bool check_bfstd(void) { + bool ret = true; + + ret &= bfs_check(asciilen("") == 0); + ret &= bfs_check(asciilen("@") == 1); + ret &= bfs_check(asciilen("@@") == 2); + ret &= bfs_check(asciilen("\xFF@") == 0); + ret &= bfs_check(asciilen("@\xFF") == 1); + ret &= bfs_check(asciilen("@@@@@@@@") == 8); + ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16); + ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24); + ret &= bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15); + ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16); + ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17); + ret &= bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); + ret &= bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); + ret &= bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); + + // From man 3p basename + ret &= check_base_dir("usr", ".", "usr"); + ret &= check_base_dir("usr/", ".", "usr"); + ret &= check_base_dir("", ".", "."); + ret &= check_base_dir("/", "/", "/"); + // check_base_dir("//", "/" or "//", "/" or "//"); + ret &= check_base_dir("///", "/", "/"); + ret &= check_base_dir("/usr/", "/", "usr"); + ret &= check_base_dir("/usr/lib", "/usr", "lib"); + ret &= check_base_dir("//usr//lib//", "//usr", "lib"); + ret &= check_base_dir("/home//dwc//test", "/home//dwc", "test"); + + ret &= check_wordesc("", "\"\"", WESC_SHELL); + ret &= check_wordesc("word", "word", WESC_SHELL); + ret &= check_wordesc("two words", "\"two words\"", WESC_SHELL); + ret &= check_wordesc("word's", "\"word's\"", WESC_SHELL); + ret &= check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL); + ret &= check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL); + ret &= check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("~user", "\"~user\"", WESC_SHELL); + + const char *charmap = nl_langinfo(CODESET); + if (strcmp(charmap, "UTF-8") == 0) { + ret &= check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); + ret &= check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); + } + + ret &= bfs_check(xstrwidth("Hello world") == 11); + ret &= bfs_check(xstrwidth("Hello\1world") == 10); + + return ret; +} diff --git a/tests/bit.c b/tests/bit.c new file mode 100644 index 0000000..674d1b2 --- /dev/null +++ b/tests/bit.c @@ -0,0 +1,141 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "bit.h" +#include "diag.h" +#include <limits.h> +#include <stdint.h> +#include <string.h> + +bfs_static_assert(UMAX_WIDTH(0x1) == 1); +bfs_static_assert(UMAX_WIDTH(0x3) == 2); +bfs_static_assert(UMAX_WIDTH(0x7) == 3); +bfs_static_assert(UMAX_WIDTH(0xF) == 4); +bfs_static_assert(UMAX_WIDTH(0xFF) == 8); +bfs_static_assert(UMAX_WIDTH(0xFFF) == 12); +bfs_static_assert(UMAX_WIDTH(0xFFFF) == 16); + +#define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1) +#define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1) +#define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1) + +bfs_static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); +bfs_static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); +bfs_static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH)); + +bfs_static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH)); +bfs_static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH)); +bfs_static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH)); + +bfs_static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); +bfs_static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); +bfs_static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH)); + +bfs_static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH)); +bfs_static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH)); +bfs_static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH)); + +bfs_static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); +bfs_static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); +bfs_static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH)); + +bfs_static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH)); +bfs_static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH)); +bfs_static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH)); + +bfs_static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); +bfs_static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); +bfs_static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH)); + +bfs_static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH)); +bfs_static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH)); +bfs_static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH)); + +#define check_eq(a, b) \ + bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b)) + +bool check_bit(void) { + bool ret = true; + + const char *str = "\x1\x2\x3\x4"; + uint32_t word; + memcpy(&word, str, sizeof(word)); + +#if ENDIAN_NATIVE == ENDIAN_LITTLE + ret &= check_eq(word, 0x04030201); +#elif ENDIAN_NATIVE == ENDIAN_BIG + ret &= check_eq(word, 0x01020304); +#else +# warning "Skipping byte order tests on mixed/unknown-endian machine" +#endif + + ret &= check_eq(bswap((uint8_t)0x12), 0x12); + ret &= check_eq(bswap((uint16_t)0x1234), 0x3412); + ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412); + ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412); + + ret &= check_eq(count_ones(0x0), 0); + ret &= check_eq(count_ones(0x1), 1); + ret &= check_eq(count_ones(0x2), 1); + ret &= check_eq(count_ones(0x3), 2); + ret &= check_eq(count_ones(0x137F), 10); + + ret &= check_eq(count_zeros(0), INT_WIDTH); + ret &= check_eq(count_zeros(0L), LONG_WIDTH); + ret &= check_eq(count_zeros(0LL), LLONG_WIDTH); + ret &= check_eq(count_zeros((uint8_t)0), 8); + ret &= check_eq(count_zeros((uint16_t)0), 16); + ret &= check_eq(count_zeros((uint32_t)0), 32); + ret &= check_eq(count_zeros((uint64_t)0), 64); + + ret &= check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A); + ret &= check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123); + ret &= check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345); + ret &= check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678); + + ret &= check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A); + ret &= check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341); + ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123); + ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678); + + for (int i = 0; i < 16; ++i) { + uint16_t n = (uint16_t)1 << i; + for (int j = i; j < 16; ++j) { + uint16_t m = (uint16_t)1 << j; + uint16_t nm = n | m; + ret &= check_eq(count_ones(nm), 1 + (n != m)); + ret &= check_eq(count_zeros(nm), 15 - (n != m)); + ret &= check_eq(leading_zeros(nm), 15 - j); + ret &= check_eq(trailing_zeros(nm), i); + ret &= check_eq(first_leading_one(nm), j + 1); + ret &= check_eq(first_trailing_one(nm), i + 1); + ret &= check_eq(bit_width(nm), j + 1); + ret &= check_eq(bit_floor(nm), m); + if (n == m) { + ret &= check_eq(bit_ceil(nm), m); + ret &= bfs_check(has_single_bit(nm)); + } else { + if (j < 15) { + ret &= check_eq(bit_ceil(nm), (m << 1)); + } + ret &= bfs_check(!has_single_bit(nm)); + } + } + } + + ret &= check_eq(leading_zeros((uint16_t)0), 16); + ret &= check_eq(trailing_zeros((uint16_t)0), 16); + ret &= check_eq(first_leading_one(0), 0); + ret &= check_eq(first_trailing_one(0), 0); + ret &= check_eq(bit_width(0), 0); + ret &= check_eq(bit_floor(0), 0); + ret &= check_eq(bit_ceil(0), 1); + + ret &= bfs_check(!has_single_bit(0)); + ret &= bfs_check(!has_single_bit(UINT32_MAX)); + ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1))); + + return ret; +} diff --git a/tests/bsd/L_acl.out b/tests/bsd/L_acl.out index 1dae00a..dd89800 100644 --- a/tests/bsd/L_acl.out +++ b/tests/bsd/L_acl.out @@ -1,2 +1,2 @@ -scratch/acl -scratch/link +./acl +./link diff --git a/tests/bsd/L_acl.sh b/tests/bsd/L_acl.sh index cf573e4..a3fcbc8 100644 --- a/tests/bsd/L_acl.sh +++ b/tests/bsd/L_acl.sh @@ -1,9 +1,9 @@ -clean_scratch +cd "$TEST" -skip_unless invoke_bfs scratch -quit -acl +invoke_bfs . -quit -acl || skip -"$XTOUCH" scratch/{normal,acl} -skip_unless set_acl scratch/acl -ln -s acl scratch/link +"$XTOUCH" normal acl +set_acl acl || skip +ln -s acl link -bfs_diff -L scratch -acl +bfs_diff -L . -acl diff --git a/tests/bsd/L_xattr.out b/tests/bsd/L_xattr.out index 12fac95..21eb50f 100644 --- a/tests/bsd/L_xattr.out +++ b/tests/bsd/L_xattr.out @@ -1,3 +1,3 @@ -scratch/link -scratch/xattr -scratch/xattr_2 +./link +./xattr +./xattr_2 diff --git a/tests/bsd/L_xattr.sh b/tests/bsd/L_xattr.sh index 7c27e0d..f8b56d8 100644 --- a/tests/bsd/L_xattr.sh +++ b/tests/bsd/L_xattr.sh @@ -1,3 +1,3 @@ -skip_unless invoke_bfs scratch -quit -xattr -skip_unless make_xattrs -bfs_diff -L scratch -xattr +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 index 4dc4836..9e4c172 100644 --- a/tests/bsd/L_xattrname.out +++ b/tests/bsd/L_xattrname.out @@ -1,2 +1,2 @@ -scratch/link -scratch/xattr +./link +./xattr diff --git a/tests/bsd/L_xattrname.sh b/tests/bsd/L_xattrname.sh index 39d6a77..8108d57 100644 --- a/tests/bsd/L_xattrname.sh +++ b/tests/bsd/L_xattrname.sh @@ -1,11 +1,11 @@ -skip_unless invoke_bfs scratch -quit -xattr -skip_unless make_xattrs +invoke_bfs . -quit -xattr || skip +make_xattrs || skip case "$UNAME" in Darwin|FreeBSD) - bfs_diff -L scratch -xattrname bfs_test + bfs_diff -L . -xattrname bfs_test ;; *) - bfs_diff -L scratch -xattrname security.bfs_test + bfs_diff -L . -xattrname security.bfs_test ;; esac diff --git a/tests/bsd/X.sh b/tests/bsd/X.sh index 03d9eee..54000cf 100644 --- a/tests/bsd/X.sh +++ b/tests/bsd/X.sh @@ -1,2 +1 @@ -bfs_diff -X weirdnames -[ $? -eq $EX_BFS ] +! bfs_diff -X weirdnames diff --git a/tests/bsd/acl.out b/tests/bsd/acl.out index ddf8446..92e2f67 100644 --- a/tests/bsd/acl.out +++ b/tests/bsd/acl.out @@ -1 +1 @@ -scratch/acl +./acl diff --git a/tests/bsd/acl.sh b/tests/bsd/acl.sh index 1665684..a13c75f 100644 --- a/tests/bsd/acl.sh +++ b/tests/bsd/acl.sh @@ -1,9 +1,9 @@ -clean_scratch +cd "$TEST" -skip_unless invoke_bfs scratch -quit -acl +invoke_bfs . -quit -acl || skip -"$XTOUCH" scratch/{normal,acl} -skip_unless set_acl scratch/acl -ln -s acl scratch/link +"$XTOUCH" normal acl +set_acl acl || skip +ln -s acl link -bfs_diff scratch -acl +bfs_diff . -acl diff --git a/tests/bsd/exit.sh b/tests/bsd/exit.sh index 524a75f..248349c 100644 --- a/tests/bsd/exit.sh +++ b/tests/bsd/exit.sh @@ -1,11 +1,5 @@ -invoke_bfs basic -name foo -exit 42 -if [ $? -ne 42 ]; then - return 1 -fi +check_exit 42 invoke_bfs basic -name foo -exit 42 -invoke_bfs basic -name qux -exit 42 -if [ $? -ne 0 ]; then - return 1 -fi +check_exit 0 invoke_bfs basic -name qux -exit 42 bfs_diff basic/g -print -name g -exit diff --git a/tests/bsd/f_incomplete.sh b/tests/bsd/f_incomplete.sh index acb63af..0dfb19f 100644 --- a/tests/bsd/f_incomplete.sh +++ b/tests/bsd/f_incomplete.sh @@ -1,2 +1 @@ -fail invoke_bfs -f - +! invoke_bfs -f diff --git a/tests/bsd/flags.out b/tests/bsd/flags.out index 11998ed..3216ff5 100644 --- a/tests/bsd/flags.out +++ b/tests/bsd/flags.out @@ -1 +1 @@ -scratch/bar +./bar diff --git a/tests/bsd/flags.sh b/tests/bsd/flags.sh index ffb1cc2..eb9bc22 100644 --- a/tests/bsd/flags.sh +++ b/tests/bsd/flags.sh @@ -1,8 +1,8 @@ -skip_unless invoke_bfs scratch -quit -flags offline +invoke_bfs . -quit -flags offline || skip -clean_scratch +cd "$TEST" -"$XTOUCH" scratch/{foo,bar} -skip_unless chflags offline scratch/bar +"$XTOUCH" foo bar +chflags offline bar || skip -bfs_diff scratch -flags -offline,nohidden +bfs_diff . -flags -offline,nohidden diff --git a/tests/bsd/mtime_bad_unit.sh b/tests/bsd/mtime_bad_unit.sh index 3921f80..6e2caf1 100644 --- a/tests/bsd/mtime_bad_unit.sh +++ b/tests/bsd/mtime_bad_unit.sh @@ -1 +1 @@ -fail invoke_bfs times -mtime +1q +! invoke_bfs times -mtime +1q diff --git a/tests/bsd/mtime_missing_unit.sh b/tests/bsd/mtime_missing_unit.sh index 3ac4c97..f6b1f93 100644 --- a/tests/bsd/mtime_missing_unit.sh +++ b/tests/bsd/mtime_missing_unit.sh @@ -1 +1 @@ -fail invoke_bfs times -mtime +1w2 +! invoke_bfs times -mtime +1w2 diff --git a/tests/bsd/rm.out b/tests/bsd/rm.out index fb188b9..9c558e3 100644 --- a/tests/bsd/rm.out +++ b/tests/bsd/rm.out @@ -1 +1 @@ -scratch +. diff --git a/tests/bsd/rm.sh b/tests/bsd/rm.sh index 9ee2b0a..595d514 100644 --- a/tests/bsd/rm.sh +++ b/tests/bsd/rm.sh @@ -1,6 +1,4 @@ -clean_scratch -"$XTOUCH" -p scratch/foo/bar/baz - -(cd scratch && invoke_bfs . -rm) - -bfs_diff scratch +cd "$TEST" +"$XTOUCH" -p foo/bar/baz +invoke_bfs . -rm +bfs_diff . 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/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/bsd/xattr.out b/tests/bsd/xattr.out index 109e7c9..0afed35 100644 --- a/tests/bsd/xattr.out +++ b/tests/bsd/xattr.out @@ -1,3 +1,3 @@ -scratch/xattr -scratch/xattr_2 -scratch/xattr_link +./xattr +./xattr_2 +./xattr_link diff --git a/tests/bsd/xattr.sh b/tests/bsd/xattr.sh index 727c220..68f729a 100644 --- a/tests/bsd/xattr.sh +++ b/tests/bsd/xattr.sh @@ -1,3 +1,3 @@ -skip_unless invoke_bfs scratch -quit -xattr -skip_unless make_xattrs -bfs_diff scratch -xattr +invoke_bfs . -quit -xattr || skip +make_xattrs || skip +bfs_diff . -xattr diff --git a/tests/bsd/xattrname.out b/tests/bsd/xattrname.out index 0285ac1..ef732bd 100644 --- a/tests/bsd/xattrname.out +++ b/tests/bsd/xattrname.out @@ -1,2 +1,2 @@ -scratch/xattr -scratch/xattr_link +./xattr +./xattr_link diff --git a/tests/bsd/xattrname.sh b/tests/bsd/xattrname.sh index 6a0fe7e..38b111a 100644 --- a/tests/bsd/xattrname.sh +++ b/tests/bsd/xattrname.sh @@ -1,11 +1,11 @@ -skip_unless invoke_bfs scratch -quit -xattr -skip_unless make_xattrs +invoke_bfs . -quit -xattr || skip +make_xattrs || skip case "$UNAME" in Darwin|FreeBSD) - bfs_diff scratch -xattrname bfs_test + bfs_diff . -xattrname bfs_test ;; *) - bfs_diff scratch -xattrname security.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..805d2b8 --- /dev/null +++ b/tests/color.sh @@ -0,0 +1,37 @@ +#!/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 +} diff --git a/tests/common/L_ilname.sh b/tests/common/L_ilname.sh index cfb15a8..e0495ed 100644 --- a/tests/common/L_ilname.sh +++ b/tests/common/L_ilname.sh @@ -1,2 +1,2 @@ -skip_unless invoke_bfs -quit -ilname PATTERN +invoke_bfs -quit -ilname PATTERN || skip bfs_diff -L links -ilname '[AQ]' diff --git a/tests/common/L_ls.sh b/tests/common/L_ls.sh index ced16c6..7ee2b44 100644 --- a/tests/common/L_ls.sh +++ b/tests/common/L_ls.sh @@ -1,2 +1 @@ -clean_scratch -invoke_bfs -L rainbow -ls >scratch/L_ls.out +invoke_bfs -L rainbow -ls >"$OUT" diff --git a/tests/common/L_mount.out b/tests/common/L_mount.out index 2e80082..788579d 100644 --- a/tests/common/L_mount.out +++ b/tests/common/L_mount.out @@ -1,5 +1,5 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/foo/qux -scratch/mnt +. +./foo +./foo/bar +./foo/qux +./mnt diff --git a/tests/common/L_mount.sh b/tests/common/L_mount.sh index dad7e00..fd8042a 100644 --- a/tests/common/L_mount.sh +++ b/tests/common/L_mount.sh @@ -1,15 +1,13 @@ -skip_unless test "$SUDO" -skip_if test "$UNAME" = "Darwin" +test "$UNAME" = "Darwin" && skip -clean_scratch -mkdir scratch/{foo,mnt} -sudo mount -t tmpfs tmpfs scratch/mnt -ln -s ../mnt scratch/foo/bar -"$XTOUCH" scratch/mnt/baz -ln -s ../mnt/baz scratch/foo/qux +cd "$TEST" +mkdir foo mnt -bfs_diff -L scratch -mount -ret=$? +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt -sudo umount scratch/mnt -return $ret +ln -s ../mnt foo/bar +"$XTOUCH" mnt/baz +ln -s ../mnt/baz foo/qux + +bfs_diff -L . -mount diff --git a/tests/common/delete.out b/tests/common/delete.out index fb188b9..9c558e3 100644 --- a/tests/common/delete.out +++ b/tests/common/delete.out @@ -1 +1 @@ -scratch +. diff --git a/tests/common/delete.sh b/tests/common/delete.sh index 89cf2a2..638f307 100644 --- a/tests/common/delete.sh +++ b/tests/common/delete.sh @@ -1,7 +1,4 @@ -clean_scratch -"$XTOUCH" -p scratch/foo/bar/baz - -# Don't try to delete '.' -(cd scratch && invoke_bfs . -delete) - -bfs_diff scratch +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 index fb188b9..9c558e3 100644 --- a/tests/common/delete_many.out +++ b/tests/common/delete_many.out @@ -1 +1 @@ -scratch +. diff --git a/tests/common/delete_many.sh b/tests/common/delete_many.sh index 6274319..48fe4c2 100644 --- a/tests/common/delete_many.sh +++ b/tests/common/delete_many.sh @@ -1,8 +1,8 @@ # Test for https://github.com/tavianator/bfs/issues/67 -clean_scratch -mkdir scratch/foo -"$XTOUCH" scratch/foo/{1..256} +cd "$TEST" +mkdir foo +"$XTOUCH" foo/{1..256} -invoke_bfs scratch/foo -delete -bfs_diff scratch +invoke_bfs foo -delete +bfs_diff . diff --git a/tests/gnu/empty.out b/tests/common/empty.out index a0f4b76..a0f4b76 100644 --- a/tests/gnu/empty.out +++ b/tests/common/empty.out diff --git a/tests/gnu/empty.sh b/tests/common/empty.sh index 95ee988..95ee988 100644 --- a/tests/gnu/empty.sh +++ b/tests/common/empty.sh diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out new file mode 100644 index 0000000..da45e23 --- /dev/null +++ b/tests/common/empty_error.out @@ -0,0 +1,3 @@ +./bar +./baz +./qux diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh new file mode 100644 index 0000000..7c8049c --- /dev/null +++ b/tests/common/empty_error.sh @@ -0,0 +1,7 @@ +cd "$TEST" + +"$XTOUCH" -p foo/ bar/ baz qux +chmod -r foo baz +defer chmod +r foo baz + +! bfs_diff . -empty diff --git a/tests/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/gnu/empty_special.sh b/tests/common/empty_special.sh index 31e9d2e..31e9d2e 100644 --- a/tests/gnu/empty_special.sh +++ b/tests/common/empty_special.sh diff --git a/tests/common/execdir_nonexistent.sh b/tests/common/execdir_nonexistent.sh index 5d116e5..0ec013c 100644 --- a/tests/common/execdir_nonexistent.sh +++ b/tests/common/execdir_nonexistent.sh @@ -1,5 +1,2 @@ -stderr=$(invoke_bfs basic -execdir "$TESTS/nonexistent" {} \; 2>&1 >/dev/null) -[ -n "$stderr" ] || return 1 - -bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print -(($? == EX_BFS)) +bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/common/execdir_ulimit.out b/tests/common/execdir_ulimit.out index 7f53982..bf52c09 100644 --- a/tests/common/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 index 8bd9edd..122c282 100644 --- a/tests/common/execdir_ulimit.sh +++ b/tests/common/execdir_ulimit.sh @@ -1,7 +1,6 @@ -clean_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 +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 -closefrom 4 -ulimit -n 13 -bfs_diff scratch -execdir echo {} \; +ulimit -n $((NOPENFD + 10)) +bfs_diff . -execdir echo {} \; diff --git a/tests/common/ilname.sh b/tests/common/ilname.sh index 7ab0793..fc7e9e4 100644 --- a/tests/common/ilname.sh +++ b/tests/common/ilname.sh @@ -1,2 +1,2 @@ -skip_unless invoke_bfs -quit -ilname PATTERN +invoke_bfs -quit -ilname PATTERN || skip bfs_diff links -ilname '[AQ]' diff --git a/tests/common/iname.sh b/tests/common/iname.sh index 8fcc443..c25a646 100644 --- a/tests/common/iname.sh +++ b/tests/common/iname.sh @@ -1,2 +1,2 @@ -skip_unless invoke_bfs -quit -iname PATTERN +invoke_bfs -quit -iname PATTERN || skip bfs_diff basic -iname '*F*' diff --git a/tests/common/inum_bind_mount.out b/tests/common/inum_bind_mount.out index a520de3..ede8749 100644 --- a/tests/common/inum_bind_mount.out +++ b/tests/common/inum_bind_mount.out @@ -1,2 +1,2 @@ -scratch/bar -scratch/foo +./bar +./foo diff --git a/tests/common/inum_bind_mount.sh b/tests/common/inum_bind_mount.sh index e35ed4e..892713e 100644 --- a/tests/common/inum_bind_mount.sh +++ b/tests/common/inum_bind_mount.sh @@ -1,12 +1,9 @@ -skip_unless test "$SUDO" -skip_unless test "$UNAME" = "Linux" +test "$UNAME" = "Linux" || skip -clean_scratch -"$XTOUCH" scratch/{foo,bar} -sudo mount --bind scratch/{foo,bar} +cd "$TEST" +"$XTOUCH" foo bar baz -bfs_diff scratch -inum "$(inum scratch/bar)" -ret=$? +bfs_sudo mount --bind foo bar || skip +defer bfs_sudo umount bar -sudo umount scratch/bar -return $ret +bfs_diff . -inum "$(inum bar)" diff --git a/tests/common/inum_mount.out b/tests/common/inum_mount.out index 99c7511..99fa01e 100644 --- a/tests/common/inum_mount.out +++ b/tests/common/inum_mount.out @@ -1 +1 @@ -scratch/mnt +./mnt diff --git a/tests/common/inum_mount.sh b/tests/common/inum_mount.sh index f9f4e2b..7facf57 100644 --- a/tests/common/inum_mount.sh +++ b/tests/common/inum_mount.sh @@ -1,12 +1,9 @@ -skip_unless test "$SUDO" -skip_if test "$UNAME" = "Darwin" +test "$UNAME" = "Darwin" && skip -clean_scratch -mkdir scratch/{foo,mnt} -sudo mount -t tmpfs tmpfs scratch/mnt +cd "$TEST" +mkdir foo mnt -bfs_diff scratch -inum "$(inum scratch/mnt)" -ret=$? +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt -sudo umount scratch/mnt -return $ret +bfs_diff . -inum "$(inum mnt)" diff --git a/tests/common/ipath.sh b/tests/common/ipath.sh index 9e6f8c5..7d05f31 100644 --- a/tests/common/ipath.sh +++ b/tests/common/ipath.sh @@ -1,2 +1,2 @@ -skip_unless invoke_bfs -quit -ipath PATTERN +invoke_bfs -quit -ipath PATTERN || skip bfs_diff basic -ipath 'basic/*F*' diff --git a/tests/common/ls.sh b/tests/common/ls.sh index 85ca39c..bc50d90 100644 --- a/tests/common/ls.sh +++ b/tests/common/ls.sh @@ -1,2 +1 @@ -clean_scratch -invoke_bfs rainbow -ls >scratch/ls.out +invoke_bfs rainbow -ls >"$OUT" diff --git a/tests/common/maxdepth_incomplete.sh b/tests/common/maxdepth_incomplete.sh index 536dcf5..0bcb461 100644 --- a/tests/common/maxdepth_incomplete.sh +++ b/tests/common/maxdepth_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs basic -maxdepth +! invoke_bfs basic -maxdepth diff --git a/tests/common/mindepth_incomplete.sh b/tests/common/mindepth_incomplete.sh index 19a3b21..6f55a42 100644 --- a/tests/common/mindepth_incomplete.sh +++ b/tests/common/mindepth_incomplete.sh @@ -1 +1 @@ -fail invoke_bfs basic -mindepth +! invoke_bfs basic -mindepth diff --git a/tests/common/mount.out b/tests/common/mount.out index f7839fb..6253434 100644 --- a/tests/common/mount.out +++ b/tests/common/mount.out @@ -1,4 +1,4 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/mnt +. +./foo +./foo/bar +./mnt diff --git a/tests/common/mount.sh b/tests/common/mount.sh index 2732a68..c9abde5 100644 --- a/tests/common/mount.sh +++ b/tests/common/mount.sh @@ -1,13 +1,11 @@ -skip_unless test "$SUDO" -skip_if test "$UNAME" = "Darwin" +test "$UNAME" = "Darwin" && skip -clean_scratch -mkdir scratch/{foo,mnt} -sudo mount -t tmpfs tmpfs scratch/mnt -"$XTOUCH" scratch/foo/bar scratch/mnt/baz +cd "$TEST" +mkdir foo mnt -bfs_diff scratch -mount -ret=$? +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt -sudo umount scratch/mnt -return $ret +"$XTOUCH" foo/bar mnt/baz + +bfs_diff . -mount diff --git a/tests/common/newermt.sh b/tests/common/newermt.sh index 3c5be68..e816b29 100644 --- a/tests/common/newermt.sh +++ b/tests/common/newermt.sh @@ -1 +1,3 @@ -bfs_diff times -newermt 1991-12-14T00:01 +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/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/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..b987e49 --- /dev/null +++ b/tests/getopts.sh @@ -0,0 +1,179 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Argument parsing + +if command -v nproc &>/dev/null; then + JOBS=$(nproc) +else + JOBS=1 +fi +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() { + local pad=$(printf "%*s" ${#0} "") + color cat <<EOF +Usage: ${GRN}$0${RST} + [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] + [${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/find2fd}" + 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 index ed0e9a1..7ed5f0d 100644 --- a/tests/gnu/L_delete.out +++ b/tests/gnu/L_delete.out @@ -1,2 +1,2 @@ -scratch -scratch/foo +. +./foo diff --git a/tests/gnu/L_delete.sh b/tests/gnu/L_delete.sh index 6ec167c..0559c49 100644 --- a/tests/gnu/L_delete.sh +++ b/tests/gnu/L_delete.sh @@ -1,9 +1,8 @@ -clean_scratch -mkdir scratch/foo -mkdir scratch/bar -ln -s ../foo scratch/bar/baz +cd "$TEST" +mkdir foo bar +ln -s ../foo bar/baz # Don't try to rmdir() a symlink -invoke_bfs -L scratch/bar -delete || return 1 +invoke_bfs -L bar -delete -bfs_diff scratch +bfs_diff . diff --git a/tests/gnu/L_loops_continue.sh b/tests/gnu/L_loops_continue.sh index 0244137..55aeb33 100644 --- a/tests/gnu/L_loops_continue.sh +++ b/tests/gnu/L_loops_continue.sh @@ -1,2 +1 @@ -bfs_diff -L loops -[ $? -eq $EX_BFS ] +! bfs_diff -L loops diff --git a/tests/gnu/daystart.sh b/tests/gnu/daystart.sh index 9799bca..9c3be1a 100644 --- a/tests/gnu/daystart.sh +++ b/tests/gnu/daystart.sh @@ -1 +1 @@ -bfs_diff basic -daystart -mtime 0 +TZ=WAT-1 bfs_diff basic -daystart -mtime 0 diff --git a/tests/gnu/daystart_twice.sh b/tests/gnu/daystart_twice.sh index 21b2c0f..edbf18d 100644 --- a/tests/gnu/daystart_twice.sh +++ b/tests/gnu/daystart_twice.sh @@ -1 +1 @@ -bfs_diff basic -daystart -daystart -mtime 0 +TZ=WAT-1 bfs_diff basic -daystart -daystart -mtime 0 diff --git a/tests/gnu/empty_special.out b/tests/gnu/empty_special.out deleted file mode 100644 index 3927f2b..0000000 --- a/tests/gnu/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/gnu/exec_flush_fail.sh b/tests/gnu/exec_flush_fail.sh index 4772a14..5505f7a 100644 --- a/tests/gnu/exec_flush_fail.sh +++ b/tests/gnu/exec_flush_fail.sh @@ -1,3 +1,3 @@ # Failure to flush streams before exec should be caught -skip_unless test -e /dev/full -fail invoke_bfs basic -print0 -exec true \; >/dev/full +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 index 9d613e8..443aa0d 100644 --- a/tests/gnu/exec_nothing.sh +++ b/tests/gnu/exec_nothing.sh @@ -1,2 +1,2 @@ # Regression test: don't segfault on missing command -fail invoke_bfs basic -exec \; +! invoke_bfs basic -exec \; diff --git a/tests/gnu/exec_plus_flush_fail.sh b/tests/gnu/exec_plus_flush_fail.sh index 5c74fd8..53a50e5 100644 --- a/tests/gnu/exec_plus_flush_fail.sh +++ b/tests/gnu/exec_plus_flush_fail.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full +test -e /dev/full || skip +! invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full 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/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/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh index bd4fbf4..85eee8f 100644 --- a/tests/gnu/files0_from_empty.sh +++ b/tests/gnu/files0_from_empty.sh @@ -1 +1 @@ -printf "\0" | fail invoke_bfs -files0-from - +! printf "\0" | invoke_bfs -files0-from - diff --git a/tests/gnu/files0_from_error.sh b/tests/gnu/files0_from_error.sh index ab27ea2..1515d0b 100644 --- a/tests/gnu/files0_from_error.sh +++ b/tests/gnu/files0_from_error.sh @@ -1 +1 @@ -fail invoke_bfs -files0-from basic +! invoke_bfs -files0-from basic diff --git a/tests/gnu/files0_from_file.sh b/tests/gnu/files0_from_file.sh index 089a20e..81435a0 100644 --- a/tests/gnu/files0_from_file.sh +++ b/tests/gnu/files0_from_file.sh @@ -1,4 +1,4 @@ -clean_scratch +FILE="$TMP/$TEST.in" cd weirdnames -invoke_bfs -mindepth 1 -fprintf ../scratch/files0.in "%P\0" -bfs_diff -files0-from ../scratch/files0.in +invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0" +bfs_diff -files0-from "$FILE" diff --git a/tests/gnu/files0_from_none.out b/tests/gnu/files0_from_none.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/gnu/files0_from_none.out diff --git a/tests/gnu/files0_from_none.sh b/tests/gnu/files0_from_none.sh index c6e5b97..1633163 100644 --- a/tests/gnu/files0_from_none.sh +++ b/tests/gnu/files0_from_none.sh @@ -1 +1 @@ -printf "" | fail invoke_bfs -files0-from - +printf "" | bfs_diff -files0-from - diff --git a/tests/gnu/files0_from_nothing.sh b/tests/gnu/files0_from_nothing.sh index 5fdae60..fee50a8 100644 --- a/tests/gnu/files0_from_nothing.sh +++ b/tests/gnu/files0_from_nothing.sh @@ -1 +1 @@ -fail invoke_bfs -files0-from basic/nonexistent +! invoke_bfs -files0-from basic/nonexistent diff --git a/tests/gnu/files0_from_nowhere.sh b/tests/gnu/files0_from_nowhere.sh index 2337613..68eea4b 100644 --- a/tests/gnu/files0_from_nowhere.sh +++ b/tests/gnu/files0_from_nowhere.sh @@ -1 +1 @@ -fail invoke_bfs -files0-from +! invoke_bfs -files0-from diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh index 5387e5c..8e145ce 100644 --- a/tests/gnu/files0_from_ok.sh +++ b/tests/gnu/files0_from_ok.sh @@ -1 +1 @@ -printf "basic\0" | fail invoke_bfs -files0-from - -ok echo {} \; +! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/fls.sh b/tests/gnu/fls.sh index a86fa20..d2ff794 100644 --- a/tests/gnu/fls.sh +++ b/tests/gnu/fls.sh @@ -1,2 +1 @@ -clean_scratch -invoke_bfs rainbow -fls scratch/fls.out +invoke_bfs rainbow -fls "$OUT" diff --git a/tests/gnu/fls_nonexistent.sh b/tests/gnu/fls_nonexistent.sh index 4756834..2854569 100644 --- a/tests/gnu/fls_nonexistent.sh +++ b/tests/gnu/fls_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs rainbow -fls scratch/nonexistent/path +! invoke_bfs rainbow -fls nonexistent/path diff --git a/tests/gnu/fprint0_nonexistent.sh b/tests/gnu/fprint0_nonexistent.sh index d8e0f30..4906081 100644 --- a/tests/gnu/fprint0_nonexistent.sh +++ b/tests/gnu/fprint0_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprint0 scratch/nonexistent/path +! invoke_bfs basic -fprint0 nonexistent/path diff --git a/tests/gnu/fprint_duplicate.sh b/tests/gnu/fprint_duplicate.sh index 5275502..8533b05 100644 --- a/tests/gnu/fprint_duplicate.sh +++ b/tests/gnu/fprint_duplicate.sh @@ -1,7 +1,7 @@ -"$XTOUCH" -p scratch/foo.out -ln scratch/foo.out scratch/foo.hard -ln -s foo.out scratch/foo.soft +"$XTOUCH" -p "$TEST/foo.out" +ln "$TEST/foo.out" "$TEST/foo.hard" +ln -s foo.out "$TEST/foo.soft" -invoke_bfs basic -fprint scratch/foo.out -fprint scratch/foo.hard -fprint scratch/foo.soft -sort scratch/foo.out >"$OUT" +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 index e7f2394..7617034 100644 --- a/tests/gnu/fprint_error.sh +++ b/tests/gnu/fprint_error.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic -maxdepth 0 -fprint /dev/full +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 index bf772f3..8511649 100644 --- a/tests/gnu/fprint_noarg.sh +++ b/tests/gnu/fprint_noarg.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprint +! invoke_bfs basic -fprint diff --git a/tests/gnu/fprint_noerror.sh b/tests/gnu/fprint_noerror.sh index 142e935..f13a62b 100644 --- a/tests/gnu/fprint_noerror.sh +++ b/tests/gnu/fprint_noerror.sh @@ -1,3 +1,3 @@ # Regression test: /dev/full should not fail until actually written to -skip_unless test -e /dev/full +test -e /dev/full || skip invoke_bfs basic -false -fprint /dev/full diff --git a/tests/gnu/fprint_nonexistent.sh b/tests/gnu/fprint_nonexistent.sh index b6dac8a..2a403a2 100644 --- a/tests/gnu/fprint_nonexistent.sh +++ b/tests/gnu/fprint_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprint scratch/nonexistent/path +! invoke_bfs basic -fprint nonexistent/path diff --git a/tests/gnu/fprintf_nofile.sh b/tests/gnu/fprintf_nofile.sh index c2c48a5..4e79002 100644 --- a/tests/gnu/fprintf_nofile.sh +++ b/tests/gnu/fprintf_nofile.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprintf +! invoke_bfs basic -fprintf diff --git a/tests/gnu/fprintf_noformat.sh b/tests/gnu/fprintf_noformat.sh index 0d285c3..fd97f4c 100644 --- a/tests/gnu/fprintf_noformat.sh +++ b/tests/gnu/fprintf_noformat.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprintf /dev/null +! invoke_bfs basic -fprintf /dev/null diff --git a/tests/gnu/fprintf_nonexistent.sh b/tests/gnu/fprintf_nonexistent.sh index 6ed141a..b1eea10 100644 --- a/tests/gnu/fprintf_nonexistent.sh +++ b/tests/gnu/fprintf_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs basic -fprintf scratch/nonexistent/path '%p\n' +! invoke_bfs basic -fprintf nonexistent/path '%p\n' diff --git a/tests/gnu/fstype.sh b/tests/gnu/fstype.sh index 939438e..05645c3 100644 --- a/tests/gnu/fstype.sh +++ b/tests/gnu/fstype.sh @@ -1,3 +1,2 @@ -fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') -skip_if test $? -ne 0 +fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') || skip bfs_diff basic -fstype "$fstype" 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/gnu/fstype_umount.out b/tests/gnu/fstype_umount.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ 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/gid_plus.sh b/tests/gnu/gid_plus.sh index 8ad493b..ccba0e6 100644 --- a/tests/gnu/gid_plus.sh +++ b/tests/gnu/gid_plus.sh @@ -1,2 +1,2 @@ -skip_if test "$(id -g)" -eq 0 +test "$(id -g)" -eq 0 && skip bfs_diff basic -gid +0 diff --git a/tests/gnu/gid_plus_plus.sh b/tests/gnu/gid_plus_plus.sh index 7982633..ec7ae86 100644 --- a/tests/gnu/gid_plus_plus.sh +++ b/tests/gnu/gid_plus_plus.sh @@ -1,2 +1,2 @@ -skip_if test "$(id -g)" -eq 0 +test "$(id -g)" -eq 0 && skip bfs_diff basic -gid ++0 diff --git a/tests/gnu/ignore_readdir_race.sh b/tests/gnu/ignore_readdir_race.sh index 6586bcc..75165f6 100644 --- a/tests/gnu/ignore_readdir_race.sh +++ b/tests/gnu/ignore_readdir_race.sh @@ -1,5 +1,5 @@ -clean_scratch -"$XTOUCH" scratch/{foo,bar} +cd "$TEST" +"$XTOUCH" 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" {} \; +invoke_bfs . -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \; diff --git a/tests/gnu/ignore_readdir_race_notdir.sh b/tests/gnu/ignore_readdir_race_notdir.sh index 5b8b56d..12e9fe6 100644 --- a/tests/gnu/ignore_readdir_race_notdir.sh +++ b/tests/gnu/ignore_readdir_race_notdir.sh @@ -1,5 +1,7 @@ # Check -ignore_readdir_race handling when a directory is replaced with a file -clean_scratch -"$XTOUCH" -p scratch/foo/bar +cd "$TEST" +mkdir foo -invoke_bfs scratch -mindepth 1 -ignore_readdir_race -execdir rm -r {} \; -execdir "$XTOUCH" {} \; +invoke_bfs . -mindepth 1 -ignore_readdir_race \ + -type d -execdir rmdir {} \; \ + -execdir "$XTOUCH" {} \; diff --git a/tests/gnu/ignore_readdir_race_root.sh b/tests/gnu/ignore_readdir_race_root.sh index bad7391..dc41e7f 100644 --- a/tests/gnu/ignore_readdir_race_root.sh +++ b/tests/gnu/ignore_readdir_race_root.sh @@ -1,2 +1,2 @@ # Make sure -ignore_readdir_race doesn't suppress ENOENT at the root -fail invoke_bfs basic/nonexistent -ignore_readdir_race +! invoke_bfs basic/nonexistent -ignore_readdir_race diff --git a/tests/gnu/inum_automount.out b/tests/gnu/inum_automount.out index 7b53ae3..3378e2d 100644 --- a/tests/gnu/inum_automount.out +++ b/tests/gnu/inum_automount.out @@ -1 +1 @@ -scratch/automnt +./automnt diff --git a/tests/gnu/inum_automount.sh b/tests/gnu/inum_automount.sh index 648ea05..86b23e1 100644 --- a/tests/gnu/inum_automount.sh +++ b/tests/gnu/inum_automount.sh @@ -1,17 +1,14 @@ # bfs shouldn't trigger automounts unless it descends into them -skip_unless test "$SUDO" -skip_unless command -v systemd-mount &>/dev/null +command -v systemd-mount &>/dev/null || skip -clean_scratch -mkdir scratch/{foo,automnt} -skip_unless sudo systemd-mount -A -o bind basic scratch/automnt +cd "$TEST" +mkdir foo automnt -before=$(inum scratch/automnt) -bfs_diff scratch -inum "$before" -prune -ret=$? -after=$(inum scratch/automnt) +bfs_sudo systemd-mount -A -o bind "$TMP/basic" automnt || skip +defer bfs_sudo systemd-umount automnt -sudo systemd-umount scratch/automnt - -((ret == 0 && before == after)) +before=$(inum automnt) +bfs_diff . -inum "$before" -prune +after=$(inum automnt) +((before == after)) diff --git a/tests/gnu/iwholename.sh b/tests/gnu/iwholename.sh index 67e9630..0b2d038 100644 --- a/tests/gnu/iwholename.sh +++ b/tests/gnu/iwholename.sh @@ -1,2 +1,2 @@ -skip_unless invoke_bfs -quit -iwholename PATTERN +invoke_bfs -quit -iwholename PATTERN || skip bfs_diff basic -iwholename 'basic/*F*' diff --git a/tests/posix/newer_link.out b/tests/gnu/newer_link.out index d2dcdd1..d2dcdd1 100644 --- a/tests/posix/newer_link.out +++ b/tests/gnu/newer_link.out diff --git a/tests/posix/newer_link.sh b/tests/gnu/newer_link.sh index 685ac78..685ac78 100644 --- a/tests/posix/newer_link.sh +++ b/tests/gnu/newer_link.sh diff --git a/tests/gnu/ok_nothing.sh b/tests/gnu/ok_nothing.sh index 439687b..52c3547 100644 --- a/tests/gnu/ok_nothing.sh +++ b/tests/gnu/ok_nothing.sh @@ -1,2 +1,2 @@ # Regression test: don't segfault on missing command -fail invoke_bfs basic -ok \; +! 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/gnu/print_error.sh b/tests/gnu/print_error.sh index 9fd5af5..bc79637 100644 --- a/tests/gnu/print_error.sh +++ b/tests/gnu/print_error.sh @@ -1,2 +1,2 @@ -skip_unless test -e /dev/full -fail invoke_bfs basic -maxdepth 0 >/dev/full +test -e /dev/full || skip +! invoke_bfs basic -maxdepth 0 >/dev/full diff --git a/tests/gnu/printf_Y_error.out b/tests/gnu/printf_Y_error.out index 410a9b5..1dd554e 100644 --- a/tests/gnu/printf_Y_error.out +++ b/tests/gnu/printf_Y_error.out @@ -1,3 +1,3 @@ -(scratch) () d d -(scratch/bar) (foo/bar) l ? -(scratch/foo) () d d +(.) () 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 index 6487711..d3130ce 100644 --- a/tests/gnu/printf_Y_error.sh +++ b/tests/gnu/printf_Y_error.sh @@ -1,12 +1,8 @@ -clean_scratch -mkdir scratch/foo -chmod -x scratch/foo -ln -s foo/bar scratch/bar +cd "$TEST" +mkdir foo +ln -s foo/bar bar -bfs_diff scratch -printf '(%p) (%l) %y %Y\n' -ret=$? +chmod -x foo +defer chmod +x foo -chmod +x scratch/foo -clean_scratch - -[ $ret -eq $EX_BFS ] +! bfs_diff . -printf '(%p) (%l) %y %Y\n' diff --git a/tests/gnu/printf_times.sh b/tests/gnu/printf_times.sh index d952620..e4f5155 100644 --- a/tests/gnu/printf_times.sh +++ b/tests/gnu/printf_times.sh @@ -1 +1 @@ -bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %T@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n' +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/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh index a84ee29..c621b9b 100644 --- a/tests/gnu/printf_u_g_ulimit.sh +++ b/tests/gnu/printf_u_g_ulimit.sh @@ -1,3 +1,2 @@ -closefrom 4 -ulimit -n 16 +ulimit -n $((NOPENFD + 13)) [ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ] diff --git a/tests/gnu/regex_error.sh b/tests/gnu/regex_error.sh index 9bd4c8d..4af933f 100644 --- a/tests/gnu/regex_error.sh +++ b/tests/gnu/regex_error.sh @@ -1 +1 @@ -fail invoke_bfs basic -regex '[' +! invoke_bfs basic -regex '[' diff --git a/tests/gnu/regex_invalid_utf8.out b/tests/gnu/regex_invalid_utf8.out index 03f3f58..a133b1a 100644 --- a/tests/gnu/regex_invalid_utf8.out +++ b/tests/gnu/regex_invalid_utf8.out @@ -1 +1 @@ -scratch/ +./ diff --git a/tests/gnu/regex_invalid_utf8.sh b/tests/gnu/regex_invalid_utf8.sh index edb4b1e..7006dcd 100644 --- a/tests/gnu/regex_invalid_utf8.sh +++ b/tests/gnu/regex_invalid_utf8.sh @@ -1,8 +1,8 @@ -clean_scratch +cd "$TEST" # Incomplete UTF-8 sequences -skip_unless touch scratch/$'\xC3' -skip_unless touch scratch/$'\xE2\x84' -skip_unless touch scratch/$'\xF0\x9F\x92' +touch $'\xC3' || skip +touch $'\xE2\x84' || skip +touch $'\xF0\x9F\x92' || skip -bfs_diff scratch -regex 'scratch/..' +bfs_diff . -regex '\./..' diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh index d0f68cc..3cc388c 100644 --- a/tests/gnu/regextype_emacs.sh +++ b/tests/gnu/regextype_emacs.sh @@ -1,3 +1,3 @@ -skip_unless invoke_bfs -regextype emacs -quit +invoke_bfs -regextype emacs -quit || skip bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)' diff --git a/tests/gnu/regextype_grep.sh b/tests/gnu/regextype_grep.sh index 0136700..0830667 100644 --- a/tests/gnu/regextype_grep.sh +++ b/tests/gnu/regextype_grep.sh @@ -1,3 +1,3 @@ -skip_unless invoke_bfs -regextype grep -quit +invoke_bfs -regextype grep -quit || skip bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?' diff --git a/tests/gnu/uid_plus.sh b/tests/gnu/uid_plus.sh index fc4bce3..22b2c8e 100644 --- a/tests/gnu/uid_plus.sh +++ b/tests/gnu/uid_plus.sh @@ -1,2 +1,2 @@ -skip_if test "$(id -u)" -eq 0 +test "$(id -u)" -eq 0 && skip bfs_diff basic -uid +0 diff --git a/tests/gnu/uid_plus_plus.sh b/tests/gnu/uid_plus_plus.sh index 5d5e086..e021888 100644 --- a/tests/gnu/uid_plus_plus.sh +++ b/tests/gnu/uid_plus_plus.sh @@ -1,2 +1,2 @@ -skip_if test "$(id -u)" -eq 0 +test "$(id -u)" -eq 0 && skip bfs_diff basic -uid ++0 diff --git a/tests/gnu/used.out b/tests/gnu/used.out new file mode 100644 index 0000000..647621b --- /dev/null +++ b/tests/gnu/used.out @@ -0,0 +1,4 @@ +-used +7: ./nextyear +-used 1: ./tomorrow +-used 2: ./dayafter +-used 7: ./nextweek diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh new file mode 100644 index 0000000..5e5d4e9 --- /dev/null +++ b/tests/gnu/used.sh @@ -0,0 +1,40 @@ +iso8601() { + printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@" +} + +cd "$TEST" + +now=$(date '+%Y-%m-%dT%H:%M:%S') + +# Parse the current date +[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail +# Treat leading zeros as decimal, not octal +YYYY=$((10#${BASH_REMATCH[1]})) +MM=$((10#${BASH_REMATCH[2]})) +DD=$((10#${BASH_REMATCH[3]})) +hh=$((10#${BASH_REMATCH[4]})) +mm=$((10#${BASH_REMATCH[5]})) +ss=$((10#${BASH_REMATCH[6]})) + +# -used is always false if atime < ctime +yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss) +"$XTOUCH" -at "$yesterday" yesterday + +# -used rounds up +tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss) +"$XTOUCH" -at "$tomorrow" tomorrow + +dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss) +"$XTOUCH" -at "$dayafter" dayafter + +nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss) +"$XTOUCH" -at "$nextweek" nextweek + +nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss) +"$XTOUCH" -at "$nextyear" nextyear + +bfs_diff -mindepth 1 \ + -a -used 1 -printf '-used 1: %p\n' \ + -o -used 2 -printf '-used 2: %p\n' \ + -o -used 7 -printf '-used 7: %p\n' \ + -o -used +7 -printf '-used +7: %p\n' diff --git a/tests/gnu/xtype_bind_mount.out b/tests/gnu/xtype_bind_mount.out index 16804ea..d18d706 100644 --- a/tests/gnu/xtype_bind_mount.out +++ b/tests/gnu/xtype_bind_mount.out @@ -1,2 +1,2 @@ -scratch/link -scratch/null +./link +./null diff --git a/tests/gnu/xtype_bind_mount.sh b/tests/gnu/xtype_bind_mount.sh index 264b6f8..35fb3f5 100644 --- a/tests/gnu/xtype_bind_mount.sh +++ b/tests/gnu/xtype_bind_mount.sh @@ -1,13 +1,10 @@ -skip_unless test "$SUDO" -skip_unless test "$UNAME" = "Linux" +test "$UNAME" = "Linux" || skip -clean_scratch -"$XTOUCH" scratch/{file,null} -sudo mount --bind /dev/null scratch/null -ln -s /dev/null scratch/link +cd "$TEST" +"$XTOUCH" file null +ln -s /dev/null link -bfs_diff -L scratch -type c -ret=$? +bfs_sudo mount --bind /dev/null null || skip +defer bfs_sudo umount null -sudo umount scratch/null -return $ret +bfs_diff . -xtype c diff --git a/tests/ioq.c b/tests/ioq.c new file mode 100644 index 0000000..ef5ee3b --- /dev/null +++ b/tests/ioq.c @@ -0,0 +1,77 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "ioq.h" +#include "bfstd.h" +#include "diag.h" +#include "dir.h" +#include <errno.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_verify(ioq, "ioq_create(): %s", xstrerror(errno)); + + // Push enough operations to fill the queue + for (size_t i = 0; i < depth; ++i) { + struct bfs_dir *dir = bfs_allocdir(); + bfs_verify(dir, "bfs_allocdir(): %s", xstrerror(errno)); + + int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL); + bfs_verify(ret == 0, "ioq_opendir(): %s", xstrerror(errno)); + } + 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); +} + +bool check_ioq(void) { + check_ioq_push_block(); + return true; +} 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..429772b --- /dev/null +++ b/tests/main.c @@ -0,0 +1,125 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Entry point for unit tests. + */ + +#include "prelude.h" +#include "tests.h" +#include "bfstd.h" +#include "color.h" +#include <errno.h> +#include <locale.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +/** + * Test context. + */ +struct test_ctx { + /** Number of command line arguments. */ + int argc; + /** The arguments themselves. */ + char **argv; + + /** 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 argc, char **argv) { + ctx->argc = argc; + ctx->argv = argv; + + 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; +} + +/** Finalize the test context. */ +static int test_fini(struct test_ctx *ctx) { + if (ctx->cout) { + cfclose(ctx->cout); + } + + free_colors(ctx->colors); + + return ctx->ret; +} + +/** Check if a test case is enabled for this run. */ +static bool should_run(const struct test_ctx *ctx, const char *test) { + // Run all tests by default + if (ctx->argc < 2) { + return true; + } + + // With args, run only specified tests + for (int i = 1; i < ctx->argc; ++i) { + if (strcmp(test, ctx->argv[i]) == 0) { + return true; + } + } + + return false; +} + +/** Run a test if it's enabled. */ +static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) { + if (should_run(ctx, test)) { + if (fn()) { + cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test); + } else { + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test); + ctx->ret = EXIT_FAILURE; + } + } +} + +const char *bfs_errstr(void) { + return xstrerror(errno); +} + +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(); + + struct test_ctx ctx; + if (test_init(&ctx, argc, argv) != 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, "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..5786cb6 100644 --- a/tests/mksock.c +++ b/tests/mksock.c @@ -1,29 +1,16 @@ -/**************************************************************************** - * 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 +19,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 +28,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 +48,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 +100,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/L_loops.sh b/tests/posix/L_loops.sh index f737cea..01b7efc 100644 --- a/tests/posix/L_loops.sh +++ b/tests/posix/L_loops.sh @@ -1,4 +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 -errors=$(invoke_bfs -L loops 2>&1 >/dev/null) -[ -n "$errors" ] +invoke_bfs -L loops >/dev/null 2>"$OUT" && fail +test -s "$OUT" diff --git a/tests/posix/L_xdev.out b/tests/posix/L_xdev.out index 2e80082..788579d 100644 --- a/tests/posix/L_xdev.out +++ b/tests/posix/L_xdev.out @@ -1,5 +1,5 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/foo/qux -scratch/mnt +. +./foo +./foo/bar +./foo/qux +./mnt diff --git a/tests/posix/L_xdev.sh b/tests/posix/L_xdev.sh index ddbadd8..82d8605 100644 --- a/tests/posix/L_xdev.sh +++ b/tests/posix/L_xdev.sh @@ -1,15 +1,13 @@ -skip_unless test "$SUDO" -skip_if test "$UNAME" = "Darwin" +test "$UNAME" = "Darwin" && skip -clean_scratch -mkdir scratch/{foo,mnt} -sudo mount -t tmpfs tmpfs scratch/mnt -ln -s ../mnt scratch/foo/bar -"$XTOUCH" scratch/mnt/baz -ln -s ../mnt/baz scratch/foo/qux +cd "$TEST" +mkdir foo mnt -bfs_diff -L scratch -xdev -ret=$? +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt -sudo umount scratch/mnt -return $ret +ln -s ../mnt foo/bar +"$XTOUCH" mnt/baz +ln -s ../mnt/baz foo/qux + +bfs_diff -L . -xdev diff --git a/tests/posix/closed_stderr.sh b/tests/posix/closed_stderr.sh deleted file mode 100644 index cc746ef..0000000 --- a/tests/posix/closed_stderr.sh +++ /dev/null @@ -1 +0,0 @@ -fail invoke_bfs basic >&- 2>&- diff --git a/tests/posix/closed_stdout.sh b/tests/posix/closed_stdout.sh deleted file mode 100644 index 446bf56..0000000 --- a/tests/posix/closed_stdout.sh +++ /dev/null @@ -1 +0,0 @@ -fail invoke_bfs basic >&- diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh index 3d1cd60..36a88c0 100644 --- a/tests/posix/deep.sh +++ b/tests/posix/deep.sh @@ -1,4 +1,2 @@ -closefrom 4 - -ulimit -n 16 +ulimit -n $((NOPENFD + 13)) bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \; diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out index ed0e9a1..7ed5f0d 100644 --- a/tests/posix/depth_error.out +++ b/tests/posix/depth_error.out @@ -1,2 +1,2 @@ -scratch -scratch/foo +. +./foo diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh index f770210..db414ba 100644 --- a/tests/posix/depth_error.sh +++ b/tests/posix/depth_error.sh @@ -1,11 +1,7 @@ -clean_scratch -"$XTOUCH" -p scratch/foo/bar -chmod a-r scratch/foo +cd "$TEST" +"$XTOUCH" -p foo/bar -bfs_diff scratch -depth -ret=$? +chmod a-r foo +defer chmod +r foo -chmod +r scratch/foo -clean_scratch - -[ $ret -eq $EX_BFS ] +! bfs_diff . -depth diff --git a/tests/posix/exec_nonexistent.sh b/tests/posix/exec_nonexistent.sh index b4e08e0..a9ff052 100644 --- a/tests/posix/exec_nonexistent.sh +++ b/tests/posix/exec_nonexistent.sh @@ -1,8 +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 - -stderr=$(invoke_bfs basic -exec "$TESTS/nonexistent" {} \; 2>&1 >/dev/null) -[ -n "$stderr" ] || return 1 - -bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print -(($? == EX_BFS)) +bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail +test -s "$TEST/err" diff --git a/tests/posix/exec_plus_nonexistent.sh b/tests/posix/exec_plus_nonexistent.sh index f96099e..24582a3 100644 --- a/tests/posix/exec_plus_nonexistent.sh +++ b/tests/posix/exec_plus_nonexistent.sh @@ -1,5 +1,2 @@ -stderr=$(invoke_bfs basic -exec "$TESTS/nonexistent" {} + 2>&1 >/dev/null) -[ -n "$stderr" ] || return 1 - -bfs_diff basic -exec "$TESTS/nonexistent" {} + -print -(($? == EX_BFS)) +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 index ef01968..347722d 100644 --- a/tests/posix/exec_plus_nothing.sh +++ b/tests/posix/exec_plus_nothing.sh @@ -1,2 +1,2 @@ # Regression test: don't look OOB for {} + -fail invoke_bfs basic -exec + +! invoke_bfs basic -exec + diff --git a/tests/posix/exec_plus_status.sh b/tests/posix/exec_plus_status.sh index ea9e5ef..a814c4e 100644 --- a/tests/posix/exec_plus_status.sh +++ b/tests/posix/exec_plus_status.sh @@ -1,4 +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 -(($? == EX_BFS)) +! bfs_diff basic -exec false {} + -print 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 index cd8e8f8..d15022f 100644 --- a/tests/posix/extra_paren.sh +++ b/tests/posix/extra_paren.sh @@ -1 +1 @@ -fail invoke_bfs basic -print \) +! invoke_bfs basic -print \) diff --git a/tests/posix/incomplete.sh b/tests/posix/incomplete.sh index 07b1b61..bca5a13 100644 --- a/tests/posix/incomplete.sh +++ b/tests/posix/incomplete.sh @@ -1 +1 @@ -fail invoke_bfs basic \( +! invoke_bfs basic \( diff --git a/tests/posix/missing_paren.sh b/tests/posix/missing_paren.sh index ac8dd60..d906fbe 100644 --- a/tests/posix/missing_paren.sh +++ b/tests/posix/missing_paren.sh @@ -1 +1 @@ -fail invoke_bfs basic \( -print +! invoke_bfs basic \( -print diff --git a/tests/posix/name_bracket.sh b/tests/posix/name_bracket.sh index 84a417f..e2f943d 100644 --- a/tests/posix/name_bracket.sh +++ b/tests/posix/name_bracket.sh @@ -1,5 +1,9 @@ -# fnmatch() is broken on macOS -skip_if test "$UNAME" = "Darwin" +# 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/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 index 789cadf..5f2da4b 100644 --- a/tests/posix/newer_nonexistent.sh +++ b/tests/posix/newer_nonexistent.sh @@ -1 +1 @@ -fail invoke_bfs times -newer times/nonexistent +! invoke_bfs times -newer times/nonexistent diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh index 8f758c4..a39dd1f 100644 --- a/tests/posix/nogroup_ulimit.sh +++ b/tests/posix/nogroup_ulimit.sh @@ -1,8 +1,2 @@ -closefrom 4 -ulimit -n 16 - -# -mindepth 18, but POSIX -path="*/*/*/*/*/*" -path="$path/$path/$path" -bfs_diff deep -path "deep/$path" -nogroup - +ulimit -n $((NOPENFD + 13)) +bfs_diff deep -type f -nogroup diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh index 2777589..a94b8c5 100644 --- a/tests/posix/nouser_ulimit.sh +++ b/tests/posix/nouser_ulimit.sh @@ -1,7 +1,2 @@ -closefrom 4 -ulimit -n 16 - -# -mindepth 18, but POSIX -path="*/*/*/*/*/*" -path="$path/$path/$path" -bfs_diff deep -path "deep/$path" -nouser +ulimit -n $((NOPENFD + 13)) +bfs_diff deep -type f -nouser diff --git a/tests/posix/ok_plus_nothing.sh b/tests/posix/ok_plus_nothing.sh index 7cb7de5..77c7644 100644 --- a/tests/posix/ok_plus_nothing.sh +++ b/tests/posix/ok_plus_nothing.sh @@ -1,2 +1,2 @@ # Regression test: don't look OOB for {} + -fail invoke_bfs basic -ok + +! invoke_bfs basic -ok + 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/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/type_bind_mount.out b/tests/posix/type_bind_mount.out index 6435159..2f06c47 100644 --- a/tests/posix/type_bind_mount.out +++ b/tests/posix/type_bind_mount.out @@ -1 +1 @@ -scratch/null +./null diff --git a/tests/posix/type_bind_mount.sh b/tests/posix/type_bind_mount.sh index 445f6ef..97b7305 100644 --- a/tests/posix/type_bind_mount.sh +++ b/tests/posix/type_bind_mount.sh @@ -1,12 +1,9 @@ -skip_unless test "$SUDO" -skip_unless test "$UNAME" = "Linux" +test "$UNAME" = "Linux" || skip -clean_scratch -"$XTOUCH" scratch/{file,null} -sudo mount --bind /dev/null scratch/null +cd "$TEST" +"$XTOUCH" file null -bfs_diff scratch -type c -ret=$? +bfs_sudo mount --bind /dev/null null || skip +defer bfs_sudo umount null -sudo umount scratch/null -return $ret +bfs_diff . -type c 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/xdev.out b/tests/posix/xdev.out index f7839fb..6253434 100644 --- a/tests/posix/xdev.out +++ b/tests/posix/xdev.out @@ -1,4 +1,4 @@ -scratch -scratch/foo -scratch/foo/bar -scratch/mnt +. +./foo +./foo/bar +./mnt diff --git a/tests/posix/xdev.sh b/tests/posix/xdev.sh index 4591940..c59c5c8 100644 --- a/tests/posix/xdev.sh +++ b/tests/posix/xdev.sh @@ -1,13 +1,11 @@ -skip_unless test "$SUDO" -skip_if test "$UNAME" = "Darwin" +test "$UNAME" = "Darwin" && skip -clean_scratch -mkdir scratch/{foo,mnt} -sudo mount -t tmpfs tmpfs scratch/mnt -"$XTOUCH" scratch/foo/bar scratch/mnt/baz +cd "$TEST" +mkdir foo mnt -bfs_diff scratch -xdev -ret=$? +bfs_sudo mount -t tmpfs tmpfs mnt || skip +defer bfs_sudo umount mnt -sudo umount scratch/mnt -return $ret +"$XTOUCH" foo/bar mnt/baz + +bfs_diff . -xdev diff --git a/tests/run.sh b/tests/run.sh new file mode 100644 index 0000000..df3bd88 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,470 @@ +#!/hint/bash + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +## Running test cases + +# Beginning/end of line escape sequences +BOL=$'\n' +EOL=$'\n' + +# Update $EOL for the terminal size +update_eol() { + # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead + local cols="${COLUMNS-}" + if [ -z "$cols" ]; then + cols=$(tput cols 2>/dev/tty) + fi + + # Put the cursor at the last column, then write a space so the next + # character will wrap + EOL=$'\e['"${cols}G " +} + +# ERR trap for tests +debug_err() { + local ret=$? line func file + 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 "${BOL}${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST" + fi + ;; + $EX_SKIP) + if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then + color printf "${BOL}${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST" + fi + ;; + *) + if ((!VERBOSE_ERRORS)); then + cat "$TMP/$TEST.err" >&2 + fi + color printf "${BOL}${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 + wait -n -ppid + ret=$? + if [ -z "${pid:-}" ]; then + debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR + exit 1 + fi + + 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" + while read -r job status ret foo; 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]} +} + +# Run all the tests +run_tests() { + if ((VERBOSE_TESTS)); then + BOL='' + elif ((COLOR_STDOUT)); then + # Carriage return + clear line + BOL=$'\r\e[K' + + # Workaround for bash 4: checkwinsize is off by default. We can turn it + # on, but we also have to explicitly trigger a foreground job to finish + # so that it will update the window size before we use $COLUMNS + shopt -s checkwinsize + (:) + + update_eol + trap update_eol WINCH + fi + + passed=0 + failed=0 + skipped=0 + ran=0 + total=${#TEST_CASES[@]} + + if ((COLOR_STDOUT || VERBOSE_TESTS)); then + TEST_FMT="${BOL}${YLW}[%3d%%]${RST} ${BLD}%s${RST}${EOL}" + else + TEST_FMT="." + fi + + if ((${#MAKE[@]})); then + comake + fi + + # Turn off set -e (but turn it back on in run_test) + set +e + + for TEST in "${TEST_CASES[@]}"; do + wait_ready + if ((STOP && failed > 0)); then + break + fi + + percent=$((100 * ran / total)) + color printf "$TEST_FMT" $percent "$TEST" + + mkdir -p "$TMP/$TEST" + OUT="$TMP/$TEST.out" + + bg_test "$TESTS/$TEST.sh" + ((++ran)) + done + + while ((BG > 0)); do + wait_test + done + + printf "${BOL}" + + 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 + printf "${BOL}" + 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() { + skip + 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 +} + +if command -v unbuffer &>/dev/null; then + UNBUFFER=unbuffer +elif command -v expect_unbuffer &>/dev/null; then + UNBUFFER=expect_unbuffer +fi + +# Run bfs with a pseudo-terminal attached +bfs_pty() { + test -n "${UNBUFFER:-}" || skip + + bfs_verbose "$@" + + local ret=0 + "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$? + + 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 +} + +## Snapshot testing + +# Return value when a difference is detected +EX_DIFF=20 + +# Detect colored diff support +if diff --color /dev/null /dev/null &>/dev/null; then + DIFF="diff --color" +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 fd + if ! fd=$("${BFS[@]}" "$@"); then + skip + fi + + local ret=0 + eval "$fd" >"$OUT" || ret=$? + sed -i 's|/$||' "$OUT" + sort_output + diff_output || exit $EX_DIFF + + return $ret +} diff --git a/tests/stddirs.sh b/tests/stddirs.sh new file mode 100644 index 0000000..e08e6bf --- /dev/null +++ b/tests/stddirs.sh @@ -0,0 +1,152 @@ +#!/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/0" + "$XTOUCH" -p -M444 "$1/r" + "$XTOUCH" -p -M222 "$1/w" + "$XTOUCH" -p -M644 "$1/rw" + "$XTOUCH" -p -M555 "$1/rx" + "$XTOUCH" -p -M311 "$1/wx" + "$XTOUCH" -p -M755 "$1/rwx" +} + +# 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 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" +} + +# 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 + 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_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 + + rm -rf "$TMP" +} diff --git a/tests/tests.h b/tests/tests.h new file mode 100644 index 0000000..9078938 --- /dev/null +++ b/tests/tests.h @@ -0,0 +1,73 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Unit tests. + */ + +#ifndef BFS_TESTS_H +#define BFS_TESTS_H + +#include "prelude.h" +#include "diag.h" + +/** Unit test function type. */ +typedef bool test_fn(void); + +/** Memory allocation tests. */ +bool check_alloc(void); + +/** Standard library wrapper tests. */ +bool check_bfstd(void); + +/** Bit manipulation tests. */ +bool check_bit(void); + +/** I/O queue tests. */ +bool check_ioq(void); + +/** Trie tests. */ +bool check_trie(void); + +/** Process spawning tests. */ +bool check_xspawn(void); + +/** Time tests. */ +bool check_xtime(void); + +/** Don't ignore the bfs_check() return value. */ +attr(nodiscard) +static inline bool bfs_check(bool ret) { + return ret; +} + +/** + * Check a condition, logging a message on failure but continuing. + */ +#define bfs_check(...) \ + bfs_check(bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", "")) + +#define bfs_check_(str, cond, format, ...) \ + ((cond) ? true : (bfs_diag( \ + sizeof(format) > 1 \ + ? "%.0s" format "%s%s" \ + : "Check failed: `%s`%s", \ + str, __VA_ARGS__), false)) + +/** Get a string description of the last error. */ +const char *bfs_errstr(void); + +/** + * Check a condition, logging the current error string on failure. + */ +#define bfs_pcheck(...) \ + bfs_pcheck_(#__VA_ARGS__, __VA_ARGS__, "", "") + +#define bfs_pcheck_(str, cond, format, ...) \ + ((cond) ? true : (bfs_diag( \ + sizeof(format) > 1 \ + ? "%.0s" format "%s%s: %s" \ + : "Check failed: `%s`%s: %s", \ + str, __VA_ARGS__, bfs_errstr()), false)) + +#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 762687e..3890243 100755 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -1,711 +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 (e.g. ${BLD}posix/basic${RST}) -EOF -} - -DEFAULT=yes -POSIX= -COMMON= -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 - COMMON=yes - BSD=yes - ;; - --gnu) - DEFAULT= - POSIX=yes - COMMON=yes - GNU=yes - ;; - --all) - DEFAULT= - POSIX=yes - COMMON=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 - ;; - */*) - EXPLICIT=yes - SUDO=yes - enabled_tests+=("$arg") - ;; - *) - printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2 - usage >&2 - exit 1 - ;; - esac -done - -if [ "$DEFAULT" ]; then - POSIX=yes - COMMON=yes - BSD=yes - GNU=yes - ALL=yes -fi - -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 -MKSOCK="$BIN/tests/mksock" -XTOUCH="$BIN/tests/xtouch" - -# Try to resolve the path to $BFS before we cd, while also supporting -# --bfs="./bin/bfs -S ids" -read -a BFS <<<"${BFS:-$BIN/find2fd}" -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" - -cd "$TESTS" - -if [ ! "$EXPLICIT" ]; then - [ "$POSIX" ] && enabled_tests+=(posix/*.sh) - [ "$COMMON" ] && enabled_tests+=(common/*.sh) - [ "$BSD" ] && enabled_tests+=(bsd/*.sh) - [ "$GNU" ] && enabled_tests+=(gnu/*.sh) - [ "$ALL" ] && enabled_tests+=(bfs/*.sh) - - enabled_tests=("${enabled_tests[@]%.sh}") -fi - -function clean_scratch() { - if [ -e "$TMP/scratch" ]; then - # Try to unmount anything left behind - if [ "$SUDO" ] && command -v mountpoint &>/dev/null; then - for path in "$TMP"/scratch/*; do - if mountpoint -q "$path"; then - sudo umount "$path" - fi - done - fi - - # Reset any modified permissions - chmod -R +rX "$TMP/scratch" - - rm -rf "$TMP/scratch" - fi - - mkdir "$TMP/scratch" -} - -# 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/ - clean_scratch - - rm -rf "$TMP" -} - -if [ "$CLEAN" ]; then - trap cleanup EXIT -else - echo "Test files saved to $TMP" -fi - -# Creates a simple file+directory structure for tests -function 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" -} -make_basic "$TMP/basic" - -# Creates a file+directory structure with various permissions for tests -function make_perms() { - "$XTOUCH" -p -M000 "$1/0" - "$XTOUCH" -p -M444 "$1/r" - "$XTOUCH" -p -M222 "$1/w" - "$XTOUCH" -p -M644 "$1/rw" - "$XTOUCH" -p -M555 "$1/rx" - "$XTOUCH" -p -M311 "$1/wx" - "$XTOUCH" -p -M755 "$1/rwx" -} -make_perms "$TMP/perms" - -# Creates a file+directory structure with various symbolic and hard links -function 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" -} -make_links "$TMP/links" - -# Creates a file+directory structure with symbolic link loops -function 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" -} -make_loops "$TMP/loops" - -# Creates a file+directory structure with varying timestamps -function 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" -} -make_times "$TMP/times" - -# Creates a file+directory structure with various weird file/directory names -function 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" -} -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 - - "$XTOUCH" "$name" - ) - done -} -make_deep "$TMP/deep" - -# Creates a directory structure with many different types, and therefore colors -function make_rainbow() { - "$XTOUCH" -p "$1/file.txt" - "$XTOUCH" -p "$1/file.dat" - "$XTOUCH" -p "$1/star".{gz,tar,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 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* - "$XTOUCH" -p "$1"/exec.sh - chmod +x "$1"/exec.sh -} -make_rainbow "$TMP/rainbow" - -# 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() { - skip - 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 "$@" - - FD=$("${BFS[@]}" "$@") - if [ "$?" -ne 0 ]; then - skip - fi - - eval "$FD" | sed 's|/$||' | sort >"$OUT" - local STATUS="${PIPESTATUS[0]}" - if [ "$STATUS" -ne 0 ]; then - skip - fi - - 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 }' -} - -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 make_xattrs() { - clean_scratch - - "$XTOUCH" 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 -} - -cd "$TMP" -set +e - -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" - mkdir -p "${OUT%/*}" - - if [ "$VERBOSE_ERRORS" ]; then - (. "$TESTS/$TEST.sh") - else - (. "$TESTS/$TEST.sh") 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 6bc7549..4667322 100644 --- a/tests/trie.c +++ b/tests/trie.c @@ -1,23 +1,10 @@ -/**************************************************************************** - * 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 "prelude.h" +#include "tests.h" +#include "trie.h" +#include "diag.h" #include <stdlib.h> #include <string.h> @@ -50,14 +37,16 @@ const char *keys[] = { ">>>", }; -const size_t nkeys = sizeof(keys) / sizeof(keys[0]); +const size_t nkeys = countof(keys); + +bool check_trie(void) { + bool ret = true; -int main(void) { struct trie trie; trie_init(&trie); for (size_t i = 0; i < nkeys; ++i) { - assert(!trie_find_str(&trie, keys[i])); + ret &= bfs_check(!trie_find_str(&trie, keys[i])); const char *prefix = NULL; for (size_t j = 0; j < i; ++j) { @@ -70,38 +59,38 @@ 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); + ret &= bfs_check(strcmp(prefix, leaf->key) == 0); } else { - assert(!leaf); + ret &= 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); + ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); + ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); } { size_t i = 0; - TRIE_FOR_EACH(&trie, leaf) { - assert(leaf == trie_find_str(&trie, keys[i])); - assert(!leaf->prev || leaf->prev->next == leaf); - assert(!leaf->next || leaf->next->prev == leaf); + for_trie (leaf, &trie) { + ret &= bfs_check(leaf == trie_find_str(&trie, keys[i])); + ret &= bfs_check(!leaf->prev || leaf->prev->next == leaf); + ret &= bfs_check(!leaf->next || leaf->next->prev == leaf); ++i; } - assert(i == nkeys); + ret &= 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); + ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); + ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); trie_remove(&trie, leaf); leaf = trie_find_str(&trie, keys[i]); - assert(!leaf); + ret &= bfs_check(!leaf); const char *postfix = NULL; for (size_t j = i + 1; j < nkeys; ++j) { @@ -114,35 +103,35 @@ int main(void) { leaf = trie_find_postfix(&trie, keys[i]); if (postfix) { - assert(leaf); - assert(strcmp(postfix, leaf->key) == 0); + bfs_verify(leaf); + ret &= bfs_check(strcmp(postfix, leaf->key) == 0); } else { - assert(!leaf); + ret &= bfs_check(!leaf); } } - TRIE_FOR_EACH(&trie, leaf) { - assert(false); + for_trie (leaf, &trie) { + ret &= 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)); + ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); + ret &= 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); + ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); + ret &= 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); + ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); + ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); free(longstr); trie_destroy(&trie); - return EXIT_SUCCESS; + return ret; } diff --git a/tests/util.sh b/tests/util.sh new file mode 100644 index 0000000..3969db5 --- /dev/null +++ b/tests/util.sh @@ -0,0 +1,182 @@ +#!/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" +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 +} + +# Drop root priviliges 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 +} diff --git a/tests/xspawn.c b/tests/xspawn.c new file mode 100644 index 0000000..785ea48 --- /dev/null +++ b/tests/xspawn.c @@ -0,0 +1,197 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#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; +} + +/** Check that we resolve executables in $PATH correctly. */ +static bool check_use_path(bool use_posix) { + bool ret = true; + + struct bfs_spawn spawn; + ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0); + if (!ret) { + goto out; + } + + spawn.flags |= BFS_SPAWN_USE_PATH; + if (!use_posix) { + spawn.flags &= ~BFS_SPAWN_USE_POSIX; + } + + ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0); + ret &= bfs_pcheck(bfs_spawn_adddup2(&spawn, 10, 11) == 0); + ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 10) == 0); + ret &= bfs_pcheck(bfs_spawn_addfchdir(&spawn, 11) == 0); + ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 11) == 0); + if (!ret) { + goto destroy; + } + + // Check that $PATH is resolved in the parent's environment + char **envp; + ret &= bfs_pcheck(envp = envdup()); + if (!ret) { + goto destroy; + } + + // Check that $PATH is resolved after the file actions + char *old_path = getenv("PATH"); + dchar *new_path = NULL; + if (old_path) { + ret &= bfs_pcheck(old_path = strdup(old_path)); + if (!ret) { + goto env; + } + new_path = dstrprintf("tests:%s", old_path); + } else { + new_path = dstrdup("tests"); + } + ret &= bfs_check(new_path); + if (!ret) { + goto path; + } + + ret &= bfs_pcheck(setenv("PATH", new_path, true) == 0); + if (!ret) { + goto path; + } + + char *argv[] = {"xspawnee", old_path, NULL}; + pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp); + ret &= bfs_pcheck(pid >= 0, "bfs_spawn()"); + if (!ret) { + goto unset; + } + + int wstatus; + ret &= bfs_pcheck(xwaitpid(pid, &wstatus, 0) == pid) + && bfs_check(WIFEXITED(wstatus)); + if (ret) { + int wexit = WEXITSTATUS(wstatus); + ret &= bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); + } + +unset: + if (old_path) { + ret &= bfs_pcheck(setenv("PATH", old_path, true) == 0); + } else { + ret &= bfs_pcheck(unsetenv("PATH") == 0); + } +path: + dstrfree(new_path); + free(old_path); +env: + for (char **var = envp; *var; ++var) { + free(*var); + } + free(envp); +destroy: + ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0); +out: + return ret; +} + +/** Check path resolution of non-existent executables. */ +static bool check_enoent(bool use_posix) { + bool ret = true; + + struct bfs_spawn spawn; + ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0); + if (!ret) { + goto out; + } + + 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); + ret &= bfs_pcheck(pid < 0 && errno == ENOENT, "bfs_spawn()"); + + ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0); +out: + return ret; +} + +static bool check_resolve(void) { + bool ret = true; + char *exe; + + exe = bfs_spawn_resolve("sh"); + ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')"); + free(exe); + + exe = bfs_spawn_resolve("/bin/sh"); + ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0); + free(exe); + + exe = bfs_spawn_resolve("bin/tests/xspawnee"); + ret &= bfs_pcheck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); + free(exe); + + ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); + + return ret; +} + +bool check_xspawn(void) { + bool ret = true; + + ret &= check_use_path(true); + ret &= check_use_path(false); + + ret &= check_enoent(true); + ret &= check_enoent(false); + + ret &= check_resolve(); + + return ret; +} 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..d9d6c5c --- /dev/null +++ b/tests/xtime.c @@ -0,0 +1,195 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "xtime.h" +#include "bfstd.h" +#include "diag.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_pcheck(ret == -1 && errno == error, "xgetdate('%s')", str); + } else { + return bfs_pcheck(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 bool check_xgetdate(void) { + bool ret = true; + + ret &= check_one_xgetdate("", EINVAL, 0); + ret &= check_one_xgetdate("????", EINVAL, 0); + ret &= check_one_xgetdate("1991", EINVAL, 0); + ret &= check_one_xgetdate("1991-??", EINVAL, 0); + ret &= check_one_xgetdate("1991-12", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-??", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14", 0, 692668800); + ret &= check_one_xgetdate("1991-12-14-", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T??", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T10", 0, 692704800); + ret &= check_one_xgetdate("1991-12-14T10:??", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T10:11", 0, 692705460); + ret &= check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472); + ret &= check_one_xgetdate("1991-12-14T10Z", 0, 692704800); + ret &= check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460); + ret &= check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472); + ret &= check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0); + ret &= check_one_xgetdate("1991-12-14T03-07", 0, 692704800); + ret &= check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460); + ret &= check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472); + ret &= check_one_xgetdate("19911214 031112-0700", 0, 692705472);; + + return ret; +} + +#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_diag("localtime_r(%jd): %s", (intmax_t)expected, xstrerror(errno)); + return false; + } + + time_t actual; + return bfs_pcheck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm)) + && bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected); +} + +/** xmktime() tests. */ +static bool check_xmktime(void) { + bool ret = true; + + for (time_t time = -10; time <= 10; ++time) { + ret &= 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); + + return ret; +} + +/** Check one xtimegm() result. */ +static bool 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 ret = true; + ret &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb); + ret &= bfs_check(ta == -1 || tm_equal(&tma, &tmb)); + + if (!ret) { + bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma)); + bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb)); + bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); + } + + return ret; +} + +#if !BFS_HAS_TIMEGM +/** Check an overflowing xtimegm() call. */ +static bool check_xtimegm_overflow(const struct tm *tm) { + struct tm copy = *tm; + time_t time = 123; + + bool ret = true; + ret &= bfs_check(xtimegm(©, &time) == -1 && errno == EOVERFLOW); + ret &= bfs_check(tm_equal(©, tm)); + ret &= bfs_check(time == 123); + + if (!ret) { + bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy)); + bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); + } + + return ret; +} +#endif + +/** xtimegm() tests. */ +static bool check_xtimegm(void) { + bool ret = true; + + struct tm tm = { + .tm_isdst = -1, + }; + + // 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) { + ret &= check_one_xtimegm(&tm); + } + +#if !BFS_HAS_TIMEGM + // Check integer overflow cases + ret &= check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX }); + ret &= check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX }); + ret &= check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX }); + ret &= check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX }); +#endif + + return ret; +} + +bool check_xtime(void) { + bool ret = true; + ret &= check_xgetdate(); + ret &= check_xmktime(); + ret &= check_xtimegm(); + return ret; +} 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 index 9a91ec7..cd41842 100644 --- a/tests/xtouch.c +++ b/tests/xtouch.c @@ -1,28 +1,17 @@ -/**************************************************************************** - * 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. * - ****************************************************************************/ - -#include "../src/bfstd.h" -#include "../src/xtime.h" +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "bfstd.h" +#include "sanity.h" +#include "xtime.h" #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> +#include <sys/types.h> #include <time.h> #include <unistd.h> @@ -49,85 +38,128 @@ struct args { mode_t pmode; }; -/** 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; +/** 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; } -/** Create any parent directories of the given path. */ -static int mkdirs(const char *path, mode_t mode) { - char *copy = strdup(path); - if (!copy) { +/** 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; } - int ret = -1; - char *cur = copy + strspn(copy, "/"); - while (true) { - cur += strcspn(cur, "/"); + // Optimistically try the whole path first + int dfd = open_dir(args, AT_FDCWD, dir); + if (dfd >= 0) { + goto done; + } - char *next = cur + strspn(cur, "/"); - if (!*next) { - ret = 0; - break; + if (errno == ENOENT) { + if (!(args->flags & CREATE_PARENTS)) { + goto err; } + } else if (!errno_is_like(ENAMETOOLONG)) { + goto err; + } - *cur = '\0'; - if (mkdir(copy, mode) != 0 && errno != EEXIST) { - break; + // 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); } - *cur = '/'; + if (dfd < 0) { + goto err; + } + + *next = c; cur = next; } - free(copy); - return ret; +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 ret = utimensat(AT_FDCWD, path, args->times, at_flags(args)); + int dfd = open_parent(args, &path); + if (dfd < 0 && dfd != AT_FDCWD) { + return -1; + } + + int ret = utimensat(dfd, path, args->times, at_flags(args)); if (ret == 0 || errno != ENOENT) { - return ret; + goto done; } if (args->flags & NO_CREATE) { - return 0; - } else if (args->flags & CREATE_PARENTS) { - if (mkdirs(path, args->pmode) != 0) { - return -1; - } + ret = 0; + goto done; } size_t len = strlen(path); if (len > 0 && path[len - 1] == '/') { - if (mkdir(path, args->dmode) != 0) { - return -1; + if (mkdirat(dfd, path, args->dmode) == 0) { + ret = utimensat(dfd, path, args->times, at_flags(args)); } - - return utimensat(AT_FDCWD, path, args->times, at_flags(args)); } else { - int fd = open(path, O_WRONLY | O_CREAT, args->fmode); - if (fd < 0) { - return -1; - } - - if (futimens(fd, args->times) != 0) { - int error = errno; - close(fd); - errno = error; - return -1; + 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); + } } + } - return close(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 = { @@ -187,6 +219,8 @@ int main(int argc, char *argv[]) { if (marg) { char *end; long mode = strtol(marg, &end, 8); + // https://github.com/llvm/llvm-project/issues/64946 + sanitize_init(&end); if (*marg && !*end && mode >= 0 && mode < 01000) { args.fmode = args.dmode = mode; } else { @@ -200,14 +234,14 @@ int main(int argc, char *argv[]) { if (rarg) { struct stat buf; if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) { - fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, strerror(errno)); + fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno)); return EXIT_FAILURE; } - times[0] = buf.st_atim; - times[1] = buf.st_mtim; + times[0] = ST_ATIM(buf); + times[1] = ST_MTIM(buf); } else if (darg) { if (xgetdate(darg, ×[0]) != 0) { - fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, strerror(errno)); + fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno)); return EXIT_FAILURE; } times[1] = times[0]; @@ -240,7 +274,7 @@ int main(int argc, char *argv[]) { for (; optind < argc; ++optind) { const char *path = argv[optind]; if (xtouch(&args, path) != 0) { - fprintf(stderr, "%s: '%s': %s\n", cmd, path, strerror(errno)); + fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno)); ret = EXIT_FAILURE; } } |